Android消息机制常见问题汇总

Android 消息机制作用和使用场景

Android 消息机制主要用来进行线程间通信,常见的使用场景有:

  • 跨进程之后的界面消息处理

比如 Activity 的启动,就是 AMS 在进行进程间通信的时候,通过 Binder 线程将消息发送给 ApplicationThread 的消息处理者 Handler,然后再将消息分发给主线程中去执行。

  • 网络交互后切换到主线程进行 UI 更新

当子线程网络操作之后,需要切换到主线程进行 UI 更新。

总之一句话,消息机制的存在就是为了解决线程间的通信问题。

子线程访问 UI 崩溃原因和为什么建议子线程不更新 UI

在 Android 中都是通过 ViewRootImpl 进行 UI 更新的,在更新 UI 的时候会调用自己的 checkThread 方法,用来检测是否是在主线程执行 UI 更新,代码如下:

void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }

这个 mThread 就是生成 ViewRootImpl 时候的主线程,所以我们知道,只要执行更新 UI 操作的当前线程不是 UI 线程,则抛出异常。

因为 Android 中的 UI 控件不是线程安全的,如果多线程访问 UI 控件那就乱套了。那如果加锁控制呢?

  • 会降低 UI 访问的效率。本身 UI 控件就是离用户比较近的一个组件,加锁之后自然会发生阻塞,那么 UI 访问的效率会降低,最终反应到用户端就是这个手机有点卡
  • 太复杂了。本身 UI 访问时一个比较简单的操作逻辑,直接创建 UI,修改 UI 即可。如果加锁之后就让这个 UI 访问的逻辑变得很复杂,没必要

所以,Android 设计出了单线程模型来处理 UI 操作,再搭配上 Handler 机制,是一个比较合适的解决方案。

同步分隔栏的使用场景

同步分隔栏和异步消息的存在的意义就在于有些消息需要“加急处理”,比如在绘制方法 scheduleTraversals 中,为当前线程的 MessageQueue 添加同步屏障来屏蔽同步消息,保证 VSync 信号到来后立即执行绘制,而不是要等前面的同步消息。

void scheduleTraversals() {
        if (!mTraversalScheduled) {
        // 保证同时间多次更改只会刷新一次,例如TextView连续两次setText()也只会走一次绘制流程
            mTraversalScheduled = true;
            // 调用 postSyncBarrier 插入同步分隔栏,保证 VSync 到来立即执行绘制
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

Android 中为什么主线程不会因为 Looper.loop() 里的死循环卡死

我们知道线程就是一段可执行的代码,既然是一段可执行的代码,当可执行代码执行完成后,线程生命周期便该终止了,线程退出。而对于主线程,我们是绝不希望会被运行一段时间,自己就退出,那么如何保证能一直存活呢?简单做法就是可执行代码是能一直执行下去的,死循环便能保证不会被退出,例如,binder 线程也是采用死循环的方法,通过循环方式与 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");
}

thread.attach(false);便会创建一个 Binder 线程(具体是指 ApplicationThread,Binder 的服务端,用于接收系统服务 AMS 发送来的事件),该 Binder 线程通过 Handler 将 Message 发送给主线程。

另外,ActivityThread 实际上并非线程,不像 HandlerThread 类,ActivityThread 并没有真正继承 Thread 类,只是往往运行在主线程,该人以线程的感觉,其实承载 ActivityThread 的主线程就是由 Zygote fork 而创建的进程。

主线程的死循环一直运行是不是特别消耗CPU资源呢? 其实不然,这里就涉及到 Linux pipe/epoll 机制,简单说就是在主线程的 MessageQueue 没有消息时,便阻塞在 loop 的 queue.next() 中的 nativePollOnce() 方法里,此时主线程会释放 CPU 资源进入休眠状态,直到下个消息到达或者有事务发生,通过往 pipe 管道写端写入数据来唤醒主线程工作。这里采用的 epoll 机制,是一种 IO 多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质同步 I/O,即读写是阻塞的。 所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量 CPU 资源。

Handler 的 post(Runnable) 与 sendMessage 有什么区别

public final boolean post(@NonNull Runnable r) {
   return  sendMessageDelayed(getPostMessage(r), 0);
}
private static Message getPostMessage(Runnable r) {
    Message m = Message.obtain();
    m.callback = r;
    return m;
}

通过 post 的源码可知,其实 post 和 sendMessage 的区别就在于:post 方法给 Message 设置了一个 callback。

那么这个 callback 有什么用呢?我们再转到消息处理的方法 dispatchMessage 中看看:

public void dispatchMessage(@NonNull Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}

private static void handleCallback(Message message) {
    message.callback.run();
}

总结如下:

  • 如果 msg.callback 不为空,也就是通过 post 方法发送消息的时候,会把消息交给这个 msg.callback 进行处理
  • 如果 msg.callback 为空,也就是通过 sendMessage 发送消息的时候,会判断 Handler 当前的 mCallback 是否为空,如果不为空就交给 Handler.Callback.handleMessage 处理
  • 如果 mCallback.handleMessage 返回 true,则返回
  • 如果 mCallback.handleMessage 返回 false,则继续调用 handler 类重写的 handleMessage 方法

所以 post(Runnable) 与 sendMessage 的区别就在于后续消息的处理方式,是交给 msg.callback 还是 Handler.Callback 或者Handler.handleMessage,大家注意其中的优先级顺序。

Hanlder内存泄露问题

我们一般的说法是:“内部类持有了外部类的引用,也就是 Hanlder 持有了 Activity 的引用,从而导致 Activity 无法及时回收。”

其实这样回答是片面的,或者说没回答到点子上。

Java 虚拟机中使用可达性分析的算法来决定对象是否可以被回收。即通过 GCRoot 对象为起始点,向下搜索走过的路径(引用链),如果发现某个对象或者对象组为不可达状态,则将其进行回收。

而内存泄漏指的就是有些对象(短周期对象)没有用了,但是却被其他有用的类(长周期对象)所引用,从而导致无用对象占据了内存空间,形成内存泄漏。

所以上面的问题,如果仅仅回答内部类持有了外部类的引用,没有指出内部类被谁所引用,那么按道理来说是不会发生内存泄漏的,因为内部类和外部类都是无用对象了,是可以被正常回收的。

所以这一题的关键在于,内部类被谁引用了?也就是 Handler 被谁引用了?

我们必须找到那个最终的引用者,不会被回收的引用者,其实就是主线程,这条完整引用链应该是这样:

主线程 —> threadlocal —> Looper —> MessageQueue —> Message —> Handler —> Activity

Handler发生内存泄漏的情况有以下几种:

1.发送延迟消息

比如在一个 Activity 中发送一个延迟 20s 的消息。然后打开 Activity 后,马上 finish。这个时候就会产生内存泄漏,原因就是这个 Activity 的引用是被匿名内部类的实例 mHandler 持有了,而 Handler 的引用是被 Message 持有了,Message 引用是被 MessageQueue 持有了…

也就是如下:

主线程 —> threadlocal —> Looper —> MessageQueue —> Message —> Handler —> Activity

所以这次引用的头头就是主线程,主线程肯定是不会被回收的,只要是运行中的线程都不会被 JVM 回收,跟静态变量一样被 JVM 特殊照顾。

2.子线程运行没结束

是我们常用到的,在子线程中工作,比如请求网络,然后请求成功后通过 Handler 进行 UI 更新。

可以发现,这里的内存泄漏主要的原因是因为这个运行中的子线程,由于子线程这个匿名内部类持有了外部类的引用,而子线程本身是一直在运行的,刚才说过运行中的线程是不会被回收的,所以这里内存泄漏的引用链应该是:

运行中的子线程 —> Activity

当然,这里的 Handler 也是持有了 Activity 的引用的,但主要引起内存泄漏的原因还是在于子线程本身,就算子线程中不用 Handler,而是调用 Activity 的其他变量或者方法还是会发生内存泄漏。

所以这种情况我觉得不能看作Handler引起内存泄漏的情况,其根本原因是因为子线程引起的,如果解决了子线程的内存泄漏,比如在 Activity 销毁的时候停止子线程,那么 Activity 就能正常被回收,那么也不存在 Handler 的问题了。

解决内存泄漏

1.不要让长生命周期对象持有短生命周期对象的引用,而是用长生命周期对象持有长生命周期对象的引用

2.将对象的强引用改成弱引用

强引用就是对象被强引用后,无论如何都不会被回收。
弱引用就是在垃圾回收时,如果这个对象只被弱引用关联(没有任何强引用关联他),那么这个对象就会被回收。
软引用就是在系统将发生内存溢出的时候,回进行回收。
虚引用是对象完全不会对其生存时间构成影响,也无法通过虚引用来获取对象实例,用的比较少。

3.内部类写成静态类或者外部类

4.在生命周期短的组件结束的时候将可能发生内存泄漏的地方移除

比如 Handler 延迟消息,资源没关闭,集合没清理等等引起的内存泄漏,只要在 Activity 关闭的时候进行消除即可

@Override
protected void onDestroy() {
  //移除handler所有消息
  if(mHanlder != null){
		mHandler.removeCallbacksAndMessages(null)
  }
  super.onDestroy();
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值