时间: 2020-09-03 00:08:26 人气: 2271 评论: 0
作者:谷言
我们知道android是基于Looper消息循环的系统,我们通过Handler向Looper包含的MessageQueue投递Message, 不过我们常见的用法是这样吧?
new Handler(Looper.getMainLooper()).post(new Runnable() { @Override public void run() {/do something } });
一般我们比较少接触MessageQueue, 其实它内部的IdleHandler接口有很多有趣的用法,首先看看它的定义
/** * Callback interface for discovering when a thread is going to block * waiting for more messages. */ 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(); }
简而言之,就是在looper里面的message暂时处理完了,这个时候会回调这个接口,返回false,那么就会移除它,返回true就会在下次message处理完了的时候继续回调,让我们看看它有哪些有趣的用法吧~~
如果有这种需求,想要在某个activity绘制完成去做一些事情,那这个时机是什么时候呢?有同学可能觉得onResume()是一个合适的机会,不是可是这个onResume() 真的是各种绘制都已经完成才回调的吗?No, too naive ~~
你看谷老师说了,onStart是用户可见,onResume是用户可交互,谷老师可没说onResume是绘制完成吧~那么android那些耗时的measure, layout, draw是在什么时候执行的呢?它们跟onResume()又有何关系呢?让我们先来看看源码吧~
1. ActivityThread.java
我们知道app的进程其实是ActivityThread, 那么activity的生命周期自然是它来执行了,
final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume) { //省略部分代码.. //call activity的onResume ActivityClientRecord r = performResumeActivity(token, clearHide); //省略部分代码.. View decor = r.window.getDecorView(); decor.setVisibility(View.INVISIBLE); ViewManager wm = a.getWindowManager(); WindowManager.LayoutParams l = r.window.getAttributes(); a.mDecor = decor; l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION; l.softInputMode |= forwardBit; if (a.mVisibleFromClient) { a.mWindowAdded = true; //这里就是关键代码了 wm.addView(decor, l);
performResumeActivity就是回调onResume了, 我们继续看wm.addView方法, 这个ViewManager是一个接口,其实现者是WindowManagerImpl
2.WindowManagerImpl.java
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mDisplay, mParentWindow);
}
这个mGlobal是WindowManagerGlobal对象,我们继续
3.WindowManagerGlobal.java
public void addView(View view, android.view.ViewGroup.LayoutParams params, Display display, Window parentWindow) { //我们跳过不相关代码.. root = new ViewRootImpl(view.getContext(), display); view.setLayoutParams(wparams); this.mViews.add(view); this.mRoots.add(root); this.mParams.add(wparams); try { root.setView(view, wparams, panelParentView); }catch (RuntimeException var15) { //省略... } } }
这里我们new 出了ViewRootImpl对象, 我们知道这个对象就是android view的根对象了,负责view绘制的measure, layout, draw的巨长的方法 performTraversals就是这个类的,我们继续看setView方法
4.ViewRootImpl.java
public void setView(View view, LayoutParams attrs, View panelParentView) { //省略部分... this.requestLayout(); //省略部分.. switch(res) { case -9: throw new InvalidDisplayException("Unable to add window " + this.mWindow + " -- the specified display can not be found"); case -8: throw new BadTokenException("Unable to add window " + this.mWindow + " -- permission denied for this window type"); case -7: throw new BadTokenException("Unable to add window " + this.mWindow + " -- another window of this type already exists"); case -6: return; case -5: throw new BadTokenException("Unable to add window -- window " + this.mWindow + " has already been added"); case -4: throw new BadTokenException("Unable to add window -- app for token " + attrs.token + " is exiting"); case -3: throw new BadTokenException("Unable to add window -- token " + attrs.token + " is not for an application"); case -2: case -1: throw new BadTokenException("Unable to add window -- token " + attrs.token + " is not valid; is your activity running?"); default: throw new RuntimeException("Unable to add window -- unknown error code " + res); } }
这个函数调用了关键方法requestLayout(), 我们继续跟踪,顺便说下,后面一连串的BadTokenException就是我们常常遇到的dialog相关抛出的,也有些特殊场景也会出这个异常,可以到这里查看线索
public void requestLayout() { if(!this.mHandlingLayoutInLayoutRequest) { this.checkThread(); this.mLayoutRequested = true; this.scheduleTraversals(); } }
调用了scheduleTraversals, 从名字就能看出来了吧
void scheduleTraversals() { if(!this.mTraversalScheduled) { this.mTraversalScheduled = true; this.mTraversalBarrier = this.mHandler.getLooper().postSyncBarrier(); this.mChoreographer.postCallback(2, this.mTraversalRunnable, (Object)null); this.scheduleConsumeBatchedInput(); } }
它往Choreographer里面post了一个runnable, 这个Choreographer是android负责帧率刷新相关的东西,我们暂时可以不关注它,可以理解为往主线程post一个消息是一样的,顺便说下这个Choreographer可以做帧率检测相关的东西,,可以用于卡顿检测什么的。。。
final class TraversalRunnable implements Runnable { TraversalRunnable() { } public void run() { ViewRootImpl.this.doTraversal(); } }
void doTraversal() { if(this.mTraversalScheduled) { this.mTraversalScheduled = false; this.mHandler.getLooper().removeSyncBarrier(this.mTraversalBarrier); if(this.mProfile) { Debug.startMethodTracing("ViewAncestor"); } Trace.traceBegin(8L, "performTraversals"); try { this.performTraversals(); } finally { Trace.traceEnd(8L); } if(this.mProfile) { Debug.stopMethodTracing(); this.mProfile = false; } } }
我们看这个runnable果然是去执行了那个巨长无比的函数performTraversals函数, 现在我们可以总结下流程了
结论:所以如果我们想在界面绘制出来后做点什么,那么在onResume里面显然是不合适的,它先于measure等流程了, 有人可能会说在onResume里面post一个runnable可以吗?还是不行,因为那样就会变成这个样子
所以你的行为一样会在绘制之前执行,这个时候我们的主角IdleHandler就发挥作用了,我们前面说了,它是在looper里面message暂时执行完毕了就会回调,顾名思义嘛,Idle就是队列为空的意思,那么我们的onResume和measure, layout, draw都是一个个message的话,这个IdleHandler就提供了一个它们都执行完毕的回调了,大概就是这样
说了这么多,那么现在获取到这个时机有什么用呢? look!!
这个是我们地图的公交详情页面, 进入之后产品要求左边的页卡需要展示,可以看到左边的页卡是一个非常复杂的布局,那么进入之后的效果可以明显看到头部的展示信息是先显示空白再100毫秒左右之后才展示出来的,原因就是这个页卡的内容比较复杂,用数据向它填充的时候花了较长时间,代码如下
long time = System.currentTimeMillis(); detailView.populate(route); //省略部分不相关代码 drawerLayout.openDrawer(GravityCompat.START); Log.i("yangu", "cost time " + (System.currentTimeMillis() - time));
可以看到这个detailView就是这个侧滑的页卡了,填充里面的数据花了90ms,如果这个时间是用在了界面view绘制之前的话,就会出现以上的效果了,view先是白的,再出现,这样就体验不好了,如果我们把它放到IdleHandler里面呢?代码如下
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() { @Override public boolean queueIdle() { long time = System.currentTimeMillis(); detailView.populate(route); //省略部分不相关代码 drawerLayout.openDrawer(GravityCompat.START)