作者:老余
就目前一线企业的app都是多线程和多进程的,而Android进程间通信机制就是Binder,原生的线程间通信则是Handler,Binder和Handler是了解安卓运行机制必须要掌握的一个知识点,更是一线企业面试必问的知识点!
所以framework面试在面试中,是会被常问及到的问题!往往有些技术点是拿到大厂offer的关键。
到目前为止,我在Android开发已经待了近六年时间,大大小小的公司进过不少;那么我接下来说说自己在面试中会被常常问及到的面试题。希望能给你们带来一些启发!
Binder由4部分组成:Server,Client,ServiceManager和Binder驱动。 Server:服务端。负责功能的具体实现; Client:客户端。功能的调用方; ServiceManager:类似于DNS,根据Client调用的服务名找到对应的Binder引用并返回给Client; Binder驱动:类似于路由,实现进程间通信,负责Binder线程管理等。
Binder通信分为如下几步:
Server端首先向ServiceManager注册,SMgr收到注册消息后,将其对应的名称与Binder引用存储于Map中。
Client向SMgr发起请求,获取Server的引用。SMgr根据收到的参数查找到对应的Server端引用返回给Client,Client收到后就建立了Client与Server间的通信通道
Client传入特定参数给Binder驱动,Binder驱动申请一定大小的内核内存空间,并与内核接收缓存及Server用户空间的接收缓存区进行内存映射。Client线程将参数传递给内核空间的接收缓存区后,线程就挂起。由于内存映射,相当于直接传递给了Server端,Server中的分配线程解析包并调用其中的方法,执行完后将结果封装到数据包中,根据内存映射传递回内核接收缓存区。Binder驱动唤醒Client线程,并传递返回的数据包,Client收到数据包后获取结果,至此完成IPC通信。
Binder数据传输时会将调用方的pid和uid封装到数据包中,Server可以通过getCallingPid()/getCallingUid()获取到调用方的pid或uid,然后进行特定的校验。 也可以通过Binder.clearCallingIndenity()清除调用方的uid/pid。 可以清除也可以通过Binder.restoreCallingIndentity(token)恢复调用方的uid/pid
可以,相当于Client已知Server端的引用,无需通过SMgr就有通信通道。
in:只允许数据向Server输入;
out:只允许数据从Server输出;
inout:数据可从Server输出也可输入;
oneway:代表方法是异步调用。
IPC方式:Socket,信号量,管道,Binder,共享内存
默认是不可以跨进程通信,若需要跨进程通信可如下操作:
需要通信的应用设置成相同的shareUID;
获取数据的应用通过包名,拿的对应的context。然后通过该context拿到SharedPreference实例
commit和apply的区别:
两者都是原子操作,但commit是将数据直接写到数据库中,而apply是写到内存中,然后再异步写入到数据库中。
Handler是Android系统提供给开发者的一种线程间通信的方式。除了Handler,开发者也可以使用其他的线程间通信方式,只不过Google推荐优先使用handler的方式。
禁止在子线程中更新UI,是因为子线程UI状态不确定,但可以更新在子线程中创建的UI,这样UI的状态子线程可随时观察到。 另,Android使用的是单线程模式,处理多线程更新UI造成的不可控情况。但可以在onCreate中子线程直接更新UI,但增加耗时则不行,因为在onCreate的时候,ViewRootImpl还未创建,在onResume之后才创建,创建后才会进行checkThread操作。
可以在关闭StrictMode严苛模式的情况下在主线程执行网络操作
因为通过Message.obtain()获取Message实例,是从消息池中复用消息实例,避免重复创建浪费资源。
因为使用Handler需要首先执行Looper.prepare()和Looper.loop()方法,若不执行这些操作是不可以使用Handler的。 主线程之所以可以直接创建并使用,是因为在ActivityThread中的main方法中已经执行过Looper.prepare()和Looper.loop()方法。
handler.sendEmptyMessage()/postXXX()
HandlerThread就是已经在子线程中执行了Looper.prepare()和Looper.loop()操作。 而无需开发者手动执行这些操作。
当用Handler发送消息时,是将该Message入MessageQueue,并同时唤醒Looper.next()阻塞遍历消息队列。
Handler将Message入队MessageQueue,入队后唤醒Looper,Looper遍历消息取出消息,然后回调给Handler的handleMessage()接口。
当MessageQueue中无可处理的消息时会调用IdleHandler。用于处理View的相关事务后的一些额外操作,不会阻塞主线程。如View绘制完成后进行一些额外的处理,如获取宽高,打印log等操作
主线程的各个生命周期也是作为MessageQueue中的消息执行。其本身也是作为消息处理,本身一直处于阻塞中,所以不会ANR。
Handler泄漏,是因为Handler持有Activity的引用,而Handler被MessageQueue中的Message所持有,而Message被MessageQueue所持有,所以当执行长时间的消息时,Activity退出由于被持有链中的MessageQueue所持有,仍然是可达的,所以无法被回收掉,造成泄漏。 此时,可以将Handler作为static静态变量,或者将Activity用弱引用WeakReference包装后再使用。
同步屏障机制其作用是当遍历消息队列时,遇到屏障则会跳过后续的同步消息,找到异步消息并执行。故其可保证异步消息及时执行。同步屏障消息是target为null的消息,但用普通方式入队时会根据target是否等于null的条件给排除掉,所以同步屏障消息入队有特殊的接口postSyncBarrier,使用完后可通过removeSyncBarrier移除屏障消息。
Handler的休眠唤醒机制,是因为Looper是不断循环遍历队列,但若当前无消息需要执行时那么遍历就会浪费资源。故在无消息需要处理时,就阻塞Looper,待到指定时间或有消息需要处理时再唤醒Looper获取消息并处理。 Handler的休眠唤醒机制是利用linux里的epoll机制,通过epoll_create创建初始化,epoll_ctl注册事件,文件被指定事件触发时则回调,epoll_wait待超过指定时间后执行回调。 MessageQueue中的消息是根据时间顺序排队的,越早需要执行的消息排在越前。然后调用nativePollOnce方法传入延时时间,此时就会阻塞在此,当阻塞时间到达延时时间时就会被唤醒,取出消息执行。 移除延时消息时会唤醒线程再遍历一次,重新计算是否要休眠。故之前的休眠会被解除。
Fragment hide show生命周期变化
Fragment replace生命周期变化
AMS交互调用生命周期是顺序的吗?
登陆功能,登陆成功然后跳转到一个新Activity,中间涉及什么?从事件传递,网络请求,AMS交互角度分析
广播与RxBus的区别,全局广播与局部广播区别
说说App的启动过程,在ActivityThread的main方法里面做了什么事,什么时候启动第一个Activity?
onCreate,onResume,onStart里面,什么地方可以获得宽高
onMeasure,onLayout和onDraw是绘制流程过程中的三大核心过程。
onMeasure代表测量过程,测量View的大小;
onLayout代表布局过程,布置View所要放的位置;
onDraw代表绘制内容,绘制View所包含的内容。
3个步骤共构成了View的绘制流程中的核心部分。
双重缓冲:图像写数据写入一个buffer(back buffer),屏幕获取图像数据放入另一个buffer(frame buffer)。当有信号到达时,交换2者Buffer。
黄油模型:Android4.1引入。收到Vsync信号后,交换2个buffer后,cpu/gpu立刻开始计算,然后把数据写入buffer。此时cpu/gpu有完整的16.6ms时间计算。
三重缓冲:在双重缓冲基础上,增加一个buffer(Graphic Buffer)。为了处理一帧的数据16ms不够的情况。
invalidate是通过系统进行触发一次绘制流程;
postInvalidate,是将绘制消息作为消息发给ActivityThread,等待其消息队列分发执行
requestLayout内部会走到scheduleTraversal,进行绘制的流程发起通过performTraversal开始,经performMeasure,performLayout和performDraw进行一次绘制流程。
onLayout是每个子View真正执行布局的地方。
drawChild是绘制子View,View真正执行draw的操作是通过onDraw执行的。
从ViewRootImpl的performTraversal()开始,依次经过performMeasure测量,performLayout布局和performDraw绘制流程,完成View的绘制。
经过performMeasure,交给根ViewGroup,依次遍历子View,通过子View的measure方法测量子View的宽高,子View的measure测量真正执行在onMeasure()中,遍历完后最终得到根布局的宽高;
经过performLayout也是遍历子View,通过子View的layout方法进行布局,依次布置子View的位置,同样真正执行布局的接口是onLayout,不过一般是空实现,进而得到根布局的位置;
最后经过performDraw()绘制子View内容。通常View的绘制需要经过如下6大内容:
画背景
画内容
分发给子View进行绘制
画渐变效果
画前景装饰
触摸屏幕的事件会被记录在文件中,系统通过EventHub去文件取出事件,封装成InputEvent。然后由InputReader交给InputDispatcher,InputDispatcher将事件对象转化成MotionEvent然后开始分发。经一系列的InputStage交给ViewRootImpl。ViewRootImpl最终会交给ViewGroup,然后从ViewGroup开始进行事件分发。
首先当识别到按下事件时需要进行一些初始化和置位操作。然后会判断ViewGroup是否需要拦截该事件,拦截的判断首先会判断是否设置了DISPATCH_ALLOW_INTERCEPT标记未,若设置了则不会拦截;否则需要判断onIntercept()返回true还是false,若返回true则需要拦截,否则也不会拦截。
判断完是否需要拦截后,则会查找到子View中当前未在播放动画,且触摸区域在View区域范围内的View,找到满足条件的View后会判断View是否需要消耗事件,若不需要的话则继续往下传递,否则就将事件交给该View。且同一事件序列的后续事件也将交给该View处理。
若最终找不到需要消耗事件的子View的时候,事件将会回头向上传递,最终由Activity消耗该事件。
事件首先会交给ViewGroup的dispatchTouchEvent,在dispatchTouchEvent会根据onInterceptEvent的返回结果判断是否需要拦截事件。最终事件若要消耗,则会在onTouchEvent中消耗。
重写其onInterceptTouchEvent(),直接返回false,即不拦截事件,则无法处理滑动。
这里需要区分ACTION_DOWN事件ViewGroup是否会拦截:
拦截。如果DOWN事件拦截的话,那么后续的事件都会交给ViewGroup;
不拦截。如果不拦截,且子View有要拦截的话,此时ViewGroup要拦截move事件,那么会给子View发送一个cancel事件。然后事件也会发给自己或其子View。
内容太多以至于有一些答案没附上,这只是面试题的小部分。刷这些当然是不够用的;文章篇幅有限,于是我把面试Android开发岗位的面试题整理好了,以便于自己以后刷题。如阅读者在Android开发的,想跳槽,找工作,我可以把它分享出来;点击获取面试大纲PDF方式。我就介绍这么多,关键是大家有没有用心刷题哈!
###文末
简历上写的东西,一定要先搞懂,特别是简历上的专业技能。面试收到的最多反馈就是:基础知识不够扎实,技术深度不够。现在Android开发大部分需要懂点C++和Linux知识,大厂也需要刷算法面试题,在后面的学习过程中,我将从以上几个方面着手。当然也必须把基础知识学牢固,技术深度搞深入点。
最后祝大家都能拿到心仪的offer!