在Android的mediaserver代码中,需要处理复杂的时序问题,和多个模块间的调用依赖。为了解决这些问题,使用了Looper+Handler的消息处理机制。
消息机制可以认为是一种“多线程”的编程模式,其优点在于:
- 简化多线程问题。消息机制中,一般会有一个线程负责消息的读取、消息派发。其他线程只管生产请求/消息,传递给处理线程即可。因为消息的处理被集中在单一线程内,因此,一般情况下可忽略多线程问题,以单线程的思维组织代码。
- 异步处理。用户动作的产生是不确定的,存在峰值。如果提供同步函数处理用户请求,可能导致卡顿。消息机制可以异步处理用户请求,避免该类问题。
- 时序可控。由于消息会放入队列,然后单线程处理,所以得益于队列的FIFO特性,可以很严格地控制消息的处理顺序。
当然,也会引入一些缺点:
- 消息堆积。消息堆积的后果是,可能导致处理者(Handler)在非预期的状态下处理消息。
- 非实时。消息的处理的不一定是实时的(被排队)
参考资料: https://www. oschina.net/translate/t op-10-uses-for-message-queue
下面来剖析下android mediaserver中libstagefright的消息机制实现方案。
头文件在:android/frameworks/av/include/media/stagefright/foundation
源文件在:frameworks/av/media/libstagefright/foundation
类的设计
几个主要的类:
- ALooper:looper一般与一个线程对应,要么在已有的线程上启动,要么新建一个线程启动。looper主要负责消息的派发和提供线程环境用于消息的执行
- AHandler:handler中主要定义了onMessageReceived这一纯虚方法,要求子类实现。looper消息派发时会回调这个方法,handler子类在该方法内根据消息类型进行处理。
- AMessage:message表示一条消息,消息可以携带一些自定义字段。同时在AMessage上设计了
post
族的方法用于发送消息。消息只能被发往一个Handler处理。 - AReplyToken:这类似一个标识,表示要回复的是哪一条消息
用法
启动ALooper:
ALooper* looper = new ALooper;
looper->start();
实现AHandler:
struct MyHandler: public AHandler {
protected:
virtual void onMessageReceived(const sp<AMessage> &msg){
switch(msg->what()) {
case 1:
break;
……
}
}
};
MyHandler* handler = new MyHandler;
looper->registerHandler(handler);
发送消息:
AMessage* msg = new AMessage(1, handler);
msg->setInt32("extra", 3);
msg->post();
发送并等待消息回复(等待消息处理完成):
AMessage* msg = new AMessage(1, handler);
msg->setInt32("extra", 3);
AMessage response;
msg->postAndAwaitResponse(&response);
response在handler中设置:
//in onMessageReceived(const sp<AMessage> &msg)
sp<AMessage> response = new AMessage;
response->setInt32("err", err);
sp<AReplyToken> replyID;
if(msg->senderAwaitsResponse(&replyID)){
response->postReply(replyID);
}
实现分析
内存管理
android源码中自己实现了一套c++的垃圾回收机制。参考:https://blog.csdn.net/u012124438/article/details/71075423
为方便阅读,提炼几个要点:
- 其垃圾回收机制主要原理是“引用计数”
- 继承RefBase就可以接入垃圾回收机制
- sp: strong pointer,强引用,引用计数归零时将被释放
- wp: weak pointer,弱引用,不影响对象的释放。需要通过
promote
获取sp才能调用对象方法
ALooper、AHandler和AMessage都继承自RefBase。
下面图示标明了这3个类之间的引用关系:
虚线表示弱引用,实现表示强引用
箭头方向表示A持有B,如ALooper持有AMessage的强引用。
基于引用计数的内存管理最大的缺陷在于”循环引用“。因为循环引用会导致两者始终都处于计数不为0的状态,从而无法被释放。
所以,在引用关系的设计上,作者尽可能地使用了弱引用,只在必要时使用强引用。
ALooper和AHandler属于长生命周期对象,因此在设计上没有对其持有强引用的情况(在需要调用其方法的时候,会短暂promote为强引用),这样方便looper和handler实例灵活管理自己的生命期。
AMessage属于短生命周期对象,使用上基本是即用即创建。比如:
void NuPlayer::pause() {
(new AMessage(kWhatPause, this))->post();
}
所以,需要一个对象来持有它的强引用,设计上作者将这个职责分配给了ALooper(中的队列)
postAndAwaitResponse分析
接下来我们以AMessage::postAndAwaitResponse
函数为例分析源码。
如前所述,这个函数会把消息发送给ALooper,并阻塞当前线程直到AHandler回复了该消息。
源码如下:
status_t AMessage::postAndAwaitResponse(sp<AMessage> *response) {
//1. 判断looper是否还在工作,并获取强引用
sp<ALooper> looper = mLooper.promote();
if (looper == NULL) {
ALOGW("failed to post message as target looper for handler %d is gone.", mTarget);
return -ENOENT;
}
//2. 创建reply token
sp<AReplyToken> token = looper->createReplyToken();
if (token == NULL) {
ALOGE("failed to create reply token");
return -ENOMEM;
}
setObject("replyID", token);
//3. 发送消息
looper->post(this, 0 /* delayUs */);
//4. 等待回复
return looper->awaitResponse(token, response);
}
主要步骤:
- 判断looper是否还在工作,并获取强引用
- 创建reply token。token以名字replyID保存在当前消息内
- 发送消息
- 等待回复
接下来看如何”等消息“:
status_t ALooper::awaitResponse(const sp<AReplyToken> &replyToken, sp<AMessage> *response) {
// return status in case we want to handle an interrupted wait
Mutex::Autolock autoLock(mRepliesLock);
CHECK(replyToken != NULL);
while (!replyToken->retrieveReply(response)) {
{
Mutex::Autolock autoLock(mLock);
if (mThread == NULL) {
return -ENOENT;
}
}
mRepliesCondition.wait(mRepliesLock);
}
return OK;
}
典型的条件变量的用法——在mutex保护的情况下等待条件变量唤醒。
条件变量的唤醒时机是AMessage::postReply => ALooper::postReply
:
status_t ALooper::postReply(const sp<AReplyToken> &replyToken, const sp<AMessage> &reply) {
Mutex::Autolock autoLock(mRepliesLock);
status_t err = replyToken->setReply(reply);
if (err == OK) {
mRepliesCondition.broadcast();
}
return err;
}
AReplyToken扮演了两个角色,分别是唯一标识,和回复的AMessag的容器。可以通过setReply/retrieveReply
操作”容器“。
以上,postAndAwaitResponse
基本分析完了。有种”好像也不过如此“的感觉……然而,并不是。
之所以实现看起来会很简单直白,与类的设计、职责划分、内存管理这三者的设计密不可分。只有把这些搞清楚了,代码才能如此清晰。可以做个练习,抛开这里的设计,读者可以尝试自行实现一个消息机制,就知道其中的精妙了。
如果你够精明,会发现一个问题:可能在wait的消息不只一个,为何只设计了一组Mutex/Condition呢?这样岂不是在mRepliesCondition.broadcast
时会唤醒所有等待中的线程,而非唤醒目标线程?
我的理解是因为在stagefright的使用场景中,同时在等待回复消息的线程并不多(不超过5个),而且awaitResponse
会在唤醒后检查条件是否满足,不满足时继续等待。因此,即使同时唤醒也消耗不大。好处是,可以只用一组Mutex/Condition,而不是为所有的消息都去创建(和销毁)Mutex/Condition。
至此,stagefright的消息机制实现就分析完了。可以说是一个轻量而灵活的实现方案。
以上分析的源码,经过重构后,只依赖c++11,压缩在一个头文件,一个源文件中。可跨平台使用。见:
compilelife/aloopgithub.com