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

叮铃铃,叮铃铃,“今天没有BUG”小课堂打铃上课了,一位长相极其帅气的讲师进入教室,教室中间,路人甲、乙、丙三位同学头戴红领巾坐下,双手平放在桌上准备认真听课。

老师在黑板写下今天的问题:“更新UI的操作,一定要在 UI 线程中进行吗?不在 UI 线程可不可以?”

同学甲回答:众所周知,更新 UI 的操作一定要在 UI 线程进行,否则程序会崩溃。

老师点点头,在黑板上奋笔疾书,让同学们检查下面这段代码:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".uicrash.UICrashActivity">

    <androidx.appcompat.widget.AppCompatButton
            android:id="@+id/vTvRandom"
            android:layout_width="200dp"
            android:layout_height="40dp"
            android:text="随便一点文字"
            android:layout_centerInParent="true"/>

</RelativeLayout>
class UICrashActivity : ViewBindingActivity<ActivityUICrashBinding>() {

    override fun initWidget() {
        super.initWidget()
        title = javaClass.simpleName
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        mBinding.vTvRandom.text = "在UI线程修改UI"

    }

    override fun getViewBinding(): ActivityUICrashBinding = ActivityUICrashBinding.inflate(layoutInflater)
}

运行这段代码会报错吗?

同学们:不会

老师又点点头,修改 onCreate() 中的代码:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    mBinding.vTvRandom.setOnClickListener {
        ThreadPool.runOnNonUIThread{
            mBinding.vTvRandom.text = "在非UI线程修改UI"
        }
    }

}

在 button 被点击的时候,将修改文字的操作放到非 UI 线程中运行。

大家觉得会报错吗?

还是路人甲积极,回答道:easy,肯定报错,我就是这么错过来的。

没错,确实报错了,看看堆栈信息:

2022-05-08 14:12:46.352 14420-14471/com.example.essay E/AndroidRuntime: FATAL EXCEPTION: bible-pool-0
    Process: com.example.essay, PID: 14420
    android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
        at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7056)
        at android.view.ViewRootImpl.invalidateChildInParent(ViewRootImpl.java:1163)
        at android.view.ViewGroup.invalidateChild(ViewGroup.java:5207)
        at android.view.View.invalidateInternal(View.java:13715)
        at android.view.View.invalidate(View.java:13679)
        at android.view.View.invalidate(View.java:13663)
        at android.widget.TextView.checkForRelayout(TextView.java:7354)
        at android.widget.TextView.setText(TextView.java:4487)
        at android.widget.TextView.setText(TextView.java:4344)
        at android.widget.TextView.setText(TextView.java:4319)
        at com.jamgu.home.uicrash.UICrashActivity$onCreate$1$1.run(UICrashActivity.kt:24)
        at com.jamgu.common.thread.ThreadPool$RunnableJob.run(ThreadPool.java:252)
        at com.jamgu.common.thread.ThreadPool$Worker.run(ThreadPool.java:314)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1133)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)
        at java.lang.Thread.run(Thread.java:760)
        at com.jamgu.common.thread.PriorityThreadFactory$newThread$1.run(PriorityThreadFactory.kt:17)

这个报错相信大家都很熟悉,Only the original thread that created a view hierarchy can touch its views.,我们没在正确的线程更新 UI 操作。

这里的 original thread 翻译过来是原始线程的意思,原始线程会和我们的主线程意思一样吗?咱们稍后再谈。先看下这个错误是从哪里抛出来的,从上面的堆栈来看,代码的执行的先后顺序是:

com.jamgu.home.uicrash.UICrashActivity$onCreate$1$1.run(UICrashActivity.kt:24)
android.widget.TextView.setText(TextView.java:4319)
android.widget.TextView.setText(TextView.java:4344)
android.widget.TextView.setText(TextView.java:4487)
android.widget.TextView.checkForRelayout(TextView.java:7354)
android.view.View.invalidate(View.java:13663)
android.view.View.invalidate(View.java:13679)
android.view.View.invalidateInternal(View.java:13715)
android.view.ViewGroup.invalidateChild(ViewGroup.java:5207)
android.view.ViewRootImpl.invalidateChildInParent(ViewRootImpl.java:1163)
android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7056)

最后来到了 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 是 View 布局树的根对象,它是顶层视图,所以只要它存在并执行到 checkThread() 这个方法时,它都会判断当前线程是否可以更新 UI,不可以就会直接抛出 CalledFromWrongThreadException 错误。

接下来,我们修改代码如下,去除 onCreate() 处的代码,同时在 onResume() 中新增代码:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
}

override fun onResume() {
    super.onResume()

    ThreadPool.runOnNonUIThread{
        mBinding.vTvRandom.text = "在非UI线程修改UI"
    }
}

同学们会报错吗?

这时候路人乙看到代码就这么点,结合我们之前的分析,心里想不能再给甲同学抢了风头,直接原地起跳:简单!这必报错,在非 UI 线程更新 UI,这必报错!不报错我吃。。

这是路人丙及时摁住了乙的没让他说下去,丙说道:根据当前线程池的繁忙程度而定,如果线程池比较忙,子线程需要等待一定时间再执行时就会报错,否则不会报错。

谁对谁错,我们运行一下不就知道了:

1_onResume_code运行展示.gif

可以看到,按钮文字确实从 “随便一点文字” 改成了 “在非UI线程修改UI”,程序也没有报错。

给子线程的运行加上一定的延时,模拟线程池拥堵的情况:

override fun onResume() {
    super.onResume()

    ThreadPool.runOnNonUIThread({
        mBinding.vTvRandom.text = "在非UI线程修改UI"
    }, 500)
}

再运行:

2_子线程UI延迟更新报错展示.gif

程序报错了,事实证明,丙说的没错,那为什么是这样呢?

路人丙淡定地说道:ViewRootImpl 对象的创建时机在于 onResume() 方法之后,在执行 Activity 的 onResume() 方法时,ViewRootImpl 对象还没有被创建,所以不会走到 ViewRootImpl.checkThread() 的地方,自然也就不会报错了。

3_崇拜的眼神.gif

丙说完,路人甲、乙投来了崇拜的眼神。

是的,在 Activity 的启动过程中,由 ActivityThread 负责 Activity 的创建,Activity 与 Window 的相互绑定,以及各项生命周期方法的调用,比如 onCreate(),onStart(),onResume() 等方法。

在 ActivityThread 中有一个方法:

@Override
public void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest,
        boolean isForward, String reason) {
    // If we are getting ready to gc after going to the background, well
    // we are back active so skip it.
    unscheduleGcIdler();
    mSomeActivitiesChanged = true;

		// 1. 执行 Activity.onResume()
    if (!performResumeActivity(r, finalStateRequest, reason)) {
        return;
    }
  	... 
      
    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();
				...
        if (a.mVisibleFromClient) {
            if (!a.mWindowAdded) {
                a.mWindowAdded = true;
              	// 2. 将 DecorView 添加到 window 中
                wm.addView(decor, l);
            } else {
                a.onWindowAttributesChanged(l);
            }
        }
    } else if (!willBeVisible) {
        if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set");
        r.hideForNow = true;
    }
		...
}

在这个方法里,Activity 的 onResume() 会被首先调用,然后执行 wm.addView(decor,l),将 DecorVeiw 与 WindowManger 绑定,同时创建 ViewRootImpl 对象,执行 decorView 的测量,绘制,布局过程

// 1. 在初始化 Activity 的 Window 时【Activity.attach()方法】,给 WindowManager 赋值
// 赋值对象是 WindowManagerImpl
// 下面方法在,Window 类
public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
        boolean hardwareAccelerated) {
    mAppToken = appToken;
    mAppName = appName;
    mHardwareAccelerated = hardwareAccelerated;
    if (wm == null) {
        wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
    }
  	// 赋值
    mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
}

// 2. 所以 wm.addView(decor, l) 执行的 WindowManagerImpl.addView()
// 下面方法在,WindowManagerImpl 类
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyDefaultToken(params);
    mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}

// 3. mGlobal 是个 WindowManagerGlobal 对象
public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow, int userId) {
   	...
    ViewRootImpl root;
    View panelParentView = null;

    synchronized (mLock) {
        ...
        // 初始化 ViewRootImpl
        root = new ViewRootImpl(view.getContext(), display);

        try {
          	// 调用 ViewRootImpl.setView() 方法,并把 DecorView 传入
            root.setView(view, wparams, panelParentView, userId);
        } catch (RuntimeException e) {
            ...
        }
    }
}

// 4. 在 ViewRootImpl.setView() 方法中,会执行 ViewRootImpl.requestLayout() 方法
@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
      	// 从这里开始 DecorView 的测量、绘制、布局过程
        scheduleTraversals();
    }
}

也就是说 Actvitiy 的生命周期执行到 onResume() 时,ViewRootImpl 对象是还没有创建的。

这也是为什么路人丙说的对的原因。

override fun onResume() {
    super.onResume()
		
  	// 正常运行
    ThreadPool.runOnNonUIThread{
        mBinding.vTvRandom.text = "在非UI线程修改UI"
    }
		// 报错,500ms 后,ViewRootImpl 已经创建完成
    ThreadPool.runOnNonUIThread({
        mBinding.vTvRandom.text = "在非UI线程修改UI"
    }, 500)
}

所以总结一下,在 ViewRootImpl 创建前,未在 UI 线程 更新 UI 也不会报错。那么在 ViewRootImpl 创建后,不在 UI 线程更新 UI,一定会报错吗

回到之前提到的一个问题,Only the original thread that created a view hierarchy can touch its views.里的 original thread 是什么意思?原始线程指的是我们的 UI 主线程吗?

这时候路人乙心里又想:刚才这么丢人,不能再那么单纯了,我觉得是不是!如果是 UI 线程,为什么不直接写成 Only the UI thread…。

于是他又站了出来,大声说道:“不是指 UI 线程!如果是值 UI 线程,为什么不直接写成 Only the UI thread… !? 肯定不是,这是的话,我把电脑屏幕吃了!”

这下没人按住他了,路人甲、丙心中也没有明确的答案。

那么到底 original thread 是不是 ui thread 呢?我们看一个例子。

修改原来的代码如下:

override fun onResume() {
    super.onResume()

    mBinding.vTvRandom.text = "点击进行网络请求"

    mBinding.vTvRandom.setOnClickListener {
        ThreadPool.runOnNonUIThread({
            Looper.prepare()
          
            val dialog = CommonProgressDialog2.show(
                this, "正在加载",
                null, true, null
            )

            Looper.loop()
        }, 200)
    }

}

将原来的按钮名字改成:“点击进行网络请求”,延迟 200 ms 后,在子线程中让界面中间弹出一个加载窗口。

大家觉得正在加载的窗口能够正常弹出吗?

同学甲觉得,show 一个弹窗,也算 ui 操作吧?在子线程中更新 UI,应该会报错。

运行一下:

4_加载窗口.png

不可思议,弹窗在非 UI 线程被 show 了出来。

同学们都很不解,接下来还有更奇怪的:

在 Looper.loop() 上面添加下面的代码

dialog?.getLoadingTextView()?.setOnClickListener {
		val loadingMsg = dialog.getLoadingMsg()
		dialog.getLoadingTextView().text = "$loadingMsg, 正在加载"
}

点击“正在加载”区域,更新加载文字,在原来文字的基础上再加个“正在加载”。

注意,此时还是在子线程中进行的操作噢,大家觉得这样会报错吗?

会,会吧?会吗?会不会??同学们都蒙了。

运行一下:

5_添加正在加载文字展示.gif

没想到吧,程序还是照常运行。

莫非当前线程都在 UI 线程?我们在程序中加条日志:

Looper.prepare()
val dialog = CommonProgressDialog2.show(
    this, "正在加载",
    null, true, null
)

// 日志 1
JLog.d(TAG, "runOnNonUIThread is in main thread = ${ThreadPool.isMainThread()}")

dialog?.getLoadingTextView()?.setOnClickListener {
  	// 日志 2
    JLog.d(TAG, "setOnClickListener is in main thread = ${ThreadPool.isMainThread()}")
    val loadingMsg = dialog.getLoadingMsg()
    dialog.getLoadingTextView().text = "$loadingMsg, 正在加载"
}

Looper.loop()
2022-05-09 14:59:54.872 24223-24360/com.example.essay D/UICrashActivity: runOnNonUIThread is in main thread = false
2022-05-09 14:59:58.402 24223-24360/com.example.essay D/UICrashActivity: setOnClickListener is in main thread = false

当前确实没有在 UI 线程。

丙同学陷入了沉思:dialog 里面也有 window,但 dialog 打开显示这么久了,ViewRootImpl 对象不可能还没被创建吧。。

接下来更诡异的,我们修改代码,让更新文字的操作,强制运行在 UI 线程,看下会发生什么?

dialog?.getLoadingTextView()?.setOnClickListener {
  	// 转至 UI 线程
    ThreadPool.runUITask {
        JLog.d(TAG, "setOnClickListener is in main thread = ${ThreadPool.isMainThread()}")
        val loadingMsg = dialog.getLoadingMsg()
        dialog.getLoadingTextView().text = "$loadingMsg, 正在加载"
    }
}

6_强制在ui线程修改报错.gif

报错了。。啊这,看看 log。

2022-05-09 15:06:06.953 24748-24810/com.example.essay D/UICrashActivity: runOnNonUIThread is in main thread = false
2022-05-09 15:06:09.107 24748-24748/com.example.essay D/UICrashActivity: setOnClickListener is in main thread = true
2022-05-09 15:06:09.109 24748-24748/com.example.essay E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.essay, PID: 24748
    android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
        at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7056)
        at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1128)
        at android.view.View.requestLayout(View.java:19846)
        at android.view.View.requestLayout(View.java:19846)
        at android.view.View.requestLayout(View.java:19846)
        at android.view.View.requestLayout(View.java:19846)
        at android.view.View.requestLayout(View.java:19846)
        at android.widget.TextView.checkForRelayout(TextView.java:7375)
        at android.widget.TextView.setText(TextView.java:4487)
        at android.widget.TextView.setText(TextView.java:4344)
        at android.widget.TextView.setText(TextView.java:4319)
        at com.jamgu.home.uicrash.UICrashActivity$onResume$1$1$1$1.run(UICrashActivity.kt:60)
        at android.os.Handler.handleCallback(Handler.java:754)
        at android.os.Handler.dispatchMessage(Handler.java:95)
        at android.os.Looper.loop(Looper.java:163)
        at android.app.ActivityThread.main(ActivityThread.java:6401)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:901)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:791)

可见,强制在 UI 线程中修改 UI 也会报错。

似乎,更新 UI 不一定需要在 UI 线程运行。

回过头来,Only the original thread that created a view hierarchy can touch its views.这里的 original 原始线程,真不是指的 UI 线程。

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

ViewRootImpl 里的 mThread 是什么时候被赋值的?我们看下

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

mThread 是在 ViewRootImpl 对象被初始化时创建的,所以 checkThread() 中 mThread != Thread.currentThread()的意思应该是:如果当前的线程与 ViewRootImpl 对象被创建时的线程是同个线程时,checkThread() 通过

这下明白为什么之前没报错,强制执行在 UI 线程中反而报错了!切至 UI 线程后,当前线程就与 dialog 被创建时的线程不一致了!所以报错了!

原来 UI 更新不一定要在 UI 线程

日常开发中我们经常通过 runOnUIThread() 方法,将页面更新操作切换至 UI 线程进行也是因为,这些页面本来就是在 UI 线程中被创建的,我们需要将 UI 更新操作切换至对应的线程。

OHHHHH,同学们听完大受震撼,一个长期以来的误区被理清了!这堂课学到非常多,同学们向老师说着一个又一个 “可以,牛逼” ,以此来表达心中的喜悦。

极其帅气的老师心满意足的离开,同学们,下课!

小知识:”可以,牛逼“,以句子简短精炼的优点深受直男喜爱,乃直男表达认可、赞美时经常使用的口头禅,如果你也渐渐开始可以牛逼了,说明你离直男 ❌ 也不远了。

这篇文章到此结束啦,文章读起来应该挺轻松的,希望对你有所帮助~

文章参考
ViewRootImpl源码
View相关问题解惑(ViewRootImpl,PhoneWindow创建时机,View.post为何可以获取View宽高)
Android UI 线程更新UI也会崩溃???

兄dei,如果觉得我写的还不错,麻烦帮个忙呗 😃
  1. 给俺点个赞被,激励激励我,同时也能让这篇文章让更多人看见,(#.#)
  2. 不用点收藏,诶别点啊,你怎么点了?这多不好意思!
  3. 噢!还有,我维护了一个路由库。。没别的意思,就是提一下,我维护了一个路由库 =.= !!

拜托拜托,谢谢各位同学!

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值