Do you know how to ask Handler questions at the elementary, intermediate and advanced levels?

Do you know how to ask Handler questions at the elementary, intermediate and advanced levels?

Handler is a message processing mechanism in Android. It is a solution for inter-thread communication. You can also think of it as creating a queue for us in the main thread. The order of messages in the queue is the delay time we set. If you want to implement a queue function in Android, you might as well consider it first. This article is divided into three parts:

  • Handler source code and answers to common questions

1. How many Handlers, Loopers, and MessageQueues can there be in a thread at most?

2. Why doesn’t the infinite loop of Looper cause the application to freeze? Will it consume a lot of resources?

3. How to update UI in child thread, such as Dialog, Toast, etc.? Why does the system not recommend updating UI in child thread?

4. How does the main thread access the network?

5. How to deal with memory leaks caused by improper use of Handler?

6. What are the application scenarios of Handler message priority?

7. When does the main thread's Looper exit? Can it be exited manually?

8. How to determine whether the current thread is the Android main thread?

9. What is the correct way to create a Message instance?

  • Handler in-depth question answering

1. ThreadLocal

2. epoll mechanism

3. Handle synchronization barrier mechanism

4. Handler lock related issues

5. Synchronization methods in Handler

  • Some applications of Handler in the system and third-party frameworks

1. HandlerThread

2. IntentService

3. How to build an app that doesn’t crash

4. Application in Glide

Handler source code and answers to common questions

Here is the official definition:

  1. A Handler allows you to send and process Message and Runnable objects associated with a thread's MessageQueue. Each Handler instance is associated with a single thread and that thread's message queue. When you create a new Handler it is bound to a Looper. It will deliver messages and runnables to that Looper's message queue and execute them on that Looper's thread.

The general idea is that Handler allows you to send Message/Runnable to the thread's message queue (MessageQueue). Each Handler instance is associated with a thread and the message queue of that thread. When you create a Handler, you should bind it to a Looper (the main thread has created a Looper by default, and the child thread needs to create a Looper by itself). It sends Message/Runnable to the corresponding message queue of the Looper and processes the corresponding Message/Runnable in the thread where the Looper is located. The following figure is the workflow of Handler

Handler workflow diagram

It can be seen that in Thread, the conveyor belt of Looper is actually an infinite loop. It continuously takes messages from the message queue MessageQueue, and finally hands them over to Handler.dispatchMessage for message distribution. Handler.sendXXX, Handler.postXXX and other methods send messages to the message queue MessageQueue. The whole mode is actually a producer-consumer mode, which continuously produces and processes messages and sleeps when there are no messages. MessageQueue is a priority queue composed of a single linked list (it takes the head, so it is called a queue).

As mentioned earlier, when you create a Handler, you should bind it to a Looper (binding can also be understood as creation. The main thread has created a Looper by default, and the child thread needs to create a Looper by itself), so let's first look at how it is handled in the main thread:

  1. //ActivityThread.java  
  2. public static void main(String[] args) {  
  3. ···  
  4. Looper.prepareMainLooper();  
  5. ···
  6.   ActivityThread thread = new ActivityThread();  
  7. thread.attach(false, startSeq);  
  8. if ( sMainThreadHandler == null) {  
  9. sMainThreadHandler = thread .getHandler();  
  10. }  
  11. if (false) {  
  12. Looper.myLooper().setMessageLogging(new  
  13. LogPrinter(Log.DEBUG, "ActivityThread"));  
  14. }  
  15. // End of event ActivityThreadMain.  
  16. Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);  
  17. Looper.loop();  
  18. throw new RuntimeException("Main thread loop unexpectedly exited");  
  19. }

You can see that in the main method in ActivityThread, we first call the Looper.prepareMainLooper() method, then get the Handler of the current thread, and finally call Looper.loop(). Let's take a look at the Looper.prepareMainLooper() method first.

  1. //Looper.java  
  2. /**  
  3. * Initialize the current thread as a looper, marking it as an  
  4. * application's main looper. The main looper for your application  
  5. * is created by the Android environment, so you should never need  
  6. * to call this function yourself. See also: {@link #prepare()}  
  7. */  
  8. public static void prepareMainLooper() {  
  9. prepare(false);  
  10. synchronized (Looper.class) {  
  11. if (sMainLooper != null) {  
  12. throw new IllegalStateException("The main Looper has already been prepared.");  
  13. }
  14.   sMainLooper = myLooper ();  
  15. }  
  16. }  
  17. //prepare  
  18. private static void prepare(boolean quitAllowed) {  
  19. if (sThreadLocal.get() != null) {  
  20. throw new RuntimeException("Only one Looper may be created per thread");  
  21. }  
  22. sThreadLocal.set(new Looper(quitAllowed));  
  23. }

It can be seen that the Looper of the current thread is created in the Looper.prepareMainLooper() method, and the Looper instance is stored in the thread local variable sThreadLocal(ThreadLocal), that is, each thread has its own Looper. When creating a Looper, the message queue of the thread is also created. It can be seen that prepareMainLooper will determine whether sMainLooper has a value. If it is called multiple times, an exception will be thrown, so there will only be one Looper and MessageQueue for the main thread. Similarly, when Looper.prepare() is called in the child thread, the prepare(true) method will be called. If it is called multiple times, an exception will be thrown that each thread can only have one Looper. In summary, there is only one Looper and MessageQueue in each thread.

  1. //Looper.java  
  2. private Looper(boolean quitAllowed) {  
  3. mQueue = new MessageQueue(quitAllowed);  
  4. mThread = Thread.currentThread();  
  5. }

Let’s take a look at the main thread sMainThreadHandler = thread.getHandler(). What getHandler actually gets is the Handler mH.

  1. //ActivityThread.java  
  2. final H mH = new H();  
  3. @UnsupportedAppUsage  
  4. final Handler getHandler() {  
  5. return mH;  
  6. }

The Handler mH is an internal class of ActivityThread. By looking at the handMessage method, you can see that this Handler handles some messages of the four major components, Application, etc., such as creating a Service and binding some messages of the Service.

  1. //ActivityThread.java  
  2. class H extends Handler {  
  3. ···  
  4. public void handleMessage(Message msg) {  
  5. if (DEBUG_MESSAGES) Slog.v(TAG, " > > > handling: " + codeToString(msg.what));  
  6. switch (msg.what) {  
  7. case BIND_APPLICATION:  
  8. Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "bindApplication");  
  9. AppBindData data = (AppBindData)msg.obj;  
  10. handleBindApplication(data);  
  11. Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);  
  12. break;  
  13. case EXIT_APPLICATION:  
  14. if (mInitialApplication != null) {  
  15. mInitialApplication.onTerminate();  
  16. }  
  17. Looper.myLooper().quit();  
  18. break;  
  19. case RECEIVER:  
  20. Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "broadcastReceiveComp");  
  21. handleReceiver((ReceiverData)msg.obj);  
  22. Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);  
  23. break;  
  24. case CREATE_SERVICE:  
  25. Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, ("serviceCreate: " + String.valueOf(msg.obj)));  
  26. handleCreateService((CreateServiceData)msg.obj);  
  27. Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);  
  28. break;  
  29. case BIND_SERVICE:  
  30. Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "serviceBind");  
  31. handleBindService((BindServiceData)msg.obj);  
  32. Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);  
  33. break;  
  34. case UNBIND_SERVICE:  
  35. Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "serviceUnbind");  
  36. handleUnbindService((BindServiceData)msg.obj);  
  37. schedulePurgeIdler();  
  38. Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);  
  39. break;  
  40. case SERVICE_ARGS:  
  41. Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, ("serviceStart: " + String.valueOf(msg.obj)));  
  42. handleServiceArgs((ServiceArgsData)msg.obj);  
  43. Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);  
  44. break;  
  45. case STOP_SERVICE:  
  46. Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "serviceStop");  
  47. handleStopService((IBinder)msg.obj);  
  48. schedulePurgeIdler();  
  49. Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);  
  50. break;  
  51. ···  
  52. case APPLICATION_INFO_CHANGED:  
  53. mUpdatingSystemConfig = true ;  
  54. try {  
  55. handleApplicationInfoChanged((ApplicationInfo) msg.obj);  
  56. finally  
  57. mUpdatingSystemConfig = false ;  
  58. }  
  59. break;  
  60. case RUN_ISOLATED_ENTRY_POINT:  
  61. handleRunIsolatedEntryPoint((String) ((SomeArgs) msg.obj).arg1,  
  62. (String[]) ((SomeArgs) msg.obj).arg2);  
  63. break;  
  64. case EXECUTE_TRANSACTION:  
  65. final ClientTransaction transaction = (ClientTransaction) msg.obj;  
  66. mTransactionExecutor.execute(transaction);  
  67. if (isSystem()) {  
  68. // Client transactions inside system process are recycled on the client side  
  69. // instead of ClientLifecycleManager to avoid being cleared before this  
  70. // message is handled.  
  71. transaction.recycle();  
  72. }  
  73. // TODO(lifecycler): Recycle locally scheduled transactions.  
  74. break;  
  75. case RELAUNCH_ACTIVITY:  
  76. handleRelaunchActivityLocally((IBinder) msg.obj);  
  77. break;  
  78. case PURGE_RESOURCES:  
  79. schedulePurgeIdler();  
  80. break;  
  81. }  
  82. Object obj = msg .obj;  
  83. if (obj instanceof SomeArgs) {  
  84. ((SomeArgs) obj).recycle();  
  85. }  
  86. if (DEBUG_MESSAGES) Slog.v(TAG, " < < <   done: " + codeToString(msg.what));  
  87. }  
  88. }

Finally, let's look at the Looper.loop() method

  1. //Looper.java  
  2. public static void loop() {  
  3. //Get the Looper in ThreadLocal  
  4. final Looper me = myLooper ();  
  5. ···  
  6. final MessageQueue queue = me .mQueue;  
  7. ···  
  8. for (;;) { // infinite loop  
  9. //Get the message  
  10. Message msg = queue .next(); // might block  
  11. if ( msg == null ) {  
  12. // No message indicates that the message queue is quitting.  
  13. return;  
  14. }  
  15. ···  
  16. msg.target.dispatchMessage(msg);  
  17. ···  
  18. //Recycling  
  19. msg.recycleUnchecked();  
  20. }  
  21. }

In the loop method, there is an infinite loop, where messages are continuously obtained from the message queue (queue.next()), and then the messages are distributed through Handler (msg.target). In fact, there is no specific binding, because Handler has only one Looper and message queue MessageQueue in each thread, so it is naturally handled by it, that is, calling the Looper.loop() method. In the infinite loop of Looper.loop(), messages are continuously obtained and finally recycled and reused.

Here we need to emphasize the parameter target (Handler) in Message. It is this variable that enables each Message to find the corresponding Handler for message distribution, allowing multiple Handlers to work simultaneously.

Let's take a look at how it is handled in the child thread. First, create a Handler in the child thread and send a Runnable

  1. @Override  
  2. protected void onCreate(@Nullable Bundle savedInstanceState) {  
  3. super.onCreate(savedInstanceState);  
  4. setContentView(R.layout.activity_three);
  5.   new Thread(new Runnable() {  
  6. @Override  
  7. public void run() {  
  8. new Handler().post(new Runnable() {  
  9. @Override  
  10. public void run() {  
  11. Toast.makeText(HandlerActivity.this,"toast",Toast.LENGTH_LONG).show();  
  12. }  
  13. });  
  14. }  
  15. }).start();  
  16. }

After running, you can see the error log, and you can see that we need to call the Looper.prepare() method in the child thread, which actually creates a Looper and "associates" it with your Handler.

  1. --------- beginning of crash  
  2. 020-11-09 15:51:03.938 21122-21181/com.jackie.testdialog E/AndroidRuntime: FATAL EXCEPTION: Thread-2  
  3. Process: com.jackie.testdialog, PID: 21122  
  4. java.lang.RuntimeException: Can't create handler inside thread Thread[Thread-2,5,main] that has not called Looper.prepare()  
  5. at android.os.Handler. < init > (Handler.java:207)  
  6. at android.os.Handler. < init > (Handler.java:119)  
  7. at com.jackie.testdialog.HandlerActivity$1.run(HandlerActivity.java:31)  
  8. at java.lang.Thread.run(Thread.java:919)

Add Looper.prepare() to create Looper and call Looper.loop() method to start processing messages.

  1. @Override  
  2. protected void onCreate(@Nullable Bundle savedInstanceState) {  
  3. super.onCreate(savedInstanceState);  
  4. setContentView(R.layout.activity_three);  
  5. new Thread(new Runnable() {  
  6. @Override  
  7. public void run() {  
  8. //Create Looper, MessageQueue  
  9. Looper.prepare();  
  10. new Handler().post(new Runnable() {  
  11. @Override  
  12. public void run() {  
  13. Toast.makeText(HandlerActivity.this,"toast",Toast.LENGTH_LONG).show();  
  14. }  
  15. });  
  16. //Start processing messages  
  17. Looper.loop();  
  18. }  
  19. }).start();  
  20. }

It should be noted here that the quit method should be called to terminate the message loop after all things are processed, otherwise the child thread will always be in a loop waiting state. Therefore, terminate the Looper when it is not needed and call Looper.myLooper().quit().

After reading the above code, you may have a question. Is there any problem in updating the UI (performing Toast) in the child thread? Doesn't our Android not allow updating the UI in the child thread? In fact, this is not the case. The checkThread method in ViewRootImpl will check that mThread != Thread.currentThread(), and mThread is initialized in the constructor of ViewRootImpl, that is, the thread that creates ViewRootImpl must be consistent with the thread where checkThread is called, and the update of the UI is not only in the main thread.

  1. void checkThread() {  
  2. if (mThread != Thread.currentThread()) {  
  3. throw new CalledFromWrongThreadException(  
  4. "Only the original thread that created a view hierarchy can touch its views.");  
  5. }  
  6. }

Here we need to introduce some concepts. Window is a window in Android. Each Activity, Dialog, and Toast corresponds to a specific Window. Window is an abstract concept. Each Window corresponds to a View and a ViewRootImpl. Window and View are connected through ViewRootImpl. Therefore, it exists in the form of View. Let's take a look at the creation process of ViewRootImpl in Toast. Calling the show method of toast will eventually call its handleShow method.

  1. //Toast.java  
  2. public void handleShow(IBinder windowToken) {  
  3. ···  
  4. if (mView != mNextView) {  
  5. // Since the notification manager service cancels the token right  
  6. // after it notifies us to cancel the toast there is an inherent  
  7. // race and we may attempt to add a window after the token has been  
  8. // invalidated. Let us hedge against that.  
  9. try {  
  10. mWM.addView(mView, mParams); //Create ViewRootImpl  
  11. trySendAccessibilityEvent();  
  12. } catch (WindowManager.BadTokenException e) {  
  13. /* ignore */  
  14. }  
  15. }  
  16. }

The final implementer of this mWM (WindowManager) is WindowManagerGlobal, which creates ViewRootImpl in its addView method, and then performs root.setView(view, wparams, panelParentView), updates the interface through ViewRootImpl and completes the Window adding process.

  1. //WindowManagerGlobal.java  
  2. root = new ViewRootImpl(view.getContext(), display); //Create ViewRootImpl  
  3. view.setLayoutParams(wparams);  
  4. mViews.add(view);  
  5. mRoots.add(root);  
  6. mParams.add(wparams);  
  7. // do this last because it fires off messages to start doing things  
  8. try {  
  9. //ViewRootImpl  
  10. root.setView(view, wparams, panelParentView);  
  11. } catch (RuntimeException e) {  
  12. // BadTokenException or InvalidDisplayException, clean up.  
  13. if (index > = 0) {  
  14. removeViewLocked(index, true);  
  15. }  
  16. throw e;  
  17. }  
  18. }

SetView will complete the asynchronous refresh request through requestLayout, and will also call the checkThread method to verify the legitimacy of the thread.

  1. @Override  
  2. public void requestLayout() {  
  3. if (!mHandlingLayoutInLayoutRequest) {  
  4. checkThread();  
  5. mLayoutRequested = true ;  
  6. scheduleTraversals();  
  7. }  
  8. }

Therefore, our ViewRootImpl is created in the child thread, so the value of mThread is also the child thread, and our update is also in the child thread, so no exception will occur. You can also refer to this article for analysis, which is very detailed. Similarly, the following code can also verify this situation

  1. //Call in child thread  
  2. public void showDialog(){  
  3. new Thread(new Runnable() {  
  4. @Override  
  5. public void run() {  
  6. //Create Looper, MessageQueue  
  7. Looper.prepare();  
  8. new Handler().post(new Runnable() {  
  9. @Override  
  10. public void run() {  
  11. builder = new AlertDialog.Builder(HandlerActivity.this);  
  12. builder.setTitle("jackie");  
  13. alertDialog = builder .create();  
  14. alertDialog.show();  
  15. alertDialog.hide();  
  16. }  
  17. });  
  18. //Start processing messages  
  19. Looper.loop();  
  20. }  
  21. }).start();  
  22. }

Call the showDialog method in the child thread, first call the alertDialog.show() method, then call the alertDialog.hide() method. The hide method only hides the Dialog and does not do any other operations (the Window is not removed). Then call alertDialog.show() in the main thread; it will throw an exception that Only the original thread that created a view hierarchy can touch its views.

  1. 2020-11-09 18:35:39.874 24819-24819/com.jackie.testdialog E/AndroidRuntime: FATAL EXCEPTION: main  
  2. Process: com.jackie.testdialog, PID: 24819  
  3. android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.  
  4. at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8191)  
  5. at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1420)  
  6. at android.view.View.requestLayout(View.java:24454)  
  7. at android.view.View.setFlags(View.java:15187)  
  8. at android.view.View.setVisibility(View.java:10836)  
  9. at android.app.Dialog.show(Dialog.java:307)  
  10. at com.jackie.testdialog.HandlerActivity$2.onClick(HandlerActivity.java:41)  
  11. at android.view.View.performClick(View.java:7125)  
  12. at android.view.View.performClickInternal(View.java:7102)

Therefore, the key to updating the UI in the thread is whether the thread where ViewRootImpl and checkThread are created are consistent.

How to access the network in the main thread

Add the following code before the network request

  1. StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitNetwork().build();  
  2. StrictMode.setThreadPolicy(policy);

StrictMode was introduced in Android 2.3 and is used to detect two major issues: ThreadPolicy and VmPolicy. If you turn off the network detection in strict mode, you can perform network operations in the main thread, which is generally not recommended. You can read more about strict mode here.

Why does the system not recommend accessing the UI in a child thread?

This is because Android UI controls are not thread-safe. If accessed concurrently in multiple threads, the UI controls may be in an unexpected state. So why doesn't the system add a lock mechanism to the access to UI controls? There are two disadvantages:

  1. First of all, adding a lock mechanism will complicate the logic of UI access.
  2. The locking mechanism will reduce the efficiency of UI access because it will block the execution of certain threads.

Therefore, the simplest and most efficient method is to use a single-threaded model to handle UI operations. (Exploration of the Art of Android Development)

How does the child thread notify the main thread to update the UI (all messages are sent to the main thread through Handle to operate the UI)

  1. The Handler is defined in the main thread, the child thread sends messages through mHandler, and the handleMessage of the main thread Handler updates the UI.
  2. Use the runOnUiThread method of the Activity object.
  3. Create a Handler and pass in getMainLooper.
  4. View.post(Runnable r) .

Why doesn't the infinite loop of Looper cause the application to freeze? Will it consume a lot of resources?

From the analysis of the main thread and the child thread above, we can see that the Looper will continuously retrieve messages in the thread. If the child thread's Looper is in an infinite loop, once the task is completed, the user should exit manually instead of letting it sleep and wait. (Quoted from Gityuan) A thread is actually a piece of executable code. When the executable code is executed, the life cycle of the thread should be terminated and the thread exits. As for the main thread, we never want it to run for a period of time and then exit by itself, so how can we ensure that it can survive? The simple way is that the executable code can be executed all the time, and the infinite loop can ensure that it will not be exited. For example, the binder thread also uses the infinite loop method. It performs read and write operations with the Binder driver in a different loop method. Of course, it is not a simple infinite loop and will sleep when there is no message. Android is based on the message processing mechanism. The user's behavior is in this Looper loop. When we click the screen when it is dormant, we wake up the main thread to continue working.

Does the endless loop of the main thread consume a lot of CPU resources? Actually, it doesn't. This involves the Linux pipe/epoll mechanism. Simply put, when there is no message in the MessageQueue of the main thread, it is blocked in the nativePollOnce() method in the queue.next() of the loop. At this time, the main thread will release CPU resources and enter a dormant state until the next message arrives or a transaction occurs. The main thread is awakened by writing data to the write end of the pipe. The epoll mechanism used here is an IO multiplexing mechanism that can monitor multiple descriptors at the same time. When a descriptor is ready (read or write ready), it immediately notifies the corresponding program to perform a read or write operation. It is essentially synchronous I/O, that is, reading and writing are blocked. Therefore, the main thread is in a dormant state most of the time and does not consume a lot of CPU resources.

When does the main thread's Looper exit?

When the App exits, mH (Handler) in ActivityThread receives the message and executes the exit.

  1. //ActivityThread.java  
  2. case EXIT_APPLICATION:  
  3. if (mInitialApplication != null) {  
  4. mInitialApplication.onTerminate();  
  5. }  
  6. Looper.myLooper().quit();  
  7. break;

If you try to manually exit the main thread Looper, the following exception will be thrown

  1. Caused by: java.lang.IllegalStateException: Main thread not allowed to quit.  
  2. at android.os.MessageQueue.quit(MessageQueue.java:428)  
  3. at android.os.Looper.quit(Looper.java:354)  
  4. at com.jackie.testdialog.Test2Activity.onCreate(Test2Activity.java:29)  
  5. at android.app.Activity.performCreate(Activity.java:7802)  
  6. at android.app.Activity.performCreate(Activity.java:7791)  
  7. at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1299)  
  8. at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3245)  
  9. at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3409)  
  10. at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:83)  
  11. at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)  
  12. at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)  
  13. at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2016)  
  14. at android.os.Handler.dispatchMessage(Handler.java:107)  
  15. at android.os.Looper.loop(Looper.java:214)  
  16. at android.app.ActivityThread.main(ActivityThread.java:7356)  
  17. at java.lang.reflect.Method.invoke(Native Method)  
  18. at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)  
  19. at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)

Why is exit not allowed? Because the main thread is not allowed to exit. Once it exits, it means the program has hung, and the exit should not be done in this way.

Handler message processing order

When Looper executes the message loop loop(), the following line of code will be executed. msg.target is the Handler object

  1. msg.target.dispatchMessage(msg);

Let's take a look at the source code of dispatchMessage

  1. public void dispatchMessage(@NonNull Message msg) {  
  2. if (msg.callback != null) {  
  3. handleCallback(msg);  
  4. } else {  
  5. //If callback processes the msg and returns true, handleMessage will not be called back  
  6. if (mCallback != null) {
  7.   if (mCallback.handleMessage(msg)) {  
  8. return;  
  9. }  
  10. }  
  11. handleMessage(msg);  
  12. }  
  13. }

If the Message object has a CallBack callback, this CallBack is actually a Runnable, which only executes this callback and then ends. The CallBack code for creating the Message is as follows:

  1. Message msgCallBack = Message .obtain(handler, new Runnable() {  
  2. @Override  
  3. public void run() {  
  4. }  
  5. });

The handleCallback method calls the run method of Runnable.

  1. private static void handleCallback(Message message) {  
  2. message.callback.run();  
  3. }

If the Message object has no CallBack callback, enter the else branch to determine whether the Handler's CallBack is empty. If it is not empty, execute the handleMessage method of CallBack, and then return. The CallBack code for constructing the Handler is as follows:

  1. Handler.Callback callback = new Handler.Callback() {  
  2. @Override  
  3. public boolean handleMessage(@NonNull Message msg) {  
  4. //retrun true, the following logic will not be executed, which can be used for priority processing  
  5. return false;  
  6. }  
  7. };

1. Finally, the handleMessage() function of Handler is called, which is the function we often rewrite, and the message is processed in this method.

Usage scenarios

It can be seen that Handler.Callback has the priority to process messages. When a message is processed and intercepted by Callback (return true), the handleMessage(msg) method of Handler will not be called; if Callback processes the message but does not intercept it, it means that a message can be processed by Callback and Handler at the same time. We can use CallBack to intercept the message of Handler.

Scenario: Hook ActivityThread.mH. There is a member variable mH in ActivityThread. It is a Handler and an extremely important class. Almost all plug-in frameworks use this method.

Execution logic of Handler.post(Runnable r) method

We need to analyze how the commonly used Handler.post(Runnable r) method is executed. Is a new thread created? In fact, no, this Runnable object is just called by its run method, and a thread is not started at all. The source code is as follows:

  1. //Handler.java  
  2. public final boolean post(@NonNull Runnable r) {  
  3. return sendMessageDelayed(getPostMessage(r), 0);  
  4. }  
  5. private static Message getPostMessage(Runnable r) {  
  6. Message m = Message .obtain();  
  7. m.callback = r ;  
  8. return m;  
  9. }

Finally, the Runnable object is packaged into a Message object, that is, this Runnable object is the CallBack object of the Message and has the right to priority execution.

How does Handler perform thread switching?

The principle is very simple. Threads share resources. The child thread sends messages through methods such as handler.sendXXX and handler.postXXX, and then retrieves messages in the message queue through Looper.loop(), and finally hands them over to the handle.dispatchMessage method for message distribution and processing.

How to deal with memory leaks caused by improper use of Handler?

  1. If there is a delayed message, remove the Message/Runnable in time after the interface is closed and call handler.removeCallbacksAndMessages(null)
  2. Memory leaks caused by inner classes are changed to static inner classes, and weak references are used for contexts or Activities/Fragments.

For the analysis and solution of specific memory leaks, please refer to this article. At the same time, there is another key point. If there is a delayed message, when the interface is closed, the message in the Handler has not been processed, so how is the message finally processed? After testing, for example, I delay sending a message for 10 seconds after opening the interface, close the interface, and finally receive the message (print log) in the handMessage method of the Handler (created by the anonymous inner class). Because there will be a reference chain of MessageQueue -> Message -> Handler -> Activity, the Handler will not be destroyed, and the Activity will not be destroyed.

Correctly create a Message instance

  1. Obtained through the static method Message.obtain() of Message;
  2. Through the Handler's public method handler.obtainMessage()

All messages will be recycled and put into sPool, using the flyweight design pattern.

Handler in-depth question answering

ThreadLocal

ThreadLocal provides a copy of the variable for each thread, so that each thread does not access the same object at a certain time, thus isolating the data sharing of multiple threads.

If you are asked to design a ThreadLocal, and the goal of ThreadLocal is to allow different threads to have different variables V, then the most direct way is to create a Map whose Key is the thread and Value is the variable V owned by each thread. ThreadLocal can hold such a Map internally. You might design it like this

In fact, the implementation of Java is as follows. There is also a Map in the Java implementation, called ThreadLocalMap, but the one holding ThreadLocalMap is not ThreadLocal, but Thread. The Thread class has a private attribute threadLocals, whose type is ThreadLocalMap, and the Key of ThreadLocalMap is ThreadLocal.

The simplified code is as follows

  1. class Thread {  
  2. //Internally holds ThreadLocalMap  
  3. ThreadLocal.ThreadLocalMap  
  4. threadLocals;  
  5. }  
  6. class ThreadLocal <T> {  
  7. public T get() {  
  8. //First get the thread held  
  9. //ThreadLocalMap  
  10. ThreadLocalMap map =  
  11. Thread .currentThread()  
  12. .threadLocals;  
  13. //In ThreadLocalMap  
  14. //Find the variable  
  15. Entry e =  
  16. map .getEntry(this);  
  17. return e.value;  
  18. }  
  19. static class ThreadLocalMap{  
  20. //The internal is an array instead of a Map  
  21. Entry[] table;  
  22. //Find Entry based on ThreadLocal  
  23. Entry getEntry(ThreadLocal key){  
  24. //Omit the search logic  
  25. }  
  26. //Entry definition  
  27. static class Entry extends  
  28. WeakReference < ThreadLocal > {  
  29. Object value;  
  30. }  
  31. }  
  32. }

In the Java implementation, ThreadLocal is just a proxy tool class, which does not hold any thread-related data. All thread-related data is stored in Thread. From the perspective of data affinity, it is more reasonable for ThreadLocalMap to belong to Thread. Therefore, the get method of ThreadLocal actually gets the ThreadLocalMap unique to each thread.

Another reason is that it is not easy to cause memory leaks. If we use our design, the Map held by ThreadLocal will hold a reference to the Thread object, which means that as long as the ThreadLocal object exists, the Thread object in the Map will never be recycled. The life cycle of ThreadLocal is often longer than that of the thread, so this design can easily lead to memory leaks.

In the Java implementation, Thread holds ThreadLocalMap, and the reference to ThreadLocal in ThreadLocalMap is still a weak reference, so as long as the Thread object can be recycled, the ThreadLocalMap can be recycled. Although the Java implementation seems more complicated, it is safer.

ThreadLocal and memory leaks

But everything is not always so perfect. If you use ThreadLocal in the thread pool, it may cause memory leaks. The reason is that the thread in the thread pool lives too long and often dies with the program. This means that the ThreadLocalMap held by the Thread will never be recycled. In addition, the Entry in the ThreadLocalMap is a weak reference to the ThreadLocal, so as long as the ThreadLocal ends its life cycle, it can be recycled. However, the Value in the Entry is strongly referenced by the Entry, so even if the Value's life cycle ends, the Value cannot be recycled, resulting in memory leaks.

So we can manually release resources through the try{}finally{} solution

  1. ExecutorService es;  
  2. ThreadLocal tl;  
  3. es.execute(()- > {  
  4. //ThreadLocal add variables  
  5. tl.set(obj);  
  6. try {  
  7. //Omit business logic code  
  8. }finally {  
  9. //Manually clean up ThreadLocal  
  10. tl.remove();  
  11. }  
  12. });

The above ThreadLocal content is mainly referenced from here.

epoll mechanism

The application of the epoll mechanism in Handler, when there is no message in the MessageQueue of the main thread, it will be blocked in the nativePollOnce() method in the queue.next() of the loop, and finally call epoll_wait() to block and wait. At this time, the main thread will release the CPU resources and enter a dormant state until the next message arrives or a transaction occurs, and wake up the main thread by writing data to the write end of the pipe. The epoll mechanism used here is an IO multiplexing mechanism that can monitor multiple descriptors at the same time. When a descriptor is ready (ready to read or write), it will immediately notify the corresponding program to perform read or write operations. It is essentially synchronous I/O, that is, reading and writing are blocked. Therefore, the main thread is in a dormant state most of the time and does not consume a lot of CPU resources.

Here is a good in-depth article about IO multiplexing select, poll, epoll detailed explanation, here are the last two paragraphs in the article:

  1. On the surface, epoll has the best performance, but when the number of connections is small and the connections are very active, the performance of select and poll may be better than epoll. After all, epoll's notification mechanism requires many function callbacks.
  2. Select is inefficient because it needs to poll every time. But inefficiency is relative, depending on the situation, and can also be improved through good design.

The reason why I choose the epoll mechanism at the bottom of Handler is that I feel that epoll is more efficient. In select/poll, the kernel scans all monitored file descriptors only after the process calls a certain method, while epoll registers a file descriptor in advance through epoll_ctl(). Once a file descriptor is ready, the kernel will use a callback mechanism similar to callback to quickly activate the file descriptor, and the process will be notified when it calls epoll_wait(). (Here, the traversal of file descriptors is removed, and the mechanism of listening to callbacks is used. This is the charm of epoll.)

Handler's synchronization barrier mechanism

What should we do if there is an urgent message that needs to be processed first? This actually involves the design of the architecture, the design of general scenarios and special scenarios. You may think of the sendMessageAtFrontOfQueue() method, but it is actually far more than that. The Handler adds a synchronization barrier mechanism to implement the function of [asynchronous message priority] execution.

postSyncBarrier() sends a synchronization barrier, removeSyncBarrier() removes the synchronization barrier

The role of the synchronization barrier can be understood as intercepting the execution of synchronous messages. The main thread's Looper will keep calling MessageQueue's next() to take out the message at the head of the queue for execution, and then take the next one after the message is executed. When the next() method finds that the head of the queue is a message of a synchronization barrier when taking the message, it will traverse the entire queue and only look for messages with the asynchronous flag set. If an asynchronous message is found, then the asynchronous message will be taken out for execution, otherwise the next() method will be blocked. If the next() method is blocked, the main thread is idle at this time, that is, it is not doing anything. Therefore, if the head of the queue is a message of a synchronization barrier, then all the synchronous messages behind it will be intercepted until the synchronization barrier message is removed from the queue, otherwise the main thread will not process the synchronous messages behind the synchronization screen.

By default, all messages are synchronous messages. Only when the asynchronous flag is manually set, the message will be asynchronous. In addition, synchronous barrier messages can only be sent internally, and this interface is not open to us.

You will find that all the message-related codes in Choreographer have manually set the asynchronous message flag, so these operations are not affected by the synchronization barrier. The reason for this may be to ensure that the upper-level app can perform the work of traversing and drawing the View tree as soon as possible when receiving the screen refresh signal.

All actions in the Choreographer process are asynchronous messages, which ensures the smooth operation of Choreographer and the execution of doTraversal (doTraversal → performTraversals is to execute view layout, measure, and draw). If there are other synchronous messages in this process, they cannot be processed and must wait until after doTraversal.

Because if there are too many messages to be executed in the main thread, and these messages are sorted according to timestamps, if there is no synchronization barrier added, the work of traversing and drawing the View tree may be forced to delay execution because it also needs to queue. Then it is possible that when a frame is about to end, the calculation of screen data will only start to be calculated. Even if the calculation this time is less than 16.6ms, it will also cause frame loss.

So, can the control of synchronization barrier messages ensure that the work of traversing and drawing the View tree is handled as soon as the screen refresh signal is received?

It can only be said that the synchronization barrier is done as much as possible, but it does not guarantee that it can be processed as soon as possible. Because the synchronization barrier is sent to the message queue when scheduleTraversals() is called, that is, only when a View initiates a refresh request, the synchronization messages after this moment will be intercepted. If the work sent to the message queue before scheduleTraversals() will still be fetched out in sequence and executed.

The following is a partial detailed analysis:

WindowManager maintains all the activities' DecorView and ViewRootImpl. As we mentioned earlier, ViewRootImpl is initialized in the addView method of WindowManagerGlobal, and then it calls its setView method and passes the DecorView as a parameter. So let's see what ViewRootImpl does.

  1. //ViewRootImpl.java  
  2. //view is DecorView  
  3. public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {  
  4. synchronized (this) {  
  5. if ( mView == null) {  
  6. mView = view ;
  7.   ···  
  8. // Schedule the first layout -before- adding to the window  
  9. // manager, to make sure we do the relayout before receiving  
  10. // any other events from the system.  
  11. requestLayout(); //Stack layout request
  12.   ···  
  13. view.assignParent(this); //Calling the current ViewRootImpl object this as a parameter, the assignParent of the DecorView is called assignParent with the current ViewRootImpl object this as a parameter  
  14. ···  
  15. }  
  16. }  
  17. }

The assignParent of DecorView is called in the setView() method

  1. //View.java  
  2. /*  
  3. * Caller is responsible for calling requestLayout if necessary.  
  4. * (This allows addViewInLayout to not request a new layout.)  
  5. */  
  6. @UnsupportedAppUsage  
  7. void assignParent(ViewParent parent) {  
  8. if ( mParent == null) {  
  9. mParent = parent ;  
  10. } else if ( parent == null) {  
  11. mParent = null ;  
  12. } else {  
  13. throw new RuntimeException("view " + this + " being added, but"  
  14. + " it already has a parent");  
  15. }  
  16. }

The parameter is ViewParent, and ViewRootImpl implements the ViewParent interface, so here we bind DecorView and ViewRootImpl. The root layout of each activity is DecorView, and the parent of DecorView is ViewRootImpl, so in the child view, the work such as invalidate() is executed, and the parent is looped to find the parent, and finally find the ViewRootImpl. So in fact, the refresh of the View is controlled by ViewRootImpl.

Even when a small View on the interface initiates a redraw request, it must go to ViewRootImpl layer by layer, and then start traversing the View tree, traversing to this View that needs to be redrawed, and then call its onDraw() method to draw.

The last call to View.invalidate() operation for redrawing is ViewRootImpl.scheduleTraversals(), and the requestLayout method is called in the ViewRootImpl.setView() method.

  1. @Override  
  2. public void requestLayout() {  
  3. if (!mHandlingLayoutInLayoutRequest) {  
  4. checkThread();  
  5. mLayoutRequested = true ;  
  6. scheduleTraversals();  
  7. }  
  8. }

Finally, the scheduleTraversals() method was also called, which is actually the key to screen refresh.

In fact, when the life cycle of onCreate---onResume is completed, its DecoView is bound to a newly created ViewRootImpl object. At the same time, it starts to arrange a traversal View task, that is, to draw the View tree operation to wait for execution, and then set the DecoView parent to the ViewRootImpl object. Therefore, we cannot get the width and height of the View in onCreate~onResume, and the drawing of the interface is only executed after onResume. You can refer to my previous article for analysis.

This article can be used to refer to a series of analysis and screen refresh mechanism of ViewRootImpl.scheduleTraversals(). Most of the content here is also referenced, and the analysis content related to synchronization barriers is also included.

Choreographer's main function is to coordinate the time of animation, input and drawing, it receives timing pulses from the display subsystem (such as vertical synchronization) and then arranges the rendering of part of the next frame to work.

The frame rate can be monitored through Choreographer.getInstance().postFrameCallback();

  1. public class FPSFrameCallback implements Choreographer.FrameCallback {  
  2. private static final String TAG = "FPS_TEST" ;
  3.   private long mLastFrameTimeNanos;  
  4. private long mFrameIntervalNanos;  
  5. public FPSFrameCallback(long lastFrameTimeNanos) {  
  6. mLastFrameTimeNanos = lastFrameTimeNanos ;  
  7. //How many nanoseconds does each frame render time  
  8. mFrameIntervalNanos = (long) (1000000000 / 60.0);  
  9. }
  10. @Override  
  11. public void doFrame(long frameTimeNanos) { // The time when the Vsync signal arrives frameTimeNanos  
  12. //Initialization time  
  13. if ( mLastFrameTimeNanos == 0) {  
  14. //The rendering time of the previous frame  
  15. mLastFrameTimeNanos = frameTimeNanos ;  
  16. }  
  17. final long jitterNanos = frameTimeNanos - mLastFrameTimeNanos;  
  18. if (jitterNanos > = mFrameIntervalNanos) {  
  19. final long skippedFrames = jitterNanos / mFrameIntervalNanos;  
  20. if (skippedFrames > 5) {  
  21. Log.d(TAG, "Skipped " + skippedFrames + " frames! "  
  22. + "The application may be doing too much work on its main thread.");  
  23. }  
  24. }  
  25. mLastFrameTimeNanos = frameTimeNanos ;  
  26. //Register the next frame callback  
  27. Choreographer.getInstance().postFrameCallback(this);  
  28. }  
  29. }

Call method is registered in Application

  1. Choreographer.getInstance().postFrameCallback(FPSFrameCallback(System.nanoTime()))

Causes of frame dropping: There are generally two types of reasons for frame dropping. One is that the time for traversing the view tree to calculate screen data exceeds 16.6ms; the other is that the main thread has been processing other time-consuming messages, which has caused the work of traversing the view tree to be delayed, thus exceeding the opportunity for 16.6ms to switch the next frame of the bottom layer.

Handler lock related issues

Since multiple Handlers can be added to the MessageQueue (each Handler may be in different threads when sending a message), how does it ensure thread safety internally?

Handler.sendXXX, Handler.postXXX will eventually be adjusted to the enqueueMessage method of MessageQueue

The source code is as follows:

  1. boolean enqueueMessage(Message msg, long when) {  
  2. if ( msg.target == null) {  
  3. throw new IllegalArgumentException("Message must have a target.");  
  4. }  
  5. if (msg.isInUse()) {  
  6. throw new IllegalStateException(msg + " This message is already in use.");  
  7. }  
  8. //Locking to ensure safety  
  9. synchronized (this) {  
  10. ···  
  11. }  
  12. }

It uses synchronized keyword to ensure thread safety. At the same time, messagequeue.next() will also use synchronized locks to ensure thread safety when fetched, and insertion will also add locks. This problem is actually not difficult, it just depends on whether you understand the source code.

Synchronization method in Handler

How to make the handler.post message execute and then continue to execute, synchronize the method runWithScissors

  1. public final boolean runWithScissors(@NonNull Runnable r, long timeout) {  
  2. if ( r == null) {  
  3. throw new IllegalArgumentException("runnable must not be null");  
  4. }  
  5. if (timeout <   0 ) {  
  6. throw new IllegalArgumentException("timeout must be non-negative");  
  7. }  
  8. if (Looper.myLooper() == mLooper) {  
  9. r.run();  
  10. return true;  
  11. }  
  12. BlockingRunnable br = new BlockingRunnable(r);  
  13. return br.postAndWait(this, timeout);  
  14. }

Some applications of Handler in systems and third-party frameworks

HandlerThread

HandlerThread inherits from Thread. As the name suggests, it is actually an encapsulation of Handler and Thread. It has been packaged very well and is very secure for us. It also uses synchronized internally to ensure thread safety, such as the getLooper method.

  1. public Looper getLooper() {  
  2. if (!isAlive()) {  
  3. return null;  
  4. }  
  5. // If the thread has been started, wait until the looper has been created.  
  6. synchronized (this) {  
  7. while (isAlive() && mLooper == null) {  
  8. try {  
  9. wait();  
  10. } catch (InterruptedException e) {
  11.   }  
  12. }  
  13. }  
  14. return mLooper;  
  15. }

In the run method of the thread, the Looper can only be created after the thread is started and assigned to mLooper. The blocking here is to wait for the Looper to be created successfully. At the same time, the method is modified with Public, indicating that the method provides external calls and the Looper is successfully created and provided to external use.

IntentService

A brief look at the source code can see the Handler application. Handler's handMessage will eventually callback to the onHandleIntent method.

  1. public abstract class IntentService extends Service {  
  2. private volatile Looper mServiceLooper;  
  3. @UnsupportedAppUsage  
  4. private volatile ServiceHandler mServiceHandler;

How to create a program that does not crash

Create a program that does not crash, please refer to this article of mine

Applications in Glide

I believe that Glide should be very familiar with it. We all know that the control of Glide's life cycle (if you don't understand, you can read the analysis of Glide's related articles, which is the same principle as LiveData) is to add an empty Fragment to the Activity or Fragment, and then manage the life cycle of the Fragment through the FragmentMannager to achieve the control of the life cycle. The following is an excerpt from a Glide code to add a Fragment:

  1. private RequestManagerFragment getRequestManagerFragment(  
  2. @NonNull final android.app.FragmentManager fm,  
  3. @Nullable android.app.Fragment parentHint,  
  4. boolean isParentVisible) {  
  5. //1. Get the RequestManagerFragment through FragmentManager. If it has been added to FragmentManager, it will return the instance, otherwise it will be empty  
  6. RequestManagerFragment current = (RequestManagerFragment) fm.findFragmentByTag(FRAGMENT_TAG);  
  7. if ( current == null) {  
  8. //2. If there is no fm, get it from the map cache  
  9. current = pendingRequestManagerFragments .get(fm);  
  10. if ( current == null) {  
  11. //3. There is no step 1 and 2, which means that it is not created, so the creation process will be followed.  
  12. current = new RequestManagerFragment();  
  13. current.setParentFragmentHint(parentHint);  
  14. if (isParentVisible) {  
  15. current.getGlideLifecycle().onStart();  
  16. }  
  17. //4. Save the newly created fragment to the map container  
  18. pendingRequestManagerFragments.put(fm, current);  
  19. //5. Send and add fragment transaction event  
  20. fm.beginTransaction().add(current, FRAGMENT_TAG).commitAllowingStateLoss();  
  21. //6.Send remove local cache event  
  22. handler.obtainMessage(ID_REMOVE_FRAGMENT_MANAGER, fm).sendToTarget();  
  23. }  
  24. }  
  25. return current;  
  26. }  
  27. //The only difference from the above method is that this is Fragment's FragmentManager, and the above is FragmentManager of Activity  
  28. private SupportRequestManagerFragment getSupportRequestManagerFragment(  
  29. @NonNull final FragmentManager fm, @Nullable Fragment parentHint, boolean isParentVisible) {  
  30. SupportRequestManagerFragment current =  
  31. (SupportRequestManagerFragment) fm.findFragmentByTag(FRAGMENT_TAG);  
  32. if ( current == null) {  
  33. current = pendingSupportRequestManagerFragments .get(fm);  
  34. if ( current == null) {  
  35. current = new SupportRequestManagerFragment();  
  36. current.setParentFragmentHint(parentHint);  
  37. if (isParentVisible) {  
  38. current.getGlideLifecycle().onStart();  
  39. }  
  40. pendingSupportRequestManagerFragments.put(fm, current);  
  41. fm.beginTransaction().add(current, FRAGMENT_TAG).commitAllowingStateLoss();  
  42. handler.obtainMessage(ID_REMOVE_SUPPORT_FRAGMENT_MANAGER, fm).sendToTarget();  
  43. }  
  44. }  
  45. return current;  
  46. }  
  47. @Override  
  48. public boolean handleMessage(Message message) {  
  49. boolean handled = true ;  
  50. Object removed = null ;  
  51. Object key = null ;  
  52. switch (message.what) {  
  53. case ID_REMOVE_FRAGMENT_MANAGER:  
  54. //7. Remove cache  
  55. android.app.FragmentManager fm = (android.app.FragmentManager) message.obj;  
  56. key = fm ;  
  57. removed = pendingRequestManagerFragments .remove(fm);  
  58. break;  
  59. //Omit the code...  
  60. }  
  61. //Omit the code...  
  62. return handled;  
  63. }

After reading the above code, you may have doubts.

  • Why do you still need to save Fragment to the map container when adding FragmentManager (Step 4)?
  • Why do we need to judge whether the Fragment has been added? (Step 2)? Can’t FragmentManager find it?

In fact, the answer is very simple. After learning the Handler principle, we know that after step 5, we do not add the Fragment to the FragmentManager (event queue), but send the event adding the Fragment. Next, let's look at the code

  1. //FragmentManagerImpl.java  
  2. void scheduleCommit() {  
  3. synchronized (this) {  
  4. boolean postponeReady =  
  5. mPostponedTransactions != null && !mPostponedTransactions.isEmpty();  
  6. boolean pendingReady = mPendingActions != null && mPendingActions.size() == 1;  
  7. if (postponeReady || pendingReady) {  
  8. mHost.getHandler().removeCallbacks(mExecCommit);  
  9. mHost.getHandler().post(mExecCommit);
  10.   updateOnBackPressedCallbackEnabled();  
  11. }  
  12. }  
  13. }

Adding Fragment will eventually go to the scheduleCommit method of FragmentManagerImpl, we can see that it is sending events through Handler.

This explains why the Fragment was not added to the FragmentManager immediately after step 5, so the Map cache Fragment is needed to mark whether there is a Fragment added. Then, in step 6, the message removing the Map cache is sent, because the Handler handles the message in an orderly manner.

Summarize

In fact, this article does not analyze the source code very carefully, but it has been expanded from the application of various aspects of the entire system, as well as the use of some third-party frameworks. I hope this article will be helpful to you. If you like it, please like it~

<<:  Google outlines Android app development and policy changes for 2021

>>:  Why didn't anyone tell me that Android phones can actually connect to the Internet via wired internet?

Recommend

Guangdiantong advertising introduction, Guangdiantong advertising placement

Guangdiantong is an advertising platform based on...

Analysis of Zhang’s popular short video operations!

Many people have discussed why Zhang became so po...

Download the Ultimate Intelligence of the Spiritual Merchant on Baidu Cloud

The Ultimate Intelligence of the Spiritual Mercha...

Who "killed" Luo Yonghao?

A friend said he wanted to buy a ticket to attend...

Can humans achieve immortality? Can consciousness exist apart from the body?

There is a question that has always troubled mank...

Design of operation plan for Children's Day event!

There is only half a month left until Children...