通过Android子线程更新问题看原理

157 阅读3分钟

起因

学过Android开发都听过不能在子线程更新UI,会检查线程从而抛出异常。
但是经过进一步的学习后发现,怎么在onCreate中开启一个线程设置TextView的值,怎么没有报错?甚至不是在onCreate,可能在onResume也一样,这是为什么?
首先要清楚的一点是,在设置TextView 或者addView这些操作,都是会引起requsetLayout(),导致重新布局的,但是翻遍了view的requestLayout()并没有找到所谓的checkThread()方法。但我们发现在view中有这个调用方法。

image.png 它去调用了parent的requestLayout(),哦?那是viewGroup中的requestLayout方法有什么特异之处?结果却发现,viewGroup甚至都没有重写requestLayout。
但回想到在正常的view(非dialog,悬浮窗)的顶层viewGroup好像都是DecorView哎,是不是这个有什么不同,看过之后,发现好像就是普通的viewGroup啊。再好好想想,view的绘制流程是不是都是层层往上传递,最后由viewRootImpl处理的?那这个decorView是不是也被windowManger add在ViewRootImpl里面,好像有点道理。
赶紧看看ViewRootImpl的requestLayout,果然在这里发现了checkThread方法,

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

好吧以上都是戏精,在子线程中更新UI后,报错的日志就已经有提示了

2022-07-03 23:08:15.710 25421-25511/com.hua.testcontenprovider E/AndroidRuntime: FATAL EXCEPTION: Thread-2
    Process: com.hua.testcontenprovider, PID: 25421
    android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
        //从这看出,方法异常是由viewRootImpl的checkThread抛出
        at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:9873)
        at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1933)
        at android.view.View.requestLayout(View.java:26151)
        at android.view.View.requestLayout(View.java:26151)
        at android.view.View.requestLayout(View.java:26151)
        at android.view.View.requestLayout(View.java:26151)
        at android.view.View.requestLayout(View.java:26151)
        at android.view.View.requestLayout(View.java:26151)
        at android.view.ViewGroup.addView(ViewGroup.java:5091)
        at android.view.ViewGroup.addView(ViewGroup.java:5030)
        at android.view.ViewGroup.addView(ViewGroup.java:5002)
        at com.hua.testcontenprovider.NewActivity$onResume$1.invoke(NewActivity.kt:99)
        at com.hua.testcontenprovider.NewActivity$onResume$1.invoke(NewActivity.kt:97)
        at kotlin.concurrent.ThreadsKt$thread$thread$1.run(Thread.kt:30)

探索

看了以上的一些提示,恍然大悟了一声,原来是你小子(viewRootImpl)搞得鬼。但疑问再次提出,那为什么onCreate、onResume开启即时线程(开启就运行,无睡眠阻塞的线程)却没问题,这又是咋回事,大家都是setText,凭什么他们搞特殊?别急,我来运行一些代码

override fun onResume() {
    super.onResume()
    Log.d("TAG", "绘制前DecorView的parent:  ${window.decorView.parent}")
    linearLayout.postDelayed(
        {
            Log.d("TAG", "绘制后DecorView的parent:  ${window.decorView.parent}")
        },
        5000L
    )
}

有人看完,会说你这是什么意思?难不成在之前viewRootImpl不存在,所以没有检查检查线程?先别下结论,我们看看运行结果。

image.png 这结果看下来,好像就是那个意思哈,那这是为什么呢?
那我们得先知道,Activity的onResume的运行时机,这就得去看看ActivityThread了。
代码太多,捡重点看,首先在方法中调用了performResumeActivity()这个方法,这个方法会调用 activity.performResume方法,然后一步一步的回调到我们的Activity的onResume方法。

@Override
public void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest,
        boolean isForward, String reason) {
     ......
    // TODO Push resumeArgs into the activity for consideration
    // skip below steps for double-resume and r.mFinish = true case.
    if (!performResumeActivity(r, finalStateRequest, reason)) {
        return;
    }
    ....

    final Activity a = r.activity;
    ......
    
    if (r.window == null && !a.mFinished && willBeVisible) {
        r.window = r.activity.getWindow();
        View decor = r.window.getDecorView();
        decor.setVisibility(View.INVISIBLE);
        ViewManager wm = a.getWindowManager();
        WindowManager.LayoutParams l = r.window.getAttributes();
        a.mDecor = decor;
        .......
        if (a.mVisibleFromClient) {
            if (!a.mWindowAdded) {
                a.mWindowAdded = true;
                wm.addView(decor, l);
            } 
            .......
        }
   ......
}

看完performResumeActivity()方法调用,接着往下看,代码中获取到activity的window实例,在看过activity的源码中,都知道在activity.attach中new了一个PhoneWindow,而也正是在onCreate回调中,setContentView在window中实例化了decorView,也就是代码运行到此时,phoneWindow、decorView都不为空。
接着判断activity当前window是否添加,activity的mWindowAdded默认值正是false,哦?正所谓拜登大声喊:大的要来了。
我们就看看这个wm.addView中做了什么,以下是截取了部分代码。

WindowManagerGlobal.java

root = new ViewRootImpl(view.getContext(), display);

view.setLayoutParams(wparams);

mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
try {
    root.setView(view, wparams, panelParentView, userId);
} catch (RuntimeException e) {
    // BadTokenException or InvalidDisplayException, clean up.
    if (index >= 0) {
        removeViewLocked(index, true);
    }
    throw e;
}

原来如此,此时viewRootImpl才初始化,并将其添加到了window管理的所有viewRootImpls中,最后设置了setView,并在该方法中调用了requestLayout(),此时才开始了doTraversal、performTraversal、performMeasure、performLayout、performDraw这些熟悉的view绘制流程。

结论

因为在activity的onResume在执行后,才去实例化ViewRootImpl。以上也就解释了为什么,明明大家都说onResume已经是和用户交互了,为什么还能够在子线程更新UI,并不推荐,只是根据这个来看一些原理,就比如在onResume中获取不到view的宽高,但handler.post延迟了5秒就能获取到了。