当前位置: 首页 > 科技观察

你知道View.post()哪里不可靠吗?

时间:2023-03-13 15:31:17 科技观察

前言这篇文章以前发过,但是有读者指出描述中存在一些问题。后来看的时候也觉得有问题,所以把之前的文章删掉了(主线程没问题,删掉只是为了避免更多的误会),准备修改勘误后,被重新释放。这次,描述问题的Demo就完成了。有什么问题可以继续在文末留言。再次感谢细心的读者指出文章中的错误。1.前言有时候,我们需要使用View.post()方法来发送一个Runnable到主线程执行。这一切看起来很美好,最终都会通过一个Handler.post()方法来执行,这样我们就不用重新定义一个Handler对象了。但是,在Android7.0(Apilevel24)上,View.post()将不再那么可靠,你post()出来的Runnable可能永远没有机会执行。我们先来看看他们的详细资料。2.post7.0的区别2.1post方法的区别前面说了,这个问题只出现在Android7.0上。那我们先从源码上分析Android7.0对View.post()做了哪些改动。使用Diff查看它们的差异。左边是ApiLevel24(以下简称Api24)的代码,右边是ApiLevel23-(以下简称Api23)的代码。可以清楚的看到,只有当mAttachInfo为null时,执行逻辑才会不同。在Api24中会调用getRunQueue().post(action),而Api23会调用ViewRootImpl.getRunQueue().post(action)方法。这是不同之处。2.2Api23post的细节我们先简单了解一下什么是ViewRootImpl。ViewRootImpl可以理解为一个Activity的ViewTree根节点的实例。每个ViewRootImpl用于管理DecorView和ViewTree。在ViewRootImpl中,用来承载Runnable的队列是sRunQueues,它是一个静态变量,也就是说ViewRootImpl中的消息队列在App的生命周期内都是一样的。我们来看看前面提到的ViewRootImpl.getRunQueue().post()是干什么的?post()方法只是将其包装成一个HandlerAction对象,放入mActions的ArrayList中。要继续追踪,需要知道mActions中添加的HandlerAction是什么时候被消费的。使用HandlerAction的地方是executeActions()方法。最后还是调用了handler.postDelayed(),没什么好说的。关键点是何时调用executeAction()方法。executeAction()被TraversalRunnable调用doTraversa(),在doTraversa()方法中调用。而TraversalRunnable是通过Choreographer.postCallBack()循环调用的。这个Choreographer通过doScheduleCallback()发送一个MSG_DO_SCHEDULE_CALLBACK类型的消息循环调用,间隔是一个VSync间隔。关于Choreographer,不是本文的重点。有兴趣的可以单独了解一下。而在Api23以下,会循环调用executeAction()。基本上,在mActions中,只要有未执行的Runnable,就会立即被消费掉。所以在Api23以下的设备上,无论如何View.post()基本靠谱,发布的Runnable会有机会被执行。2.3Api24的细节我们来看看Api24的实现细节。Api24中调用了getRunQueue().post()方法,操作了一个HandlerActionQueue对象。内部结构其实和Api23很像,同样维护了一个HandlerAction的数组mActions。mActions最终被消费的地方还是一个executeActions()方法。回到最基本的问题,什么时候调用executeActions()方法?如果继续跟踪,可以看到在View.dispatchAttachedToWindow()方法中会调用到。因为,executeActions()方法,在Api24及以上,只会有机会在dispatchAttachedToWindow()方法中被调用,而View.dispatchAttachedToWindow()方法,只能在这个View中通过addView()方法调用,或者原本写在页面布局的xml中(其实也是调用addView()),添加到一个ViewGroup的时候会调用。这就导致,如果你只是通过new创建一个View或者使用LayoutInflater,而没有通过addView()将其添加到layoutview中,那么你通过这个View.post()传递的Runnable永远不会执行到。这就是为什么在Api24下,会出现View.post()不一致的现象。3.举个例子说明问题由于这个问题只是为了复现,所以坚持最少改动的原则,构造最简单的场景,单独新建一个View,然后通过它调用post()方法就可以看到执行结果。可以看到,这里直接新建了一个View,然后贴了一个Runnable。间隔10秒后,View被添加到根布局中。看Api23下的执行效果:可以看到在Api23下,这里是Api19,新的View对象和PostedRunnable会立即执行,不需要等待addView()的执行。再来看看Api24下的执行效果:从执行时间可以看出,贴出的Runnable并没有立即执行,而是在addView()调用之后执行,恰好在中间执行的区间是10s。据说在Android8.0上修改了这个问题,特地找了一台8.0的设备试运行结果,如下图:25是Android8.0的预览版,可以看到这里,它还是和7.0上的表现一样,会一直等到最后的addView()执行完。不知道正式版会不会有什么变化。这还有待验证。基本确认AndroidApi24+受影响,但仍需开发者关注。毕竟,发布的App运行在什么设备上并不是我们能决定的。4.总结View.post()方法,不同版本的区别,根本原因是在Api23和Api24中,executeActions()方法的调用时机不一样,导致View在没有mAttachInfo对象时表现不一样。所以我们在使用的过程中需要慎重使用,分清实际的使用场景,一般规范自己的代码:如果动态创建的View是根据条件决定是否加入根布局,那么不要用它来调用post()方法。尽量避免使用View.post()方法,可以直接使用Handler.post()方法代替。【本文为专栏作家“张扬”原创稿件,转载请微信♂联系作者获得授权】点此查看作者更多好文