通常來說,子線程是不能更新 UI 的,若是在子線程更新 UI,會報錯。html
但在某種狀況下直接開啓線程更新 UI 是不會報錯的。java
好比,在 onCreate 方法中,直接開啓子線程更新 UI,這樣是不會報錯的。app
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
textView = findViewById(R.id.tv)
thread {
textView.text = "哈哈哈哈"
}
}
複製代碼
若是在子線程中假如延時,好比加一行Thread.sleep(2000)
就會報錯。ide
這是爲何呢?oop
有人會說,由於睡眠了 2 s,所以 UI 的線程檢查機制就已經創建了,因此在子線程更新就會報錯。佈局
子線程更新的錯誤定位是 ViewRootImpl
中的 checkThread
方法和 requestLayout
方法。post
// ViewRootImpl 下 checkThread 的源碼
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
//ViewRootImpl 下 requestLayout 的源碼
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
複製代碼
從源碼中能夠看出,checkThread
就是進行線程檢測的方法,而調用是在 requestLayout
方法中。ui
要想知道 requestLayout
是什麼時候調用的,就要知道 ViewRootImpl
是如何建立的?this
由於在 onCreate
中建立子線程訪問 UI,是不報錯的,這也說明在 onCreate
中,ViewRootImpl
還未建立。spa
在 ActivityThread
的 handleResumeActivity
中調用了 performResumeActivity
進行 onResume
的回調。
@Override
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,String reason) {
// 代碼省略...
// performResumeActivity 最終會調用 Activity 的 onResume方法
// 調用鏈以下: 會調用 r.activity.performResume。
// performResumeActivity -> r.activity.performResume -> Instrumentation.callActivityOnResume(this) -> activity.onResume();
final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
// 代碼省略...
if (r.window == null && !a.mFinished && willBeVisible) {
r.activity.mVisibleFromServer = true;
mNumVisibleActivities++;
if (r.activity.mVisibleFromClient) {
// 注意這句,讓 activity 顯示,而且會最終建立 ViewRootImpl
r.activity.makeVisible();
}
}
}
複製代碼
進一步跟進 activity.makeVisible()
。
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
// 往 WindowManager 中添加 DecorView
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
複製代碼
WindowManager
是一個接口,它的實現類是 WindowManagerImpl
。
// WindowManagerImpl 的 addView 方法
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
// 最終調用了 WindowManagerGlobal 的 addView
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
// WindowManagerGlobal 的 addView
public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {
// 省略部分代碼
// ViewRootImpl 對象的聲明
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
// 省略部分代碼
// ViewRootImpl 對象的建立
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
try {
// 調用 ViewRootImpl 的 setView 方法
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
}
複製代碼
由此能夠看出,ViewRootImpl
是在 activity
的 onResume
方法調用後才由 WindowManagerGlobal
的 addView
方法建立。
requestLayout
是如何調用的呢?在上面 WindowManagerGlobal
的 addView
方法中,建立完 ViewRootImpl
後,會調用它的 setView
的方法,在 setView
方法內部會調用 requestLayout
。
此時就會去檢測 UI 更新時調用的線程了。
// ViewRootImpl 的 setView
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;
// 省略無關代碼...
// requestLayout 的調用
requestLayout();
// 省略無關代碼...
}
}
// requestLayout 方法
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
複製代碼
而在 SheduleTranversals
方法中,會調用 TraversalRunnable
的 run
方法,最終會在 performTraversals
方法中,調用 performMeasure
performLayout
performDraw
去開始 View 的繪製流程。
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
// TraversalRunnable 的 run 方法中,會開啓 UI 的measure、layout、draw
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
// 省略無關代碼...
}
}
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
void doTraversal() {
if (mTraversalScheduled) {
// 省略部分代碼
performTraversals();
}
}
private void performTraversals() {
// Ask host how big it wants to be
// 省略部分代碼
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
performLayout(lp, mWidth, mHeight);
performDraw();
}
複製代碼
既然知道了子線程更新 UI 的檢測是在 checkThread
方法中,那麼有沒有什麼方法能夠繞過呢?可否作到子線程更新 UI 呢?
答案是能夠的。
我以一個簡單的 demo 實驗一下,下面先看效果。
代碼以下:
// MainActivity
public class MainActivity extends AppCompatActivity {
private View containerView;
private ViewTreeObserver.OnGlobalLayoutListener globalLayoutListener;
private TextView mTv2;
private TextView mTv1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
containerView = findViewById(R.id.container_layout);
mTv1 = findViewById(R.id.text);
mTv2 = findViewById(R.id.text2);
// 開啓線程,啓動 GlobalLayoutListener
Executors.newSingleThreadExecutor().execute(() -> initGlobalLayoutListener());
}
private void initGlobalLayoutListener() {
globalLayoutListener = () -> {
Log.e("caihua", "onGlobalLayout : " + Thread.currentThread().getName());
ViewGroup.LayoutParams layoutParams = containerView.getLayoutParams();
containerView.setLayoutParams(layoutParams);
};
this.getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(globalLayoutListener);
}
public void updateUiInMain(View view) {
mTv1.setText("主線程更新 UI");
}
public void updateUiInThread(View view) {
new Thread(){
@Override
public void run() {
SystemClock.sleep(2000);
mTv2.setText("子線程更新 UI :" + Thread.currentThread().getName());
}
}.start();
}
}
複製代碼
原理:經過 ViewTreeObserver.OnGlobalLayoutListener
設置全局的佈局監聽,而後在 onGlobalLayout
方法中,調用 view
的 setLayoutParams
方法,setLayoutParams
方法內部會調用 requestLayout
,這樣就能夠繞過線程檢測。
爲何能繞過呢?
由於 setLayoutParams
中調用的 requestLayout
方法並非 ViewRootImpl
中 requestLayout
.
而 View
的 requestLayout
並不調用 checkThread
方法去檢測線程。
源碼以下↓
// view.setLayoutParams 源碼
public void setLayoutParams(ViewGroup.LayoutParams params) {
if (params == null) {
throw new NullPointerException("Layout parameters cannot be null");
}
mLayoutParams = params;
resolveLayoutParams();
if (mParent instanceof ViewGroup) {
((ViewGroup) mParent).onSetLayoutParams(this, params);
}
// 調用 requestLayout 方法。
requestLayout();
}
// View 的 requestLayout 方法
public void requestLayout() {
if (mMeasureCache != null) mMeasureCache.clear();
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
ViewRootImpl viewRoot = getViewRootImpl();
if (viewRoot != null && viewRoot.isInLayout()) {
if (!viewRoot.requestLayoutDuringLayout(this)) {
return;
}
}
mAttachInfo.mViewRequestingLayout = this;
}
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;
if (mParent != null && !mParent.isLayoutRequested()) {
mParent.requestLayout();
}
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
mAttachInfo.mViewRequestingLayout = null;
}
}
複製代碼