报错
UI 的线程检查机制就已经建立了,所以在子线程更新就会报错。
子线程更新的错误定位
子线程更新的错误定位是 ViewRootImpl
中的 checkThread
方法和 requestLayout
方法。
// 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();
}
}
ViewRootImpl 是何时创建的。
在 ActivityThread
的 handleResumeActivity
中调用了 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
是在 activity
的 onResume
方法调用后才由 WindowManagerGlobal
的 addView
方法创建。
然后ViewRootImpl构造方法中会拿到当前的线程,
public ViewRootImpl(Context context, Display display) {
mContext = context;
...
mThread = Thread.currentThread();
...
}
所以在ViewRootImpl的checkThread()中,确实是 拿 当前想要更新UI的线程 和 添加window时的线程作比较,不是同一个线程机会报错。
checkThread()调用条件
Only the original thread that created a view hierarchy can touch its views
通过checkThread()
抛出。
通过对 checkThread()
执行 「alt + F7」发现:
其中我们最熟悉的就是 requestLayout()
和 invalidate()
invalidate()的调用链中会走到 invalidateChildInParent()。
分析invalidate()时需要特别注意:
即开启硬件加速的情况下,invalidate()会走特殊流程后直接 return 并不会调用 checkThread()
target API 级别为 14 及更高级别,则硬件加速默认处于启用状态
基于以上分析, 得出结论: requestLayout()
和 未开启硬件加速的invalidate()
会触发checkThread()
其实硬件加速我们基本都不会关闭,只有在自定义view时,当使用了硬件加速不支持的API时才会关掉。
那为啥要一定需要checkThread呢?
因为UI控件不是线程安全的。那为啥不加锁呢?一是加锁会让UI访问变得复杂;二是加锁会降低UI访问效率,会阻塞一些线程访问UI。所以干脆使用单线程模型处理UI操作,使用时用Handler切换即可。
Toast可以在子线程show吗?
Toast可以在子线程show吗?答案是可以的
new Thread(new Runnable() {
@Override
public void run() {
//因为添加window是IPC操作,回调回来时,需要handler切换线程,所以需要Looper
Looper.prepare();
addWindow(button);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
button.setText("文字变了!!!");
}
},3000);
Toast.makeText(MainActivity.this, "子线程showToast", Toast.LENGTH_SHORT).show();
//开启looper,循环取消息。
Looper.loop();
}
}).start();
Toast也是window,show的过程就是添加Window的过程。
另外注意1,这个线程中Looper.prepare()和Looper.loop(),这是必要的。
因为添加window的过程是和WindowManagerService进行IPC的过程,IPC回来时是执行在binder线程池的,而ViewRootImpl中是默认有Handler实例的,这个handler就是用来切换binder线程池的消息到当前线程。
另外Toast还与NotificationMamagerService进行IPC,也是需要Handler实例。既然需要handler,那所以线程是需要looper的。另另外Activity还与ActivityManagerService进行IPC交互,而主线程是默认有Looper的。
扩展开,想在子线程show Toast、Dialog、popupWindow、自定义window,只要在前后调Looper.prepare()和Looper.loop()即可。
activity的onCreate
因为Activity的window添加在首次onResume之后执行的的,那ViewRootImpl的创建也是在这之后,所以也就无法checkThread了。实际上这个时期也不checkThread,因为View根本还没有显示出来。
onCreate()中执行是OK的:
绕过线程检测
原理:通过 ViewTreeObserver.OnGlobalLayoutListener 设置全局的布局监听,然后在 onGlobalLayout 方法中,调用 view 的 setLayoutParams 方法,setLayoutParams 方法内部会调用 requestLayout,这样就可以绕过线程检测。
为什么能绕过呢?
因为 setLayoutParams
中调用的 requestLayout
方法并不是 ViewRootImpl
中 requestLayout
.
而 View
的 requestLayout
并不调用 checkThread
方法去检测线程。
// 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();
}
}
// 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;
}
}
dialog
TextView.setText()
TextView.setText()引起的checkThread()只能通过requestLayout()触发?
TextView.setText()通过checkForRelayout()完成UI更新。
@UnsupportedAppUsage
private void checkForRelayout() {
// If we have a fixed width, we can just swap in a new text layout
// if the text height stays the same or if the view height is fixed.
if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT
|| (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth))
&& (mHint == null || mHintLayout != null)
&& (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
// Static width, so try making a new text layout.
int oldht = mLayout.getHeight();
int want = mLayout.getWidth();
int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();
/*
* No need to bring the text into view, since the size is not
* changing (unless we do the requestLayout(), in which case it
* will happen at measure).
*/
makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(),
false);
if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
// In a fixed-height view, so use our new text layout.
if (mLayoutParams.height != LayoutParams.WRAP_CONTENT
&& mLayoutParams.height != LayoutParams.MATCH_PARENT) {
autoSizeText();
invalidate();
return;
}
// Dynamic height, but height has stayed the same,
// so use our new text layout.
if (mLayout.getHeight() == oldht
&& (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
autoSizeText();
invalidate();
return;
}
}
// We lose: the height has changed and we have a dynamic height.
// Request a new view layout using our new text layout.
requestLayout();
invalidate();
} else {
// Dynamic width, so we have no choice but to request a new
// view layout with a new text layout.
nullLayouts();
requestLayout();
invalidate();
}
}
源码表明,当TextView 的宽高不变时,调用了invalidate()而非requestLayout(), 结合本文前部分的结论,此时如果开启了硬件加速,就不会调用checkThrea()。 所以当我们在 Activity#onCreate() 中,在子线程中对宽高一定的TextView执行setText(...)时,应用不会崩溃。