【Android异步更新UI】的四种方式
一、概述
Android中不能在其他线程更新UI,所以提供了以下几种异步更新UI的方法:
-
使用Handler消息传递机制;
-
使用AsyncTask异步任务;
-
使用runOnUiThread(action)方法;
-
使用View的post(Runnabel r)、postDelay(Runnable r,long l)方法;
二、为什么不能在其他线程更新UI
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views. at android.view.ViewRootImpl.checkThread(ViewRootImpl.Java:6581) at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:924)
子线程更新UI会报着个错,我们看下ViewRootImpl,checkThread和requestLayout方法
void checkThread() { if (mThread != Thread.currentThread()) {//mThread在初始化时赋为主线程(UI线程),这里和当前线程进行比较 throw new CalledFromWrongThreadException( "Only the original thread that created a view hierarchy can touch its views."); } }
再看一下另一个方法requestLayout
public void requestLayout() { if (!mHandlingLayoutInLayoutRequest) { checkThread(); mLayoutRequested = true; scheduleTraversals(); } }
可以看到,先调用checkThread检查线程,再调用了scheduleTraversals()方法
void scheduleTraversals() { if (!mTraversalScheduled) { mTraversalScheduled = true; TraversalBarrier= Handler.getLooper().getQueue().postSyncBarrier(); mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, TraversalRunnable, null); if (!mUnbufferedInputDispatch) { scheduleConsumeBatchedInput(); } notifyRendererOfFramePending(); pokeDrawLockIfNeeded(); } }
mChoreographer.postCallback的第二个参数
final class TraversalRunnable implements Runnable { @Override public void run() { doTraversal(); } }
void doTraversal() { if (mTraversalScheduled) { mTraversalScheduled = false; mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier); if (mProfile) { Debug.startMethodTracing("ViewAncestor"); } performTraversals(); if (mProfile) { Debug.stopMethodTracing(); mProfile = false; } } }
可以看到里面调用了performTraversals()方法,View的绘制过程就是从performTraversals方法开始的。
我们现在知道了,每一次访问UI,Android都会重新绘制View,这个很好理解。
到目前为止,我们可以得到结论:
当访问UI时,ViewRoot会调用checkThread方法检查当前访问UI的线程是哪个,如果不是UI线程则会抛出异常。
参考:https://blog.csdn.net/haoyuegongzi/article/details/79414081
那么再问:为什么要限制只能UI线程更新UI呢?
UI控件非线程安全,在多线程中并发访问可能会导致UI控件处于不可预期的状态,而不给UI加锁的原因是
- 上锁会让UI控件变得复杂和低效
- 上锁后会阻塞某些进程的执行
三、异步更新UI原理
handler和AsyncTask在其他两篇文章中都有相信的解析,欢迎移步阅读;这里主要说runOnUIThread和View.post()
runOnUiThread
这个方法是Activity的方法:
/** * Runs the specified action on the UI thread. If the current thread is the UI * thread, then the action is executed immediately. If the current thread is * not the UI thread, the action is posted to the event queue of the UI thread. * * @param action the action to run on the UI thread */ public final void runOnUiThread(Runnable action) { if (Thread.currentThread() != mUiThread) { mHandler.post(action); } else { action.run(); } }
官方说明:方法会在在UI线程执行。如果当前线程就是UI线程,会立即执,否则会被post到UI线程的事件队列里等待执行。
方法也比较简单清晰,不再后面赘述了 。可以看到, 最终调用的还是Handler的方法。
View.post(Runable)
public boolean post(Runnable action) { final AttachInfo attachInfo = mAttachInfo; 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. getRunQueue().post(action); return true; }
首先会判断mAttachInfo是否为空吗,不为null则通过attachInfo的handler来post action。如果为null则走getRunQueue().post(action)
而mAttachInfo的赋值有两处,dispatchAttachedToWindow方法中的赋值,一次是dispatchDetachedFromWindow方法中置空。
而
dispatchAttachedToWindow方法在ViewRootImpl.performTraversals中执行。
host.dispatchAttachedToWindow(mAttachInfo, 0);
在 Activity 的 onCreate() 期间, View 的 attachedToWindow 应该是还没有被调用,也就是 mAttachInfo 这时候还是为空,但我们在 onCreate() 里执行 View.post() 里的操作仍然可以保证是在 View 宽高计算完毕的,也就是开头的问题 Q2,那么这点的原理显然就是在另一个 return 那边的方法里了:getRunQueue().post()。
为什么 View.post() 可以保证操作是在 View 宽高计算完毕之后呢?跟进 getRunQueue() 看看:
private HandlerActionQueue getRunQueue() { if (mRunQueue == null) { mRunQueue = new HandlerActionQueue(); } return mRunQueue; }
public class HandlerActionQueue { private HandlerAction[] mActions; private int mCount; public void post(Runnable action) { postDelayed(action, 0); } public void postDelayed(Runnable action, long delayMillis) { final HandlerAction handlerAction = new HandlerAction(action, delayMillis); synchronized (this) { if (mActions == null) { mActions = new HandlerAction[4]; } mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction); mCount++; } } ............................. }
post(Runnable) 方法内部调用了 postDelayed(Runnable, long),postDelayed() 内部则是将 Runnable 和 long 作为参数创建一个 HandlerAction 对象,然后添加到 mActions 数组里。下面先看看 HandlerAction:
private static class HandlerAction { final Runnable action; final long delay; public HandlerAction(Runnable action, long delay) { this.action = action; this.delay = delay; } public boolean matches(Runnable otherAction) { return otherAction == null && action == null || action != null && action.equals(otherAction); } }
很简单的数据结构,就一个 Runnable 成员变量和一个 long 成员变量。这个类作用可以理解为用于包装 View.post(Runnable) 传入的 Runnable 操作的,当然因为还有 View.postDelay() ,所以就还需要一个 long 类型的变量来保存延迟的时间了,这样一来这个数据结构就不难理解了吧。
所以,我们调用 View.post(Runnable) 传进去的 Runnable 操作,在传到 HandlerActionQueue 里会先经过 HandlerAction 包装一下,然后再缓存起来。至于缓存的原理,HandlerActionQueue 是通过一个默认大小为4的数组保存这些 Runnable 操作的,当然,如果数组不够用时,就会通过 GrowingArrayUtils 来扩充数组,具体算法就不继续看下去了,不然越来越偏。
当我们在 Activity 的 onCreate() 里执行 View.post(Runnable) 时,因为这时候 View 还没有 attachedToWindow,所以这些 Runnable 操作其实并没有被执行,而是先通过 HandlerActionQueue 缓存起来。
参考:https://www.jianshu.com/p/c12769aae54d
http://www.cnblogs.com/dasusu/p/8047172.html
浙公网安备 33010602011771号