零基礎帶你吃掉JNI全家桶(一)

散人丶發表於2019-02-17

前言

大家好!我又來了,這次準備著手寫一個JNI開發系列,畢竟,現在JNI開發也是在各個公司越來越重要了,如果專案畢竟大,可能涉及的模組較多,比如你作為應用層的開發,難免避免不了需要使用一些庫,一些加密操作等等,一般都會放在本地方法裡面,比較安全,人家丟給你so檔案或者靜態a檔案。。你不會用豈不是很尷尬。網上資料比較雜,而且很亂,大部分還是在用.mk的方法。

本系列就基於CMake形式,希望能夠帶著一些希望學習JNI開發的小夥伴一起學會JNI開發~比心❤

零基礎帶你吃掉JNI全家桶(二)

零基礎帶你吃掉JNI全家桶(三)

從一個栗子說起

c++ support
注意:要支援CMake,此時我們需要勾選 Include C++ support,然後點選Next--->Finish,完成工程的建立。 建立完成後,我們開啟工程目錄,發現增加了幾個不一樣的地方:
image.png
發現AS已經幫我們生產一個cpp目錄以及一個native-lib.cpp的c++檔案,在根目錄下,也多了一個CMakeLists.txt檔案,我們主要來關注CMakeLists.txt裡面的東東

#定義cmake支援的最小版本號
cmake_minimum_required(VERSION 3.4.1)


add_library( # 設定生成so庫的檔名稱,例如此處生成的so庫檔名稱應該為:libnative-lib.so
             native-lib

             # 設定生成的so庫型別,型別只包含兩種:
             # STATIC:靜態庫,為目標檔案的歸檔檔案,在連結其他目標的時候使用
             # SHARED:動態庫,會被動態連結,在執行時被載入
             SHARED

             # 設定原始檔的位置,可以是很多個原始檔,都要新增進來,注意相對位置
             src/main/cpp/native-lib.cpp )

# 從系統裡查詢依賴庫,可新增多個
find_library( # 例如查詢系統中的log庫liblog.so
              log-lib

              # liblog.so庫指定的名稱即為log,如同上面指定生成的libnative-lib.so庫名稱為native-lib一樣
              log )
# 配置目標庫的連結,即相互依賴關係
target_link_libraries( # 目標庫(最終生成的庫)
                       native-lib

                        # 依賴於log庫,一般情況下,如果依賴的是系統中的庫,需要加 ${} 進行引用,
                        # 如果是第三方庫,可以直接引用庫名,例如:
                        # 引用第三方庫libthird.a,引用時直接寫成third;注意,引用時,每一行只能引用一個庫
                       ${log-lib} )
複製程式碼

這裡我把註釋進行了縮減,標註了中文註釋,比較詳細,不明白的可以看下每個的作用,當然還有很多API可以使用,後續再詳細說明,也可以看看官方文件,[戳我戳我] (developer.android.google.cn/ndk/guides/…)

我們新建一個Helper類來編寫native方法

public class NativeHelper  {
    static {
        System.loadLibrary("native-lib");
    }
    public  native String stringFromJNI();
    public  native int add(int a,int b);
 
}
複製程式碼

開啟我們的MainActivity

public class MainActivity extends AppCompatActivity {

    // Used to load the 'native-lib' library on application startup.
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Example of a call to a native method
        TextView tv = (TextView) findViewById(R.id.sample_text);
        tv.setText(stringFromJNI());
    }
}
複製程式碼

可以看到最上面,靜態程式碼塊引用了native-lib這個庫,然後直接呼叫native本地方法,將C++中返回的字串拿到進行顯示。然後看看C++具體是怎麼是實現的

#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring

JNICALL
Java_com_example_hik_cmake_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}
複製程式碼

程式碼很簡單,引用兩個標頭檔案,然後定義了一個方法,返回“Hello from C++”這個字串,那有的朋友要問了,為什麼java層直接呼叫stringFromJNI()方法能夠直接對映到C++裡面的方法呢,細心的小夥伴可能發現了,C++裡面的這個方法名很長而且很熟悉。。這不是Java包名加上方法名拼湊而成的字串嗎,這種方式呢叫做靜態註冊,這樣就能通過這個對映方式找到C++中的方法。

有的朋友又要說了,這麼長方法名也太麻煩了,雖然可以自動生成,但是多不美觀,多不優雅。。是滴!有靜態註冊,那當然就有動態註冊了~我們來改一改程式碼:

#include <jni.h>
#include <string>
#include <android/log.h>

#define TAG "JNI_"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,TAG,__VA_ARGS__)
JNICALL
jstring backStringToJava(JNIEnv *env, jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}
//動態註冊
jint registerMethod(JNIEnv *env) {
    jclass clz = env->FindClass("com/example/taolin/jni_project/NativeHelper");
    if (clz == NULL) {
        LOGD("con't find class: com/example/taolin/jni_project/NativeHelper");
    }
    JNINativeMethod jniNativeMethod[] = {{"stringFromJNI", "()Ljava/lang/String;", (void *) backStringToJava}};
    return env->RegisterNatives(clz,jniNativeMethod, sizeof(jniNativeMethod)/ sizeof(jniNativeMethod[0]));
}
jint JNI_OnLoad(JavaVM *vm, void *reserved){
    JNIEnv * env = NULL;
    if (vm->GetEnv((void**)&env,JNI_VERSION_1_6)!=JNI_OK){
        return JNI_ERR;
    }
    jint result = registerMethod(env);
    LOGD("RegisterNatives result: %d", result);
    return JNI_VERSION_1_6;
}
複製程式碼

這裡呢,為了在C++中列印日誌,我們需要引入log.h標頭檔案,然後我們把之前的方法名改成backStringToJava,然後因為沒有了靜態註冊的規則,Java層呼叫的使用當然就找不到我們對應的方法了,我們定義一個動態註冊的方法,將Java中的方法和C++中的方法進行動態的繫結:

  • 通過env指標,拿到MainActivity的class物件,具體env指標後續會詳細說明
  • 定義一系列的方法物件,每個包含三個引數,第一個是java中的方法名,第二個是方法對應的簽名,第三個是C++中的方法名
  • 在JNI_OnLoad方法中,呼叫動態註冊繫結方法進行繫結

有些朋友可能對方法簽名不太明白,後續語法會詳細說明,這裡先簡單說下,方法簽名也就是一個方法唯一性的一個標準,上面的()Ljava/lang/String;就是stringFromJNI的簽名,前面的括號裡面是引數的簽名,因為這裡沒有引數,所以為空,緊接著後面是返回值得簽名,規則是,如果是基本資料型別就是相對應的基本資料型別,如果不是基本資料型別,那麼就是L+物件包名+“;”,注意這裡的分號不可省略!!根據這個規則,下面那個方法的簽名就是(II)I,依次類推,沒明白的也沒關係,後面會詳細對JNI中的語法詳細解釋,先知道有這麼回事就好了。

public native String stringFromJNI(); 
//簽名:()Ljava/lang/String;
public native int add(int a int b) 
//簽名:"(II)I"
複製程式碼

然後我們直接執行APP,可以發現頁面上顯示出來了“Hello from C++”字串,然後看看我們生成的so檔案在哪:

image.png

OK!大功告成,我們的第一步就完成了,成功的完成了Java呼叫C++的方法,但別高興的太早,這只是第一步,好了,看到這的獎勵自己跟辣條吧~,溜了溜了。。

相關文章