面试官: Handler中有Loop死循环,为何没有阻塞主线程,原理是什么
心理分析:该问题很难被考到,可是若是一旦问到,100%会回答不上来。开发者很难注意到一个主线程的四循环竟然没有阻塞住主线程
求职者:应该从 主线程的消息循环机制 与Linux的循环异步等待做用讲起。最后将handle引发的内存泄漏,内存泄漏必定是一个加分项
先上一份整理好的面试目录java
Android的消息机制主要是指Handler的运行机制,对于你们来讲Handler已是轻车熟路了,但是真的掌握了Handler?本文主要经过几个问题围绕着Handler展开深刻并拓展的了解。android
站在巨人的肩膀上会看的更远。你们有兴趣的也能够到Gityuan的博客上多了解了解,所有都是干货。并且他写的东西比较权威,毕竟也是小米系统工程师的骨干成员。git
回答一: Looper 死循环为何不会致使应用卡死?github
线程默认没有Looper的,若是须要使用Handler就必须为线程建立Looper。咱们常常提到的主线程,也叫UI线程,它就是ActivityThread
,ActivityThread
被建立时就会初始化Looper,这也是在主线程中默承认以使用Handler的缘由。
首先咱们看一段代码面试
new Thread(new Runnable() { @Override public void run() { Log.e("qdx", "step 0 "); Looper.prepare(); Toast.makeText(MainActivity.this, "run on Thread", Toast.LENGTH_SHORT).show(); Log.e("qdx", "step 1 "); Looper.loop(); Log.e("qdx", "step 2 "); } }).start();
咱们知道Looper.loop()
;里面维护了一个死循环方法,因此按照理论,上述代码执行的应该是 step 0 –>step 1 也就是说循环在Looper.prepare()
;与Looper.loop()
;之间。算法
在子线程中,若是手动为其建立了Looper,那么在全部的事情完成之后应该调用quit方法来终止消息循环,不然这个子线程就会一直处于等待(阻塞)状态,而若是退出Looper之后,这个线程就会马上(执行全部方法并)终止,所以建议不须要的时候终止Looper。
执行结果也正如咱们所说,这时候若是了解了ActivityThread
,而且在main方法中咱们会看到主线程也是经过Looper方式来维持一个消息循环安全
public static void main(String[] args) { Looper.prepareMainLooper();//建立Looper和MessageQueue对象,用于处理主线程的消息 ActivityThread thread = new ActivityThread(); thread.attach(false);//创建Binder通道 (建立新线程) if (sMainThreadHandler == null) { sMainThreadHandler = thread.getHandler(); } Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); Looper.loop(); //若是能执行下面方法,说明应用崩溃或者是退出了... throw new RuntimeException("Main thread loop unexpectedly exited"); }
那么回到咱们的问题上,这个死循环会不会致使应用卡死,即便不会的话,它会慢慢的消耗愈来愈多的资源吗?多线程
对于线程便是一段可执行的代码,当可执行代码执行完成后,线程生命周期便该终止了,线程退出。而对于主线程,咱们是毫不但愿会被运行一段时间,本身就退出,那么如何保证能一直存活呢?简单作法就是可执行代码是能一直执行下去的,死循环便能保证不会被退出,例如,binder线程也是采用死循环的方法,经过循环方式不一样与Binder驱动进行读写操做,固然并不是简单地死循环,无消息时会休眠。但这里可能又引起了另外一个问题,既然是死循环又如何去处理其余事务呢?经过建立新线程的方式。真正会卡死主线程的操做是在回调方法onCreate/onStart/onResume
等操做时间过长,会致使掉帧,甚至发生ANR,looper.loop自己不会致使应用卡死。主线程的死循环一直运行是否是特别消耗CPU资源呢? 其实否则,这里就涉及到
Linux pipe/epoll
机制,简单说就是在主线程的MessageQueue
没有消息时,便阻塞在loop的queue.next()
中的nativePollOnce()
方法里,此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,经过往pipe管道写端写入数据来唤醒主线程工做。这里采用的epoll机制,是一种IO多路复用机制,能够同时监控多个描述符,当某个描述符就绪(读或写就绪),则马上通知相应程序进行读或写操做,本质同步I/O,即读写是阻塞的。 因此说,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。 Gityuan–Handler(Native层)并发
回答二:主线程的消息循环机制是什么?app
事实上,会在进入死循环以前便建立了新binder线程,在代码ActivityThread.main()中:
public static void main(String[] args) { //建立Looper和MessageQueue对象,用于处理主线程的消息 Looper.prepareMainLooper(); //建立ActivityThread对象 ActivityThread thread = new ActivityThread(); //创建Binder通道 (建立新线程) thread.attach(false); Looper.loop(); //消息循环运行 throw new RuntimeException("Main thread loop unexpectedly exited"); }
Activity的生命周期都是依靠主线程的Looper.loop,当收到不一样Message时则采用相应措施:一旦退出消息循环,那么你的程序也就能够退出了。 从消息队列中取消息可能会阻塞,取到消息会作出相应的处理。若是某个消息处理时间过长,就可能会影响UI线程的刷新速率,形成卡顿的现象。
thread.attach(false)
方法函数中便会建立一个Binder线程(具体是指ApplicationThread
,Binder的服务端,用于接收系统服务AMS发送来的事件),该Binder线程经过Handler将Message发送给主线程。「Activity 启动过程」
好比收到msg=H.LAUNCH_ACTIVITY
,则调用ActivityThread.handleLaunchActivity()
方法,最终会经过反射机制,建立Activity实例,而后再执行Activity.onCreate()
等方法;
再好比收到msg=H.PAUSE_ACTIVITY
,则调用ActivityThread.handlePauseActivity()
方法,最终会执行Activity.onPause()
等方法。
主线程的消息又是哪来的呢?固然是App进程中的其余线程经过Handler发送给主线程
system_server进程
system_server
进程是系统进程,java framework
框架的核心载体,里面运行了大量的系统服务,好比这里提供ApplicationThreadProxy
(简称ATP),ActivityManagerService
(简称AMS),这个两个服务都运行在system_server
进程的不一样线程中,因为ATP和AMS都是基于IBinder接口,都是binder线程,binder线程的建立与销毁都是由binder驱动来决定的。
App进程
App进程则是咱们常说的应用程序,主线程主要负责Activity/Service
等组件的生命周期以及UI相关操做都运行在这个线程; 另外,每一个App进程中至少会有两个binder线程ApplicationThread(
简称AT)和ActivityManagerProxy
(简称AMP),除了图中画的线程,其中还有不少线程
Binder
Binder用于不一样进程之间通讯,由一个进程的Binder客户端向另外一个进程的服务端发送事务,好比图中线程2向线程4发送事务;而handler用于同一个进程中不一样线程的通讯,好比图中线程4向主线程发送消息。
结合图说说Activity生命周期,好比暂停Activity,流程以下:
1.线程1的AMS中调用线程2的ATP;(因为同一个进程的线程间资源共享,能够相互直接调用,但须要注意多线程并发问题)
2.线程2经过binder传输到App进程的线程4;
3.线程4经过handler消息机制,将暂停Activity的消息发送给主线程;
4.主线程在looper.loop()
中循环遍历消息,当收到暂停Activity的消息时,便将消息分发给ActivityThread.H.handleMessage()
方法,再通过方法的调用,
5.最后便会调用到Activity.onPause()
,当onPause()
处理完后,继续循环loo
p下去。
补充:
ActivityThread的main
方法主要就是作消息循环,一旦退出消息循环,那么你的程序也就能够退出了。
从消息队列中取消息可能会阻塞,取到消息会作出相应的处理。若是某个消息处理时间过长,就可能会影响UI线程的刷新速率,形成卡顿的现象。
最后经过《Android开发艺术探索》的一段话总结 :
ActivityThread
经过ApplicationThread
和AMS进行进程间通信,AMS以进程间通讯的方式完成ActivityThread的请求后会回调ApplicationThread
中的Binder方法,而后ApplicationThread
会向H发送消息,H收到消息后会将ApplicationThread
中的逻辑切换到ActivityThread
中去执行,即切换到主线程中去执行,这个过程就是。主线程的消息循环模型
另外,ActivityThread
实际上并不是线程,不像HandlerThread
类,ActivityThread
并无真正继承Thread类
那么问题又来了,既然ActivityThread不是一个线程,那么ActivityThread中Looper绑定的是哪一个Thread,也能够说它的动力是什么?
回答三:ActivityThread 的动力是什么?
进程每一个app运行时前首先建立一个进程,该进程是由Zygote fork
出来的,用于承载App上运行的各类Activity/Service等组件。进程对于上层应用来讲是彻底透明的,这也是google有意为之,让App程序都是运行在Android Runtime
。大多数状况一个App就运行在一个进程中,除非在AndroidManifest.xml
中配置Android:process
属性,或经过native代码fork进程。
线程 线程对应用来讲很是常见,好比每次new Thread().start
都会建立一个新的线程。该线程与App所在进程之间资源共享,从Linux角度来讲进程与线程除了是否共享资源外,并无本质的区别,都是一个task_struct
结构体,在CPU看来进程或线程无非就是一段可执行的代码,CPU采用CFS调度算法,保证每一个task都尽量公平的享有CPU时间片。
其实承载ActivityThread的主线程就是由Zygote fork而建立的进程。
回答四:Handler 是如何可以线程切换
其实看完上面咱们大体也清楚线程间是共享资源的。因此Handler处理不一样线程问题就只要注意异步状况便可。
这里再引伸出Handler的一些小知识点。 Handler建立的时候会采用当前线程的Looper来构造消息循环系统,Looper在哪一个线程建立,就跟哪一个线程绑定,而且Handler是在他关联的Looper对应的线程中处理消息的。(敲黑板)
那么Handler内部如何获取到当前线程的Looper呢—–ThreadLocal
。ThreadLocal
能够在不一样的线程中互不干扰的存储并提供数据,经过ThreadLocal
能够轻松获取每一个线程的Looper。
固然须要注意的是:
①线程是默认没有Looper的,若是须要使用Handler,就必须为线程建立Looper。咱们常常提到的主线程,也叫UI线程,它就是ActivityThread,
②ActivityThread
被建立时就会初始化Looper,这也是在主线程中默承认以使用Handler的缘由。
系统为何不容许在子线程中访问UI?(摘自《Android开发艺术探索》) 这是由于Android的UI控件不是线程安全的,若是在多线程中并发访问可能会致使UI控件处于不可预期的状态,那么为何系统不对UI控件的访问加上锁机制呢?
缺点有两个:
①首先加上锁机制会让UI访问的逻辑变得复杂
②锁机制会下降UI访问的效率,由于锁机制会阻塞某些线程的执行。 因此最简单且高效的方法就是采用单线程模型来处理UI操做。
那么问题又来了,子线程必定不能更新UI?
看到这里,又留下两个知识点等待下篇详解:View的绘制机制与Android Window内部机制。
回答五:子线程有哪些更新UI的方法
主线程中定义Handler
,子线程经过mHandler
发送消息,主线程Handler的handleMessage
更新UI。 用Activity对象的runOnUiThread
方法。 建立Handler,传入getMainLooper
。 View.post(Runnabler)
。runOnUiThread
第一种我们就不分析了,咱们来看看第二种比较经常使用的写法。
先从新温习一下上面说的
Looper
在哪一个线程建立,就跟哪一个线程绑定,而且Handler是在他关联的Looper
对应的线程中处理消息的。(敲黑板)
new Thread(new Runnable() { @Override public void run() { runOnUiThread(new Runnable() { @Override public void run() { //DO UI method } }); } }).start(); final Handler mHandler = new Handler(); public final void runOnUiThread(Runnable action) { if (Thread.currentThread() != mUiThread) { mHandler.post(action);//子线程(非UI线程) } else { action.run(); } }
进入Activity类里面,能够看到若是是在子线程中,经过mHandler发送的更新UI消息。 而这个Handler是在Activity中建立的,也就是说在主线程中建立,因此便和咱们在主线程中使用Handler更新UI没有差异。 由于这个Looper,就是ActivityThread
中建立的Looper(Looper.prepareMainLooper())
。
建立Handler,传入getMainLooper
那么同理,咱们在子线程中,是否也能够建立一个Handler,并获取MainLooper
,从而在子线程中更新UI呢? 首先咱们看到,在Looper类中有静态对象sMainLooper
,而且这个sMainLooper
就是在ActivityThread
中建立的MainLooper
。
private static Looper sMainLooper; // guarded by Looper.class public static void prepareMainLooper() { prepare(false); synchronized (Looper.class) { if (sMainLooper != null) { throw new IllegalStateException("The main Looper has already been prepared."); } sMainLooper = myLooper(); } }
因此不用多说,咱们就能够经过这个sMainLooper
来进行更新UI操做。
new Thread(new Runnable() { @Override public void run() { Log.e("qdx", "step 1 "+Thread.currentThread().getName()); Handler handler=new Handler(getMainLooper()); handler.post(new Runnable() { @Override public void run() { //Do Ui method Log.e("qdx", "step 2 "+Thread.currentThread().getName()); } }); } }).start();
View.post(Runnabler)
老样子,咱们点入源码
//View
/** * <p>Causes the Runnable to be added to the message queue. * The runnable will be run on the user interface thread.</p> * * @param action The Runnable that will be executed. * * @return Returns true if the Runnable was successfully placed in to the * message queue. Returns false on failure, usually because the * looper processing the message queue is exiting. * */ public boolean post(Runnable action) { final AttachInfo attachInfo = mAttachInfo; if (attachInfo != null) { return attachInfo.mHandler.post(action); //通常状况走这里 } // Postpone the runnable until we know on which thread it needs to run. // Assume that the runnable will be successfully placed after attach. getRunQueue().post(action); return true; } /** * A Handler supplied by a view's {@link android.view.ViewRootImpl}. This * handler can be used to pump events in the UI events queue. */ final Handler mHandler;
竟然也是Handler从中做祟,根据Handler的注释,也能够清楚该Handler能够处理UI事件,也就是说它的Looper也是主线程的sMainLooper。这就是说咱们经常使用的更新UI都是经过Handler实现的。
另外更新UI 也能够经过AsyncTask
来实现,难道这个AsyncTask
的线程切换也是经过 Handler 吗? 没错,也是经过Handler……
Handler实在是......
回答六:子线程中Toast,showDialog,的方法
可能有些人看到这个问题,就会想: 子线程原本就不能够更新UI的啊 并且上面也说了更新UI的方法
兄台且慢,且听我把话写完
new Thread(new Runnable() { @Override public void run() { Toast.makeText(MainActivity.this, "run on thread", Toast.LENGTH_SHORT).show();//崩溃无疑 } }).start();
看到这个崩溃日志,是否有些疑惑,由于通常若是子线程不能更新UI控件是会报以下错误的(子线程不能更新UI)
因此子线程不能更新Toast的缘由就和Handler有关了,据咱们了解,每个Handler都要有对应的Looper对象,那么。 知足你。
new Thread(new Runnable() { @Override public void run() { Looper.prepare(); Toast.makeText(MainActivity.this, "run on thread", Toast.LENGTH_SHORT).show(); Looper.loop(); } }).start();
这样便能在子线程中Toast,不是说子线程…? 老样子,咱们追根到底看一下Toast内部执行方式。
//Toast
/** * Show the view for the specified duration. */ public void show() {
INotificationManager service = getService();//从SMgr中获取名为notification的服务 String pkg = mContext.getOpPackageName(); TN tn = mTN; tn.mNextView = mNextView; try { service.enqueueToast(pkg, tn, mDuration);//enqueue? 难不成和Handler的队列有关? } catch (RemoteException e) { // Empty }
}
在show方法中,咱们看到Toast的show方法和普通UI 控件不太同样,而且也是经过Binder进程间通信方法执行Toast绘制。这其中的过程就不在多讨论了,有兴趣的能够在`NotificationManagerService`类中分析。 如今把目光放在TN 这个类上(难道越重要的类命名就越简洁,如H类),经过TN 类,能够了解到它是Binder的本地类。在Toast的show方法中,将这个TN对象传给`NotificationManagerService`就是为了通信!而且咱们也在TN中发现了它的show方法。
private static class TN extends ITransientNotification.Stub {//Binder服务端的具体实现类
/** * schedule handleShow into the right thread */ @Override public void show(IBinder windowToken) { mHandler.obtainMessage(0, windowToken).sendToTarget(); } final Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { IBinder token = (IBinder) msg.obj; handleShow(token); } };
}
看完上面代码,就知道子线程中Toast报错的缘由,由于在TN中使用Handler,因此须要建立Looper对象。 那么既然用Handler来发送消息,就能够在`handleMessage`中找到更新Toast的方法。 在`handleMessage`看到由handleShow处理。 **//Toast的TN类**
public void handleShow(IBinder windowToken) {
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); mParams.x = mX; mParams.y = mY; mParams.verticalMargin = mVerticalMargin; mParams.horizontalMargin = mHorizontalMargin; mParams.packageName = packageName; mParams.hideTimeoutMilliseconds = mDuration == Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT; mParams.token = windowToken; if (mView.getParent() != null) { mWM.removeView(mView); } mWM.addView(mView, mParams);//使用WindowManager的addView方法 trySendAccessibilityEvent(); } }
看到这里就能够总结一下:
Toast本质是经过window显示和绘制的(操做的是window),而主线程不能更新UI 是由于ViewRootImpl
的checkThread
方法在Activity维护的View树的行为。 Toast中TN类使用Handler是为了用队列和时间控制排队显示Toast,因此为了防止在建立TN时抛出异常,须要在子线程中使用Looper.prepare()
;和Looper.loop()
;(可是不建议这么作,由于它会使线程没法执行结束,致使内存泄露)
Dialog亦是如此。同时咱们又多了一个知识点要去研究:Android 中Window是什么,它内部有什么机制?
回答七:如何处理Handler 使用不当致使的内存泄露? 首先上文在子线程中为了节目效果,使用以下方式建立Looper
Looper.prepare();
Looper.loop();
实际上这是很是危险的一种作法 > 在子线程中,若是手动为其建立Looper,那么在全部的事情完成之后应该调用`quit`方法来终止消息循环,不然这个子线程就会一直处于等待的状态,而若是退出Looper之后,这个线程就会马上终止,所以建议不须要的时候终止Looper。(【 `Looper.myLooper().quit();` 】) 那么,若是在Handler的**handleMessage**方法中(或者是run方法)处理消息,若是这个是一个延时消息,会一直保存在主线程的消息队列里,而且会影响系统对Activity的回收,形成内存泄露。 具体能够参考Handler内存泄漏分析及解决 总结一下,解决Handler内存泄露主要2点 1 有延时消息,要在Activity销毁的时候移除Messages 2 匿名内部类致使的泄露改成匿名静态内部类,而且对上下文或者Activity使用弱引用。 ##### 总结 想不到Handler竟然能够腾出这么多浪花,与此同时感谢前辈的摸索。 另外Handler还有许多鲜为人知的秘密,等待你们探索,下面我再简单的介绍两分钟 - HandlerThread - IdleHandler **HandlerThread** >`HandlerThread`继承`Thread`,它是一种可使用Handler的`Thread`,它的实现也很简单,在`run`方法中也是经过`Looper.prepare()`来建立消息队列,并经过`Looper.loop()`来开启消息循环(与咱们手动建立方法基本一致),这样在实际的使用中就容许在`HandlerThread`中建立Handler了。 因为`HandlerThread`的run方法是一个无限循环,所以当不须要使用的时候经过quit或者`quitSafely`方法来终止线程的执行。 `HandlerThread`的本质也是线程,因此切记关联的Handler中处理消息的`handleMessage`为子线程。 IdleHandler
/**
*/
public static interface IdleHandler {
/** * Called when the message queue has run out of messages and will now * wait for more. Return true to keep your idle handler active, false * to have it removed. This may be called if there are still messages * pending in the queue, but they are all scheduled to be dispatched * after the current time. */ boolean queueIdle();
}
根据注释能够了解到,这个接口方法是在消息队列所有处理完成后或者是在阻塞的过程当中等待更多的消息的时候调用的,返回值false表示只回调一次,true表示能够接收屡次回调。 具体使用以下代码
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
@Override public boolean queueIdle() { return false; } });
另外提供一个小技巧:在`HandlerThread`中获取`Looper的`MessageQueue`方法之反射。 由于 `Looper.myQueue()`若是在主线程调用就会使用主线程looper 使用`handlerThread.getLooper().getQueue()`最低版本须要23 `//HandlerThread`中获取`MessageQueue`
Field field = Looper.class.getDeclaredField("mQueue"); field.setAccessible(true); MessageQueue queue = (MessageQueue) field.get(handlerThread.getLooper());
那么Android的消息循环机制是经过Handler,是否能够经过IdleHandler来判断Activity的加载和绘制状况(measure,layout,draw等)呢?而且`IdleHandler`是否也隐藏着鲜为人知的特殊功能? **更多面试内容,面试专题,flutter视频 全套,音视频从0到高手开发。** 关注**GitHub:**https://github.com/xiangjiana/Android-MS **免费获取面试PDF合集** 