Handler原理

news2024/11/28 16:04:21

Handler原理

  • 前言
    • 1. Handler作用
    • 2. Handler概述
    • 3. 核心类
  • 一、Handler源码分析
    • 1.创建Handler
    • 2.发送消息
    • 3.取消息
    • 4.消息处理
    • 5.线程切换
  • 二、相关内容
    • 1.Handler原理
    • 2.epoll机制
      • 2.1什么是epoll机制?
      • 2.2 select/poll/epoll
      • 2.3 Handler中epoll是如何实现消息队列的阻塞和唤醒?
      • 2.4 epoll高效的本质:
      • 2.5 epoll扩展内容
    • 3.ThreadLocal
    • 4.异步消息与消息屏障
      • 补充:Choreographer
    • 5.消息池Message.obtain()
    • 5.内存泄漏
  • 三、常见问题
    • Q1:Looper死循环为什么不会导致线程卡死?子线程中维护Looper在消息队列无消息的时候处理方案是怎么样的?
    • Q2:描述下什么是epoll机制?
    • Q3:关于ThreadLocal,你是怎么理解的?
    • Q4:Message有哪几种创建方式?哪种效果更好?为什么?
    • Q5:为什么不能在子线程更新UI?
    • Q6:为什么主线程可以直接new Handler?其它子线程可以吗?怎么做?
    • Q7:一个线程有几个Looper?如何保证?又可以有几个Handler?
    • Q8:Handler内存泄漏的原因,其它内部类为什么没有这个问题?
    • Q9:Handler中生产者-消费者设计模式你理解不?
    • Q10:既然存在多个Handler往MessageQue中添加数据(发消息时各个Handler处于不同线程),内部如何保证安全?
    • 多个Handler发送消息;Looper怎么知道要把消息给哪个Handler处理?
    • Q11:使用Handler的postDelay()后消息队列会发生什么变化?
  • 参考

前言

1. Handler作用

Handler作用:线程切换,线程异步消息处理。
Q:Android为什么要有handler呢?
Android UI操作并不是线程安全的,并且这些操作必须在UI线程执行。
如果非要在子线程中更新UI,那会出现什么情况呢?

android.view.ViewRoot$CalledFromWrongThreadException: 
Only the original thread that created a view hierarchy can touch its views.

很容易抛一个CalledFromWrongThreadException异常。
如果在子线程访问UI线程,Android提供了以下的方式:
Activity.runOnUiThread(Runnable)
View.post(Runnable)
View.postDelayed(Runnable, long)
Handler

2. Handler概述

异步消息处理线程启动后会进入一个无限的循环体之中,每循环一次,从其内部的消息队列中取出一个消息,然后回调相应的消息处理函数,执行完成一个消息后则继续循环。若消息队列为空,线程则会阻塞等待。

3. 核心类

  • Handler
  • Looper
  • Mesage
  • MessageQue

Handler负责发送消息,Looper负责的就是创建一个MessageQueue,然后进入一个无限循环体不断从该MessageQueue中读取Mesage。

一、Handler源码分析

1.创建Handler

创建两个Handler对象,一个在主线程中创建,一个在子线程中创建,代码如下所示:

import android.os.Bundle;
import android.os.Handler;

public class MainActivity extends AppCompatActivity {

    private Handler handler1;
    private Handler handler2;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        handler1 = new Handler();
        new Thread(new Runnable() {
            @Override
            public void run() {
                handler2 = new Handler();
            }
        }).start();
    }
}

运行程序,你会发现,在子线程中创建的Handler是会导致程序崩溃的:
在这里插入图片描述
我们尝试在子线程中先调用一下Looper.prepare()呢,代码如下所示:

new Thread(new Runnable() {
	@Override
	public void run() {
		Looper.prepare();
		handler2 = new Handler();
	}
}).start();

果然这样就不会崩溃了。
为什么不调用Looper.prepare()就不行呢?我们来看下Handler的源码(基于android-30),Handler的构造函数如下所示:
在这里插入图片描述
在第224行调用了Looper.myLooper()方法获取了一个Looper对象,如果Looper对象为空,则会抛出一个运行时异常。
什么时候Looper对象会为空呢?接着看Looper.myLooper()中的代码:

这个方法非常简单,就是从sThreadLocal对象中取出Looper。如果sThreadLocal中有Looper存在就返回Looper,如果没有Looper存在就返回空,通过注释也可以看出。
在这里插入图片描述
接着查找是在哪里给sThreadLocal设置Looper,发现是Looper.prepare()方法
在这里插入图片描述
可以看到,首先判断sThreadLocal中是否已经存在Looper了,如果还没有则创建一个新的Looper设置进去。这样也就完全解释了为什么我们要先调用Looper.prepare()方法,才能创建Handler对象。同时也可以看出每个线程中最多只会有一个Looper对象。

Q:这里你可能会有个疑问,主线程中的Handler也没有调用Looper.prepare()方法,为什么就没有崩溃呢?
这是由于在程序启动的时候,系统已经帮我们自动调用了Looper.prepare()方法。查看ActivityThread中的main()方法,代码如下所示:

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

        // CloseGuard defaults to true and can be quite spammy.  We
        // disable it here, but selectively enable it later (via
        // StrictMode) on debug builds, but using DropBox, not logs.
        CloseGuard.setEnabled(false);

        Environment.initForCurrentUser();

        // Set the reporter for event logging in libcore
        EventLogger.setReporter(new EventLoggingReporter());

        // Make sure TrustedCertificateStore looks in the right place for CA certificates
        final File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());
        TrustedCertificateStore.setDefaultUserDirectory(configDir);

        Process.setArgV0("<pre-initialized>");

        Looper.prepareMainLooper();

        // Find the value for {@link #PROC_START_SEQ_IDENT} if provided on the command line.
        // It will be in the format "seq=114"
        long startSeq = 0;
        if (args != null) {
            for (int i = args.length - 1; i >= 0; --i) {
                if (args[i] != null && args[i].startsWith(PROC_START_SEQ_IDENT)) {
                    startSeq = Long.parseLong(
                            args[i].substring(PROC_START_SEQ_IDENT.length()));
                }
            }
        }
        ActivityThread thread = new ActivityThread();
        thread.attach(false, startSeq);

        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }

        if (false) {
            Looper.myLooper().setMessageLogging(new
                    LogPrinter(Log.DEBUG, "ActivityThread"));
        }

        // End of event ActivityThreadMain.
        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
        Looper.loop();

        throw new RuntimeException("Main thread loop unexpectedly exited");
    }

可以看到,在第20行调用了Looper.prepareMainLooper()方法,而这个方法又会再去调用Looper.prepare()方法,代码如下所示:
在这里插入图片描述
我们应用程序的主线程开启的时候就会创建一个Looper对象,从而不需要再手动去调用Looper.prepare()方法了。

这样基本就将Handler的创建过程完全搞明白了,总结一下就是在主线程中可以直接创建Handler对象,而在子线程中需要先调用Looper.prepare()才能创建Handler对象。

2.发送消息

看完了如何创建Handler之后,接下来我们看一下如何发送消息,new出一个Message对象,然后可以使用setData()方法或arg参数等方式为消息携带一些数据,再借助Handler将消息发送出去就可以了,示例代码如下:

new Thread(new Runnable() {
            @Override
            public void run() {
                Message message = handler1.obtainMessage();
                message.arg1 = 1;
                Bundle bundle = new Bundle();
                bundle.putString("data", "data");
                message.setData(bundle);
                handler1.sendMessage(message);
            }
        }).start();

这里Handler到底是把Message发送到哪里去了呢?为什么之后又可以在Handler的handleMessage()方法中重新得到这条Message呢?看来又需要通过阅读源码才能解除我们心中的疑惑了:
在这里插入图片描述
调用了sendMessageDelayed:
在这里插入图片描述
接着调用了sendMessageAtTime,这个方法的源码如下所示:
在这里插入图片描述
sendMessageAtTime()方法接收两个参数,其中msg参数就是我们发送的Message对象,而uptimeMillis参数则表示发送消息的时间,它的值等于自系统开机到当前时间的毫秒数再加上延迟时间,如果你调用的不是sendMessageDelayed()方法,延迟时间就为0,然后将这两个参数都传递到MessageQueue的enqueueMessage()方法中。
在这里插入图片描述
这个MessageQueue又是什么东西呢?其实从名字上就可以看出了,它是一个消息队列,用于将所有收到的消息以队列的形式进行排列,并提供入队和出队的方法。这个类是在Looper的构造函数中创建的,因此一个Looper也就对应了一MessageQueue。
那么enqueueMessage()方法毫无疑问就是入队的方法了,我们来看下这个方法的源码:
(Handler中提供了很多个发送消息的方法,最终都会辗转调用到enqueueMessage()方法中)

boolean enqueueMessage(Message msg, long when) {
        synchronized (this) {
  			//仅保留核心代码
            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;
            } else {
                // Inserted within the middle of the queue.  Usually we don't have to wake
                // up the event queue unless there is a barrier at the head of the queue
                // and the message is the earliest asynchronous message in the queue.
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                msg.next = p; // invariant: p == prev.next
                prev.next = msg;
            }
        }
        return true;
    }

MessageQueue并没有使用一个集合把所有的消息都保存起来,它只使用了一个mMessages对象表示当前待处理的消息。然后观察上面的代码我们就可以看出,所谓的入队其实就是将所有的消息按时间来进行排序,这个时间就是msg.when。具体的操作方法就根据时间的顺序调用msg.next,从而为每一个消息指定它的下一个消息是什么。当然如果你是通过sendMessageAtFrontOfQueue()方法来发送消息的,它也会调用enqueueMessage()来让消息入队,只不过时间为0,这时会把mMessages赋值为新入队的这条消息,然后将这条消息的next指定为刚才的mMessages,这样也就完成了添加消息到队列头部的操作。

3.取消息

入队操作我们就已经看明白了,那出队操作是在哪里进行的呢?这个就需要看一看Looper.loop()方法的源码了,如下所示:

public static final void loop() {
    Looper me = myLooper();
    MessageQueue queue = me.mQueue;
    while (true) {
        Message msg = queue.next(); // might block
        if (msg != null) {
            if (msg.target == null) {
                return;
            }
            if (me.mLogging!= null) me.mLogging.println(
                    ">>>>> Dispatching to " + msg.target + " "
                    + msg.callback + ": " + msg.what
                    );
            msg.target.dispatchMessage(msg);
            if (me.mLogging!= null) me.mLogging.println(
                    "<<<<< Finished to    " + msg.target + " "
                    + msg.callback);
            msg.recycle();
        }
    }
}

可以看到,这个方法从第4行开始,进入了一个死循环,然后不断地调用的MessageQueue的next()方法,我想你已经猜到了,这个next()方法就是消息队列的出队方法。不过由于这个方法的代码稍微有点长,我就不贴出来了,它的简单逻辑就是如果当前MessageQueue中存在mMessages(即待处理消息),就将这个消息出队,然后让下一条消息成为mMessages,否则就进入一个阻塞状态,一直等到有新的消息入队。继续看loop()方法的第14行,每当有一个消息出队,就将它传递到msg.target的dispatchMessage()方法中,那这里msg.target又是什么呢?其实就是Handler啦,你观察一下上面sendMessageAtTime()方法的第6行就可以看出来了。

ps:这里msg.target通过target将Handler存入Message,是为了解决在多个Hander的情况无法找到处理当前消息的Handler问题。实际上是一种架构设计上的妥协,我们常见的Hander内存泄漏问题也是源于此。最终导致Activity无法及时回收:
Thread–>Looper–>MessageQue–>Message.target–>mHandler–>Activity

4.消息处理

那么发送消息后,最终又是怎么调用到handleMessage的呢?接下来看一下Handler中dispatchMessage()方法的源码,如下所示:
在这里插入图片描述
在第101行进行判断,如果mCallback不为空,则调用mCallback的handleMessage()方法,否则直接调用Handler的handleMessage()方法,并将消息对象作为参数传递过去。这样我相信大家就都明白了为什么handleMessage()方法中可以获取到之前发送的消息了吧!

5.线程切换

我们接下来继续分析一下,为什么使用异步消息处理的方式就可以对UI进行操作了呢?这是由于Handler总是依附于创建时所在的线程,比如我们的Handler是在主线程中创建的,而在子线程中又无法直接对UI进行操作,于是我们就通过一系列的发送消息、入队、出队等环节,最后调用到了Handler的handleMessage()方法中,这时的handleMessage()方法已经是在主线程中运行的,因而我们当然可以在这里进行UI操作了。整个异步消息处理流程的示意图如下图所示:
在这里插入图片描述
另外除了发送消息之外,我们还有以下几种方法可以在子线程中进行UI操作:

  1. Handler的post()方法
  2. View的post()方法
  3. Activity的runOnUiThread()方法
    我们先来看下Handler中的post()方法,代码如下所示:
public final boolean post(Runnable r)
{
   return  sendMessageDelayed(getPostMessage(r), 0);
}

原来这里还是调用了sendMessageDelayed()方法去发送一条消息啊,并且还使用了getPostMessage()方法将Runnable对象转换成了一条消息,我们来看下这个方法的源码:

private final Message getPostMessage(Runnable r) {
    Message m = Message.obtain();
    m.callback = r;
    return m;
}

在这个方法中将消息的callback字段的值指定为传入的Runnable对象。咦?这个callback字段看起来有些眼熟啊,喔!在Handler的dispatchMessage()方法中原来有做一个检查,如果Message的callback等于null才会去调用handleMessage()方法,否则就调用handleCallback()方法。那我们快来看下handleCallback()方法中的代码吧:

private final void handleCallback(Message message) {
    message.callback.run();
}

竟然就是直接调用了一开始传入的Runnable对象的run()方法。因此在子线程中通过Handler的post()方法进行UI操作就可以这么写:

public class MainActivity extends Activity {
 
	private Handler handler;
 
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		handler = new Handler();
		new Thread(new Runnable() {
			@Override
			public void run() {
				handler.post(new Runnable() {
					@Override
					public void run() {
						// 在这里进行UI操作
					}
				});
			}
		}).start();
	}
}

虽然写法上简洁很多,但是原理是完全一样的,我们在Runnable对象的run()方法里更新UI,效果完全等同于在handleMessage()方法中更新UI。

然后再来看一下View中的post()方法,代码如下所示:

public boolean post(Runnable action) {
    Handler handler;
    if (mAttachInfo != null) {
        handler = mAttachInfo.mHandler;
    } else {
        ViewRoot.getRunQueue().post(action);
        return true;
    }
    return handler.post(action);
}

原来就是调用了Handler中的post()方法。
最后再来看一下Activity中的runOnUiThread()方法,代码如下所示:

public final void runOnUiThread(Runnable action) {
    if (Thread.currentThread() != mUiThread) {
        mHandler.post(action);
    } else {
        action.run();
    }
}

如果当前的线程不等于UI线程(主线程),就去调用Handler的post()方法,否则就直接调用Runnable对象的run()方法。

通过以上所有源码的分析,我们已经发现了,不管是使用哪种方法在子线程中更新UI,其实背后的原理都是相同的,必须都要借助异步消息处理的机制来实现。

二、相关内容

1.Handler原理

Handler发送消息后,通过MessageQue将Message入队存放,再通过Looper开启循环取出消息,取出消息后再调用Handler分发和处理消息。
sendMessage–》enqueueMessage–》Looper.loop–》dispatchMessage–》handleMessage

2.epoll机制

2.1什么是epoll机制?

epoll是linux系统一种高效的IO多路复用机制,采用事件驱动的方式实现。可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质同步I/O,即读写是阻塞的。
在linux 没有实现epoll事件驱动机制之前,我们一般选择用select或者poll等IO多路复用的方法来实现并发服务程序。在linux新的内核中,有了一种替换它的机制,就是epoll。

2.2 select/poll/epoll

在 select/poll中,通过遍历文件描述符,而epoll则是通过监听回调的的机制。

2.3 Handler中epoll是如何实现消息队列的阻塞和唤醒?

阻塞:通过调用 epoll_wait 方法,当前线程进入休眠,等待被唤醒。
唤醒:对 mWakeEventFd 发起写入操作,从而唤醒 nativePollOnce 中通过 epoll_wait 进入休眠的线程。
参考:Handler 中的 epoll 源码分析:https://jishuin.proginn.com/p/763bfbd35a2d

epoll_wait这里也是整个Android消息机制阻塞的真正位置,阻塞等待期间可以保证线程进入休眠状态,不占用CPU资源,同时监听所注册的事件。

2.4 epoll高效的本质:

1.减少用户态和内核态之间的文件句柄拷贝;
2.减少对可读可写文件句柄的遍历。
具体实现是:通过红黑树和双链表数据结构,并结合回调机制,造就了epoll的高效。

2.5 epoll扩展内容

IO多路复用:
IO 多路复用是一种同步 IO 模型,实现一个线程可以监视多个文件句柄。一旦某个文件句柄就绪,就能够通知应用程序进行相应的读写操作,没有文件句柄就绪时会阻塞应用程序,交出 cpu。

与多进程和多线程技术相比,IO 多路复用技术的最大优势是系统开销小,系统不必为每个 IO 操作都创建进程或线程,也不必维护这些进程或线程,从而大大减小了系统的开销。

epoll优势:
1.监视的描述符数量不受限制,所支持的FD上限是最大可以打开文件的数目,具体数目可以cat /proc/sys/fs/file-max查看,一般来说这个数目和系统内存关系很大,以3G的手机来说这个值为20-30万。
2.IO性能不会随着监视fd的数量增长而下降。epoll不同于select和poll轮询的方式,而是通过每个fd定义的回调函数来实现的,只有就绪的fd才会执行回调函数。

ps:如果没有大量的空闲或者死亡连接,epoll的效率并不会比select/poll高很多。但当遇到大量的空闲连接的场景下,epoll的效率大大高于select/poll。

select、poll、epoll三种多路复用机制适用场景?
Q: 设想一下如下场景:有100万个客户端同时与一个服务器进程保持着TCP连接。而每一时刻,通常只有几百上千个TCP连接是活跃的(事实上大部分场景都是这种情况)。如何实现这样的高并发?
在select/poll时代,服务器进程每次都把这100万个连接告诉操作系统(从用户态复制句柄数据结构到内核态),让操作系统内核去查询这些套接字上是否有事件发生,轮询完后,再将句柄数据复制到用户态,让服务器应用程序轮询处理已发生的网络事件,这一过程资源消耗较大,因此,select/poll一般只能处理几千的并发连接。
epoll:要实现上面说是的场景,只需要在进程启动时建立一个epoll对象,然后在需要的时候向这个epoll对象中添加或者删除连接。同时,epoll_wait的效率也非常高,因为调用epoll_wait时,并没有一股脑的向操作系统复制这100万个连接的句柄数据,内核也不需要去遍历全部的连接。

epoll高效处理事件:
epoll通过红黑树和双链表数据结构,并结合回调机制,实现高效地处理事件:
检测到有事件发生时,
发生的事件都会挂载在红黑树中,重复添加的事件就可以通过红黑树高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度)。
另外,添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当相应的事件发生时会调用这个回调方法。发生的事件添加到rdlist双链表中。

epoll机制工作模式,及高效的原因?
epoll高效的原因:
当调用 epoll_wait检查是否有发生事件的连接时,只是检查 eventpoll对象中的 rdllist双向链表是否有 epitem元素而已,如果 rdllist链表不为空,则把这里的事件复制到用户态内存中,同时将事件数量返回给用户。因此,epoll_wait的效率非常高。epoll_ctl在向 epoll对象中添加、修改、删除事件时,从 rbr红黑树中查找事件也非常快,也就是说,epoll是非常高效的,它可以轻易地处理百万级别的并发连接。

eventfd文件描述符:
eventfd 是 Linux 系统中一个用来通知事件的文件描述符,基于内核向用户空间应用发送通知的机制,可以有效地被用来实现用户空间事件驱动的应用程序。

简而言之:eventfd 就是用来触发事件通知,它只有一个系统调用接口:

int eventfd(unsigned int initval, int flags);

表示打开一个 eventfd 文件并返回文件描述符,支持 epoll/poll/select 操作。

之所以要了解 eventfd,是因为在 Android 6.0 后,Handler 底层替换为 eventfd/epoll 实现。而 6.0 之前是由 pipe/epoll 实现的。

Android中Handler为什么使用epoll?根本原因是什么?
一种说法是:当通过 postDelayed 发送延时消息后,会调用 epoll_wait 的 timeout 进行等待,利用了 epoll_wait 函数的挂起,达到了延时的目的,时间到后阻塞结束,epoll_wait 返回,线程被重新唤醒并获得CPU资源。

第二种说法是:为了省电,虽然 Looper 无限循环,但是没活干了就释放 CPU 资源进入休眠状态,并不会消耗大量资源。应该不是根本原因,使用 Java wait / notify 也可以达到目的(wait释放锁,sleep不释放锁)。

还有一种说法是:为了可以同时处理 native 侧消息。

到底根本原因是什么呢?
我认为是为了处理native侧的消息。
更详细一点,为了处理键盘鼠标轨迹球等输入设备的消息。Android基于linux内核,是以事件为驱动的,将各种输入设备被抽象成了文件,监控这些文件描述符的I/O事件正是epoll的拿手好戏。

epoll由来?
其实研究到最后,我才悟出了为什么epoll要叫epoll,因为它和原来poll机制的最大区别就是改善成了event(事件)驱动,这个e如果我没猜错的话应该代表的就是event。
关于Linux系统调用为什么可以挂起且不占用CPU时间片,CPU的定时器和中断又是怎么实现的?那可能就涉及到硬件知识了。

3.ThreadLocal

ThreadLocal是什么?
ThreadLocal起作用的根源是Thread类:每个线程都有一个threadLocals 属性,它是一个ThreadLocal.ThreadLocalMap 类型的Map集合。而ThreadLocal是维护ThreadLocal.ThreadLocalMap这个属性的一个工具类。
ThreadLocal具体实现:
当变量被访问时,ThreadLocal在访问变量的每个线程中都创建了一个副本变量(即每个线程的threadLocals属性),每个线程可以访问自己内部的副本变量,且该副本只能由当前 Thread 使用,从而实现线程间的数据隔离。
ThreadLocal本质是采用空间换时间的方式,避免多线程资源抢占的耗时。

ThreadLocal原理:
ThreadLocalMap 类相当于一个Map,key 是 ThreadLocal 本身,value 就是我们的值。
当我们通过 threadLocal.set(new Integer(123)); ,我们就会在这个线程中的 threadLocals 属性中放入一个键值对,key 是这个threadLocal.set(new Integer(123)) 的 threadlocal,value就是值new Integer(123)。
当我们通过 threadlocal.get() 方法的时候,首先会根据这个线程得到这个线程的 threadLocals 属性,然后由于这个属性放的是键值对,我们就可以根据键 threadlocal 拿到值。

ThreadLocal在Handler中作用:
利用ThreadLocal将Looper对象与当前线程进行关联(把Looper存到Thread对象map中), 实现Looper–Thread一一对应。
保证一个线程的Looper唯一,存到线程
Looper的生命周期,是当前线程的生命周期
一个线程只有一个Looper,可以有多个Handler。

4.异步消息与消息屏障

什么是消息屏障?
当要处理的消息为消息屏障,则延迟执行后续的普通消息,优先执行异步消息,常用在绘制任务的处理。消息屏障使得消息处理有了优先级。
有些博文说它是同步屏障,其实两者是同一个东西。

屏障消息如何插入消息队列?
同步屏障是通过MessageQueue的postSyncBarrier方法插入到消息队列的。从这个方法可以知道:
屏障消息和普通消息的区别在于屏障没有tartget,普通消息有target是因为它需要将消息分发给对应的target,而屏障不需要被分发,它就是用来挡住普通消息来保证异步消息优先处理的。
屏障和普通消息一样可以根据时间来插入到消息队列中的适当位置,并且只会挡住它后面的同步消息的分发。
postSyncBarrier返回一个int类型的数值,通过这个数值可以撤销屏障。
postSyncBarrier方法是私有的,如果我们想调用它就得使用反射。
插入普通消息会唤醒消息队列,但是插入屏障不会。

为什么要有消息屏障?
在Android系统中存在一个VSync垂直同步信号,它主要负责每16ms更新一次屏幕展示,如果用户同步消息在16ms内没有执行完成,那么VSync消息的更新操作就无法执行在用户看来就出现了掉帧或卡顿的情况,为此Android开发要求每个消息的执行需要限制在16ms之内完成。
但是消息队列中可能会包含多个同步消息,假如当前主线程消息队列有10个同步消息,每个同步消息要执行10ms,总共也就需要执行100ms,这段时间内就会有近7帧无法正常刷新展示,应用执行过程中遇到这种情况还是很普遍的。

Android系统设计时自然也会考虑到这种情况,同步消息会导致延迟主要原因在于排队等候,如果消息发送后不必排队等待直接就执行就能够解决消息延迟问题。Android系统中的异步消息就是专门解决消息处理延迟的问题,它需要配合同步屏障(SyncBarrier)一起工作,在发送异步消息的时候向消息队列投放同步屏障对象,消息队列会返回同步屏障的token,此时消息队列中的同步消息都会被暂停处理,优先执行异步消息处理,等异步消息处理完成再通过消息队列移除token对应的同步屏障,消息队列继续之前暂停的同步消息处理。

主线程发送异步消息及发送消息屏障流程?
源码分析:
(1)ViewRootImpl发消息屏障到主线程消息队列,Choreographer把绘制任务放入队列,并申请VSync信号;
(2)Choreographer的内部类FrameDisplayEventReceiver提供接收VSync信号以及申请VSync信号的方法;
(3)当FrameDisplayEventReceiver接收到VSync信号,往主线程发送异步消息,消息任务就是自己本身;
(4)MessageQueue取到异步消息进行处理;
(5)FrameDisplayEventReceiver.run方法被执行,遍历绘制任务队列,以此执行;
(6)最后移除消息屏障;
(7)VSync信号是SurfaceFlinger实现并定时发送;

详细可参考:https://www.jianshu.com/p/c1f6e1181e8e

补充:Choreographer

该类中文名字编舞者,它负责通过内部类的父类DisplayEventReceiver.scheduleVsync注册VSync信号,通过内部类FrameDisplayEventReceiver.onVsync接收信号。

5.消息池Message.obtain()

5.内存泄漏

Q:既然Handler容易引发内存泄漏,Google为什么不对此进行改进?
通过target将Handler存入Message,是为了解决在多个Hander的情况无法找到处理当前消息的Handler问题。实际上是一种架构设计上的妥协。

三、常见问题

Q1:Looper死循环为什么不会导致线程卡死?子线程中维护Looper在消息队列无消息的时候处理方案是怎么样的?

1、主线程本身就是需要一直运行的,因为要处理各个View,界面变化。所以需要这个死循环来保证主线程一直执行下去,不会被退出。
2、真正会卡死的操作是在某个消息处理的时候操作时间过长,导致掉帧、ANR,而不是loop方法本身。
3、在主线程以外,会有其他的线程来处理接受其他进程的事件,比如Binder线程(ApplicationThread),会接受AMS发送来的事件
4、在收到跨进程消息后,会交给主线程的Hanlder再进行消息分发。所以Activity的生命周期都是依靠主线程的Looper.loop,当收到不同Message时则采用相应措施,比如收到msg=H.LAUNCH_ACTIVITY,则调用ActivityThread.handleLaunchActivity()方法,最终执行到onCreate方法。
5、当没有消息的时候,会阻塞在loop的queue.next()中的nativePollOnce()方法里,此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生。所以死循环也不会特别消耗CPU资源。

当消息不可用或者没有消息的时候就会阻塞在next方法,而阻塞的办法是通过pipe/epoll机制。

Q2:描述下什么是epoll机制?

epoll机制是一种IO多路复用的机制,具体逻辑就是一个进程可以监视多个描述符,当某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作,这个读写操作是阻塞的。在Android中,会创建一个Linux管道(Pipe)来处理阻塞和唤醒。

  • 当消息队列为空,管道的读端等待管道中有新内容可读,就会通过epoll机制进入阻塞状态。
  • 当有消息要处理,就会通过管道的写端写入内容,唤醒主线程。

Q3:关于ThreadLocal,你是怎么理解的?

在每个线程中都有一个threadLocals变量,这个变量存储着ThreadLocal和对应的需要保存的对象。

这样带来的好处就是,在不同的线程,访问同一个ThreadLocal对象,但是能获取到的值却不一样。

其实就是其内部获取到的Map不同,Map和Thread绑定,所以虽然访问的是同一个ThreadLocal对象,但是访问的Map却不是同一个,所以取得值也不一样。

好处:线程之间进行隔离。

Q4:Message有哪几种创建方式?哪种效果更好?为什么?

new Message和message.obtain方式,message.obtain方式更好,内部消息池会进行复用,避免创建过多对象。

Q5:为什么不能在子线程更新UI?

因为Android中的UI控件不是线程安全的,如果多线程访问UI控件那还不乱套了。

那为什么不加锁呢?

会降低UI访问的效率。本身UI控件就是离用户比较近的一个组件,加锁之后自然会发生阻塞,那么UI访问的效率会降低,最终反应到用户端就是这个手机有点卡。
太复杂了。本身UI访问时一个比较简单的操作逻辑,直接创建UI,修改UI即可。如果加锁之后就让这个UI访问的逻辑变得很复杂,没必要。
所以,Android设计出了 单线程模型 来处理UI操作,再搭配上Handler,是一个比较合适的解决方案。

子线程访问UI的 崩溃原因 和 解决办法?
崩溃发生在ViewRootImpl类的checkThread方法中,其实就是判断了当前线程 是否是 ViewRootImpl创建时候的线程,如果不是,就会崩溃。

而ViewRootImpl创建的时机就是界面被绘制的时候,也就是onResume之后,所以如果在子线程进行UI更新,就会发现当前线程(子线程)和View创建的线程(主线程)不是同一个线程,发生崩溃。

解决办法有三种:

在新建视图的线程进行这个视图的UI更新,主线程创建View,主线程更新View。
在ViewRootImpl创建之前进行子线程的UI更新,比如onCreate方法中进行子线程更新UI。
子线程切换到主线程进行UI更新,比如Handler、view.post方法。

Q6:为什么主线程可以直接new Handler?其它子线程可以吗?怎么做?

可以,但要调用Looper.prepare()方法;主线程启动时候就创建了MainLooper,构造方法中默认调用了Looper.prepare()方法。

Q7:一个线程有几个Looper?如何保证?又可以有几个Handler?

一个,通过ThreadLocal中的ThreadLocal存储当前线程ID和Looper对象保证。可以有多个Handler。

Looper、MessageQueue、线程是一一对应关系,而他们与Handler是可以一对多的。

Q8:Handler内存泄漏的原因,其它内部类为什么没有这个问题?

message.target持有Handler引用;内存泄漏是因为长生命周期对象持有了短生命周期对象的引用,Handler内存泄漏,内部类持有外部类的引用只是表象,源头是由于message.target持有Handler引用,导致Thread持有了Activity的引用,Activity无法及时回收。其它内部类虽然持有了外部类的引用,但只要它的实例持有对象的生命周期不大于外部类对象,就不会造成内存泄漏。

Q9:Handler中生产者-消费者设计模式你理解不?

Handler机制就是一个生产者消费者模式。可以这么理解,Handler发送消息,它就是生产者,生产的是一个个Message。Looper可以理解为消费者,在其loop()方法中,死循环从MessageQueue取出Message进行处理。而MessageQueue就是缓冲区了,Handler产生的Message放到MessageQueue中,Looper从MessageQueue取出消息。

Q10:既然存在多个Handler往MessageQue中添加数据(发消息时各个Handler处于不同线程),内部如何保证安全?

Looper创建时,会创建一个MessageQueue,且是唯一对应的
这也就说明一个Thread,Looper,MessageQueue都是唯一对应的关系
在这里插入图片描述
那么在添加消息时,synchronized (this) 的this 就是MessageQueue,而根据对应关系,这里加锁,其实就等于锁住了当前线程。就一个线程内算多个Handler同时添加消息,他们也会被锁限制,从而保证了消息添加的有序性,取消息同理
在这里插入图片描述

多个Handler发送消息;Looper怎么知道要把消息给哪个Handler处理?

target标志
在Handler发送消息的时候,会把当前Handler保存到Message的target变量里,当处理消息的时候,就可以直接取出Handler并处理消息;

Q11:使用Handler的postDelay()后消息队列会发生什么变化?

无论是即时消息还是延迟消息,都是计算出具体的时间,然后作为消息的when字段进程赋值。

然后在MessageQueue中找到合适的位置(安排when小到大排列),并将消息插入到MessageQueue中。

这样,MessageQueue就是一个按照消息时间排列的一个链表结构。

参考

https://blog.csdn.net/guolin_blog/article/details/9991569 郭霖,Android异步消息处理机制完全解析
https://www.cnblogs.com/lao-liang/p/5073257.html 《深入理解Android内核设计思想》
https://www.jianshu.com/p/c1f6e1181e8e 编舞者Choreographer,绘制任务处理流程
https://jishuin.proginn.com/p/763bfbd35a2d Handler 中的 epoll 源码分析
常见Handler问题

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

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

相关文章

誉天在线项目~ElementPlus Tag标签用法

效果图 页面展现 <el-form-item label"课程标签"><el-tagv-for"tag in dynamicTags":key"tag"class"mx-1"closable:disable-transitions"false"close"handleClose(tag)"style"margin:5px;">…

解决Custom EmptyStringException: The string is empty

解决Custom EmptyStringException: The string is empty 解决Custom EmptyStringException: The string is empty摘要引言正文1. 理解异常的根本原因2. 处理空字符串问题3. 用户输入验证4. 异常处理 总结参考资料 博主 默语带您 Go to New World. ✍ 个人主页—— 默语 的博客&…

gitlab runner 不清理云端已经删除的tag和branch问题记录

在使用gitlab runner的过程中&#xff0c;发现由于git存储限制&#xff0c;不允许在a/b分支后创建a/b/c的分支。所以移除云端a/b分支后创建a/b/c分支。 但是由于gitlab runner出于拉取代码的速度考虑&#xff0c;将本地代码路径做了持久化缓存。 在上述场景中&#xff0c;新建…

Layui + Flask | 实现注册、登录功能(案例篇)(08)

此案例内容比较多,建议滑到最后点击阅读原文,阅读体验更佳。后续也会录制案例视频,将在本周内上传到同名的 b 站账号。 已经看了 layui 表单相关的知识,接下来就可以实现注册功能,功能逻辑如下: 项目创建 新建 flask 项目下载 layui 文件,解压之后复制到指定文件编写前…

算法--选择排序

算法步骤 /*** 选择排序** version 1.0* date 2023/09/01 17:57:05*/ public class Select {/*** 升序选择排序** param a 待排序的数组* date 2023/9/1 15:29:10*/public static void sortAes(int[] a) {//数组长度int length a.length;for (int i 0; i < length-2; i) {…

Web应用开发 - 实训三 B Servlet基础

Web应用开发 - 实训三 B Servlet基础 前言&#xff1a; 零、前期准备准备工具创建项目导入 jar 包配置运行设置 一、实训第一部分第一张图第二张图第三张图 二、实训第二部分第一张图第二张图 前言&#xff1a; eclipse 是不可能用的&#xff0c;并不是说它界面丑&#xff0c;…

Navicat导入Excel数据顺序变了

项目场景&#xff1a; Navicat导入Excel数据 问题描述 从Excel表格中导入数据到数据库中。但是&#xff0c;在导入的过程中&#xff0c;我们常会发现数据顺序出现了问题&#xff0c;导致数据错位&#xff0c;给数据的处理带来了极大的麻烦。 原因分析&#xff1a; 这个问题的…

【CVPR2020】DEF:Seeing Through Fog Without Seeing Fog论文阅读分析与总结

Challenge&#xff1a; 之前网络架构的设计假设数据流是一致的&#xff0c;即出现在一个模态中的对象也出现在另一个模态中。然而&#xff0c;在恶劣的天气条件下&#xff0c;如雾、雨、雪或极端照明条件&#xff0c;多模态传感器配置中的信息可能不对称。不同传感器在特征提取…

第六章 图 八、有向无环图的描述表达式

一、定义 有向无环图: 若一个有向图中不存在环&#xff0c;则称为有向无环图&#xff0c;简称DAG图(Directed Acyclic Graph) 解题方法:

如何将安防视频监控系统/视频云存储EasyCVR平台推流到公网直播间?

视频云存储/安防监控EasyCVR视频汇聚平台基于云边端智能协同&#xff0c;支持海量视频的轻量化接入与汇聚、转码与处理、全网智能分发、视频集中存储等。音视频流媒体视频平台EasyCVR拓展性强&#xff0c;视频能力丰富&#xff0c;具体可实现视频监控直播、视频轮播、视频录像、…

C++ QT qml 学习之 做个登录界面

最近在学习QT&#xff0c;也初探到qml 做ui 的灵活性与强大&#xff0c;于是手痒痒&#xff0c;做个demo 记录下学习成果 主要内容是如何自己编写一个按钮以及qml多窗口。 参考WX桌面版&#xff0c;做一个登录界面&#xff0c;这里面按钮是写的一个组合控件&#xff0c;有 按…

HTTP协议的基本概念与理解!

一、什么是HTTP协议 HTTP&#xff08;超文本传输协议&#xff09;是一个基于请求与响应&#xff0c;无状态的&#xff0c;应用层的协议&#xff0c;常基于TCP/IP协议传输数据&#xff0c;互联网上应用最为广泛的一种网络协议,所有的WWW文件都必须遵守这个标准。设计HTTP的初衷…

Fedora CoreOS 安装部署详解

《OpenShift 4.x HOL教程汇总》 Fedora CoreOS 的裸机安装方法_fedora coreos 安装-CSDN博客 OpenShift 4 - Fedora CoreOS (1) - 最简安装_fedora core 安装_dawnsky.liu的博客-CSDN博客 OpenShift 和 CoreOS 我们知道 Red Hat Enterprise Linux CoreOS&#xff08;简称RHCOS&…

SP2-1503|0152:CMD窗口的SQLPLUS命令无法登录Oracle

场景还原 今天有小伙伴把Oracle卸载后重新安装&#xff0c;尝试以下三种方案均无法登录数据库 1.、在使用PLSQL Developer时&#xff0c;输入账号密码机械能登录操作&#xff0c;弹出空白弹框界面 即没有任何提示错误代码 只有一个白板的框 2、利用自身的SQL PLUS登录直接窗…

【SpringBoot集成Redis + Session持久化存储到Redis】

目录 SpringBoot集成Redis 1.添加 redis 依赖 2.配置 redis 3.手动操作 redis Session持久化存储到Redis 1.添加依赖 2.修改redis配置 3.存储和读取String类型的代码 4.存储和读取对象类型的代码 5.序列化细节 SpringBoot集成Redis 1.添加 redis 依赖 …

运算放大器学习笔记

目录 一、基本定理二、基本定义三、负反馈电路四、同向放大电路五、反向放大电路六、差分放大电路 一、基本定理 【电路示意图】 开环放大公式 VOAvo(V-V-) 开环放大倍数&#xff08;增益&#xff09;非常大&#xff0c;105 或 106 输入阻抗超级大&#xff08;可以理解为电…

辅助驾驶功能开发-控制篇(03)-基于PID的请求角度转扭矩算法

1 文档概述 本文档主要描述请求角度转扭矩的功能、性能要求、算法程序设计,后续可作为程序编程和功能及性能测试参考文档。 2 功能要求 转角扭矩管理(SteeringTorqueManager)将方向盘请求转角转换为电机叠加扭矩,进行横向路径跟踪,并与驾驶员方向盘手感交互,实现自适应调整…

2023年五一杯数学建模B题快递需求分析问题求解全过程论文及程序

2023年五一杯数学建模 B题 快递需求分析问题 原题再现&#xff1a; 网络购物作为一种重要的消费方式&#xff0c;带动着快递服务需求飞速增长&#xff0c;为我国经济发展做出了重要贡献。准确地预测快递运输需求数量对于快递公司布局仓库站点、节约存储成本、规划运输线路等具…

红魔8SPro强解BL+完美ROOT权限-刷MIUI14系统-修复指纹丢失/内存等问题

早前我们刷过红魔8pro手机&#xff0c;该手机支持解锁BL刷入MIU14系统&#xff0c;红魔8Pro由于官方并没有修改解锁BL指令&#xff0c;所以我们的解锁BL非常简单&#xff0c;只需要在fastboot下一键完成。随着红魔8SPro的上架&#xff0c;红魔UI6.0的发布&#xff0c;官方解锁指…

【数据仓库设计基础1】关系数据模型理论与数据仓库Inmon方法论

文章目录 一. 关系数据模型中的结构1&#xff0e;关系2&#xff0e;属性3&#xff0e;属性域4&#xff0e;元组5. 关系数据库6&#xff0e;关系表的属性7&#xff0e;关系数据模型中的键 二. 关系完整性1&#xff0e;空值&#xff08;NULL&#xff09;2&#xff0e;关系完整性规…