Android NDK中的UI執行緒
概述
在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);
但如果在子執行緒中獲取時,就會出現找不到類的情況。關於這一問題,詳見 ofollow,noindex">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中切換主執行緒的能力。
如有問題,歡迎指正。