写点什么

字节 Android 岗面试:Handler 中有 Loop 死循环,为什么没有阻塞主线程

用户头像
Android架构
关注
发布于: 2021 年 11 月 06 日

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()处理完后,继续循环 loop 下去。


补充:


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(Runnable r) 。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(Runnable r) 老样子,我们点入源码


//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 run


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


nable 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(); 】)

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
字节Android岗面试:Handler中有Loop死循环,为什么没有阻塞主线程