Android 老生常谈之为什么不能在子线程中更新UI

首先声明一点:子线程里面是可以更新UI的——创建一个空白的Activity,在其xml文件中放一个空白TextView,Java代码如下:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_load_gif);
    tvThread = (TextView) findViewById(R.id.tvThread);
    new Thread(new Runnable() {
    @Override
        tvThread.setText("子线程加载");
    }).start();
}

运行结果表示,更新UI时成功的。
现在我们先让子线程休眠100ms再更新UI:

new Thread(new Runnable() {
    @Override
    public void run() {
        try{
            Thread.sleep(100);
        }catch (Exception e){
        }
        tvThread.setText("子线程加载");
}).start();

结果你会发现,程序崩了。抛出了如下很熟悉的异常:

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)

 Only the original thread that created a view hierarchy can touch its views:意思是只有创建这个View的线程才能够访问更新这个View。

从异常信息可以知道:
异常是从android.view.ViewRootImpl的checkThread方法抛出的。而ViewRootImpl是接口ViewRoot的实现类。ViewRootImpl的checkThread方法的源码如下:

void checkThread() {
    if (mThread != Thread.currentThread()) {      
        throw new CalledFromWrongThreadException("Only the original thread that created a view hierarchy can touch its views.");
    }       
}

其中mThread是主线程(UI线程或者MainThread线程),在应用程序启动的时候,就已经被初始化了。

由此我们可以得出结论:

在访问UI的时候,ViewRoot会去检查当前是哪个线程访问的UI,如果不是主线程,就会抛出异常:Only the original thread that created a view hierarchy can touch its views。

再看异常的另一段:

at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:924)

再看看它涉及到的requestLayout方法:

@Override  
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();      
        mLayoutRequested=true;    
        scheduleTraversals();
    }
}

这里先是调用了checkThread()方法来检查当前线程;然后调用scheduleTraversals()方法,scheduleTraversals,字面理解就是线程遍历循环:

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        TraversalBarrier= Handler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, TraversalRunnable, null);
        if (!mUnbufferedInputDispatch) {            
            scheduleConsumeBatchedInput();        
        }
        notifyRendererOfFramePending();        
        pokeDrawLockIfNeeded();     
    }       
}

注意到postCallback方法的的第二个参数:TraversalRunnable,意思是遍历线程,是一个后台任务:

final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}

里面除了调用了doTraversal();方法,啥也没有干,继续看doTraversal():

void doTraversal() {   
    if (mTraversalScheduled) {       
        mTraversalScheduled = false;
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
        if (mProfile) {        
            Debug.startMethodTracing("ViewAncestor");      
        }
        performTraversals();
        if (mProfile) {   
            Debug.stopMethodTracing();   
        }    
    }
}

可以看到里面调用了performTraversals()方法,View的绘制过程就是从performTraversals方法开始的。它的代码量有点大这里就不多说了,如果继续跟就是学习View的绘制了,偏离了我们的目标。

我们现在知道了,每一次访问UI,Android都会重新绘制View,这个很好理解。

到目前为止,我们可以得到结论:

当访问UI时,ViewRoot会调用checkThread方法检查当前访问UI的线程是哪个,如果不是UI线程则会抛出异常。

但是为什么一开始在Activity的onCreate方法中创建子线程更新UI不抛异常呢?
唯一的解释就是执行onCreate方法的那个时候ViewRootImpl对象还没创建,无法去检查当前线程。

哪ViewRootImpl对象是在哪里,在什么时候被创建的呢?
在ActivityThread中,通过分析我们找到了handleResumeActivity方法:

final void handleResumeActivity(IBinder token,boolean clearHide, boolean isForward, boolean reallyResume) {
    unscheduleGcIdler();
    mSomeActivitiesChanged = true;
    ActivityClientRecord r = performResumeActivity(token, clearHide);
    if (r != null) {
        final Activity a = r.activity;
        ......
            r.activity.mVisibleFromServer = true;
            mNumVisibleActivities++;
            if (r.activity.mVisibleFromClient) {
                r.activity.makeVisible();
            }
        }
        ......  
    } 
}

内部调用了performResumeActivity方法,这个方法看名字像是回调onResume方法的入口的:

public final ActivityClientRecord performResumeActivity(IBinder token,boolean clearHide) {
    ActivityClientRecord r = mActivities.get(token);
    if (localLOGV){
        Slog.v(TAG, "Performing resume of " + r + " finished=" + r.activity.mFinished);
    } 
    if (r != null && !r.activity.mFinished) {
        ......
        r.activity.performResume();
        ......
    }
    return r;
}

可以看到r.activity.performResume()这行代码:

final void performResume() {
    performRestart();
    mFragments.execPendingActions();
    mLastNonConfigurationInstances = null;
    mCalled = false;
    mInstrumentation.callActivityOnResume(this);
    ......
}

上面Instrumentation调用了callActivityOnResume方法,callActivityOnResume源码如下:

public void callActivityOnResume(Activity activity) {
    activity.mResumed = true;
    activity.onResume();
    if (mActivityMonitors != null) {
        synchronized (mSync) {
            final int N = mActivityMonitors.size();
            for (int i=0; i<N; i++) {
                final ActivityMonitor am = mActivityMonitors.get(i);
                am.match(activity, activity, activity.getIntent());
            }
        }
    }
}

找到了activity.onResume()。这也证实了performResumeActivity方法是回调onResume方法的入口。那么现在我们再回头看handleResumeActivity方法,执行完performResumeActivity方法回调了onResume方法后, 会再执行这一块代码:

......
r.activity.mVisibleFromServer = true;
mNumVisibleActivities++;
if (r.activity.mVisibleFromClient) {
    r.activity.makeVisible();
}

r.activity调用了makeVisible方法,makeVisible方法是干什么的呢?我们跟进去看看:

void makeVisible() {
    if (!mWindowAdded) {
        ViewManager wm = getWindowManager();
        wm.addView(mDecor, getWindow().getAttributes());
        mWindowAdded = true;
    }
    mDecor.setVisibility(View.VISIBLE);
}

他是往WindowManager中添加DecorView,那现在应该关注的就是WindowManageraddView方法了。而WindowManager是一个接口,我们找到WindowManager的实现类WindowManagerImpl。这个和ViewRoot是一样的,就名字多了个impl。在WindowManagerImpladdView方法如下:

@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyDefaultToken(params);
    mGlobal.addView(view, params, mDisplay, mParentWindow);
}

里面调用了WindowManagerGlobaladdView方法,那现在就锁定WindowManagerGlobaladdView方法:

public void addView(View view, ViewGroup.LayoutParams params,
    Display display, Window parentWindow) {
        ......

        ViewRootImpl root;
        View panelParentView = null;
        ......
        root = new ViewRootImpl(view.getContext(), display);
        view.setLayoutParams(wparams);
        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);
    }
    try {
        root.setView(view, wparams, panelParentView);
    } catch (RuntimeException e) {
        synchronized (mLock) {
            final int index = findViewLocked(view, false);
            if (index >= 0) {
                removeViewLocked(index, true);
            }
        }
        throw e;
    }
}

终于看到我们想要看的关键信息:ViewRootImpl是在WindowManagerGlobaladdView方法中创建的。

因此得出一个总结:

ViewRootImpl的创建是在onResume方法回调之后,而我们一开篇是在onCreate方法中创建子线程并访问UI,在那个时刻,ViewRootImpl还没有来得及创建,无法检测当前线程是否是UI线程,所以程序没有崩溃。而之后修改了程序,让线程休眠了100毫秒后再更新UI,程序就崩了。很明显在这100毫秒内ViewRootImpl已经完成了创建,并能执行checkThread方法检查当前访问并更新UI的线程是不是UI线程。

同样的,我们还可以猜测,在onStart方法和onResume方法里面创建子线程并访问更新UI,同样是可以运行成功的。这一点留给读者去验证。

这是从源码的角度分析为什么不能在子线程中更新UI。

哪再问:为什么谷歌要提出:“UI更新一定要在UI线程里实现”这一规则呢?原因如下:

目的在于提高移动端更新UI的效率和和安全性,以此带来流畅的体验。原因是:

Android的UI访问是没有加锁的,多个线程可以同时访问更新操作同一个UI控件。也就是说访问UI的时候,android系统当中的控件都不是线程安全的,这将导致在多线程模式下,当多个线程共同访问更新操作同一个UI控件时容易发生不可控的错误,而这是致命的。所以Android中规定只能在UI线程中访问UI,这相当于从另一个角度给Android的UI访问加上锁,一个伪锁

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值