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絕對路徑
路徑包括:
/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
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 = []
}
}
}