A brief analysis of Android Looper Handler mechanism

Recently I wanted to write a player demo, which uses Looper Handler. I read a lot of information but couldn't understand it thoroughly, so I decided to take a look at the relevant source code myself and record my experience here, hoping to help people in need.

This article will use 猜想 + log验证the method to learn Android Looper Handler. Some complex codes will be skipped, as long as you can understand their design principles. The opinions in this article are all my humble opinions. If there are any mistakes, please let me know.

Please add image description

1、Looper Handler MessageQueue

Looper plays a role in the entire message processing mechanismMessage waiting and message distributionFunction:

  • Message waiting: Looper blocks the program running and waits for upcoming messages;
  • Message distribution: After Looper receives the message, it distributes the message to the designated processor for processing in the current thread.

First, let’s look at the problem of blocking. When the app starts, it will create a Looper and call the loop function to block the main function. This Looper is called the main thread/main process Looper. For the code, refer to ActivityThread.java :

    public static void main(String[] args) {
    
    
        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");
        Looper.prepareMainLooper();
        Looper.loop();
    }

The main thread Looper (or also called mainLooper) has the function of preventing the program from ending. This will lead to other questions. Why can the program continue to run after it blocks it?

In the message waiting function above, we mentioned that when the Looper blocking program is running, it will wait for the upcoming message. What message will come?

I wrote a demo with only one Button on the interface. Click the icon to start the app and click the button. Then we will observe the log:

2023-08-28 22:17:51.969 6768-6768/com.example.loopertest D/MainActivity: onCreate
2023-08-28 22:17:51.993 6768-6768/com.example.loopertest D/MainActivity: onStart
2023-08-28 22:17:51.993 6768-6768/com.example.loopertest D/MainActivity: onPostCreate
2023-08-28 22:17:51.994 6768-6768/com.example.loopertest D/MainActivity: onResume
2023-08-28 22:17:51.994 6768-6768/com.example.loopertest D/MainActivity: onPostResume
2023-08-28 22:17:54.424 6768-6768/com.example.loopertest D/MainActivity: onClick

You can see that the thread number of the log is the same as the process number, which means that all messages are processed in the main thread (UI thread)/main process. Then the Looper receives the start event of the Activity and the click event of the button. By extension, the main process will receive and handle all UI events.

How does Looper receive events and process them? There is an infinite loop in the loop method, loopOnce is continuously executed, and blocking also occurs here:

    public static void loop() {
    
    
        for (;;) {
    
    
            if (!loopOnce(me, ident, thresholdOverride)) {
    
    
                return;
            }
        }
    }

You will see an object in loopOnce MessageQueue, which is the message queue held by Looper. It maintains a linked list internally, and message waiting is also completed by it.

    private static boolean loopOnce(final Looper me,
            final long ident, final int thresholdOverride) {
    
    
        Message msg = me.mQueue.next(); // might block
        if (msg == null) {
    
    
            // No message indicates that the message queue is quitting.
            return false;
        }
        ...
        try {
    
    
            msg.target.dispatchMessage(msg);
            if (observer != null) {
    
    
                observer.messageDispatched(token, msg);
            }
            dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
        } 
        ....
        return true;
	}

MessageQueue.nextIt is a blocking method. When there is no message, it will block and wait, which can prevent the upper level for loop from idling; when there is a message, it returns Message and distributes it to the designated Handler for processing.

Let's look at the loop again. How to exit the loop? From the above code we can see that when the returned Message is null, loopOnce returns false and the entire loop ends.

The MessageQueue.next code is as follows:

    Message next() {
    
    
		......
        for (;;) {
    
    
			......
            nativePollOnce(ptr, nextPollTimeoutMillis);
            ......
            if (mQuitting) {
    
    
                dispose();
                return null;
            }   
        }   
    }

The code here is relatively long, the core content is:

  1. Call nativePollOnce to block the poll message in the native layer;
  2. Sort the polled messages according to the specified execution time. If no time is specified, the messages will be sorted in the order of the end;
  3. If the first message in the linked list needs to be delayed, continue to call nativePollOnce and set a timeout;
  4. Return the message that needs to be processed to loopOnce;
  5. If the quit method is called, null is returned, terminating the loop.

In the above code, we see that the way to obtain messages is to call nativePollOnce to wait. The native messages here may be sent to mainLooper by the android system, such as touch click events and so on. In addition, we can also actively send messages to mainLooper, which requires using Handler.

We call the Handler's sendMessage method, which will eventually be executed into enqueueMessage:

    private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
            long uptimeMillis) {
    
    
        msg.target = this;
        msg.workSourceUid = ThreadLocalWorkSource.getUid();

        if (mAsynchronous) {
    
    
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }

Will call MessageQueue.enqueueMessageto add the message to the Message linked list. The question is, where does the MessageQueue here come from?

This needs to be traced back to the Handler's constructor, and a Looper needs to be passed in as a parameter. The MessageQueue is obtained from the Looper.

This means that each Handler can only handle one Looper's transaction. Why can only one Looper transaction be processed? My understanding is this: Looper collects all messages or transactions, and then forwards them one by one for execution. Although the messages are sent asynchronously, the Handler executes the task synchronously, so there is no thread synchronization problem.

    boolean enqueueMessage(Message msg, long when) {
    
    
        if (msg.target == null) {
    
    
            throw new IllegalArgumentException("Message must have a target.");
        }
        synchronized (this) {
    
    
            if (mQuitting) {
    
    
                IllegalStateException e = new IllegalStateException(
                        msg.target + " sending message to a Handler on a dead thread");
                Log.w(TAG, e.getMessage(), e);
                msg.recycle();
                return false;
            }

            msg.markInUse();
            msg.when = when;
            Message p = mMessages;
            boolean needWake;
            if (p == null || when == 0 || when < p.when) {
    
    
                // New head, wake up the event queue if blocked.
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            }
            if (needWake) {
    
    
                nativeWake(mPtr);
            }
        }
        return true;
    }

After entering MessageQueue.enqueueMessage, the message will be added to the queue. If the current message queue is empty, nativeWake will be called to interrupt the execution of nativePollOnce, thereby immediately processing the message we actively posted.

When Looper distributes messages, who should be handed over to handle it? The answer is which Handler sends the message, the message will be processed by which Handler. You can check the enqueueMessage method for this.

In addition to using Handler to post messages, Message itself also has a sendToTargetmethod that can send itself to the specified Handler, and then the Handler will be added to the MessageQueue queue. If you are interested, you can read the relevant code.

In addition to using Handler sendMessage, we often see the use of Handler post Runnable. What is Runnable?

The Message sent by sendMessage is often set with what information, and then the Handler will perform corresponding processing based on what. The code example is as follows:

        Handler handler = new Handler(getMainLooper()) {
    
    
            @Override
            public void handleMessage (Message msg) {
    
    
                switch (msg.what) {
    
    
                    case 1:
                        break;
                    default:
                        break;
                }
            }
        };
        Message msg = Message.obtain();
        msg.what = 1;
        handler.sendMessage(msg);

Sometimes we don't want the Handler to identify and process the messages we send, but just want to complete a task. In this case, we can post Runnable. The Runnable will encapsulate a Message in the form of a callback. During distribution processing, the transaction written in the Runnable will be directly executed, and there is no need to enter the handleMessage method.

    public void dispatchMessage(@NonNull Message msg) {
    
    
        if (msg.callback != null) {
    
    
            handleCallback(msg);
        } else {
    
    
            if (mCallback != null) {
    
    
                if (mCallback.handleMessage(msg)) {
    
    
                    return;
                }
            }
            handleMessage(msg);
        }
    }

We can also dump the Message in the Looper to see the content in the MessageQueue and the current status of the Looper. The following is a dump in the Button's onClick method. The log is as follows:

2023-08-28 23:50:15.953 7495-7495/com.example.loopertest D/MainActivity: onClick
2023-08-28 23:50:15.953 7495-7495/com.example.loopertest D/MainActivity: Looper (main, tid 2) {
    
    f4105a7}
2023-08-28 23:50:15.954 7495-7495/com.example.loopertest D/MainActivity:   Message 0: {
    
     when=-2ms callback=android.view.View$UnsetPressedState target=android.view.ViewRootImpl$ViewRootHandler }
2023-08-28 23:50:15.954 7495-7495/com.example.loopertest D/MainActivity:   (Total messages: 1, polling=false, quitting=false)

At this point, the basic principles of Looper Handler MessageQueue are finished. Next, let’s understand how they are actually used.

2、How to use

2.1、Thread

Before knowing how to use it, there are two points to note:

  1. The processing of UI content can only be placed in the main thread;
  2. A thread can only have one Looper;

The second point is that a thread can only have one Looper because each thread can only block at one place. If a thread has two Loopers, then the loop of one Looper will be blocked by the loop of the other Looper.

According to the previous content, we know that when the app starts, it will automatically create a MainLooper to process UI-related messages. However, in actual app writing, there will be various tasks. If all tasks are placed on the UI thread, If it is executed during execution, it may affect the processing of UI events and cause ANR and other situations.

For example, I add the following code to onCreate:

        new Handler(getMainLooper()).post(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                for (int i = 0; i < 5; i++) {
    
    
                    try {
    
    
                        Log.d(LOG_TAG, "i = " + i +" thread id = " + Process.myTid());
                        sleep(1000);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
            }
        });

As you can see, when starting up, you need to wait for the Runnable to finish executing before rendering the UI, which is obviously not appropriate. In order to solve the problem of general transactions occupying the UI thread, transactions unrelated to the UI or some time-consuming transactions are often processed in a new thread.

The simplest way to use threads with Looper Handler is as follows:

		// 问题示例
		private Handler handler;
        new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                Looper.prepare();
                handler = new Handler(Looper.myLooper()) {
    
    
                    @Override
                    public void handleMessage (Message msg) {
    
    
                        switch (msg.what) {
    
    
                            case 1:
                                Log.d(LOG_TAG, "Process Message 1");
                                break;
                            default:
                                break;
                        }
                    }
                };
                Looper.loop();
            }
        }).start();
        Message msg = Message.obtain();
        msg.what = 1;
        handler.sendMessage(msg);

To use Looper Handler in a child thread, we have 3 things to do:

  1. Create and start threads;
  2. Create a Looper in the thread and call Looper.loop to block the thread;
  3. Create a Handler and bind it to the Looper in the thread;

Is there anything wrong with the above code? Let’s run the app and take a look, ah! There will be a null pointer error:

08-28 16:05:44.815  8436  8436 E AndroidRuntime: Process: com.example.loopertest, PID: 8436
08-28 16:05:44.815  8436  8436 E AndroidRuntime: java.lang.RuntimeException: Unable to start activity ComponentInfo{
    
    com.example.loopertest/com.example.loopertest.MainActivity}: java.lang.NullPointerExceptio
n: Attempt to invoke virtual method 'boolean android.os.Handler.sendMessage(android.os.Message)' on a null object reference

Analyze calmly! This is because thread startup will be executed in parallel with sendMessage. When sendMessage is executed, the handler may not have been created yet, so a null pointer error will occur. The solution is to add a delay before sendMessage to ensure that the handler has been created:

		// 解决办法
        Message msg = Message.obtain();
        msg.what = 1;
        try {
    
    
            Thread.sleep(1000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        handler.sendMessage(msg);

At this time, someone may ask, why don't I create Looper outside the thread first, and then call Looper.loop inside the thread? Good question. Remember what we said before that a thread can only have one Looper. The external one is the main thread. If it is created externally, the main thread will have two Loopers. Let’s look at Looper’s statement:

public final class Looper {
    
    
	static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
	private static Looper sMainLooper;  // guarded by Looper.class
    public static void prepare() {
    
    
        prepare(true);
    }

    private static void prepare(boolean quitAllowed) {
    
    
        if (sThreadLocal.get() != null) {
    
    
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        sThreadLocal.set(new Looper(quitAllowed));
    }

sMainLooperIt is a static variable, which means that there can only be one MainLooper in our process, which is what we call the main thread Looper; in addition, Java also provides a local static variable of the thread to ensure that the static variables of sThreadLocal each thread are different. .

The prepare method is used to create a Looper in a thread. If prepare is called twice in a thread, an exception will be thrown. This also confirms what we said above that a thread can only have one Looper.

At this time, someone may ask again, why don't I create a Handler instance outside and then bind the Thread's Looper to the Handler? Not bad, let’s try it:

    class MyThread extends Thread {
    
    
        @Override
        public void run() {
    
    
            Looper.prepare();
            Looper.loop();
        }

        Looper getLooper() {
    
    
            return Looper.myLooper();
        }
    }

	private MyThread myThread;
    myThread = new MyThread();
    myThread.start();
    handler = new Handler(myThread.getLooper()) {
    
    
        @Override
        public void handleMessage (Message msg) {
    
    
            switch (msg.what) {
    
    
                case 1:
                    Log.d(LOG_TAG, "Process Message 1");
                    break;
                default:
                    break;
            }
        }
    };
    Message msg = Message.obtain();
    msg.what = 1;
    handler.sendMessage(msg);

The actual running results are as follows, no problem! This will be used from now on

2023-08-29 21:27:16.583 9284-9284/com.example.loopertest D/MainActivity: Process Message 1

2.2、HandlerThread

Do you want to applaud our wit? Hold on! Android seems to have implemented the method we want, this is it HandlerThread! HandlerThread has exactly the same idea as ours, so the usage is exactly the same! Here are examples without further explanation.

        handlerThread = new HandlerThread("Test HandlerThread");
        handlerThread.start();
        Message msg = Message.obtain();
        msg.what = 1;
        new Handler(handlerThread.getLooper()) {
    
    
            @Override
            public void handleMessage (Message msg) {
    
    
                switch (msg.what) {
    
    
                    case 1:
                        Log.d(LOG_TAG, "Process Message 1");
                        break;
                    default:
                        break;
                }
            }
        }.sendMessage(msg);
2023-08-29 00:15:51.853 8743-8743/com.example.loopertest D/MainActivity: onCreate
2023-08-29 00:15:51.871 8743-8858/com.example.loopertest D/MainActivity: Process Message 1
2023-08-29 00:15:51.871 8743-8743/com.example.loopertest D/MainActivity: onStart
2023-08-29 00:15:51.872 8743-8743/com.example.loopertest D/MainActivity: onPostCreate
2023-08-29 00:15:51.872 8743-8743/com.example.loopertest D/MainActivity: onResume
2023-08-29 00:15:51.872 8743-8743/com.example.loopertest D/MainActivity: onPostResume

2.3. Send messages to each other between threads

We may often hear that the sub-thread sends messages to the main thread, the main thread sends messages to the sub-thread, and the sub-thread sends messages to the sub-thread. Are you a little dizzy when you see so many situations?

In fact, the principles of these three types of mutual messaging are the same. Remember what we said above, the Handler must be bound to a Looper when it is created:

public Handler(@NonNull Looper looper) {
    
    
	this(looper, null, false);
}

After binding the Looper, the Handler can only process the messages distributed by the bound Looper. So how do the messages in the bound Looper come from? In addition to the messages sent by native, can we call the Handler bound to Looper to send messages?

That is to say, to let the message be executed in the specified Looper thread, just call the post/sendMessage method of the Handler bound to the Looper of the thread.

It's that simple!

2.4. How to stop Looper

Here I want to talk about the most common memory leak problem on the Internet. First, give an example (I don’t know if I understand it correctly):

We add the following code to the onCreate method and close the app immediately after opening it

        new Handler(getMainLooper()).postDelayed(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                for (int i = 0; i < 5; i++) {
    
    
                    try {
    
    
                        Log.d(LOG_TAG, "i = " + i +" thread id = " + Process.myTid());
                        sleep(1000);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
            }
        }, 5000);

You can see the following phenomena:

2023-08-29 21:51:30.568 9599-9599/com.example.loopertest D/MainActivity: onCreate
2023-08-29 21:51:30.586 9599-9599/com.example.loopertest D/MainActivity: onStart
2023-08-29 21:51:30.587 9599-9599/com.example.loopertest D/MainActivity: onPostCreate
2023-08-29 21:51:30.587 9599-9599/com.example.loopertest D/MainActivity: onResume
2023-08-29 21:51:30.587 9599-9599/com.example.loopertest D/MainActivity: onPostResume
2023-08-29 21:51:31.783 9599-9599/com.example.loopertest D/MainActivity: onKeyDown keyCode = 4
2023-08-29 21:51:31.873 9599-9599/com.example.loopertest D/MainActivity: onPause
2023-08-29 21:51:32.399 9599-9599/com.example.loopertest D/MainActivity: onStop
2023-08-29 21:51:32.399 9599-9599/com.example.loopertest D/MainActivity: onDestroy
2023-08-29 21:51:35.624 9599-9599/com.example.loopertest D/MainActivity: i = 0 thread id = 9599
2023-08-29 21:51:36.635 9599-9599/com.example.loopertest D/MainActivity: i = 1 thread id = 9599
2023-08-29 21:51:37.677 9599-9599/com.example.loopertest D/MainActivity: i = 2 thread id = 9599
2023-08-29 21:51:38.701 9599-9599/com.example.loopertest D/MainActivity: i = 3 thread id = 9599
2023-08-29 21:51:39.744 9599-9599/com.example.loopertest D/MainActivity: i = 4 thread id = 9599

Hey! Obviously onDestroy has been called, why can the loop content still be printed? I wonder if this is the memory leak mentioned in other blog posts. When exiting the activity, the activity's resources are still occupied, causing the resources to not be released normally?

Some solutions on the Internet are to declare Handler as a static class. I don't think this is a good solution here.

When exiting a program or activity, the thread where the Looper is located is running, and we normally need to stop the thread. Since Looper.loop is blocking, we need to call Looper.quitor Looper.quitSafelyexit the infinite loop.

I want to ask a question here. After calling quit / quitSafely, does Looper really stop?

The answer is no, Looper does not necessarily stop immediately, it needsCan only be stopped after executing the current task! If the current task is a time-consuming task, it will not really stop until it finishes executing the Looper.

What do we have to do?

  1. Add an interruption mechanism when executing time-consuming work in Looper;
  2. Looper.quit is called when activity ends;
  3. Call Thread.join to block and wait for the thread to end;

I think the resources can be released normally and end in this way. I don’t know if my understanding is correct. Next, use HandlerThread to give an example:

Add the following code in onCreate and onDestroy:

	protected void onCreate(Bundle savedInstanceState) {
    
    
        handlerThread = new HandlerThread("Test HandlerThread quit");
        handlerThread.start();
        Message msg = Message.obtain();
        msg.what = 1;
        new Handler(handlerThread.getLooper()).post(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                int i = 0;
                while (i < 10) {
    
    
                    try {
    
    
                        Log.d(LOG_TAG, "i = " + i +" thread id = " + Process.myTid());
                        sleep(1000);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                    i++;
                }
            }
        });
    }

    protected void onStop() {
    
    
        Log.d(LOG_TAG, "onStop");
        super.onStop();
        handlerThread.quitSafely();
    }

Exit the app immediately after starting the app. You can see that we have called quitSafely, but the thread still does not stop immediately and the resources are not released normally:

2023-08-29 22:43:24.101 10559-10559/com.example.loopertest D/MainActivity: onCreate
2023-08-29 22:43:24.203 10559-10586/com.example.loopertest D/MainActivity: i = 0 thread id = 10586
2023-08-29 22:43:24.207 10559-10559/com.example.loopertest D/MainActivity: onStart
2023-08-29 22:43:24.210 10559-10559/com.example.loopertest D/MainActivity: onPostCreate
2023-08-29 22:43:24.211 10559-10559/com.example.loopertest D/MainActivity: onResume
2023-08-29 22:43:24.212 10559-10559/com.example.loopertest D/MainActivity: onPostResume
2023-08-29 22:43:25.205 10559-10586/com.example.loopertest D/MainActivity: i = 1 thread id = 10586
2023-08-29 22:43:26.004 10559-10559/com.example.loopertest D/MainActivity: onKeyDown keyCode = 4
2023-08-29 22:43:26.207 10559-10586/com.example.loopertest D/MainActivity: i = 2 thread id = 10586
2023-08-29 22:43:26.231 10559-10559/com.example.loopertest D/MainActivity: onPause
2023-08-29 22:43:26.765 10559-10559/com.example.loopertest D/MainActivity: onStop
2023-08-29 22:43:26.766 10559-10559/com.example.loopertest D/MainActivity: onDestroy
2023-08-29 22:43:27.208 10559-10586/com.example.loopertest D/MainActivity: i = 3 thread id = 10586
2023-08-29 22:43:28.242 10559-10586/com.example.loopertest D/MainActivity: i = 4 thread id = 10586
2023-08-29 22:43:29.258 10559-10586/com.example.loopertest D/MainActivity: i = 5 thread id = 10586
2023-08-29 22:43:30.275 10559-10586/com.example.loopertest D/MainActivity: i = 6 thread id = 10586
2023-08-29 22:43:31.293 10559-10586/com.example.loopertest D/MainActivity: i = 7 thread id = 10586
2023-08-29 22:43:32.308 10559-10586/com.example.loopertest D/MainActivity: i = 8 thread id = 10586
2023-08-29 22:43:33.348 10559-10586/com.example.loopertest D/MainActivity: i = 9 thread id = 10586

We add join after quitSafely. You can see that when exiting the program, onStop blocks at the join position and waits for the thread to end:

2023-08-29 22:43:56.616 10618-10618/com.example.loopertest D/MainActivity: onCreate
2023-08-29 22:43:56.745 10618-10644/com.example.loopertest D/MainActivity: i = 0 thread id = 10644
2023-08-29 22:43:56.748 10618-10618/com.example.loopertest D/MainActivity: onStart
2023-08-29 22:43:56.749 10618-10618/com.example.loopertest D/MainActivity: onPostCreate
2023-08-29 22:43:56.750 10618-10618/com.example.loopertest D/MainActivity: onResume
2023-08-29 22:43:56.750 10618-10618/com.example.loopertest D/MainActivity: onPostResume
2023-08-29 22:43:57.746 10618-10644/com.example.loopertest D/MainActivity: i = 1 thread id = 10644
2023-08-29 22:43:58.748 10618-10644/com.example.loopertest D/MainActivity: i = 2 thread id = 10644
2023-08-29 22:43:59.240 10618-10618/com.example.loopertest D/MainActivity: onKeyDown keyCode = 4
2023-08-29 22:43:59.354 10618-10618/com.example.loopertest D/MainActivity: onPause
2023-08-29 22:43:59.750 10618-10644/com.example.loopertest D/MainActivity: i = 3 thread id = 10644
2023-08-29 22:43:59.863 10618-10618/com.example.loopertest D/MainActivity: onStop
2023-08-29 22:44:00.784 10618-10644/com.example.loopertest D/MainActivity: i = 4 thread id = 10644
2023-08-29 22:44:01.797 10618-10644/com.example.loopertest D/MainActivity: i = 5 thread id = 10644
2023-08-29 22:44:02.840 10618-10644/com.example.loopertest D/MainActivity: i = 6 thread id = 10644
2023-08-29 22:44:03.881 10618-10644/com.example.loopertest D/MainActivity: i = 7 thread id = 10644
2023-08-29 22:44:04.922 10618-10644/com.example.loopertest D/MainActivity: i = 8 thread id = 10644
2023-08-29 22:44:05.926 10618-10644/com.example.loopertest D/MainActivity: i = 9 thread id = 10644
2023-08-29 22:44:06.948 10618-10618/com.example.loopertest D/MainActivity: onDestroy

Let’s add another condition for thread exit:

        new Handler(handlerThread.getLooper()).post(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                int i = 0;
                while (i < 10) {
    
    
                    if (isQuit)
                        break;
                    try {
    
    
                        Log.d(LOG_TAG, "i = " + i +" thread id = " + Process.myTid());
                        sleep(1000);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                    i++;
                }
            }
        });

As you can see, when we click the return button to exit the app, the program can exit normally, and the sub-thread no longer prints out content, so there will be no memory leaks.

2023-08-29 22:48:03.391 10748-10748/com.example.loopertest D/MainActivity: onCreate
2023-08-29 22:48:03.525 10748-10773/com.example.loopertest D/MainActivity: i = 0 thread id = 10773
2023-08-29 22:48:03.528 10748-10748/com.example.loopertest D/MainActivity: onStart
2023-08-29 22:48:03.529 10748-10748/com.example.loopertest D/MainActivity: onPostCreate
2023-08-29 22:48:03.530 10748-10748/com.example.loopertest D/MainActivity: onResume
2023-08-29 22:48:03.530 10748-10748/com.example.loopertest D/MainActivity: onPostResume
2023-08-29 22:48:04.525 10748-10773/com.example.loopertest D/MainActivity: i = 1 thread id = 10773
2023-08-29 22:48:05.527 10748-10773/com.example.loopertest D/MainActivity: i = 2 thread id = 10773
2023-08-29 22:48:06.528 10748-10773/com.example.loopertest D/MainActivity: i = 3 thread id = 10773
2023-08-29 22:48:06.733 10748-10748/com.example.loopertest D/MainActivity: onKeyDown keyCode = 4
2023-08-29 22:48:06.854 10748-10748/com.example.loopertest D/MainActivity: onPause
2023-08-29 22:48:07.399 10748-10748/com.example.loopertest D/MainActivity: onStop
2023-08-29 22:48:07.531 10748-10748/com.example.loopertest D/MainActivity: onDestroy

Okay, that’s it for the analysis of the Android Looper Handler mechanism. If you find this article helpful, please don’t hesitate to like and follow it. Goodbye!

Guess you like

Origin blog.csdn.net/qq_41828351/article/details/132529181