写点什么

Android C++ 系列:JNI 中的 Handler--ALooper

作者:轻口味
  • 2022 年 1 月 27 日
  • 本文字数:2953 字

    阅读完需:约 10 分钟

Android C++系列:JNI中的Handler--ALooper

1. Android Handler 回顾

在 Android 中,UI 线程是一个很重要的概念。我们在日常开发中对 UI 的更新和一些系统行为,都必须在 UI 线程(主线程)中进行调用。我们在子线程更新 UI 时最常用的手段就是 Handler,Handler 的主要原理:


主要是有一个 Looper 不停的从队列读消息,子线程通过持有 Handler 向队列写消息,以此来实现线程通信。但让 Looper 线程不一定是主线程,子线程也可以通过Looper.prepare();来创建 Looper,构建 Handler 时可以将 Looper 传入到 Handler 构造方法来和 Looper 绑定。

2. JNI 中实现 Looper

理论上我们日常开发中不会涉及 JNI 中更新 UI 的问题,就算需要也可以回调到 Java 层,在 Java 层切换。但是当我们遇到很多线程需要回调 JNI,而 JNI 线程回调 Java 需要通过 JavaVM 来创建 JNIEnv,每个线程都来 AttachCurrentThread 会带来性能上的开销,我们会想都通过一个线程回调 Java 来解决这个问题,这个时候是不是就开始怀念 Java 的 Handler 了?


我们可以手动实现一个队列来实现一个线程回调 Java:


template <typename T>class BlockingQueue{public:    BlockingQueue()            :m_mutex(),             m_condition(),             m_data()    {    }
// 禁止拷贝构造 BlockingQueue(BlockingQueue&) = delete;
~BlockingQueue() { }
void push(T&& value) { // 往队列中塞数据前要先加锁 std::unique_lock<std::mutex> lock(m_mutex); m_data.push(value); m_condition.notify_all(); }
void push(const T& value) { std::unique_lock<std::mutex> lock(m_mutex); m_data.push(value); m_condition.notify_all(); }
T take() { std::unique_lock<std::mutex> lock(m_mutex); while(m_data.empty()) { m_condition.wait(lock); } assert(!m_data.empty()); T value(std::move(m_data.front())); m_data.pop();
return value; }
size_t size() const { std::unique_lock<std::mutex> lock(m_mutex); return m_data.size(); }private: // 实际使用的数据结构队列 std::queue<T> m_data;
// 条件变量的锁 std::mutex m_mutex; std::condition_variable m_condition;};
复制代码


在 JNI 线程中读队列:


void callbackWorkThread() {  JNIEnv *recJniEnv;    if (javaVM->AttachCurrentThread(&recJniEnv, NULL) != JNI_OK) {        LOGE("java VM AttachCurrentThread failed");        return;    }    while (isWorking) {        Event *value = blockingqueue->take();        //xxxxx    }}
复制代码


除了自己实现有没有其他办法?

3. ALooper

JNI 中为我们提供了 ALooper,在头文件looper.h中,ALooper 的创建过程:


mainlooper = ALooper_prepare(0);int ret = ALooper_addFd(mainlooper, readpipe, 1, ALOOPER_EVENT_INPUT, handle_message, NULL);
复制代码


下面我们看看这两个方法的具体说明:


/** * Prepares a looper associated with the calling thread, and returns it. * If the thread already has a looper, it is returned.  Otherwise, a new * one is created, associated with the thread, and returned. * * The opts may be ALOOPER_PREPARE_ALLOW_NON_CALLBACKS or 0. */ALooper* ALooper_prepare(int opts);
复制代码


通过注释,我们可以看到,ALooper_prepare会准备一个 looper 并关联到被调用线程。如果当前线程已经有 Looper 则直接返回,如果没有则创建并返回。由于我们是在主线程对 MainLooper 进行的初始化,主线程默认会创建 Looper,所以直接返回的主线程的 looper。


接下来再来看一下ALooper_addFd方法:


/** * Adds a new file descriptor to be polled by the looper. * If the same file descriptor was previously added, it is replaced. * * "fd" is the file descriptor to be added. * "ident" is an identifier for this event, which is returned from ALooper_pollOnce(). * The identifier must be >= 0, or ALOOPER_POLL_CALLBACK if providing a non-NULL callback. * "events" are the poll events to wake up on.  Typically this is ALOOPER_EVENT_INPUT. * "callback" is the function to call when there is an event on the file descriptor. * "data" is a private data pointer to supply to the callback. * * There are two main uses of this function: * * (1) If "callback" is non-NULL, then this function will be called when there is * data on the file descriptor.  It should execute any events it has pending, * appropriately reading from the file descriptor.  The 'ident' is ignored in this case. * * (2) If "callback" is NULL, the 'ident' will be returned by ALooper_pollOnce * when its file descriptor has data available, requiring the caller to take * care of processing it. * * Returns 1 if the file descriptor was added or -1 if an error occurred. * * This method can be called on any thread. * This method may block briefly if it needs to wake the poll. */int ALooper_addFd(ALooper* looper, int fd, int ident, int events,        ALooper_callbackFunc callback, void* data);
复制代码


这里面用到了文件描述符,我们出于效率考虑,不会直接使用对应 SD 卡的文件,而是使用管道,管道一端负责写入,管道另一端会在 looper 所在的线程中,当监测到 fd 变化时,调用 callback 方法。


通过初始中的这样两个方法,我们就构建了一条通往主线程的通道。

3. 具体示例

在初始化的方法中,我们构筑了一条消息通道。接下来,我们就需要将消息发送至主线程。


void MainLooper::init() {
int msgpipe[2]; pipe(msgpipe);//管道知识之前系列做过介绍 readpipe = msgpipe[0]; writepipe = msgpipe[1];
mainlooper = ALooper_prepare(0); int ret = ALooper_addFd(mainlooper, readpipe, 1, ALOOPER_EVENT_INPUT, MainLooper::handle_message, NULL);}


int MainLooper::handle_message(int fd, int events, void *data) {
char buffer[LOOPER_MSG_LENGTH]; memset(buffer, 0, LOOPER_MSG_LENGTH); read(fd, buffer, sizeof(buffer)); LOGD("receive msg %s" , buffer); Toast::GetInstance()->toast(buffer); return 1;}

void MainLooper::send(const char *msg) {
pthread_mutex_lock(&looper_mutex_); LOGD("send msg %s" , msg); write(writepipe, msg, strlen(msg)); pthread_mutex_unlock(&looper_mutex_);}
复制代码


上面这种写法读固定长度的 buffer 会有粘包问题,即多个线程写,looper 中读到的内容可能是错乱的,这时候我们应该指定通信协议,比如头两个字节做 Header 存放长度。

4. 总结

本文回顾了 Android 传统 Handler 机制,以及在 JNI 中实现 Looper 和 JNI 提供的 ALooper 的使用方式和技巧:使用管道来实现线程通信,并通过自定义通信协议来解决粘包问题。

发布于: 刚刚阅读数: 2
用户头像

轻口味

关注

🏆2021年InfoQ写作平台-签约作者 🏆 2017.10.17 加入

Android、音视频、AI相关领域从业者。 欢迎加我微信wodekouwei拉您进InfoQ音视频沟通群 邮箱:qingkouwei@gmail.com

评论

发布
暂无评论
Android C++系列:JNI中的Handler--ALooper