NDK Samples [2] - hello-jniCallback
NDK Samples目錄:ofollow,noindex">GoogleSamples - NDK Samples
專案地址:https://github.com/googlesamples/android-ndk/tree/master/hello-jniCallback
說明文件:https://github.com/googlesamples/android-ndk/blob/master/hello-jniCallback/README.md
該專案演示如何從C程式碼呼叫Java層方法。
最低要求:
- Android+Studio/">Android Studio 版本大於 2.2
該專案的演示的方法主要是:
-
JniHandler :: getBuildVersion()
演示C程式碼如何回撥Java層的靜態方法。
-
JniHandler :: getRuntimeMemorySize()
演示C程式碼如何回撥Java層的例項方法。
-
JniHandler :: updateStatus(String msg)
演示C程式碼如何回撥Java層帶參方法。
程式碼主要分析 hello-jnicallback.c ,java層程式碼比較簡單直接省略:
聲明瞭結構體 TickContext,主要負責了:
1.快取JavaVM、jniHelper/MainActiviy的類和例項
2.互斥鎖lock
3.主要邏輯中的迴圈的執行狀態 done。
對應的結構體變數為 g_ctx。
/* 宣告結構體 tick_context */ typedef struct tick_context { JavaVM*javaVM;// javaVM jclassjniHelperClz;// JniHandler類,全域性引用 jobjectjniHelperObj;// JniHandler例項,全域性引用 jclassmainActivityClz;// MainActivity類,全域性引用 jobjectmainActivityObj;// MainActivity例項,全域性引用 pthread_mutex_tlock;// 一個互斥鎖 intdone;// 迴圈的執行狀態,1表示已完成,退出迴圈 } TickContext; /* 定義結構體變數 g_ctx */ TickContext g_ctx;
JNI_OnLoad在System.loadLibrary後被呼叫,相當於動態庫的初始化方法:
1:快取JavaVM
2:快取JniHelper類的快取,建立了一個JniHelper例項並快取
3:呼叫queryRuntimeInfo方法
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { // JniEnv JNIEnv* env; // 初始化結構體 g_ctx memset(&g_ctx, 0, sizeof(g_ctx)); // 快取 JavaVM g_ctx.javaVM = vm; // 檢查是否支援 JNI_VERSION_1_6 版本, 不支援時返回JNI_ERR if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { return JNI_ERR; } // 查詢JniHandler類,並快取 jclass clz = (*env)->FindClass(env, "com/example/hellojnicallback/JniHandler"); g_ctx.jniHelperClz = (*env)->NewGlobalRef(env, clz); // 建立一個JniHandler例項,並快取 jmethodID jniHelperCtor = (*env)->GetMethodID(env, g_ctx.jniHelperClz, "<init>", "()V"); jobject handler = (*env)->NewObject(env, g_ctx.jniHelperClz, jniHelperCtor); g_ctx.jniHelperObj = (*env)->NewGlobalRef(env, handler); // 獲取引用的執行資訊 // 主要是 展示如何從 c程式碼呼叫java層 JniHelper 的方法。 queryRuntimeInfo(env, g_ctx.jniHelperObj); // 初始化執行狀態 g_ctx.done = 0; g_ctx.mainActivityObj = NULL; // 返回該動態庫的JNI版本 returnJNI_VERSION_1_6; }
queryRuntimeInfo方法主要演示瞭如何從C層呼叫Java層方法,包括靜態方法和例項方法。
注意:還演示如何處理資源的手動釋放,以避免記憶體洩漏
以下程式碼分析忽略非空檢測,移除非空檢測程式碼塊:
void queryRuntimeInfo(JNIEnv *env, jobject instance) { // 查詢 JniHelper 靜態方法 getBuildVersion 的 ID jmethodID versionFunc = (*env)->GetStaticMethodID( env, g_ctx.jniHelperClz, "getBuildVersion", "()Ljava/lang/String;"); // 呼叫靜態方法 getBuildVersion,獲取返回值 // !Java物件需要在使用結束後釋放引用 jstring buildVersion = (*env)->CallStaticObjectMethod( env, g_ctx.jniHelperClz, versionFunc); // 將 jstring 轉換成一個 UTF-8 的 C字串 // !在不關注拷貝結果的情況下,請勿對 GetStringXXX 結果進行修改 // !GetXXX 返回值在使用後,需要呼叫對應的 ReleaseXXX,以釋放對Java層物件的引用 // !參考:https://www.cnblogs.com/codc-5117/archive/2012/09/06/2672833.html const char *version = (*env)->GetStringUTFChars(env, buildVersion, NULL); // 列印資訊 LOGI("Android Version - %s", version); // 釋放 GetStringUTFChars 產生的引用 (*env)->ReleaseStringUTFChars(env, buildVersion, version); // 釋放 CallStaticObjectMethod 返回的 jstring 的引用 (*env)->DeleteLocalRef(env, buildVersion); // 查詢 JniHelper 例項方法 getRuntimeMemorySize 的 ID jmethodID memFunc = (*env)->GetMethodID(env, g_ctx.jniHelperClz, "getRuntimeMemorySize", "()J"); // 呼叫例項方法 getRuntimeMemorySize,獲取返回值 // !jlong等基本資料型別不需要手動釋放 // !參考:https://blog.csdn.net/zhangguixian5/article/details/8490114 jlong result = (*env)->CallLongMethod(env, instance, memFunc); // 列印資訊 LOGI("Runtime free memory size: %" PRId64, result); // 遮蔽編譯器警告 (void)result; }
在MainActivity的宣告週期onResume中,會呼叫本地方法startTick:
1.快取MainActivity類和例項
2.建立一個新執行緒,新執行緒中呼叫UpdateTicks完成實際邏輯
注意:還演示了C層如何建立一個新的執行緒:
JNIEXPORT void JNICALL Java_com_example_hellojnicallback_MainActivity_startTicks( JNIEnv *env, jobject instance) { // 執行緒ID pthread_tthreadInfo_; // 執行緒屬性 pthread_attr_tthreadAttr_; // 初始化執行緒屬性 pthread_attr_init(&threadAttr_); // 設定為分離執行緒 // !參考:https://blog.csdn.net/inuyashaw/article/details/78904231 pthread_attr_setdetachstate(&threadAttr_, PTHREAD_CREATE_DETACHED); // 初始化互斥鎖 pthread_mutex_init(&g_ctx.lock, NULL); // 快取MainActivity類和例項 jclass clz = (*env)->GetObjectClass(env, instance); g_ctx.mainActivityClz = (*env)->NewGlobalRef(env, clz); g_ctx.mainActivityObj = (*env)->NewGlobalRef(env, instance); // 建立執行緒 int result= pthread_create( &threadInfo_, &threadAttr_, UpdateTicks, &g_ctx); assert(result == 0); // 銷燬執行緒屬相,釋放記憶體 pthread_attr_destroy(&threadAttr_); (void)result; }
UpdateTicks 是一個死迴圈,主要邏輯為定時呼叫Java層 MainActivity::updateTimer 方法:
注意:還展示了C層時間函式的簡單使用
以下程式碼分析忽略sendJavaMsg(主要為Java層日誌),移除sendJavaMsg及相關的 statusId:
void* UpdateTicks(void* context) { // TickContext 結構體變數 TickContext *pctx = (TickContext*) context; // javaVM JavaVM *javaVM = pctx->javaVM; // 呼叫 GetEnv 獲取當前執行緒 JniEnv // 當前當前執行緒未附加到 VM 時,呼叫 AttachCurrentThread 將當前執行緒附加到 VM // !文件: // https://docs.oracle.com/javase/6/docs/technotes/guides/jni/spec/invocation.html#GetEnv // https://docs.oracle.com/javase/6/docs/technotes/guides/jni/spec/invocation.html#attach_current_thread JNIEnv *env; jint res = (*javaVM)->GetEnv(javaVM, (void**)&env, JNI_VERSION_1_6); if (res != JNI_OK) { res = (*javaVM)->AttachCurrentThread(javaVM, &env, NULL); if (JNI_OK != res) { LOGE("Failed to AttachCurrentThread, ErrorCode = %d", res); return NULL; } } // 獲取 MainActivity 例項方法 updateTimer 的 ID // updateTimer 的 Java層邏輯 十分簡單:每呼叫一次,時間顯示 +1s jmethodID timerId = (*env)->GetMethodID(env, pctx->mainActivityClz, "updateTimer", "()V"); // 定義時間結構體 timeval 變數 beginTime, curTime, usedTime, leftTime // !timeval 有兩個成員,一個是秒,一個是微秒 struct timeval beginTime, curTime, usedTime, leftTime; // 定義時間結構體 timeval 常量 kOneSecond const struct timeval kOneSecond = { (__kernel_time_t)1, (__kernel_suseconds_t) 0 }; // 主要邏輯的死迴圈 while(1) { // 記錄開始時間 gettimeofday(&beginTime, NULL); // 互斥鎖加鎖 pthread_mutex_lock(&pctx->lock); // 獲取當前的呼叫狀態 int done = pctx->done; // 還原結構體的呼叫狀態為0 if (pctx->done) { pctx->done = 0; } // 互斥鎖解鎖 pthread_mutex_unlock(&pctx->lock); // 當前狀態為1時,表示完成邏輯,退出主迴圈 if (done) { break; } // 主邏輯開始 // 呼叫 MainActivity 靜態方法 updateTimer (Java層的時間顯示 +1s) (*env)->CallVoidMethod(env, pctx->mainActivityObj, timerId); // 記錄結束時間 gettimeofday(&curTime, NULL); // 計算呼叫 updateTimer 的耗時(結束時間 - 開始時間) timersub(&curTime, &beginTime, &usedTime); // 計算剩餘需要等待的時間 (1s - 耗時) timersub(&kOneSecond, &usedTime, &leftTime); // 定義結構體 timespec 變數 sleepTime // !timespec 有兩個成員,一個是秒,一個是納秒 struct timespec sleepTime; // 把計算的剩餘時間賦值到 sleepTime sleepTime.tv_sec = leftTime.tv_sec; sleepTime.tv_nsec = leftTime.tv_usec * 1000;// 微杪轉納秒 * 1000 // 等待時間的秒數等於1秒或少於1秒,呼叫 nanosleep 進行奈米級睡眠 // 等待時間大於1秒???啥情況 if (sleepTime.tv_sec <= 1) { nanosleep(&sleepTime, NULL); } else { /* Log */ } } // 把當前執行緒從 VM 中分離 // 不進行分離操作會導致執行緒無法退出引起資源洩漏 (*javaVM)->DetachCurrentThread(javaVM); return context; }
在MainActivity的宣告週期onPause中,會呼叫本地方法StopTicks:
1.修改執行狀態為 1 (表示已完成)
2.銷燬startTicks中產生的全域性引用和變數
(這裡的StopTicks方法首字母是大寫,可能是一個小坑吧,不影響)
JNIEXPORT void JNICALL Java_com_example_hellojnicallback_MainActivity_StopTicks(JNIEnv *env, jobject instance) { // 互斥鎖加鎖,確保執行狀態的執行緒安全 pthread_mutex_lock(&g_ctx.lock); // 執行狀態設定為已完成,會導致邏輯執行緒退出迴圈 g_ctx.done = 1; // 互斥鎖解鎖 pthread_mutex_unlock(&g_ctx.lock); // 定義結構體 timespec 變數 sleepTime struct timespec sleepTime; // 初始化sleepTime,置為0 memset(&sleepTime, 0, sizeof(sleepTime)); // 設定sleepTime,時長等於0.1秒 sleepTime.tv_nsec = 100000000; // 等待邏輯執行緒的迴圈結束 // 當邏輯執行緒執行時,會將上面的 狀態1 設成 狀態0,這裡就是等待邏輯執行緒的執行 // 每次等待的時間為0.1秒 while (g_ctx.done) { nanosleep(&sleepTime, NULL); } // 此處開始,邏輯執行緒已退出主迴圈 // 刪除在 startTicks 中快取的 MainActivity類和物件 的全域性引用,避免記憶體洩漏 (*env)->DeleteGlobalRef(env, g_ctx.mainActivityClz); (*env)->DeleteGlobalRef(env, g_ctx.mainActivityObj); // 置空 g_ctx.mainActivityObj = NULL; g_ctx.mainActivityClz = NULL; // 銷燬在 startTicks 中建立的互斥鎖 pthread_mutex_destroy(&g_ctx.lock); }
此次完成專案的主要部分的原始碼分析,共演示了:
- C層中呼叫Java層的靜態/例項方法
- C層中建立執行緒並附加到VM
- 部分函式,如互斥鎖,時間函式的簡單使用
- 基本的資源手動釋放處理,以避免記憶體洩漏