你真的了解Handler吗?
Handler
,一个面试中常问的高频词汇。
大家想想这个知识点一般是怎么考察的?请解释一下Handler的原理?
不不不,这个问题已经烂大街了,我要是面试官,我会这么问。
我们知道在Handler中,存在一个方法叫 sendMessageDelay , 作用是延时发送消息,请解释一下Handler是如何实现延时发送消息的?
Looper.loop是一个死循环,拿不到需要处理的Message就会阻塞,那在UI线程中为什么不会导致ANR?
也请各位读者先自己思考一下这两个问题,换做是你该怎么回答。
Handler
我们先从Handler的定义来认识它,先上谷歌原文:
/**
* A Handler allows you to send and process {@link Message} and Runnable
*/
下面由我这枚英语渣上线,强行翻译一波。
Handler
是用来结合线程的消息队列来发送、处理Message对象
和Runnable对象
的工具。每一个Handler实例化之后会关联一个线程和该线程的消息队列。当你创建一个Handler的时候,它就会自动绑定到到所在的线程或线程的消息队列,并陆续把Message/Runnable分发到消息队列,然后在它们出队的时候去执行。Handler
主要有两个用途: (1) 调度在将来某个时候执行的Message
和Runnable
。(2)把需要在另一个线程执行的操作加入到消息队列中去。当
post runnable
或send message
到handler
时,您可以在消息队列准备就绪后立即处理该事务。也可以延迟一段时间执行,或者指定某个特定时间去执行。
我们先从Handler的构造方法来认识一下它:
public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async)
Handler的构造方法有很多个,但最终调用的就是上述构造方法。
老规矩,先上官方解释,再上学渣翻译。
* Use the provided {@link Looper} instead of the default one and take a callback
* interface in which to handle messages. Also set whether the handler
* should be asynchronous.
*
* Handlers are synchronous by default unless this constructor is used to make
* one that is strictly asynchronous.
*
* Asynchronous messages represent interrupts or events that do not require global ordering
* with respect to synchronous messages. Asynchronous messages are not subject to
* the synchronization barriers introduced by conditions such as display vsync.
使用提供的
Looper
而不是默认的Looper
,并使用回调接口来处理消息。还设置处理程序是否应该是异步的。默认情况下,
Handler
是同步的,除非此构造函数用于生成严格异步的Handler
。异步消息指的是不需要进行全局排序的中断或事件。异步消息不受同步障碍(比如display vsync)的影响。
Handler中的方法主要分为以下两类:
获取及查询消息,比如
obtainMessage(int what)
,hasMessages(int what)
。将message或runnable添加/移出消息队列,比如
postAtTime(@NonNull Runnable r, long uptimeMillis)
,sendEmptyMessageDelayed(int what, long delayMillis)
。
在这些方法中,我们重点需要关注一下enqueueMessage
这个方法。
为什么呢?
无论是 postAtTime
、sendMessageDelayed
还是其他的post、send方法,它们最终都会调到enqueueMessage
这个方法里去。
比如:
public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
if (delayMillis < 0) {
delayMillis = 0;
}
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
可以看到,sendMessageDelayed方法里将延迟时间转换为消息触发的绝对时间,最终调用的是sendMessageAtTime方法。
public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
MessageQueue queue = mQueue;
if (queue == null) {
RuntimeException e = new RuntimeException(
this + " sendMessageAtTime() called with no mQueue");
Log.w("Looper", e.getMessage(), e);
return false;
}
return enqueueMessage(queue, msg, uptimeMillis);
}
而sendMessageAtTime方法调用了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);
}
enqueueMessage
方法直接将message交给了MessageQueue
去执行。
Message
在分析MessageQueue
之前,我们应该先来认识一下Message
这个消息载体类。
老规矩,先从定义看起:
* Defines a message containing a description and arbitrary data object that can be
* sent to a {@link Handler}. This object contains two extra int fields and an
* extra object field that allow you to not do allocations in many cases.
*
* <p>While the constructor of Message is public, the best way to get
* one of these is to call {@link #obtain Message.obtain()} or one of the
* {@link Handler#obtainMessage Handler.obtainMessage()} methods, which will pull
* them from a pool of recycled objects.</p>
下面是渣翻译:
定义一条包含描述和任意数据对象的消息,该对象可以发送到
Handler
。此对象包含两个额外的int字段和一个额外的object字段。尽管
Message
的构造方法是public,但获取一个Message的最好的方法是调用Message.obtain
或者Handler.obtainMessage
方法,这些方法会从可回收的线程池中获取Message对象。
我们来认识一下Message里的字段:
public final class Message implements Parcelable {
}
在Message中,我们需要关注一下Message的回收机制。
先来看下recyclerUnchecked
方法:
void recycleUnchecked() {
// Mark the message as in use while it remains in the recycled object pool.
// Clear out all other details.
flags = FLAG_IN_USE;
what = 0;
arg1 = 0;
arg2 = 0;
obj = null;
replyTo = null;
sendingUid = UID_NONE;
workSourceUid = UID_NONE;
when = 0;
target = null;
callback = null;
data = null;
synchronized (sPoolSync) {
if (sPoolSize < MAX_POOL_SIZE) {
next = sPool;
sPool = this;
sPoolSize++;
}
}
}
在这个方法中,有三个关键变量。
- sPoolSync :主要是给Message加一个对象锁,不允许多个线程同时访问Message类和recycleUnchecked方法。
- sPool:存储我们循环利用Message的单链表。这里sPool只是链表的头节点。
- sPoolSize:单链表的链表的长度,即存储的Message对象的个数。
当我们调用recycleUnchecked方法时,首先会将当前Message对象的属性清空。然后判断Message是否已到达缓存的上限(50个),如果没有,将当前的Message对象置于链表的头部。
那么取缓存的操作呢?
我们来看下obtain
方法:
public static Message obtain() {
synchronized (sPoolSync) {
if (sPool != null) {
Message m = sPool;
sPool = m.next;
m.next = null;
m.flags = 0; // clear in-use flag
sPoolSize--;
return m;
}
}
return new Message();
}
可以看出,Message会尝试取出sPool链表的第一个元素,并将sPool的头元素往后移动一位。如果sPool链表为空,将会返回一个新的Message对象。
Message里提供obtain方法获取Message对象,使得Message到了重复的利用,减少了每次获取Message时去申请空间的时间。同时,这样也不会永无止境的去创建新对象,减小了Jvm垃圾回收的压力,提高了效率。
MessageQueue
MessageQueue用于保存由Looper
发送的消息的列表。消息不会直接添加到消息队列,而是通过Handler
对象中关联的Looper
里的MessageQueue完成添加的动作。
您可以使用Looper.myQueue()
检索当前线程的MessageQueue。
我们先来看看MessageQueue
如何实现添加一个Message的操作。
boolean enqueueMessage(Message msg, long when) {
//判断msg是否有target属性以及是否正在使用中
if (msg.target == null) {
throw new IllegalArgumentException("Message must have a target.");
}
if (msg.isInUse()) {
throw new IllegalStateException(msg + " This message is already in use.");
}
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;
}
// We can assume mPtr != 0 because mQuitting is false.
if (needWake) {
//唤醒消息
nativeWake(mPtr);
}
}
return true;
}
mMessages
是一个按照消息实际触发时间msg.when排序的链表,越往后的越晚触发。enqueueMessage
方法根据新插入消息的when,将msg插入到链表中合适的位置。如果是及时消息,还需要唤醒MessageQueue
。
我们接着来看看nativeWake
方法,nativeWake
方法的源码位于\frameworks\base\core\jni\android_os_MessageQueue.cpp
。
static void android_os_MessageQueue_nativeWake(JNIEnv* env, jclass clazz, jlong ptr) {
NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);
nativeMessageQueue->wake();
}
继续看NativeMessageQueue
里的wake函数。
void NativeMessageQueue::wake() {
mLooper->wake();
}
它又转交给了Looper(源码位置/system/core/libutils/Looper.cpp
)去处理。
void Looper::wake() {
#if DEBUG_POLL_AND_WAKE
ALOGD("%p ~ wake", this);
#endif
uint64_t inc = 1;
ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd.get(), &inc, sizeof(uint64_t)));
if (nWrite != sizeof(uint64_t)) {
if (errno != EAGAIN) {
LOG_ALWAYS_FATAL("Could not write wake signal to fd %d (returned %zd): %s",
mWakeEventFd.get(), nWrite, strerror(errno));
}
}
}
Looper
里的wake
函数很简单,它只是向mWakeEventFd
里写入了一个 1
值。
上述的mWakeEventFd
又是什么呢?
Looper::Looper(bool allowNonCallbacks)
: mAllowNonCallbacks(allowNonCallbacks),
mSendingMessage(false),
mPolling(false),
mEpollRebuildRequired(false),
mNextRequestSeq(0),
mResponseIndex(0),
mNextMessageUptime(LLONG_MAX) {
mWakeEventFd.reset(eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC));
...
}
从Looper的构造函数里可以找到答案,mWakeEventFd本质上是一个eventfd
。至于什么是eventfd
,这里只能说是eventfd
是Linux 2.6提供的一种系统调用,它可以用来实现事件通知,更具体的内容需要各位读者自行查阅资料了。
既然有发送端,那么必然有接收端。接收端在哪呢?
void Looper::awoken() {
#if DEBUG_POLL_AND_WAKE
ALOGD("%p ~ awoken", this);
#endif
uint64_t counter;
TEMP_FAILURE_RETRY(read(mWakeEventFd.get(), &counter, sizeof(uint64_t)));
}
可以看到,awoken
函数里的内容很简单,只是做了一个读取的动作,它并不关系读到的具体值是啥。为什么要这样设计呢,我们得结合awoken
函数在哪里调用去分析。
awoken
函数在Looper
的pollInner
函数里调用。pollInner
函数里有一条语句
int eventCount = epoll_wait(mEpollFd.get(), eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
它在这里起到阻塞的作用,如果没有调用nativeWake
函数,epoll_wait
将一直等待写入事件,直到超时为止。
如此,便回到我们文章一开始提出的问题了。
Looper.loop是一个死循环,拿不到需要处理的Message就会阻塞,那在UI线程中为什么不会导致ANR?
首先,我们需要明确一点,Handler中到底有没有阻塞?
答案是有!!!那它为什么不会导致ANR呢?
这得从ANR产生的原理说起。
ANR的本质也是一个Message,这一点很关键。我们拿前台服务的创建来举例,前台服务创建时,会发送一个 what值为ActivityManagerService.SERVICE_TIMEOUT_MSG
的延时20s的Message,如果Service的创建 工作在上述消息的延时时间内完成,则会移除该消息,否则,在Handler正常收到这个消息后,就会进行服务超时处理,即弹出ANR对话框。
为什么不会ANR,现在各位读者清楚了吗?ANR消息本身就是通过Handler去派发的,Handler阻塞与否与ANR并没有必然关系。
我们看了MessageQueue
是如何加入一条消息的,接下来,我们来看看它是如何取出一条消息的。
Message next() {
//如果消息循环已退出并已被释放,则return
//如果应用程序在退出后尝试重新启动looper,则可能发生这种情况
final long ptr = mPtr;
if (ptr == 0) {
return null;
}
int pendingIdleHandlerCount = -1; // -1 only during first iteration
int nextPollTimeoutMillis = 0;
for (;;) {
if (nextPollTimeoutMillis != 0) {
//将当前线程中挂起的所有Binder命令刷新到内核驱动程序。
//在执行可能会阻塞很长时间的操作之前调用此函数非常有用,以确保已释放任何挂起的对象引用,
//从而防止进程保留对象的时间超过需要的时间。
Binder.flushPendingCommands();
}
// Run the idle handlers.
// We only ever reach this code block during the first iteration.
for (int i = 0; i < pendingIdleHandlerCount; i++) {
final IdleHandler idler = mPendingIdleHandlers[i];
mPendingIdleHandlers[i] = null; // release the reference to the handler
boolean keep = false;
try {
keep = idler.queueIdle();
} catch (Throwable t) {
Log.wtf(TAG, "IdleHandler threw exception", t);
}
if (!keep) {
synchronized (this) {
mIdleHandlers.remove(idler);
}
}
}
// Reset the idle handler count to 0 so we do not run them again.
pendingIdleHandlerCount = 0;
// While calling an idle handler, a new message could have been delivered
// so go back and look again for a pending message without waiting.
nextPollTimeoutMillis = 0;
}
}
next
方法里主要做了三件事,(1)使用nativePollOnce
阻塞指定时间,等待下一条消息的执行。 (2)获取下一条消息,并返回此消息。 (3)如果消息队列为空,则执行IdleHandler。
这里有个新名词IdleHandler
,IdleHandler
是可以在 Looper 事件循环的过程中,当出现空闲的时候,允许我们执行任务的一种机制。 MessageQueue
中提供了addIdleHandler
和removeIdleHandler
去添加删除IdleHandler
。
next
方法的第一行有个ptr
变量,这个ptr
变量是什么含义呢?
MessageQueue(boolean quitAllowed) {
mQuitAllowed = quitAllowed;
mPtr = nativeInit();
}
mPtr是一个long型变量,它是在MessageQueue
的构造方法中,通过nativeInit
方法初始化的。
static jlong android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz) {
NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue();
if (!nativeMessageQueue) {
jniThrowRuntimeException(env, "Unable to allocate native queue");
return 0;
}
nativeMessageQueue->incStrong(env);
return reinterpret_cast<jlong>(nativeMessageQueue);
}
可以看到,ptr的本质是对 jni层的NativeMessageQueue对象的指针的引用。
我们重点来看下nativePollOnce
方法,探寻一下Handler
中的阻塞机制。nativePollOnce
方法最终调用的是Looper.cpp
中的pollOnce
函数。
int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) {
int result = 0;
for (;;) { //一个死循环
while (mResponseIndex < mResponses.size()) {
const Response& response = mResponses.itemAt(mResponseIndex++);
int ident = response.request.ident;
if (ident >= 0) {
int fd = response.request.fd;
int events = response.events;
void* data = response.request.data;
#if DEBUG_POLL_AND_WAKE
ALOGD("%p ~ pollOnce - returning signalled identifier %d: "
"fd=%d, events=0x%x, data=%p",
this, ident, fd, events, data);
#endif
if (outFd != nullptr) *outFd = fd;
if (outEvents != nullptr) *outEvents = events;
if (outData != nullptr) *outData = data;
return ident;
}
}
if (result != 0) {
#if DEBUG_POLL_AND_WAKE
ALOGD("%p ~ pollOnce - returning result %d", this, result);
#endif
if (outFd != nullptr) *outFd = 0;
if (outEvents != nullptr) *outEvents = 0;
if (outData != nullptr) *outData = nullptr;
return result;
}
result = pollInner(timeoutMillis);
}
}
函数里有个关于mResponses
的while循环,我们从java层调用的暂时不用管它,它是ndk的handler处理逻辑。我们重点来看pollInner
函数。
int Looper::pollInner(int timeoutMillis) {
// 根据下一条消息的到期时间调整超时。
if (timeoutMillis != 0 && mNextMessageUptime != LLONG_MAX) {
nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
int messageTimeoutMillis = toMillisecondTimeoutDelay(now, mNextMessageUptime);
if (messageTimeoutMillis >= 0
&& (timeoutMillis < 0 || messageTimeoutMillis < timeoutMillis)) {
timeoutMillis = messageTimeoutMillis;
}
}
// 默认触发唤醒事件,POLL_WAKE == -1
int result = POLL_WAKE;
mResponses.clear();
mResponseIndex = 0;
// We are about to idle.
mPolling = true;
struct epoll_event eventItems[EPOLL_MAX_EVENTS];
//等待写入事件,写入事件由awoken函数触发。timeoutMillis为超时时间,0立即返回,-1一直等待
int eventCount = epoll_wait(mEpollFd.get(), eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
// No longer idling.
mPolling = false;
// Acquire lock.
mLock.lock();
...
// Check for poll error.
if (eventCount < 0) {
if (errno == EINTR) {
goto Done;
}
ALOGW("Poll failed with an unexpected error: %s", strerror(errno));
//POLL_ERROR == -4
result = POLL_ERROR;
goto Done;
}
// Check for poll timeout.
if (eventCount == 0) {
//POLL_TIMEOUT == -3,epoll超时会走此分支
result = POLL_TIMEOUT;
goto Done;
}
// Handle all events.
for (int i = 0; i < eventCount; i++) {
int fd = eventItems[i].data.fd;
uint32_t epollEvents = eventItems[i].events;
if (fd == mWakeEventFd.get()) {
if (epollEvents & EPOLLIN) {
//将eventfd里的数值取出,无实际含义,只是为了清空epoll事件和eventfd里的数据
awoken();
} else {
ALOGW("Ignoring unexpected epoll events 0x%x on wake event fd.", epollEvents);
}
} else {
//不会走到此分支,忽略它
ssize_t requestIndex = mRequests.indexOfKey(fd);
if (requestIndex >= 0) {
int events = 0;
if (epollEvents & EPOLLIN) events |= EVENT_INPUT;
if (epollEvents & EPOLLOUT) events |= EVENT_OUTPUT;
if (epollEvents & EPOLLERR) events |= EVENT_ERROR;
if (epollEvents & EPOLLHUP) events |= EVENT_HANGUP;
pushResponse(events, mRequests.valueAt(requestIndex));
} else {
ALOGW("Ignoring unexpected epoll events 0x%x on fd %d that is "
"no longer registered.", epollEvents, fd);
}
}
}
Done: ;
// 中间省略的代码不做探究,和ndk的handler实现有关
...
return result;
}
可以看到,pollInner
函数主要的逻辑是使用epoll_wait
去读取唤醒事件,它有一个最大的等待时长,其最大等待时长和下一条消息的触发时间有关。
需要注意一下pollInner
的返回值result,它有三种状态。进入方法默认为POLL_WAKE,表示触发唤醒事件。 接下来通过对epoll_wait
返回值的判断,它可能会变更为另两种状态。epoll_wait
返回值为0,表示epoll_wait
因超时而结束等待,result值设为POLL_TIMEOUT;epoll_wait
返回值为-1,表示epoll_wait
因系统中断等原因而结束等待,result值设为POLL_ERROR。但不管result值设为哪一个,都会导致pollOnce
退出死循环,然代码流程回到java层的next
方法中,去取得下一个Message对象。
因此,nativePollOnce
简单意义上的理解,它就是一个阻断器,可以将当前线程阻塞,直到超时或者因需立即执行的新消息入队才结束阻塞。
各位读者,看到这里,大家再回过头去想想文章的第一个问题该怎么回答吧。
Looper
Handler 机制中,我们还剩最后一个一个模块没有分析———— Looper。我们先从官方定义来看起:
* Class used to run a message loop for a thread. Threads by default do
* not have a message loop associated with them; to create one, call
* {@link #prepare} in the thread that is to run the loop, and then
* {@link #loop} to have it process messages until the loop is stopped.
概括一下:
Looper
是一个用于在线程中循环遍历消息的类。默认情况下,线程没有与之关联的消息循环;如果要创建一个,请在运行Looper的线程中调用Looper.prepare()
,然后使用Looper.loop()
让它处理消息直到循环停止。
上面的定义提到了两个比较关键的方法,我们一个一个来看。
Looper.prepare()
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));
}
prepare
的方法内容非常简单,创建一个Looper
对象,并把它放到sThreadLocal里,其中sThreadLocal是一个ThreadLocal
类。
ThreadLocal
类又是什么呢?
多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量,这样就不会存在线程不安全问题。
因此,使用ThreadLocal能够保证不同线程的Looper对象都有一个独立的副本,它们彼此独立,互不干扰。
Looper.looper()
public static void loop() {
//获取当前线程的Looper对象
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
//获取与Looper关联的messagequeue
final MessageQueue queue = me.mQueue;
// Make sure the identity of this thread is that of the local process,
// and keep track of what that identity token actually is.
Binder.clearCallingIdentity();
final long ident = Binder.clearCallingIdentity();
// Allow overriding a threshold with a system prop. e.g.
// adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'
final int thresholdOverride =
SystemProperties.getInt("log.looper."
+ Process.myUid() + "."
+ Thread.currentThread().getName()
+ ".slow", 0);
boolean slowDeliveryDetected = false;
for (;;) {
//进入死循环,不断去从MessageQueue中去拉取Message
Message msg = queue.next(); // next方法我们已经在MessageQueue中做了分析
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
// Make sure the observer won't change while processing a transaction.
final Observer observer = sObserver;
...
final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;
final long dispatchEnd;
Object token = null;
if (observer != null) {
token = observer.messageDispatchStarting();
}
long origWorkSource = ThreadLocalWorkSource.setUid(msg.workSourceUid);
try {
//注意这里,msg.target是一个handler对象,这个方法最终调用了handler的dispatchMessage
//去做消息分发
msg.target.dispatchMessage(msg);
if (observer != null) {
observer.messageDispatched(token, msg);
}
dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
} catch (Exception exception) {
if (observer != null) {
observer.dispatchingThrewException(token, msg, exception);
}
throw exception;
} finally {
ThreadLocalWorkSource.restore(origWorkSource);
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}
//回收Message,上文中有做过分析
msg.recycleUnchecked();
}
}
loop
方法主要的工作是:建立一个死循环,不断的通过调用MessageQueue
中的next
方法获取下一个消息,并最终通过取得的消息关联的handler去完成消息的分发。
总结
最后,我们再来理一理 Handler
、Message
、MessageQueue
、Looper
四者的关系和职责。
- Handler : 消息分发的管理者。负责获取消息、封装消息、派发消息以及处理消息。
- Message :消息的载体类。
- MessageQueue :消息的容器。负责按消息的触发时间对消息入队出队,以及在合适的时间唤醒或休眠消息队列。
- Looper : 消息分发的执行者。负责从消息队列中拉去消息并交给handler去执行。
为了更好的理解它们的关系,拿现实生活中的场景来举个例子:
Handler是快递员,负责收快递,取快递,查快递以及退回快递。
Message是快递包裹,message的target属性就是收件地址,而延时消息就是收件人预约了派送时间,
希望在指定的时间上门派送。
MessageQueue是菜鸟驿站,要对快递进行整理并摆放在合适的位置。
Looper是一个24小时不休息的资本家,他总是不停的在看菜鸟驿站有没有需要派送的快递,一有快递就立马取
出然后压榨快递员去派送。
最后,我们用一张四者之间的流程图来结束整篇文章: