Handler

使用方法

Handler是Android提供用来切换线程的机制,子线程发送消息,主线程从消息队列中取出消息并执行。比如在子线程中进行网络请求获取数据后需要调用主线程去展示数据,就可以用到Handler。在主线程创建一个Handler对象,重写handleMessage方法,然后在子线程调用这个handler的sendMessage等一系列方法发送一个Message对象,在Message中加数据或者处理类型,最后这个Message就会交给主线程的Handler进行处理。

如果想在别的线程使用Handler的话,还需要手动创建子线程的Looper,调用Looper.prepare(),创建handler,然后调用Looper的loop()开始消息机制,需要注意,调用了loop()之后,后面的代码都是执行不到的了。

原理

Handler是Android中的消息机制。消息机制主要有四部分组成:Message、Looper、MessageQueue和Handler。

我们先来说一下他们之间的关系:每一个线程都有一个Looper,但是可以有多个Handler。Looper是存储在ThreadLocal的ThreadLocalMap中的,Handle创建的时候需要和一个Looper进行绑定。然后Looper中会有一个MessageQueue消息队列,她是一个单链表结构。

然后我就具体来说一下他的工作原理:刚刚说到,一个线程有一个Looper,对于子线程来说,你必须通过Looper.prepare()方法去创建一个Looper,而对于主线程来说,他在ActivityThread创建的时候,就调用了Looper.prepare()以及Looper.loop()。Looper对象创建完了之后我们就可以创建Handler了。然后我们就需要发送消息,消息是Message,我们可以通过Message.obtain()方法去获取Message对象,这个方法他会先从消息池里面去寻找有没有可用的消息,如果有就直接哪来用,没有就再去创建一个,这样我们就可以对Message实现复用,避免创建没必要的资源。然后我们就需要通过sendMessage方法,里面传入一个Message对象,或者post方法里面传入一个Runnable对象,就可以把消息发送到MessageQueue里面去了。其实post方法也是调用了send方法,他会把Runnable封装成Message。我们说过MessageQueue是一个单链表接口,它里面会根据Message的when字段去排序,然后在插入的地方,把message插进去。这时会调用native层的nativeWake方法去唤醒消息队列的读取操作。接着我们可以调用Looper.loop()开启消息循环,它会一直循环去获取消息队列中的消息,消息队列的读取是一个阻塞操作,他会调用native层的nativePollOnce()方法,这个方法需要传入一个int型的参数,如果这个参数小于0,代表里面没有消息需要去处理,则继续阻塞;如果等于0,就代表有消息需要立即去执行;>0则代表有消息,只是执行时间还没到。我们在读取消息的时候可能会遇到消息屏障。如果第一次循环消息队列空闲,它会通过IdelHandler执行一些空闲时任务。如果有消息返回,说明它到了执行时间,就调用对应的handler对象的dispatchMessage去处理此消息,里面会判断有没有runnable,有没有callback,然后交给handler的handleMessage()去处理,最后将消息放到回收消息池中。如果是子线程,我们还需要在处理完消息之后需要调用Looper.exit去退出Looper,否则loop()会一直循环,该子线程也将不会终止。

消息屏障

在Handler中其实有三种消息:同步消息、异步消息和消息屏障。可以通过MessageQueue中的postSyncBarrier来发送一个消息屏障。方法是私有的,需要通过反射去调用。消息屏障就是说发送了一条target为null的消息,这样当MessageQueue取到这条消息的时候,由于他没有设置target,导致不知道发给谁,这样就阻塞了他后面所有的同步消息,对异步消息没影响。移除的话就调用removeSyncBarrier。

异步消息

异步消息其实和同步消息没有区别,只是在发送Message之前,需要调用Message的setAsynchronous传入true,将其设置为异步消息。在正常情况下执行的之后和同步消息时没区别的,但是如果是发送了消息屏障之后,同步消息就没法执行了,只能执行异步消息。

他可以用来某些需要高优先级的情况,比如更新UI,需要优先处理。

IdelHandler

				MessageQueue messageQueue = Looper.myQueue();
        messageQueue.addIdleHandler(new MessageQueue.IdleHandler() {
            @Override
            public boolean queueIdle() {
                return false;
            }
        });

当MessageQueue为空的或者是还没到消息的执行时间的时候,就会调用queueIdle方法进行操作。

例如,当启动Activity时,需要延时执行一些操作,以免启动过慢,我们常常使用以下方式延迟执行任务,但是在延迟时间上却不好控制。

Handler.Callback

Handler的很多构造方法中都要求传入Callback,那么他的作用是啥?

我们可以从dispatchMessage这个方法中看出端倪,

public void dispatchMessage(Message msg) {
  //这里的 callback 是 Runnable
  if (msg.callback != null) {
    handleCallback(msg);
  } else {
    //如果 callback 处理了该 msg 并且返回 true, 就不会再回调 handleMessage
    if (mCallback != null) {
      if (mCallback.handleMessage(msg)) {
        return;
      }
    }
    handleMessage(msg);
  }
}

我们可以看到,如果设置了Callback,那么就有优先处理消息的权利。当一条消息被Callback处理并拦截了,那么就不会执行接下来的handleMessage方法了;而如果callback.handleMessage返回false,那么就可以继续执行handleMessage方法。

通过Callback,我们可以很好的拦截该Handler的所有消息,然后根据情况,判断是callback自己处理,还是继续交给Handler的handleMessage处理。

Activity中使用Handler

就是如果你直接创建Handler对象并重写handleMessage方法的话,AS一般都会报一个warning:

This Handler class should be static or leaks might occur.
该处理程序类应为静态,否则可能发生泄漏

因为Handler允许我们发送延时消息,但是如果在延时期间,用户关闭了Activity。这时Message持有Handler,而又因为Java的特性,内部类会持有外部类,也就是说Handler会持有Activity,这样就导致Activity泄漏了。

解决办法就是

  1. 把Handler定义为静态内部类,并在内部持有Activity的弱引用,并及时移除所有消息。
  2. Activity Destroy的时候调用mHandler.removeCallbackAndMessages()方法移除掉所有的消息。
private static class SafeHandler extends Handler {
    private WeakReference<MainActivity> ref;

    public SafeHandler(MainActivity activity) {
        this.ref = new WeakReference<>(activity);
    }

    @Override
    public void handleMessage(@NonNull Message msg) {

    }
}

Handler native层

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tObC3q3T-1615848923423)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5a332389-5dff-4bce-a073-00dfabfd745c/Untitled.png)]

nativeInit()

在Handler中,Java中的MessageQueue中有一个属性叫做ptr,这个属性保存着native层中NativeMessageQueue的地址。然后再Lopper.prepare调用Looper的构造函数的时候,会自动创建MessageQueue对象,然后再MessageQueue的构造方法中,会调用native层的nativeInit()方法去初始化native层的nativeMessageQueue并将地址值返回。在构造nativeMessageQueue的时候,这块地方和Java不同,他是通过去构建nativeMessageQueue的对象来去构建Looper的对象。这样就将native层的Looper和MessageQueue绑定起来了。

然后再Looper的构造方法中,我们去创建了epoll实例(epoll是Linux新的IO多路复用的机制,相交于以前老的select和poll机制通过轮询的方式去扫描文件描述符去查找哪些句柄发生了时间,他是通过红黑树去存储的句柄,所以他搜索起来更快),并注册了wake管道。并为epoll设置一个唤醒机制。

nativeDestroy()

然后如果我们在java层中调用MessageQueue的dispose()方法的话,就会去调用nativeDestroy方法,他就会获取到我们在MessageQueue存储的ptr并获取到ptr对应的nativeMessageQueue,然后再调用nativeMessageQueue的decStrong方法去释放对象资源。

nativePollOnce()

如果调用了java层的Looper.next方法,那就会去调用nativePollOnce方法,同样的,nativePollOnce也是首先会拿到nativeMessageQueue指针地址,然后去调用他的pollOnce方法,在pollOnce方法中直接调用了Loper.pollOnce方法,在这个方法中有调用了pollInner方法,在pollInner方法中,我们首先调用epoll_wake方法去向管道写端写入字符,然后去处理所有的事件,首先会遍历所有的request,从管道去读取,一并放入response数组,然后再处理native的message,并调用相应的回调方法,处理完message再去处理刚刚我们放入response数组的request。

HandlerThread

HandlerThread是Thread的一个子类,类似于Handler+Thread。HandlerThread自带一个Looper使得他可以通过消息队列来重复使用当前线程,节省资源开销。但是他也存在的问题就是,每一个任务都以队列的形式执行,导致如果有某一个任务执行时间过长的时候,后面的任务就只能等他执行完毕再执行。

HandlerThread thread = new HandlerThread("MyHandlerThread");
thread.start();
Handler mHandler= new Handler( thread.getLooper() ) {
    @Override
    public boolean handleMessage(Message msg) {
        //消息处理
        return true;
    }
});
mHandler.sendMessage(message);
mHandler.post(new Runnable(){...});
mHandlerThread.quit()

他和普通线程的区别就是他在内部维护了一个looper(也正是这个原因所以他可以创建对应的Handler),然后就会通过Handler的方式来通过Looper执行下一个任务。

由于内部的Looper是一个无限循环,所以当我们不再使用HandlerThread的时候就需要调用quit方法来终止他。

优势:

  1. 将loop运行在子线程中处理,减轻了主线程的压力,使主线程更流畅
  2. 串行执行,开启一个线程起到多个线程的作用
  3. 有自己的消息队列,不会干扰UI线程

劣势:

  1. 由于每一个任务队列逐步执行,一旦队列耗时过长,消息延时
  2. 对于IO等操作,线程等待,不能并发

面试问题总结

1. 为什么主线程调用了Looper的loop方法主线程却不会卡死

对于主线程,我们是绝不希望会被运行一段时间,自己就退出,那么如何保证能一直存活呢?简单做法就是可执行代码是能一直执行下去的,死循环便能保证不会被退出。

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

那么别人为什么能主线程发送消息

会在进入死循环之前便创建了新binder线程,

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),除了图中画的线程,其中还有很多线程

2. Handler为什么会导致内存泄漏

一般情况下,我们在主线程创建Handler一般都是通过内部类的形式,但是由于Java的特性,非静态内部类会持有外部类的引用;通过我们在通过Handler往MessageQueue中发送消息的Message的target字段会持有处理他的Handler的引用。

其实一般情况下是不会内存泄漏的,但是当我们发送了一条延迟消息,并且在倒计时还没结束时,也就是还没到执行时间的时候,我们退出了当前的Activity,所以虚拟机需要将Activity GC掉,但是又由于内部类Handler持有着Activity的引用,而MessageQueue中的Message也持有者Handler

的引用,这样对Activity来说他是GC Root可达的,也就代表这GC清除不了,所以这样就导致了内存泄漏。

解决办法

1. 静态内部类+弱引用

将Handler声明为静态的内部类,这样他就不会持有外部类Activity的引用,这样就不会出现上述的问题。而如果需要在Handler使用到Activity的话,就声明为弱引用。

2. 手动清除所有的消息

就是重写Activity的onDestroy方法,在onDestroy时调用handler的removeCallbackAndMessages方法,这样就会将MessageQueue中target为当前Handler的消息全部删掉。

3. View的post和Handler的post的区别

表面上看,View的post也是调用了Handler的post来实现的,只不过他此处会多一个判断,也就是如果view还没加入到window中,此时会导入ViewRootImpl的RunQueue中去。

而放入到这里面之后,就与Handler机制无关了,他会在performTraversals的时候,把RunQueue中的Runnable拿出来执行。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值