Detailed explanation of the relationship between Android main thread and child thread

Detailed explanation of the relationship between Android main thread and child thread

Main thread and screen rendering

When the user launches an application, Android creates a new Linux process and execution thread. This main thread is also called the interface thread (UI thread) and is responsible for all activities that occur on the screen.

In Android, the design of the main thread is very simple: its only job is to get tasks (work blocks) from the thread-safe work queue and execute them until the application is terminated.

These tasks performed by the main thread come from the following sources: callbacks related to life cycle information, user events (such as input), or events from other applications and processes. Of course, we can also implement task queues by ourselves without using frameworks in application development.

Almost any code block executed by the application is associated with event callbacks (such as input, layout expansion, or drawing). When an operation triggers an event, the child thread that has the event will push the event from the child thread to the message queue of the main thread. Then, the main thread can provide services for the event.

When an animation or screen update is being performed, the system will try to perform a task (responsible for drawing the screen) every 16ms or so, so as to render at a smooth speed of 60 frames per second. For the system to achieve this goal, the interface/view hierarchy must be updated on the main thread. However, if there are too many or too long tasks in the message queue of the main thread, the main thread cannot complete the update fast enough. If the main thread cannot complete the task within 16ms, the user may perceive a freeze, delay, or the interface does not respond to input. If the main thread is blocked for about 5 seconds, the system will display an "application not responding" (ANR) dialog box, allowing the user to directly close the application.

In order to avoid the main thread from performing a large number of time-consuming tasks and causing problems such as stalls, we should move a large number of or time-consuming tasks from the main thread to the sub-threads for processing, so that it does not affect smooth rendering and quickly respond to user input.

Reference relationship between child threads and UI objects

According to the design of Android, UI objects in Android are not thread-safe. Regardless of whether it is creating, using or destroying UI objects, the application should be performed on the main thread. If you try to modify or even reference UI objects in threads other than the main thread, it may cause exceptions, silent failures, crashes, and other undefined abnormal behaviors.

Therefore, we must operate UI objects in the main thread. As mentioned above, we need to perform time-consuming tasks in child threads, but what if the time-consuming tasks we perform are related to UI objects? For example, our common data request operation is to request data first, and then update the UI object. Requesting data is usually a time-consuming operation. If it is operated on the main thread, it will inevitably cause the main thread to block and cause jams. So, how do we use child threads to solve the problem?

Before discussing, let's discuss the two types of UI object references from child threads: explicit references and implicit references.

Show references

The ultimate goal of many tasks on non-main threads is to update UI objects. However, if one of the threads accesses an object in the view hierarchy, the application may become unstable: if the worker thread changes the properties of the object while any other threads are referencing the object at the same time, the result cannot be determined.

For example, suppose an application directly references UI objects on the worker thread. The object on the worker thread may contain a reference to the View; but before the work is complete, the View is removed from the view hierarchy. When these two operations occur at the same time, the reference will keep the View object in memory and set properties on it. However, the user will never see the object at this time, and the application will delete the object after the object reference disappears.

To give another example, suppose that the View object contains a reference to the Activity to which it belongs. If the Activity is destroyed, but there are still thread processing tasks that directly or indirectly refer to it, the garbage collector will wait until the processing task is completed before collecting the Activity.

If an Activity lifecycle event (such as screen rotation) occurs during the execution of thread processing work, this situation may cause problems. The system will not be able to perform garbage collection until the work in progress is completed. Therefore, when garbage collection is available, there may be two Activity objects in memory.

In such cases, we must not include explicit references to interface objects in the application's thread processing tasks. Avoiding such references helps prevent these types of memory leaks while avoiding thread processing contention.

In any case, the application should only update the interface objects on the main thread. How should the child thread and the main thread work together?

We can go from the main thread to the sub-thread, and use the sub-thread to handle time-consuming tasks (such as the data request operation in this example). When the task processing is completed, return to the main thread to perform UI updates. In this way, the work task is processed in the child thread, and the final UI view update is transferred back to the main thread for unified processing, avoiding various problems caused by the child thread displaying the reference UI object.

Implicit reference

In Java internal classes (including anonymous internal classes and named internal classes), their objects will implicitly hold references to external class objects, which will cause the problem that external classes cannot be garbage collected.

Give a common example in Android:

    public class MainActivity extends Activity {
      // ...
      public class MyAsyncTask extends AsyncTask<Void, Void, String>   {
        @Override protected String doInBackground(Void... params) {...}
        @Override protected void onPostExecute(String result) {...}
      }
    }

In the sample code, the thread processing object MyAsyncTask is declared as a non-static inner class of an Activity (or an inner class in Kotlin). This declaration causes the inner class MyAsyncTask to hold an implicit reference to the Activity instance. Therefore, before the thread processing work is completed, the object always contains a reference to the corresponding Activity, which causes a delay in the destruction of the referenced Activity and causes a memory leak problem.

How to solve it?

In fact, it's simple, just define the inner class MyAsyncTask as a static class to remove the implicit reference.

Declare the AsyncTask object as a static nested class (or remove the internal qualifier in Kotlin). Doing so can eliminate the implicit reference problem, because static nested classes are different from inner classes: instances of inner classes require instantiation of instances of outer classes, and can directly access methods and fields of encapsulated instances. In contrast, a static nested class does not need to refer to an instance of the encapsulating class, so it does not contain references to members of the outer class.

    public class MainActivity extends Activity {
      // ...
      static public class MyAsyncTask extends AsyncTask<Void, Void, String>   {
        @Override protected String doInBackground(Void... params) {...}
        @Override protected void onPostExecute(String result) {...}
      }
    }

Thread and application Activity life cycle

The application life cycle affects how the child threads work in the APP. We may need to determine whether the child thread should be retained after the Activity is destroyed, and we should also pay attention to the relationship between the thread priority and whether the Activity is running in the foreground or the background.

Reserved thread

When the UI interface is destroyed, we need to confirm whether we should continue to retain the child thread to continue to perform the original task.

The following is an example.

Assuming that an Activity generates a set of threads to process work tasks, and then is destroyed before the work threads can perform the work tasks, how should the application handle the work tasks being performed?

If the work task will update the interface that has been destroyed, the work does not need to continue. For example, if the task is to load user information from the database and then update the view, the thread is no longer needed.

In contrast, work tasks deal with data operations that are not completely related to updating the interface, and the data may need to be used later. In this case, we should keep the thread. For example, a data packet may be waiting to download an image, cache it to disk, and update the associated View object. Although the object no longer exists, it may still be useful to download and cache the image in case the user returns to the destroyed Activity.

Thread priority

When creating and managing threads in an application, be sure to set the thread priority so that the thread gets the correct priority. If the priority of the sub-thread is set too high, it may interfere with the main thread and the rendering thread, causing the application to drop frames and cause jams. If the priority of the child thread is set too low, it may cause asynchronous tasks (such as image loading) to fail to reach the required speed.

The system's thread scheduler will give priority to higher-priority threads, and make a trade-off between these priorities and the need to eventually complete all work. Generally speaking, the foreground group accounts for about 95% of the total equipment execution time, while the background group accounts for about 5%.

Every time a thread is created, setThreadPriority() should be called to set the thread's priority.

For the setting of thread priority, please refer to "The Correct Way to Set Thread Priority in Android (2 Methods)" .

In addition, we usually need to set the priority of threads in the thread pool. How to do it? Please refer to: "Android Code Implementation of Thread Priority in Thread Pool" .

The system also uses the Process class to assign each thread its own priority value.

By default, the system sets the priority of a thread to have the same priority and group membership as the thread that spawned it, that is, by default, the priority of the child thread inherits the priority of the parent thread that created it. However, we should use setThreadPriority() to explicitly adjust the thread priority.

The Process class provides a set of constants for thread priority to help simplify the assignment of priority values. For example, THREAD_PRIORITY_DEFAULT represents the default value of the thread. If the work performed by the thread is not too urgent, the application should set the thread's priority to THREAD_PRIORITY_BACKGROUND.

We can also use THREAD_PRIORITY_LESS_FAVORABLE and THREAD_PRIORITY_MORE_FAVORABLE constants as incrementers to set relative priority.

How to create and use asynchronous threads

Android provides the same Java classes and primitives to facilitate thread processing, such as Thread, Runnable and Executors classes. And, in order to help ease and develop thread processing for Android, Android provides a set of auxiliary programs that can assist in development, such as AsyncTaskLoader and AsyncTask. Each auxiliary class has a specific set of performance nuances, dedicated to solving a small number of specific thread processing problems. Using the wrong class in the wrong situation can cause performance problems.

For the method of using threads in Android, you can refer to the previous article: "Detailed Explanation of Six Implementation Methods of Android Asynchronous Tasks" .

How many threads should we create?

As mentioned above, the use of sub-threads can prevent the main thread from stalling due to time-consuming tasks. So, is it better to create more threads?

Although at a technical level, we can create hundreds of threads in the code, but doing so can cause performance issues. Our application shares limited CPU resources with background services, rendering programs, audio engines, networks, etc. The CPU can actually only process a small number of threads in parallel (this is related to the number of cores of the device); once the limit is exceeded, it will encounter priority and scheduling problems. Therefore, it is important to create the right number of threads based on workload requirements. In addition, the priority of child threads must be controlled at the same time.

The child thread and the main thread compete for CPU resources:

[External link image transfer failed. The source site may have an anti-leech link mechanism. It is recommended to save the image and upload it directly (img-Tl4v5SpC-1608193450595)(evernotecid://6FE75482-54A0-433A-9625-A01F7FEE92EC/appyinxiangcom/9896050/ENResource /p2888)]

In fact, how many threads to create depends on many variables, but you can choose a value (for example, choose 4 first) and use Systrace for testing. This strategy is as reliable as any other strategy. We can use trial and error to find at least how many threads should be reduced to avoid problems.

When deciding how many threads to create, you also need to consider that threads are not free, they will take up memory. Threads in Android are finally created through the native layer. FixStackSize is used to set the thread stack size. By default, the total memory size required by the thread stack = 1M + 8k + 8k, which is 1040k. The numerous applications installed on the device will add up to this number quickly, especially when the call stack is significantly expanded.

In fact, under normal circumstances, we should reuse threads for resource optimization. We can reuse the existing thread pool, which can reduce memory and processing resource contention, thereby helping to improve performance.


**PS: For more exciting content, please check --> "Android Development"
**PS: For more exciting content, please check --> "Android Development"
**PS: For more exciting content, please check --> "Android Development"

Guess you like

Origin blog.csdn.net/u011578734/article/details/111318450