Android面试Android线程间通信Handler消息机制

Posted Rose J

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android面试Android线程间通信Handler消息机制相关的知识,希望对你有一定的参考价值。

handler在android中我们常常用来线程切换,也是Android线程间通信的一种消息机制

Handler的工作流程

维持handler机制的主要有五大类,

1.handler

2.Message

3.Looper

4.MessageQueue

以及一个默默付出的Thread

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WzLsf7i6-1620208798961)(C:\\Users\\Lenovo\\AppData\\Roaming\\Typora\\typora-user-images\\1620132082972.png)]

源码解析:

发送消息:一般子线程通过调用handler的sendmessage方法,而sendMessage最终就是调用了MessageQueue的enqueueMessage,把消息发送到消息队列。

消息排序:而这个MessageQueue消息队列是一个优先级队列,是一种根据时间排序的队列,他的数据结构是链表,每当添加一条新消息,就会和消息队列中的消息进行时间排序,通过一个一个对比,然后将消息插入正确的位置,而sendMessage中调用了sendMessageDelay,如果没有设置延迟时间,消息发送的时间默认为0,就是调用时候的系统时间。

取出消息:取出消息队列中的消息需要用到Looper,主线程会自动初始化Looper,子线程需要调用Looper.prepare进行初始化,然后调用Looper.loop方法创建looper

,looper是一个死循环,looper中的loop方法会调用messageQueue中的next方法,通过next方法去取出messageQueue中的消息。

方法回调:取出消息以后,就会调用target也就是消息的dispatchMessage方法,而在dispatchMessage回调了handler的handleMessage方法,

从而就可以达到子线程通过handler机制通知主线程进行逻辑操作的作用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lPN5bHnQ-1620208798964)(C:\\Users\\Lenovo\\AppData\\Roaming\\Typora\\typora-user-images\\1620132724542.png)]

一个线程有几个Handler?

一个线程可以有多个handler,想new几个就有几个

一个线程有几个Looper?怎么保证?

一个线程只有一个Looper。

怎么保证?

通过threadLocal。

looper在prepare方法中会调用set方法
(map.set(this, value);)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uWUP7Xcc-1620226438160)(C:\\Users\\Lenovo\\AppData\\Roaming\\Typora\\typora-user-images\\1620226400158.png)]

set方法里面的this对应的就是当前的ThreadLocal,所以这个set方法就使ThreadLocal与Looper进行一一绑定了,而ThreadLocal是和线程进行绑定的,

一个线程对应一个map, map是中的entry是通过(key-value)的存储方式,从而就使ThreadLocal和Looper进行一一对应,而且ThreadLocal是final修饰的,所以他只有一个值。

而每次调用looper.prepare方法都会去检查ThreadLocal对应的value是否为空,如果不为空 就会报出一个异常,提示一个looper只能创建一次在一个线程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2LhECCfX-1620209998510)(C:\\Users\\Lenovo\\AppData\\Roaming\\Typora\\typora-user-images\\1620137578470.png)]

总结:一个线程只有一个map,map里面有多个entry,entry中有一个key对应一value,key就是ThreadLocal,value就是绑定的looper,所以这就形成了一个线程对应一个looper

Handler内存泄漏原因?为什么其他内部类没有出现这个问题?

匿名内部类持有外部类的对象的引用,作为一个内部类,他默认持有对外部类的对象引用,就是activity.this的引用,默认持有activity的引用。

为什么只有handler内存泄漏,但是其他的内部类没有内存泄漏呢?

handler处理的原理

在Message中,包含targe,

在enqueueMessage方法中msg.target=this,这个this就是handler

然后这个handler持有activity

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h8cZjF2P-1620208798973)(C:\\Users\\Lenovo\\AppData\\Roaming\\Typora\\typora-user-images\\1620143390265.png)]

而message在messagequeue中,也就是messagequeue持有了message。

如果message设置延迟一分钟处理,messagequeue就会在一分钟后处理消息,hanlder也会在一分钟后解除对activity的引用,如果message一直不被处理,message就会一直放在messageQueue中,handler也会一直持有handler的引用

messageQueue属于内存, 当jvm进行回收的时候,这个messageQueue有一个漫长的持有链,就算activity调用了onDestroy,但是activity在messageQueue内存中被持有了,那么activity仍旧不会被释放

所以内存该释放的时候没被释放,仍保持对象的持有,所以这就产生了内存泄漏

怎么解决handler的内存泄漏?

  1. 使用static 修饰的handler(静态内部类),然后弱引用activity对象,因为要使用activity对象中的成员
 private static class MyHandler extends Handler {
        private WeakReference<HandlerOOMActivity> weakReference;
//        private HandlerOOMActivity activity;
 
        public MyHandler(HandlerOOMActivity activity) {
            weakReference = new WeakReference<>(activity);
//            this.activity = activity;
        }
 
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            Log.e(TAG, "handleMessage: ");
//            HandlerOOMActivity activity = weakReference.get();
            switch (msg.what) {
                case 0:
//                    if (activity != null) {
//                        activity.tv.setText("我是更改后的文字");
//                    }
//                    activity.tv.setText("我是更改后的文字");
                    break;
            }
        }
    }

  • 2.使用内部类的handler,在onDestroy方法中removeCallbacksAndMessages

为什么主线程可以new Handler

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X2JXvFEn-1620208798974)(C:\\Users\\Lenovo\\AppData\\Roaming\\Typora\\typora-user-images\\1620145705583.png)]

在ActivityThread中,会完成looper的初始化,以及loop方法的调用,所以主线程就可以直接new Handler去用。

如何在子线程new Handler,需要做些什么?

我们需要在子线程准备一个looper

Looper.prepare

Looper.loop();//调用loop方法

子线程中维护的Looper,消息队列无消息的时候的处理方案是什么?有什么用?

loop方法中,每一个消息都要经过loop去轮询,(通过queue.next),当没有消息时,可能会block阻塞,

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jJ7dWo7J-1620208798975)(C:\\Users\\Lenovo\\AppData\\Roaming\\Typora\\typora-user-images\\1620179299124.png)

因为在next方法中,如果消息队列里没有消息了,nextPollTimeoutMillis就会等于-1

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zDj1EgVP-1620208798976)(C:\\Users\\Lenovo\\AppData\\Roaming\\Typora\\typora-user-images\\1620178829712.png)]

然后调用continu方法,跳出循环,进入下一次循环

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SkDL3pHp-1620208798977)(C:\\Users\\Lenovo\\AppData\\Roaming\\Typora\\typora-user-images\\1620179207442.png)]

然后会在nativePollOnce(ptr, nextPollTimeoutMillis);传一个-1进去

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e1BJMHQx-1620208798977)(C:\\Users\\Lenovo\\AppData\\Roaming\\Typora\\typora-user-images\\1620179276740.png)]

nativePollOnce的作用是用来等待,这是一个native修饰的函数,这个函数最终会调用linux中epoll机制,在Linux中也有一个messagequeue,messagequeue会调用epoll函数,epoll函数会根据传的值进行变化,如果传的值为-1,则会无限等待,直到有事件发生,

也就是这个时候线程挂起了(sleep),Looper一直处理等待过程,线程做不了其他事情了,

那怎么唤醒线程呢?在Looper当中有一个quit方法,我们可以通过looper调用quit方法就会调用messagequeue的quit方法,调用quit方法后,首先会把mQuitting变量设置为ture,然后会把消息队列中的消息全部处理掉,会把所有消息回收掉,(回收消息就是把所有消息中的变量赋值为null),

然后通过nativeWake唤醒线程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NjoJihc5-1620208798977)(C:\\Users\\Lenovo\\AppData\\Roaming\\Typora\\typora-user-images\\1620183973485.png)]

然后继续走next方法,这个时候因为mQuitting为true,所以会返回null值,

然后在Looper循环中对消息的判断会为空,然后就会退出消息队列。线程也就释放了

主线程需要释放吗?为什么不行?

在主线程调用quit方法,会报异常,直接就退出quit方法了

为什么要这么设计呢?

因为AMS是围绕handler来对activity进行管理的

既然可以存在多个Handler往messageQueue中添加数据(发消息时各个Handler可能处于不同线程) ,那它内部是怎么保证线程安全呢?

一个线程 ——>对应一个Looper———>对应一个MessageQueue

通过加锁 synchronized(this)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nNu7JUt2-1620208798978)(C:\\Users\\Lenovo\\AppData\\Roaming\\Typora\\typora-user-images\\1620203048304.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u7M23tnM-1620208798980)(C:\\Users\\Lenovo\\AppData\\Roaming\\Typora\\typora-user-images\\1620203058873.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yeBkuC78-1620208798980)(C:\\Users\\Lenovo\\AppData\\Roaming\\Typora\\typora-user-images\\1620203077408.png)]

怎么创建Message?

1.Message message = new message();

享元设计模式

2.Message message = Message.obtain();
3.Message message = handler.obtainMessage();

当消息处理完后,会放到一个sPool消息队列中,构造了一个头插的链表,

每次添加一个消息,都会放到链表的头部

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s7Q8KUiM-1620208798981)(C:\\Users\\Lenovo\\AppData\\Roaming\\Typora\\typora-user-images\\1620203320927.png)]

obtain方法就是,在需要创建Message的时候,通过从sPool取出消息,并且把消息中的值修改来达到创建消息的目的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gZKNzmQI-1620208798981)(C:\\Users\\Lenovo\\AppData\\Roaming\\Typora\\typora-user-images\\1620203513062.png)]

这样的好处就是减少了每次需要去new消息的过程,如果不断去new的话,会带什么效果呢?每new一次,jvm就会分配一次内存,如果不使用享元模式,message就需要destroy,因为handler机制中所有的一切都是用消息来实现的,所以频繁的创建与销毁,就会出现内存抖动,在一些极端的情况下就会出现oom

(享元模式:运用共享技术有效地支持大量细粒度对象的复用)

Looper死循环为什么不会导致应用卡死

ANR的原因:

5秒钟之内没有响应输入的事件,比如按键,屏幕触摸等

然后还有广播接收器10秒内没有执行完毕

ANR 的原因 因为Looper对象MessageQueue队列中的事件没有能够得到及时执行。所以Looper死循环不是导致ANR的必然原因,

而且Looper是一个阻塞式死循环,有消息来了就会处理消息,没有消息主线程就会进入休眠状态,所以也不会导致OOM

结论:应用卡死 ANR压根与Looper没有关系,应用在没有消息需要处理的时候,他是在睡眠,释放线程,卡死是ANR,是如果消息队列里有消息,looper却没有及时处理导致的,和Looper是死循环无关

子线程一定不能更新UI吗?

答:不一定。
1.Activity存在一种审计机制,这个机制会在Activity完全显示之后工作,如果子线程在Activity完全显示之前更新UI是可
行的;
2.SurfaceView:多媒体视频播放,也可以在子线程中更新UI
3.Progress:进度相关控件,也可以在子线程中更新UI

为什么Android系统不建议子线程访问UI?

首先,UI控件不是线程安全的,如果多线程并发访问UI控件可能会出现不可预期的状态
那为什么系统不对UI控件的访问加上锁机制呢? 缺点有两个:
加上锁机制会让UI访问的逻辑变得复杂;
锁机制会降低UI访问的效率,因为锁机制会阻塞某些线程的执行

使用Handler的postDealy后消息队列有什么变化?

MessageQueue里的消息会以时间顺序(执行的先后顺序)来排序,使用Handler的postDealy后,MessageQueue里的消息会进行重新排序。

**使用Handler的postDealy后消息队列可能会进行重新排序。**消息队列里消息按执行先后时间进行排序,先执行的在前,后执行的在后。postDealy发送的消息会根据延迟时间与消息队列里存在的消息的执行时间进行比较,然后寻找插入位
置插入消息。

同步屏障机制

同步屏障机制是一套为了让某些特殊的消息得以更快被执行的机制

a8a041d80a956d0f194178265dab37fd.png

通过 postSyncBarrier 方法添加一个target=null的消息来添加同步屏障,然后looper会忽略所有的同步消息,直到遇到一个异步消息

这样设计的目的其实是为了使得当队列中遇到同步屏障时,则会使得异步的消息优先执行,这样就可以使得一些消息优先执行。

removeSyncBarrier方法移除同步屏障

handler.post和handler.sendMessage的区别和联系

post这个方法是把任务r转成一个message放进了handler所在的线程中的messageQueue消息队列中,并且是立刻发送的消息,这样它既不是异步的也不是延时的,

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XrHs9OVs-1620208798983)(C:\\Users\\Lenovo\\AppData\\Roaming\\Typora\\typora-user-images\\1620208531417.png)]

最大区别:post就是把要执行的任务转成消息放进消息队列中

结论

1.post和sendMessage本质上是没有区别的,只是实际用法中有一点差别

2.post也没有独特的作用,post本质上还是用sendMessage实现的,post只是一中更方便的用法而已

以上是关于Android面试Android线程间通信Handler消息机制的主要内容,如果未能解决你的问题,请参考以下文章

Android面试 Handler机制

Android 高级面试题及答案

Android工程师面试准备知识点

Android 面试必备:Android多进程间通信—Binder机制

31到Android面试题,收藏下?

Android 高级面试题及答案