热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

事件驱动模型Handler与Flutterfuture

业界相关资讯4月11日华为在P30的发布会上,华为消费者终端业务CEO余承东公布了方舟编译器,并宣布开源,称可提升app性能。表示开发者将开发好的APK用该编译器编译一下,即可大大

业界相关资讯

4月11日华为在P30的发布会上,华为消费者终端业务CEO余承东公布了方舟编译器,并宣布开源,称可提升app性能。表示开发者将开发好的APK用该编译器编译一下,即可大大提升App性能。从图中可以看出原理和Android系统的Ahead of Time与Just in Time类似。有网友猜想apk通过编译器会编译成机器码。让我们拭目以待吧。

《事件驱动模型 Handler 与 Flutter future》
《事件驱动模型 Handler 与 Flutter future》

事件驱动

《事件驱动模型 Handler 与 Flutter future》
《事件驱动模型 Handler 与 Flutter future》

以操作系统为例,我们每次的鼠标点击,键盘按下都会发出一个事件,然后加入操作系统的消息队列中,处理线程提取任务然后分发给对应的处理句柄去处理消息事件。上述的流程即可理解为事件驱动。下面是百度百科的解释

早期则存在许多非事件驱动的程序,这样的程序,在需要等待某个条件触发时,会不断地检查这个条件,直到条件满足,这是很浪费cpu时间的。而事件驱动的程序,则有机会释放cpu从而进入睡眠态(注意是有机会,当然程序也可自行决定不释放cpu),当事件触发时被操作系统唤醒,这样就能更加有效地使用cpu。

一个典型的事件驱动的程序,就是一个死循环,并以一个线程的形式存在,这个死循环包括两个部分,第一个部分是按照一定的条件接收并选择一个要处理的事件,第二个部分就是事件的处理过程。程序的执行过程就是选择事件和处理事件,而当没有任何事件触发时,程序会因查询事件队列失败而进入睡眠状态,从而释放cpu。

事件驱动的程序,必定会直接或者间接拥有一个事件队列,用于存储未能及时处理的事件

事件驱动的程序,还有一个最大的好处,就是可以按照一定的顺序处理队列中的事件,而这个顺序则是由事件的触发顺序决定的,这一特性往往被用于保证某些过程的原子化。

目前windows,linux等都是事件驱动的,只有一些单片机可能是非事件驱动的。

事件驱动模型 Handler

《事件驱动模型 Handler 与 Flutter future》
《事件驱动模型 Handler 与 Flutter future》

无论在Android开发还是Android面试中经常使用和被问到的就是handler,根据上述事件驱动的描述来看handler本质就是事件驱动模型。在Android系统中每次点击事件、activity与service的启动,生命周期的执行、view的布局事件等,Android系统均会把上述事件转化成一个消息msg,放在消息队列MessageQueen中。由每个App进程的主线程去不断的获取消息并分发给句柄Handler去处理。下面我们分析一下handler中的一些事件驱动的策略。

Androd中的几个使用场景

为了证实我们上面提出的观点,我们看看在源码里的体现,我们只看消息的接收处理的逻辑,因为Android是基于C/S架构,我们的事件产生都是经过server端产生然后通过binder通信传递给Client(App进程)。

1.activity与service的启动,生命周期的执行的消息处理(ActivityThread中的H类)

class H extends Handler {
.... 省略
public void handleMessage(Message msg) {
....省略
case EXIT_APPLICATION:
if (mInitialApplication != null) {
mInitialApplication.onTerminate();
}
Looper.myLooper().quit();
break;
case RECEIVER:
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "broadcastReceiveComp");
handleReceiver((ReceiverData)msg.obj);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
break;
case CREATE_SERVICE:
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, ("serviceCreate: " + String.valueOf(msg.obj)));
handleCreateService((CreateServiceData)msg.obj);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
break;
case BIND_SERVICE:
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "serviceBind");
handleBindService((BindServiceData)msg.obj);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
break;

2.Activity点击事件分发
当系统点击手机屏幕时,Linux内核会将硬件产生的触摸事件包装为Event存到/dev/input/event目录下,loop会通过epoll机制监听该事件,然后最终回调ViewRootImpl的WindowInputEventReceiver的方法,传递给对应的Activity去处理改点击事件

epoll机制

Linux本身的一个设计思想也是一切皆文件,epoll机制可以理解对文件亦或是流的监听,当该文件/流不可读(缓冲区取完),epoll机制会使线程进入休眠状态(epoll_wait),不浪费cpu资源。当文件/流可读(缓存区有数据),epoll机制会唤醒线程然后读取数据。

休眠阻塞

当Looper.loop()开始调用时,内部就开始死循环获取MessageQueue中的消息。如果当前时间段没有要执行的消息,如果还在不断的死循环进行消息的遍历,无疑是对CPU的浪费。所以在没有消息处理时,会使用epoll机制使当前线程进入休眠状态。

Message next() {
// Return here if the message loop has already quit and been disposed.
// This can happen if the application tries to restart a looper after quit
// which is not supported.
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.flushPendingCommands();
}

//关键代码
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
// Try to retrieve the next message. Return if found.
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
if (msg != null && msg.target == null) {
// Stalled by a barrier. Find the next asynchronous message in the queue.
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
if (msg != null) {
if (now // Next message is not ready. Set a timeout to wake up when it is ready.
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// Got a message.
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
return msg;
}
} else {
// No more messages.
nextPollTimeoutMillis = -1;
}

nativePollOnce(ptr, nextPollTimeoutMillis) 即为关键所在,该方法是个native方法,从其名字PollOnce表示轮询一次并看不出他有阻塞的含义,还有就是native内部有什么需要轮询呢?接下来我们看看native代码的实现

int Looper::pollInner(int timeoutMillis) {
...
int result = POLL_WAKE;
mResponses.clear();
mRespOnseIndex= 0;
mPolling = true; //即将处于idle状态
struct epoll_event eventItems[EPOLL_MAX_EVENTS]; //fd最大个数为16
//等待事件发生或者超时,在nativeWake()方法,向管道写端写入字符,则该方法会返回;
int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);

epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis) 可以看出会进入休眠状态,timeoutMillis值解释如下:
1.如果timeoutMillis =-1,一直阻塞不会超时。
2.如果timeoutMillis =0,不会阻塞,立即返回。
3.如果timeoutMillis>0,最长阻塞timeoutMillis毫秒(超时),如果期间有程序唤醒会立即返回。

唤醒

上面小节我们了解了Handler的休眠逻辑,那如何唤醒呢?无非两种情况
1.指定的timeoutMillis时间已到,可以理解为自己睡醒了
2.别人叫醒
我们可以猜测一下唤醒线程的执行时机实际就是新加入的事件消息是否需要马上执行。
我们来分析一下别人叫醒的地方。代码如下:

boolean enqueueMessage(Message msg, long when) {
····
msg.when = when;
Message p = mMessages;
boolean needWake;
if (p == null || when == 0 || 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 break;
}
if (needWake && p.isAsynchronous()) {
needWake = false;
}
}
msg.next = p; // invariant: p == prev.next
prev.next = msg;
}
// We can assume mPtr != 0 because mQuitting is false.
if (needWake) {
nativeWake(mPtr);
}

根据上述代码我们根据msg插入不同,将msg分为三类

《事件驱动模型 Handler 与 Flutter future》
《事件驱动模型 Handler 与 Flutter future》

1.如果异步线程向主线程新加入的消息是插入消息队列对头则需要唤醒队列

if (p == null || when == 0 || when // New head, wake up the event queue if blocked.
msg.next = p;
mMessages = msg;
needWake = mBlocked;
}

2.如果异步线程向主线程新加入的消息是异步消息,并且在队列的第二个位置,并且开启了同步屏障,则唤醒队列

3.其他情况的msg,均不会唤醒消息队列

大家可以看出上面强调了异步线程,因为主线程处于休眠状态。所以上面的方法只能异步线程调用。这个异步线程会是谁呢?留给大家一起思考。

接下来看唤醒的逻辑,nativeWake方法内部关键代码如下:

void Looper::wake() {
uint64_t inc = 1;
// 向管道mWakeEventFd写入字符1
ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd, &inc, sizeof(uint64_t)));
if (nWrite != sizeof(uint64_t)) {
if (errno != EAGAIN) {
ALOGW("Could not write wake signal, errno=%d", errno);
}
}
}

我们向管道中写入了数据,由于我们使用epoll监听了该管道,所以epoll_wait会被唤醒。

同步屏障机制(sync barrier)

有这样一个场景:某个消息加入消息队列后,我们希望他立即被处理掉。但是我们的消息都是按照系统运行时间排序的。我们如果达到该目的呢。如果了解同步屏障机制的话改问题就不在是一个问题。

还是从代码入手

Message next() {
....

nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
// Try to retrieve the next message. Return if found.
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;

//关键逻辑位置
if (msg != null && msg.target == null) {
// Stalled by a barrier. Find the next asynchronous message in the queue.
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}

从关键位置代码的逻辑可以看出只要队列头部的msg的target为null就会查找队列中的异步消息

我们如何发送target为null的msg到队列头部呢?可以使用该方法

int token = mHandler.getLooper().getQueue().postSyncBarrier();

然后我们发送消息时设置当前消息为异步消息就可以了。
当然我们还需要移除target为null的消息,不然同步消息就永远不执行了。

mHandler.getLooper().getQueue().removeSyncBarrier(token)

Android系统中的触发View布局测量流程的msg就是一个异步消息,从而加速布局和绘制来减少卡顿。

空闲消息(idelHadler)

Message next() {
...省略代码
// No more messages.
nextPollTimeoutMillis = -1;
}
// Process the quit message now that all pending messages have been handled.
if (mQuitting) {
dispose();
return null;
}
//关键代码
// If first time idle, then get the number of idlers to run.
// Idle handles only run if the queue is empty or if the first message
// in the queue (possibly a barrier) is due to be handled in the future.
if (pendingIdleHandlerCount <0
&& (mMessages == null || now pendingIdleHandlerCount = mIdleHandlers.size();
}
if (pendingIdleHandlerCount <= 0) {
// No idle handlers to run. Loop and wait some more.
mBlocked = true;
continue;
}
if (mPendingIdleHandlers == null) {
mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
}
mPendingIdleHandlers = mIdleHandle

If first time idle, then get the number of idlers to run.Idle handles only run if the queue is empty or if the first message in the queue (possibly a barrier) is due to be handled in the future.

线程轮询消息队列是发现没有要处理的消息时,发现注册了idelHadler。线程表示我就不休息了处理你吧。

以上我们了解了idelHadler的执行时机。idelHadler有哪些应用场景呢?

《事件驱动模型 Handler 与 Flutter future》
《事件驱动模型 Handler 与 Flutter future》

1.在开发过程中,在Activiy业务得到某个view的高度然后进行相关操作,在resume方法中必然是不好使的,因为还没有触发计算的msg并且还没有执行。 所以我们要等到消息执行完成才可以获取大小。怎么才能知道测量的消息执行完成了,idelHadler就派上了用场。我们可以在resume方法中想获取大小的逻辑,加到idelHadler回调中。

2.resume方法中数据填充太耗时,我们同样可以加到idelHadler回调中加快展示速度。

事件驱动模型 Flutter future

相信一些小伙伴已经接触了Flutter开发,Flutter也是事件驱动的,也有自己的Event Loop。

《事件驱动模型 Handler 与 Flutter future》
《事件驱动模型 Handler 与 Flutter future》

可以看出其有两个消息队列 微任务队列(MicroTask queue)和事件队列(Event queue)

  1. 事件队列包含外部事件,例如I/O, Timer,绘制事件等等
  2. 微任务队列则包含有Dart内部的微任务,主要是通过scheduleMicrotask来调度

同样我们不应该在Future执行耗时操作不然会卡。

大家计算一下输出的结果是啥?

import 'dart:async';
main() {
print('1');
scheduleMicrotask(() => print('3'));
new Future.delayed(new Duration(seconds:1),
() => print('7'));
new Future(() => print('5'));
new Future(() => print('6'));
scheduleMicrotask(() => print('4'));
print('2');
}

输出如下:

1
2
3
4
5
6
7

可以看出main方法内调用new Future() 或 scheduleMicrotask()实质上是向队列中发送消息,main方法结束然后开始轮询消息队列执行回调。

总结

本文通过事件驱动模型从而引出Handler和Future, 总结一下Handler中的几个关键名词 epoll机制,休眠/唤醒策略,同步屏障,异步/同步消息idelHadler。Flutter Future总结的就比较少了记住两个两个任务队列 微任务队列(MicroTask queue)事件队列(Event queue)。如有问题欢迎指正,共同学习共同进步。

Q&A

你以为你以为的就是你以为的吗?

实践是检验真理的唯一标准。

参考

事件驱动编程
Flutter学习之事件循环机制、数据库、网络请求
Flutter for Android Developers &#8211; Async UI
Flutter/Dart中的异步
你真的了解Handler吗?
异步消息
Handler之同步屏障机制
关于MessageQueue
你知道android的MessageQueue.IdleHandler吗
Android Event事件流分析
Android应用处理MotionEvent的过程
事件驱动
epoll原理是什么


推荐阅读
  • 计算机存储系统的层次结构及其优势
    本文介绍了计算机存储系统的层次结构,包括高速缓存、主存储器和辅助存储器三个层次。通过分层存储数据可以提高程序的执行效率。计算机存储系统的层次结构将各种不同存储容量、存取速度和价格的存储器有机组合成整体,形成可寻址存储空间比主存储器空间大得多的存储整体。由于辅助存储器容量大、价格低,使得整体存储系统的平均价格降低。同时,高速缓存的存取速度可以和CPU的工作速度相匹配,进一步提高程序执行效率。 ... [详细]
  • 本文介绍了RPC框架Thrift的安装环境变量配置与第一个实例,讲解了RPC的概念以及如何解决跨语言、c++客户端、web服务端、远程调用等需求。Thrift开发方便上手快,性能和稳定性也不错,适合初学者学习和使用。 ... [详细]
  • Java在运行已编译完成的类时,是通过java虚拟机来装载和执行的,java虚拟机通过操作系统命令JAVA_HOMEbinjava–option来启 ... [详细]
  • 众筹商城与传统商城的区别及php众筹网站的程序源码
    本文介绍了众筹商城与传统商城的区别,包括所售产品和玩法不同以及运营方式不同。同时还提到了php众筹网站的程序源码和方维众筹的安装和环境问题。 ... [详细]
  • 本文介绍了在Linux下安装和配置Kafka的方法,包括安装JDK、下载和解压Kafka、配置Kafka的参数,以及配置Kafka的日志目录、服务器IP和日志存放路径等。同时还提供了单机配置部署的方法和zookeeper地址和端口的配置。通过实操成功的案例,帮助读者快速完成Kafka的安装和配置。 ... [详细]
  • 深入理解Kafka服务端请求队列中请求的处理
    本文深入分析了Kafka服务端请求队列中请求的处理过程,详细介绍了请求的封装和放入请求队列的过程,以及处理请求的线程池的创建和容量设置。通过场景分析、图示说明和源码分析,帮助读者更好地理解Kafka服务端的工作原理。 ... [详细]
  • Oracle优化新常态的五大禁止及其性能隐患
    本文介绍了Oracle优化新常态中的五大禁止措施,包括禁止外键、禁止视图、禁止触发器、禁止存储过程和禁止JOB,并分析了这些禁止措施可能带来的性能隐患。文章还讨论了这些禁止措施在C/S架构和B/S架构中的不同应用情况,并提出了解决方案。 ... [详细]
  • GPT-3发布,动动手指就能自动生成代码的神器来了!
    近日,OpenAI发布了最新的NLP模型GPT-3,该模型在GitHub趋势榜上名列前茅。GPT-3使用的数据集容量达到45TB,参数个数高达1750亿,训练好的模型需要700G的硬盘空间来存储。一位开发者根据GPT-3模型上线了一个名为debuid的网站,用户只需用英语描述需求,前端代码就能自动生成。这个神奇的功能让许多程序员感到惊讶。去年,OpenAI在与世界冠军OG战队的表演赛中展示了他们的强化学习模型,在限定条件下以2:0完胜人类冠军。 ... [详细]
  • 恶意软件分析的最佳编程语言及其应用
    本文介绍了学习恶意软件分析和逆向工程领域时最适合的编程语言,并重点讨论了Python的优点。Python是一种解释型、多用途的语言,具有可读性高、可快速开发、易于学习的特点。作者分享了在本地恶意软件分析中使用Python的经验,包括快速复制恶意软件组件以更好地理解其工作。此外,作者还提到了Python的跨平台优势,使得在不同操作系统上运行代码变得更加方便。 ... [详细]
  • 一次上线事故,30岁+的程序员踩坑经验之谈
    本文主要介绍了一位30岁+的程序员在一次上线事故中踩坑的经验之谈。文章提到了在双十一活动期间,作为一个在线医疗项目,他们进行了优惠折扣活动的升级改造。然而,在上线前的最后一天,由于大量数据请求,导致部分接口出现问题。作者通过部署两台opentsdb来解决问题,但读数据的opentsdb仍然经常假死。作者只能查询最近24小时的数据。这次事故给他带来了很多教训和经验。 ... [详细]
  • linux进阶50——无锁CAS
    1.概念比较并交换(compareandswap,CAS),是原⼦操作的⼀种,可⽤于在多线程编程中实现不被打断的数据交换操作࿰ ... [详细]
  • 云原生边缘计算之KubeEdge简介及功能特点
    本文介绍了云原生边缘计算中的KubeEdge系统,该系统是一个开源系统,用于将容器化应用程序编排功能扩展到Edge的主机。它基于Kubernetes构建,并为网络应用程序提供基础架构支持。同时,KubeEdge具有离线模式、基于Kubernetes的节点、群集、应用程序和设备管理、资源优化等特点。此外,KubeEdge还支持跨平台工作,在私有、公共和混合云中都可以运行。同时,KubeEdge还提供数据管理和数据分析管道引擎的支持。最后,本文还介绍了KubeEdge系统生成证书的方法。 ... [详细]
  • 解决Cydia数据库错误:could not open file /var/lib/dpkg/status 的方法
    本文介绍了解决iOS系统中Cydia数据库错误的方法。通过使用苹果电脑上的Impactor工具和NewTerm软件,以及ifunbox工具和终端命令,可以解决该问题。具体步骤包括下载所需工具、连接手机到电脑、安装NewTerm、下载ifunbox并注册Dropbox账号、下载并解压lib.zip文件、将lib文件夹拖入Books文件夹中,并将lib文件夹拷贝到/var/目录下。以上方法适用于已经越狱且出现Cydia数据库错误的iPhone手机。 ... [详细]
  • Java和JavaScript是什么关系?java跟javaScript都是编程语言,只是java跟javaScript没有什么太大关系,一个是脚本语言(前端语言),一个是面向对象 ... [详细]
  • 2016 linux发行版排行_灵越7590 安装 linux (manjarognome)
    RT之前做了一次灵越7590黑苹果炒作业的文章,希望能够分享给更多不想折腾的人。kawauso:教你如何给灵越7590黑苹果抄作业​zhuanlan.z ... [详细]
author-avatar
书友54330525
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有