• 欢迎访问搞代码网站,推荐使用最新版火狐浏览器和Chrome浏览器访问本网站!
  • 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏搞代码吧

源码深度解析-Handler-机制及应用

android 搞代码 3年前 (2022-03-02) 30次浏览 已收录 0个评论
文章目录[隐藏]

本文以源码剖析+理论利用的模式,具体解说了 Handler 机制的原理,以及在开发中的应用场景和要留神的中央。

一、基本原理回顾

在 Android 开发中,Handler及相干衍生类的利用常常用到,Android的运行也是建设在这套机制上的,所以理解其中的原理细节,以及其中的坑对于每位开发者来说都是十分有必要的。Handler机制的五个组成部分:Handler、Thread(ThreadLocal)、Looper、MessageQueue、Message。

1、Thread(ThreadLocal)

Handler机制用到的跟Thread相干的,而根本原因是Handler必须和对应的Looper绑定,而Looper的创立和保留是跟Thread一一对应的,也就是说每个线程都能够创立惟一一个且互不相干的Looper,这是通过ThreadLocal来实现的,也就是说是用ThreadLocal对象来存储Looper对象的,从而达到线程隔离的目标。

<code class="java">static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
 
private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    sThreadLocal.set(new Looper(quitAllowed));
}

2、Handler

<code class="java">Handler()
 
Handler(Callback callback)
 
Handler(Looper looper)
 
Handler(Looper looper, Callback callback)
 
Handler(boolean async)
 
Handler(Callback callback, boolean async)
 
Handler(Looper looper, Callback callback, boolean async)

2.1 创立Handler大体上有两种形式:

一种是不传Looper

这种就须要在创立Handler前,事后调用Looper.prepare来创立以后线程的默认Looper,否则会报错。

一种是传入指定的Looper

这种就是Handler和指定的Looper进行绑定,也就是说Handler其实是能够跟任意线程进行绑定的,不局限于在创立Handler所在的线程里。

2.2 async参数

这里Handler有个async参数,通过这个参数表明通过这个Handler发送的音讯全都是异步音讯,因为在把音讯压入队列的时候,会把这个标记设置到message里.这个标记是全局的,也就是说通过结构Handler函数传入的async参数,就确定了通过这个Handler发送的音讯都是异步音讯,默认是false,即都是同步音讯。至于这个异步音讯有什么非凡的用处,咱们在前面讲了屏障音讯后,再分割起来讲。

<code class="java">private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    msg.target = this;
    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}

2.3 callback参数

这个回调参数是音讯被散发之后的一种回调,最终是在msg调用Handler的dispatchMessage时,依据理论状况进行回调:

<code class="java">public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}

3、Looper

用于为线程运行音讯循环的类。默认线程没有与它们相关联的Looper;所以要在运行循环的线程中调用prepare(),而后调用loop()让它循环解决音讯,直到循环进行。

<code class="java">private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        sThreadLocal.set(new Looper(quitAllowed));
    }
     
    public static void loop() {
        ...
     
        for (;;) {
        ...
        }
         
        ...
    }
 
class LooperThread extends Thread {
    public Handler mHandler;
 
    public void run() {
         
        Looper.prepare();  
         
        mHandler = new Handler() { 
            public void handleMessage(Message msg) {
                 
                Message msg=Message.obtain();
            }
        };
        
        Looper.loop(); 
    }
}

既然在应用Looper前,必须调用prepare创立Looper,为什么咱们平时在主线程里没有看到调用prepare呢?这是因为Android主线程创立的时候,在ActivityThread的入口main办法里就曾经默认创立了Looper。

<code class="java">public static void main(String[] args) {
    ...
    Looper.prepareMainLooper();
    ...
    Looper.loop();
    ...
}

咱们再来回顾一下Looper相干类的之间的分割:

4、MessageQueue 和 Message

MessageQueue是一个音讯队列,Handler将Message发送到音讯队列中,音讯队列会依照肯定的规定取出要执行的Message。Message并不是间接加到MessageQueue的,而是通过Handler对象和Looper关联到一起。

MessageQueue里的message是按工夫排序的,越早退出队列的音讯放在队列头部,优先执行,这个工夫就是sendMessage的时候传过来的,默认是用的以后零碎从启动到当初的非休眠的工夫SystemClock.uptimeMillis()。

sendMessageAtFrontOfQueue 这个办法传入的工夫是0,也就是说调用这个办法的message必定会放到对音讯队列头部,然而这个办法不要轻易用,容易引发问题。

存到MessageQueue里的音讯可能有三种:同步音讯,异步音讯,屏障音讯。

4.1 同步音讯

咱们默认用的都是同步音讯,即后面讲Handler里的结构函数参数的async参数默认是false,同步音讯在MessageQueue里的存和取齐全就是依照工夫排的,也就是通过msg.when来排的。

4.2 异步音讯

异步音讯就是在创立Handler如果传入的async是true或者发送来的Message通过msg.setAsynchronous(true);后的音讯就是异步音讯,异步音讯的性能要配合上面要讲的屏障音讯才无效,否则和同步音讯是一样的解决。

<code class="java">private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    msg.target = this;
    // 这个mAsynchronous就是在创立Handler的时候传入async参数
    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}

4.3 Barrier(屏障)音讯

屏障(Barrier) 是一种非凡的Message,它最大的特色就是target为null(只有屏障的target能够为null,如果咱们本人设置Message的target为null的话会报异样),并且arg1属性被用作屏障的标识符来区别不同的屏障。屏障的作用是用于拦挡队列中同步音讯,放行异步音讯。

那么屏障音讯是怎么被增加和删除的呢?咱们能够看到在MessageQueue里有增加和删除屏障音讯的办法:

<code class="java">
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) {
            // 这里是说如果p指向的音讯工夫戳比屏障音讯小,阐明这个音讯比屏障音讯先进入队列,
            // 那么这个音讯不应该受到屏障音讯的影响(屏障音讯只影响比它后退出音讯队列的音讯),找到第一个比屏障音讯晚进入的音讯指针
            while (p != null && p.when <= when) {
                prev = p;
                p = p.next;
            }
        }
        // 下面找到第一个比屏障音讯晚进入的音讯指针之后,把屏障音讯插入到音讯队列中,也就是屏障音讯指向第一个比它晚进入的音讯p,
        // 上一个比它早进入音讯队列的prev指向屏障音讯,这样就实现了插入。
        if (prev != null) { // invariant: p == prev.next
            msg.next = p;
            prev.next = msg;
        } else {
        // 如果prev是null,阐明下面没有通过挪动,也就是屏障音讯就是在音讯队列的头部了。
            msg.next = p;
            mMessages = msg;
        }
        return token;
    }
}
 
public void removeSyncBarrier(int token) {
    // Remove a sync barrier token from the queue.
    // If the queue is no longer stalled by a barrier then wake it.
    synchronized (this) {
        Message prev = null;
        Message p = mMessages;
        // 后面在插入屏障音讯后会生成一个token,这个token就是用来删除该屏障音讯用的。
        // 所以这里通过判断target和token来找到该屏障音讯,从而进行删除操作
        // 找到屏障音讯的指针p
        while (p != null && (p.target != null || p.arg1 != token)) {
            prev = p;
            p = p.next;
        }
        if (p == null) {
            throw new IllegalStateException("The specified message queue synchronization "
                    + " barrier token has not been posted or has already been removed.");
        }
        final boolean needWake;
        // 下面找到屏障音讯的指针p后,把前一个音讯指向屏障音讯的后一个音讯,这样就把屏障音讯移除了
        if (prev != null) {
            prev.next = p.next;
            needWake = false;
        } else {
            mMessages = p.next;
            needWake = mMessages == null || mMessages.target != null;
        }
        p.recycleUnchecked();
 
        // If the loop is quitting then it is already awake.
        // We can assume mPtr != 0 when mQuitting is false.
        if (needWake && !mQuitting) {
            nativeWake(mPtr);
        }
    }
}

4.4 屏障音讯的作用

说完了屏障音讯的插入和删除,那么屏障音讯在哪里起作用的?它跟后面提到的异步音讯又有什么关联呢?咱们能够看到MessageQueue的next办法里有这么一段:

<code class="java">// 这里就是判断以后音讯是否是屏障音讯,判断根据就是msg.target==null, 如果存在屏障音讯,那么在它之后进来的音讯中,
// 只把异步音讯放行继续执行,同步音讯阻塞,直到屏障音讯被remove掉。
if (msg != null && msg.target == null) {
    // Stalled by a barrier.  Find the next asynchronous message in the queue.
    do {
        prevMsg = msg;
        msg = msg.next;
        // 这里的isAsynchronous办法就是后面设置进msg的async参数,通过它判断如果是异步音讯,则跳出循环,把该异步音讯返回
        // 否则是同步音讯,把同步音讯阻塞。
    } while (msg != null && !msg.isAsynchronous());
}

4.5 屏障音讯的理论利用

屏障音讯的作用是把在它之后入队的同步音讯阻塞,然而异步音讯还是失常按程序取出执行,那么它的理论用处是什么呢?咱们看到ViewRootImpl.scheduleTraversals()用到了屏障音讯和异步音讯。

TraversalRunnable的run(),在这个run()中会执行doTraversal(),最终会触发View的绘制流程:measure(),layout(),draw()。为了让绘制流程尽快被执行,用到了同步屏障技术。

<code class="java">void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        // 这里先将主线程的MessageQueue设置了个音讯屏障
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        // 这里发送了个异步音讯mTraversalRunnable,这个mTraversalRunnable最终会执行doTraversal(),也就是会触发View的绘制流程
        // 也就是说通过设置屏障音讯,会把主线程的同步音讯先阻塞,优先执行View绘制这个异步音讯进行界面绘制。
        // 这很好了解,界面绘制的工作必定要优先,否则就会呈现界面卡顿。
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}
 
private void postCallbackDelayedInternal(int callbackType,
        Object action, Object token, long delayMillis) {
    if (DEBUG_FRAMES) {
        Log.d(TAG, "PostCallback: type=" + callbackType
                + ", action=" + action + ", token=" + token
                + ", delayMillis=" + delayMillis);
    }
 
    synchronized (mLock) {
        final long now = SystemClock.uptimeMillis();
        final long dueTime = now + delayMillis;
        mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
 
        if (dueTime <= now) {
            scheduleFrameLocked(now);
        } else {
            Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
            msg.arg1 = callbackType;
            // 设置该音讯是异步音讯
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, dueTime);
        }
    }
}

4.6咱们能用屏障音讯做什么?

那么除了零碎中应用到了屏障音讯,咱们在开发中有什么场景能派上用场吗? 使用屏障音讯能够阻塞同步音讯的个性,咱们能够用来实现UI界面初始化和数据加载同时进行。

咱们个别在Activity创立的时候,为了缩小空指针异样的产生,都会在onCreate先setContent,而后findView初始化控件,而后再执行网络数据加载的异步申请,待网络数据加载实现后,再刷新各个控件的界面。

试想一下,怎么利用屏障音讯的个性来达到界面初始化和异步网络数据的加载同时进行,而不影响界面渲染?先来看一个时序图:

咱们通过上面伪代码进一步加深了解:

<code class="java">
// 在上一个页面里异步加载下一个页面的数据
 // 网络申请返回的数据
    Data netWorkData;
    // 创立屏障音讯会生成一个token,这个token用来删除屏障音讯,很重要。
    int barrierToken;
 
    // 创立异步线程加载网络数据
    HandlerThread thread = new HandlerThread("preLoad"){
        @Override
        protected void onLooperPrepared() {
            Handler mThreadHandler = new Handler(thread.getLooper());
            // 1、把申请网络耗时音讯推入音讯队列
            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    // 异步耗时操作:网络申请数据,赋值给netWorkData
                    netWorkData = xxx;
 
                }
            });
 
            // 2、而后给异步线程的队列发一个屏障音讯推入音讯队列
            barrierToken = thread.getLooper().getQueue().postSyncBarrier();
 
            // 3、而后给异步线程的音讯队列发一个刷新UI界面的同步音讯
            // 这个音讯在屏障音讯被remove前得不到执行的。
            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    // 回调主线程, 把netWorkData赋给监听办法,刷新界面
 
                }
            });
        }
    };
    thread.start();
 
 
// 以后界面初始化界面
protected void onCreate(Bundle savedInstanceState) {
    setContentView(view);
 
    // 各种findview操作实现
    Button btn = findViewById(R.id.xxx);
    ...
    // 4、待控件初始化实现,把异步线程设置的屏障音讯remove掉,这样异步线程申请数据实现后,3、处的刷新UI界面的同步音讯就有机会执行,就能够平安得刷新界面了。
    thread.getLooper().getQueue().removeSyncBarrier(barrierToken);
}

然而,MessageQueue源码里咱们咱们看到,屏障音讯的创立和删除都是暗藏办法(@hide),咱们没法间接调用,只能用反射来调用,所以在理论应用中还得综合验证。

4.7 IdleHandler及利用

IdleHandler,字面意思就是闲暇的处理器(就是说我是在音讯队列闲暇的时候才会执行的,如果音讯队列里有其余非IdleHandler音讯在执行,则我先不执行),它其实就是一个接口,咱们就认为它是闲暇音讯吧,只不过它不是存在MessageQueue里,而是以数组的模式保留的。

public static interface IdleHandler {
    /**
     * Called when the message queue has run out of messages and will now
     * wait for more.  Return true to keep your idle handler active, false
     * to have it removed.  This may be called if there are still messages
     * pending in the queue, but they are all scheduled to be dispatched
     * after the current time.
     */
    boolean queueIdle();
}

咱们看到MessageQueue有增加和删除IdleHandler的办法,IdleHandler被保留在一个ArrayList里:

<code class="java">private final ArrayList<IdleHandler> mIdleHandlers = new ArrayList<IdleHandler>();
 
...
 
public void addIdleHandler(@NonNull IdleHandler handler) {
    if (handler == null) {
        throw new NullPointerException("Can't add a null IdleHandler");
    }
    synchronized (this) {
        mIdleHandlers.add(handler);
    }
}
 
public void removeIdleHandler(@NonNull IdleHandler handler) {
    synchronized (this) {
        mIdleHandlers.remove(handler);
    }
}

那么,它是怎么实现在音讯队列闲暇的间隙失去执行的呢?还是看next()办法。

<code class="java">// If first time idle, then get the number of idlers to run.
// Idle handles only run if the queue is empty or if the first message
// in the queue (possibly a barrier) is due to be handled in the future.
// pendingIdleHandlerCount < 0是说for循环只执行第一次
// mMessages == null || now < mMessages.when) 是说以后音讯队列没有音讯或者要执行的音讯晚于以后工夫
// 阐明当初音讯队列处于闲暇。
if (pendingIdleHandlerCount < 0
        && (mMessages == null || now < mMessages.when)) {
    pendingIdleHandlerCount = mIdleHandlers.size();
}
if (pendingIdleHandlerCount <= 0) {
    // No idle handlers to run.  Loop and wait some more.
    mBlocked = true;
    continue;
}

在下面这段代码断定以后音讯队列处于闲暇后,就会拿到闲暇音讯的大小,上面这段代码就是把把闲暇音讯执行一遍。

<code class="java">// Run the idle handlers.
// We only ever reach this code block during the first iteration.
for (int i = 0; i < pendingIdleHandlerCount; i++) {
    final IdleHandler idler = mPendingIdleHandlers[i];
    mPendingIdleHandlers[i] = null; // release the reference to the handler
 
    boolean keep = false;
    try {
        // 如果queueIdle返回true,则该闲暇音讯不会被主动删除,在下次执行next的时候,如果还呈现队列闲暇,会再次执行。
        // 如果返回false,则该闲暇音讯会在执行完后,被主动删除掉。
        keep = idler.queueIdle();
    } catch (Throwable t) {
        Log.wtf(TAG, "IdleHandler threw exception", t);
    }
 
    if (!keep) {
        synchronized (this) {
            mIdleHandlers.remove(idler);
        }
    }
}
 
// Reset the idle handler count to 0 so we do not run them again.
// 这里把闲暇音讯标记置为0,而不置为-1,就是说本次曾经解决完,避免for循环反复执行,影响其余音讯的执行
pendingIdleHandlerCount = 0;
 
// While calling an idle handler, a new message could have been delivered
// so go back and look again for a pending message without waiting.
nextPollTimeoutMillis = 0;

总结一下:

  • 如果本次循环拿到的音讯为空,或者这个音讯是一个延时的音讯而且还没到指定的触发工夫,那么,就认定以后的队列为闲暇状态。
  • 接着就会遍历mPendingIdleHandlers数组(这个数组外面的元素每次都会到mIdleHandlers中去拿)来调用每一个IdleHandler实例的queueIdle办法, 如果这个办法返回false的话,那么这个实例就会从mIdleHandlers中移除,也就是当下次队列闲暇的时候,不会持续回调它的queueIdle办法了。
  • 解决完IdleHandler后会将nextPollTimeoutMillis设置为0,也就是不阻塞音讯队列,当然要留神这里执行的代码同样不能太耗时,因为它是同步执行的,如果太耗时必定会影响前面的message执行。

IdleHandler的原理大略就是下面讲的那样,那么能力决定用途,从实质上讲就是趁着音讯队列闲暇的时候干点事件,具体做什么,是在IdleHandler的queueIdle()办法里。那么IdleHandler在零碎源码里应用场景是怎么的?咱们能够看到它在主线程生命周期解决中应用比拟多,比方在ActivityThread里有个 就有一个名叫GcIdler的外部类,实现的就是IdleHandler接口,它的作用就是在主线程闲暇的时候对内存进行强制GC。

<code class="java">final class GcIdler implements MessageQueue.IdleHandler {
    @Override
    public final boolean queueIdle() {
        doGcIfNeeded();
        return false;
    }
}
 
 
 
// 这里的意思就是说判断间隔上次GC的工夫是否超过5秒,超过则执行后盾强制GC
void doGcIfNeeded() {
    mGcIdlerScheduled = false;
    final long now = SystemClock.uptimeMillis();
    //Slog.i(TAG, "**** WE MIGHT WANT TO GC: then=" + Binder.getLastGcTime()
    //        + "m now=" + now);
    if ((BinderInternal.getLastGcTime()+MIN_TIME_BETWEEN_GCS) < now) {
        //Slog.i(TAG, "**** WE DO, WE DO WANT TO GC!");
        BinderInternal.forceGc("bg");
    }
}

咱们看看它是在哪里增加到音讯队列的:

<code class="java">// 这个办法是在mH的handleMessage办法里调的,也就是说也是通过AMS(ActivityManagerService)把音讯发送到主线程音讯队列
void scheduleGcIdler() {
    if (!mGcIdlerScheduled) {
        mGcIdlerScheduled = true;
        Looper.myQueue().addIdleHandler(mGcIdler);
    }
    mH.removeMessages(H.GC_WHEN_IDLE);
}

还有就是在ActivityThread的performLaunchActivity办法执行时,最终会执行到Instrumentation.callActivityOnCreate办法,在这个办法里,也有用到IdleHandler做一些额定的事件。

<code class="java">public void callActivityOnCreate(Activity activity, Bundle icicle) {
    prePerformCreate(activity);
    activity.performCreate(icicle);
    postPerformCreate(activity);
}
 
private void prePerformCreate(Activity activity) {
    if (mWaitingActivities != null) {
        synchronized (mSync) {
            final int N = mWaitingActivities.size();
            for (int i=0; i<N; i++) {
                final ActivityWaiter aw = mWaitingActivities.get(i);
                final Intent intent = aw.intent;
                if (intent.filterEquals(activity.getIntent())) {
                    aw.activity = activity;
                    mMessageQueue.addIdleHandler(new ActivityGoing(aw));
                }
            }
        }
    }
}

除此之外,在一些第三方库中都有应用IdleHandler,比方LeakCanary,Glide中有应用到。

那么对于咱们来说,IdleHandler能够有些什么应用场景呢?依据它最外围的原理,在音讯队列闲暇的时候做点事件,那么对于主线程来讲,咱们有很多的一些代码不是必须要追随生命周期办法同步执行的,就能够用IdleHandler,缩小主线程的耗时,也就缩小利用或者Activity的启动工夫。例如:一些第三方库的初始化,埋点尤其是延时埋点上报等,都能够用IdleHandler增加到音讯队列里。

==好了,提个问题:后面咱们说了在主线程创立的main函数里创立了Handler和Looper,回顾了下面的Handler机制的原理,咱们都晓得个别线程执行完就会退出,由零碎回收资源,那Android UI线程也是基于Handler Looper机制的,那么为什么UI线程能够始终常驻?不会被阻塞呢?==

因为Looper在执行loop办法里,是一个for循环,也就是说线程永远不会执行完退出,所以关上APP能够始终显示,Activity的生命周期就是通过音讯队列把音讯一个一个取出来执行的,而后因为MessageQueue的休眠唤醒机制,当音讯队列里没有音讯时,音讯队列会进入休眠,并开释CPU资源,当又有新音讯进入队列时,会唤醒队列,把音讯取出来执行。

二、Handler利用之HandlerThread

HandlerThread实质上是一个Thread,所不同的是,它充分利用了Handler机制,通过在外部创立Looper循环,内部通过Handler把异步工作推送给音讯队列,从而达到不必反复创立多个Thread,即能将多个异步工作排队进行异步执行,它的原理很简略:

<code class="java">@Override
public void run() {
    mTid = Process.myTid();
    Looper.prepare();
    synchronized (this) {
        mLooper = Looper.myLooper();
        notifyAll();
    }
    Process.setThreadPriority(mPriority);
    onLooperPrepared();
    Looper.loop();
    mTid = -1;
}

在线程的run办法里创立了looper循环,这样这个线程不被动quit的话,不会销毁,有音讯则执行音讯,没有音讯依据MessageQueue休眠机制,会开释CPU资源,进入休眠。

应用HandlerThread时,咱们留神到,在创立Handler时,是要传入线程的Looper进行绑定的,所以必须先执行HandlerThread的start办法,因为执行start办法,才会执行HandlerThread的run办法,才会创立线程的Looper,创立Handler传入的Looper才不会是null。

所以咱们个别应用是这样的:

  • 创立HandlerThread后,调用start,而后再创立Handler;
  • 从run办法里咱们看到有个onLooperPrepared()办法,能够实现这个办法,在这个办法里创立Handler,这样就不受start地位的限度了,原理就是认为run办法是在调用start办法后才会执行。

那么怎么回收一个HandlerThread呢?咱们看到HandlerThread里有个quit办法,这个办法最终会调用到MessageQueue的quit办法,从而完结音讯散发,最终终止一个HandlerThread线程。

<code class="java">public boolean quit() {
    Looper looper = getLooper();
    if (looper != null) {
        looper.quit();
        return true;
    }
    return false;
}

三、Handler利用之IntentService

IntentService其实是Service和HandlerThread的结合体,咱们能够看到在onCreate里创立了个HandlerThread并创立了个Handler和该HandlerThread绑定,而后在onStat办法里以音讯的模式发送给HandlerThread执行

<code class="java">@Override
public void onCreate() {
    // TODO: It would be nice to have an option to hold a partial wakelock
    // during processing, and to have a static startService(Context, Intent)
    // method that would launch the service & hand off a wakelock.
 
    super.onCreate();
    // 创立HandlerThread
    HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
    thread.start();
 
    // 创立Handler和HandlerThread绑定
    mServiceLooper = thread.getLooper();
    mServiceHandler = new ServiceHandler(mServiceLooper);
}
 
@Override
public void onStart(@Nullable Intent intent, int startId) {
    Message msg = mServiceHandler.obtainMessage();
    msg.arg1 = startId;
    msg.obj = intent;
    // 想HandlerThread的音讯队列发送音讯
    mServiceHandler.sendMessage(msg);
}

最终在handleMessage里执行

<code class="java">private final class ServiceHandler extends Handler {
        public ServiceHandler(Looper looper) {
            super(looper);
        }
 
        @Override
        public void handleMessage(Message msg) {
            onHandleIntent((Intent)msg.obj);
            stopSelf(msg.arg1);
        }
    }

所以咱们应用IntentService都必须实现onHandleIntent这个形象办法,在这个形象办法里做具体的业务操作。

咱们都晓得IntentService在执行完异步工作后,会主动销毁,这是怎么实现的?

<code class="java">public ServiceHandler(Looper looper) {
        super(looper);
    }
 
    @Override
    public void handleMessage(Message msg) {
        onHandleIntent((Intent)msg.obj);
        // 答案在这里:在这里会进行Service
        stopSelf(msg.arg1);
    }
}
 
// 而后在onDestory里会终止掉音讯循环,从而达到销毁异步线程的目标:
@Override
public void onDestroy() {
    mServiceLooper.quit();
}

四、Handler.post和View.post

咱们先来看个大家平时常常应用的案例:获取View的宽高。

<code class="java">@Override
protected void onCreate(Bundle savedInstanceState) {
 
    // 地位1
    Log.i("view_w_&_h", "onCreate " + mView.getWidth() + " " + mView.getHeight());
    mView.post(new Runnable() {
        @Override
        public void run() {
        // 地位2
            Log.i("view_w_&_h", "onCreate postRun " + mView.getWidth() + " " + mView.getHeight());
        }
    });
 
    new Handler(Looper.getMainLooper()).post(new Runnable() {
        @Override
        public void run() {
        // 地位3
            Log.i("view_w_&_h", "onCreate Handler " + mView.getWidth() + " " + mView.getHeight());
        }
    });
}
 
@Override
protected void onResume() {
    super.onResume();
    // 地位4
    Log.i("view_w_&_h", "onResume " + mView.getWidth() + " " + mView.getHeight());
 
    new Handler(Looper.getMainLooper()).post(new Runnable() {
        @Override
        public void run() {
            // 地位5
            Log.i("view_w_&_h", "onResume Handler " + mView.getWidth() + " " + mView.getHeight());
        }
    });
}

这几个地位,哪些能获取到mView的宽高?

咱们都晓得在View被attach到window之前,是获取不到View的宽高的,因为这个时候View还没有被Measure、layout、draw,所以在onCreate或者onResume间接调用View的宽高办法,都是0,Handler.post在onCreate里也是获取不到,然而在onResume里能获取到,而View.post无论放在onCreate或者onResume里,都能获取到View的宽高,为什么?

咱们先看个简版的View的绘制流程:

咱们都晓得View的最终绘制是在performTraversals()办法里,包含measure、layout、draw,从下面的图往上追溯,咱们晓得,View的绘制是在ActivityThread的handleResumeActivity办法里,这个办法置信大家不会生疏,这个办法就是会回调Activity的onResume办法的顶级办法。

<code class="java">final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
    ...
    // 这里追溯进去,最终会调用Activity的onStart办法和onResume办法
    r = performResumeActivity(token, clearHide, reason);
     
    ...
     
    // 调用WindowManager的addView办法,这里就是最终执行View绘制的中央
    wm.addView(decor, l);
     
    ...
}

从下面的代码片段执行程序来看,Activity的onStart和onResume被执行的时候,其实界面还没有开始进行绘制(wm.addView(decor, l)还没执行到),这里就能够解释为什么用Handler.post在onCreate里拿不到宽高。因为Handler机制,它是把音讯推送到主线程的音讯队列里去,在onCreate里把音讯推到音讯队列时,onResume的音讯都还没入队,也就没有执行,所以拿不到。那为什么onResume里能拿到呢?因为音讯队列的机制,Handler.post推送的音讯,必须得等上一个音讯执行完能力失去执行,所以它必须得等handleResumeActivity执行完,而handleResumeActivity执行实现后,View曾经绘制实现了,当然就能拿到宽高了。

好了,当初解释第二个疑难,为什么View.post在onCreate里能拿到View的宽高呢?咱们先看下View.post办法:

<code class="java">public boolean post(Runnable action) {
    final AttachInfo attachInfo = mAttachInfo;
    // attachInfo不为null,阐明View曾经被attach到window,也就是实现了绘制,所以间接把音讯推送到主线程的音讯队列执行。
    if (attachInfo != null) {
        return attachInfo.mHandler.post(action);
    }
 
    // Postpone the runnable until we know on which thread it needs to run.
    // Assume that the runnable will be successfully placed after attach.
    // 要害就在这里,走到这里,阐明attachInfo为null,也就是当初View还没attach到window,所以把音讯长期保留到RunQueue里
    getRunQueue().post(action);
    return true;
}

下面咱们能够看到,如果attachInfo为null,则Runnable会长期存储起来,保留到RunQueue里,并没有立刻执行,那么保留到RunQueue是什么时候被执行的呢?

咱们看到HandlerActionQueue有个executeActions办法,这个办法就是用来执行保留其中的Runnable的:

<code class="java">public void executeActions(Handler handler) {
    synchronized (this) {
        final HandlerAction[] actions = mActions;
        for (int i = 0, count = mCount; i < count; i++) {
            final HandlerAction handlerAction = actions[i];
            handler.postDelayed(handlerAction.action, handlerAction.delay);
        }
 
        mActions = null;
        mCount = 0;
    }
}

那么这个办法是在什么机会调用的呢?接着往下看:在View的dispatchAttachedToWindow办法里,咱们看到调用了RunQueue的executeActions,执行保留在RunQueue里的runnable。

<code class="java">void dispatchAttachedToWindow(AttachInfo info, int visibility) {
 
    ...
     
    // Transfer all pending runnables.
    if (mRunQueue != null) {
        mRunQueue.executeActions(info.mHandler);
        mRunQueue = null;
    }
     
    onAttachedToWindow();
     
    ...
}

那么dispatchAttachedToWindow又是在什么时候被调用呢?在ViewRootImpl的performTraversals办法里,咱们看到dispatchAttachedToWindow被执行。host就是DecorView。

<code class="java">private void performTraversals() {
    ...
    host.dispatchAttachedToWindow(mAttachInfo, 0);
    ...
    performMeasure();
    ...
    performLayout();
    ...
    performDraw();
}

从后面的View绘制的UML时序图,咱们晓得,performTraversals是在ActivityThread的handleResumeActivity被调用的。

总结一下:

零碎在执行ActivityThread的handleResumeActivity的办法里,最终会调到ViewRootImpl的performTraversals()办法,performTraversals()办法调用host的dispatchAttachedToWindow()办法,host就是DecorView也就是View,接着在View的dispatchAttachedToWindow()办法中调用mRunQueue.executeActions()办法,这个办法外部会遍历HandlerAction数组,利用Handler来post之前寄存的Runnable。

这里就能够解释为什么View.post在onCreate里同样能够失去View的宽高,是因为View.post收回的音讯,它被执行的机会是在View被绘制之后。

==可能有同学要问了:dispatchAttachedToWindow 办法是在 performMeasure 办法之前调用的,既然在调用的时候还没有执行performMeasure来进行测量,那么为什么在执行完dispatchAttachedToWindow办法后就能够获取到宽高呢?==

还是回到Handler机制最根本的原理,音讯是以队列的模式存在音讯队列里,而后顺次期待Loop执行的,而performTraversals的执行它自身就是在一个Runnable音讯里,所以performTraversals在执行的时候,其余音讯得等performTraversals执行完了能力失去执行,也就是说mRunQueue.executeActions()的音讯必须得等performTraversals彻底执行完能力失去执行,所以View.post(runnable)中的runnable执行是要在performTraversals办法执行完之后的,并非一调用dispatchAttachedToWindow就会执行。

后面还遗留了一个问题:View.post办法里的mAttachInfo是在什么时候赋值的呢?

public ViewRootImpl(Context context, Display display) {
    ...
     
    mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
        context);
    ...
}

咱们看到它是在ViewRootImpl的构造函数里被赋值的,那么ViewRootImpl是什么时候被创立的呢?顺着往上找,咱们看到,它是在WindowManagerGlobal的add办法里被创立的。

<code class="java">public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {
    ...
    ViewRootImpl root;
    ...
    root = new ViewRootImpl(view.getContext(), display);
    ...
}

后面也讲了WindowManagerGlobal的addView办法是在ActivityThread的handleResumeActivity()办法里被执行的,所以问题就解开了,为什么View.post办法里会先判断mAttachInfo是否为空,不为空,阐明View.post的调用机会是在onResume之后,也就是View绘制实现之后,这个时候间接推入主线程音讯队列执行就能够。而如果mAttachInfo为空,阐明View还没绘制完,先暂存起来,待绘制完后再顺次推入主线程执行。

要留神的是View.post办法是有坑的,android版本 < 24,也就是7.0以下的零碎。

<code class="java">// 7.0以下零碎
public boolean post(Runnable action) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        return attachInfo.mHandler.post(action);
    }
    // Assume that post will succeed later
    // 留神此处,不同于我7.0及以上零碎,
    ViewRootImpl.getRunQueue().post(action);
    return true;
}

而咱们看一下 ViewRootImpl 的RunQueue是怎么实现的:

<code class="java">static final ThreadLocal<RunQueue> sRunQueues = new ThreadLocal<RunQueue>();
static RunQueue getRunQueue() {
    RunQueue rq = sRunQueues.get();
    if (rq != null) {
        return rq;
    }
    rq = new RunQueue();
    sRunQueues.set(rq);
    return rq;
}

联合后面讲的ThreadLocal个性,它是跟线程相干的,也就是说保留其中的变量只在本线程内可见,其余线程获取不到。

好了,假如有这种场景,咱们子线程里用View.post一个音讯,从下面的代码看,它会保留子线程的ThreadLocal里,然而在执行RunQueue的时候,又是在主线程里去找runnable调用,因为ThreadLocal线程隔离,主线程永远也找不到这个音讯,这个音讯也就没法失去执行了。

而7.0及以上没有这个问题,是因为在post办法里把runnable保留在主线程里:getRunQueue().post(action)。

总结一下:

下面这个问题的前提有两个:View被绘制前,且在子线程里调用View.post。如果View.post是在View被绘制之后,也就是mAttachInfo非空,那么会立刻推入主线程调用,也就不存在因线程隔离找不到runnable的问题。

作者:He Ying


搞代码网(gaodaima.com)提供的所有资源部分来自互联网,如果有侵犯您的版权或其他权益,请说明详细缘由并提供版权或权益证明然后发送到邮箱[email protected],我们会在看到邮件的第一时间内为您处理,或直接联系QQ:872152909。本网站采用BY-NC-SA协议进行授权
转载请注明原文链接:源码深度解析-Handler-机制及应用

喜欢 (0)
[搞代码]
分享 (0)
发表我的评论
取消评论

表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址