Android Looper Handler 机制浅析

news2024/11/19 9:27:29

最近想写个播放器demo,里面要用到 Looper Handler,看了很多资料都没能理解透彻,于是决定自己看看相关的源码,并在此记录心得体会,希望能够帮助到有需要的人。

本文会以 猜想 + log验证 的方式来学习 Android Looper Handler,对于一些复杂的代码会进行跳过,能够理解它们的设计原理即可。本文观点皆个人拙见,如有错误恳请赐教。

请添加图片描述

1、Looper Handler MessageQueue

Looper 在整个消息处理机制中起着 消息等待与消息分发 的作用:

  • 消息等待:Looper 阻塞程序运行,等待即将到来的消息;
  • 消息分发:Looper 收到消息后,将消息分发给指定处理者在当前线程处理。

首先来看阻塞的问题,app 启动时会创建一个 Looper,并且调用 loop 函数阻塞 main 函数,这个 Looper 被称为主线程/主进程 Looper,代码参考 ActivityThread.java :

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

主线程 Looper(或者又叫 mainLooper)有着阻止程序结束的作用,这里就会引申出其他问题,为什么它阻塞了程序还能继续运行呢?

上文的消息等待作用中,我们说到 Looper 阻塞程序运行时,会等待即将到来的消息,什么消息会到来呢?

我写了一个 demo,界面上只有一个 Button,点击图标启动 app 并且点击按钮,接着我们来观察 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

可以看到 log 的线程号和进程号相同,也就是说所有的消息都是在主线程(UI线程)/主进程中进行处理的,那 Looper 收到有 Activity 的启动事件、按钮的点击事件,引申来说主进程将会收到并处理所有的 UI 事件。

Looper 是如何接收事件并处理的呢?loop 方法中有一个死循环,不断执行 loopOnce,阻塞也在此发生:

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

loopOnce 中会看到一个 MessageQueue 对象,这是 Looper 所持有的消息队列,它内部维护着一个链表,消息等待也由它完成。

    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.next 是一个阻塞方法,当没有消息时,它将阻塞等待,可以防止上一层 for 循环空转;有消息时就返回 Message,分发给指定的 Handler 来处理。

我们再看下 loop 循环要如何退出?从上面的代码我们可以看到,当返回的 Message 是 null 时,loopOnce 返回 false,整个 loop 循环结束。

MessageQueue.next 代码如下:

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

这里的代码比较长,核心内容是:

  1. 调用 nativePollOnce 在 native 层阻塞 poll 消息;
  2. 将 poll 到的消息根据指定的执行时间进行排序,如果没有指定时间则按消息到底的先后顺序排序;
  3. 如果链表中第一条消息需要延时,则继续调用 nativePollOnce,并且设定一个超时时间;
  4. 将需要处理的消息返回给 loopOnce;
  5. 如果调用了 quit 方法,则返回 null,终止循环。

以上代码我们看到消息的获取方式是调用 nativePollOnce 进行等待,这里的 native 消息可能就是 android 系统发送给 mainLooper 的,例如触摸点击事件等等。除此之外,我们也可以主动给 mainLooper 发消息,这就需要使用 Handler 了。

我们调用 Handler 的 sendMessage 方法,最终会执行到 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);
    }

会调用 MessageQueue.enqueueMessage 将消息加入到 Message 链表中。问题来了,这里的 MessageQueue 是哪里来的?

这就需要追溯到 Handler 的构造函数,需要将一个 Looper 作为参数传递进去,MessageQueue 就是从 Looper 中获得的。

这也就意味着,每个 Handler 只能处理一个 Looper 的事务。为什么只能处理一个 Looper 的事务呢?我的理解是这样:Looper 将所有的消息或事务进行收集,接着再一条一条转发执行,虽然消息是异步发出的,但是 Handler 执行任务是同步的,这就没有了线程同步的问题。

    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;
    }

进入到 MessageQueue.enqueueMessage 后,消息会被加入到队列当中,如果当前消息队列是空的,就会调用 nativeWake 来打断 nativePollOnce 的执行,从而立即处理我们主动 post 的消息。

Looper 进行消息分发时,应该交给谁来处理呢?答案是哪个 Handler send 的消息,消息就由哪个 Handler 处理,这一点可以查看 enqueueMessage 方法。

除了用 Handler post 消息外,Message 本身也有一个 sendToTarget 方法,可以将自己发送给指定的 Handler,由 Hander 再加入到 MessageQueue 队列中,有兴趣可以阅读相关代码。

除了用 Handler sendMessage 之外,我们还常常见到用 Handler post Runnable。Runnable是什么?

被 sendMessage 发出的 Message 常常会设定有 what 信息,之后 Handler 会根据 what 来做对应的处理,代码示例如下:

        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);

有的时候我们并不希望 Handler 对我们发出的消息进行识别处理,而是仅仅是想完成一项任务,这时候我们可以去 post Runnable。Runnable 会以 callback 的形式封装称为一个 Message,在分发处理时直接执行 Runnable 中所写的事务,就不需要再进入到 handleMessage 方法中了。

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

我们也可以 dump Looper 中的 Message,看到 MessageQueue 中的内容以及 Looper 当前的状态,以下是在 Button 的 onClick 方法中进行 dump,log 如下:

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)

到这里,Looper Handler MessageQueue 的基本原理就讲完了,接下来我们来了解它们的实际是如何使用的。

2、How to use

2.1、Thread

在了解如何使用之前,有两点需要先注意:

  1. UI内容的处理只能放在主线程当中;
  2. 一个线程只能有一个 Looper;

第二点一个线程只能有一个 Looper,是因为每个线程只能有一处阻塞,如果有一个线程有两个 Looper,那么其中一个 Looper 的 loop 循环会被另一个 Looper 的 loop 循环所阻塞。

根据前面的内容我们了解到,app 在启动时会自动创建一个 MainLooper 用于处理 UI 相关的消息,但是在实际 app 编写中会有各种各样的任务,如果将所有的任务都放在 UI 线程中执行,那么可能就会影响 UI 事件的处理,出现 ANR 等情况。

例如我在 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();
                    }
                }
            }
        });

可以看到,启动的时候需要等待 Runnable 执行结束才能渲染出 UI,这显然是不太合适的。为了解决一般事务占用 UI 线程的问题,常常会在一个新的线程中来处理与 UI 不相干的事务或者一些耗时的事务。

线程 与 Looper Handler 协同使用的最简单的方式如下:

		// 问题示例
		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);

想要在子线程中使用 Looper Handler,我们有3件事需要做:

  1. 创建并启动线程;
  2. 在线程中创建 Looper,并且调用 Looper.loop 阻塞线程;
  3. 创建 Handler 并且与线程中的 Looper 相绑定;

以上代码是否有问题呢?我们将 app 跑起来看一下,呀!会有空指针的错误:

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

冷静分析!这是因为 线程启动 会和 sendMessage 并列执行,当执行到 sendMessage 时,handler 可能还没有创建,所以会出现空指针的错误。解决办法是在 sendMessage 之前加一个 delay,保证 handler 已经被创建:

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

这时候可能有人要问了,我为什么不在线程外部先创建 Looper,然后在线程内部调用 Looper.loop 呢?好问题,还记得我们之前说过一个线程只能有一个 Looper 吗,外部是主线程,如果在外部创建,主线程就会有两个 Looper 了。来看 Looper 的声明:

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));
    }

sMainLooper 是一个静态变量,意味着我们的进程中只能存在一个 MainLooper,也就是我们所说的主线程 Looper;另外java还提供了一种线程的局部静态变量 sThreadLocal ,保证每个线程的静态变量是不同的。

prepare方法用于在线程中创建 Looper,如果一个线程中调用两次 prepare,那么就会抛出异常了,这也印证了我们上文所说的一个线程只能有一个 Looper。

这时候可能又有人要问了,为什么我不在不在外面创建 Handler 实例,然后将 Thread 的 Looper 与 Handler 绑定呢?不错,我们来试试:

    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);

实际跑起来结果如下,没有问题!以后就这么用了

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

2.2、HandlerThread

是不是要为我们的机智鼓个掌呢?稍等!android 好像已经实现了我们所想的方法,这就是 HandlerThread!HandlerThread 简直就和我们的想法一模一样,所以用法也是一模一样!这里给出示例,就不再多做解释了。

        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、线程间互发消息

我们可能经常会听到 子线程往主线程发送消息,主线程往子线程发送消息,子线程往子线程发送消息,一看到这么多情况是不是有点晕?

其实这三种互发消息的原理是一致的,还记得我们上文说过,Handler 创建时要与一个 Looper 绑定:

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

绑定 Looper 之后,Handler 只能处理绑定的 Looper 分发的消息了,那绑定的 Looper 中的消息怎么来的?除了 native 发送的消息外,我们是不是可以调用与 Looper 绑定的 Handler 来发送消息?

也就是说,要让消息在指定 Looper 线程中执行,只要调用与该线程 Looper 绑定的 Handler 的 post / sendMessage 方法。

就是这么简单!

2.4、Looper 如何停止

这里要谈一谈网上讲的最多的内存泄漏的问题,首先给个示例(不知道我理解的对不对哈):

我们在 onCreate 方法中增加以下代码,打开 app 后立刻关闭

        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);

可以看到以下现象:

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

嘿!明明已经调用了 onDestroy 了,为什么还能打印出循环内容呢?不知道这是不是其他博文中所说的内存泄漏,退出 activity 时仍占用着 activity 的资源,导致资源无法正常释放?

网上的一些解法是将 Handler 声明为静态类,这里我并不认为这是好的解法。

退出程序或者 activity 时,Looper 所在的线程正在运行,我们正常是需要停止线程的。由于 Looper.loop 正在阻塞运行,我们要调用 Looper.quit 或者 Looper.quitSafely 退出死循环。

这里我要提出一个问题,调用了 quit / quitSafely 后,Looper 就真的停止了吗?

答案是否定的,Looper 并不一定会立即停止,它需要执行完当前的任务才能停止!如果当前任务是一个耗时任务,那么会等他执行完 Looper 才会真正停止。

我们所要做的是什么呢?

  1. 把耗时工作放到 Looper 中执行时添加打断机制;
  2. ativity 结束时调用 Looper.quit;
  3. 调用 Thread.join 阻塞等待线程结束;

我觉得这样资源可以正常释放结束,不知道我理解的是否正确。接下来用 HandlerThread 给出一个示例:

在 onCreate 和 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();
    }

启动 app 后立刻退出 app,可以看到我们已经调用了 quitSafely,线程仍然没有立即停止,资源没有正常释放:

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

我们在 quitSafely 后面加上 join,可以看到退出程序时,onStop 阻塞在 join 的位置上等待线程结束:

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

我们再给线程退出加上一个条件:

        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++;
                }
            }
        });

可以看到,我们点击返回键退出 app 时,程序可以正常退出,并且子线程没有再打印出内容了,这样是不是就不会有内存泄漏了呢。

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

好了,关于 Android Looper Handler 机制就分析到这里,如果觉得这篇文章有帮助,还请不要吝啬点赞关注哦,再见!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/946171.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Kafka3.0.0版本——Follower故障处理细节原理

目录 一、服务器信息二、服务器基本信息及相关概念2.1、服务器基本信息2.2、LEO的概念2.3、HW的概念 三、Follower故障处理细节 一、服务器信息 三台服务器 原始服务器名称原始服务器ip节点centos7虚拟机1192.168.136.27broker0centos7虚拟机2192.168.136.28broker1centos7虚拟…

数组(个人学习笔记黑马学习)

一维数组 1、定义方式 #include <iostream> using namespace std;int main() {//三种定义方式//1.int arr[5];arr[0] 10;arr[1] 20;arr[2] 30;arr[3] 40;arr[4] 50;//访问数据元素/*cout << arr[0] << endl;cout << arr[1] << endl;cout &l…

工地扬尘监测系统 yolo

工地扬尘监测系统算法能够通过yolo网络框架模型&#xff0c;工地扬尘监测系统算法自动对区域的扬尘、粉尘颗粒进行实时监测识别&#xff0c;并及时进行预警&#xff0c;有效防止扬尘污染。Yolo意思是You Only Look Once&#xff0c;它并没有真正的去掉候选区域&#xff0c;而是…

nginx的安装和全局配置

目录 何为nginx Nginx 功能介绍 基础特性 nginx架构 Nginx 进程结构 nginx 模块 模块分类 使用方式 编译安装nginx 第一步&#xff1a;获取安装包 第二步&#xff1a;安装依赖 第三步&#xff1a;创建用户nginx 第四步&#xff1a;解压安装包 第五步&#xff1a;编译…

DSP_TMS320F28377D_算法加速方法1_拷贝程序到RAM运行

TI C2000系列的DSP芯片算力有限&#xff0c;用于来控制有时候常会出现控制程序无法实现实时运行的情况&#xff0c;因此从本文开始&#xff0c;将陆续推出几篇DSP算法加速的方法 此方法只需要添加一行代码和一个预定义&#xff0c;即可达到算法整体加速的目的。先声明本文是讲的…

jeecgboot的online在线开发高级版【伸手党福利】

本文章在于充分发挥jeecgboot的在线开发功能&#xff0c;将平时开发80%以上的工作量全部收束 目录 树形结构使用固定值替换id从其他表中引用字段输入和搜索&#xff08;下拉搜索&#xff09;输入用户、单位等框架内固有值主附表 树形结构 是否树》是 自动生成pid&#xff0c;…

oracle19c-图形安装(centos7)

目录 一.环境准备1.关闭防火墙2.关闭SELINUX3.配置本地yum源4.安装ORACLE先决条件的软件包5.修改LINUX的内核文件6.添加下列参数到/etc/security/limits.conf7.添加下列条目到/etc/pam.d/login8.环境变量中添加下列语句9.创建文件目录和相应的用户10.配置oracle用户的环境变量1…

paddle 1-高级

目录 为什么要精通深度学习的高级内容 高级内容包含哪些武器 1. 模型资源 2. 设计思想与二次研发 3. 工业部署 4. 飞桨全流程研发工具 5. 行业应用与项目案例 飞桨开源组件使用场景概览 框架和全流程工具 1. 模型训练组件 2. 模型部署组件 3. 其他全研发流程的辅助…

别考,快跑!

写在前面 我一般不直接推荐院校&#xff0c;但是根据这所院校的23情况来看&#xff0c;复试一票否决率太高了&#xff0c;我看的怀疑人生&#xff0c;所以纠结很久&#xff0c;还是说出我自己的想法&#xff1a;我不推荐我的粉丝考这所学校&#xff0c;我更希望大家考研上岸是…

PMP教材改版难不难?考生应如何应对?

有很多同学都在担心PMP教材改版后&#xff0c;考试会不会增加难度&#xff0c;今天胖圆给大家详细讲解一下&#xff0c;在PMP改革背景下&#xff0c;考生应该怎样应对&#xff1f; 新教材的改动意味着8月以后的考试都将 《PMBOK指南》第七版的内容纳入考试范围&#xff0c;其中…

【USRP】调制解调系列7:GMSK、MSK、基于labview的实现

MSK 在数字调制中&#xff0c;最小频移键控&#xff08;Minimum-Shift Keying&#xff0c;缩写&#xff1a;MSK&#xff09;是一种连续相位的频移键控方式&#xff0c;在1950年代末和1960年代产生。与偏移四相相移键控&#xff08;OQPSK&#xff09;类似&#xff0c;MSK同样将…

JavaScript关于函数的小挑战

题目 回到两个体操队&#xff0c;即海豚队和考拉队! 有一个新的体操项目&#xff0c;它的工作方式不同。 每队比赛3次&#xff0c;然后计算3次得分的平均值&#xff08;所以每队有一个平均分&#xff09;。 只有当一个团队的平均分至少是另一个团队的两倍时才会获胜。否则&…

openGauss学习笔记-54 openGauss 高级特性-MOT

文章目录 openGauss学习笔记-54 openGauss 高级特性-MOT54.1 MOT特性及价值54.2 MOT关键技术54.3 MOT应用场景54.4 不支持的数据类型54.5 使用MOT54.6 将磁盘表转换为MOT openGauss学习笔记-54 openGauss 高级特性-MOT openGauss引入了MOT&#xff08;Memory-Optimized Table&…

Glide分析和总结

1. Glide概述 Glide是一款图片处理的框架&#xff0c;从框架设计的角度出发&#xff0c;最基本要实现的就是 加载图片 和 展示。 它把一个图片请求封装成一个Request对象&#xff0c;里面有开启、暂停、关闭、清除网络请求、以及载体生命周期的监听等操作。然后通过RequestBu…

导入表解析与IATHook

IAT&#xff1a;导入地址表// PE文件解析.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。 //#define _CRT_SECURE_NO_WARNINGS #include <iostream> #include <Windows.h>//函数向前声明 DWORD RvaToFoa(DWORD dwRva, const char* szBuff…

【人工智能】—_贝叶斯网络、概率图模型、全局语义、因果链、朴素贝叶斯模型、枚举推理、变量消元

文章目录 频率学派 vs. 贝叶斯学派贝叶斯学派Probability&#xff08;概率&#xff09;:独立性/条件独立性&#xff1a;Probability Theory&#xff08;概率论&#xff09;:Graphical models &#xff08;概率图模型&#xff09;什么是图模型&#xff08;Graphical Models&…

【Vue3】transition-group 过渡列表

1. 基本使用 transition-group 和 transition 的用法基本上是一样的。 <template><div class"content"><button click"add">add</button><button click"pop">pop</button><div class"wrap"&…

Michael.W基于Foundry精读Openzeppelin第33期——EIP712.sol

Michael.W基于Foundry精读Openzeppelin第33期——EIP712.sol 0. 版本0.1 EIP712.sol 1. 目标合约2. 代码精读2.1 constructor(string memory name, string memory version)2.2 _domainSeparatorV4()2.3 _hashTypedDataV4(bytes32 structHash) 0. 版本 [openzeppelin]&#xff…

OpenVINO2023使用简介

1 下载安装 先在anaconda中创建一个虚拟环境&#xff0c;该环境的python版本为3.7&#xff0c;之所以使用python3.7&#xff0c;是因为我在3.9上安装过程中出现不少bug&#xff0c;后面新建了一个3.7的环境才解决&#xff0c;我不知道是否由于和我已有环境中某些包不兼容&…

222. 完全二叉树的节点个数 Python

文章目录 一、题目描述示例 1![在这里插入图片描述](https://img-blog.csdnimg.cn/8c74d1b796b74286999e09d4c6b7682f.png#pic_center)示例 2示例 3 二、代码三、解题思路 一、题目描述 给你一棵 完全二叉树 的根节点 root &#xff0c;求出该树的节点个数。 完全二叉树 的定…