Why can’t I update the UI on a child thread
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)
Copy the code
“android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views. If you update the UI on a child thread, you get an error, so why can’t you update the UI on a child thread? Is it really possible to update the UI on a child thread?
Check the requestLayout and checkThread of the ViewRootImpl.
public void requestLayout(a) {
if(! mHandlingLayoutInLayoutRequest) { checkThread(); mLayoutRequested =true; scheduleTraversals(); }}Copy the code
It executes the checkThread method and throws an exception
void checkThread(a) {
if(mThread ! = Thread.currentThread()) {throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views."); }}Copy the code
Many people would think that this method checks the main thread ActivityThread, but if you look at the source code, it checks whether the mThread is the current thread, so you should look at the assignment of the mThread
mThread = Thread.currentThread();
Copy the code
MThread is the thread on which the constructor is called, so whether the method is called by the main thread depends on how the ViewRootImpl is created. If the method is called from an ActivityThread, it is the main thread.
Requestlayout (checkThread) {requestLayout (checkThread) {requestLayout (checkThread) {requestLayout (checkThread) {requestLayout (checkThread) {requestLayout (checkThread) {requestLayout (checkThread) {requestLayout (checkThread) { RequestLayout of ViewRootImpl is executed.
View::requestLayout
public void requestLayout(a) {...if(mParent ! =null && !mParent.isLayoutRequested()) {
mParent.requestLayout();
}
Copy the code
View requestLayout () {parent requestLayout () {parent requestLayout () {ViewRootImpl (); And how is that done?
To answer this question, you need to understand the structure of your Activity.
The structure of the Activity page
When developing an Activity, the first step is to pass in the onCreate setContentView to the resource file.
Activity::setContentView Note that this is not analyzing AppCompatActivity
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
Copy the code
The Activity actually calls the setContentView of getWindow(). The Activity first holds a Window, which is an abstract class that has a unique implementation called PhoneWindow, You can think of each Activity as having a PhoneWindow first. There is also a DecorActionBar, which is a ViewStub that sets whether the page contains an ActionBar. ViewStub performs better than setting Invisible.
PhoneWindow::setContentView
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
...
mLayoutInflater.inflate(layoutResID, mContentParent);
Copy the code
Determine if there is contentParent, if not, go to installDecor.
private void installDecor(a) {
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);
Copy the code
Here the end generateLayout (mDecor) to the contentParent, new a DecorView generateDecor method. The layoutInflater.inflate method is then called in the PhoneWindow setContentView method to inflate the XML resource file.
To summarize, there is a PhoneWindow built into the Activity. The outermost layer of the PhoneWindow is a DecorView with an ActionBar that is a ViewStub. If the parent of a DecorView is the ViewRootImpl, it depends on how the ViewRootImpl was created.
The ViewRootImpl creation process
ActivityThrad calls performResumeActivity in the handleResumeActivity, and then executes WindowManagerImpl addView, Windows ManagerGlobal’s addView method new view file. The performResumeActivity method calls the activity’s performResume internally, and then executes the Instrumentation callActivityOnResume, This method calls the Activity’s onResume method. One conclusion to draw from this is that the activity has not yet created the page when it executes 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);Copy the code
WMGlobal executes the view otimpl setView method after addView
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);
Copy the code
The ViewRootImpl setView method has a key method, RequestLayout, which is also seen in the initial exception, and then checkThread, initialization process thread is of course consistent, This also answers the question of whether mThread is the main thread, in this case the call stack is clearly the main thread. One of the key methods in requestLayout is scheduleTraversals
void scheduleTraversals(a) {
if(! mTraversalScheduled) { mTraversalScheduled =true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); notifyRendererOfFramePending(); pokeDrawLockIfNeeded(); }}Copy the code
You can see that an asynchronous task is performed to execute mTraversalRunnable which is also called doTraversal
void doTraversal(a) {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false; }}}Copy the code
Formtraversals is the core method of view, measuring, plotting, and layout. So if I go back to setView, I have another line down here
view.assignParent(this);
Copy the code
Passing the view to the view, which is the argument to the setView, which is the argument to WMGlobal’s addView, which is called by ActivityThread, passes the DecorView
r.window = r.activity.getWindow(); View decor = r.window.getDecorView(); . wm.addView(decor, l);Copy the code
At this point, the ViewRootImpl becomes the parent of the DecorView, so the call stack reporting the error is clear.
Page rendering
Back to performTraversals
booleanlayoutRequested = mLayoutRequested && (! mStopped || mReportNextDraw);if (layoutRequested) {
...
// Ask host how big it wants to be
windowSizeMayChange |= measureHierarchy(host, lp, res,
desiredWindowWidth, desiredWindowHeight);
Copy the code
It contains a variable mLayoutRequested, which is set to true every time layoutRequest is called and the first setView is invoked. If it is true, getRootMeasureSpec is invoked on measureHierarchy
MeasureSpec is set according to rootDimension. Once the width and height are set, performMeasure is called in the DecorView measure method, where the first measure of the control tree is made.
And then a second measurement, same principle as WRapcontent, because you can’t determine the size with one measurement
Back to the performTraversals method, performLayout is called
final booleandidLayout = layoutRequested && (! mStopped || mReportNextDraw);boolean triggerGlobalLayoutListener = didLayout
|| mAttachInfo.mRecomputeGlobalAttributes;
if (didLayout) {
performLayout(lp, mWidth, mHeight);
Copy the code
The layout method of the DecorView is called inside
if (triggerGlobalLayoutListener) {
mAttachInfo.mRecomputeGlobalAttributes = false;
mAttachInfo.mTreeObserver.dispatchOnGlobalLayout();
}
Copy the code
Then use the dispatchOnGlobalLayout of the ViewTreeObserver to return the gauge size information
Here mLayoutRequested is set to false, and if this method is reinvoked because of an exception it does not remeasure directly on the draw panel
Back to PerformTraversals
booleancancelDraw = 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();
Copy the code
When the page is not invisible, performDraw is called to draw
In performDraw, the Viewrotimpl draw method is called, and the hardware acceleration is set for ThreadedRender’s draw and the software is set for DecorView’s Draw
How do I update the UI in child threads
Now that we know why we can’t update the UI on the child thread, what do we do if we have to update the UI on the child thread?
requestLayout
Go back to the requestLayout of the View
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;
if(mParent ! =null && !mParent.isLayoutRequested()) {
mParent.requestLayout();
}
Copy the code
PFLAG_FORCE_LAYOUT is a flag that is checked in requestLayout and cleared when the layout is complete. If a requestLayout method is used in the main thread, this flag is set to true. The parent requestLayout is called only when the view flag is false. Requestlayout will not be called to the decorView and will not execute the checkThread.
Handwritten addView
The checkThread method checks the addView thread, which is the main thread when the Decorview is initialized, but you can call the WindowManager to write the addView. So you need to start a looper on the child thread yourself. This will allow child threads to update the UI.
SurfaceView
After all, UI asyncrony and latency can cause a lot of display and interaction problems, and if the above two are risky evils, Android SurfaceView is not. There is a holder in the SurfaceView, which can obtain the Canvas object and draw the UI by itself in the sub-thread. Because of this, SurfaceView has high performance and is commonly used in games, audio and video scenes.