如何作到在子線程更新 UI?

通常來說,子線程是不能更新 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 的線程檢查機制就已經創建了,因此在子線程更新就會報錯。佈局

更新 UI 的線程檢測是何時開始的

子線程更新 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

ViewRootImpl 是什麼時候建立的。

ActivityThreadhandleResumeActivity 中調用了 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 是在 activityonResume 方法調用後才由 WindowManagerGlobaladdView 方法建立。

requestLayout 是如何調用的呢?

在上面 WindowManagerGlobaladdView 方法中,建立完 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 方法中,會調用 TraversalRunnablerun方法,最終會在 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 實戰

既然知道了子線程更新 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 方法中,調用 viewsetLayoutParams 方法,setLayoutParams 方法內部會調用 requestLayout,這樣就能夠繞過線程檢測。

爲何能繞過呢?

由於 setLayoutParams 中調用的 requestLayout 方法並非 ViewRootImplrequestLayout.

ViewrequestLayout 並不調用 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;
    }
}

複製代碼

原文連接:caihuasay.com/posts/threa…

相關文章
相關標籤/搜索