Handler的知识总结

192 阅读5分钟

本篇是由我学习并总结,有不对的地方多多指正。

Handler的使用

Handler的使用很简单,有匿名内部类的方式,静态内部类的方式等等。
常见的大概就是这两种吧,一会也将对比他们之间的不同

private val handler = object :Handler(Looper.getMainLooper()){

}

private val handler2 = MainActivity.TestHandler(this)

class TestHandler(activity: Activity) : Handler(Looper.getMainLooper()) {
    private val weakActivity: WeakReference<Activity> = WeakReference(activity)
    override fun handleMessage(msg: Message) {
        super.handleMessage(msg)
    }
}

此时我们已经获取到了Handler,那么就可以post发送一个Runnable,或者sendMessage来发送消息,或从子线程中更新UI了。

handler.sendMessage(Message())
handler.post { 
    gotoDo()
}

既然已经了解到使用了,那为什么handler就可以将线程切换到主线程?

Handler的消息处理分析

首先要清楚Handler的组成,分别有Looper,MessageQueue,Message以及Handler。 他们之间的持有关系则是 Looper->MessageQueue->Message->Handler,那handler既然是发送数据的,为什么Message会持有Handler的引用?这是接下来要说的。

如何获取到Looper

仔细查看Looper的源码,会发现Looper的实例化其实是私有的,也就是外界并没有实例化Looper的办法。

Looper.java

private Looper(boolean quitAllowed) {
    mQueue = new MessageQueue(quitAllowed);
    mThread = Thread.currentThread();
}

那就得看官方提供的有哪些方法可以实例化,毕竟无法实例化那怎么处理数据,在观察后发现,Looper提供了静态方法Looper.prepare用于初始化Looper。

Looper.java

static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
/** Initialize the current thread as a looper.
  * This gives you a chance to create handlers that then reference
  * this looper, before actually starting the loop. Be sure to call
  * {@link #loop()} after calling this method, and end it by calling
  * {@link #quit()}.
  */
public static void prepare() {
    prepare(true);
}

private static void prepare(boolean quitAllowed) {
    //当该线程已经存在Looper执行者,那就会抛出异常
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    sThreadLocal.set(new Looper(quitAllowed));
}

其中有一个静态的sThreadLocal来保证每个线程只有一个Looper,来保持消息处理无误。并在上面Looper的实例化中看到,实例化了MessageQueue消息队列。 此刻可能还会有疑惑,那既然Looper需要初始化才可以进行处理消息,为什么我们并没有做任何操作,主线程的Looper就已经存在了?

ActivityThread.java

public static void main(String[] args) {

    Looper.prepareMainLooper();
    .....
    ActivityThread thread = new ActivityThread();
    thread.attach(false, startSeq);
    .....
    Looper.loop();

    throw new RuntimeException("Main thread loop unexpectedly exited");
}

看了ActivityThread的main方法就懂了,Android程序与Java程序其实并无差别,都需要一个main方法才可以运行,而Android只是进行了层层封装,使得我们可以不用管这些,只需要处理好自己的业务逻辑

Handler发送消息或post一个runnable到了哪?

现在追踪sendMessage的方法,来看看我们的message具体到了哪

Handler.java


public final boolean sendMessage(@NonNull Message msg) {
    //发送一个延时为0的消息
    return sendMessageDelayed(msg, 0);
}
public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
    if (delayMillis < 0) {
        delayMillis = 0;
    }
    //发送消息,该消息将在什么时候执行
    return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
    MessageQueue queue = mQueue;
    if (queue == null) {
        RuntimeException e = new RuntimeException(
                this + " sendMessageAtTime() called with no mQueue");
        Log.w("Looper", e.getMessage(), e);
        return false;
    }
    //加入消息队列
    return enqueueMessage(queue, msg, uptimeMillis);
}

private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
        long uptimeMillis) {
    msg.target = this;
    msg.workSourceUid = ThreadLocalWorkSource.getUid();

    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    //此刻才是真正的在queue中添加到了队列
    return queue.enqueueMessage(msg, uptimeMillis);
}

而在此处也正好看到了,在enqueueMessage时,会将消息的target指向发送消息的Handler,而此时Message被加入到MessageQueue中。这就解释了为什么MessageQueue在持有Message的同时,会持有发送消息的Handler。恩,这些都懂,但为什么Message需要持有发送他的Handler?这个一会再说,我们接着看post方法。

//发送一个runnable,在下面仍然会返回sendMessageDelayed,与上面相同,所以不再罗列
//重点看看getPostMessage(r)
public final boolean post(@NonNull Runnable r) {
   return  sendMessageDelayed(getPostMessage(r), 0);
}
//为message的callBack设置为传递过来的runnable
private static Message getPostMessage(Runnable r) {
    Message m = Message.obtain();
    m.callback = r;
    return m;
}

也就是到最后,都会进入到enqueueMessage方法,添加到MessageQueue中。

MessageQueue的添加队列

在添加到MessageQueue时,MessageQueue都做了什么?

boolean enqueueMessage(Message msg, long when) {
    //如果target为空,抛出异常
    if (msg.target == null) {
        throw new IllegalArgumentException("Message must have a target.");
    }

    synchronized (this) {
        ......
        msg.markInUse();
        msg.when = when;
        Message p = mMessages;
        boolean needWake;
        //如果当前消息队列为空,或者现在发送的非延时消息,又或者当前的时间节点小于第一个的时间节点(也就是如果第一个是需要2秒后处理,而现在要插入的是及时处理)
        if (p == null || when == 0 || when < p.when) {
            // New head, wake up the event queue if blocked.
            //那么把当前发送的消息的next指向消息队列的头部
            msg.next = p;
            //并将当前的消息队列重新设置为发送的
            mMessages = msg;
            needWake = mBlocked;
        } else {
            // Inserted within the middle of the queue.  Usually we don't have to wake
            // up the event queue unless there is a barrier at the head of the queue
            // and the message is the earliest asynchronous message in the queue.
            needWake = mBlocked && p.target == null && msg.isAsynchronous();
            //prev 可以理解为左半的消息队列
            Message prev;
            for (;;) {
                prev = p;
                p = p.next;
                //当p==null也就是消息遍历完,或者当前的时间小于下个要处理的时间,退出
                if (p == null || when < p.when) {
                    break;
                }
                //判断是否要唤醒Looper处理
                if (needWake && p.isAsynchronous()) {
                    needWake = false;
                }
            }
            msg.next = p; // invariant: p == prev.next
            prev.next = msg;
            
            
        }

        // We can assume mPtr != 0 because mQuitting is false.
        if (needWake) {
            nativeWake(mPtr);
        }
    }
    return true;
}

从这里可以看出,一直说的消息队列竟然是个链表,还是个单向链表。 这里可能会有很多同学第一个会迷住,这样处理会有啥结果啊?为什么要这样处理?不要急,这就分析一波,首先分析第一个if判断语句,这三个都会将当前消息放在链表的头部,作为下个首要处理的消息。

image.png 接下来看插入到对应时间,是怎么做到的,首先假设好原给的消息队列和要插入的消息,

image.png 可能会说在执行后Prev已经指向的是第二个消息,为什么到最后还能连起来,因为mMessages仍然指向链表,而计算出Prev和p,只是为了让消息更好的插入进入,毕竟它只是个单向链表。 image.png

Looper的处理消息

在Looper.loop方法中可以看到,在这里形成了死循环在处理消息,而其中最重要的就是queue的next()取消息了。

Looper.java
for (;;) {
    if (!loopOnce(me, ident, thresholdOverride)) {
        return;
    }
}

private static boolean loopOnce(final Looper me,
        final long ident, final int thresholdOverride) {
    Message msg = me.mQueue.next(); // might block
    ......

    //消息分发
   msg.target.dispatchMessage(msg);

    }

在取到消息后,就正式开始处理,并在一系列后,进入到msg.target.dispatchMessage中处理消息,这也就意味着为什么message需要持有一个target,因为在具体处理时,可以进行回调到发送message的Handler的handleMessage进行消息的处理。
来看看dispatchMessage都做了什么吧

public void dispatchMessage(@NonNull Message msg) {
    //当回调不为空时,这个指的就是 runnable事件
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        //调用handler的hanldMessage
        handleMessage(msg);
    }
}

自此Handler消息发送到处理的流程就走完了。 别急,还有更深入的内容。

为什么Looper是死循环不会ANR

首先,我们要知道MessageQueue的next取消息是一个阻塞方法,当首个消息需要延时,或无消息时, nativePollOnce(ptr, nextPollTimeoutMillis)进行阻塞,尤其是无消息时,会传入-1,表示无限期阻塞,这就相当于 Scanner(System.in),但一直阻塞怎么办?新增加的消息不执行?
并不是的,什么时候消息会发生改变,只有在enqueueMessage时,添加消息到消息队列时,所以在这个方法中可以看到nativeWake来唤醒Looper的阻塞。 并且屏幕的绘制、页面刷新等都依靠Looper,loop的停止运行才会导致整个app的崩溃。

throw new RuntimeException("Main thread loop unexpectedly exited");

为什么匿名内部类的方式会内存泄漏

学过java的都知道,只要是非静态的内部类,都会默认持有外部类的引用,比如可以直接获取外部类的属性等。那message中持有了Handler,当Handler为非静态时,同样也会持有Activity的引用。

那么一定会内存泄漏吗?

不一定,如果只是简单的消息任务,那么在执行后,并无大碍。但是在handler发送了一个延时的消息,并且在activity销毁时,延时消息还没有执行,就可能会引起内存泄漏,activity被持有,无法释放。

怎么解决?

最简单的方式就是,创建一个静态内部类,并在内部声明一个弱引用的activity对象,弱引用可以在GC时,无论内存是否充足,都会回收弱引用。

private val handler2 = MainActivity.TestHandler(this)

class TestHandler(activity: Activity) : Handler(Looper.getMainLooper()) {
    private val weakActivity: WeakReference<Activity> = WeakReference(activity)
    override fun handleMessage(msg: Message) {
        super.handleMessage(msg)
    }
}

Looper怎么就做到了切换线程?

其实Looper什么都没做,他只是在它自己的线程中不断的循环取出消息,处理消息。因为在同一进程中,不同线程之间的内存是共享的,无论在哪个线程中发送消息,Looper都能够收到唤醒且处理。

Handler异步消息(同步屏障)

有人可能有疑问,Handler不是从子线程调回到主线程处理消息?怎么还有异步消息了?
首先我们需要知道,异步消息是为了更好的处理UI绘制和更新的。哦?这怎么说。
首先如果当前已有很多的消息都在MessageQueue中,虽然都没有延时消息,但是胜在多,而屏幕为60HZ的,将会在16ms刷新一次,消息过多,而没有及时处理绘制UI的消息,那结果会怎么样,屏幕卡顿掉帧。
熟悉的scheduleTraversals就是如此,

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        //开启同步屏障
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        //发送异步消息
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}
@UnsupportedAppUsage
@TestApi
public int postSyncBarrier() {
    return postSyncBarrier(SystemClock.uptimeMillis());
}

private int postSyncBarrier(long when) {
    // Enqueue a new sync barrier token.
    // We don't need to wake the queue because the purpose of a barrier is to stall it.
    synchronized (this) {
        final int token = mNextBarrierToken++;
        final Message msg = Message.obtain();
        msg.markInUse();
        msg.when = when;
        msg.arg1 = token;

        Message prev = null;
        Message p = mMessages;
        if (when != 0) {
            while (p != null && p.when <= when) {
                prev = p;
                p = p.next;
            }
        }
        //将target为空的msg放在链表头部
        if (prev != null) { // invariant: p == prev.next
            msg.next = p;
            prev.next = msg;
        } else {
            msg.next = p;
            mMessages = msg;
        }
        return token;
    }
}
void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        //在处理异步消息时,取消同步屏障
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

        if (mProfile) {
            Debug.startMethodTracing("ViewAncestor");
        }

        performTraversals();

        if (mProfile) {
            Debug.stopMethodTracing();
            mProfile = false;
        }
    }
}

在开启同步屏障后,如果不手动关闭,将无法再处理同步消息,用户态好像并不可以开启同步屏障。
而在上面的处理中发现,开启同步屏障,就是生成了一个Message,并将其target不设置值也就是null,这样就等于开启了同步屏障。在取数据时,就有了以下的判断.

if (msg != null && msg.target == null) {
    // 当有同步屏障后,取出第一个异步消息
    do {
        prevMsg = msg;
        msg = msg.next;
    } while (msg != null && !msg.isAsynchronous());
}

那既然target == null就是同步屏障,那我们直接设置null不行嘛?不行的,首先会在Handler的enqueueMessage中设置消息的target,其实在MessageQueue中的enqueueMessage中,判断target == null就会直接抛出异常。

if (msg.target == null) {
    throw new IllegalArgumentException("Message must have a target.");
}

那我们如何发布异步消息呢?

在之前API并没有公开,而在现在用户可以使用Handler.createAsync()就可以返回一个,异步消息专属的Handler了。
这也并看不出什么啊,怎么就特殊了?异步专属了?来看源码

@NonNull
public static Handler createAsync(@NonNull Looper looper) {
    if (looper == null) throw new NullPointerException("looper must not be null");
    return new Handler(looper, null, true);
}
@UnsupportedAppUsage
public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) {
    mLooper = looper;
    mQueue = looper.mQueue;
    mCallback = callback;
    //重要参数
    mAsynchronous = async;
}

在实例化后,我们发布消息,再来看看enqueMessage方法。

private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
        long uptimeMillis) {
    msg.target = this;
    msg.workSourceUid = ThreadLocalWorkSource.getUid();
    //在判断mAsynchronous为true时,设置msg的Asynchronous为true,这样就和上面的对应
    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}

而在next取消息时,也能看到,如果开启了同步屏障,就会找第一个msg的Asynchronous为true的消息。

if (msg != null && msg.target == null) {
    // Stalled by a barrier.  Find the next asynchronous message in the queue.
    do {
        prevMsg = msg;
        msg = msg.next;
    } while (msg != null && !msg.isAsynchronous());
}

结尾

以上呢,就是看Handler源码的结果,可能还有并没有总结的,希望各位多多批评指正,谢谢。哪里不清楚的,也可以在评论区问我。