Android neutron threads can update the UI under certain conditions.

A chestnut:

public class MainActivity extends AppCompatActivity {

    private ImageView mImageView;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
      	mImageView = (ImageView)findViewById(R.id.iv);
        new Thread(new Runnable() {
            @Override
            public void run(a) {
				mImageView.setImageResource(R.drawable.ic_book);/ / update the UI} }).start(); }}Copy the code

As shown above, a new thread in the onCreate method performs operations on the mImageView and successfully updates the UI from the child thread.

But if you let the thread sleep for a period of time (say 300ms),

new Thread(new Runnable() {
    @Override
    public void run(a) {
        try {
            Thread.sleep(300);// Sleep 300 ms
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        mImageView.setImageResource(R.drawable.ic_book);/ / update the UI
    }
}).start();
Copy the code

The following error is likely to be reported :(if 300ms is not reported, it can be changed to 1000ms)

  android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
      at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7194)
      at android.view.ViewRootImpl.invalidateChildInParent(ViewRootImpl.java:1111)
      at android.view.ViewGroup.invalidateChild(ViewGroup.java:4833)
      at android.view.View.invalidateInternal(View.java:12102)
      at android.view.View.invalidate(View.java:12062)
      at android.view.View.invalidate(View.java:12046)
      at android.widget.ImageView.setImageDrawable(ImageView.java:456)
      at android.support.v7.widget.AppCompatImageView.setImageDrawable(AppCompatImageView.java:100)
      at android.support.v7.widget.AppCompatImageHelper.setImageResource(AppCompatImageHelper.java:89)
      at android.support.v7.widget.AppCompatImageView.setImageResource(AppCompatImageView.java:94)
      at com.android.rdc.librarysystem.MainActivity$1.run(MainActivity.java:52)
      at java.lang.Thread.run(Thread.java:818)
Copy the code

Analysis of the

Where is the exception thrown from?

An exception can be seen in the error stack message thrown by the ViewRootImpl#checkThread() method.

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

When accessing the UI, ViewRootImpl calls the checkThread method to check whether the current thread accessing the UI is the same thread that created the UI, and if not. An exception is thrown. But why does MainActivity work when it first creates a child thread in the onCreate method to access the UI?

The Thread in the above example was executed before the ViewRootImpl was created. The ViewRootImpl cannot perform performTraversals on the root DecorView of the View Tree. All views in the view tree are not assigned to mAttachInfo (note: AttachInfo stores a set of information. When a View is attached to its parent, AttachInfo is assigned to the View.

When onCreate completes, the Activity does not finish initializing the View Tree. View Tree initialization starts with performTraversals performed by the ViewRootImpl. This traversals the View tree from the root DecorView and initializes all views, including the size layout of the view. And initialization of AttachInfo, ViewParent and other property fields.

ImageView#setImageResource triggers the call flow

ImageView#setImageResource --> if the width or height of the new resource is different from the existing one, View#requestLayout --> meets the condition. ViewRootImpl#requestLayout --> View#invalidate --> View#invalidate(Boolean) --> View#invalidateInternal MAttachInfo and mParent are not null --> ViewGroup#invalidateChild --> ViewRootImpl#invalidateChildInParent --> ViewRootImpl#checkThread CheckThread, If the current thread is not to create the UI thread will throw an exception else -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- //View#invalidateInternal final AttachInfo ai = mAttachInfo; final ViewParent p = mParent; // Redraw if (p!) only if mAttachInfo and mParent are not empty = null && ai ! = null && l < r && t < b) { //.... p.invalidateChild(this, damage); }Copy the code

ViewGroup#invalidateChild will only be called if mAttachInfo and mParent are not null. This method will trigger the checkThread. When the onCreate method is called, the ViewRootImpl is not yet created, and mAttachInfo and mParent are both null, so UI changes in child threads do not report errors.

But at this point the modification to the View is effective. So, how does an application update the UI before ViewRootImpl is created? The original ImageView#setImageResource method assigned the image resource ID to an ImageView attribute, mResource, which will be updated after the ViewRootImpl is created.

When is ViewRootImpl created?

In retrospect, since the method of throwing an exception is a method in ViewRootImpl, you should first look at where and when the ViewRootImpl was created.

If you know anything about the Activity startup process, you should know that both the start and lifecycle of an Activity are triggered by methods corresponding to ActivityThread. We know that every Activity has a top-level View — a DecorView. The DecorView must have been created when the View in the Activity is displayed. The ViewRootImpl, which acts as a “bridge” between the DecorView and WindowManager, should also be created before the view becomes visible. When it comes to views that are visible or not, onResume usually comes to mind (in fact, the Activity’s view may not be visible when onResume is called).

Start with the ActivityThread#handleLaunchActivity method to see the call flow

ActivityThread#handleLaunchActivity --> performLaunchActivity --> handleResumeActivity --> PerformResumeActivity // Call back onResume --> Activity#performResume(); --> Instrumentation#callActivityOnResume --> Activity#onResume(); // call onResume **--> Activity#makeVisible(); --> WindowManagerGlobal#addView() --> root = new ViewRootImpl(view.getContext(), display); // create ViewRootImpl --> ViewRootImpl#setViewCopy the code

As you can see from the flow above, the ViewRootImpl is created in the WindowManagerGlobal#addView() method. It is created after the Activity#onResume method is called. Therefore, if we create a child thread in the onResume method to modify the UI, it will be successful in most cases.

A kernel that updates the UI in a child thread:

Create a handlerThread and call its start method to get looper from the handlerThread to construct a Handler. The View is added to a WindowManger in the handleMessage method of this Handler (which runs in a child thread) and updated.

Sample code address

Conclusion:

The ViewRootImpl is created after the onResume method callback, and we started by creating the child thread in the onCreate method and accessing the UI, and at that point, the ViewRootImpl hasn’t been created yet, We called ImageView#setImageResource on the child thread. Although View#requestLayout and View#invalidate() may trigger, the ViewRootImpl has not been created yet, So the ViewRootImpl#checkThread is not called, that is, the logic to check whether the current thread is the UI that was created is not executed, so the program will run without crashing. And then I changed the program, put the thread to sleep for 300 milliseconds, and the program crashed. The ViewRootImpl is clearly created after 300 milliseconds, and the current thread can be checked with the checkThread method.

The child thread we created in the onCreate method in the opening example accessing the UI is an extreme case. You don’t do that in real development.

Next time someone asks you is it true that the Android neutron thread can’t update the UI? You can reply by saying:

Any thread can update its own UI. Just make sure you meet the following criteria

  • Before the ViewRootImpl was created
    • There is no thread limit for UI modification operations.
  • After the ViewRootImpl is created
    1. Make sure “Create viewrotimPL action” and “Perform UI modification action” are on the same thread. That is, the ViewManager#addView and ViewManager#updateViewLayout methods are called in the same thread.
      • Note: ViewManager is an interface, and the Windows Manager interface inherits from this interface. We usually use Windows Manager (specifically WindowMangerImpl) for view add remove update operations.
    2. The corresponding thread needs to create Looper and call Looper#loop to start the message loop.

If condition 1 is true, we can avoid the exception when the checkThread throws an exception. Why do I need to turn on message recycling?

  • Condition 1 avoids checking exceptions, but there is no guarantee that the UI will be drawn.
  • Condition 2 allows the updated UI to appear
    • Windowmanager #addView will eventually call the WindowManageGlobal#addView method, which triggers the viewrotimp #setView method, The ViewRootImpl#requestLayout method is called internally.
    • For those of you who know UI rendering, the next step is scheduleTraversals, which inserts a message barrier into the message queue and calls Choreographer#postCallback, Insert an asynchronous MSG_DO_SCHEDULE_CALLBACK message into looper. Wait for the vSYNC signal to return.
      • Note: ViewRootImpl has one Choreographer member variable, and the constructor of ViewRootImpl calls Choreographer#getInstance(); Method to get a local instance of Choreographer for the current thread.

Is there a practical case for using child threads to update the UI?

The SurfaceView in Android typically uses a child thread to refresh the page. If our custom View needs to be refreshed frequently, or the refresh process is a lot of data processing, then we can consider using SurfaceView instead of View.

References and learning resources are recommended

  • Is it true that Android subthreads can’t update the UI?
  • Multithreaded learning – Is it really impossible to update the UI in child threads?
  • Android Neutron thread really can’t update UI?
  • Is it a lie that Android only changes the UI in the main thread? Why does this code work perfectly?

Due to my limited skills, I may make mistakes due to misunderstanding or clerical errors. If you find any problems or have any questions about the content of the article, please tell me in the comments section below. Please describe the problem in detail as much as possible, so that I can quickly find the root of the problem. Thank you very much!