“一篇就够“系列: Handler扩展篇

前言

Handler系列文章共两篇:

第一篇:"一篇就够"系列: Handler消息机制完全解析

第二篇: "一篇就够"系列: Handler扩展篇

上一篇中,我们对Handler的主体部分进行了讲解,今天,我们就来学习一下Handler相关的一些扩展知识,讲完这些扩展知识后,在来回答之前列出来的一系列问题

妙用 Looper 机制

1、我们可以通过LoopergetMainLooper方法获取主线程Looper,从而可以判断当前线程是否是主线程

2、将 Runnable post 到主线程执行

public final class MainThread {

    private MainThread() {
    }

    private static final Handler HANDLER = new Handler(Looper.getMainLooper());

    public static void run(@NonNull Runnable runnable) {
        if (isMainThread()) {
            runnable.run();
        }else{
            HANDLER.post(runnable);
        }
    }

    public static boolean isMainThread() {
        return Looper.myLooper() == Looper.getMainLooper();
    }
}

子线程弹Toast

//1
new Thread(){
    @Override
    public void run() {
        Toast.makeText(MainActivity.this, "子线程弹Toast", Toast.LENGTH_SHORT).show();
    }
}.start();

//2
new Thread(){
    @Override
    public void run() {
        Looper.prepare();
        Toast.makeText(MainActivity.this, "子线程弹Toast", Toast.LENGTH_SHORT).show();
        Looper.loop();
    }
}.start();
复制代码

上述1代码运行会奔溃,会报这么一个异常提示:"Can't toast on a thread that has not called Looper.prepare()"

原因就是Toast的实现也是依赖Handler,而我们知道在子线程中创建Handler,需先创建Looper并开启消息循环,这点在Toast中的源码也有体现,如下图:

image-20210223131023498

因此我们在子线程创建Toast就需要使用上述2代码的方式

子线程弹Dialog

new Thread(){
    @Override
  	public void run() {
            Looper.prepare();
      	    new AlertDialog.Builder(MainActivity.this)
                  .setTitle("标题")
                  .setMessage("子线程弹Dialog")
                  .setNegativeButton("取消",null)
                  .setPositiveButton("确定",null)
                  .show();
            Looper.loop();     
    }    
}.start();
复制代码

和上面Toast差不多,这里贴出正确的代码示例,它的实现也是依赖Handler,我们在它的源码中可以看到:

private final Handler mHandler = new Handler();
复制代码

他直接就new了一个Handler实例,我们知道,创建Handler,需要先创建Looper并开启消息循环,主线程中已经给我们创建并开启消息循环,而子线程中并没有,如果不创建那就会报这句经典的异常提示:"Can't create handler inside thread that has not called Looper.prepare() ",因此在子线程中,需要我们手动去创建并开启消息循环

到这里,Handler相关的扩展知识就全部讲完了,我们会发现也有着很多使用的小技巧,比如 IdleHandler,判断是否是主线程等等

由于 Handler 的特性,它在 Android 里的应用非常广泛,比如: AsyncTask、HandlerThread、Messenger、IdleHandler 和 IntentService 等等,下面我们来回答上一篇中列出来的一系列问题

问题

1、Handler有哪些作用?

答:

1、Handler能够进行线程之间的切换

2、Handler能够按照顺序处理消息,避免并发

3、Handler能够阻塞线程

4、Handler能够发送并处理延迟消息

解析:

1、Handler能够进行线程之间的切换,是因为使用了不同线程的Looper处理消息

2、Handler能够按照顺序处理消息,避免并发,是因为消息在入队的时候会按照时间升序对当前链表进行排序,Looper读取的时候,MessageQueue的next方法会循环加锁,同时配合阻塞唤醒机制

3、Handler能够阻塞线程主要是基于Linux的epoll机制实现的

4、Handler能够处理延迟消息,是因为MessageQueue的next方法中会拿当前消息时间和当前时间做比较,如果是延迟消息,那么就会阻塞当前线程,等阻塞时间到,在执行该消息

2、为什么我们能在主线程直接使用Handler,而不需要创建Looper?

答:主线程已经创建了Looper,并开启了消息循环

3、如果想要在子线程创建Handler,需要做什么准备?

答:需要先创建Looper,并开启消息循环

4、一个线程有几个Handler?

答:可以有任意多个

5、一个线程有几个Looper?如何保证?

答:一个线程只有一个Looper,通过ThreadLocal来保证

6、Handler发送消息的时候,时间为啥要取SystemClock.uptimeMillis() + delayMillis,可以把SystemClock.uptimeMillis() 换成System.currentTimeMillis()吗?

答:不可以

SystemClock.uptimeMillis() 这个方法获取的时间,是自系统开机到现在的一个毫秒数,这个时间是个相对的

System.currentTimeMillis() 这个方法获取的是自1970-01-01 00:00:00 到现在的一个毫秒数,这是一个和系统强关联的时间,而且这个值可以做修改

1、使用System.currentTimeMillis()可能会导致延迟消息失效

2、最终这个时间会被设置到Message的when属性,而Message的when属性只是需要一个时间差来表示消息的先后顺序,使用一个相对时间就行了,没必要使用一个绝对时间

7、为什么Looper死循环,却不会导致应用卡死?

答:因为当Looper处理完所有消息的时候,会调用Linux的epoll机制进入到阻塞状态,当有新的Message进来的时候会打破阻塞继续执行。

应用卡死即ANR: 全称Applicationn Not Responding,中文意思是应用无响应,当我发送一个消息到主线程,Handler经过一定时间没有执行完这条消息,那么这个时候就会抛出ANR异常

Looper死循环: 循环执行各种事务,Looper死循环说明线程还活着,如果没有Looper死循环,线程结束,应用就退出了,当Looper处理完所有消息的时候会调用Linux的epoll机制进入到阻塞状态,当有新的Message进来的时候会打破阻塞继续执行

8、Handler内存泄露原因? 如何解决?

内存泄漏的本质是长生命周期的对象持有短生命周期对象的引用,导致短生命周期的对象无法被回收,从而导致了内存泄漏

下面我们就看个导致内存泄漏的例子

public class MainActivity extends AppCompatActivity {

    private final Handler mHandler = new Handler() {
        @Override
        public void handleMessage(@NonNull Message msg) {
           //do something
        }
    };
  
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //发送一个延迟消息,10分钟后在执行
        mHandler.sendEmptyMessageDelayed(0x001,10*60*1000);
    }
}
复制代码

上述代码:

1、我们通过匿名内部类的方式创建了一个Handler的实例

2、在onCreate方法里面通过Handler实例发送了一个延迟10分钟执行的消息

我们发送的这个延迟10分钟执行的消息它是持有Handler的引用的,根据Java特性我们又知道,非静态内部类会持有外部类的引用,因此当前Handler又持有Activity的引用,而Message又存在MessageQueue中,MessageQueue又在当前线程中,因此会存在一个引用链关系:

当前线程->MessageQueue->Message->Handler->Activity

因此当我们退出Activity的时候,由于消息需要在10分钟后在执行,因此会一直持有Activity,从而导致了Activity的内存泄漏

通过上面分析我们知道了内存泄漏的原因就是持有了Activity的引用,那我们是不是会想,切断这条引用,那么如果我们需要用到Activity相关的属性和方法采用弱引用的方式不就可以了么?我们实际操作一下,把Handler写成一个静态内部类

public class MainActivity extends AppCompatActivity {

  	private final SafeHandler mSafeHandler = new SafeHandler(this);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
      	//发送一个延迟消息,10分钟后在执行
        mSafeHandler.sendEmptyMessageDelayed(0x001,10*60*1000);
    }

    //静态内部类并持有Activity的弱引用
    private static class SafeHandler extends Handler{
      
        private final WeakReference<MainActivity> mWeakReference;
      
        public SafeHandler(MainActivity activity){
            mWeakReference = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(@NonNull Message msg) {
            MainActivity mMainActivity = mWeakReference.get();
            if(mMainActivity != null){
                //do something
            }
        }
    }
}
复制代码

上述代码

1、把Handler定义成了一个静态内部类,并持有当前Activity的弱引用,弱引用会在Java虚拟机发生gc的时候把对象给回收掉

经过上述改造,我们解决了Activity的内存泄漏,此时的引用链关系为:

当前线程->MessageQueue->Message->Handler

我们会发现Message还是会持有Handler的引用,从而导致Handler也会内存泄漏,所以我们应该在Activity销毁的时候,在他的生命周期方法里,把MessageQueue中的Message都给移除掉,因此最终就变成了这样:

public class MainActivity extends AppCompatActivity {

  	private final SafeHandler mSafeHandler = new SafeHandler(this);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
      	//发送一个延迟消息,10分钟后在执行
        mSafeHandler.sendEmptyMessageDelayed(0x001,10*60*1000);
    }
  
  	@Override
    protected void onDestroy() {
        super.onDestroy();
        mSafeHandler.removeCallbacksAndMessages(null);
    }

    //静态内部类并持有Activity的弱引用
    private static class SafeHandler extends Handler{
      
        private final WeakReference<MainActivity> mWeakReference;
      
        public SafeHandler(MainActivity activity){
            mWeakReference = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(@NonNull Message msg) {
            MainActivity mMainActivity = mWeakReference.get();
            if(mMainActivity != null){
                //do something
            }
        }
    }
}
复制代码

因此当Activity销毁后,引用链关系为:

当前线程->MessageQueue

而当前线程和MessageQueue的生命周期和应用生命周期是一样长的,因此也就不存在内存泄漏了,完美。

所以解决Handler内存泄漏最好的方式就是:将Handler定义成静态内部类,内部持有Activity的弱引用,并在Activity销毁的时候移除所有消息

9、线程维护的Looper,在消息队列无消息时的处理方案是什么?有什么用?

答:当消息队列无消息时,Looper会阻塞当前线程,释放cpu资源,提高App性能

我们知道Looper的loop方法中有个死循环一直在读取MessageQueue中的消息,其实是调用了MessageQueue中的next方法,这个方法会在无消息时,调用Linux的epoll机制,使得线程进入阻塞状态,当有新消息到来时,就会将它唤醒,next方法里会判断当前消息是否是延迟消息,如果是则阻塞线程,如果不是,则会返回这条消息并将其从优先级队列中给移除

10、MessageQueue什么情况下会被唤醒?

答:需要分情况

1、发送消息过来,此时MessageQueue中无消息或者当前发送过来的消息携带的when为0或者有延迟执行的消息,那么需要唤醒

2、当遇到同步屏障且当前发送过来的消息为异步消息,判断该异步消息是否插入在所有异步消息的队首,如果是则需要唤醒,如果不是,则不唤醒

11、线程什么情况下会被阻塞?

答:分情况

1、当MessageQueue中没有消息的时候,这个时候会无限阻塞,

2、当前MessageQueue中全部是延迟消息,阻塞时间为(当前延迟消息时间 - 当前时间),如果这个阻塞时间超过来Integer类型的最大值,则取Integer类型的最大值

12、我们可以使用多个Handler往消息队列中添加数据,那么可能存在发消息的Handler存在不同的线程,那么Handler是如何保证MessageQueue并发访问安全的呢?

答:循环加锁,配合阻塞唤醒机制

我们可以发现MessageQueue其实是“生产者-消费者”模型,Handler不断地放入消息,Looper不断地取出,这就涉及到死锁问题。如果Looper拿到锁,但是队列中没有消息,就会一直等待,而Handler需要把消息放进去,锁却被Looper拿着无法入队,这就造成了死锁。Handler机制的解决方法是循环加锁。在MessageQueue的next方法中:

Message next() {
   ...
    for (;;) {
  ...
        nativePollOnce(ptr, nextPollTimeoutMillis);
        synchronized (this) {
            ...
        }
    }
}
复制代码

我们可以看到他的等待是在锁外的,当队列中没有消息的时候,他会先释放锁,再进行等待,直到被唤醒。这样就不会造成死锁问题了。

那在入队的时候会不会因为队列已经满了然后一边在等待消息处理一边拿着锁呢?这一点不同的是MessageQueue的消息没有上限,或者说他的上限就是JVM给程序分配的内存,如果超出内存会抛出异常,但一般情况下是不会的。

13、Handler是如何进行线程切换的呢?

答:使用不同线程的Looper处理消息

我们通常处理消息是在Handler的handleMessage方法中,那么这个方法是在哪里回调的呢?看下面这段代码

public static void loop() {
    //开启死循环读取消息
    for (;;) {
         // 调用Message对应的Handler处理消息
         msg.target.dispatchMessage(msg);
    }
}
复制代码

上述代码中msg.target其实就是我们发送消息的Handler,因此他会回调Handler的dispatchMessage方法,而dispatchMessage这个方法我们在上一篇中重点分析过,其中有一部分逻辑就是会回调到Handler的handleMessage方法,我们还可以发现,Handler的handleMessage方法所在的线程是由Looper的loop方法决定的。平时我们使用的时候,是从异步线程发送消息到 Handler,而这个 Handler 的 handleMessage() 方法是在主线程调用的,因为Looper是在主线程创建的,所以消息就从异步线程切换到了主线程。

14、我们在使用Message的时候,应该如何去创建它?

答:Android 给 Message 设计了回收机制,官方建议是通过Message.obtain方法来获取,而不是直接new一个新的对象,所以我们在使用的时候应尽量复用 Message ,减少内存消耗,方式有二:

1、调用 Message 的一系列静态重载方法 Message.obtain 获取

2、通过 Handler 的公有方法 handler.obtainMessage,实际上handler.obtainMessage内部调用的也是Message.obtain的重载方法

15、Handler里面藏着的CallBack能做什么?

答: 利用此CallBack拦截Handler的消息处理

在上一篇中我们分析到,dispatchMessage方法的处理步骤:

1、首先,检查Message的callback是否为null,不为null就通过handleCallBack来处理消息,Message的callback是一个Runnable对象,实际上就是Handler的post系列方法所传递的Runnable参数

2、其次,检查Handler里面藏着的CallBack是否为null,不为null就调用mCallback的handleMessage方法来处理消息,并判断其返回值:为true,那么 Handler 的 handleMessage(msg) 方法就不会被调用了;为false,那么就意味着一个消息可以同时被 Callback 以及 Handler 处理

3、最后,调用Handler的handleMessage方法来处理消息

通过上面分析我们知道Handler处理消息的顺序是:Message的Callback > Handler的Callback > Handler的handleMessage方法

使用场景: Hook ActivityThread.mH , 在 ActivityThread 中有个成员变量 mH ,它是个 Handler,又是个极其重要的类,几乎所有的插件化框架都使用了这个方法。

16、Handler阻塞唤醒机制是怎么一回事?

答: Handler的阻塞唤醒机制是基于Linux的阻塞唤醒机制。

这个机制也是类似于handler机制的模式。在本地创建一个文件描述符,然后需要等待的一方则监听这个文件描述符,唤醒的一方只需要修改这个文件,那么等待的一方就会收到文件从而打破唤醒。和Looper监听MessageQueue,Handler添加message是比较类似的。具体的Linux层知识读者可通过这篇文章详细了解(传送门

17、什么是Handler的同步屏障?

答: 同步屏障是一种使得异步消息可以被更快处理的机制

18、能不能让一个Message被加急处理?

答:可以,添加加同步屏障,并发送异步消息

19、什么是IdleHandler?

答: IdleHandler是MessageQueue中一个静态函数型接口,它在主线程执行完所有的View事务后,回调一些额外的操作,且不会阻塞主线程

总结

Handler消息机制在Android系统源码中进行了大量的使用,可以说是涉及了Android的方方面面,比如我们四大组件的启动,Application的创建等等,学好Handler相关的知识,可以帮助我们更好的去阅读Android源码,而且Handler在我们日常开发中直接或间接的会被用到。同时通过对Handler源码的学习,让我感受到了代码设计的背后,蕴藏着工程师大量的智慧,心里直呼666,哈哈。

到了这里,关于Handler相关的知识就都讲完了,如果你还有什么问题,评论区告诉我吧。

参考和推荐

Android全面解析之Handler机制(终篇):常见问题汇总

Handler 都没搞懂,拿什么去跳槽啊?

换个姿势,带着问题看Handler

全文到此,欢迎点赞,收藏,评论和转发,你的认可是我创作的动力

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值