美文网首页面试题
[视频笔记] -剖析framework面试,冲击Android高

[视频笔记] -剖析framework面试,冲击Android高

作者: New_X | 来源:发表于2020-01-14 18:09 被阅读0次

1.1 谈谈对zygote的理解?

zygote的作用是什么?

  1. 启动SystemServer:需要zygote里准备好的系统资源(常用类、JNI函数、主题资源、共享库),直接从zygote继承对性能有很大的提升
  2. 孵化应用进程

zygote的启动流程? --> 或者说工作原理

  1. 通过init进程(linux第一个进程)启动之后,通过init.rc加载启动配置文件,看看哪些系统服务要启动,如,如zygote, service manager。
  2. 有两种启动方式,注意fork要单线程
  • fork + handle
  • fork + execve
    需要注意的是,pid为0是子进程;默认情况,创建子进程是继承了父进程的系统资源,但是调用了execve系统调用去加载另一个二进制程序的话,继承的父进程的系统资源就会被替换成加载的二进制程序
  1. 启动分为两个部分,native世界和java世界
  • zygote的native世界:启动虚拟机 -> 注册Android的jni函数 -> 进入java世界
  • zygote的java世界:preload resources(预加载资源) -> fork System Server到单独的线程 -> loop等待消息
  1. loop等待socket消息,收到以后执行runOnce()方法,fork新进程,并在新进程中调用ActivityThread.main()方法

拓展问题:

  1. 孵化应用进程为什么不交给SystemServer来做,而专门设计一个zygote?
    出于必要共享资源的可以通过init来初始化,共享给子线程,加快启动速度,但是SystemServer内部系统服务太多太杂,不适合被继承。
  2. zygote的ipc机制为什么不采用binder?如果采用binder会有什么问题?
  • 出于避免死锁、状态不一致等其他多线程问题,所以fork流程不允许存在多线程。但是Binder通信偏偏是多线程(代理对象对binder的调用是在binder线程,需要再通过Hander调用主线程来操作)
  • zygote和systemserver本就是父子关系,对于简单的消息通信,用管道或者socket非常方便(binder机制可以从启动来看是比较繁琐的)

1.2 说说Android系统的启动?

考察点:主要是zygote和SystemServer如何启动,再外加一些细节。

  1. zygote的启动流程
  2. 通过Zygote.forkSystemServer创建进程,然后启用binder机制,并调用SystemServer的java类的main函数 --> SystemServer主要用于启动系统中的服务(分批、分阶段启动)
  3. 然后是桌面的启动,SystemServer启动好了以后会去调用systemReady,再调用startHomeActivityLocked来启动LoaderTask,去向PMS查询所有已经安装的应用,以图标的方式显示到桌面上

细节上:
1.SystemServer的java类的启动方式和应用启动代码很像,

Looper.preMainLooper();
1. 加载共用库
2. 创建SystemContext
3. 分批启动Service(Bootstrap->Core->Other)  
Looper.loop();
  1. 系统服务跑在什么线程?
    几乎没有在主线程上的,大部分随着创建在binder线程,部分例如DisplayThread、FgThread(前台线程)、IoThread、UiThread跑在工作线程
    拓展:
  2. 为什么系统服务不都跑在binder线程里?
    binder线程是共享的,如果binder线程忙碌,会影响系统服务响应的实时性
  3. 为什么系统服务不都跑在自己私有的工作线程里?
    如果每个服务都启动工作线程,会导致应用进程负载过重,且线程切换太多性能很差
  4. 跑在binder线程和跑在工作线程,如何取舍?
    如果对实时性要求不高且不耗时的任务可以放在binder线程(启动工作线程可以避免同步问题)
  5. 怎么解决系统服务启动的相互依赖?
  • 分批启动:AMS、PMS、PKMS(上层的晚点启动)
  • 分阶段启动:一个服务不一定完全启动,他依赖的其他服务启动好了通知该服务去做这个阶段的初始化 --> 这个6<用的多也让人眼花缭乱>

1.3 你知道怎么添加一个系统服务吗?

  • 添加系统服务的时机:如果想跑到SystemServer里,可以利用SystemServer发布自己的服务;如果想跑在单独的进程,需要改init.rc的配置且要有main入口函数
  • 服务端要做的事:
  1. 启用binder机制和其他线程通信
  2. 初始化配置
  3. 把服务的binder注册到ServiceManager
  • 应用端:为了保证调用方式一致,需要为这个服务注册ServiceFactory
    ---> 实际操作,细节很多

1.4 系统服务和bind应用服务有什么区别?

考察点:启动方式、注册方式、使用方式

  1. 启动方式
  • 系统服务:在SystemServer进程启动的时候启动(分批、分阶段启动),大部分服务是跑在binder线程的 <startBootstarpService/startCoreService/startOtherService>
  • 应用服务:由应用发起,发到AMS创建ServiceRecord(AMS只负责Service的管理和调度,真正Service的启动和加载是在应用端(handleCreateService)
  1. 注册方式
  • 系统服务:把Binder实体对象注册到ServiceManager(有权限,只有系统服务才能注册进去)
  • 应用服务:
    1. 应用端向AMS发起bindService
    2. AMS会判断这个Service是否注册过了,注册过直接回调binder给应用,如果没有,AMS会向Service请求binder对象
    3. Service响应这个请求,并且把这个binder对象发布到AMS
    4. 然后AMS把这个binder对象回调给应用
      -->说明了注册过的直接返回回调binder,是被动注册,需要通过AMS请求一下
  1. 使用方式
  • 系统服务:找到Service对应的ServiceFetcher,然后通过ServiceFetcher来拿到服务的管理对象
    --> 通过ServiceManager的getService()获取对应binder对象,然后把这个binder对象封装一层服务的管理对象返回(封装为了方便上层调用)
  • 应用服务:通过bindService向AMS发送绑定服务端请求,AMS通过onServiceConnected()回调把服务的binder对象返回给业务端,然后把这个对象封装成业务接口对象给业务接口调用

1.5 ServiceManager启动和工作原理是怎样的?

ServiceManager的启动(SerivceManager比较特殊)

  • 启动进程:init进程拉起来的
  • 启动Binder机制
  • 发布自己的服务
  • 等待并相应请求
    --> 如何判断是读还是写,是看read_size和write_size是否大于0,都大于0 ,优先写 (应该是考虑过才这样设计的)

如何获取ServiceManager?
<以FlingerManager为例>

  1. defaultServiceManager(有可能是获取不到的,因为FlingerManager和ServiceManager都是由init进程拉起来的,有可能去获取的时候)
  2. 获取ServiceManager的Proxy,实际上是个BpBinder,是0号
    --> 简而言之:以0号注册个BpBinder

怎么添加Service?

  • 通过addService函数
  • 通过remote()拿到BpBinder对象,然后通过这个BpBinder调用transact()发送出去,(通过handle)和驱动交互
    --> 接收方通过svcmgr_handler(),switch处理code,调用do_add_service注册为一个handle,插到一个单链表里

怎么获取Service?

  • 通过getService函数
  • 接收方也是通过svcmgr_handler(),switch处理code,调用do_find_service找到对应binder对象的handle值 返回

2.1 你知道应用进程怎么启动的吗?

两种进程启动方式:都是fork,区别是一个创建的时候调用execve()

什么时候触发的进程启动?
被动启动:启动组件的时候都会去判断ProcessRecord和其中的Thread是否不为null,不为null的时候,调用startProcessLocked()来启动
---> 为什么Thread也需要不为null?
这个Thread实际上是应用的Binder句柄,目的是为了
1. 告诉AMS这个Application已经注册了;
2. 把句柄注册到AMS里,方便AMS可以在需要的时候给Application发起binder调用,实现双向调用

进程怎么启动?

  1. 由AMS通过socket向zygote发起 --> socket是为了避免启动多线程
  2. zygote fork出应用进程,即通过startProcessLocked方法,打开本地socket,发送参数列表,返回创建的进程id,执行ActivityThread的main函数
  3. 进程启动之后向AMS报告,整个启动才算结束(AMS确定进程启动后才能去启动各个组件)

2.2 应用是怎么启用Binder机制的?

什么时候支持binder机制的?
通过系统启动可知,binder机制是zygote拉起SystemServer服务的时候开始支持的

怎么启动binder机制?
1.打开binder驱动
2.映射内存,分配缓冲区
3.注册binder线程
4.进入binder loop

拓展:应用天生就支持binder机制,是不是从zygote继承过来的?为什么不能从zygote继承?
不是,因为zygote就不支持binder机制,是单线程的。

2.3 谈谈你对Application的理解?

  1. Application的作用
  • 保存应用进程内的全局变量(当然单例更适合)
  • 初始化操作(合适)
  • 提供应用上下文(横跨生命周期的)
    特点:1.生的早;2.活得久
  1. 需要注意的是Application是跟着进程走的而不是根据应用走的,所以多进程Application初始化多次
  2. Application继承自ContextWrapper,实际处理交给了ContextWrapper的mBase,Application是个空壳
  3. 生命周期:
    1. 构造函数
    2. attachBaseContext()
    3. onCreate()
      ---> 所以需要注意Application的构造函数里context是未初始化的
  4. 不要在Application生命周期进行耗时操作:会阻塞UI线程,耽误应用的组件的启动 --> 可以引申出启动优化
  5. Application中使用使用静态变量的问题:切到后台,在系统内存不足应用被杀掉后,重建application,恢复Activity,这个静态变量没有初始化了,可能会产生bug
  6. <深入理解Application的初始化原理>
    ---> 细节应该是Application避免重复创建检查,启动好后通知AMS
    (实际创建的是ContextImpl)

2.4 谈谈你对Context的理解?

考察点:

  • 了解Context的作用
  • 熟悉Context的初始化流程
  • 深入理解不同应用组件之间Context的区别

应用里面有多少个Context?不同的Context之间有什么区别?
application+activity+service
区别:Activity的Context是继承自ContextThemeWrapper(UI组件)

Context是如何初始化的?

  • Application:跟随应用进程的初始化创建
  1. 继承关系:Application <-- ContextWrapper <-- Context
  2. 调用顺序:<init> --> attachBaseContext --> onCreate
  3. ContextWrapper里包含一个Context,调用都委托给他了(反射替换Context:插件化有用到)
    <packageInfo.makeApplication方法不要看调用多次,其实是只创建了一次,后面是直接返回>
  • Activity:跟着Activity启动初始化的(performLaunchActivity),和Application非常类似

  • Serveice的Context相关内容和Application很像

  • 广播没有继承自ContextWrapper
    广播的Context哪里来的?静态(注册时注入),动态注册(以Application为mBase的ContextWrapper)

  • ContentProvider的onCreate在Application之前调用,Context也和广播一样是注册时注入的

3.1 说说Activity的启动流程?

考察点:

  • 启动Activity会经历哪些生命周期回调
  • 冷启动大致流程,涉及哪些组件,通信过程是怎么样的?
  • Activity启动过程中,生命周期回调的原理?

Activity启动流程

  1. 向AMS发送startActivity请求
  2. 如果应用没启动,通过Socket向zygote发送启动进程请求
  3. zygote收到以后,会去启动应用进程
  4. 应用进程启动之后就会向AMS发起attachApplication的IPC调用,目的是注册ApplicationThread
  5. 接下来AMS会向应用发起bindApplication的IPC调用,目的是初始化应用Application
  6. 完了之后,AMS又向应用发起了scheduleLaunchActivity的IPC调用,目的是给应用执行和加载Activity,并且执行Activity的生命周期

3.2 说说Activity的显示原理?

考察点:

  • setContentView原理是什么?
  • Activity为什么要在onResume之后显示?
  • ViewRoot是干嘛的?是View Tree的rootView吗?

回答要点:

  • PhoneWindow是什么,怎么创建的,什么时候创建的?
    Window在Activity的attach()初始化,初始化的对象就是PhoneWindow
    ===> 回顾下Activity的初始化过程:

    1. 创建Activity对象
    2. 创建Context对象
    3. 创建Application对象
    4. attach上下文 --> 由此可见onCreate之前创建了PhoneWindow
    5. Activty onCreate
      PhoneWindw看名字就知道是手机的Window,用来管理手机的窗口。
      --> 往下切入,实质调用的就算PhoneWindow的setContentView
  • setContentView原理,DecorView是什么?
    调用PhoneWindow的setContentView(),调用到installDecor()来创建DecorView<是个FrameLayout>
    所以,原理是创建DecorView,初始化页面布局,把自己的布局加到content里,创建出一个ViewTree(还没显示)
    --> 真实显示是handleResumeActivity(),activity的makeVisible()设置可见
    ----> 通过ViewRootImpl的调用到了两个非常重要的函数:

    1. requestLayout() ===> 显示原理的重点
      调用mChoregrapher.postCallBack()传入一个Runnable,在下一个vsync信号来的时候触发doTraversal()->performTraversals() --> 调用了4个非常重要的函数:
    2. relayoutWindow(params) --> 申请Surface
      <剩下的是Measure、Layout、Draw>
    3. mWindowSession.addToDisplay(mWindow,...):应用可以和WMS进行双向调用
  • ViewRoot是什么?有什么作用?
    --> ViewRootImpl的requestLayout()流程

  • View的显示原理是什么?WMS发挥了什么作用?
    ---> (上面一气呵成讲了)
    DecorView里的对象ViewRootImpl是关键,在首次初始化的时候注册到WMS,可以与WMS双向通信,并且WMS给ViewRootImpl分配了Surface用来显示

3.3 应用的UI线程是怎么启动的?

考察点:

  • 什么是UI线程?

UI线程==主线程吗?

  1. 对于Activity来说,UI线程就是主线程
  2. 对View来说,他的UI线程就是ViewRootImpl创建的时候所在的线程

那ViewRootImpl创建是什么时候创建的呢?
分析checkTread
---> Activity的DecorView对应的ViewRootImpl是在主线程创建的(handleResumeActivity)
---> 所以是因为恰好在主线程创建了View所以是UI线程,但是如果在子线程创建,那就必须在子线程中刷新了<单线程模型> --> 遇到过WebView的post()调用loadUrl()不在同个线程会报错的问题
<ViewRootImpl是负责事件分发,UI绘制,和WMS通信>

了解Android的UI显示原理,UI线程和UI之间是怎么关联的?
ViewRootImpl的原理

4.1 说说Service的启动原理?

考察点:

  • service启动有哪几种方式?
  • service启动过程中主要流程有哪些?
  • service启动流程涉及哪些参与者,通信过程是怎样的?

启动流程:

  1. 进程向AMS发起startService调用
  2. AMS如果发现进程没启动,通过Socket向zygote进程发起通信,请求启动应用进程
  3. 应用启动起来以后,通过ActivityThread的main函数向AMS发起attachApplication的binder调用
  4. 然后AMS就知道这个应用进程已经就绪了,就向应用进程发起bindApplication的调用,让应用准备去创建它的Application
  5. 之后,AMS开始处理这个应用进程里面Pending相关的组件,比如Service相关的(先调用到onCreate<scheduleCeateService>,再调用到onStartCommand<scheduleServiceArgs>,都是在主线程进行的 --> 通过H) --> 顺序是在realStartServiceLocked里定义了/Service的创建流程和Activity区别不大,把Service存在map里(token为key)

bindService和startService区别:
bindService不会触发onStartServiceCommand(),因为binderService没有把ServiceRecord加到pendingStarts队列里 (pendingStarts是需要启动的Service列表) --> startServiceLocked函数里才会加到pendingStarts队列里

4.2 说说Service的绑定原理

考察点:

  • 知道bindService的用法
  • 了解bindService的大致流程
  • bindService涉及到哪些参与者,通信过程是怎样的?

具体流程:
  1. 应用向AMS发起bindService
    <bindService的时候,如果Service没有启动,则先走启动流程,启动了则直接走bindService流程 --> 多次bindService是什么样子有画面了吧>
    --> 防止重复调用到onServiceConnected()
    --> onServiceDisconnected()一般是不会触发的,手动解绑也不会,只有这个Service被系统回收或者所在进程挂了才会调用到<为什么这样设计>
  2. AMS检查是否有Service的binder句柄,如果有直接回调binder句柄给应用
  3. 如果没有,AMS就会向Service请求binder句柄,请求到以后把binder句柄发布到AMS,然后AMS把这个句柄回调给应用
  4. 应用拿到这个binder句柄之后,就可以向Service发起binder调用
    ---> 是不是有种恍然大悟的感觉

注意:

  1. ServiceConnection是普通的接口,不能跨进程传到AMS里,所以要生成了个带ServiceConnection引用的binder对象<IServiceConnection>(可以调用ServiceConnection的函数)
  2. ServiceConnection和IServiceConnection不是1对1对关系,ServiceConnection和Context形成的二元组才和IServiceConnection 1对1

涉及的数据结构包含关系(前对应后都是1对多关系):
ServiceRecord
-> IntentBindRecord(通过不同的Intent绑定到同一个Service)
-> AppBindRecord(这些Intent可以来自同一个或者不同应用进程)
-> ConnectionRecord(一个进程可能对应多个ConnectionRecord)

onRebind什么时候调用
这个Service还在,但没有哪个应用进程用这个intent绑定的Service的时候,Service就可以执行onUnRebind回调,其返回值决定是否向AMS报告

4.3 说说动态广播的注册和收发原理

考察点:

  • 动态广播的注册原理
  • 广播的发送原理
  • 广播的接收原理

细节:(大部分Binder对象都是以IXXX命名的,四大组件都是以ArrayMap存储的)
和Service一样,Context+BroadcastReceiver组成一个二元组,与IIntentReceiver一一对应

注册和分发流程:

  1. 应用A向AMS注册广播,会生成binder对象,并且和IntentFilter注册到AMS
  2. 应用B发送带intent的广播
  3. AMS在所有注册的Receiver里面,根据这个Intent找到匹配的Receiver,然后开始分发
  4. 普通的动态广播:系统端并行分发,到了应用端变成串行分发
  5. 通过binder对象找到对应的BroadcastReceiver执行他的onReceive函数

动态广播分发原理

4.4 说说静态广播的注册和收发原理

考察点:

  • 广播的注册原理
  • 广播的发送原理
  • 广播的接收原理

静态广播注册原理
AndroidManifest.xml里注册,在启动的时候,通过PackageManagerService来解析xml,生成Activity对象(不是应用端的Activity,是Component<表示是应用组件>),添加到列表里,完成静态广播的注册。

静态广播的分发逻辑

  1. 先把并行分发的广播分发完,再来分发串行广播
  2. 如果有pending广播,就先直接返回,这个广播等待应用进程启动
  3. 如果当前广播分发超时了,就废弃这个广播,处理下一个广播(超时60秒,给很久了;进程崩溃也会导致超时)
  4. 如果没有超时,并且正在分发中,就先返回,什么也不做
  5. 如果当前广播已经分发完一个receiver了,就继续分发下一个receiver
  6. 如果这个receiver是动态注册的receiver,就直接分发
  7. 如果这个receiver是静态注册的receiver,就先看进程启动没有
  8. 如果进程启动了,就直接分发
  9. 没启动的话就先启动进程,然后把广播标记为pending
  10. 进程启动后attachApplication时继续处理这个pending的广播
    ----> 1.需要处理串行;2.需要判断进程是否启动;3.处理超时逻辑
    <动态Receicer比较简 --> 动态注册的,说明进程肯定在>

细节点:

  1. 超时机制里面:进行了ANR的Dialog发起
  2. BroadCastQueue有两个:一个normal的,一个紧急的

4.5 说说Provider的启动原理

Provider流程

  1. 应用A向Provider发起CRUD的调用
  2. 因为不是一个进程,所以没有这个provider,那就需要向AMS请求这个provider(实质上是为了拿provider的binder对象)
  3. AMS如果发现这个provider的进程没启动,则通过socket通信向zygote发起请求创建进程
  4. zygote把provider所在的应用进程启动起来
  5. 该应用进程起来的第一件事,调用attachApplication向AMS报告
  6. AMS知道应用启动起来以后,向应用进程下的第一道命令就是bindApplication(3-6常规操作)
  7. 应用进程接收到bindApplication后,完成两个重要工作:创建Application & 初始化Provider(installProvider,创建Provider对象,调用生命周期,把provider的binder对象通过publishContentProvider发布到AMS)
  8. AMS里就有provider的binder对象了,可以把binder对象返回给应用A,应用A就可以发起增删改查的调用
    ---> 如果provider的进程已经启动了,那就只要向该应用进程请求一下即可


注意点:

  1. provider可能是有多个实例的(本进程和其他进程都有),好处是免去了IPC调用,性能更好
  2. provider能跑在当前进程的条件(即可以在应用内创建Provider的条件):
    1. uid相同
    2. 开启了multiprocess 或者 processName相同

5.1 说说屏幕刷新的机制

屏幕缓存不止一个
因为只有一个的话,一边读一边写,会导致显示很奇怪(显示了下一帧的图像),所以有两个缓存,一个读一个写,互不干扰,需要显示下一帧的时候交换一下两个缓存
---> 关于这点,我认为是现在技术不够,所以实现方式还是像电影一样一帧帧。

绘制流程 --> 从常见入口 requestLayout() 开始
requestLayout() --> scheduleTraverals() --> Choreographer(创建是利用了ThreadLocal,线程独有)
注意点:一次vsync周期,只会调度一次Choreographer的绘制

应用端是什么时候开始绘制的? --> 应用端绘制是独立的,vsync是刷新屏幕
卡在刷新的时候进行绘制,当前帧是不变化的,即使当前绘制操作优化的很好,频率过高也会给用户感觉丢帧(倒霉踩雷了)
--> 优化思路:在vsync信号来的时候才进行绘制,那绘制过程都优化在16ms内,应用就会非常流程
---> 实现:Choreographer(舞蹈指导),把绘制操作封装成一个runnable丢给Choreographer,下一个vsync信号来的时候,就开始处理消息,真正的开始重绘(相当于绘制节奏,交由Choreographer处理了)

Choreographer工作原理

  1. 应用层的View调用requestLayout方法,想要重绘
  2. 其实是new了一个runnable,丢到Choreographer的消息队列里
  3. Choreographer没有马上处理消息,调用requestNextVsync函数,向SurfaceFlinger请求下一个vsync信号
  4. SurfaceFlinger在下一个vsync信号来的时候,通过postSyncEvent函数通知Choreographer
  5. Choreographer收到通知以后,就会处理消息队列里的消息,之前的requestLayout对应的就算执行里面的performTraversal函数,真正执行绘制
    注意点:
  6. 利用 SyncBarrier:屏障,把这个屏障插到消息队列里,后面的普通消息是不能处理的。但对异步消息是没有影响的,为的就是让一些紧急处理的消息先执行。
  7. 处理绘制消息的对象:FrameDisplayEventReceiver(利用handler不是为了切线程,而是按时间戳发消息) --> doFrame()
  8. 交互是通过管道机制,BitTube,一对Socket描述符(mSendFd和mReceiverFd),读信号和写信号能相互唤醒 --> 这个套路很常见,要么openSession要么建立通道


补充问题

  1. 丢帧一般是什么原因引起的?
    主线程有耗时操作,耽误了View的绘制

  2. Android刷新频率60帧/秒,每隔16ms调onDraw绘制一次?
    刷新频率是vsync的频率,并不是每调一次绘制一次。
    需要应用端主动发起重绘,才会向SurfaceFlinger请求接收vsync信号,在下次来vsync信号的时候才会真正的去绘制

  3. onDraw完之后屏幕会马上刷新吗?
    不会立即刷新,需要等到下次vsync信号

  4. 如果界面没有重绘,还会每隔16ms刷新屏幕么?
    没有重绘,屏幕还是会刷新,只是画面数据一直用的是旧的,看起来没有变化

  5. 如果在屏幕快要刷新的时候才去onDraw绘制会丢帧吗?
    不会,View的重绘不会马上执行,需要等待下一次vsync信号

5.2 surface跨进程原理?

看几个问题

  1. 怎么理解surface,它是一块buffer吗?
    不是buffer,是个壳子,里面包含一个能生产buffer对象的GraphicBufferProducer(GBP)
  2. 如果是,surface跨进程传递怎么带上这个buffer?
  3. 如果不是,那surface根buffer又是什么关系?
    传递GraphicBufferProducer对象
  4. sueface到底是怎么跨进程传递的?
    系统创建的是SurfaceControl对象,里面有GBP,可以创建Surface,跨进程返回应用

**Activity的Surface是怎么跨进程传递的? **
Activity第一次绘制就会去申请Surface(relayoutWindow的时候申请),以mWindowSession为通道,向WMS传了一个空壳的Surface,WMS从native层获取SurfaceControl,里面有个GraphicBufferProducer,可从里面拷贝buffer到Surface

注意:

  • surface的本质是GraphicBufferProducer,而不是buffer
  • surface跨进程传递,本质上就是GraphicBufferProducer的传递 --> 实际上是个binder对象,跨进程传递非常快,如果传buffer就很累赘了。

5.3 surface绘制原理?

---> 其实是GraphicBufferProducer 的工作原理

  1. surface绘制的buffer是怎么来的?
    通过GBP向BufferQueue申请

  2. buffer绘制完了又是怎么提交的?
    通过GBP向BufferQueue提交
    ---> 对Surface来说,GBP是他的灵魂

surface绘制流程

  1. 应用要绘制图像,首先要创建Surface
  2. 要在Surface上绘制,需要buffer
  3. 要获取buffer,得在SurfaceFlinger里创建一个BufferQueue --> 一个Surface对应一个BufferQueue
  4. 这个BufferQueue有两端(producer和consumer),producer端需要跨进程传给应用,交由Surface保管
  5. Surface需要绘制的时候,通过producer端向BufferQueue发起binder调用,申请一块buffer
  6. 这个buffer作为Canvas的Bitmap缓冲区(和我们用的Bitmap不同,是底层Skia绘制用的),绘制操作完成把这个buffer返回给BufferQueue
  7. BufferQueue通知consumer端,回调它的onFrameAvaliable,表示又有一帧数据绘制完毕了
    --> consumer可以在SurfaceFlinger进程里,也可以传给其他应用进程,就是用来消耗这一帧数据的

5.4 你对vsync机制有了解吗?

---> 其实是讲清楚vsync在SurfaceFlinger端是怎么生成和分发的?

vsync信号的生成
硬件生成(HWComposer)/软件生成(VSyncThread),但是都是通过统一的接口回调到上层

vsync分发流程:

  1. 工作线程DispSyncThread收到vsync信号,把它分发一分为二,分成两路进行分发(app-EventThread/sf—EventThread)
    ---> 一分为二的原因:vsync发生的时候需要通知app去绘制UI和通知sf去对应用生成对图像进行合成渲染。如果都挤在vsync信号来的时候,就会去抢占CPU资源(类似放假错峰的处理),两个信号分发时机有一定偏移

vsync分发流程

  1. vsync信号过来以后,首先唤醒EventThread线程
  2. EventThread线程唤醒之后,把vsync信号通过注册的Connection分发
  3. 每个Connection有两个描述符(mSenderFd,mReceiverFd),发送实际上就是往mSenderFd写数据,对应在SurfaceFlinger/App进程里的mReceiverFd就能收到(Looper消息队列里监听epoll_wait)

6.1 Android framework用到了哪些跨进程通信方式?

考察点:

  • 看你是否了解Linux常用的跨进程通信方式
  • 是否研究过Android Framework并了解一些实现原理
  • 是否了解Framework各组件之间的通信原理
  1. 管道
  • 半双工的,单向的(但实现上是给了收和发两个描述符)
  • 一般是在父子进程之间用的
    <针对的是无名管道>
    ---> 应用:老版本的Looper(高版本替换成了EventFd)
    <数据量不怎么大的时候很方便>
  1. Socket
  • 全双工的,即可读又可写
  • 两个进程之间无需存在亲缘关系
    ---> 应用:在zygote通过socket接受AMS请求,去启动应用进程
  1. 共享内存
  • 很快,不需要多次拷贝(其他的只是两次,读和写嘛) --> 拿到文件描述符,把他同时映射到内存空间,一个写,另一个就能读到
  • 进程之间无需存在亲缘关系
    ---> 应用:涉及到进程间大数据量传输的(图像相关),MemoryFile(利用mmap)
  1. 信号
  • 单向的,发出去之后怎么处理是别人的事
  • 只能带个信号,不能带别的参数
  • 知道进程pid就能发信号了,也可以一次给一群进程发信号
    --> 也需要权限,root权限或者和其他进程uid相同(进程启动后会马上重新设置uid,不能给其他进程发信号的)
    ---> 应用:关闭进程,子进程退出后要发信号通知zygote回收,还有native异常抛出
  1. binder

6.2 谈谈你对binder的理解?

考察点:

  1. binder是干嘛的?
    binder是Android最重要的IPC通信机制
  2. binder存在的意义是什么?
    <binder不是基于linux的,是跑在驱动层的,是内核态的>
  • 性能:只需要1次拷贝
  • 方便易用:逻辑简单直接,不容易出问题(共享内存性能好,但不方便)
  • 安全:比如Socket方式的IP地址是开放的,管道方式拿到匿名管道的管道名称就可以往里面写数据(binder声明信息只能在内核态添加)
  1. binder的架构原理是怎样的?

binder的通信架构


---> 只有系统服务才能注册到ServiceManager,需要权限验证
<Client是应用进程、Server是系统服务可能在system-server进程,也可能是单独的进程,ServiceManager是单独的系统进程,无论在哪个进程都需要先启动binder机制,这是binder通信的前提>

回顾:进程如何启动binder机制?

  1. 打开binder驱动 --> binder驱动给进程创立一份档案
  2. 将返回的描述符,进行内存映射,分配缓冲区(binder通信要用到缓冲区)
  3. 启动binder线程 --> 这个线程注册到binder驱动,这个进入Looper循环不断和binder驱动交互

binder通信的分层架构
---> 很像网络传输的分层
<角色角度、分层角度、Binder对象角度(代理端,实体端)>


IPC驱动层通过IPCThreadState调用transact和onTransact的时候,没有什么BpBinder、BBinder,交互的时候只关心handle<外观模式>

6.3 一次完整的ipc通信流程是怎么样的?

考察点:

  • 了解binder的整体架构原理
  • 了解应用和binder驱动的交互方式<Client端和Server端>
  • 了解IPC通信过程中的协议

IPC通信流程

  1. 首先Client端向Binder驱动写了一个BC_TRANSACTION的指令
  2. Binder驱动收到以后,给Client一个回执,就是BR_TRANSACTION_COMPLETE(Client端进入休眠等待回复)
  3. 回执发完之后,Binder驱动把这个指令提供BR_TRANSACTION转发给Server端
  4. Server端收到后,退出休眠,处理这个请求,处理完之后,提过BC_REPLY返回给Binder驱动
  5. Binder驱动收到Server端的回复之后,也会给Server端一个回执,就是BR_TRANSACTION_COMPLETE(Server端收到后进入休眠)
  6. 最后Binder驱动把这个返回结果通过BR_REPLY返回给Client端
    <Client端等待回复的时候处于休眠状态,Server端在处理请求外处于休眠状态>

6.4 binder对象跨进程传递原理是怎么样的?

考察点:

  • binder有哪些传递方式?
  • binder在传递过程种是怎么存储的?
  • binder对象序列化和反序列化过程?
  • binder对象传递过程中驱动层做了什么?

关键字:binder对象、跨进程

要点:分层回答

  • Java层
  1. 通过android.os.Parcel进行跨进程传输,通过writeStrongBinder写到Parcel里,目标进程通过readStrongBinder从Parcel中读出来
  • Native层
  1. binder以flat_binder_object形式存储在Parcel缓冲区里,Parcel有个数组专门保存其偏移,所以到目标进程,就可以根据这个偏移还原出flat_binder_object(所谓的一次拷贝)
  • 驱动层
  1. Parcel传到binder驱动里之后,这个驱动根据flat_binder_object里的binder对象创建一些数据结构,包括binder_node(实体对象)和传给其他进程的binder_ref(引用对象),一个binder实体对象可能会对应多个binder引用对象

然后收尾,
4.往上目标进程根据binder_ref的handle创建BpBinder(同一个进程直接返回)
5.由BpBinder再往上到BinderProxy到业务层的Proxy

6.5 说一说binder的oneway机制?

考察点:

  • binder的oneway是什么意思?
  • oneway有哪些特性?
  • 它的实现原理是怎样的?

回答要点:

  • oneway是异步binder调用
  • server端串形化处理
  • oneway的实现机制是怎么样的?

什么是oneway?
带oneway和不带oneway的接口(普通aidl接口)区别:是否需要等待回复
注意点:oneway的接口,函数默认是没有返回值的,带上编译会出错

---> 回顾IPC通信过程,oneway重点放在了Client端

publishBinder流程:

  1. 不管有几个binder线程,同一个oneway的实体对象一次只能处理一个请求,其余的需要排队 --> 应用端异步(应用端能很快返回),但是在Server端还是串形处理的
  2. 对于一个oneway接口,他的所有binder接口都会被串形化,哪怕不是同一个函数,不是同一个线程甚至不是一个进程
    --> Server端内,整个oneway的实体对象里的函数串形调用

oneway的通信协议时序图
--> 和非oneway比较(非oneway需要等待)


---> 感觉好不靠谱的样子...只能知道是不是发不出了
----> 在framework应用还很多:系统服务向应用端发起binder调用,基本都是oneway的 (懂了,免得应用端搞事,影响系统)
  1. oneway是异步的,就算应用端处理非常耗时也不会阻塞系统服务
  2. oneway是串形化的,系统服务是一个一个的把binder调用分发给应用端

7.1 线程的消息队列是怎么创建的?

先看几个问题:

  • 可以在子线程创建handler么?
    可以,先调用Looper.prepare()即可

  • 主线程的Looper和子线程的Looper有什么区别?
    主线程的不可以退出,子线程的可以(prepare方法决定)

  • Looper和MessageQueue有什么关系?
    <Handler:Looper:MessageQueue = N:1:1>
    一个线程有一个Looper,对应一个MessageQueue,一个looper可以对应多个mLooper副本,即多个Handler,每个Handler都可以往MessageQueue插入消息,MessageQueue可以根据target分发到不同的Handler

MessageQueue是怎么创建的?

  1. Java层的MessageQueue在创建的时候就会调用一个native函数去创建Native层的MessageQueue
  2. Native层的MessageQueue里面会去创建一个Native层的Looper,放在局部缓存里
  3. Looper里面会创建一个eventFd,并且添加一个可读事件到epoll里面

Native层的Looper低版本是基于管道实现,后面换成了eventFd
--> 性能问题,管道读字符和写字符,需要拷贝,通过eventFd(里面只有计数器)只需要加加减减,性能更好

7.2 说说Android线程间消息传递机制?

考察点:

  • 消息循环过程是怎样的? --> looper.loop()原理
    Message里的消息为空,则会阻塞
    --> 原因:native层Looper的epoll_wait函数等待event
    ---> 阻塞中断条件:1.出错;2.超时;3.事件发生
  • 消息是怎么发送的? --> Handler.sendMessage
  • 消息是怎么处理的? --> Handler.dispatchMessage

Looper唤醒机制


实际上就是for循环里不断调用nativePollOnce()方法,有消息的时候,插入消息并且往eventFd里写一个数,epoll_wait(nativePollOnce调用到的)就能收到eventFd的可读事件,然后进行处理了。

7.3 handler的消息延迟是怎么实现的?

看几个问题:

  • 消息延迟是做了什么特殊处理么?
  • 是发送延时了,还是消息处理延时了?
  • 处理精度怎么样?

andler的消息延迟实现机制

  1. 是延时处理消息,发是马上发的,消息队列按消息触发事件排序
  2. 计算并设置epoll_wait的超时时间,使其在特定时间唤醒
  3. 延时精度,肯定不高了,比如有些消息处理耗时,导致这个消息延时处理了,所以通过设置epoll_wait只是粗略的延时。

7.4 说说idleHandler的原理?

考察点:

  • 了解IdleHandler的作用以及调用方式
  • 了解IdleHandler有哪些使用场景
  • 熟悉IdleHandler的实现原理

IdleHandler是干嘛的?
使用场景:

  1. 延迟执行(不紧急的事情 --> 对于产品,巴不得什么都紧急)
  2. 批量任务:(特点:1.任务密集;2.只关注最终结果)
    --> 比如:收到一堆推送,来推送就刷新界面,那会很卡,可以开一个工作线程,来一条推送,把它封装成一个消息丢进去,等消息处理完,工作线程空闲的时候,再去汇总结果,刷新界面<整合event>

它的实现原理是怎么样的?(触发,返回值意义)
IdleHandler是个接口,关键在于Looper.loop()函数,从中切入nativePollOnce()
调用时机:消息队列为空,或者第一条的消息触发时间还没到,并且在等待的时候触发
---> return为true表示一直生效,下次出现这种情况再次触发,false仅一次<一般场景是用false,很少有idle的时候就要触发什么固定的操作>
<并且不会重复触发idle,比如移除消息屏障后消息队列为空的时候>

framework里用到IdleHandler

  • GCIdler(return的是false,仅一次) --> 主线程第一idle的时候调用GC
  • 异步回调使用waitForIdle/同步等待线程idle状态
    ---> 需要传入EmptyRunnable(是为了优化当前就处于idle的特殊情况)

7.5 主线程进入loop循环了为什么没有ANR?

考察点:

  • 了解ANR触发的原理
  • 了解应用大致启动流程 --> 应用的主线程怎么进入loop循环的
  • 了解线程的消息循环机制 --> 又是nativePollOnce了(epoll_waite),回顾下
  • 了解应用和系统服务通信过程

首先要知道:

  1. 主线程的Looper不能随便退出,退出表示应用进程停止了
  2. ANR是在AMS里弹的,是系统进程

ANR场景
Service、BroadCast、ContentProvider、InputDispatch

ANR实现
Service为例,执行的时候发送ANR消息,AMS中调用serviceDoneExecuting,把发送的ANR消息移除,如果超时还没完成,就报ANR了,和应用开发差不多。

ANR原因

  1. ANR是应用没有在规定的时间内完成AMS指定的任务导致的
    --> 与for循环无关(消息机制本就需要一个循环)
  2. 进入for循环还是可以执行AMS制定的任务,因为AMS发过来的任务都会封装成消息,发送到应用主进程的消息队列里面,唤醒主线程进行处理<进入for循环,不代表进入死循环,还是可以处理系统发过来的请求>
  3. ANR不是因为主线程loop循环,而是因为主线程中有耗时任务(本身或者其他任务)

7.6 听说过消息屏障吗?

--> 屏幕刷新机制相关

消息种类:

  1. 普通消息
  2. 屏障消息
  3. 异步消息(与普通消息相比,设置了异步的标志位 --> setAsynchoronous)

消息屏障特点:

  1. 没有target(Handler) --> 通过有没有handler判断是否是消息屏障(正常发消息肯定需要Handler的)
  2. 也带时间戳,只会影响它后面的消息
  3. 可以插多个消息屏障
  4. 插入消息屏障没有唤醒线程
  5. 插入消息屏障后会返回token(屏障序列号,累加)
    --> 移除消息屏障的时候用,移除的时候可能要唤醒线程(唤醒条件:线程是因为消息屏障block住的,且该屏障后面没有跟着一个屏障)
  6. 是私有的,外部不可用(只能通过反射,通过Handler发一个不带target的消息消息不行,因为enqueueMessage()的第一个判断就是不允许没有handler)

处理消息屏障:Message的next()
---> 只会block住普通消息,不会block住异步消息

  1. 如果第一条消息就是屏障,就往后遍历,看看是否有异步消息
  2. 如果没有异步消息,就无限休眠,等待被别人唤醒
  3. 如果有异步消息,就看离这个消息触发还有多久,设置一个超时,继续休眠

有消息屏障的时候插入消息:

  1. 插入普通消息:如果插入到队列头,当前线程数休眠的,唤醒线程
  2. 插入异步消息:如果当前消息是最早的一条异步消息,且队列头是屏障,且线程是休眠的,唤醒线程(根据时间是否到了设置)

结合idleHandler
---> idleHandler的触发时机是消息队列为空,或者第一条的消息触发时间还没到

  • 消息队列是空的时候,插一个屏障,会触发idleHandler吗?
    不会,插入屏障不会唤醒线程,当前线程还在休眠

  • 如果删除了屏障,消息队列为空了,会触发idleHandler吗?
    不会,因为休眠前肯定已经调用过一次idleHandler了,不会重复触发

  • 如果消息队列只有一个屏障消息,插入一个普通消息会idleHandler吗?
    可能会,idleHandler触发规则是消息为空或者第一条消息还没到,关键是这个屏障的时间到了没有,如果没到,走一遍判断就触发idle了

  • 如果消息队列只有一个屏障消息,插入一个异步消息会idleHandler吗?
    可能会,与普通消息同理

framework里消息屏障的应用(不多)
Choreographer里的scheduleTraversals(),把屏障后面的普通消息block住,让界面绘制的异步消息优先执行
---> 消息屏障会block住普通消息,给异步消息开通个绿色通道,让异步消息优先执行(比如界面绘制、输入输出事件分发等等比较紧急的消息)

8.1 怎么跨进程传递大图片?

考察点:

  • 了解各种跨进程传输数据的方式及各自的优缺点
  • 了解TransactionTooLargeException的触发原因和底层机制
  • 了解Bitmap传输底层原理

<变相问法:intent传输数据限制,怎么突破/怎么跨进程传输大数据>

跨进程传大图,有哪些方案?

  • 把图片保存到固定的地方,传key给对方
    --> 缓存的实现(跨进程实现:文件存储,但是会多次拷贝,性能低)
  • 通过IPC的方式转发图片数据
    1. Binder --> 大小限制(突破限制:binder调用传图,底层ashmemFd机制)
    2. Socket、管道 --> 至少两次拷贝,大小限制
    3. 共享内存

考虑点:
1. 性能,减少拷贝次数
2. 内存泄露,资源及时关闭

为什么Intent直接带大Bitmap会异常,以Binder携带可以?
因为直接携带的时候,直接拷到Parcel缓冲区了,没有利用Parcel的ashmem机制 <setAllowFds(false)>
<Bundle的allowFds:Parcel里是否允许带描述符>
---> 四大组件都禁用了<execStartActivity> --> 原因未明(估计是为了严格要求,提高性能吧)

native层,平时利用Intent传数据的时候,实际上就是把对象组装成map,存到Parcel里的ArrayMap中。如果对象是Parcel,判断其是否大于16K直接存在Parcel缓存区里,否则利用ashmem机制,创建匿名共享内存返回fd,把数据拷进去

TransactionTooLargeException(事务太巨大了)
---> client端向server端发起调用,一直到调用结束,算一个事务(单事务角度)

  1. 发出去或者返回的数据量太大
  2. Binder缓存用于该进程所有正在进行中的Binder事务(多事务角度)
    --> 发数据收数据都需要buffer,事务结束时才释放,所有binder事务共享缓存空间,所以尽量别同时跑多个事务,尤其是大数据量的事务
  3. 建议:大数据量打碎分批发,或按需发
    <抛异常逻辑比较粗糙,事务失败+Parcel大于200k,则认为是TransactionTooLargeException>

8.2 说说ThreadLocal的原理?

考察点:

  • ThreadLocal适用于什么场景?
  • ThreadLocal的使用方式是怎样的?
  • ThreadLocal的实现原理是怎样的?

使用场景

  1. 调用prepare的时候,就new了一个Looper,把它塞到一个静态变量ThreadLocal里面;在Looper的myLooper()里取出来,不同线程获取不同的实例
  2. Choreographer有个静态变量ThreadLocal,不同线程调用getInstance()获取以当前Looper作为参数的Choreographer对象

ThrealLocal原理

  1. key和value成对存在于一个数组内,key对应WeakReference<ThreadLocal>,value可以是Looper/Choreographer对象 etc.
  2. 一个应用可以定义多个ThreadLocal,每个ThreadLocal对应一个他的hash值,通过hashCounter和mask取余为作为key的index
    --> 算index发生冲突的时候,从当前index往下遍历,直到找到一个为空的为止(感觉效率好低,但数据量应该不会很大)

ThreadLocal要点

  • 同一个ThradLocal对象,在不同线程get返回不同value
  • ThreadLocal原理:每个Thread对象里有张表,保存ThreadLocal到value的映射关系
  • 这张表上怎么实现的? --> 数组
  • 是如何解决hash冲突的? --> 从当前index往下找

8.3 来说说looper的副业?

回顾下looper的原理:for循环了不断的从MessageQueue里取下一条消息,如果消息不为空,就丢给对应的Handler就去分发消息。继续往下看,就是Message的next函数不断调用nativePollOnce函数,切入Looper的pollInner函数。

副业是在Looper的pollInner函数,不是wakeEventFd的时候进行的
--> Looper被唤醒的两个情况:1. 别的线程往当前线程写消息,wakeEventFd触发;2. 别的fd写消息

framework里应用(java层好像没用到,native层用到比较多)
vsync机制,SurfaceFlinger通知的应用进程刷新,就是通过在Choreographer初始化的时候在Looper里注册了fd --> 所以Choreographer的ThreadLocal用Looper做参数

系统服务向应用发起调用的方式:

  1. 通过epoll_wait+描述符
    --> 应用端好控制(什么时候处理,什么线程处理)<对于双方都是异步的>
    ----> 适用于简单的消息通知


  2. 通过BpBinder发起binder调用
    --> 应用端不好控制;不是oneway的话,服务端容易阻塞(应用处理这个调用非常耗时的时候),应用端是同步的
    ----> 适用于跨进程的函数调用



    ---> 有点难以应用到项目中

总结

  • Looper里可以监听其他的描述符
    <描述符可以是文件、管道、Socket>
    ---> 开放了函数(MessageQueue.addOnFileDescriptorEventListener),让应用可以添加描述符,在Looper的epoll_wait里监听
  • 可以在应用层创建管道(一对),把读描述符传到另外一个进程,在另外一个进程用looper监听描述符事件,在这个进程写数据,另外一个进程就能收到这个事件并且读出来 ---> 管道是0拷贝
  • Intent不允许带描述符,不能启动Service的时候通过Intent传过去,所以需要通过bindService传过去

8.4 怎么检查线程有耗时任务?

检测机制:

  • WatchDog(framework自带) --> 检查死锁或者异常的情况
  • BlockCanary --> 线程里的耗时任务

WatchDog是干什么的?

  • 检查是否发生了死锁(针对锁,不是针对线程)
  • 检查线程是否被任务blocked

WatchDog原理

  1. Watchdog自身就是个线程,不会因其他线程异常或者block,只需一个单例,即可监控全部
  2. 判断是否死锁只需在这个线程里获取下这个锁就可以了
  3. 实现核心:
  • MonitorCheck:检查死锁(所有系统服务都会往WatchDog里面注册monitor) --> 其实也是个HandlerCheck(在另外的线程尝试获取这把锁,所以系统服务调用传this即可)
  • HandlerCheck:系统服务把自己的工作线程new一个HandlerCheck添加到WatchDog的HandlerCheck列表里,每个HandlerCheck对应一个线程,在这个线程里去派发HandlerCheck
    --> 往这个线程头部插入一个runnable(不影响正常流程 --> 侧面反应异步消息的作用),开始记录,run完以后完成记录
    <每隔30秒去检查所有的HandlerCheck的完成情况,有一个没完成就是表示是block住的> --> block的程度是分阶段的,60秒没完成表示很严重,state值越大表示越严重

BlockCanary原理:
利用dispatchMessage的前后有可定制的log,相减即为耗时

8.5 怎么同步处理消息?

---> 需要等待消息处理结果
适用场景:应用要访问服务(binder线程在调用过程中一直等待)
原因:
1. 服务端不想上锁,交由应用层去做(ipc调用涉及到多线程同步的问题)
2. 有些任务必须要在工作线程去做(OpenGL要在有GL的上下文的线程才能去工作)

应用:

  1. native层:SurfaceFlinger给应用端创建Surface
  2. java层:runWithScissors(BlockingRunnable)

<为什么经常需要封装Callback,因为Runnable没有返回值>

总结:

  • 同步等待消息的处理
  • binder调用统一切换工作线程 --> 动态代理(InvocationHandler)

相关文章

网友评论

    本文标题:[视频笔记] -剖析framework面试,冲击Android高

    本文链接:https://www.haomeiwen.com/subject/sdymsqtx.html