前言
平時我們在開發過程中知道主線程不能進行耗時操作,子線程不能更新UI,於是有了線程間通訊,有了Handler機制,那麼子線程真的不能更新UI麼?很多小夥伴在面試的時候也會經常被問到這個問題,網上已經有了不少詳解這一問題的博客,不過這裏我還是帶着複習一遍的態度,把這個流程再摸一遍。
正文
子線程一定不能更新UI麼?
先說答案:是不一定,在Activity的onResume聲明週期之前就可以。
下面我們看一下原理:
我們都知道在Android中有一個ActivityThread類,這個類非常重要,包括Activity的創建都和這個類有關,而且ActivityThread的main方法中還有Looper.preMainLooper,說明主線程的Looper也是在這裏創建的,還有Looper.looper也是在這個地方調用的,還創建了主線程的Handler H:
final H mH = new H();
class H extends Handler {
...
}
在這裏我們看ActivityThread這個類當中的一個方法handleResumeActivity:
@Override
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
String reason) {
...
final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
r.activity.mVisibleFromServer = true;
mNumVisibleActivities++;
if (r.activity.mVisibleFromClient) {
r.activity.makeVisible();
}
...
}
只看這裏主要的幾行代碼,發現這裏調用了performResumeActivity方法:
@VisibleForTesting
public ActivityClientRecord performResumeActivity(IBinder token, boolean finalStateRequest,
String reason) {
...
r.activity.performResume(r.startsNotResumed, reason);
r.state = null;
r.persistentState = null;
r.setState(ON_RESUME);
...
return r;
}
裏面又調用了Activity中的performResume方法:
final void performResume(boolean followedByPause, String reason) {
...
// mResumed is set by the instrumentation
mInstrumentation.callActivityOnResume(this);
writeEventLog(LOG_AM_ON_RESUME_CALLED, reason);
...
}
這裏又調用了Instrumentation的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方法,說明是在這裏執行了Activity的onResume回調函數。
再反過頭來看一下,我們最開始ActivityThread類handleResumeActivity中的另一行代碼:
r.activity.makeVisible();
是調用了Activity中的makeVisible方法:
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
這裏通過getWindowManager方法獲取到了一個ViewManager,在這裏我們要說明的一點是,getWindowManager方法返回的是一個WindowManager類的對象,而WindowManager是一個接口繼承於ViewManager。而這裏得到的ViewManager對象實際上是WindowManager的實現類WindowManagerImpl。然後調用WindowManagerImpl中的addView方法:
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
這裏又調用了WindowManagerGlobal當中的addView方法:
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
...
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
...
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
// do this last because it fires off messages to start doing things
try {
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
}
在這裏我們看到創建了一個ViewRootImpl的實例,並調用了ViewRootImpl的setView方法:
/**
* We have one child
*/
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
...
requestLayout();
...
}
發現這裏調用了ViewRootImpl中的requestLayout方法:
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
在requestLayout方法中,我們看到有調用一個checkThread方法和一個scheduleTraversals方法:
我們先看checkThread方法:
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
我們發現這裏是做了一個線程的判斷,拿主線程和當前線程做了一個判斷,也就是說如果當前線程不是主線程那麼就會拋出一個CalledFromWrongThreadException異常。
由此看來是在Activity的onResume回調函數之後做的checkThread線程判斷。所以說我們只要在Activity的onResume回調函數之前在子線程更新UI就不會拋異常。(也就是在onResume回調函數的super.onResume函數之前調用,就不會拋異常)。
好了,說到這裏,我們已經解答了本篇文章的問題,但是剛纔那個scheduleTraversals方法我們也來分析一下:
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
我們看這裏,調用了
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
我們看一下這裏的mTraversalRunnable:
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
裏面的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方法:
在performTraversals方法裏面分別調用了:performMeasure()、performLayout()、performDraw()方法。
而這三個方法又分別調用了measure、layout、draw。
又由於measure方法是final類型的,不可被子類繼承所以對外提供了onMeasure方法,所以我們在自定義View的使用,重寫的是onMeasure、onLayout、onDraw方法。
總結
Android可以在子線程更新UI,但是前提是要在Activity的onResume回調方法前調用子線程更新UI的操作。不過我們一般不這樣搞,還是遵循主線程更新UI,子線程進行耗時操作的邏輯比較合理一些。