android activity 绘制完成之后 MessageQueue.IdleHandler

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处理完了的时候继续回调,让我们看看它有哪些有趣的用法吧~~

一.提供一个android没有的声明周期回调时机

       如果有这种需求,想要在某个activity绘制完成去做一些事情,那这个时机是什么时候呢?有同学可能觉得onResume()是一个合适的机会,不是可是这个onResume() 真的是各种绘制都已经完成才回调的吗?No, too naive  ~~

android activity 绘制完成之后 MessageQueue.IdleHandler_第1张图片


       谷歌官方可是说了,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函数,  现在我们可以总结下流程了

android activity 绘制完成之后 MessageQueue.IdleHandler_第2张图片

结论:所以如果我们想在界面绘制出来后做点什么,那么在onResume里面显然是不合适的,它先于measure等流程了, 有人可能会说在onResume里面post一个runnable可以吗?还是不行,因为那样就会变成这个样子

android activity 绘制完成之后 MessageQueue.IdleHandler_第3张图片

        所以你的行为一样会在绘制之前执行,这个时候我们的主角IdleHandler就发挥作用了,我们前面说了,它是在looper里面message暂时执行完毕了就会回调,顾名思义嘛,Idle就是队列为空的意思,那么我们的onResume和measure, layout, draw都是一个个message的话,这个IdleHandler就提供了一个它们都执行完毕的回调了,大概就是这样

android activity 绘制完成之后 MessageQueue.IdleHandler_第4张图片


说了这么多,那么现在获取到这个时机有什么用呢? look!!

android activity 绘制完成之后 MessageQueue.IdleHandler_第5张图片


        这个是我们地图的公交详情页面, 进入之后产品要求左边的页卡需要展示,可以看到左边的页卡是一个非常复杂的布局,那么进入之后的效果可以明显看到头部的展示信息是先显示空白再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);                Log.i("yangu", "cost time " + (System.currentTimeMillis() - time));                
               return false;            }        });

效果是这样的

android activity 绘制完成之后 MessageQueue.IdleHandler_第6张图片


        看出不同了吗?顶部的页卡先展示出来了,这样体验是不是会更好一些呢。虽然只有短短90ms,不过我们做app也应该关注这种细节优化的,是吧~ 这个做法也提供了一种思路,android本身提供的activity框架和fragment框架并没有提供绘制完成的回调,如果我们自己实现一个框架,就可以使用这个IdleHandler来实现一个onRenderFinished这种回调了。

二.可以结合HandlerThread, 用于单线程消息通知器

        我们先思考一个问题,如果有一个model数据管理模块,怎么设计?比如地图的收藏模块的model部分。就是下面这个图的小星星


android activity 绘制完成之后 MessageQueue.IdleHandler_第7张图片

它原来的model设计大概是这个样子的


public class FavoriteModel {
   private static FavoriteModel ourInstance = new FavoriteModel();
       public static FavoriteModel getInstance() {
               return ourInstance;    }  
                 
   private FavoriteModel() {    }    //收藏点数据    private List mData = new ArrayList<>();
       //同步锁    public Object lock;
          //添加收藏点    public void add(Favorite f) {        
       synchronized (lock){            doSomeThing();        }    }
   //删除一个收藏点    public void delete(Favorite f) {
       synchronized (lock){            doSomeThing();        }    }
   //获取当前收藏列表    public List get() {        
         return mData;    }
 //同步服务器收藏数据    public void sync() {        
       synchronized (lock){            doSomeThing();        }    } }

        由于这个model是单例的,而且是多线程可以访问的,所以它的增删改查都加上了锁,而且由于外部访问需要遍历有哪些收藏点,所以外部遍历列表也需要加锁,大概是这样的


public void visit(List favorites){        
synchronized (FavoriteModel.getInstance().lock){            
for (Favorite favorite : favorites) {                doSomeThing();            }        }    }

        因为是多线程可访问的,如果遍历不加锁的话,其他线程删除了一个收藏,就会crash的,原来的这样设计有几个不好的地方

1.外部使用者需要关系锁的使用,增加了负担,不用还不安全

2.如果在主线程加锁的话,可能另一个线程执行操作会阻塞主线程造成anr

        总之,多线程代码就是容易出错,而且真的出错的时候查起来太费劲了,目前收藏夹模块就有N多bug,所以我想用单线程来解决这个问题,由于model层的访问需要数据库和网络等,所以需要异步线程,那么单线程队列+异步线程,首先想到的就是HandlerThread, 大概架构如下

android activity 绘制完成之后 MessageQueue.IdleHandler_第8张图片


        现在,我们把原来多线程的逻辑改到了单线程里面,各种收藏的model共用一个HandlerThread,这样我们增删改查都不用加锁了,出错几率大大减小,而且这种model的设计有点类似插件的意思,可以很方便的增加其他收藏

        ok, 那么跟我们的主题IdleHandler有什么关系呢?思考这样一个问题,地图上的小星星需要实时更新,也就是model的任何变化都需要显示到地图上,那么收藏的小星星就应该作为model的观察者,以前的做法是向收藏model注册监听,在每一个增删改查操作后都对观察者回调,大概是这样


//添加收藏点    public void add(Favorite f) {        
       synchronized (lock){            doSomeThing();            notifyObserver();        }    }    //删除一个收藏点    public void delete(Favorite f) {
       synchronized (lock){            doSomeThing();            notifyObserver();        }    }

        这样有一个小小的问题,就是如果有一个操作生成10个快速连续的增删改查操作,那么我们的UI就会收到10次回调,而这种场景下我们其实只需要最后一次回调就够了,中间操作其实不用刷新UI的

        那么现在改成单线程模型,我们又该如何处理这个问题呢?当然我们也能在每个post到异步线程的runnable里面去回调观察者,但这样未免不够优雅,所以这个时候IdleHandler不就又可以发挥作用了吗?它是在消息暂时处理完的时候回调的呀,不是很符合我们的时机么,对吧


public AbsFavoriteModel() {        
     if (sThread == null) {            sThread = new HandlerThread("favorite-model");            sThread.start();        }        mHandler = new Handler(sThread.getLooper());        
     try {            Field field = Looper.class.getDeclaredField("mQueue");            field.setAccessible(true);            MessageQueue queue = (MessageQueue) field.get(sThread.getLooper());            queue.addIdleHandler(new MessageQueue.IdleHandler() {                

              @Override
              public boolean queueIdle() {                    
              if (mListeners != null){                        
                   for (DataChangeListener mListener : mListeners) {                        mListener.onDataChange(new ArrayList<>(mData));                     }                }                    
              return true;           } });        } catch (Exception e) {            e.printStackTrace();        }    }

        就是这个样子了,这里为什么不用第一个场景下的Looper.myQueue().addIdleHandler()呢?注意这个地方Looper.myQueue()如果在主线程调用就会使用主线程looper了,所以我选择反射这个HandlerThread的looper来设置它,这个IdleHandler我们返回了true, 表示我们要长期监听消息队列,因为返回false,下次就没有回调了哦。

        好了,结论是这个地方IdleHandler用作了一个消息的触发器,是不是挺有意思的呢?

  2017-10-26   yangu  








你可能感兴趣的:(Android开发)