Android NDK中的UI线程

作者: oceanLong | 来源:发表于2018-10-29 17:40 被阅读7次

    概述

    在Android中,UI线程是一个很重要的概念。我们对UI的更新和一些系统行为,都必须在UI线程(主线程)中进行调用。

    同时,我们在进行底层跨平台开发时,我们会选择NDK,在Linux系统上进行开发。在Linux中是没有主线程这一概念的。

    那么,如果我们在子线程调用了一个native方法,在C++的代码中,我们想要切换到主线程调用某个方法时,该如何切换线程呢?

    需求

    众所周知,Toast消息,是无法在子线程调用的。如果我们在子线程中执行C++的代码,此时想调用toast方法,该如何是好呢?

                    final String s = mEditTest.getText().toString();
                    for (int i = 0 ; i < 3 ; i++){
                        new Thread(new Runnable() {
                            @Override
                            public void run() {
                                nativeToast(s);
                            }
                        }).start();
                    }
    
        public native void nativeToast(String text);
    
        public static void toast(String text){
            Toast.makeText(MyAppImpl.getAppContext(), text, Toast.LENGTH_SHORT).show();
        }
    

    在上面的代码中,native层的nativeToast其实就是调用了Java层的toast方法。只是在调用之前,做了线程的转换,在C++层的主线程调用了toast。

    实现

    初始化

    MainActivity.java

        static {
            System.loadLibrary("native-lib");
        }
    
        Button mBtnTest;
        EditText mEditTest;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            init();
            mBtnTest = findViewById(R.id.test_btn);
            mEditTest = findViewById(R.id.test_input);
            mBtnTest.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
    
                    final String s = mEditTest.getText().toString();
                    for (int i = 0 ; i < 3 ; i++){
                        new Thread(new Runnable() {
                            @Override
                            public void run() {
                                nativeToast(s);
                            }
                        }).start();
                    }
                }
            });
    
        }
    
        public native void init();
    

    native-lib.cpp

    #include <jni.h>
    #include <string>
    #include "main_looper.h"
    #include "jvm_helper.h"
    
    extern "C"
    {
    
    JNIEXPORT void JNICALL
    Java_com_example_oceanlong_ndkmaintest_MainActivity_init(JNIEnv *env, jobject instance) {
    
        JniHelper::setJVM(env);
        MainLooper::GetInstance()->init();
        LOGD("init env : %p", env);
    
    
    }
    
    JNIEXPORT void JNICALL
    Java_com_example_oceanlong_ndkmaintest_MainActivity_nativeToast(JNIEnv *env, jobject instance,jstring text_) {
        const char* ctext = JniHelper::jstr2char(env, text_);
        LOGD("nativeToast  : %s", ctext);
        MainLooper::GetInstance()->send(ctext);
        env->ReleaseStringUTFChars(text_, ctext);
    
    }
    
    
    }
    
    

    初始化的代码中,其实只做了两件事情:

    • 缓存一个全局的JNIEnv *
    • 初始化native的looper

    初始化必须在主线程中执行!

    MainLooper的初始化

    main_looper.h

    #include <android/looper.h>
    #include <string>
    #include "logger.h"
    
    class MainLooper
    {
    public:
        static MainLooper *GetInstance();
        ~MainLooper();
        void init();
        void send(const char* msg);
    
    private:
        static MainLooper *g_MainLooper;
        MainLooper();
        ALooper* mainlooper;
        int readpipe;
        int writepipe;
        pthread_mutex_t looper_mutex_;
        static int handle_message(int fd, int events, void *data);
    };
    

    main_looper.cpp

    #include <fcntl.h>
    #include "main_looper.h"
    #include <stdint.h>
    #include "string.h"
    #include <stdlib.h>
    #include <unistd.h>
    #include "toast_helper.h"
    
    #define LOOPER_MSG_LENGTH 81
    
    MainLooper *MainLooper::g_MainLooper = NULL;
    
    
    MainLooper *MainLooper::GetInstance()
    {
        if (!g_MainLooper)
        {
            g_MainLooper = new MainLooper();
        }
        return g_MainLooper;
    }
    
    MainLooper::MainLooper(){
        pthread_mutex_init(&looper_mutex_, NULL);
    }
    
    MainLooper::~MainLooper() {
        if (mainlooper && readpipe != -1)
        {
            ALooper_removeFd(mainlooper, readpipe);
        }
        if (readpipe != -1)
        {
            close(readpipe);
        }
        if (writepipe != -1)
        {
            close(writepipe);
        }
        pthread_mutex_destroy(&looper_mutex_);
    }
    
    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;
    }
    

    初始化中,最关键的两句话是:

    mainlooper = ALooper_prepare(0);
        int ret = ALooper_addFd(mainlooper, readpipe, 1, ALOOPER_EVENT_INPUT, MainLooper::handle_message, NULL);
    

    looper.h

    /**
     * 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。由于我们是在主线程对MainLooper进行的初始化,返回的也是主线程的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);
    

    我们需要的用法简而言之就是,fd监测到变化时,会在looper所在的线程中,调用callback方法。

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

    发往主线程

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

    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_);
    }
    
    

    首先我们可以看到,在init方法中,我们创建了通道msgpipe。将readpipe加入了ALooper_addFd中。

    所以,我们接下来只需要对writepipe进行写入,即可将消息发送至主线程。

    MainActivity.java

            mBtnTest.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
    
                    final String s = mEditTest.getText().toString();
                    new Thread(new Runnable() {
                        @Override
                        public void run() {
                            nativeToast(s);
                        }
                    }).start();
    
                }
            });
    

    从日志中,我们已经可以看到,receive msg input:123表示,我们已经收到了子线程的消息,并调用了handle_message方法。

    调用toast

    我们在这个方法中,调用toast方法:

    toast_helper.cpp

    #include "toast_helper.h"
    #include "jvm_helper.h"
    #include "logger.h"
    
    Toast *Toast::g_Toast = NULL;
    
    Toast *Toast::GetInstance() {
        if (!g_Toast){
            g_Toast = new Toast();
        }
        return g_Toast;
    }
    
    void Toast::toast(std::string text) {
        JNIEnv *env = JniHelper::getJVM();
        LOGD("toast env : %p", env);
    
        jstring jtext = JniHelper::char2jstr(text.c_str());
    
        jclass javaclass = JniHelper::findClass(env,"com/example/oceanlong/ndkmaintest/MainActivity");
        jmethodID jfuncId = env->GetStaticMethodID(javaclass, "toast", "(Ljava/lang/String;)V");
        env->CallStaticVoidMethod(javaclass, jfuncId, jtext);
        env->DeleteLocalRef(jtext);
    }
    
    

    jvm_helper.cpp:

    jstring JniHelper::char2jstr(const char* pat) {
        JNIEnv *env = getJVM();
        LOGD("char2jstr %p", env);
        // 定义java String类 strClass
        jclass strClass = (env)->FindClass("java/lang/String");
        //获取String(byte[],String)的构造器,用于将本地byte[]数组转换为一个新String
        jmethodID ctorID = (env)->GetMethodID(strClass, "<init>", "([BLjava/lang/String;)V");
        //建立byte数组
        jbyteArray bytes = (env)->NewByteArray(strlen(pat));
        //将char* 转换为byte数组
        (env)->SetByteArrayRegion(bytes, 0, strlen(pat), (jbyte*) pat);
        // 设置String, 保存语言类型,用于byte数组转换至String时的参数
        jstring encoding = (env)->NewStringUTF("UTF-8");
        //将byte数组转换为java String,并输出
        return (jstring) (env)->NewObject(strClass, ctorID, bytes, encoding);
    
    
    }
    
    jclass JniHelper::findClass(JNIEnv *env, const char* name) {
        jclass result = nullptr;
        if (env)
        {
            //这句会出错,所以要处理错误
            result = env->FindClass(name);
            jthrowable exception = env->ExceptionOccurred();
            if (exception)
            {
                env->ExceptionClear();
                return static_cast<jclass>(env->CallObjectMethod(gClassLoader, gFindClassMethod, env->NewStringUTF(name)));
            }
        }
        return result;
    }
    

    这里是toast的实现,最终还是调用了Java层的toast方法:

    MainActivity.java:

        public static void toast(String text){
            Toast.makeText(MyAppImpl.getAppContext(), text, Toast.LENGTH_SHORT).show();
        }
    

    值得注意的坑

    findClass失败

    通常,我们在native层想调用Java方法时,我们首先要获取Java中的方法所在的类。我们一般的方法是:

    result = env->FindClass(name);

    但如果在子线程中获取时,就会出现找不到类的情况。关于这一问题,详见StackOverFlow

    简单来讲,当我们在自己创建的子线程想要通过JVM获取Class时,Android会为我们启动系统的ClassLoader而不是我们App的ClassLoader

    Google提供了几种解决方法,在这里不一一赘述。本文中采用的方法是:通过缓存一个静态的全局ClassLoader对象,当env->findClass失败时,通过缓存的ClassLoader获取需要的类。

    jvm_helper.cpp:

    void JniHelper::setJVM(JNIEnv *env) {
        jvmEnv = env;
        jclass randomClass = env->FindClass("com/example/oceanlong/ndkmaintest/MainActivity");
        jclass classClass = env->GetObjectClass(randomClass);
        jclass classLoaderClass = env->FindClass("java/lang/ClassLoader");
        jmethodID getClassLoaderMethod = env->GetMethodID(classClass, "getClassLoader",
                                                          "()Ljava/lang/ClassLoader;");
        jobject localClassLoader = env->CallObjectMethod(randomClass, getClassLoaderMethod);
        gClassLoader = env->NewGlobalRef(localClassLoader);
        //我在Android中用findClass不行,改成loadClass才可以找到class
        gFindClassMethod = env->GetMethodID(classLoaderClass, "findClass",
                                            "(Ljava/lang/String;)Ljava/lang/Class;");
    }
    
    jclass JniHelper::findClass(JNIEnv *env, const char* name) {
        jclass result = nullptr;
        if (env)
        {
            result = env->FindClass(name);
            jthrowable exception = env->ExceptionOccurred();
            if (exception)
            {
                env->ExceptionClear();
                return static_cast<jclass>(env->CallObjectMethod(gClassLoader, gFindClassMethod, env->NewStringUTF(name)));
            }
        }
        return result;
    }
    

    ALooper_addFd的"粘包现象"

    当我并发给main_looper发送消息时,发现ALooper_addFd没有解决并发问题。

    比如当我这样调用:

    MainActivity.java:

            mBtnTest.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
    
                    final String s = mEditTest.getText().toString();
                    for (int i = 0 ; i < 5 ; i++){
                        new Thread(new Runnable() {
                            @Override
                            public void run() {
                                nativeToast(s);
                            }
                        }).start();
                    }
                }
            });
    

    5个线程几乎同时发送消息。最终的日志是:

    image.png

    我们总共发送了5次,但handle_message只调用了两次

    但幸运的是,内容没有丢失。

    这个地方,我还没有找到好的解决方式。如果读者对此有些了解,望能赐教。

    目前,我能够想到的是,根据内容,在handle_message中实现“解包”。

    总结

    在native层,想要切到主线程调用方法。其根本是在应用启动时,就在主线程调用初始化,构建好一个消息通道。然后,通过ALooper_AddFd方法,在接收到消息时,调用handle_message方法。这样,我们只需要在子线程中,以一定的编码格式向主线程发送消息,即可完成在native中切换主线程的能力。


    如有问题,欢迎指正。

    相关文章

      网友评论

        本文标题:Android NDK中的UI线程

        本文链接:https://www.haomeiwen.com/subject/wlqitqtx.html