百度 Android 面试:Handler 中有 Loop 死循环,为什么没有阻塞主线程,android 开发应用实战详解
//创建 ActivityThread 对象 Activity
《Android 学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
【docs.qq.com/doc/DSkNLaERkbnFoS0ZF】 完整内容开源分享
Thread 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() {@Overridepublic void run() {
runOnUiThread(new Runnable() {@Overridepublic 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() {@Overridepublic void run() {
Log.e("qdx", "step 1 "+Thread.currentThread().getName());
Handler handler=new Handler(getMainLooper());handler.post(new Runnable() {@Overridepublic void run() {
//Do Ui methodLog.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
*/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() {@Overridepublic void run() {
Toast.makeText(MainActivity.this, "run on thread", Toast.LENGTH_SHORT).show();//崩溃无疑
}}).start();
看到这个崩溃日志,是否有些疑惑,因为一般如果子线程不能更新 UI 控件是会报如下错误的(子线程不能更新 UI)
所以子线程不能更新 Toast 的原因就和 Handler 有关了,据我们了解,每一个 Handler 都要有对应的 Looper 对象,那么。 满足你。
new Thread(new Runnable() {@Overridepublic 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*/@Overridepublic void show(IBinder windowToken) {mHandler.obtainMessage(0, windowToken).sendToTarget();}
final Handler mHandler = new Handler() {@Overridepublic 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
学习分享
在当下这个信息共享的时代,很多资源都可以在网络上找到,只取决于你愿不愿意找或是找的方法对不对了
很多朋友不是没有资料,大多都是有几十上百个 G,但是杂乱无章,不知道怎么看从哪看起,甚至是看后就忘
如果大家觉得自己在网上找的资料非常杂乱、不成体系的话,我也分享一套给大家,比较系统,我平常自己也会经常研读。
2021 最新上万页的大厂面试真题

七大模块学习资料:如 NDK 模块开发、Android 框架体系架构…

只有系统,有方向的学习,才能在段时间内迅速提高自己的技术。
这份体系学习笔记,适应人群:**第一,**学习知识比较碎片化,没有合理的学习路线与进阶方向。**第二,**开发几年,不知道如何进阶更进一步,比较迷茫。**第三,**到了合适的年纪,后续不知道该如何发展,转型管理,还是加强技术研究。
评论