单线程模型
Android 单线程模型是指:所有与 UI 相关的操作(如绘制、事件响应)必须在主线程(也称 UI 线程)中执行,而 Android UI 组件非线程安全,禁止在子线程直接更新 UI。
Thread、Looper、Handler、MessageQueue的包含关系
- 每个线程对应一个 Looper,保存在每个线程的线程本地存储
ThreadLocal中; - 每个线程可有多个 Handler 共享同一个 Looper 和 MessageQueue;
-
每个 Handler 绑定到线程的 Looper,Handler绑定到 Looper,不“包含”队列,是 Message 的发送者与处理者。
-
Handler不一定创建在Looper线程中,只需要绑定到指定线程的Looper。
-
Handler可指定绑定的Looper,不指定时则默认绑定到其创建线程的Looper
- 多个 Handler 可共享同一个线程的 Looper;
- 每个 Looper 持有一个 MessageQueue(在 Looper 构造时创建);
- MessageQueue 存储 Message 对象,
- 每个 Message 指定一个 target Handler 用于处理;
简言之:一个线程 → 一个 Looper → 一个 MessageQueue;一个线程→ 多个 Handler 可绑定同一个 Looper;Message 属于 MessageQueue ,Message由指定的 Handler 处理。
Handler
在Looper和Handler两者中,Looper实现了线程间通信,而Handler是对Looper的封装,是Looper的友好接口方便用户调用。而且Handler在各个线程间共享,以达到多线程通信的目的。
Handler是Android操作系统中的线程间通信工具,它主要由两个作用:
(1)安排消息或Runnable 在主线程中某个地方执行。
(2)安排一个动作在另外的线程中执行。
每个Handler对象维护两个队列(FIFO):消息队列和Runnable队列, 都是由Android操作系统提供的。Handler可以通过这两个队列来分别:
- 发送、接受、处理消息–消息队列;
- 启动、结束、休眠线程–Runnable队列;
Handler的使用方法大体分为3个步骤:1.创建Handler对象。2.创建Runnable和消息。3.调用post以及sendMessage方法将Runnable和消息添加到队列。
Handler中的消息队列是引用的Looper中的消息队列。
Android的线程异步处理机制:Handler对象维护一个任务队列,有新的Runnable送来的时候,把它放在队尾,而处理 Runnable的时候,从队头取出Runnable执行。当向队列发送一个Runnable后,立即就返回,并不理会Runnable是否被执行,执行 是否成功等。而具体的执行则是当排队排到该Runnable后系统拿来执行的。
1. Handler与UI同线程时,可用于在子线程中通知主线程更新。
诞生一个主线程的Handler物件,当做Listener去让子线程能将讯息Push到主线程的Message Quene里,以便触发主线程的handlerMessage()函数,让主线程知道子线程的状态,并在主线程更新UI。
例如,在子线程的状态发生变化时,我们需要更新UI。如果在子线程中直接更新UI,通常会抛出下面的异常,为此,需要通过Handler通知主线程Ui Thread来更新界面。
2. 在Android的开发中,在非UI线程中不能操作UI线程中的控件,即UI是非线程安全的;
ProgressBar控件里面在一个线程中直接刷新进度条,看似违反了ANDROID的UI刷新原则,其实是这个函数里面自己做了同步操作。
Handler 的 post()方式或 send()方式发送消息,最终都是调用了 sendMessageAtTime()方法。Activity 中的 runOnUiThread()中更新 UI,其实也是发送消息 通知主线程更新 UI,最终也会调用 sendMessageAtTime()方法。
Handler postDelay 方法原理
判断如果这 个 Message 有延迟,就调用 nativePollOnce(ptr, nextPollTimeoutMillis)进行阻塞。 nativePollOnce()的作用类似与 object.wait()。得出结论是通过阻塞实现的。
如果在阻塞这段时间里有无延迟 message 又加入 MessageQueen 中是怎么实现立即处理这个 message 的?
如果当前插入的消息不是延迟 message,或比当前的延迟短, 这个消息就会插入头部并且唤起线程nativeWake(mPtr)(唤起线程)。
Handler 引起的内存泄露的原因以及解决方案
Handler如果发送延时消息,然后在延时期间用户关闭了 Activity,那么该 Activity会Destroy但其内存却不会释放导致内存泄露。内存泄露是因为 Message 持有 Handler,而Java 非静态内部类持有外部类,使得 Activity 被 Handler 持有,最终就导致 Activity 泄露。
解决该问题的最有效的方法是将 Handler 定义成静态内部类,在内部持有 Activity 的弱引用,并在Activity销毁前通过Handler清空消息队列。
Looper
一个简单的 main 函数执行完毕后,整个进程也就结束了,为了让一个进程长时间的运行下去,就需要 无限循环加事件通知的机制。
一个典型的Looper线程示例:
// 典型的关于Handler/Looper的线程
class LooperThread extends Thread {
public Handler mHandler;
public void run() {
//为当前线程初始化Looper
Looper.prepare();
mHandler = new Handler() {
public void handleMessage(Message msg) {
//定义消息处理逻辑.
Message msg = Message.obtain();
}
};
//启动Looper
Looper.loop();
}
}
// 其他线程向此looper线程发送一个消息
looperThread.mHandler.sendEmptyMessage(0);
使用上来说:
在Looper 线程中:
调用Looper.prepare()初始化 Looper
初始化一个 Handler 对象
Looper .loop() 进入循环休眠状态
在其他线程中:
获取到 Looper 线程中的 Handler 对象
构建一个 Message 对象
调用 Handler 对象的 sendMessage 发送消息给 Looper
Looper有一个静态的 static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>(); 成员变量,ThreadLocal本质上是一系列线程号和Looper对象的key-value键值对,因为sThreadLocal是静态的,所以它为所有的线程所共享。 而sThreadLocal.get()就是获取当前线程对应的Looper。Looper.prepare()就是重新创建一个Looper,查看Looper源码可看到调用了sThreadLocal.set(new Looper(quitAllowed)); 也就是往sThreadLocal插入了一个键值对。 另外一个Looper有一个MessageQueue,用来存储Message。
HandlerThread
- android.os.HandlerThread线程类,它具有消息处理能力,其内部创建了Looper消息循环实现线程间通信。
- 适用于子线程持续性任务,并且需要和主线程之间通信的场景。
- 使用方式:
- 创建HandlerThread并start,它会轮询内部的消息队列。
- 创建静态Handler绑定HandlerThread的Looper,实现消息处理。
- 通过Handler发送消息。
- 需手动调用quit/quitSafely销毁资源。
Android 线程优先级
Android优先级范围:通过android.os.Process.setThreadPriority()可设置更精细的优先级(−20∼19),值越小优先级越高(UI线程默认为-10)。
通过setThreadPriority设置的优先级和通过thread.setPriority设置的优先级是隔离的。
- JDK限制:Thread.setPriority()设置效果不明显,且超出[1∼10]范围会抛出IllegalArgumentException。
- Android优化机制:Process.setThreadPriority()对线程调度有明显影响,设计目的是让UI线程获取更多CPU时间片提高响应速度。
- 作用域隔离:两种设置方式互不影响,Process.setThreadPriority()不会改变Thread.getPriority()的返回值。
安卓消息机制
安卓消息机制的核心组件包括Handler、Looper和MessageQueue。
- Handler用于发送和处理消息,
- Looper负责驱动消息队列的循环,
- MessageQueue存储待处理的消息。
这些组件共同构成了安卓的消息通信机制,支撑了安卓作为消息驱动系统的运行。
1.Handler、Looper、MessageQueue的三角关系
- Handler的作用是发送和处理消息,通过post或send方法将消息加入MessageQueue。
- MessageQueue是一个单向链表,按消息触发时间排序,头部消息触发时间最近。
- Looper通过无限循环驱动消息队列,检查并分发符合条件的消息。
- 消息类型分为三种:同步屏障(Barrier Message)、异步消息(Async Message)和同步消息。
- 消息分发优先级:优先回调Message中的callback(如Runnable对象),其次为Handler的callback,最后为handleMessage方法。
- Looper的无限循环不会占用过高CPU资源,当队列为空时线程进入阻塞状态,新消息到来时唤醒线程。
- 一个线程至多有一个Looper,一个Looper对应一个MessageQueue,一个MessageQueue可包含多个Message,一个Looper可绑定多个Handler。
1) Handler的构造对象
- Handler构造函数通过Looper.myLooper()获取当前线程的Looper。
- 主线程的Looper在ActivityThread.main()中创建,调用Looper.prepareMainLooper()和Looper.loop()初始化。
- 若线程未调用Looper.prepare(),则Looper为null,此时创建Handler会抛出异常。
2) Looper的创建
- Looper通过prepare()或prepareMainLooper()创建,确保一个线程仅有一个Looper实例。
- Looper创建时关联的MessageQueue同时被初始化。
- Looper实例存储在ThreadLocal中,通过get和set方法实现线程私有变量的存取。
- ThreadLocal的优势是无需传递变量,可在线程任意位置访问。
3) 子线程的消息分发
- 子线程需显式调用Looper.prepare()和Looper.loop()以具备消息分发能力。
- 子线程中显示Toast需先开启Looper循环,避免线程检查报错。
- 子线程Looper需手动调用quit()退出循环,否则线程无法销毁。
2.消息入队
- 消息入队通过post或send方法实现,最终调用enqueueMessage将消息插入MessageQueue。
- 消息按触发时间排序,插入队列时调整链表结构,即在放入中就进行排序,链表头的延迟时间小,尾部延迟时间最大。
-
MessageQueue 是按照 Message 触发时间的先后顺序排列的,队头的消息是将要 最早触发的消息。
-
当有消息需要加入消息队列时,会从队列头开始遍历,直到找 到消息应该插入的合适位置,以保证所有消息的时间顺序。
1) 获取消息体
- post方法通过getPostMessage将Runnable包装为Message对象。
- 享元设计模式
- 避免直接new Message(),推荐使用Message.obtain()从复用池获取消息对象,减少内存占用。Message复用池采用享元设计模式,最多缓存50个Message对象。复用池通过链表管理,插入和删除效率高于ArrayList。
- 消息执行完毕后调用recycle()重置并回收对象,插入链表头节点复用。
- 类似设计见于MotionEvent等源码,避免频繁GC问题。
深入理解Android消息机制
1.消息队列
- Message对象为单向链表结构,每个节点通过message.next指向下一个消息节点,串联形成链表。消息队列仅需单向链表即可满足需求。
- 发送消息:sendMessage或postMessage最终调用enqueueMessage将消息插入队列;另可通过postSyncBarrier发送屏障消息。
2.消息入队
- 消息插入规则:按触发时间戳排序插入队列。
- enqueueMessage核心操作:
- 调整链表引用关系:若新消息时间戳早于队头消息,则插入队头并更新messages指向。
- 线程唤醒机制:若线程因队列空或无可用消息阻塞(block=true),插入队头消息后需通过nativeWake唤醒线程。
1) enqueueMessage方法
- handler.enqueueMessage功能:将当前handler赋值给message.target字段,确保消息最终由目标handler处理。
- 消息target字段检查
- MessageQueue校验规则:同步/异步消息的target字段不可为空,否则无法通过handler.dispatchMessage分发处理。
- 对头消息的判断及插入新消息操作
- 队头插入条件:队列为空、新消息触发时间为0,或新消息时间戳早于队头消息时间戳。
- 链表操作:新消息插入队头时,将其next指向原队头节点,并更新messages引用。
- 线程阻塞标记:needWake标记仅在队列空或无可用消息时为true,插入队头消息后需唤醒线程继续轮询。
- 新消息插入队列操作
- 异步消息唤醒条件:线程阻塞且队头为同步屏障消息(target=null)时,插入异步消息需唤醒线程。
- 遍历插入逻辑:通过无限循环找到首个时间戳大于新消息的节点,调整其前驱节点next指向新消息,新消息next指向后续节点。
3.postSyncBarrier同步屏障消息
- 同步屏障作用:标记队列中同步消息暂不执行,优先处理异步消息,常用于紧急任务(如UI测绘、生命周期事件)。
1) 同步屏障消息介绍
- 屏障消息特征:target=null,仅作为标记,不被执行。
- 系统级应用:仅系统源码可使用,如ViewRootImpl处理垂直同步信号时配合异步消息使用。
- 发送同步屏障消息方法
- 时间戳选择:使用SystemClock.uptimeMillis(设备运行时间,不含休眠时间)而非currentTimeMillis(可被系统设置修改)。
- 休眠影响:设备休眠期间,基于uptimeMillis的消息不会触发。
- 构建新消息体
- 屏障消息创建:通过Message.obtain构建,未赋值target字段,故无法被执行。
- 移除规则:由添加者(如ViewRootImpl)在异步任务完成后移除对应屏障消息。
消息的消费
1.消息分发
消息的分发依赖于队列中的路牌机制,通过该机制实现消息的传递与处理。
1) Looper类的loop方法
- loop方法通过无限循环实现消息的持续分发,确保线程不会退出。
- MessageQueue.next()方法获取可执行的Message对象,若队列中无消息,线程将进入阻塞状态。MessageQueue.next()方法的逻辑复杂,涉及消息的插入、删除及队列的动态调整。
- 消息分发优先级:获取消息后调用message.target.dispatchMessage()进行分发,处理完成后通过recycleUnchecked()回收消息。
2) MessageQueue类的next方法
- next方法通过无限循环检索消息,若未找到可处理的消息,线程将根据nextPollTimeoutMillis值进入阻塞状态(正数:定时阻塞;-1:无限阻塞)。
- 线程唤醒:新消息插入时(如Binder线程插入Activity生命周期消息)会唤醒阻塞线程。
- 延迟消息机制:通过SystemClock.uptimeMillis()计算消息执行时间,结合epoll命令实现阻塞超时后自动恢复检查。
- 同步屏障消息:若队列头部为同步屏障消息(target=null),则优先检索异步消息执行。
- 消息删除逻辑:
- 中间节点删除:调整前驱节点的next指向被删除节点的后继节点。
- 头节点删除:直接调整队列头指针指向后继节点。
- 空闲状态处理:在next方法中如果检测到空闲则会通过IdleHandler执行设置的小任务。使用IdleHandler的正确的方法 将启动过程中非紧急的主线程任务全部放进一个任务列表里,然后一个执行一个小任务,并切记单个消息耗时不要太长。
2.C++层代码分析
- native方法:Java层的nativePollOnce调用C++的Looper::pollOnce,最终通过Linux的epoll_wait监听文件变动(如屏幕输入、外设消息)。
- 线程唤醒:C++层执行完毕后调用awaken唤醒Java线程。
- 两套消息机制:Java与C++层的消息机制相互独立,Java层借助native方法实现线程阻塞能力。
AsyncTask
异步任务抽象类android.os.AsyncTask相对来说更轻量级一些,适用于简单的异步处理。
- 特点:轻量级异步任务工具类,提供任务执行进度回调给UI线程。
- 适用场景:需要知晓任务执行进度,多个任务串行执行。
- 缺点:生命周期与宿主不同步可能造成内存泄漏,默认串行执行任务。
三种执行方式:
- 子类复写方法(可计算进度时)
- AsyncTask.execute静态方法(串行执行),不需要进度。
- executeOnExecutor中使用THREAD_POOL_EXECUTOR并发线程池来并发执行,或使用AsyncTask.THREAD_POOL_EXECUTOR.execute静态方法并发执行。
使用AsyncTask时必须遵守如下规则:
- 必须在UI线程中创建AsyncTask的实例,以及调用AsyncTask的execute()方法。
- AsyncTask 的 onPreExecute()、onPostExecute(Result)、doInBackground (Params...params)、onProgressUpdate(Progress...values)方法,不应该主动调用,而是由Android系统调用。
- 每个AsyncTask只能被执行一次,多次调用将会引发异常。
在子线程中进行 UI 操作的方法
-
View 的 postInvalidate()方法。View通过mAttachInfo.mHandler发送持invalidate事件,源码如下:
public void postInvalidate() {
postInvalidateDelayed(0);
}
public void postInvalidateDelayed(long delayMilliseconds) {
// We try only with the AttachInfo because there's no point in invalidating
// if we are not attached to our window
if (mAttachInfo != null) {
Message msg = Message.obtain();
msg.what = AttachInfo.INVALIDATE_MSG;
msg.obj = this;
mAttachInfo.mHandler.sendMessageDelayed(msg, delayMilliseconds);
}
}
-
View.post(Runnable action):通过任意已附加到窗口的 View 发送 Runnable 到主线程消息队列,简单轻量,适合局部 UI 更新。 -
Handler(Looper.getMainLooper()):在主线程创建 Handler,子线程通过sendMessage()或post()发送消息/任务,由主线程的 Handler 处理。 -
Activity 的 runOnUiThread(
Runnable action):在 Activity 中调用,将 Runnable 投递到主线程执行,适用于有 Activity 上下文的场景。 -
现代替代方案(如 Kotlin 协程或
Executor+Handler):推荐使用withContext(Dispatchers.Main)(Kotlin 协程)或Handler配合线程池,避免手动管理线程。
异步不一定要切换线程,关键是要切换函数调用栈
handler.post、handler.send也是异步调用,虽然没有切换线程,但它同样因为调用栈的切换而一定程度上影响了我们对于逻辑执行的把控。也就是说,handler的执行体执行会晚于handler调用语句之后的语句,并且执行于handler对应的looper线程中,looper线程也可能就是调用所在的线程。异步不一定要切换线程,关键是要切换函数调用栈。异步与切换线程的关系:一旦线程发生切换,那么函数调用栈必然切换,自然就产生了异步操作,而异步不一定需要切换线程,因此切换线程是异步的充分不必要条件。

313

被折叠的 条评论
为什么被折叠?



