android handle 阻塞,Android全面解析之Handler机制:常见问题汇总

主线程为什么不用初始化Looper?

答:因为应用在启动的过程中就已经初始化主线程Looper了。

每个java应用程序都是有一个main方法入口,Android是基于Java的程序也不例外。Android程序的入口在ActivityThread的main方法中:

// 初始化主线程Looper

Looper.prepareMainLooper();

...

// 新建一个ActivityThread对象

ActivityThread thread = new ActivityThread();

thread.attach(false, startSeq);

// 获取ActivityThread的Handler,也是他的内部类H

if (sMainThreadHandler == null) {

sMainThreadHandler = thread.getHandler();

}

...

Looper.loop();

// 如果loop方法结束则抛出异常,程序结束

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

}

main方法中先初始化主线程Looper,新建ActivityThread对象,然后再启动Looper,这样主线程的Looper在程序启动的时候就跑起来了。我们不需要再去初始化主线程Looper。

为什么主线程的Looper是一个死循环,但是却不会ANR?

答: 因为当Looper处理完所有消息的时候会进入阻塞状态,当有新的Message进来的时候会打破阻塞继续执行。

这其实没理解好ANR这个概念。ANR,全名Application Not Responding。当我发送一个绘制UI 的消息到主线程Handler之后,经过一定的时间没有被执行,则抛出ANR异常。Looper的死循环,是循环执行各种事务,包括UI绘制事务。Looper死循环说明线程没有死亡,如果Looper停止循环,线程则结束退出了。Looper的死循环本身就是保证UI绘制任务可以被执行的原因之一。同时UI绘制任务有同步屏障,可以更加快速地保证绘制更快执行。同步屏障下面会讲。

Handler如何保证MessageQueue并发访问安全?

答:循环加锁,配合阻塞唤醒机制。

我们可以发现MessageQueue其实是“生产者-消费者”模型,Handler不断地放入消息,Looper不断地取出,这就涉及到死锁问题。如果Looper拿到锁,但是队列中没有消息,就会一直等待,而Handler需要把消息放进去,锁却被Looper拿着无法入队,这就造成了死锁。Handler机制的解决方法是循环加锁。在MessageQueue的next方法中:

Message next() {

...

for (;;) {

...

nativePollOnce(ptr, nextPollTimeoutMillis);

synchronized (this) {

...

}

}

}

我们可以看到他的等待是在锁外的,当队列中没有消息的时候,他会先释放锁,再进行等待,直到被唤醒。这样就不会造成死锁问题了。

那在入队的时候会不会因为队列已经满了然后一边在等待消息处理一边拿着锁呢?这一点不同的是MessageQueue的消息没有上限,或者说他的上限就是JVM给程序分配的内存,如果超出内存会抛出异常,但一般情况下是不会的。

Handler是如何切换线程的?

答: 使用不同线程的Looper处理消息。

前面我们聊到,代码的执行线程,并不是代码本身决定,而是执行这段代码的逻辑是在哪个线程,或者说是哪个线程的逻辑调用的。每个Looper都运行在对应的线程,所以不同的Looper调用的dispatchMessage方法就运行在其所在的线程了。

Handler的阻塞唤醒机制是怎么回事?

答: Handler的阻塞唤醒机制是基于Linux的阻塞唤醒机制。

这个机制也是类似于handler机制的模式。在本地创建一个文件描述符,然后需要等待的一方则监听这个文件描述符,唤醒的一方只需要修改这个文件,那么等待的一方就会收到文件从而打破唤醒。

能不能让一个Message加急被处理?/ 什么是Handler同步屏障?

答:可以 / 一种使得异步消息可以被更快处理的机制

如果向主线程发送了一个UI更新的操作Message,而此时消息队列中的消息非常多,那么这个Message的处理就会变得缓慢,造成界面卡顿。所以通过同步屏障,可以使得UI绘制的Message更快被执行。

什么是同步屏障?这个“屏障”其实是一个Message,插入在MessageQueue的链表头,且其target==null。Message入队的时候不是判断了target不能为null吗?不不不,添加同步屏障是另一个方法:

public int postSyncBarrier() {

return postSyncBarrier(SystemClock.uptimeMillis());

}

private int postSyncBarrier(long when) {

synchronized (this) {

final int token = mNextBarrierToken++;

final Message msg = Message.obtain();

msg.markInUse();

msg.when = when;

msg.arg1 = token;

Message prev = null;

Message p = mMessages;

// 把当前需要执行的Message全部执行

if (when != 0) {

while (p != null && p.when <= when) {

prev = p;

p = p.next;

}

}

// 插入同步屏障

if (prev != null) { // invariant: p == prev.next

msg.next = p;

prev.next = msg;

} else {

msg.next = p;

mMessages = msg;

}

return token;

}

}

可以看到同步屏障就是一个特殊的target,哪里特殊呢?target==null,我们可以看到他并没有给target属性赋值。那这个target有什么用呢?看next方法:

Message next() {

...

// 阻塞时间

int nextPollTimeoutMillis = 0;

for (;;) {

...

// 阻塞对应时间

nativePollOnce(ptr, nextPollTimeoutMillis);

// 对MessageQueue进行加锁,保证线程安全

synchronized (this) {

final long now = SystemClock.uptimeMillis();

Message prevMsg = null;

Message msg = mMessages;

/**

* 1

*/

if (msg != null && msg.target == null) {

// 同步屏障,找到下一个异步消息

do {

prevMsg = msg;

msg = msg.next;

} while (msg != null && !msg.isAsynchronous());

}

if (msg != null) {

if (now < msg.when) {

// 下一个消息还没开始,等待两者的时间差

nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);

} else {

// 获得消息且现在要执行,标记MessageQueue为非阻塞

mBlocked = false;

/**

* 2

*/

// 一般只有异步消息才会从中间拿走消息,同步消息都是从链表头获取

if (prevMsg != null) {

prevMsg.next = msg.next;

} else {

mMessages = msg.next;

}

msg.next = null;

msg.markInUse();

return msg;

}

} else {

// 没有消息,进入阻塞状态

nextPollTimeoutMillis = -1;

}

// 当调用Looper.quitSafely()时候执行完所有的消息后就会退出

if (mQuitting) {

dispose();

return null;

}

...

}

...

}

}

这个方法我在前面讲过,我们重点看一下关于同步屏障的部分,看注释1的地方的代码:

if (msg != null && msg.target == null) {

// 同步屏障,找到下一个异步消息

do {

prevMsg = msg;

msg = msg.next;

} while (msg != null && !msg.isAsynchronous());

}

如果遇到同步屏障,那么会循环遍历整个链表找到标记为异步消息的Message,即isAsynchronous返回true,其他的消息会直接忽视,那么这样异步消息,就会提前被执行了。注释2的代码注意一下就可以了。

注意,同步屏障不会自动移除,使用完成之后需要手动进行移除,不然会造成同步消息无法被处理。从源码中可以看到如果不移除同步屏障,那么他会一直在那里,这样同步消息就永远无法被执行了。

有了同步屏障,那么唤醒的判断条件就必须再加一个:MessageQueue中有同步屏障且处于阻塞中,此时插入在所有异步消息前插入新的异步消息。这个也很好理解,跟同步消息是一样的。如果把所有的同步消息先忽视,就是插入新的链表头且队列处于阻塞状态,这个时候就需要被唤醒了。看一下源码:

boolean enqueueMessage(Message msg, long when) {

...

// 对MessageQueue进行加锁

synchronized (this) {

...

if (p == null || when == 0 || when < p.when) {

msg.next = p;

mMessages = msg;

needWake = mBlocked;

} else {

/**

* 1

*/

// 当线程被阻塞,且目前有同步屏障,且入队的消息是异步消息

needWake = mBlocked && p.target == null && msg.isAsynchronous();

Message prev;

for (;;) {

prev = p;

p = p.next;

if (p == null || when < p.when) {

break;

}

/**

* 2

*/

// 如果找到一个异步消息,说明前面有延迟的异步消息需要被处理,不需要被唤醒

if (needWake && p.isAsynchronous()) {

needWake = false;

}

}

msg.next = p;

prev.next = msg;

}

// 如果需要则唤醒队列

if (needWake) {

nativeWake(mPtr);

}

}

return true;

}

同样,这个方法我之前讲过,把无关同步屏障的代码忽视,看到注释1处的代码。如果插入的消息是异步消息,且有同步屏障,同时MessageQueue正处于阻塞状态,那么就需要唤醒。而如果这个异步消息的插入位置不是所有异步消息之前,那么不需要唤醒,如注释2。

那我们如何发送一个异步类型的消息呢?有两种办法:

使用异步类型的Handler发送的全部Message都是异步的

给Message标志异步

Handler有一系列带Boolean类型的参数的构造器,这个参数就是决定是否是异步Handler:

public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) {

mLooper = looper;

mQueue = looper.mQueue;

mCallback = callback;

// 这里赋值

mAsynchronous = async;

}

在发送消息的时候就会给Message赋值:

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

}

但是异步类型的Handler构造器是标记为hide,我们无法使用,所以我们使用异步消息只有通过给Message设置异步标志:

public void setAsynchronous(boolean async) {

if (async) {

flags |= FLAG_ASYNCHRONOUS;

} else {

flags &= ~FLAG_ASYNCHRONOUS;

}

}

但是!!!!,其实同步屏障对于我们的日常使用的话其实是没有多大用处。因为设置同步屏障和创建异步Handler的方法都是标志为hide,说明谷歌不想要我们去使用他。所以这里同步屏障也作为一个了解,可以更加全面地理解源码中的内容。

什么是IdleHandler?

答: 当MessageQueue为空或者目前没有需要执行的Message时会回调的接口对象。

IdleHandler看起来好像是个Handler,但他其实只是一个有单方法的接口,也称为函数型接口:

public static interface IdleHandler {

boolean queueIdle();

}

在MessageQueue中有一个List存储了IdleHandler对象,当MessageQueue没有需要被执行的Message时就会遍历回调所有的IdleHandler。所以IdleHandler主要用于在消息队列空闲的时候处理一些轻量级的工作。

IdleHandler的调用是在next方法中:

Message next() {

// 如果looper已经退出了,这里就返回null

final long ptr = mPtr;

if (ptr == 0) {

return null;

}

// IdleHandler的数量

int pendingIdleHandlerCount = -1;

// 阻塞时间

int nextPollTimeoutMillis = 0;

for (;;) {

if (nextPollTimeoutMillis != 0) {

Binder.flushPendingCommands();

}

// 阻塞对应时间

nativePollOnce(ptr, nextPollTimeoutMillis);

// 对MessageQueue进行加锁,保证线程安全

synchronized (this) {

final long now = SystemClock.uptimeMillis();

Message prevMsg = null;

Message msg = mMessages;

if (msg != null && msg.target == null) {

// 同步屏障,找到下一个异步消息

do {

prevMsg = msg;

msg = msg.next;

} while (msg != null && !msg.isAsynchronous());

}

if (msg != null) {

if (now < msg.when) {

// 下一个消息还没开始,等待两者的时间差

nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);

} else {

// 获得消息且现在要执行,标记MessageQueue为非阻塞

mBlocked = false;

// 一般只有异步消息才会从中间拿走消息,同步消息都是从链表头获取

if (prevMsg != null) {

prevMsg.next = msg.next;

} else {

mMessages = msg.next;

}

msg.next = null;

msg.markInUse();

return msg;

}

} else {

// 没有消息,进入阻塞状态

nextPollTimeoutMillis = -1;

}

// 当调用Looper.quitSafely()时候执行完所有的消息后就会退出

if (mQuitting) {

dispose();

return null;

}

// 当队列中的消息用完了或者都在等待时间延迟执行同时给pendingIdleHandlerCount<0

// 给pendingIdleHandlerCount赋值MessageQueue中IdleHandler的数量

if (pendingIdleHandlerCount < 0

&& (mMessages == null || now < mMessages.when)) {

pendingIdleHandlerCount = mIdleHandlers.size();

}

// 没有需要执行的IdleHanlder直接continue

if (pendingIdleHandlerCount <= 0) {

// 执行IdleHandler,标记MessageQueue进入阻塞状态

mBlocked = true;

continue;

}

// 把List转化成数组类型

if (mPendingIdleHandlers == null) {

mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];

}

mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);

}

// 执行IdleHandler

for (int i = 0; i < pendingIdleHandlerCount; i++) {

final IdleHandler idler = mPendingIdleHandlers[i];

mPendingIdleHandlers[i] = null; // 释放IdleHandler的引用

boolean keep = false;

try {

keep = idler.queueIdle();

} catch (Throwable t) {

Log.wtf(TAG, "IdleHandler threw exception", t);

}

// 如果返回false,则把IdleHanlder移除

if (!keep) {

synchronized (this) {

mIdleHandlers.remove(idler);

}

}

}

// 最后设置pendingIdleHandlerCount为0,防止再执行一次

pendingIdleHandlerCount = 0;

// 当在执行IdleHandler的时候,可能有新的消息已经进来了

// 所以这个时候不能阻塞,要回去循环一次看一下

nextPollTimeoutMillis = 0;

}

}

代码很多,可能看着有点乱,我梳理一下逻辑,然后再回去看源码就会很清晰了:

当调用next方法的时候,会给pendingIdleHandlerCount赋值为-1

如果队列中没有需要处理的消息的时候,就会判断pendingIdleHandlerCount是否为<0,如果是则把存储IdleHandler的list的长度赋值给pendingIdleHandlerCount

把list中的所有IdleHandler放到数组中。这一步是为了不让在执行IdleHandler的时候List被插入新的IdleHandler,造成逻辑混乱

然后遍历整个数组执行所有的IdleHandler

最后给pendingIdleHandlerCount赋值为0。然后再回去看一下这个期间有没有新的消息插入。因为pendingIdleHandlerCount的值为0不是-1,所以IdleHandler只会在空闲的时候执行一次

同时注意,如果IdleHandler返回了false,那么执行一次之后就被丢弃了。

建议读者再回去把源码看一遍,这样逻辑会清晰很多。

最后

到这里,Handler系列的文章就结束了。如果有地方遗漏没介绍到,或者有疑问,可评论区留言。 感谢阅读。

大家如果还想了解更多Android 相关的更多知识点可以点进我的【GitHub】项目中,里面记录了许多的Android 知识点。

3c5fa3e14146

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

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

相关文章

gulp使用

gulp需要全局安装和当前目录都安装才能使用gulp命令 安装gulp插件 npm install gulp-rev gulp-rev-replace gulp-useref gulp-filter gulp-uglify gulp-csso --save-dev gulp-rev&#xff1a;给每个文件添加版本号&#xff0c;根据文件内容计算hash码&#xff0c;修改文件名&am…

android监听器作用,android - 监听器和接收器(Android)有什么区别?

例如&#xff0c;我需要一个BroadcastReceiver来获取这些事件:重新启动或关机屏幕开或关电池状态(电压&#xff0c;接通电源&#xff0c;温度)物理按钮按下(相机&#xff0c;媒体等)但是我需要监听器来获取以下事件:用于传感器事件(加速度&#xff0c;磁场&#xff0c;方向&…

UML中的6大关系(关联、依赖、聚合、组合、泛化、实现)

UML定义的关系主要有六种&#xff1a;依赖、类属、关联、实现、聚合和组合。这些类间关系的理解和使用是掌握和应用UML的关键&#xff0c;而也就是这几种关系&#xff0c;往往会让初学者迷惑。这里给出这六种主要UML关系的说明和类图描述&#xff0c;一看之下&#xff0c;清晰明…

android 相册 uri空,Android---相册getContentResolver().query结果为空指针

针对小米4手机上测试如下代码&#xff1a;Uri uri data.getData();String[] proj {MediaStore.Images.Media.DATA};//Cursor cursor managedQuery(uri, proj, null, null, null);Cursor cursor getContentResolver().query(uri, proj, null, null, null);cursor.moveToFirs…

用Spark学习FP Tree算法和PrefixSpan算法

在FP Tree算法原理总结和PrefixSpan算法原理总结中&#xff0c;我们对FP Tree和PrefixSpan这两种关联算法的原理做了总结&#xff0c;这里就从实践的角度介绍如何使用这两个算法。由于scikit-learn中没有关联算法的类库&#xff0c;而Spark MLlib有&#xff0c;本文的使用以Spa…

android os n9005,SM-N9005

hkSM-N9005ZKETELSM-N9005{"softwares":[{"description":"SAMSUNG Kies,PC Sync","fileName":"Kies3Setup.exe","fileModifiedDate":"2013年9月11日","fileVersion":"Kies 3.0(13091_39…

性能调优-硬盘方面,操作系统方面,文件系统方面

硬盘对数据库性能的影响 传统机械硬盘 当前大多数数据库使用的都是传统的机械硬盘。机械硬盘的技术目前已非常成熟&#xff0c;在服务器领域一般使用SAS或SATA接口的硬盘。服务器机械硬盘开始向小型化转型&#xff0c;目前已经有大量2.5寸的SAS机械硬盘。 机械硬盘有两个重要的…

chrome Android 80,Chrome OS 80将为Chromebook带来侧载Android应用的支持

如需体验这项功能&#xff0c;需在启动 Crostini 容器时加上一行特殊的命令 —— 从 Chromebook 命令行启动时&#xff0c;请加上 –enable-features ArcAdbSideloading 。最终&#xff0c;我们希望这回成为一项明示的标记(flags)功能。如变更日志所述&#xff0c;用户可通过托…

BZOJ4122 : [Baltic2015]File paths

对于在$o$点的某个询问&#xff0c;有两种情况&#xff1a; 情况1&#xff1a;走到任意一个点$x$然后超链接跳到$o$的某个祖先$y$再走到$o$。 枚举所有$y$看看是否存在$x$即可。 时间复杂度$O(nm)$。 情况2&#xff1a;走到$o$的某个祖先$x$&#xff0c;然后走到$x$子树内某个点…

android 日历仿IOS,基于Android week view仿小米和iphone日历效果

前言最近由于项目需求&#xff0c;要做一个仿小米日历的功能&#xff0c;下面显示一天的日程&#xff0c;header以周为单位进行滑动&#xff0c;github上找了很久也没有找到合适的&#xff0c;但找到一相近的开源项目Android-week-view&#xff0c;它不是我们项目所需要的效果&…

Deque - leetcode 【双端队列】

239. Sliding Window Maximum //大概思路是用双向队列保存数字的下标&#xff0c;遍历整个数组&#xff0c;如果此时队列的首元素是i - k的话&#xff0c;表示此时窗口向右移了一步&#xff0c;则移除队首元素。然后比较队尾元素和将要进来的值&#xff0c;如果小的话就都移除&…

android开源系统brvah,Android Jetpack之通用Adapter(Databinding+BRVAH)

之前发过一个databinding版的通用adapter&#xff0c;能实现一般需求&#xff0c;不过功能比较简陋&#xff0c;实际开发中大家更倾向于使用BRVAH等功能丰富的第三方框架。现在给出一个基于BRVAH的databinding版通用Adapter。BaseAdapterabstract class BaseAdapter(private va…

1、管理员登录中间件和注销

1、根据session去判断用户是否登录&#xff0c;登录后才可以进index首页&#xff0c;否则返回login页面&#xff0c;借助中间件 (1)修改路由 Route::group([middleware > [web,admin.login],prefix>admin,namespace>Admin], function () { //注册一个中间件admin.logi…

HTML阅读打开点击不了,如何让网页文章中的代码可以点击运行

在网页中运行代码HTML5学堂&#xff1a;很多代码网站当中&#xff0c;都会提供运行代码段功能&#xff0c;便于查看代码效果&#xff0c;那么这个是如何实现的呢&#xff1f;一起来看一下——如何在网页中运行代码。HTML5学堂-刘国利said&#xff1a;应该是在2013年的时候&…

angular-cli构建

angular-cli全称Command Line Interface 命令行界面 1使用命令 npm install -g angular-cli 安装angular-cli, 2安装完成后使用cli快速常见一个angular模板, 3ng serve启动项目 4在模板(seed/种子)的基础上进行修改 ############################## 在typescript中引用leanclou…

模仿块级作用域

模仿块级作用域 JavaScript中没有块级作用域&#xff0c;这意为着在块语句中定义的变量&#xff0c;实际上是包含在函数中而非语句中创建的。 下面的这个实例可以看出: function outputNumbers(count){for (var i0; i < count; i){alert(i);}alert(i); //count }outputNum…

html留言回复评论页面模板,HTML5实现留言和回复的页面样式

这篇文章主要介绍了用HTML5如何实现留言和回复样式,需要的朋友可以参考下具体就不做详细讲解了&#xff0c;直接上代码&#xff1a;web开发-webkfa.com*{margin:0;padding:0;-webkit-touch-callout: none; /* prevent callout to copy image, etc when tap to hold */-webkit-t…

UVA-714 二分

把可能的进行二分判断&#xff0c;判断的时候尽量向右取&#xff0c;一直取到不能去为止&#xff0c;这样才有可能成功分割。 判断是否可以把up作为最大值的代码&#xff1a; bool judge(LL up){if(up < Big) return false; //Big是数组中最大值&#xff0c;如果up小于最大值…

html5不支持的属性,HTML5 常用语法一览(列举不支持的属性)

HTML头部标记标记描述HTML5标准定义页面中所有链接的基准URL设定显示在浏览器左上方的标题内容表明该文档是一个可用于检索的网关脚本不支持文档本身的元信息,例如查询关键词,有效期等设定CSS层叠样式表内容设定外部资源的链接页面脚本内容元信息标记属性标记描述值HTML5标准ht…

守护线程

/*守护线程&#xff08;后台线程&#xff09;:在一个进程中如果只剩下了守护线程&#xff0c;那么守护线程也会死亡。需求&#xff1a; 模拟QQ下载更新包。一个线程默认都不是守护线程。*/ public class Demo7 extends Thread {public Demo7(String name){super(name);}Overrid…