案例二,子线程和主线程分别showToast
1)onCreate方法中弹出toast,崩溃——Can't toast on a thread that has not called Looper.prepare()
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_ui) thread { showToast("我去年买了个表") } }
2)onCreate
方法中弹出toast,增加Looper.prepare(),Looper.loop()
方法。不崩溃。
加上延时3秒,不崩溃。
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_ui) thread { //Thread.sleep(3000) Looper.prepare() showToast("我去年买了个表") Looper.loop() } }
3)使用同一个Toast
实例,在子线程中的Toast
没消失之前点击按钮,在主线程中修改Toast
文字并显示,则程序崩溃——Only the original thread that created a view hierarchy can touch its views.。
重新运行,在子线程中显示并消失后,点击按钮,不崩溃。
换个手机——三星s9
,重新运行,在子线程中的Toast
没消失之前点击按钮,不崩溃。
lateinit var mToast: Toast override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_ui) thread { Looper.prepare() mToast=Toast.makeText(this@UIMainActivity,"我去年买了个表",Toast.LENGTH_LONG) mToast.show() Looper.loop() } btn_ui.setOnClickListener { mToast.setText("我今年买了个表") mToast.show() } }
案例二分析
在解开谜底之前,我们先了解下Toast
。
Toast原理
Toast.makeText(this,msg,Toast.LENGTH_SHORT).show()
简单又常用的一句代码,还是通过流程图的方式看看它是怎么创建并展示的。
和DecorView
加载绘制流程如出一辙,首先加载了布局文件,创建了View
。然后通过addView
方法,再次新建一个ViewRootImpl
实例,作为parent
,进行测量布局和绘制。
崩溃分析
1)首先,说下第一次崩溃——Can't toast on a thread that has not called Looper.prepare()
,也就是在创建Toast
的线程必须要有Looper
在运行。
根据源码我们也得知Toast
的显示和隐藏都是通过Handler
传递消息的,所以必须要有Handler
使用环境,也就是绑定Looper
对象,并且通过loop
方法开始循环处理消息。
2)第二次崩溃——Only the original thread that created a view hierarchy can touch its views
。
这里的崩溃和之前更新Button
一样的报错,所以我们有理由怀疑也是一样的原因,在不同的线程调用了ViewRootImpl
的requestLayout
方法。
我们看到点击按钮的时候,调用了mToast.setText()
方法,咦,这不就跟案例一一模一样
了吗。
setText
方法中调用了TextView
的setText()
方法,然后由于Toast中的TextView宽高都是wrap_content
的,所以会触发requestLayout
方法,最后会调用到最上层View也就是ViewRootImpl
的requestLayout
方法。
所以崩溃的原因就是因为Toast
在第一次在子线程中show的时候,新建了一个ViewRootImpl
实例,绑定了当前线程也就是子线程到mThread
变量。然后同一个Toast
,在主线程调用setText方法,最终会调用到ViewRootImpl的requestLayout
方法,引起线程检查,当前线程也就是主线程并不是当初那个创建ViewRootImpl
实例的线程,所以导致崩溃。
3)那为什么等Toast消失之后,点击按钮又不崩溃了呢?
原因就在Toast的hide
方法中,最终会调用到View的assignParent
方法,将Toast的mParent
设置为null,也就是ViewRootImpl
设置为null了。所以调用setText方法的时候也就执行不到requestLayout
方法了,也就不会到checkThread
方法检查线程了。贴下代码:
public void handleHide() { if (mView != null) { if (mView.getParent() != null) { mWM.removeViewImmediate(mView); } mView = null; } } removeViewImmediate--->removeViewLocked private void removeViewLocked(int index, boolean immediate) { ViewRootImpl root = mRoots.get(index); View view = root.getView(); //... if (view != null) { view.assignParent(null); if (deferred) { mDyingViews.add(view); } } } void assignParent(ViewParent parent) { if (mParent == null) { mParent = parent; } else if (parent == null) { mParent = null; } else { throw new RuntimeException("view " + this + " being added, but"+ " it already has a parent"); } }
4)但是但是,为啥换个手机又不崩溃了呢?
这是我偶然发现的,在我的三星S9
手机上,运行时不会崩溃的,而且界面给我的反馈并不是修改当前页面上Toast
上的文字,而是像新建了一个Toast
展示,即时代码中写的是setText
方法。
所以我猜测在部分手机上,应该是改变了Toast
的设置,当调用setText
方法的时候,就会马上结束当前的Toast
展示,调用hide
方法。然后再进行Toast
文字修改并展示,也就是刚才第三点的做法。
当然这只是我的猜测,有研究过手机源码的大神也可以补充下。
总结
任何线程都可以更新UI,也都有更新UI导致崩溃的可能。
其中的关键就是view被绘制到界面时候的线程(也就是最顶层ViewRootImpl
被创建时候的线程)和进行UI更新时候的线程是不是同一个线程,如果不是就会报错。
参考
https://www.jianshu.com/p/1cdd5d1b9f3d
https://www.cnblogs.com/fangg/p/12917235.html
拜拜~