Android鬼點子-通過Google官方示例學NDK(1)

我是綠色大米呀發表於2017-12-26

如果你看遍了網上那些只是在C++裡面輸出一個 ‘ helloWorld ’ 的NDK教程的話,可以看看本系列的文章,本系列是通過NDK的運用的例子來學習NDK。

Android鬼點子-通過Google官方示例學NDK(2)——主要是說的不使用java程式碼,用c++寫一個activity。

Android鬼點子-通過Google官方示例學NDK(3)——這是一個opengl的例子。

Android鬼點子-通過Google官方示例學NDK(4)——主要是說的視訊解碼相關的內容。

程式碼地址,建議先拉下來看看。

程式碼的功能就是一個簡單的計時器。介面上的時間每秒增加1。

圖1

通過這個裡可以瞭解到如何C++呼叫java程式碼。 如何在C++起一個執行緒

/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.example.hellojnicallback;

import android.support.annotation.Keep;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    int hour = 0;
    int minute = 0;
    int second = 0;
    TextView tickView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        tickView = (TextView) findViewById(R.id.tickView);
    }
    @Override
    public void onResume() {
        super.onResume();
        hour = minute = second = 0;
        ((TextView)findViewById(R.id.hellojniMsg)).setText(stringFromJNI());
        startTicks();
    }

    @Override
    public void onPause () {
        super.onPause();
        StopTicks();
    }

    /*
     * A function calling from JNI to update current timer
     */
    @Keep
    private void updateTimer() {
        ++second;
        if(second >= 60) {
            ++minute;
            second -= 60;
            if(minute >= 60) {
                ++hour;
                minute -= 60;
            }
        }
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                String ticks = "" + MainActivity.this.hour + ":" +
                        MainActivity.this.minute + ":" +
                        MainActivity.this.second;
                MainActivity.this.tickView.setText(ticks);
            }
        });
    }
    static {
        System.loadLibrary("hello-jnicallback");
    }
    public native  String stringFromJNI();
    public native void startTicks();
    public native void StopTicks();
}

複製程式碼

Activity中有3個jni方法。一個更新介面方法。這裡主要了解這3個jni方法。

首先看stringFromJNI()方法,這只是一個簡單的C++呼叫java獲取資料。

JNIEXPORT jstring JNICALL
Java_com_example_hellojnicallback_MainActivity_stringFromJNI( JNIEnv* env, jobject thiz )
{
#if defined(__arm__)
    #if defined(__ARM_ARCH_7A__)
    #if defined(__ARM_NEON__)
      #if defined(__ARM_PCS_VFP)
        #define ABI "armeabi-v7a/NEON (hard-float)"
      #else
        #define ABI "armeabi-v7a/NEON"
      #endif
    #else
      #if defined(__ARM_PCS_VFP)
        #define ABI "armeabi-v7a (hard-float)"
      #else
        #define ABI "armeabi-v7a"
      #endif
    #endif
  #else
   #define ABI "armeabi"
  #endif
#elif defined(__i386__)
#define ABI "x86"
#elif defined(__x86_64__)
#define ABI "x86_64"
#elif defined(__mips64)  /* mips64el-* toolchain defines __mips__ too */
#define ABI "mips64"
#elif defined(__mips__)
#define ABI "mips"
#elif defined(__aarch64__)
#define ABI "arm64-v8a"
#else
#define ABI "unknown"
#endif
    return (*env)->NewStringUTF(env, "Hello from JNI !  Compiled with ABI " ABI ".");
}
複製程式碼

這個方法有兩個引數,JNIEnv* env 是jni的執行環境,不同執行緒的JNIEnv 彼此獨立; jobject thiz是該方法的呼叫者(static方法的話就是clazz)。

第一個方法就這樣,說另外兩個方法之前,先看一下JniHandler.java:

/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
package com.example.hellojnicallback;

import android.os.Build;
import android.support.annotation.Keep;
import android.util.Log;

/*
 * A helper class to demo that JNI could call into:
 *     private non-static function
 *     public non-static function
 *     static public function
 * The calling code is inside hello-jnicallback.c
 */
public class JniHandler {
    /*
     * Print out status to logcat
     */
    @Keep
    private void updateStatus(String msg) {
        if (msg.toLowerCase().contains("error")) {
            Log.e("JniHandler", "Native Err: " + msg);
        } else {
            Log.i("JniHandler", "Native Msg: " + msg);
        }
    }

    /*
     * Return OS build version: a static function
     */
    @Keep
    static public String getBuildVersion() {
        return Build.VERSION.RELEASE;
    }

    /*
     * Return Java memory info
     */
    @Keep
    public long getRuntimeMemorySize() {
        return Runtime.getRuntime().freeMemory();
    }
}

複製程式碼

JniHandler中的3個方法會在jni中被呼叫。

再看一下hello-jnicallback.c中的JNI_OnLoad方法 。這個方法,會在System.loadLibrary("hello-jnicallback");的時候被呼叫。

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
    JNIEnv* env;
    memset(&g_ctx, 0, sizeof(g_ctx)); //全部初始化未0

    g_ctx.javaVM = vm; //javaVM
    if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { //env
        return JNI_ERR; // JNI version not supported.
    }

    jclass  clz = (*env)->FindClass(env,
                                    "com/example/hellojnicallback/JniHandler");//找到clz
    g_ctx.jniHelperClz = (*env)->NewGlobalRef(env, clz);//生成引用

    jmethodID  jniHelperCtor = (*env)->GetMethodID(env, g_ctx.jniHelperClz,
                                                   "<init>", "()V"); //獲取clz的建構函式並生成一個物件
    jobject    handler = (*env)->NewObject(env, g_ctx.jniHelperClz,
                                           jniHelperCtor);
    g_ctx.jniHelperObj = (*env)->NewGlobalRef(env, handler);//生成並保持引用
    queryRuntimeInfo(env, g_ctx.jniHelperObj);

    g_ctx.done = 0;
    g_ctx.mainActivityObj = NULL;
    return  JNI_VERSION_1_6;
}
複製程式碼

這個方法主要是初始化了下面的結構體

typedef struct tick_context {
    JavaVM  *javaVM;
    jclass   jniHelperClz;
    jobject  jniHelperObj;
    jclass   mainActivityClz;
    jobject  mainActivityObj;
    pthread_mutex_t  lock;
    int      done;
} TickContext;
TickContext g_ctx;
複製程式碼

這個結構裡面儲存了JniHandler物件和activity物件,還有一個執行緒鎖。

queryRuntimeInfo(env, g_ctx.jniHelperObj)方法是在拿到了JniHandler物件後,呼叫JniHandler物件的方法。這裡就是C++呼叫Java的例子。因為之前已經儲存了JniHandler物件和JniHandler的clazz,所以queryRuntimeInfo方法中只是找方法,然後呼叫方法。

jmethodID versionFunc = (*env)->GetStaticMethodID(
            env, g_ctx.jniHelperClz,
            "getBuildVersion", "()Ljava/lang/String;");
jstring buildVersion = (*env)->CallStaticObjectMethod(env,g_ctx.jniHelperClz, versionFunc);
複製程式碼

如果你看不懂“()Ljava/lang/String;”,這裡就是表示方法的返回值型別和引數型別。具體的可以參考

上面是靜態方法的呼叫,如果是普通方法,就需要物件作為引數。

jmethodID memFunc = (*env)->GetMethodID(env, g_ctx.jniHelperClz,
                                            "getRuntimeMemorySize", "()J");
jlong result = (*env)->CallLongMethod(env, instance, memFunc);
複製程式碼

instance就是g_ctx.jniHelperObj。

那麼,接下來看開始計時的實現:

JNIEXPORT void JNICALL
Java_com_example_hellojnicallback_MainActivity_startTicks(JNIEnv *env, jobject instance) {
    pthread_t       threadInfo_; //用於宣告執行緒ID
    pthread_attr_t  threadAttr_; //用於儲存執行緒屬性

    pthread_attr_init(&threadAttr_);

    //表示新執行緒是否與程式中其他執行緒脫離同步,新執行緒不能用pthread_join()來同步,且在退出時自行釋放所佔用的資源
    pthread_attr_setdetachstate(&threadAttr_, PTHREAD_CREATE_DETACHED);

    //使用互斥鎖,NULL使用預設的互斥鎖屬性,預設屬性為快速互斥鎖
    pthread_mutex_init(&g_ctx.lock, NULL);

    jclass clz = (*env)->GetObjectClass(env, instance);//這個方法依賴的類物件的class物件
    g_ctx.mainActivityClz = (*env)->NewGlobalRef(env, clz);
    g_ctx.mainActivityObj = (*env)->NewGlobalRef(env, instance);////這個方法依賴的類物件

    //執行緒建立,第三個引數是方法,該方法的引數是通過第四個引數傳入的(void*型別)
    int result  = pthread_create( &threadInfo_, &threadAttr_, UpdateTicks, &g_ctx);//把第二個引數設定為NULL的話,將採用預設的屬性配置
    assert(result == 0);

    pthread_attr_destroy(&threadAttr_);

    (void)result;
}
複製程式碼

主要是起了一個執行緒,然後讓UpdateTicks方法執行在上面。接下來看看UpdateTicks方法。

void*  UpdateTicks(void* context) {
    TickContext *pctx = (TickContext*) context;
    JavaVM *javaVM = pctx->javaVM;
    JNIEnv *env;//不同執行緒的JNIEnv相互獨立
    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;
        }
    }

    jmethodID statusId = (*env)->GetMethodID(env, pctx->jniHelperClz,
                                             "updateStatus",
                                             "(Ljava/lang/String;)V");//找到方法JniHandler.updateStatus
    sendJavaMsg(env, pctx->jniHelperObj, statusId,
                "TickerThread status: initializing...");

    // get mainActivity updateTimer function
    jmethodID timerId = (*env)->GetMethodID(env, pctx->mainActivityClz,
                                            "updateTimer", "()V");

    struct timeval beginTime, curTime, usedTime, leftTime;
    const struct timeval kOneSecond = {
            (__kernel_time_t)1,//秒
            (__kernel_suseconds_t) 0//零頭:微秒
    };

    sendJavaMsg(env, pctx->jniHelperObj, statusId,
                "TickerThread status: start ticking ...");
    while(1) {
        gettimeofday(&beginTime, NULL);//獲得當前精確時間(1970年1月1日到現在的時間),時區是null
        pthread_mutex_lock(&pctx->lock);//加鎖
        int done = pctx->done;
        if (pctx->done) {
            pctx->done = 0;
        }
        pthread_mutex_unlock(&pctx->lock);//解鎖
        if (done) {
            break;
        }
        (*env)->CallVoidMethod(env, pctx->mainActivityObj, timerId);

        gettimeofday(&curTime, NULL);//當前時間
        timersub(&curTime, &beginTime, &usedTime); //usedTime = curTime - beginTime,使用了的時間
        timersub(&kOneSecond, &usedTime, &leftTime); //leftTime = kOneSecond(1s) - usedTime,剩餘的時間
        struct timespec sleepTime; //計算需要睡眠的時間
        sleepTime.tv_sec = leftTime.tv_sec;
        sleepTime.tv_nsec = leftTime.tv_usec * 1000; //微妙轉毫秒

        if (sleepTime.tv_sec <= 1) {
            nanosleep(&sleepTime, NULL);//執行緒暫停
        } else {
            sendJavaMsg(env, pctx->jniHelperObj, statusId,
                        "TickerThread error: processing too long!");
        }
    }

    sendJavaMsg(env, pctx->jniHelperObj, statusId,
                "TickerThread status: ticking stopped");
    (*javaVM)->DetachCurrentThread(javaVM);//white結束後,收回執行緒
    return context;
}
複製程式碼

這個有個while迴圈,並且通過pctx->done來作為終止條件。然後看1秒鐘還剩多少時間,然後剩餘的時間進行睡眠。

這裡有個if (pctx->done) { pctx->done = 0;}的操作,我看到這裡也比較迷惑,但是我們接下來往後看。

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

    // waiting for ticking thread to flip the done flag
    // 等待UpdateTicks中執行break後,才可以繼續收回資源
    struct timespec sleepTime;
    memset(&sleepTime, 0, sizeof(sleepTime));
    sleepTime.tv_nsec = 100000000;//100s
    while (g_ctx.done) {
        nanosleep(&sleepTime, NULL);
    }

    // release object we allocated from StartTicks() function
    (*env)->DeleteGlobalRef(env, g_ctx.mainActivityClz);
    (*env)->DeleteGlobalRef(env, g_ctx.mainActivityObj);
    g_ctx.mainActivityObj = NULL;
    g_ctx.mainActivityClz = NULL;

    pthread_mutex_destroy(&g_ctx.lock);
}
複製程式碼

原註釋waiting for ticking thread to flip the done flag的意思是等待計數執行緒翻轉done的標誌。我的理解是要等待計數執行緒退出之後,這裡的方法才可以繼續往下走,收回資源。

程式碼中我覺得有必要的地方我都加了中文註解,如果有理解錯誤的地方歡迎交流。

相關文章