大家在项目中肯定都碰到过在子线程中需要刷新 UI 界面的需求,对于大多数实现了跨线程消息通信的 GUI 框架来说,开发者要实现这点就很容易,但当 GUI 没有提供跨线程消息机制的时候,就需要我们自己造一个了。
下面是我最近在项目中实现的一个跨线程通信消息队列,主要设计参考了 Android 的 Handler。不同点是 Android 主线程是完全消息驱动的,在没有消息时会阻塞,而我们的项目中主线程除了管理 UI 外,还有其他工作,所以不能采用这种阻塞模型,而是使用的定时询问。
好了,下面我们一起看看具体实现吧。
1、设计目的及思路
该消息队列是为了实现跨线程消息通信。考虑如下场景:
现有线程 1 和线程 2 两个线程,线程 1 负责刷新 UI,现在线程 2 也想刷新 UI,然而它不能自己去刷新,得委托线程 1 来干这个活。没问题,但线程 1 不知道 UI 具体有哪些变动,只有线程 2 才清楚,所以怎么刷还得线程 2 去定义,线程 1 只负责调用。
添加图片注释,不超过 140 字(可选)
这个刷新 UI 的过程可以总结为:
- 线程 2 发送刷新 UI 的消息给线程 1;
- 线程 1 调用线程 2 定义的方法来刷新。
需求就是这么简单一回事,怎么实现呢?
- 我们需要一个组件发消息,线程 2 通过这个组件能把消息发送给线程 1;
- 消息里面有什么,肯定得有消息 id,然后还得告诉线程 1 具体的刷法,所以可以一并放在消息里。
- 线程 1 有一个处理消息的组件,什么时候处理消息呢?可以一直等待着,有消息过来就处理,没有就干等;还可以定期检查是否有消息过来,有就进行处理,没有就继续干别的事。这里就要按自身的需求选择了,我们的线程 1 还有别的活,所以不能干等,得采用定期检查的方式。
我是参照的 Android Handler 的设计,命名也就直接延用过来。
- 发送消息的组件叫做 Handler;
- 处理消息的组件叫做 Looper;
- 消息就叫 Message,哪都是。
2、具体的设计
2.1 Message 的设计
我们刚刚已经分析出,Message 需要包括消息 id 和该消息的具体处理方法两部分。大部分时候是够用的,但有时间用户可能想放点数据,在处理的时候使用,所以还要添加一项 userDate。
好,Message 的主要部分就这些东西了。
实际项目中,为了避免内存的频繁创建和释放,尤其是消息这种短生命周期的对象,常常会采用消息池的设计。我们也搞个简单的消息池,就直接在 Message 里定义两个静态方法就好了,obtainMessage() 用于拿取消息对象,freeMessage() 用于释放消息对象,释放不是真释放,只是打个 flag 而已,那么显然,我们的消息还需要加上一个 flag 成员用于表示是否释放。
添加图片注释,不超过 140 字(可选)
消息类的定义如上,有同学可能有疑问了。
不是说消息类要包含对该消息的具体处理方法嘛,上面似乎没有?
再看看,你们肯定也发现了有一个 Handler* 类型的成员变量,我们之前说过 Handler 是消息的发送者,并且它知道具体怎么处理消息。是不是明白了,放一个 Handler 就已经能知道怎么处理该消息了。
Message.h
#ifndef MESSAGE_H
#define MESSAGE_H
#include <list>
#include <mutex>
class Handler;
class Message {
public:
Message();
static Message* obtainMessage();
static void freeMessage(Message* msg);
int id;
void* userDate;
Handler* mHandler;
private:
bool free;
static std::list<Message*> msgPool;
static int poolSize;
static std::mutex poolMutex;
};
#endif // MESSAGE_H
Message.cpp
#include "Message.h"
#include <iostream>
std::list<Message*> Message::msgPool;
int Message::poolSize = 20;
std::mutex Message::poolMutex;
Message::Message() : free(true) {
}
Message* Message::obtainMessage() {
poolMutex.lock();
auto it = msgPool.begin();
for (; it != msgPool.end(); it++) {
if ((*it)->free) {
(*it)->free = false;
poolMutex.unlock();
return *it;
}
}
if (msgPool.size() < poolSize) {
Message *msg = new Message();
msg->free = false;
msgPool.emplace_back(msg);
poolMutex.unlock();
return msg;
}
poolMutex.unlock();
printf("no free msg, obtain fail");
return nullptr;
}
void Message::freeMessage(Message* msg) {
poolMutex.lock();
msg->free = true;
poolMutex.unlock();
}
这里的内存池大小为 32,是因为我在 Looper 中定义的消息队列最大长度为 16,32 能够让两个线程同时拥有全满的消息队列,在实践中已经足够了。
2.2、Handler 的设计
你可能在前面就想知道 Handler 是怎么设计的了,之前我们分析过,Handler 有两个主要作用。
- 用来发送消息;
- 管理具体怎么处理消息。
所以 Handler 的成员方法里肯定有一个用来发送消息的方法,还有一个用来处理消息的方法。Handler 怎么把消息发送给接收者 Looper 呢?那必然得持有一个 Looper 了,这样就知道发送给谁了。
添加图片注释,不超过 140 字(可选)
Handler.h
#ifndef HANDLER_H
#define HANDLER_H
#include "Looper.h"
class Handler {
public:
Handler();
Handler(Looper* looper);
virtual ~Handler();
void sendMessage(int id, void* userDate);
virtual void handleMessage(int id, void* userDate) = 0;
private:
Looper* mLooper;
};
#endif // HANDLER_H
Handler.cpp
#include "Handler.h"
#include "Message.h"
#include <iostream>
#include <thread>
Handler::Handler() : mLooper(nullptr) {
Looper* looper = Looper::getLooper();
if (!looper) {
printf("[%s] fail, cur thread dose not exist looper\n", __func__);
} else {
mLooper = looper;
}
}
Handler::Handler(Looper* looper) : mLooper(looper) {
}
Handler::~Handler() {}
void Handler::sendMessage(int id, void* userDate) {
std::cout<<"["<<__func__<<"]"<<"sendMessage:"<<id
<<", thread:"<<std::this_thread::get_id()<<std::endl;
if (!mLooper) {
printf("fail, no looper");
return;
}
Message* msg = Message::obtainMessage();
if (!msg) {
printf("obtain message fail");
return;
}
msg->id = id;
msg->userDate = userDate;
msg->mHandler = this;
mLooper->addMsg(msg);
}
有几个点值得说一下:
- Handler 有两个构造方法。当传入一个 Looper 对象的时候,Handler 将和传入的 Looper 绑定,后续发消息就会发给这个 Looper;而当未传人参数时,Handler 就会绑定当前线程的 Looper,如果当前线程不存在 Looper,就会绑定失败;
- handleMessage() 就是具体的怎么处理消息的方法,但在这里是个纯虚方法,具体实现要交给子类来完成。因为我们不知道使用者需要处理什么消息,以及怎么处理每个消息,所以使用者需要定义自己的 Handler 继承自当前类,并重写 handleMessage() 方法。
我们看下我实现的 Handler 具体类。
MyHandler.h
#ifndef MYHANDLER_H
#define MYHANDLER_H
#include "Handler.h"
class MyHandler : public Handler {
public:
MyHandler();
MyHandler(Looper* looper);
void handleMessage(int id, void* userDate) override;
};
#endif //MYHANDLER_H
MyHandler.cpp
#include "MyHandler.h"
#include <iostream>
MyHandler::MyHandler() : Handler() {}
MyHandler::MyHandler(Looper* looper) : Handler(looper) {}
void MyHandler::handleMessage(int id, void* userDate) {
std::cout<<"["<<__func__<<"]"<<"handleMessage:"<<id
<<", thread:"<<std::this_thread::get_id()<<std::endl;
}
我们重写了 handleMessage() 这个方法,在里面打印了消息的 id,具体实践中当然可以根据消息 id 作出不同的处理逻辑。
2.3 Looper 的设计
我们说 Looper 是处理消息的组件,并采用定时查询的方式,处理 Handler 传过来的消息,那一个 Looper 必须要包含以下几点:
- 一个保存消息的队列,Handler 把消息放到这个队列里,当 Looper 定时查看发现队列里有消息就会去处理;
- 刚刚说 Handler 把消息放到队列里,那怎么放呢?这,显然是 Looper 要提供一个把消息放进队列里的方法,不然 Handler 怎么放呢。
- 定时查询怎么做呢?最简单的就是放一个 timer 成员,每隔一定时间就去处理消息队列里的消息。至于这个 timer 怎么搞,如果你使用的 GUI 提供了这个机制就很方便,如果没有呢?那我们有机会再聊怎么自己做一个,跟本文的主旨关系不是那么密切。
- 好,到此 Looper 包含的要素已经齐全了。作的好一点,我们可以再包装一个处理消息队列里消息的方法,这样 timer 每次到时间后就调用这个方法就好了。
- 有没有什么遗漏呢?我们的这套机制提出来是用来做跨线程通信的,接收消息的线程需要实现这么一个 Looper,怎么实现呢?用户并不想关心怎么实现,我们得提供这样一个接口;此外,发送消息的 Handler 也需要一个 Looper,我们得提供另一个接口用来获取已经实现的 Looper(并不是创建新的 Looper)。
- 补充,本条写在全文完结后。每个线程可以开启 Looper 循环,那么也可以选择结束 Looper,所以还应该提供一个静态方法,让线程注销其 Looper,操作就是从 map 中移除该线程对应的 Looper,当然别忘了释放相应的内存。我就偷个懒,只在这里说明一下吧,全文就不动了。
添加图片注释,不超过 140 字(可选)
Looper.h
#ifndef LOOPER_H
#define LOOPER_H
#include "Message.h"
#include <deque>
#include <unordered_map>
#include <thread>
#include <mutex>
class Looper {
public:
void addMsg(Message* msg);
static void prepareLooper(bool isUiLooper = false);
static Looper* getMainLooper();
static Looper* getLooper();
private:
void dispatchMessage();
void prePare();
std::deque<Message*> msgQue;
std::mutex mMutex;
static int queMaxsize;
static std::mutex looperMutex;
static std::unordered_map<std::thread::id, Looper*> threadLooper;
static std::thread::id mainThread;
};
#endif // LOOPER_H
Looper.cpp
#include "Looper.h"
#include "Handler.h"
#include <iostream>
#include <chrono>
int Looper::queMaxsize = 16;
std::mutex Looper::looperMutex;
std::unordered_map<std::thread::id, Looper*> Looper::threadLooper;
std::thread::id Looper::mainThread;
void Looper::dispatchMessage() {
while (!msgQue.empty()) {
mMutex.lock();
auto msg = msgQue.front();
msgQue.pop_front();
mMutex.unlock();
msg->mHandler->handleMessage(msg->id, msg->userDate);
Message::freeMessage(msg);
}
printf("[%s] messageque is empty\n",__func__);
}
void Looper::addMsg(Message* msg) {
mMutex.lock();
if (msgQue.size() == queMaxsize) {
printf("[%s] drop message : %d\n",__func__, msgQue.front()->id);
Message::freeMessage(msgQue.front());
msgQue.pop_front();
}
msgQue.emplace_back(msg);
mMutex.unlock();
}
void Looper::prepareLooper(bool isUiLooper) {
looperMutex.lock();
if (threadLooper.find(std::this_thread::get_id()) != threadLooper.end()) {
printf("[%s]the looper is already in work", __func__);
looperMutex.unlock();
return;
}
Looper* looper = new Looper();
threadLooper.emplace(std::pair<std::thread::id, Looper*>(std::this_thread::get_id(), looper));
if (isUiLooper) {
mainThread = std::this_thread::get_id();
}
looperMutex.unlock();
looper->prePare();
}
void Looper::prePare() {
printf("[%s] succeed, looper is in work now\n", __func__);
/*
* 由于 C++ 没有现成的 timer,所以这里就用 sleep 模拟每隔一段时间的轮询
* 1600 ms,这里是为了后面展示才设慢的。
*/
while (true) {
std::this_thread::sleep_for(std::chrono::milliseconds(1600));
dispatchMessage();
}
}
Looper* Looper::getMainLooper() {
looperMutex.lock();
auto looper = threadLooper.find(mainThread)->second;
looperMutex.unlock();
return looper;
}
Looper* Looper::getLooper() {
looperMutex.lock();
auto looperPair = threadLooper.find(std::this_thread::get_id());
if (looperPair == threadLooper.end()) {
looperMutex.unlock();
return nullptr;
}
looperMutex.unlock();
return looperPair->second;
}
- prepareLooper() 函数提供了给当前线程创建 Looper 对象的接口,创建后存放到 unordered_map 中,并能保证一个线程最多只有一个 Looper 存在。此处有一个 isUiLooper 参数,用来区分是否为创建主线程 Looper。
- getMainLooper() 函数获取主线程的 Looper,getLooper() 获取当前线程的 Looper。
- prePare() 方法中采用了 sleep() 的方式来模拟每隔一段时间(1.6 秒) Looper 进行一次消息查询,这里本应该是创建一个 timer,在 timer 里周期性的查询,所以只是为了简单才使用了 sleep()。后续有机会的话,我们会讨论怎么自己实现一个 timer。
2.4 总的设计
添加图片注释,不超过 140 字(可选)
从上图看,这三个类成了相互持有的关系,那我们在每个类析构的时候应该做些什么呢?
- 当 Message 类对象析构时,它虽然持有了 Handler 类对象的指针,但 Handler 对象是否释放,是由使用者来决定的,不能因为消息没了,就要把发送消息的人也干掉,那不科学。那是否应该通知持有它的 Looper 呢?也不用,其实,我们使用了消息池来管理消息,压根不用考虑消息的释放问题,每次都是改变 free 这个标识,消息的真正释放是在整个系统结束时,此时 Looper 和 Handler 会先一步释放。
- 当 Handler 类对象析构时,要考虑到 Handler 是被 Message 类持有的,如果 Handler 对象已经释放了,但是消息队列中还存放有需要该对象去处理的消息,那么当调用到该消息的 Handler 对象时,就会发生地址错误。所以,当 Handler 对象释放时,需要通知到它持有的 Looper 对象,对应的 Looper 对象就会去检查它的消息队列,将含有要释放的 Handler 的消息全部标记为 free,也就是已经被释放。
- 当 Looper 对象析构时,它要清空它的消息队列。不仅如此,它还要通知到它通过消息队列间接持有的 Handler 对象,它要释放了,以后就别发消息给它了。Handler 收到后,会将其的 mLooper 指针置为空,后续可能会选择紧跟着析构。
上面所考虑的情形,我并没有在代码中去实现,不然篇幅有点过长,重点就不突出了,毕竟我们主要讲的是跨线程消息通信,有兴趣的朋友可以试一下。
3、跨线程消息组件的测试
#include "Handler.h"
#include "MyHandler.h"
#include <iostream>
#include <thread>
#include <chrono>
void uiThread() {
std::cout<<"ui thread:"<<std::this_thread::get_id()<<std::endl;
Looper::prepareLooper(true);
}
void taskThread() {
std::cout<<"task thread:"<<std::this_thread::get_id()<<std::endl;
Handler* handler = new MyHandler(Looper::getMainLooper());
while (true) {
static int id = 1;
std::this_thread::sleep_for(std::chrono::milliseconds(50));
handler->sendMessage(id, nullptr);
id++;
}
}
int main() {
printf("messageque test\n");
std::thread ui(uiThread);
std::this_thread::sleep_for(std::chrono::milliseconds(5000));
std::thread task(taskThread);
ui.join();
task.join();
}
我们在 main() 方法里先创建了 ui 线程,UI 线程启动后会初始化 Looper,准备接收来自其他线程的消息;
然后在 5 秒之后,创建了一个 task 线程,在该线程里会创建一个和 ui 线程绑定的 Handler,每隔 50 ms 向 ui 线程发送一次消息。
我们看一下跑的结果。
添加图片注释,不超过 140 字(可选)
发现确实做到了在 task 线程发送消息,而在 ui 线程中处理消息。当然上述测试只用了一个 task 线程,也可以用多个 task 线程向主线程发送消息进行测试。
4、总结
本文实现了一个跨线程通信的消息组件,设计目的是在子线程中更像 ui 界面,该组件主要由发送者(Handler)、接收者兼消息循环者(Looper)、消息(Message)三部分组成。Handler 负责在子线程中发送消息,并且知道怎么具体处理某个消息;Looper 会定时检查消息队列中的消息,并把该消息交给对应的 Handler 来处理;Message 就是信息的载体。
本文已经同步至个人公众号 难念的码,欢迎关注交流。