Android Jni/NDK 開發(fā)入門詳解

本人為初學(xué)者,文章寫得不好,如有錯(cuò)誤,請(qǐng)大力懟我


如何使用jni進(jìn)行開發(fā)

本文主要針對(duì)Android環(huán)境進(jìn)行NDK\Native\Jni開發(fā)進(jìn)行介紹

使用2.2版本之前的Android Studio進(jìn)行ndk開發(fā)是比較繁瑣的,如果你還在使用舊版本的Android Studio,那么建議更新到3.0,現(xiàn)階段3.0已經(jīng)比較穩(wěn)定了(雖然舊項(xiàng)目的gradle升級(jí)可能需要折騰一下)。下面介紹舊版本的開發(fā)流程只是為了能夠更加詳細(xì)地介紹jni。

jni并不是android框架內(nèi)的概念,所以也會(huì)提及其他環(huán)境使用jni開發(fā)的方法,基本上大同小異,不過你可能還需要查閱其他文章來處理一些細(xì)節(jié)問題(如Windows下生成dll文件)


AS 2.2之前的做法


1.編寫C/C++

  • 首先創(chuàng)建一個(gè)java文件,聲明一個(gè)自定義的native方法,對(duì)我們來說,這個(gè)方法就是java層到native層的入口,另外,還需要使用靜態(tài)域?qū)o包加載進(jìn)來
    package com.linjiamin.jnishare;
    
    /**
    * Created by Albert on 17/11/16.
    */
    
    public class JniUtil {
    
        static {
            System.loadLibrary("sotest");
        }
    
        public static native int sum(int num1,int num2);
    }
  • 開始編寫 C/C++代碼之前我們需要兩個(gè)頭文件。其中一個(gè)是 jni.h,該頭文件包含了對(duì)jni數(shù)據(jù)類型和接口的定義(之后還會(huì)介紹),現(xiàn)在開始你所編寫的所有C/C++代碼都需要引入這個(gè)頭文件。另外你還需要一個(gè)根據(jù)剛剛編寫的native方法簽名及類信息生成的頭文件。對(duì)前者,簡(jiǎn)單地include進(jìn)來即可,而對(duì)于后者,可以使用javah命令生成,當(dāng)然你也可以選擇親自編寫,使用命令生成的方法如下
    //在終端中
    cd app/src/main/java
    javac com/linjiamin/jnishare/JniUtil.java
    javah com.linjiamin.jnishare.JniUtil
    
    //生成的頭文件如下
    
    /* DO NOT EDIT THIS FILE - it is machine generated */
    #include <jni.h>
    /* Header for class com_linjiamin_jnishare_JniUtil */
    
    #ifndef _Included_com_linjiamin_jnishare_JniUtil
    #define _Included_com_linjiamin_jnishare_JniUtil
    #ifdef __cplusplus
    extern "C" {
    #endif
    /*
    * Class:     com_linjiamin_jnishare_JniUtil
    * Method:    sum
    * Signature: (II)I
    */
    JNIEXPORT jint JNICALL Java_com_linjiamin_jnishare_JniUtil_sum 
    (JNIEnv *, jclass, jint, jint);
    
    #ifdef __cplusplus
    }
    #endif
    #endif
  • 簡(jiǎn)單說一下如何手寫這個(gè)頭文件,預(yù)處理指令的寫法都是相同的,將完整類名替換進(jìn)去即可,對(duì)于函數(shù)簽名,從左到右按以下順序編寫,當(dāng)然還是使用javah方法生成更好
    JNIEXPORT: 在android和linux中是空定義的宏,而在windows下被定義為__declspec(dllexport),具體的作用我們不需要關(guān)心
    
    jni數(shù)據(jù)類型:如jint,jboolean,jstring,它們對(duì)應(yīng)于本地方法的返回類型(int,boolean,String)之后會(huì)進(jìn)一步介紹、
    
    JNICALL : 這是__stdcall等函數(shù)調(diào)用約定(calling conventions)的宏,這些宏用于提示編譯器該函數(shù)的參數(shù)如何入棧(從左到右,從右到左),以及清理堆棧的方式等
    
    方法名: Java + 完整類名 + 方法名
    
    參數(shù)列表:JNIEnv * + jclass\jobject + 所有你定義的參數(shù)所對(duì)應(yīng)的jni數(shù)據(jù)類型 ,JNIEnv*是指向jvm函數(shù)表的指針,如果該方法為靜態(tài)方法則第二個(gè)參數(shù)為class否則為jobject,它是含有該方法的class對(duì)象或?qū)嵗?    
    注意JNIEXPORT和JNICALL是固定的
  • 函數(shù)具體實(shí)現(xiàn)如下,相信大家都能看懂
    #include "jni.h"
    #include "com_linjiamin_jnishare_JniUtil.h"
    //
    // Created by Albert Humbert on 17/11/17.
    //
    
    JNIEXPORT jint JNICALL Java_com_linjiamin_jnishare_JniUtil_sum
    (JNIEnv * env, jclass obj, jint num1, jint num2){
      return num1 + num2;
    }


2.使用ndk編譯so包


包結(jié)構(gòu)
  • 現(xiàn)在在main包下創(chuàng)建一個(gè)jni包,將你的頭文件和c/c++文件放進(jìn)去,然后,你還需要兩份mk文件,mk文件是makefile文件的一部分,makefile包含c/c++編譯器的編譯命令、順序和規(guī)則,如果你不了解makefile是什么,那也沒什么關(guān)系,后面會(huì)講解Android.mk和Application.mk文件的書寫規(guī)范
  • 注意請(qǐng)?jiān)贏ndroid Library中進(jìn)行ndk開發(fā),不要使用Java Library,前者會(huì)生成aar包,可以包含so以及其他資源文件,后者會(huì)生成jar,jar通常只能調(diào)用外部so包,網(wǎng)上也有文章將jar當(dāng)中的so包用文件流寫到本地調(diào)用的,建議不要嘗試這種騷操作


編寫Android.mk
  • 一個(gè)最基本的Android.mk如下
    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_MODULE := libsotest
    LOCAL_SRC_FILES := com_linjiamin_jnishare_JniUtil.cpp
    include $(BUILD_SHARED_LIBRARY)
  • LOCAL_PATH := $(call my-dir),Android.mk必須以該屬性開頭,它用于指定源文件的路徑,my-dir是一個(gè)返回Android.mk文件所在目錄的宏
  • include $(CLEAR_VARS) ,指定負(fù)責(zé)文件的清理的makefile文件,一般固定這么寫就好
  • LOCAL_SRC_FILES,需要編譯的c/c++文件,如無后綴則默認(rèn)為cpp文件
  • include $(BUILD_SHARED_LIBRARY) ,收集上次清理后的源文件信息,并決定如何編譯
  • LOCAL_C_INCLUDES,頭文件的搜索路徑
  • TARGET_ARCH,指定AB,如armeabi,armeabi-v7a


編寫Application.mk
  • 一個(gè)典型的Application.mk文件如下
    APP_PLATFORM = android-24
    APP_ABI := armeabi,armeabi-v7a,x86_64,arm64-v8a
    APP_STL := stlport_static
    APP_OPTIM := debug
  • APP_PLATFORM,ndk版本號(hào),你可以在ndk-bundle文件夾中查看所本地ndk版本
    # for mac
    /Users/alberthumbert/Library/Android/sdk/ndk-bundle/platforms
  • APP_ABI,指定APP_ABI版本,這會(huì)決定ndk編譯出的so包數(shù)量,關(guān)于ABI的介紹見下文,推薦至少包含armeabi或armeabi-v7a
  • APP_STL 如何連接c++標(biāo)準(zhǔn)庫 ,包括 stlport_static ,stlport_shared ,system,分別表示靜態(tài),動(dòng)態(tài),系統(tǒng)默認(rèn)
  • APP_OPTIM,包括debug,和release,這會(huì)決定so中是否包含調(diào)試信息
  • APP_MODULES,填寫so包的名字,如果沒有這個(gè)屬性,則按照Android.mk中的進(jìn)行命名,注意如果文件中含有多個(gè)該屬性,則會(huì)按照先后順序?yàn)槟憔幾g出來的so文件命名
  • 填寫完這兩個(gè)mk文件之后,需要在gradle中指定so庫的路徑,gradle會(huì)自動(dòng)將so文件打包進(jìn)來,在andorid閉包中添加
    sourceSets.main {
    jniLibs.srcDir 'src/main/libs'
    jni.srcDirs = []
    }
  • 如無意外這個(gè)時(shí)候我們的項(xiàng)目就可以運(yùn)行起來了,打印log如下
    11-17 16:33:37.563 3824-3824/? D/JniUtil: test: 2


人生苦短,我用AS 3.0


自動(dòng)生成函數(shù)

  • 如果出于不幸、粗心、經(jīng)驗(yàn)不足等原因,你的項(xiàng)目無法運(yùn)行,請(qǐng)不要懷疑你的智商,下面為你帶來傻瓜式的ndk開發(fā)流程
  • 最新版的AS,可以為你使用ndk開發(fā)提供很大的方便,請(qǐng)確保你在SDK Tools中下載了CMake、LLDB、NDK
  • 首先創(chuàng)建一個(gè)新項(xiàng)目,并勾選Include C++ support
  • C++ Standard中可以選擇使用的C++標(biāo)準(zhǔn),默認(rèn)是CMake所使用的標(biāo)準(zhǔn),Exceptions Support可以啟用C++異常處理,一般這些選項(xiàng)使用默認(rèn)的就可以了
  • 項(xiàng)目創(chuàng)建完畢之后你可以看見官方已經(jīng)為你做好了很多工作,并且?guī)Я艘粋€(gè)c++的hello world示例,你需要關(guān)注的主要有cpp和External Build Files 兩個(gè)目錄,前者用于放置你的C++源文件,后者根據(jù)不同的ABI版本放置了CMake腳本
  • 接著我們直接在MainActivity中添加一個(gè)native方法,然后選中該方法,按下alt+enter,讓IDE為我們自動(dòng)生成C++函數(shù)
        public native boolean booleanFromJNI();
  • 在native-lib.cpp中可以看見自動(dòng)生成的函數(shù),我們只需要實(shí)現(xiàn)該函數(shù)即可
    JNIEXPORT jboolean JNICALL
    Java_com_linjiamin_myapplication_MainActivity_booleanFromJNI(JNIEnv *env, jobject instance) {
    
        // TODO
    
    }
  • 注意使用上面的方法你可以在任意一個(gè)java文件中聲明native方法,IDE會(huì)自動(dòng)在native-lib.cpp中為你生成對(duì)應(yīng)的函數(shù)簽名,當(dāng)然,你也不是非要把所有的C/C++代碼都寫在一個(gè)文件里,下面來講解一下CMakeList的基本寫法


編寫CMakeList

  • 下面是一份官方寫好的CMakeList.txt,這個(gè)文件可以在你當(dāng)前項(xiàng)目的app目錄下看到
    add_library( # 設(shè)置編譯出來的so包的名字. 不需要添加lib前綴
                 native-lib
    
                 # 設(shè)置為共享鏈接庫. 有SHARED,STATIC兩種可選
                 SHARED
    
                 # 設(shè)置源文件的相對(duì)路徑,可將多個(gè)源文件進(jìn)行編譯
                 src/main/cpp/native-lib.cpp )
    
    # Searches for a specified prebuilt library and stores the path as a
    # variable. Because CMake includes system libraries in the search path by
    # default, you only need to specify the name of the public NDK library
    # you want to add. CMake verifies that the library exists before
    # completing its build.
    
    find_library( # 設(shè)置外部引用庫.這個(gè)庫支持你在c/c++中打印log,具體請(qǐng)見  android/log.h
                log-lib
                # 外部引用庫的名稱
                log )
    
    # Specifies libraries CMake should link to your target library. You
    # can link multiple libraries, such as libraries you define in this
    # build script, prebuilt third-party libraries, or system libraries.
    
    target_link_libraries( # 指定被鏈接的庫.
                        native-lib
    
                        # 鏈接log-lib到native-lib
                        ${log-lib} )
  • 現(xiàn)在我們想要將不同的源文件編譯成多份so包,例如我在cpp目錄下添加一份test-lib.cpp文件,代碼如下
    extern "C"
    JNIEXPORT jboolean JNICALL
    Java_com_linjiamin_myapplication_JniUtil_booleanFromJNI(JNIEnv *env, jobject instance) {
    
    //上面提到的log庫可以這么使用,而且你應(yīng)該使用宏讓它好看些
    __android_log_print(ANDROID_LOG_DEBUG,"stringFromJNI","%d",0);
    return (jboolean) true;
    
    }
  • 那么可以在剛剛的CMakeList中設(shè)定我們的so包,在最后加上
    add_library( test-lib SHARED src/main/cpp/test-lib.cpp )
  • 編譯之后 可以看到 build/intermediates/cmake/debug/obj/ 路徑下不同的ABI目錄中都有了兩份so文件,分別是libnative-lib.so,libtest-lib.so
  • 如果你在不同的路徑下放置了源文件,并且希望對(duì)于每一個(gè)特定的路徑都有一份自己特定的CMakeList文件來描述這些源文件的打包規(guī)則(這看起來是個(gè)好習(xí)慣),可以使用add_subdirectory("目錄名")方法指定子路徑,子路徑當(dāng)中的放置CMakeList會(huì)被執(zhí)行


使用g++編譯so包

  • 對(duì)于非安卓開發(fā)者,這里再簡(jiǎn)單介紹一個(gè)使用g++編譯so包的方法,使用這種方法你無需ndk環(huán)境,也不用編寫mk、CMakeList文件,完全使用命令行進(jìn)行編譯,當(dāng)然我更推薦你去學(xué)習(xí)cmake
  • 編寫java文件并用javah指令生成頭文件,再編寫cpp文件,這個(gè)流程對(duì)于不同平臺(tái)的jni開發(fā)都是相同的(雖然Intelligent Idea這種IDE可以為你自動(dòng)生成頭文件),那么現(xiàn)在需要一份對(duì)應(yīng)平臺(tái)下的jni.h文件,可以在你的jdk當(dāng)中查找,編譯器可能還會(huì)提示你需要一份jni_md.h文件,它也在jdk當(dāng)中
    $ cd /Library/Java/JavaVirtualMachines/jdk1.7.0_71.jdk
    $ find . -iname jni.h
    ./Contents/Home/include/jni.h
  • 我這里用的是ndk當(dāng)中的jni.h文件,下載ndk之后在sdk當(dāng)中可以找到
    $ cd /Users/alberthumbert/Library/Android/sdk/ndk-bundle
    $ find . -iname jni.h
    ./sysroot/usr/include/jni.h
  • 現(xiàn)在假定你以及有了一份java文件,兩份頭文件,一份cpp或c文件,那么可以使用如下命令將他們編譯成so文件,注意最后的參數(shù)不一定是必要的,如果你想編譯安卓平臺(tái)可用的so包那么建議加上
    g++ com_linjiamin_jnishare_JniUtil.cpp -fPIC -shared -o libsotest.so -Wl,--hash-style=sysv

*注意,nix平臺(tái)使用lib表示一個(gè)so庫,所以so文件必須以lib開頭,但加載時(shí)請(qǐng)將lib前綴去掉**

  • 你可以用絕對(duì)路徑加載一個(gè)so文件
    System.load("\***\***\***.so")
  • 你也可以將so文件放入系統(tǒng)加載路徑當(dāng)中,調(diào)用 System.getProperty("java.library.path")方法得到系統(tǒng)加載路徑,想要修改這個(gè)路徑,可以在修改.bashrc(或者當(dāng)前使用的其他shell)中添加
    export PATH=PATH:/XXX
  • 運(yùn)行jar時(shí)動(dòng)態(tài)指定路徑也是可以的,這樣會(huì)暫時(shí)覆蓋上面寫入的屬性
    java -jar -Djava.library.path=/**/**   **.jar
  • 重新在java代碼中加載so文件,*nix平臺(tái)注意去掉lib前綴
    System.loadLibrary ("***");


ABI與so

  • 這里再啰嗦一下別的東西,可以先跳過,之后再倒回來看
  • 由于目前我們的項(xiàng)目很簡(jiǎn)單,沒有用到第三方so庫和也沒有去除多余so庫為apk瘦身,因此不用考慮兼容問題,但實(shí)際開發(fā)項(xiàng)目時(shí)通常沒這么簡(jiǎn)單。我們知道,編譯出來的so是二進(jìn)制文件,由于不同的CPU支持不同的指令集,所以我們需要考慮兼容性的問題。一個(gè)包含多種指令集及其相關(guān)約定的實(shí)現(xiàn)被稱之為ABI,一個(gè)CPU架構(gòu)支持一種到多種ABI。安卓平臺(tái)就是針對(duì)ABI進(jìn)行編譯和打包的。
  • 可能看了上面這一段會(huì)比較暈,那么我就舉一個(gè)例子來說明,比如ARMv7架構(gòu)的CUP,支持 armeabi和armeabi-v7a兩種ABI,而armeabi這種ABI支持Thumb-1,ARMV5TE等指令集,armeabi-v7a這種ABI又支持Thumb-2和VFPv3-D16等指令集,也就是說,一種CPU架構(gòu)對(duì)應(yīng)多種ABI類型,一種ABI類型對(duì)于多種指令集
  • 一個(gè)so文件只支持一種ABI,因此你會(huì)發(fā)現(xiàn)在lib下每一個(gè)包都是以ABI來命名的,同名的so文件被按照其支持的ABI進(jìn)行分類
  • 目前ABI一共有七種,那么是不是意味者我們的每一個(gè)so都需要編譯成七種,然后全都打包進(jìn)apk當(dāng)中呢,答案是否定的。目前CUP流行的架構(gòu)主要有ARM系列,x86,x86_64,但移動(dòng)設(shè)備大部分都是ARMv7架構(gòu),少數(shù)是ARM架構(gòu),由于ARMv7架構(gòu)兼容armeabi,因此類似淘寶、微信、餓了么的國(guó)內(nèi)大廠通常只使用armeabi一種ABI,F(xiàn)acebook,Twitter等外國(guó)大廠則是只保留了armeabi-v7a,這是十分合理的,apk只保留一種通用的ABI,而最適應(yīng)的so可以在外部去下載
  • 那么是不是只要編譯一種so文件就可以了呢?不完全正確,如果你引用了第三方的ndk,而第三方在兼容性做得比較好的情況下適配了多種ABI,又或者目前你的lib下so包的數(shù)量參差不齊。當(dāng)一個(gè)apk安裝時(shí)就有可能查找到了最適用的ABI的路徑存在,但里面又沒有想要的so,這時(shí)它不會(huì)自動(dòng)去查找其他ABI版本的so,而是會(huì)crash,為了解決這個(gè)問題,請(qǐng)?jiān)趌ib包下只保留一個(gè)包(通常是armeabi或者armeabi-v7a),或者每個(gè)名字的so在不同包下都存在對(duì)應(yīng)版本,并且在app的gradle的defaultConfig閉包中添加你所適配好的ABI,這樣安裝時(shí)只會(huì)從你所指定的ABI中查找so包
    ndk{
        abiFilters  "armeabi-v7a", "x86", "armeabi"
    }


什么是jni

現(xiàn)在我們已經(jīng)可以進(jìn)行簡(jiǎn)單的ndk開發(fā)了,但為了加深理解認(rèn)識(shí),讓我再來啰嗦一下jni

看過這一部分之后你對(duì)jni應(yīng)該會(huì)有更近一步的感性認(rèn)識(shí)


jni.h


jni數(shù)據(jù)類型

  • 現(xiàn)在來看看c/c++層的數(shù)據(jù)類型是怎么對(duì)應(yīng)到j(luò)ava層的
  • 首先是基本類型,根據(jù)java中的定義定義了j*類型
    /* Primitive types that match up with Java equivalents. */
    typedef uint8_t  jboolean; /* unsigned 8 bits */
    typedef int8_t   jbyte;    /* signed 8 bits */
    typedef uint16_t jchar;    /* unsigned 16 bits */
    typedef int16_t  jshort;   /* signed 16 bits */
    typedef int32_t  jint;     /* signed 32 bits */
    typedef int64_t  jlong;    /* signed 64 bits */
    typedef float    jfloat;   /* 32-bit IEEE 754 */
    typedef double   jdouble;  /* 64-bit IEEE 754 */
  • 對(duì)于引用類型,c和c++有區(qū)別,在c++中 jobject是類,而jstring和各種類型的數(shù)組都是jobject的子類的指針,在c中jobject是一個(gè)void*指針,而其他引用類型其實(shí)都是jobject
    class _jobject {};
    class _jclass : public _jobject {};
    class _jstring : public _jobject {};
    class _jarray : public _jobject {};
    class _jobjectArray : public _jarray {};
    class _jbooleanArray : public _jarray {};
    class _jbyteArray : public _jarray {};
    class _jcharArray : public _jarray {};
    class _jshortArray : public _jarray {};
    class _jintArray : public _jarray {};
    class _jlongArray : public _jarray {};
    class _jfloatArray : public _jarray {};
    class _jdoubleArray : public _jarray {};
    class _jthrowable : public _jobject {};
    
    typedef _jobject*       jobject;
    typedef _jclass*        jclass;
    typedef _jstring*       jstring;
    typedef _jarray*        jarray;
    typedef _jobjectArray*  jobjectArray;
    typedef _jbooleanArray* jbooleanArray;
    typedef _jbyteArray*    jbyteArray;
    typedef _jcharArray*    jcharArray;
    typedef _jshortArray*   jshortArray;
    typedef _jintArray*     jintArray;
    typedef _jlongArray*    jlongArray;
    typedef _jfloatArray*   jfloatArray;
    typedef _jdoubleArray*  jdoubleArray;
    typedef _jthrowable*    jthrowable;
    typedef _jobject*       jweak;
    
    
    /*
     * Reference types, in C.
     */
    typedef void*           jobject;
    typedef jobject         jclass;
    typedef jobject         jstring;
    typedef jobject         jarray;
    typedef jarray          jobjectArray;
    typedef jarray          jbooleanArray;
    typedef jarray          jbyteArray;
    typedef jarray          jcharArray;
    typedef jarray          jshortArray;
    typedef jarray          jintArray;
    typedef jarray          jlongArray;
    typedef jarray          jfloatArray;
    typedef jarray          jdoubleArray;
    typedef jobject         jthrowable;
    typedef jobject         jweak;
  • jvalue是一個(gè)比較特殊的聯(lián)合體,一般在需要調(diào)用java層方法時(shí)做為方法參數(shù)傳入,如 void CallVoidMethodA(jobject obj, jmethodID methodID, jvalue* args) ,通過jobject和表示其方法的jmethodID即可特定一個(gè)具體的方法,然后將我們的jvalue作為函數(shù)列表傳入
    typedef union jvalue {
            jboolean    z;
            jbyte       b;
            jchar       c;
            jshort      s;
            jint        i;
            jlong       j;
            jfloat      f;
            jdouble     d;
            jobject     l;
        } jvalue;
  • 在這一方面我們可以討論的內(nèi)容比較少,總的來說,由于C/C++ 中基本類型的字節(jié)數(shù)依賴與實(shí)現(xiàn),所以在native層轉(zhuǎn)換到j(luò)ava層是不能直接使用原本的int,long等類型而是根據(jù)java中的約定使用jni.h指定了相同長(zhǎng)度與有符號(hào)的類型,而java中的類則可以使用類或結(jié)構(gòu)體的指針來解決


常用的接口

在講解JNIEnv和JavaVM之前先來嘗試一下各種jni的基本操作,版本較新的AS已經(jīng)支持了對(duì)C/C++ 的智能提示和代碼補(bǔ)全功能,你可以很方便地試用JNIEnv提供的接口

這里只介紹幾個(gè)例子,以后有時(shí)間我會(huì)另寫文章介紹這些接口,強(qiáng)烈推薦你使用AS把可調(diào)用的函數(shù)瀏覽并選擇性地使用一遍


修改成員變量

  • 通過之前的例子你應(yīng)該已經(jīng)知道怎么從native層中獲取一個(gè)變量了,現(xiàn)在再進(jìn)一步,我們使用native方法直接改變成員變量的值,在MainActivity中定義一個(gè)native方法
    public class MainActivity extends AppCompatActivity {
    
        public String mString = null;
    
    static {
            System.loadLibrary("native-lib");
        }
    
        private static final String TAG = "MainActivity";
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            Log.d(TAG, "onCreate: "+mString);
            getFieldFromJNI();
            Log.d(TAG, "onCreate: "+ mString);
        }
    
        public native String getFieldFromJNI();
    
    }
  • 在來看C++實(shí)現(xiàn),注意你可能在閱覽其他文章時(shí)發(fā)現(xiàn)調(diào)用jni函數(shù)時(shí)有兩種不同的寫法 env-> 和 (*env)->,這是由于C與C++的實(shí)現(xiàn)有所差異,不影響我們使用。如果你使用過反射改變成員變量的值,應(yīng)該可以毫不費(fèi)力地理解下面這段代碼
    extern "C"
    JNIEXPORT jstring JNICALL
    Java_com_linjiamin_jnilearning_MainActivity_getFieldFromJNI(JNIEnv *env, jobject instance) {
    
        jclass clazz = env->GetObjectClass(instance);
        //獲取調(diào)用者的class對(duì)象
        jfieldID  jfID = env ->GetFieldID(clazz,"mString","Ljava/lang/String;");
        //獲取成員變量的鍵
        jstring strValue = (jstring) env->GetObjectField(instance, jfID);
        //獲取成員變量的值,不做操作
        char chars[10] = "whatever";
        jstring newValue = env->NewStringUTF(chars);
        //創(chuàng)建一個(gè)String對(duì)象
        env->SetObjectField(instance,jfID,newValue);
        //設(shè)置新的值
        return strValue;
    }


創(chuàng)建引用


引用類型
  • 了解jni中的引用類型,有助于你編寫高效的代碼并且解決內(nèi)存泄漏等問題,jni中的引用類型可以分為三種,局部引用,全局引用,弱(全局)引用,通常jvm會(huì)在函數(shù)返回后自動(dòng)為你釋放局部引用,但你需要自行管理全局引用的生命周期


局部引用
  • 其實(shí)我們之前已經(jīng)接觸過局部引用了,調(diào)用jni函數(shù)通常會(huì)創(chuàng)建新對(duì)象的實(shí)例并返回一個(gè)局部引用,局部引用只在一次native函數(shù)的調(diào)用周期中存在,在函數(shù)結(jié)束時(shí)被釋放
  • 通常我們需要避免返回全局引用,而是返回創(chuàng)建出來的局部引用,例如這樣
    return (jstring) env->NewLocalRef(someValue);
  • 我們剛剛說過,局部引用在函數(shù)的調(diào)用過程中存在,也就是說如果不進(jìn)行人為的銷毀操作,它將一直存在,在任意native函數(shù)中執(zhí)行下面這段代碼,你將接受到一個(gè)異常,跟java不同,gc不會(huì)及時(shí)回收通過這種方法創(chuàng)建出來的變量
    for(int i = 0;i<1000000000;i++){
        jstring newValue = env->NewStringUTF(chars);
    }
  • 你可以調(diào)用 DeleteLocalRef函數(shù)銷毀一個(gè)局部引用,這個(gè)函數(shù)現(xiàn)在就可以執(zhí)行了,不過他會(huì)比一般函數(shù)耗時(shí)些
        for(int i = 0;i<1000000000;i++){
            jstring newValue = env->NewStringUTF(chars);
            env->DeleteLocalRef(newValue);
        }


全局引用
  • 之前說過局部引用在函數(shù)的調(diào)用過程中存在,我們不能直接使用顯式賦值的方式將局部引用強(qiáng)行將其緩存起來
    jobject gInstance;
    
    …
    
    extern "C" JNIEXPORT void JNICALL 
    Java_com_linjiamin_jnilearning_MainActivity_useCPlusThread(JNIEnv *env, jobject instance) {
    methodID = env->GetMethodID(clazz,"sayHello","()V");
    gInstance = instance;
    ...
    }
  • 上面的例子將會(huì)報(bào)錯(cuò),不過對(duì)于jni開發(fā),你可能常常只能收到含糊的報(bào)錯(cuò)信息,甚至收不到報(bào)錯(cuò)信息
    JNI DETECTED ERROR IN APPLICATION: native code passing in reference to invalid stack indirect reference table or invalid reference: 0x7fff871642e0
  • 我們可以使用 NewGlobalRef 函數(shù)來創(chuàng)建一個(gè)全局引用,但是注意,你需要對(duì)自己的行為負(fù)責(zé),全局引用只有在你的手動(dòng)調(diào)用 DeleteGlobalRef 函數(shù)之后才會(huì)被釋放,你可以在JNI_OnLoad 中進(jìn)行緩存工作,在JNI_OnUnload函數(shù)中進(jìn)行緩存的清除
    jobject gInstance;
    
    …
    extern "C" JNIEXPORT void JNICALL 
    Java_com_linjiamin_jnilearning_MainActivity_useCPlusThread(JNIEnv *env, jobject instance) {
    
    methodID = env->GetMethodID(clazz,"sayHello","()V");
    gInstance = env->NewGlobalRef(instance);
    ...
    }
    
    …
    
    void fun() {
    ...
    ...
    env->DeleteGlobalRef(gInstance);
    }


弱引用
  • 弱引用和全局引用大體上類似,但是當(dāng)內(nèi)存不足時(shí)它會(huì)被GC回收,通過 NewWeakGlobalRef 函數(shù)可以創(chuàng)建一個(gè)弱引用,和Java層的弱引用一致,它不會(huì)阻止自己所指向的對(duì)象被GC回收
    gInstance = env->NewWeakGlobalRef(instance);
  • 但是這不意味著你可以不用管理弱引用的生命周期,在不需要它時(shí)請(qǐng)主動(dòng)釋放弱引用,注意,弱引用的釋放不會(huì)導(dǎo)致它所指向的對(duì)象被GC回收
    env->DeleteWeakGlobalRef(gInstance)
  • 最好在使用弱引用時(shí)判斷它的對(duì)象是否已被釋放,你可能會(huì)理所當(dāng)然地使用 == 進(jìn)行判斷,這種方法是錯(cuò)誤的,除非這個(gè)引用從來就沒有被初始化過,不然表達(dá)式將永遠(yuǎn)為真,解決方案是使用jni提供的接口進(jìn)行比較,有的文章也推薦再次使用NewWeakGlobalRef來達(dá)到這樣的效果,個(gè)人認(rèn)為這兩種方案除了在可讀性上的區(qū)別外沒什么不同
    if (env->IsSameObject(gInstance,NULL)) {
        __android_log_print(ANDROID_LOG_DEBUG,"fun","%s","instance is NULL");
    }
    
        //或者
    
    if (!gInstance || !env->NewWeakGlobalRef(gInstance)) {
        __android_log_print(ANDROID_LOG_DEBUG,"fun","%s","instance is NULL");
    }


JNIEnv,JavaVM 以及多線程

  • 你可能已經(jīng)意識(shí)到,目前為止我們都是通過JNIEnv來使用jni的,實(shí)際上JNIEnv提供了Native函數(shù)的基礎(chǔ)環(huán)境,具體來說,它包含了一個(gè)指向函數(shù)表的指針,這也就是為什么我們需要通過JNIEnv才能調(diào)用native方法,JNIEnv也代表了具體的進(jìn)程環(huán)境,因此不允許跨進(jìn)程調(diào)用,最好的做法是永遠(yuǎn)不要緩存JNIEnv,你可以通過JavaVM來創(chuàng)造它的實(shí)例
  • JavaVM是java虛擬機(jī)的代表,它可以跨線程調(diào)用,它是一個(gè)全局對(duì)象,典型的jni環(huán)境中一個(gè)進(jìn)程可以有多個(gè)JavaVM,但是在安卓環(huán)境當(dāng)中他在每個(gè)進(jìn)程中只有一個(gè)實(shí)例,通常你可以在JNI_OnLoad 函數(shù),或其可以獲取JNIEnv的地方得到它_
        env->GetJavaVM(&gVm);
  • 下面在C++線程中模仿耗時(shí)操作,并調(diào)用Java層方法傳回?cái)?shù)據(jù),首先定義接受數(shù)據(jù)的方法和一個(gè)native方法,這里的參數(shù)列表稍微定義得復(fù)雜一點(diǎn),方便之后演示jvalue的使用方法
    public void resultCallback(boolean isSuccess,int result,String data){
        Log.d(TAG, "resultCallback: "+ isSuccess + " "+result + " " +data);
    }
    
    public native void useCPlusThread();
  • native方法的實(shí)現(xiàn)如下,這里我們通過GetJavaVM方法得到了JavaVM對(duì)象,JavaVM用于我們之后獲取JNIEnv,同時(shí)我們把調(diào)用者通過全局引用緩存起來,注意這里的methodID不需要使用NewGlobalRef,它是一個(gè)結(jié)構(gòu)體,直接賦值即可,由于java支持重載,需要輸入方法函數(shù)列表的標(biāo)識(shí)才可以特定一個(gè)方法,每個(gè)基本類型都有其對(duì)應(yīng)的縮寫,而對(duì)于類我們需要通過包名和類名來指定。然后我們開啟五個(gè)線程進(jìn)行耗時(shí)操作
    extern "C"
    JNIEXPORT void JNICALL
    Java_com_linjiamin_jnilearning_MainActivity_useCPlusThread(JNIEnv *env, jobject instance) {
    
    env->GetJavaVM(&gVm);
    jclass clazz = env->GetObjectClass(instance);
    
    
    methodID = env->GetMethodID(clazz, "resultCallback", "(ZILjava/lang/String;)V");
    gInstance = env->NewGlobalRef(instance);
    
    pthread_t pthread[5];
    
    for(int i = 0;i<5;i++){
        pthread_create(&pthread[i], NULL, &fun, NULL);
    }
    }
  • 線程方法的實(shí)現(xiàn)如下,linux系統(tǒng)的sleep函數(shù)定義在unistd.h文件中,我們使用它來模仿耗時(shí)操作,像剛剛說過的一樣JNIEnv不能跨進(jìn)程調(diào)用,那么這里使用AttachCurrentThread函數(shù)得到實(shí)例,這個(gè)函數(shù)同時(shí)也會(huì)將當(dāng)前線程綁定到JavaVM上,然后我們使用CallVoidMethodA來調(diào)用剛剛緩存起來的實(shí)例的方法,也就是java層的resultCallback方法,jvalue數(shù)組可以作為參數(shù)列表傳入,另外你也可以使用更為簡(jiǎn)便的CallVoidMethod函數(shù),最后記得使用DetachCurrentThread函數(shù)解綁,除非你使用DeleteLocalRef函數(shù)釋放引用,不然你通過JNIEnv獲取的局部引用在你調(diào)用DetachCurrentThread之前都不會(huì)被銷毀,并且在函數(shù)結(jié)束后造成內(nèi)存泄漏
    void *fun(void *arg) {
    sleep(3);
    
    JNIEnv *env;
    if (gVm->AttachCurrentThread(&env, NULL) != JNI_OK) {
        __android_log_print(ANDROID_LOG_DEBUG, "callJniInDifferentThread", "%s", "attach failed");
        return NULL;
    }
    
    jvalue * args = new jvalue[3];
    args[0].z = (jboolean) true;
    args[1].i = 1000;
    args[2].l = env->NewStringUTF("some data");
    
    env->CallVoidMethodA(gInstance, methodID, args);
    
    if (gVm->DetachCurrentThread() != JNI_OK) {
        __android_log_print(ANDROID_LOG_DEBUG, "callJniInDifferentThread", "%s", "detach failed");
    }
    
    return NULL;
    
    }
  • 注,各種類型對(duì)應(yīng)的縮寫如下,請(qǐng)使用一下的縮寫特定具體的方法
類型 縮寫
Boolean Z
Byte B
Char C
Short S
Int I
Long L
Float F
Double D
Void V
Object 以"L"開頭,以";"結(jié)尾,中間是用"/" 隔開的包及類名。比如:Ljava/lang/String;如果是嵌套類,則用$來表示嵌套。例如 "(Ljava/lang/String;Landroid/os/FileUtils$FileStatus;)Z"
返回值與參數(shù) 例 (IB)L 表示返回類型為long,參數(shù)為int和byte的函數(shù)


內(nèi)存泄漏


局部引用的內(nèi)存模型

  • 剛剛提到JVM在一定程度上會(huì)為你管理局部引用的生命周期,但這不意味著局部引用等于局部變量。每當(dāng)線程從Java層切換到native層時(shí),JVM會(huì)創(chuàng)建局部引用表,它們維系了你的C/C++變量和Java層變量。
  • 下圖中出現(xiàn)的本地方法棧是和虛擬機(jī)棧類似的一種概念,但它用于運(yùn)行native方法,注意,規(guī)范只約定了jni的操作和使用方法,對(duì)實(shí)現(xiàn)沒有明確的要求,有些虛擬機(jī)會(huì)將虛擬機(jī)棧與本地方法棧合并實(shí)現(xiàn),這里只大致地描繪本地方法棧的結(jié)構(gòu)。當(dāng)java層切入到native層(以下簡(jiǎn)稱J2N過程,反之為N2J),或者在native函數(shù)中調(diào)用了jni接口時(shí)會(huì)導(dǎo)致本地方法棧的入棧操作,本地引用表在J2N時(shí)創(chuàng)建,并在N2J時(shí)銷毀,在這個(gè)過程中,每當(dāng)局部引用被合法創(chuàng)建,該局部引用都會(huì)被添加到表中并映射到j(luò)ava堆中的一個(gè)對(duì)象
  • 看回前面這個(gè)例子,我們?cè)谘h(huán)中不斷創(chuàng)建新的局部引用,并且賦值給變量newValue,這些不斷創(chuàng)建的引用并不會(huì)立即釋放,并且我們之后也無法獲取到這些還留在表中的引用,所以他們都導(dǎo)致了內(nèi)存泄漏。一般情況下局部引用表分配到的內(nèi)存空間很小,這種內(nèi)存泄漏很容易就會(huì)導(dǎo)致內(nèi)存溢出,虛擬機(jī)崩潰。為了編寫更加安全流暢的代碼,我建議你遵循下面幾個(gè)規(guī)范
    for(int i = 0;i<1000000000;i++){
        jstring newValue = env->NewStringUTF(chars);
    }


引用的使用規(guī)范

  • native編程首先需要遵循C/C++自身的內(nèi)存管理機(jī)制,除了局部引用以外,JVM不會(huì)為你做更多的內(nèi)存釋放工作,所以當(dāng)你使用malloc函數(shù)分配內(nèi)存空間后必須使用free函數(shù)進(jìn)行釋放,這和其他平臺(tái)上的C/C++編程沒什么不同
  • 全局變量對(duì)java層對(duì)象的引用一直有效,請(qǐng)?jiān)诓挥脮r(shí)進(jìn)行刪除,否它所指向的對(duì)象將一直留在堆中
  • 和剛剛介紹局部引用時(shí)說的一樣,在函數(shù)返回之前,局部引用不會(huì)自動(dòng)釋放,如果創(chuàng)建過多的引用將會(huì)導(dǎo)致內(nèi)存溢出的風(fēng)險(xiǎn),如果你的函數(shù)只會(huì)創(chuàng)建為數(shù)不多的局部引用,那么完全可以將刪除引用的操作交給JVM去處理,但如果你的函數(shù)會(huì)創(chuàng)建大量的引用,特別是在開啟循環(huán)的請(qǐng)況下,請(qǐng)自行調(diào)用DeleteLocalRef函數(shù)


推薦閱讀

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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