头图

1. Android Handler回顾

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

image.png

主要是有一个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的使用方式和技巧:使用管道来实现线程通信,并通过自定义通信协议来解决粘包问题。


轻口味
16.9k 声望3.9k 粉丝

移动端十年老人,主要做IM、音视频、AI方向,目前在做鸿蒙化适配,欢迎这些方向的同学交流:wodekouwei