Android Looper Handler 机制浅析

最近想写个播放器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 blockif (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()) {@Overridepublic 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() {@Overridepublic 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() {@Overridepublic void run() {Looper.prepare();handler = new Handler(Looper.myLooper()) {@Overridepublic 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.classpublic 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 {@Overridepublic void run() {Looper.prepare();Looper.loop();}Looper getLooper() {return Looper.myLooper();}}private MyThread myThread;myThread = new MyThread();myThread.start();handler = new Handler(myThread.getLooper()) {@Overridepublic 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()) {@Overridepublic 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() {@Overridepublic 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() {@Overridepublic 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() {@Overridepublic 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.rhkb.cn/news/111414.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

第62步 深度学习图像识别:多分类建模(Pytorch)

基于WIN10的64位系统演示 一、写在前面 上期我们基于TensorFlow环境做了图像识别的多分类任务建模。 本期以健康组、肺结核组、COVID-19组、细菌性&#xff08;病毒性&#xff09;肺炎组为数据集&#xff0c;基于Pytorch环境&#xff0c;构建SqueezeNet多分类模型&#xff0…

Android Activity启动过程一:从Intent到Activity创建

关于作者&#xff1a;CSDN内容合伙人、技术专家&#xff0c; 从零开始做日活千万级APP。 专注于分享各领域原创系列文章 &#xff0c;擅长java后端、移动开发、人工智能等&#xff0c;希望大家多多支持。 目录 一、概览二、应用内启动源码流程 (startActivity)2.1 startActivit…

ADRV9009子卡 设计原理图:FMCJ450-基于ADRV9009的双收双发射频FMC子卡 便携测试设备

FMCJ450-基于ADRV9009的双收双发射频FMC子卡 一、板卡概述 ADRV9009是一款高集成度射频(RF)、捷变收发器&#xff0c;提供双通道发射器和接收器、集成式频率合成器以及数字信号处理功能。北京太速科技&#xff0c;这款IC具备多样化的高性能和低功耗组合&#xff0c;FMC子…

基于亚马逊云科技无服务器服务快速搭建电商平台——部署篇

受疫情影响消费者习惯发生改变&#xff0c;刺激了全球电商行业的快速发展。除了依托第三方电商平台将产品销售给消费者之外&#xff0c;企业通过品牌官网或者自有电商平台销售商品也是近几年电商领域快速发展的商业模式。独立站电商模式可以进行多方面、全渠道的互联网市场拓展…

Git分布式版本控制系统与github

第四阶段提升 时 间&#xff1a;2023年8月29日 参加人&#xff1a;全班人员 内 容&#xff1a; Git分布式版本控制系统与github 目录 一、案例概述 二、版本控制系统 &#xff08;一&#xff09; 本地版本控制 &#xff08;二&#xff09;集中化的版本控制系统 &…

DP读书:鲲鹏处理器 架构与编程(十三)操作系统内核与云基础软件

操作系统内核与云基础软件 鲲鹏软件构成硬件特定软件 鲲鹏软件构成硬件特定软件1. Boot Loader2. SBSA 与 SBBR3. UEFI4. ACPI 操作系统内核Linux系统调用Linux进程调度Linux内存管理Linux虚拟文件系统Linux网络子系统Linux进程间通信Linux可加载内核模块Linux设备驱动程序Linu…

Vue 项目性能优化 — 实践指南

前言 Vue 框架通过数据双向绑定和虚拟 DOM 技术&#xff0c;帮我们处理了前端开发中最脏最累的 DOM 操作部分&#xff0c; 我们不再需要去考虑如何操作 DOM 以及如何最高效地操作 DOM&#xff1b;但 Vue 项目中仍然存在项目首屏优化、Webpack 编译配置优化等问题&#xff0c;所…

自然语言处理(四):全局向量的词嵌入(GloVe)

全局向量的词嵌入&#xff08;GloVe&#xff09; 全局向量的词嵌入&#xff08;Global Vectors for Word Representation&#xff09;&#xff0c;通常简称为GloVe&#xff0c;是一种用于将词语映射到连续向量空间的词嵌入方法。它旨在捕捉词语之间的语义关系和语法关系&#…

Python小知识 - Python中的多线程

Python中的多线程 线程是进程中的一个执行单元&#xff0c;是轻量级的进程。一个进程可以创建多个线程&#xff0c;线程之间共享进程的资源&#xff0c;比如内存、文件句柄等。 在Python中&#xff0c;使用threading模块实现线程。 下面的代码创建了两个线程&#xff0c;一个输…

2023.8.29 关于性能测试

目录 什么是性能测试&#xff1f; 性能测试常见术语及其性能测试衡量指标 并发 用户数 响应时间 事务 点击率 吞吐量 思考时间 资源利用率 性能测试分类 基准性能测试 负载性能测试 压力性能测试 可靠性性能测试 性能测试执行流程 什么是性能测试&#xff1f; 性…

实用的面试经验分享:程序员们谈论他们的面试历程

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…

docker 学习-- 04 实践2 (lnpmr环境)

docker 学习 系列文章目录 docker 学习-- 01 基础知识 docker 学习-- 02 常用命令 docker 学习-- 03 环境安装 docker 学习-- 04 实践 1&#xff08;宝塔&#xff09; docker 学习-- 04 实践 2 &#xff08;lnpmr环境&#xff09; 文章目录 docker 学习 系列文章目录1. 配…

财务数据分析?奥威BI数据可视化工具很擅长

BI数据可视化工具通常是可以用户各行各业&#xff0c;用于不同主题的数据可视化分析&#xff0c;但面对财务数据分析这块难啃的骨头&#xff0c;能够好好地完成的&#xff0c;还真不多。接下来要介绍的这款BI数据可视化工具不仅拥有内存行列计算模型这样的智能财务指标计算功能…

flutter 上传图片并裁剪

1.首先在pubspec.yaml文件中新增依赖pub.dev image_picker: ^0.8.75 image_cropper: ^4.0.1 2.在Android的AndroidManifest.xml文件里面添加权限 <activityandroid:name"com.yalantis.ucrop.UCropActivity"android:screenOrientation"portrait"andro…

Django基础7——用户认证系统、Session管理、CSRF安全防护机制

文章目录 一、用户认证系统二、案例&#xff1a;登陆认证2.1 平台登入2.2 平台登出2.3 login_required装饰器 三、Django Session管理3.1 Django使用Session3.1.1 Cookie用法3.1.2 Session用法 3.2 案例&#xff1a;用户登录认证 四、Django CSRF安全防护机制 一、用户认证系统…

3d激光slam建图与定位(2)_aloam代码阅读

1.常用的几种loam算法 aloam 纯激光 lego_loam 纯激光 去除了地面 lio_sam imu激光紧耦合 lvi_sam 激光视觉 2.代码思路 2.1.特征点提取scanRegistration.cpp&#xff0c;这个文件的目的是为了根据曲率提取4种特征点和对当前点云进行预处理 输入是雷达点云话题 输出是 4种特征点…

透过源码理解Flutter InheritedWidget

InheritedWidget的核心是保存值和保存使用这个值的widget&#xff0c;通过对比值的变化&#xff0c;来决定是否要通知那些使用了这个值的widget更新自身。 1 updateShouldNotify和notifyClients InheritedWidget通过updateShouldNotify函数控制依赖其的子组件是否在Inherited…

vue cli构建的项目出现 Uncaught runtime errors

使用 vue/cli 脚手架构建的项目&#xff0c;在 npm run dev 运行后&#xff0c;页面出现 Uncaught runtime errors 报错遮罩层&#xff0c;如下图所示。 报错原因 这种错误通常是运行时出的问题&#xff0c;可能是网络错误&#xff0c;可能是变量未定义等等。 这种错误默认在开…

七、Kafka-Kraft 模式

目录 7.1 Kafka-Kraft 架构7.2 Kafka-Kraft 集群部署 7.1 Kafka-Kraft 架构 左图为 Kafka 现有架构&#xff0c;元数据在 zookeeper 中&#xff0c;运行时动态选举 controller&#xff0c;由controller 进行 Kafka 集群管理 右图为 kraft 模式架构&#xff08;实验性&#xff…

华为数通方向HCIP-DataCom H12-821题库(单选题:161-180)

第161题 以下关于 URPF(Unicast Reverse Path Forwarding) 的描述&#xff0c; 正确的是哪一项 A、部署了严格模式的 URPF&#xff0c;也能够可以同时部署允许匹配缺省路由模式 B、如果部署松散模式的 URPF&#xff0c;默认情况下不需要匹配明细路由 C、如果部署松散模式的…