本人為初學(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
- 你可以直接登錄 http://android.mk ,查看相關(guān)文檔
- 一個(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ù)