- JNI概述
- 學習JNI實例:MediaScanner
- 注冊JNI函數(shù)
- 數(shù)據(jù)類型轉(zhuǎn)換
- JNIEnv介紹
一著角、JNI概述
JNI全稱,JavaNativeInterface——Java提供了Java層與Native層交互的橋梁。通過JNI技術(shù)我們可以做到如下:
- Java程序中的函數(shù)可以調(diào)用Native[C/C++]語言編寫的函數(shù)做院。
- Native層中的函數(shù)可以調(diào)用Java層的函數(shù),也就是說C/C++函數(shù)中可以調(diào)用Java函數(shù)。
二鼓黔、JNI學習實例:MediaScanner類
MediaScanner類中的部分函數(shù)由Native層實現(xiàn)央勒,JNI層中對應的是libmedia_jni.so,media_jni為JNI庫的名字澳化。libmedia.so庫完成了實際功能崔步。MediaScanner通過JNI庫libmedia_jni.so和Native層的libmedia.so進行交互。
#2.1 Java層的MediaScanner分析
- MediaScanner
class MediaScanner {
......
static {
System.loadLibrary("media_jni");
native_init();
}
......
private static native final void native_init();
......
}
Media類中的靜態(tài)代碼塊執(zhí)行了兩個操作:
- 加載media_jni庫缎谷。
- 調(diào)用native_init()完成native層初始化操作井濒。
Java函數(shù)中調(diào)用native函數(shù),必須通過位于JNI層的動態(tài)庫來實現(xiàn)慎陵。一般采用的做法是在類的static代碼塊中眼虱,通過調(diào)用System.loadLibrary(String libraryName)來完成對動態(tài)庫的加載。
-
加載JNI庫
由System類的靜態(tài)成員函數(shù)loadLibrary負責加載動態(tài)庫席纽。
- System
public static void loadLibrary(String libname) {
Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
}
loadLibrary方法中調(diào)用了Runtime類的成員函數(shù)loadLibrary0();
- Runtime
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;
//如果 loader不為空進入該分支
if (loader != null) {
//查找?guī)焖诘穆窂? 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);
}
loadLibrary0方法中所完成的操作是捏悬,如果ClassLoader不為空,則調(diào)用其成員函數(shù)findLibrary獲取到庫所在的路徑润梯,然后調(diào)用Runtime類的成員函數(shù)doLoad對庫進行加載过牙,doLoad函數(shù)則將具體的加載過程轉(zhuǎn)發(fā)給Runtime類中定義的native層函數(shù)nativeLoad,進而完成后續(xù)加載過程纺铭。
- ClassLoader.findLibrary
獲取庫所在的本地路徑寇钉。
/**
* Returns the absolute path name of a native library. The VM invokes this
* method to locate the native libraries that belong to classes loaded with
* this class loader. If this method returns <tt>null</tt>, the VM
* searches the library along the path specified as the
* "<tt>java.library.path</tt>" property.
*
* @param libname
* The library name
*
* @return The absolute path of the native library
*
* @see System#loadLibrary(String)
* @see System#mapLibraryName(String)
*
* @since 1.2
*/
protected String findLibrary(String libname) {
return null;
}
- Runtime.doLoad
調(diào)用native層函數(shù)nativeLoad完成對庫加載。
private String doLoad(String name, ClassLoader loader) {
......
// internal natives.
synchronized (this) {
return nativeLoad(name, loader, librarySearchPath);
}
}
nativeLoad后續(xù)的執(zhí)行步驟大致為:
- 調(diào)用dlopen函數(shù)舶赔,打開一個so文件并創(chuàng)建一個handle;
- 調(diào)用dlsym()函數(shù)扫倡,查看相應的so文件的JNI_OnLoad()函數(shù)指針,并執(zhí)行相應函數(shù)竟纳。
#2.2 JNI層的MediaScanner分析
MediaScanner的JNI層代碼在android_media_MediaScanner.cpp中撵溃,如下所示:
- android_media_MediaScanner.cpp
static void android_media_MediaScanner_native_init(JNIEnv *env)
{
ALOGV("native_init");
jclass clazz = env->FindClass(kClassMediaScanner);
if (clazz == NULL) {
return;
}
fields.context = env->GetFieldID(clazz, "mNativeContext", "J");
if (fields.context == NULL) {
return;
}
}
Java層的native_init對應JNI層的android_media_MediaScanner_native_init,下面詳細分析其綁定過程锥累。
當Java層調(diào)用native_init函數(shù)時缘挑,它會從對應的JNI庫中尋找Java_android_media_Media_Scanner_native_init函數(shù),如果沒有桶略,就會報錯语淘。如果找到,則會為這個native_init和Java_android_media_Media_Scanner_native_init建立一個關(guān)聯(lián)關(guān)系际歼,其實就是保存JNI函數(shù)的函數(shù)指針惶翻。以后調(diào)用native_init函數(shù)時,直接使用這個函數(shù)指針就可以了蹬挺,這項工作是由虛擬機完成的维贺。
三、注冊JNI函數(shù)
“注冊”是將Java層的native函數(shù)和JNI層對應的實現(xiàn)函數(shù)關(guān)聯(lián)起來巴帮。JNI函數(shù)注冊的方式有兩種:
- 靜態(tài)注冊
- 動態(tài)注冊
#3.1 靜態(tài)注冊
靜態(tài)注冊的大體流程如下:
- 先編寫Java代碼溯泣,然后編譯生成.class文件虐秋。
- 使用Java的工具程序javah,如javah -o output packagename.classname垃沦,這樣會生成一個叫output.h的JNI層頭文件客给。其中packagename.classname是Java代碼編譯后的class文件,而在生成的output.h文件里肢簿,聲明了對應的JNI層函數(shù)靶剑,只要實現(xiàn)里面的函數(shù)即可。
#3.1.1 :編寫Java代碼
package com.next.hhu.jnidemo;
public class StaticJNITest {
public static native int add(int a, int b);
}
#3.1.2 編譯生成.class文件
執(zhí)行[1]操作進入到java目錄下池充,然后執(zhí)行[2]操作生成.class文件桩引。
//[1] 進入到java目錄下
cd app/src/main/java
//[2] 生成.class文件
javac com/next/hhu/jnidemo/StaticJNITest.java
#3.1.3 生成頭文件
然后利用javah命令生成頭文件,這樣會在/java目錄下生成com_next_hhu_jnidemo_StaticJNITest.h頭文件收夸。
javah com.next.hhu.jnidemo.StaticJNITest
#3.1.4 配置CMakeLists.text文件
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html
# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.4.1)
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
add_library( # Sets the name of the library.
native-lib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
src/main/cpp/native-lib.cpp)
add_library( # Sets the name of the library.
native-lib1
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
src/main/cpp/com_next_hhu_jnidemo_StaticJNITest.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( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
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( # Specifies the target library.
native-lib
# Links the target library to the log library
# included in the NDK.
${log-lib} )
#3.2 動態(tài)注冊
JNI中用JNINativeMethod結(jié)構(gòu)來記錄Java的Native方法和JNI方法的關(guān)聯(lián)關(guān)系坑匠。
- jni.h
typedef struct {
const char* name; //Java方法名
const char* signature; //Java方法的簽名信息
void* fnPtr; //JNI中對應的函數(shù)指針
} JNINativeMethod;
MediaScanner JNI層中采用的是靜態(tài)注冊。
#3.2.1 定義JNINativeMethod[]
定義一個JNINativeMethod[]卧惜,其成員就是MediaScanner中所有成員函數(shù)的一一對應關(guān)系厘灼。
android_media_MediaScanner.cpp
static JNINativeMethod gMethods[] = {
{
"processDirectory",
"(Ljava/lang/String;Landroid/media/MediaScannerClient;)V",
(void *)android_media_MediaScanner_processDirectory
},
{
"processFile",
"(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",
(void *)android_media_MediaScanner_processFile
},
{
"setLocale",
"(Ljava/lang/String;)V",
(void *)android_media_MediaScanner_setLocale
},
{
"extractAlbumArt",
"(Ljava/io/FileDescriptor;)[B",
(void *)android_media_MediaScanner_extractAlbumArt
},
{
"native_init",
"()V",
(void *)android_media_MediaScanner_native_init
},
{
"native_setup",
"()V",
(void *)android_media_MediaScanner_native_setup
},
{
"native_finalize",
"()V",
(void *)android_media_MediaScanner_native_finalize
},
};
#3.2.2 注冊JNINativeMetod[]
- android_media_MediaScanner.cpp
// This function only registers the native methods, and is called from
// JNI_OnLoad in android_media_MediaPlayer.cpp
int register_android_media_MediaScanner(JNIEnv *env)
{
return AndroidRuntime::registerNativeMethods(env,
kClassMediaScanner, gMethods, NELEM(gMethods));
}
調(diào)用AndroidRuntime的registerNativeMethods函數(shù)來完成注冊工作。
- AndroidRuntime.cpp
/*
* Register native methods using JNI.
*/
/*static*/ int AndroidRuntime::registerNativeMethods(JNIEnv* env,
const char* className, const JNINativeMethod* gMethods, int numMethods)
{
return jniRegisterNativeMethods(env, className, gMethods, numMethods);
}
調(diào)用JNIHelp.c中的jniRegisterNativeMethods方法來完成注冊咽瓷。
- JNIHelp.c
int jniRegisterNativeMethods(JNIENV *env,const char *className,
const JNINativeMethod *gMethods,int numMethods)
{
jclass clazz;
clazz = (*env)->findClass(env,className);
......
//實際上調(diào)用JNIEnv的RegisterNatives函數(shù)完成注冊的
if((*env)->RegisterNatives(env,clazz,gMethods,numMethods) < 0) {
return -1;
}
return 0;
}
#3.2.3 JNI_OnLoad函數(shù)中調(diào)用注冊函數(shù)
當Java層通過System.loadLibrary加載完成JNI動態(tài)庫后设凹,緊接著會查找該庫中一個叫JNI_OnLoad的函數(shù)。如果有茅姜,就調(diào)用它闪朱,動態(tài)注冊工作在這里完成。
//該函數(shù)的第一個參數(shù)類型為JavaVM钻洒,代表JNI層的Java虛擬機监透,每一個Java進程有且僅有一個
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserve) {
JNIEnv *env = NULL;
jint result = -1;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK) {
goto bail;
}
if (env == NULL) {
return -1;
}
jclass clazz = env->FindClass("com/next/hhu/jnidemo/JNIHelper");
if (clazz == NULL) {
return -1;
}
......
//動態(tài)注冊MediaScanner的JNI函數(shù)
if(register_android_media_MediaScanner(env) < 0) {
goto bail;
}
.....
return JNI_VERSION_1_4;//必須返回這個值,否則會報錯航唆。
}
四、數(shù)據(jù)類型轉(zhuǎn)換
#4.1 基本數(shù)據(jù)類型轉(zhuǎn)換
Signature格式 | Java | Native |
---|---|---|
B | byte | jbyte |
C | char | jchar |
D | double | jdouble |
F | float | jfloat |
I | int | jint |
S | short | jshort |
J | long | jlong |
Z | boolean | jboolean |
V | void | void |
#4.2 數(shù)組數(shù)據(jù)類型
Signature格式 | Java | Native |
---|---|---|
[B | byte[] | jbyte |
[C | char[] | jchar |
[D | double[] | jdouble |
[F | float[] | jfloat |
[I | int[] | jint |
[S | short[] | jshort |
[J | long[] | jlong |
[Z | boolean[] | jboolean |
#4.3 引用數(shù)據(jù)類型轉(zhuǎn)換
Signature格式 | Java | Native |
---|---|---|
Ljava/lang/String; | String | jstring |
L+classname+; | 所有對象 | jobject |
[L+classname+; | Object[] | jobjectArray |
Ljava.lang.Class; | Class | jclass |
Ljava.lang.Throwable; | Throwable | jthrowable |
#4.4 Signature
Java函數(shù) | 對應的簽名 |
---|---|
void foo() | ()V |
float foo(int i) | (I)F |
long foo(int[] i) | ([I)J |
double foo(Class c) | (Ljava/lang/Class;)D |
boolean foo(int[] i,String s) | ([ILjava/lang/String;)Z |
String foo(int i) | (I)Ljava/lang/String; |
五院刁、JNIEnv介紹
JNIEnv是一個與線程相關(guān)的代表JNI環(huán)境的結(jié)構(gòu)體糯钙,JNIEnv提供了一些JNI系統(tǒng)函數(shù)。通過這些函數(shù)可以做到:
- 調(diào)用JAVA函數(shù)退腥。
- 操作jobject對象等很多事情任岸。
JNIEnv是一個線程相關(guān)的變量。由于線程相關(guān)狡刘,所以不能在一個線程中使用另一個線程的JNIEnv結(jié)構(gòu)體享潜,有一種情況當后臺線程收到一個網(wǎng)絡消息,而又需要由Native層函數(shù)主動回調(diào)Java層函數(shù)時嗅蔬,JNIEnv該如何處理剑按?
//全進程只有一個JavaVM對象疾就,所以可以保存,并且在任何地方使用都沒有問題艺蝴。
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved);
JavaVM和JNIEnv之間的關(guān)系:
- 調(diào)用JavaVM的AttachCurrentThread函數(shù)猬腰,就可以得到這個線程的JNIEnv結(jié)構(gòu)體。這樣就可以在后臺線程中回調(diào)Java函數(shù)猜敢。
- 另外姑荷,在后臺線程退出前,需要調(diào)用JavaVM的DetachCurrentThread的函數(shù)來釋放對應的資源缩擂。
/*
* C++ object wrapper.
*
* This is usually overlaid on a C struct whose first element is a
* JNINativeInterface*. We rely somewhat on compiler behavior.
*/
struct _JNIEnv {
/* do not rename this; it does not seem to be entirely opaque */
const struct JNINativeInterface* functions;
#if defined(__cplusplus)
......
#endif /*__cplusplus*/
};
#5.1 通過JNIEnv操作jobject
Java的引用類型除了少數(shù)幾個外鼠冕,最終在JNI層都會用jobject來表示對象的數(shù)據(jù)類型,操作jobject步驟:
#5.1.1 jfieldID和jmethodID介紹
jfieldID (*GetFieldID)(JNIEnv*, jclass clazz, const char *name, const char *sig);
jmethodID (*GetMethodID)(JNIEnv*, jclass clazz, const char *name, const char *sig);
其中胯盯,jclass代表Java類懈费,name表示成員函數(shù)或成員變量的名字,sig為這個函數(shù)和變量的簽名信息陨闹。
public:
MyMediaScannerClient(JNIEnv *env, jobject client)
: mEnv(env),
mClient(env->NewGlobalRef(client)),
mScanFileMethodID(0),
mHandleStringTagMethodID(0),
mSetMimeTypeMethodID(0)
{
ALOGV("MyMediaScannerClient constructor");
jclass mediaScannerClientInterface =
env->FindClass(kClassMediaScannerClient);
if (mediaScannerClientInterface == NULL) {
ALOGE("Class %s not found", kClassMediaScannerClient);
} else {
mScanFileMethodID = env->GetMethodID(
mediaScannerClientInterface,
"scanFile",
"(Ljava/lang/String;JJZZ)V");
mHandleStringTagMethodID = env->GetMethodID(
mediaScannerClientInterface,
"handleStringTag",
"(Ljava/lang/String;Ljava/lang/String;)V");
mSetMimeTypeMethodID = env->GetMethodID(
mediaScannerClientInterface,
"setMimeType",
"(Ljava/lang/String;)V");
}
}
將scanFile和handleStringTag函數(shù)的jmethodId保存為MyMediaScannerClient的成員變量楞捂,供后續(xù)使用。
#5.1.2 使用jfieldID和jmethodID
virtual status_t scanFile(const char* path, long long lastModified,
long long fileSize, bool isDirectory, bool noMedia)
{
ALOGV("scanFile: path(%s), time(%lld), size(%lld) and isDir(%d)",
path, lastModified, fileSize, isDirectory);
jstring pathStr;
if ((pathStr = mEnv->NewStringUTF(path)) == NULL) {
mEnv->ExceptionClear();
return NO_MEMORY;
}
//調(diào)用JNIEnv的CallVoidMethod函數(shù)趋厉,注意CallVoidMethod的參數(shù):
//第一個參數(shù)代表MediaScannerClient的jobject對象寨闹,
//第二個參數(shù)是函數(shù)scanFile的jmethodID,后面是Java中scanFile的參數(shù)君账。
mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified,
fileSize, isDirectory, noMedia);
mEnv->DeleteLocalRef(pathStr);
return checkAndClearExceptionFromCallback(mEnv, "scanFile");
}
通過JNIEnv輸出CallVoidMethod繁堡,再把jobject、jmethodID和對應的參數(shù)傳進去乡数,JNI層就能夠調(diào)用Java對象的函數(shù)了椭蹄。
#5.2 jstring介紹
Java中String為引用類型,JNI規(guī)范中單獨創(chuàng)建一個jstring類型來表示Java中的String類型净赴。
- 調(diào)用JNIEnv的NewString(JNIEnv *env,const jchar *unicodeChars,jsize len),可以從Native字符串得到一個jstring對象绳矩。可以把jstring對象看作是Java中String對象在JNI層的代表玖翅,也就是說jstring是一個Java String翼馆。由于Java String中存儲的是Unicode字符串,所以NewString函數(shù)的參數(shù)也必須是Unicode字符串金度。
- 調(diào)用JNIEnv的NewStringUTF將根據(jù)Native的一個UTF-8字符串得到一個jstring對象应媚。
- JNIEnv中GetStringChars和GetStringUTFChars函數(shù),它們可以將Java String對象轉(zhuǎn)換成本地字符串猜极。
- 如果在代碼中調(diào)用了上面幾個函數(shù)中姜,在做完相關(guān)工作后,就需要調(diào)用ReleaseStringChars或ReleaseStringUTFChars函數(shù)來對應地釋放資源跟伏,否則會導致JVM內(nèi)存泄漏丢胚。
static void
android_media_MediaScanner_processFile(
JNIEnv *env, jobject thiz, jstring path,
jstring mimeType, jobject client)
{
ALOGV("processFile");
// Lock already hold by processDirectory
MediaScanner *mp = getNativeScanner_l(env, thiz);
if (mp == NULL) {
jniThrowException(env, kRunTimeException, "No scanner available");
return;
}
if (path == NULL) {
jniThrowException(env, kIllegalArgumentException, NULL);
return;
}
const char *pathStr = env->GetStringUTFChars(path, NULL);
if (pathStr == NULL) { // Out of memory
return;
}
const char *mimeTypeStr =
(mimeType ? env->GetStringUTFChars(mimeType, NULL) : NULL);
if (mimeType && mimeTypeStr == NULL) { // Out of memory
// ReleaseStringUTFChars can be called with an exception pending.
env->ReleaseStringUTFChars(path, pathStr);
return;
}
MyMediaScannerClient myClient(env, client);
MediaScanResult result = mp->processFile(pathStr, mimeTypeStr, myClient);
if (result == MEDIA_SCAN_RESULT_ERROR) {
ALOGE("An error occurred while scanning file '%s'.", pathStr);
}
env->ReleaseStringUTFChars(path, pathStr);
if (mimeType) {
env->ReleaseStringUTFChars(mimeType, mimeTypeStr);
}
}
#5.3 JNI類型簽名介紹
Java中支持函數(shù)重載翩瓜,也就是說,可以定義同名但不同參數(shù)的函數(shù)嗜桌。因此奥溺,僅僅靠函數(shù)名沒辦法找到具體的函數(shù)。JNI技術(shù)中將參數(shù)類型和返回值類型的組合作為一個函數(shù)的簽名信息骨宠,有了簽名信息和函數(shù)名浮定,就能找到Java中的函數(shù)了。
Java中提供了javap的工具來幫助生成函數(shù)或變量的簽名信息层亿。
//XXX是編譯后的.class文件
javap -s -p XXX
C:\Users\hhu\Desktop\11\JNIDemo\app\src\main\java>javap -s -p com.next.hhu.jnidemo.StaticJNITest
Compiled from "StaticJNITest.java"
public class com.next.hhu.jnidemo.StaticJNITest {
public com.next.hhu.jnidemo.StaticJNITest();
descriptor: ()V
public static native int add(int, int);
descriptor: (II)I
static {};
descriptor: ()V
}
#5.3 垃圾回收
JNI提供了三種引用類型:
- Local Reference:本地引用桦卒。在JNI層函數(shù)中使用的非全局引用對象都是Local Reference,它包括函數(shù)調(diào)用時傳入的jobject和在JNI層函數(shù)中創(chuàng)建的jobject匿又。Local Reference最大特點是方灾,一旦JNI層函數(shù)返回,這些object就可能被垃圾回收碌更。
- Global Reference:全局引用裕偿,這種對象如不主動釋放,它永遠不會被垃圾回收痛单。
- Weak Global Reference:弱全局引用嘿棘,一種特殊的Global Reference,在運行過程中可能會被垃圾回收旭绒,因此在使用它之前鸟妙,需要調(diào)用JNIEnv的IsSameObject判斷它是否被回收了。
public:
MyMediaScannerClient(JNIEnv *env, jobject client)
: mEnv(env),
mClient(env->NewGlobalRef(client)),
{
......
}
virtual ~MyMediaScannerClient()
{
ALOGV("MyMediaScannerClient destructor");
mEnv->DeleteGlobalRef(mClient);
}
#5.4 JNI中的異常處理
如果調(diào)用JNIEnv的某些函數(shù)出錯了挥吵,則會產(chǎn)生一個異常重父,但這個異常不會中斷本地函數(shù)的執(zhí)行,直到從JNI層返回到Java層后忽匈,虛擬機才會拋出這個異常房午。雖然在JNI層中產(chǎn)生的異常不會中斷本地函數(shù)的運行,但一旦產(chǎn)生異常后丹允,就只能做一些資源清理工作歪沃。所以安全編碼顯得十分重要。