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。
  • jclass/jobject jclass: 靜態(tài)方法奔坟,jobject: 非靜態(tài)方法

四携栋、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)系作者
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者迟隅。
  • 序言:七十年代末但骨,一起剝皮案震驚了整個濱河市励七,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌奔缠,老刑警劉巖掠抬,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異校哎,居然都是意外死亡两波,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門闷哆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來腰奋,“玉大人,你說我怎么就攤上這事抱怔×臃唬” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵屈留,是天一觀的道長局冰。 經(jīng)常有香客問我,道長灌危,這世上最難降的妖魔是什么康二? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮勇蝙,結(jié)果婚禮上沫勿,老公的妹妹穿的比我還像新娘。我一直安慰自己浅蚪,他們只是感情好藕帜,可當(dāng)我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著惜傲,像睡著了一般洽故。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上盗誊,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天时甚,我揣著相機與錄音,去河邊找鬼哈踱。 笑死荒适,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的开镣。 我是一名探鬼主播刀诬,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼邪财!你這毒婦竟也來了陕壹?” 一聲冷哼從身側(cè)響起质欲,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎糠馆,沒想到半個月后嘶伟,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡又碌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年九昧,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片毕匀。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡铸鹰,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出期揪,到底是詐尸還是另有隱情掉奄,我是刑警寧澤规个,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布凤薛,位于F島的核電站,受9級特大地震影響诞仓,放射性物質(zhì)發(fā)生泄漏缤苫。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一墅拭、第九天 我趴在偏房一處隱蔽的房頂上張望活玲。 院中可真熱鬧,春花似錦谍婉、人聲如沸舒憾。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽镀迂。三九已至,卻和暖如春唤蔗,著一層夾襖步出監(jiān)牢的瞬間探遵,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工妓柜, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留箱季,地道東北人。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓棍掐,卻偏偏與公主長得像藏雏,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子作煌,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容