为什么不能在子线程更新UI
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8798)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1606)
at android.view.View.requestLayout(View.java:25390)
"android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views"这句异常大家都见过。如果在子线程更新UI就会报错,那么为什么不能在子线程更新UI呢,就真的不可以在子线程更新UI吗?
大家看到报错一般都会直接点到报错行,这里就直接来看一下这个ViewRootImpl的requestLayout和checkThread。
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
里面执行了这个checkThread方法,抛出了异常
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
很多人在这里就会认为这个方法检查了主线程ActivityThread,但看源码发现是判断mThread是不是当前线程,所以要去看mThread的赋值
mThread = Thread.currentThread();
可以看到mThread是构造函数被调用的时候的 线程,那么这个方法是不是被主线程调用的就得看ViewRootImpl的创建过程,而该方法如果是在ActivityThread里被调用的,不当然是主线程了吗,这里留个疑问。
当分析ViewRootImpl的构造的时候看到了requestlayout,看到了这个checkThread方法,也有人会直接根据报错来看这个方法,不过看报错代码可以看到是在不断执行View的requestLayout,怎么就执行到了ViewRootImpl的requestLayout了呢?
View::requestLayout
public void requestLayout() {
...
if (mParent != null && !mParent.isLayoutRequested()) {
mParent.requestLayout();
}
看了View的requestLayout发现这个方法实在不断调用parent的requestLayout,那么View最终的parent就是ViewRootImpl了吗?这又是如何做到的呢。
要回答这个问题就需要先了解Activity的结构。
Activity页面的结构
当开发一个Activity的时候,首先要在onCreate里setContentView,把资源文件传入。
Activity::setContentView 注意这里分析的不是AppCompatActivity
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
实际上Activity去调用了getWindow()的setContentView,Activity首先持有了一个Window,而window是一个抽象类,它有一个唯一实现就是PhoneWindow,可以认为每个Activity里首先是含有一个PhoneWindow。这里还有一个DecorActionBar,这是一个ViewStub用于设置页面是否含有ActionBar。ViewStub比设置invisible性能更高。
PhoneWindow::setContentView
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
...
mLayoutInflater.inflate(layoutResID, mContentParent);
判断是否含有contentParent,没有的话就去installDecor。
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
mDecor = generateDecor(-1);
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
这里最终把generateLayout(mDecor)给到了contentParent,generateDecor方法new了一个DecorView。之后仍然在PhoneWindow的setContentView方法中调用了layoutInflater.inflate方法把xml资源文件进行inflate。
简单总结一下Activity内置一个PhoneWindow,PhoneWindow最外层是个DecorView,上面有个ActionBar是个ViewStub。 那么再回到上面的问题,是否DecorView的parent就是ViewRootImpl,这就得看ViewRootImpl的创建过程了。
ViewRootImpl的创建过程
追溯源码直接说结论,ActivityThrad在handleResumeActivity里调用了performResumeActivity,然后执行了WindowManagerImpl的addView,WindowManagerGlobal的addView方法new了ViewRootImpl。 performResumeActivity方法内部调用了activity的performResume,然后执行了Instrumentation的callActivityOnResume,在这个方法里调用了Activity的onResume方法。在这里可以得出一个结论,activity在执行onResume的时候还没有创建好页面。 handleResumeActivity:
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
...
wm.addView(decor, l);
再看WMGlobal在addView之后又执行了ViewRootImpl的setView方法
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);
而ViewRootImpl的setView方法中有一个很关键的方法,requestlayout,这也是在一开始的异常报错看见的方法,然后checkThread,初始化过程thread当然是一致的,这里也回答了上面的问题mThread是不是主线程,在这里这个调用栈很明显是主线程。在requestLayout里有个很关键的方法 scheduleTraversals
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
可以看到是执行了一个异步任务去执行mTraversalRunnable这个Runnanble也就是doTraversal
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
而performTraversals就是view的核心方法,测量、绘制、布局。 再回到setview,下面还有一行
view.assignParent(this);
把ViewRootImpl给了这个view,这个view是setview的参数,而setview的参数是WMGlobal的addView的参数也就是ActivityThread调用的把DecorView传递过来了
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
...
wm.addView(decor, l);
至此,ViewRootImpl就成为了DecorView的parent,这样报错的调用栈就清晰了。
页面绘制
回过头看performTraversals
boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
if (layoutRequested) {
...
// Ask host how big it wants to be
windowSizeMayChange |= measureHierarchy(host, lp, res,
desiredWindowWidth, desiredWindowHeight);
里面有一个变量mLayoutRequested,在每次调用layoutRequest和第一次setView的时候会设置为true,这个true了就会重新布局,在measureHierarchy里调用了getRootMeasureSpec
根据rootDimension设置measureSpec,设置好了宽高以后调用performMeasure,在里面调用了DecorView的measure方法,在这里完成了控件树的第一次测量。
然后进行第二次测量,原理同wrapcontent,因为一次测量没法确定大小
回到performTraversals方法,后面调用了performLayout
final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
boolean triggerGlobalLayoutListener = didLayout
|| mAttachInfo.mRecomputeGlobalAttributes;
if (didLayout) {
performLayout(lp, mWidth, mHeight);
在里面调用了DecorView的layout方法
if (triggerGlobalLayoutListener) {
mAttachInfo.mRecomputeGlobalAttributes = false;
mAttachInfo.mTreeObserver.dispatchOnGlobalLayout();
}
之后使用ViewTreeObserver的dispatchOnGlobalLayout回调控件大小信息
这里mLayoutRequested 设置为false,如果因为异常重新调用这个方法不会重新测量直接绘制
再回到PerformTraversals
boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
if (!cancelDraw) {
if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).startChangingAnimations();
}
mPendingTransitions.clear();
}
performDraw();
当页面不是不可见的时候,就调用performDraw进行绘制
performDraw里调用了ViewRootImpl的draw方法,在这里有硬件加速的设置,硬件加速方法是ThreadedRender的draw,软件是DecorView的draw方法
如何在子线程更新UI
现在知道了为什么不能在子线程更新UI以后,那么如果就一定要在子线程更新UI需要怎么做呢?
requestLayout
回到View的requestLayout
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;
if (mParent != null && !mParent.isLayoutRequested()) {
mParent.requestLayout();
}
里面有flag叫PFLAG_FORCE_LAYOUT,在requestlayout里面会进行判断,而layout完成后会清除该flag,如果在主线程里写了requestlayout方法,那么这个flag就会被设置为true,只要在这个view的flag为false的时候才会去调用parent的requestlayout。所以就不会调用到decorview的requestlayout,自然就不会去执行checkThread就不会报错。
手写addView
前面看到checkThread方法其实判断的是addView,Decorview初始化的时候的那个线程,一般来说是主线程,但是可以自己调用windowmanager去写addView,这个时候因为需要一个looper,所以还需要自己在子线程去启动一个looper。这样就可以子线程更新UI了。
SurfaceView
毕竟UI的异步和延迟会导致很多显示和交互的问题,如果说上面两种属于有风险的歪门邪道的话,Android官方提供的SurfaceView就不是了。在SurfaceView里有个holder,可以获取到canvas对象,自己用canvas去绘制UI就可以在子线程进行了,正因如此SurfaceView具备较高的性能,在游戏、音视频场景比较常用。