Android子线程操作UI

现象

时常听到传闻"子线程不能更新UI"

1. 运行中常规更新

比如我们声明一个Button,点击事件中尝试子线程更新UI

fun updateUI() {
    
    
    Thread {
    
    
        textView?.text = "Update..."
    }.start()
}

结果,果然报错了android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

   android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
        at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:9462)
        at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1776)
        at android.view.View.requestLayout(View.java:25760)
        at android.view.View.requestLayout(View.java:25760)
        at android.view.View.requestLayout(View.java:25760)
        at android.view.View.requestLayout(View.java:25760)
        at android.view.View.requestLayout(View.java:25760)
        at android.view.View.requestLayout(View.java:25760)
        at androidx.constraintlayout.widget.ConstraintLayout.requestLayout(ConstraintLayout.java:3605)
        at android.view.View.requestLayout(View.java:25760)
        at android.widget.TextView.checkForRelayout(TextView.java:9734)
        at android.widget.TextView.setText(TextView.java:6322)
        at android.widget.TextView.setText(TextView.java:6150)
        at android.widget.TextView.setText(TextView.java:6102)
        at com.example.demoapplication.MainActivity$Companion.updateUI$lambda-0(MainActivity.kt:51)
        at com.example.demoapplication.MainActivity$Companion.$r8$lambda$Yh_cZvNSUZBQlCsg8jtjl4QkxGQ(Unknown Source:0)
        at com.example.demoapplication.MainActivity$Companion$$ExternalSyntheticLambda0.run(Unknown Source:0)
        at java.lang.Thread.run(Thread.java:923)

2. 启动时

但是如果我们在onCreate()onStart()onResume()之中呢?结果是正常运行,没报错

override fun onResume() {
    
    
    super.onResume()
    Thread {
    
    
        textView?.text = "New Thread Update onResume()"
    }.start()
}

3. Thread.sleep

但是假设我们延迟操作Thread.sleep(1000),竟然又报错了

override fun onResume() {
    
    
    super.onResume()
    Thread {
    
    
        Thread.sleep(1000)
        textView?.text = "Update..."
    }.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:9462)
        at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1776)
        at android.view.View.requestLayout(View.java:25760)
        at android.view.View.requestLayout(View.java:25760)
        at android.view.View.requestLayout(View.java:25760)
        at android.view.View.requestLayout(View.java:25760)
        at android.view.View.requestLayout(View.java:25760)
        at android.view.View.requestLayout(View.java:25760)
        at androidx.constraintlayout.widget.ConstraintLayout.requestLayout(ConstraintLayout.java:3605)
        at android.view.View.requestLayout(View.java:25760)
        at android.widget.TextView.checkForRelayout(TextView.java:9734)
        at android.widget.TextView.setText(TextView.java:6322)
        at android.widget.TextView.setText(TextView.java:6150)
        at android.widget.TextView.setText(TextView.java:6102)
        at com.example.demoapplication.MainActivity.onResume$lambda-2(MainActivity.kt:54)

4. Toast或Dialog

当你操作的是ToastDialog时,又有其他的错误

override fun onResume() {
    
    
    super.onResume()
    Thread {
    
    
        // 1
        textView?.text = "Update..."
        // 2
        val dialog = Dialog(this)
        // 3
        Toast.makeText(this, "Hi, Toast.", Toast.LENGTH_LONG).show()
    }.start()
}
java.lang.RuntimeException: Can't create handler inside thread Thread[Thread-4,5,main] that has not called Looper.prepare()
    at android.os.Handler.<init>(Handler.java:227)
    at android.os.Handler.<init>(Handler.java:129)
    at android.app.Dialog.<init>(Dialog.java:133)
    at android.app.Dialog.<init>(Dialog.java:162)
    at com.example.demoapplication.MainActivity.onResume$lambda-2(MainActivity.kt:50)
    at com.example.demoapplication.MainActivity.$r8$lambda$Fpwj7mya3SwKMUmQd_HphQeDxxw(Unknown Source:0)
    at com.example.demoapplication.MainActivity$$ExternalSyntheticLambda0.run(Unknown Source:2)
    at java.lang.Thread.run(Thread.java:923)
    
java.lang.NullPointerException: Can't toast on a thread that has not called Looper.prepare()
    at com.android.internal.util.Preconditions.checkNotNull(Preconditions.java:157)
    at android.widget.Toast.getLooper(Toast.java:179)
    at android.widget.Toast.<init>(Toast.java:164)
    at android.widget.Toast.makeText(Toast.java:492)
    at android.widget.Toast.makeText(Toast.java:480)
    at com.example.demoapplication.MainActivity.onResume$lambda-2(MainActivity.kt:52)

原因

先看一下是在哪抛出异常的

// android.view.ViewRootImpl

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

原因出自mThread,再来看它的赋值时机在哪

// android.view.ViewRootImpl

public ViewRootImpl(@UiContext Context context, Display display, IWindowSession session,
        boolean useSfChoreographer) {
    
    
    ...
    mThread = Thread.currentThread();
    ...
}

那就是说ViewRootImpl初始化的时候呗,问题变成ViewRootImpl啥时候初始化了

// android.app.ActivityThread

@Override
public void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest,
        boolean isForward, String reason) {
    
    
    ...
    if (r.window == null && !a.mFinished && willBeVisible) {
    
    
        ...
        if (r.mPreserveWindow) {
    
    
            a.mWindowAdded = true;
            r.mPreserveWindow = false;
            ViewRootImpl impl = decor.getViewRootImpl();
            if (impl != null) {
    
    
                impl.notifyChildRebuilt();
            }
        }
        if (a.mVisibleFromClient) {
    
    
            if (!a.mWindowAdded) {
    
    
                a.mWindowAdded = true;
                wm.addView(decor, l);
            } else {
    
    
                a.onWindowAttributesChanged(l);
            }
        }
        ...
    } 
}
// android.view.WindowManagerGlobal

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

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

        view.setLayoutParams(wparams);

        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);
        ...
}

ViewRootImpl对象的创建时机在于onResume()方法之后 , 更加详情启动流程以后单独分析吧
对于DialogToast,构造的时候的Handler都是默认当前线程的LooperActivity的声明周期是有AMS调用而Dialog是应用程序自己调用的。ViewRootImpl的初始化在Activity会在onResume()方法之后,而是Dialog被调用show方法时触发的。
如果当前线程的Looper没有prepare那么必定会抛异常,如果仅仅执行了prepare那么崩溃不会产生了,但是依旧不展示。因为整个Looper还没有开始,里面的Message都未进行处理。最后我们将代码中注释的 Looper.prepare(); 和Looper.loop(); 打开就可以正常在异步线程进行ToastDialog的展现。
TODO

参考

Android在子线程中操作UI:弹出Toast、改变TextView内容

同学们上课,今天我们学习:UI 操作一定要在 UI 线程吗?

ViewRootImpl的独白,我不是一个View(布局篇)

Dialog、Toast的Window和ViewRootImpl

猜你喜欢

转载自blog.csdn.net/b1tb1t/article/details/128840718
今日推荐