【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

 

posted @ 2019-04-28 23:44  Ivo-oo  阅读(735)  评论(0)    收藏  举报