NDK開發(fā)(二)- JNI

JNI(Java Native Interface):Java調(diào)用C/C++的規(guī)范。

一、JNI數(shù)據(jù)類型

基本數(shù)據(jù)類型:
JAVA JNI
boolean jboolean
byte jbyte
char jchar
short jshort
int jint
long jlong
float jfloat
double jdouble
void void
引用類型:
JAVA JNI
Object jobject
Class jclass
String jstring
Object[] jobjectArray
boolean[] jbooleanArray
char[] jbyteArray
short[] jshortArray
int[] jintArray
long[] jlongArray
float[] jfloatArray
double[] jdouble
Throwable jthrowable

二、JNI簽名

數(shù)據(jù)簽名:
JAVA JNI
byte B
char C
short S
int I
long L
float F
double D
void V
boolean Z
object L開頭,然后以/分隔包的完整類型,后面再加; 比如String的簽名就是 "Ljava/lang/String;"
數(shù)組 基本數(shù)據(jù)類型: [ + 其類型的域描述符 ,引用類型: [ + 其類型的域描述符 + ;
多維數(shù)組 N個[ 其余語法和數(shù)組一致

舉例:
int[ ] [I
int[][] [[I
float[ ] [F
String[ ] [Ljava/lang/String;
Object[ ] [Ljava/lang/Object;

方法簽名:

(參數(shù)的域描述符的疊加)返回

舉例:
public int add(int index, String value,int[] arr)

簽名:(ILjava/util/String;[I)I

括號內(nèi)為參數(shù)描述符,依次為I、Ljava/util/String;、[I ,括號外為返回值I
通過方法簽名和方法名來唯一確認(rèn)一個JNI函數(shù)。

注:
如果不好確認(rèn)對應(yīng)方法的簽名,可以通過javap命令去查看:
javap -s -p -v xxx.class 找到對應(yīng)方法去查看具體簽名。

三、native函數(shù)

extern "C" JNIEXPORT jstring JNICALL
Java_com_stan_jnidemo_MainActivity_stringFromJNI(
        JNIEnv *env,
       jobject /* this */) {
    std::string hello = "print string";
   return env->NewStringUTF(hello.c_str());
}
  • extern “C” 使用C語言的命名和調(diào)用約定,而不是C++的命名機制。對于JNI來說,使用extern "C" 是必須的。
  • JNIEXPORT 宏定義,在Linux平臺即將方法返回值之前加入 attribute ((visibility (“default”))) 標(biāo)識,使so對外可見,即保證在本動態(tài)庫中聲明的方法 , 能夠在其他項目中可以被調(diào)用。
  • JNICALL 宏定義 Linux沒有進行定義 , 直接置空。
  • jstring 返回參數(shù)。
  • JNIEnv 以java線程為單位的執(zhí)行環(huán)境,通過它來使用JNI API。一個線程 = 一個獨立的。 JNIEnv。
  • jclass/jobject jclass: 靜態(tài)方法,jobject: 非靜態(tài)方法。
  • JavaVM:整個虛擬機實例,全局唯一。

四、JNI使用模板及案例舉例

native函數(shù)中使用JNI主要就是玩4類東西:類、對象、屬性、方法以及C/C++相關(guān)數(shù)據(jù)操作,非常類似于java的反射。

4.1 類操作:

1)獲取jclass通用方法:通過具體java類來獲取

jclass claz1 = env->FindClass(“com/stan/base/network/NetworkFactory”);

2)在非靜態(tài)方法中獲取當(dāng)前函數(shù)所在的類:通過native方法參數(shù)jobject來轉(zhuǎn)換

jclass claz2 = env->GetObjectClass(jobject);
4.2 對象操作:

1)獲取jobject通用型方法:通過jclass創(chuàng)建jobject

jobject obj = env->NewObject(jclass,jmethodID);//這里jmethodID對應(yīng)的是類的構(gòu)造方法

2)在非靜態(tài)方法中獲取當(dāng)前函數(shù)所在的對象:直接用native方法參數(shù)jobject

4.3 屬性操作:

1)獲取屬性id

jfieldID jfieldId = env->GetFieldID(jclazz, "key", "Ljava/lang/String;”);//獲取數(shù)據(jù)id。

2)get屬性值

jint  value = env->GetIntField(jobject obj, jfieldID fieldID);//非靜態(tài)int數(shù)據(jù)獲取。

3)set屬性值

env->SetIntField(jobject obj, jfieldID fieldID,jint value);

這里獲取屬性id和get/set屬性值都區(qū)分靜態(tài)非靜態(tài)。

4.4 方法操作:

1)獲取方法id

jmethodID methodId = env->GetMethodID(network_cls, "<init>", "()V”);

2)調(diào)用方法

jint result = env->CallIntMethod(jclass,jmethodID);

這里獲取方法id和調(diào)用方法區(qū)別靜態(tài)非靜態(tài)。

4.5 數(shù)據(jù)操作:

這部分是JNI數(shù)據(jù)類型的創(chuàng)建和JNI數(shù)據(jù)類型和C/C++數(shù)據(jù)類型相互轉(zhuǎn)換,這里以字符串舉例

1)創(chuàng)建引用類型

jstring jstr = env->NewStringUTF(str.c_str());

2)JNI數(shù)據(jù)類型和C/C++數(shù)據(jù)類型相互轉(zhuǎn)換

jboolean *iscopy;
//jstring 轉(zhuǎn)char *
const char *c_str = env->GetStringUTFChars(str, iscopy);//str為方法傳入的參數(shù)
if (iscopy == JNI_TRUE) {//重新開辟內(nèi)存空間保存
    printf("is copy:true");
} else if (iscopy == JNI_FALSE) {//與str內(nèi)存空間一致
    printf("is copy:false");
}

//釋放字符串,如果是重新開辟內(nèi)存空間的則直接釋放,否則通知JVM可以釋放,由JVM自行釋放。
env->ReleaseStringUTFChars(str, c_str);

這里簡單歸納了一些高頻操作。JNIEnv中的方法非常多,這里肯定不會一一列舉,玩api就是熟能生巧。

案例舉例:

public class MainActivity extends AppCompatActivity {
    static {
        System.loadLibrary("native-lib");
   }

    public String tips;
   public void showToast() {
        Toast.makeText(this, tips, Toast.LENGTH_SHORT).show();
   }

    public static native int[] sort(int[] arr);
   public native void show();

   @Override
   protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
       int[] arr = {5, 2, 1, 4, 3};
       int[] sortArr = sort(arr);
       for (int i = 0; i < sortArr.length; i++) {
            Log.d("jnitest", sortArr[i] + "");
       }
        show();
   }
}

#native-lib.cpp

extern "C"
JNIEXPORT jintArray JNICALL
Java_com_stan_jnidemo_MainActivity_sort(JNIEnv *env, jclass clazz, jintArray arr) {
    jboolean *isCopy;
   jint *intArr = env->GetIntArrayElements(arr, isCopy);
   int len = env->GetArrayLength(arr);
   for (int i = 0; i < len; i++) {
        for (int j = 0; j < len - i; j++) {
            if (intArr[j] > intArr[j + 1]) {
                int tmp = intArr[j + 1];
               intArr[j + 1] = intArr[j];
               intArr[j] = tmp;
           }
        }
    }
    env->ReleaseIntArrayElements(arr, intArr, 0);
   return arr;
}

extern "C"
JNIEXPORT void JNICALL
Java_com_stan_jnidemo_MainActivity_show(JNIEnv *env, jobject jobject) {
    jclass jclaz = env->GetObjectClass(jobject);
   jfieldID fieldId = env->GetFieldID(jclaz, "tips", "Ljava/lang/String;");
   jstring jstr = env->NewStringUTF("suc");
   env->SetObjectField(jobject, fieldId, jstr);
   jmethodID jmethodId = env->GetMethodID(jclaz, "showToast", "()V");
   env->CallVoidMethod(jobject, jmethodId);
}

五、引用類型

1)局部引用

定義:jobject NewLocalRef(JNIEnv *env, jobject ref);//ref:全局或者局部引用 ,return:局部引用。
釋放方式:1.jvm自動釋放,2.DeleteLocalRef(JNIEnv *env, jobject localRef);。JNI局部引用表,512個局部引用,太依賴jvm自動釋放會導(dǎo)致溢出。

2)全局引用

定義:jobject NewGlobalRef(JNIEnv *env, jobject obj); //obj:任意類型的引用,return:全局引用,如果內(nèi)存不足返回NULL。
釋放方式:無法垃圾回收,釋放它需要顯示調(diào)用void DeleteGlobalRef(JNIEnv *env, jobject globalRef);

3)弱全局引用

定義:jweak NewWeakGlobalRef(JNIEnv *env, jobject obj);
釋放方式:1.當(dāng)內(nèi)存不足時,可以被垃圾回收;2void DeleteWeakGlobalRef(JNIEnv *env, jweak obj);

env->IsSameObject(weakGlobalRef, NULL);//判斷該對象是否被回收了

六、注冊方式

public class MainActivity extends AppCompatActivity {
   static {
        System.loadLibrary("native-lib");
   }
   public native String stringFromJNI();
}
靜態(tài)注冊
extern "C" JNIEXPORT jstring JNICALL
Java_com_stan_jni_MainActivity_stringFromJNI(
        JNIEnv* env,
       jobject /* this */) {
    std::string hello = "Hello from C++";
   return env->NewStringUTF(hello.c_str());
}
動態(tài)注冊

native端:

  • 編寫C/C++代碼, 實現(xiàn)JNI_Onload()方法;
  • 將Java 方法和 C/C++方法通過簽名信息一一對應(yīng)起來;
  • 通過JavaVM獲取JNIEnv, JNIEnv主要用于獲取Java類和調(diào)用一些JNI提供的方法;
  • 使用類名和對應(yīng)起來的方法作為參數(shù), 調(diào)用JNI提供的函數(shù)RegisterNatives()注冊方法;

注:
javaVM:與進程對應(yīng);
JNIEnv:與線程對應(yīng);

//對應(yīng)實現(xiàn)的native方法
jstring native_stringFromJNI(
        JNIEnv *env,
       jobject /* this */) {
    std::string hello = "Hello from dynamic C++";
   return env->NewStringUTF(hello.c_str());
}

//需要注冊的函數(shù)列表,放在JNINativeMethod類型的數(shù)組中,以后如果需要增加函數(shù),只需在這里添加就行了
static JNINativeMethod gMethods[] = {
        {"stringFromJNI", "()Ljava/lang/String;", (void *) native_stringFromJNI}
};

//此函數(shù)通過調(diào)用RegisterNatives方法來注冊我們的函數(shù)
static int registerNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *getMethods,
                                int methodsNum) {
    jclass clazz;
   //找到聲明native方法的類
   clazz = env->FindClass(className);
   if (clazz == NULL) {
        return JNI_FALSE;
   }
    //注冊函數(shù) 參數(shù):java類 所要注冊的函數(shù)數(shù)組 注冊函數(shù)的個數(shù)
   if (env->RegisterNatives(clazz, getMethods, methodsNum) < 0) {
        return JNI_FALSE;
   }
    return JNI_TRUE;
}

static int registerNatives(JNIEnv *env) {
    //指定類的路徑,通過FindClass 方法來找到對應(yīng)的類
   const char *className = "com/stan/jni/MainActivity";
   return registerNativeMethods(env, className, gMethods,sizeof(gMethods) / sizeof(gMethods[0]));
}

//System.loadLibrary回調(diào)函數(shù)
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
    JNIEnv *env = NULL;
   //獲取JNIEnv
   if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
        return -1;
   }

    assert(env != NULL);
   //注冊函數(shù) registerNatives ->registerNativeMethods
   if (!registerNatives(env)) {
        return -1;
   }

    //返回jni 的版本
   return JNI_VERSION_1_6;
}

靜態(tài)注冊與動態(tài)注冊的對比:

  • 靜態(tài)注冊:java的native方法與c/c++方法一一對應(yīng)地書寫。當(dāng)需要修改包名、類名時需要逐個修改;
  • 動態(tài)注冊:動態(tài)關(guān)聯(lián)java的native方法與c/c++方法,第一次書寫比較繁瑣,之后修改類名包名、增加刪除方法比較靈活。

七、System.loadLibrary源碼簡析

這里簡單分析下System.loadLibrary(libName)如何加載so,代碼基于Android 8.0。

System.loadLibrary(libName) 通過Runtime來加載:

libcore/ojluni/src/main/java/java/lang/Runtime.java

synchronized void loadLibrary0(ClassLoader loader, String libname) {
    if (libname.indexOf((int)File.separatorChar) != -1) {
        throw new UnsatisfiedLinkError(
"Directory separator should not appear in library name: " + libname);
   }
    String libraryName = libname;
   if (loader != null) {
        String filename = loader.findLibrary(libraryName);
       if (filename == null) {
            // It's not necessarily true that the ClassLoader used
           // System.mapLibraryName, but the default setup does, and it's
           // misleading to say we didn't find "libMyLibrary.so" when we
           // actually searched for "liblibMyLibrary.so.so".
           throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
                                           System.mapLibraryName(libraryName) + "\"");
       }
        String error = doLoad(filename, loader);
       if (error != null) {
            throw new UnsatisfiedLinkError(error);
       }
        return;
   }
    String filename = System.mapLibraryName(libraryName);
   List<String> candidates = new ArrayList<String>();
   String lastError = null;
   for (String directory : getLibPaths()) {
        String candidate = directory + filename;
       candidates.add(candidate);
       if (IoUtils.canOpenReadOnly(candidate)) {
            String error = doLoad(candidate, loader);
           if (error == null) {
                return; // We successfully loaded the library. Job done.
           }
            lastError = error;
       }
    }
    if (lastError != null) {
        throw new UnsatisfiedLinkError(lastError);
   }
    throw new UnsatisfiedLinkError("Library " + libraryName + " not found; tried " + candidates);
}

整個方法主要功能是首先通過BaseDexClassLoader.findLibrary去找,找不到則通過getLibPaths()去找,其中之一能找到則通過doLoad去加載so。那么整體看來,Runtime.loadLibrary0 主要分兩步走:先找so,在加載so。

1.找so
1)通過ClassLoader.findLibrary來獲取so絕對路徑
ClassLoader.findLibrary調(diào)用時序圖

路徑包括:

/data/app/com.stan.cmakedemo-PgoTyGH7OVC_1VGtmnSlGg==/lib/arm64,  應(yīng)用安裝拷貝的目錄尋找so
/data/app/com.stan.cmakedemo-PgoTyGH7OVC_1VGtmnSlGg==/base.apk!/lib/arm64-v8a, 對apk內(nèi)部尋找so
/system/lib64, 系統(tǒng)目錄
/system/product/lib64 系統(tǒng)設(shè)備目錄

不同架構(gòu)和Android版本的手機應(yīng)該會有差異,這里是小米9,x64架構(gòu),Android 10系統(tǒng),僅供參考。

2)通過getLibPaths()來獲取

即String javaLibraryPath = System.getProperty("java.library.path");

路徑包括:

/system/lib64, 系統(tǒng)目錄
/system/product/lib64 系統(tǒng)設(shè)備目錄

找so總結(jié):

如果ClassLoader不為null,則通過ClassLoader去找,先找從應(yīng)用內(nèi)部找,然后再找系統(tǒng)目錄,如果ClassLoader為null,則直接去系統(tǒng)目錄找。

2.加載so

Runtime.doLoad

Runtime.doLoad調(diào)用時序圖

OpenNativeLibrary最終通過dlopen方式打開so文件,返回文件操作符handle。

應(yīng)用側(cè)so加載重試:System.load(Build.CPU_ABI ) > System.loadLibrary(libName) > System.load(absPath);

八、引入三方so庫

1.so在工程中存放位置選擇:

  • app/libs
  • app/src/main/jniLibs

2.CMakeList.txt配置 ( third-party.so)

#1設(shè)置so庫路徑
#CMAKE_SOURCE_DIR :CMakeList.txt文件所在的絕對路徑
set(my_lib_path ${CMAKE_SOURCE_DIR}/libs)

#2將第三方庫作為動態(tài)庫引用
add_library( 
             third-party
             SHARED
             IMPORTED )

#3指明第三方庫的絕對路徑
#ANDROID_ABI :當(dāng)前需要編譯的版本平臺
set_target_properties( 
                        third-party
                       PROPERTIES IMPORTED_LOCATION
                       ${my_lib_path}/${ANDROID_ABI}/ third-party.so )

#2+3的另一種寫法
add_library( # Sets the name of the library.
             third-party
             # Sets the library as a shared library.
             SHARED
             # Provides a relative path to your source file(s).
             # 也可以直接指定路徑
             ${my_lib_path}/${ANDROID_ABI}/ third-party.so)

#4 鏈接對應(yīng)的so庫
target_link_libraries( # Specifies the target library.
                       third-party
                       ${log-lib} )

3 gradle配置

android {
    defaultConfig {
        externalNativeBuild {
            cmake {
                abiFilters 'armeabi-v7a', 'arm64-v8a’ //選擇有so支持的平臺
                cppFlags ""
            }
        }
    }

    externalNativeBuild {
        cmake {
            path "CMakeLists.txt” //CMakeLists.txt路徑 也有放cpp目錄的:src/main/cpp/CMakeLists.txt
        }
    }

    sourceSets {
        main {  
            jniLibs.srcDir('libs’)//指定so文件夾路徑
            jni.srcDirs = []
    }
}
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容