android jni動態註冊

題外話       

        轉眼間2019年也已經接近尾聲了,回顧這一年也發生了很多事,換工作、搬家、趕項目、學習新技術等等很多事,忙碌的一年,博客也被放下了,真的是越來越懶了,每次都有一萬個不寫博客的理由,之前每年至少還要更新幾篇文章,然而今年一篇文章也沒寫,趁着這個週末沒事情,抓住2019年的小尾巴,把自己一直想寫的文章寫了,於是就有了今天這篇文章。

jni簡介

JNI是Java Native Interface的縮寫,它提供了若干的接口實現了Java和其他語言的通信(主要是c、c++)。從Java1.1開始,JNI標準成爲java平臺的一部分,它允許Java代碼和其他語言寫的代碼進行交互。jni是Android中java和c++之間連接的橋樑,jni是jvm提供的一種與native方法對接的方式。

jni的註冊方式分爲靜態註冊和動態註冊兩種,之前有一篇文章寫過jni相關的知識,那篇文章介紹的關於jni的知識就是靜態註冊的方式,今天主要寫的是jni的動態註冊。靜態註冊和動態註冊, 兩者優缺點如下:

  • 靜態註冊
    優點: 理解和使用方式簡單, 屬於傻瓜式操作, 使用相關工具按流程操作就行, 出錯率低
    缺點: 當需要更改類名,包名或者方法時, 需要按照之前方法重新生成頭文件, 靈活性不高
  • 動態註冊
    優點: 靈活性高, 更改類名,包名或方法時, 只需對更改模塊進行少量修改, 效率高
    缺點: 對新手來說稍微有點難理解, 同時會由於搞錯簽名, 方法, 導致註冊失敗

靜態註冊

開始jni動態註冊之前,可以先來回顧一下靜態註冊的流程:

  1. 編寫一個java類,在裏面加載對應的so庫並且通過native關鍵字定義需要調用的函數
  2. 在對應路徑命令行下輸入 javac xxx.java 生成xxx.class文件,然後在src目錄下通過 javah xxx.class 生成 com_xxx_xxx.h 頭文件
  3. .將頭文件拷貝到jni目錄下(eclipse在src同級目錄建立文件夾,Android studio 在java同級目錄建立文件夾)
  4. 編寫C/C++源代碼 並把剛拷貝的頭文件包含進去 ,複製頭文件中函數的定義部分,並實現其中的你想要的功能,然後編寫Android.mk Application.mk(ndk-build編譯方式,cmake方式可以不寫)

ndk-build編譯方式在命令行中進入jni目錄,輸入ndk-build 即可生產對應so庫,cmake方式build一下項目便會生成對應的so庫,庫文件會自動放在libs文件夾下 至此就可以運行程序了

動態註冊

動態註冊基本思想是在JNI_Onload()函數中通過JNI中提供的RegisterNatives()方法來將C/C++方法和java方法對應起來(註冊), 我們在調用 System.loadLibrary的時候,會在C/C++文件中回調一個名爲 JNI_OnLoad ()的函數,在這個函數中一般是做一些初始化相關操作, 我們可以在這個方法裏面註冊函數, 註冊整體流程如下:

  1. 編寫Java端的相關native方法
  2. 編寫C/C++代碼, 根據Java文件路徑和方法簽名確定JNINativeMethod,實現JNIEXPORT int JNICALL JNI_OnLoad(JavaVM *vm,void *reserved)方法
  3. 將Java 方法和 C/C++方法通過簽名信息一一對應起來
  4. 通過JavaVM獲取JNIEnv, JNIEnv主要用於獲取Java類和調用一些JNI提供的方法
  5. 使用類名和對應起來的方法作爲參數, 調用JNI提供的函數RegisterNatives()註冊方法

相關實例代碼如下:

  • 定義jni工具類,裏面定義native方法
public class JniUtil {

    // Used to load the 'native-lib' library on application startup.
    static {
        System.loadLibrary("native-lib");
    }


    /**
     * A native method that is implemented by the 'native-lib' native library,
     * which is packaged with this application.
     */
    public native String stringFromJNI();


    public native String dynamicMethod();
}
  • 在對應的native-lib中實現定義的本地方法
#include <jni.h>
#include <string>

#define JNI_JAVA_CLASS "com/example/dynamicjnidemo/JniUtil"


//靜態註冊對應本地方法
extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_dynamicjnidemo_JniUtil_stringFromJNI(JNIEnv *env, jobject thiz) {
    std::string hello = "jniUtil,Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

//動態註冊對應本地方法
extern "C"
JNIEXPORT jstring JNICALL
defineDynamicMethod(JNIEnv *env, jobject thiz) {
    return env->NewStringUTF("dynamic register jni method from jni");
}

/*需要註冊的函數列表,放在JNINativeMethod 類型的數組中,
以後如果需要增加函數,只需在這裏添加就行了
參數:
1.java中用native關鍵字聲明的函數名
2.簽名(傳進來參數類型和返回值類型的說明)
3.C/C++中對應函數的函數名(地址)
*/
static JNINativeMethod all_methods[] = {
//        多個方法依次都好分割寫在這裏
        {"dynamicMethod", "()Ljava/lang/String;", (void *) defineDynamicMethod},
};

//必須實現的回調函數
JNIEXPORT int JNICALL JNI_OnLoad(JavaVM *vm, void *unused) {
    JNIEnv *env;
//    獲取JNIEnv
    if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
        return JNI_ERR;
    }

//    指定類路徑,通過FindClass()方法找到對應類
    jclass java_class = env->FindClass(JNI_JAVA_CLASS);
    if (java_class == NULL) {
        return JNI_ERR;
    }

    int method_count = sizeof(all_methods) / sizeof(all_methods[0]);
//    註冊對應的本地方法 參數:java類 所要註冊的函數數組 註冊函數的個數
//    registerNatives ->registerNativeMethods ->env->RegisterNatives
    if (env->RegisterNatives(java_class, all_methods, method_count) < 0) {
        return JNI_ERR;
    }
//    返回jni 的版本
    return JNI_VERSION_1_6;
}
  • 在需要地方調用
package com.example.dynamicjnidemo;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    private JniUtil jniUtil = new JniUtil();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Example of a call to a native method
        TextView tv = findViewById(R.id.sample_text);
        tv.setText(jniUtil.stringFromJNI());
        TextView tvDynamic = findViewById(R.id.textViewDynamic);
        tvDynamic.setText(jniUtil.dynamicMethod());
    }


}
  • 最終效果如下圖:

動態註冊效果圖

上面的代碼就能實現動態註冊JNI了 以後要增加函數只需在java文件中聲明native方法,在C/C++文件中實現,
並在getMethods數組添加一個元素並指明對應關係,通過ndk-build 生成so庫就可以運行了。

Java方法簽名

關於查詢Java方法簽名,可以使用javap -s命令查詢對應的class文件(先編譯出對應的class文件,再查詢),如果方法簽名錯了,編譯能通過,但運行時會報NoSuchMethod異常。動態註冊中 JNINativeMethod 結構體中第二個參數需注意
括號內代表傳入參數的簽名符號,爲空可以不寫,括號外代表返回參數的簽名符號,爲空填寫 V,對應關係入下表:

簽名對應關係表
簽名符號 C/C++ java
V void void
Z jboolean boolean
I jint int
J jlong long
D jdouble double
F jfloat float
B jbyte byte
C jchar char
S jshort short
[Z jbooleanArray boolean[]
[I jintArray int[]
[J jlongArray long[]
[D jdoubleArray double[]
[F jfloatArray float[]
[B jbyteArray byte[]
[C jcharArray char[]
[S jshortArray short[]
L完整包名加類名; jobject class

如果有內部類 則用 $ 來分隔 如:Landroid/os/xxx$xxx;

總結:

一般來說,靜態註冊書寫簡單,但是由於方法名的對應關係,所以耦合性強,如果包名發生變化,那麼需要大量修改,而且初次運行的效率也不如動態註冊,所以動態註冊無疑是註冊函數的更好方式, 唯一要注意的是註冊函數時, 需要額外小心, 別把類名,函數名和簽名寫錯了, 不然loadLibraries時會導致應用Crash,能力有限就先寫這麼多,後面再補充。

文章最後附上demo的地址:https://download.csdn.net/download/you__are_my_sunshine/12060989

發佈了25 篇原創文章 · 獲贊 24 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章