JNI 和 NDK
Android.mk
LOCAL_PATH := $(call my-dir)
- 每个Android.mk文件都必须以定义LOCAL_PATH变量开始。其目的是为了定位源文件的位置。 生成系统提供的宏函数(macro function)‘my-dir'用来返回当前路径(即放有Android.mk文件的文件夹)
include $(CLEAR_VARS)
- CLEAR_VARS变量是生成系统提供的,它指向一个特殊的GNU Makefile。这个Makefile将会为你自动清除许多名为LOCAL_XXX的变量。
LOCAL_MODULE := XXX
- 标识每个组件,必须定义LOCAL_MODULE变量。这个名字必须要唯一的并且不能包含空格。注意:生成系统会自动地为相应生成的文件加入前缀或后缀。换言之,一个名叫foo的共享库组件会生成'libfoo.so'。
LOCAL_SRC_FILES := XXX
- LOCAL_SRC_FILES变量必须包含一系列将被构建和组合成组件的C/C++源文件。注意:你不需要列出头文件或include文件,因为生成系统会为你自动计算出源文件的依赖关系。仅仅列出那些将直接传给编译器的源文件足矣。
include $(BUILD_SHARED_LIBRARY)
表示编译成动态库include $(BUILD_STATIC_LIBRARY)
表示编译成静态库include $(BUILD_EXECUTABLE)
表示编译成可执行程序
Application.mk
APP_ABI
:缺省情况下,NDK build system会产生'armeabi' ABI。
jni编程的一般步骤
- 在java层定义需要的
native
方法 - 通过
javah 包名+类名
生成.h头文件 - 根据这个头文件写对应的C/C++代码
- 编写
Android.mk
和Application.mk
文件 - 通过
ndk-build
编译成so文件
JNI的实现原理
Android系统在启动启动过程中,先启动Kernel创建init进程,紧接着由init进程fork第一个横穿Java和C/C++的进程,即Zygote进程。Zygote启动过程中会调用AndroidRuntime.cpp中的startVm创建虚拟机,VM创建完成后,紧接着调用startReg完成虚拟机中的JNI方法注册。
JNI方式注册无非是Android系统启动过程中Zygote注册以及通过System.loadLibrary方式注册,对于系统启动过程注册的,可以通过查询AndroidRuntime.cpp中的gRegJNI是否存在对应的register方法,如果不存在,则大多数情况下是通过LoadLibrary方式来注册。
System.java:
public static void loadLibrary(String libName) {
Runtime.getRuntime().loadLibrary(libName, VMStack.getCallingClassLoader());
}
Runtime.java:
void loadLibrary(String libraryName, ClassLoader loader) {
//loader不会空,则进入该分支
if (loader != null) {
//查找库所在路径
String filename = loader.findLibrary(libraryName);
if (filename == null) {
throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
System.mapLibraryName(libraryName) + "\"");
}
//加载库
String error = doLoad(filename, loader);
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
return;
}
//loader为空,则会进入该分支
String filename = System.mapLibraryName(libraryName);
List<String> candidates = new ArrayList<String>();
String lastError = null;
for (String directory : mLibPaths) {
String candidate = directory + filename;
candidates.add(candidate);
if (IoUtils.canOpenReadOnly(candidate)) {
//加载库
String error = doLoad(candidate, loader);
if (error == null) {
return;//加载成功
}
lastError = error;
}
}
if (lastError != null) {
throw new UnsatisfiedLinkError(lastError);
}
throw new UnsatisfiedLinkError("Library " + libraryName + " not found; tried " + candidates);
}
上面的代码走了2步:
- 获取到library path。对于这一点,上面的那个函数,依据于所传递的ClassLoader的不同,会有两种不同的方法。如果ClassLoader非空,则会利用ClassLoader的findLibrary()方法来取library的path。而如果ClassLoader为空,则会首先依据传递进来的library name,获取到library file的name,比如传递“hello”进来,它的library file name,经过System.mapLibraryName(libraryName)将会是“libhello.so”;然后再在一个path list(即上面那段code中的mLibPaths)中查找到这个library file,并最终确定library 的path。
- 调用nativeLoad()这个native方法来load library
》》》上面有2个问题 《《《
- library path是有哪些?
ClassLoader -> PathClassLoader -> BaseDexClassLoader:
@Override
public String findLibrary(String name) {
return pathList.findLibrary(name);
}
...
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.originalPath = dexPath;
this.originalLibraryPath = libraryPath;
this.pathList =
new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
DexPathList:
this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
private static File[] splitLibraryPath(String path) {
ArrayList<File> result = splitPaths(
path, System.getProperty("java.library.path", "."), true);
return result.toArray(new File[result.size()]);
}
总结一下,ClassLoader的那个findLibrary()实际上会在两个部分的folder中去寻找System.loadLibrary()要load的那个library,一个部分是,构造ClassLoader时,传进来的那个library path,即是app folder,另外一个部分是system property。
- /vendor/lib
- /system/lib
- /data/app-lib/XXX
~
- load library的过程
Runtime.java:
private String doLoad(String name, ClassLoader loader) {
String ldLibraryPath = null;
if (loader != null && loader instanceof BaseDexClassLoader) {
ldLibraryPath = ((BaseDexClassLoader) loader).getLdLibraryPath();
}
synchronized (this) {
return nativeLoad(name, loader, ldLibraryPath);
}
}
nativeLoad()这个函数的实现是在dalvik/vm/native/java_lang_Runtime.cpp
中,它主要做了以下一些事情:
- 调用
dlopen()
打开一个so文件,创建一个handle。 - 调用
dlsym()
函数,查找到so文件中的JNI_OnLoad()
这个函数的函数指针。 - 执行上一步找到的那个
JNI_OnLoad()
函数。
~
- JNI_OnLoad
以MediaPlayer
为例来说明。
[-> android_media_MediaPlayer.cpp]
jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
JNIEnv* env = NULL;
//【见3.3】 注册JNI方法
if (register_android_media_MediaPlayer(env) < 0) {
goto bail;
}
...
}
[-> android_media_MediaPlayer.cpp]
static int register_android_media_MediaPlayer(JNIEnv *env)
{
//【见3.4】
return AndroidRuntime::registerNativeMethods(env,
"android/media/MediaPlayer", gMethods, NELEM(gMethods));
}
static JNINativeMethod gMethods[] = {
{"prepare", "()V", (void *)android_media_MediaPlayer_prepare},
{"_start", "()V", (void *)android_media_MediaPlayer_start},
{"_stop", "()V", (void *)android_media_MediaPlayer_stop},
{"seekTo", "(I)V", (void *)android_media_MediaPlayer_seekTo},
{"_release", "()V", (void *)android_media_MediaPlayer_release},
{"native_init", "()V", (void *)android_media_MediaPlayer_native_init},
...
};
这里涉及到结构体JNINativeMethod,其定义在jni.h文件:
typedef struct {
const char* name; //Java层native函数名
const char* signature; //Java函数签名,记录参数类型和个数,以及返回值类型
void* fnPtr; //Native层对应的函数指针
} JNINativeMethod;
[-> AndroidRuntime.cpp]
int AndroidRuntime::registerNativeMethods(JNIEnv* env,
const char* className, const JNINativeMethod* gMethods, int numMethods)
{
//【见3.5】
return jniRegisterNativeMethods(env, className, gMethods, numMethods);
}
[-> JNIHelp.cpp]
extern "C" int jniRegisterNativeMethods(C_JNIEnv* env, const char* className,
const JNINativeMethod* gMethods, int numMethods)
{
JNIEnv* e = reinterpret_cast<JNIEnv*>(env);
scoped_local_ref<jclass> c(env, findClass(env, className));
if (c.get() == NULL) {
e->FatalError("");//无法查找native注册方法
}
//【见3.6】 调用JNIEnv结构体的成员变量
if ((*env)->RegisterNatives(e, c.get(), gMethods, numMethods) < 0) {
e->FatalError("");//native方法注册失败
}
return 0;
}
再往下深入就到了虚拟机内部吧。 总之,这个过程完成了gMethods数组中的方法的映射关系,比如java层的native_init()方法,映射到native层的android_media_MediaPlayer_native_init()方法。
其他
(一)垃圾回收 对于Java开发人员来说无需关系垃圾回收,完全由虚拟机GC来负责垃圾回收,而对于JNI开发人员,对于内存释放需要谨慎处理,需要的时候申请,使用完记得释放内容,以免发生内存泄露。在JNI提供了三种Reference类型,Local Reference(本地引用), Global Reference(全局引用), Weak Global Reference(全局弱引用)。其中Global Reference如果不主动释放,则一直不会释放;对于其他两个类型的引用都是释放的可能性,那是不是意味着不需要手动释放呢?答案是否定的,不管是这三种类型的那种引用,都尽可能在某个内存不再需要时,立即释放,这对系统更为安全可靠,以减少不可预知的性能与稳定性问题。
另外,ART虚拟机在GC算法有所优化,为了减少内存碎片化问题,在GC之后有可能会移动对象内存的位置,对于Java层程序并没有影响,但是对于JNI程序可要小心了,对于通过指针来直接访问内存对象是,Dalvik能正确运行的程序,ART下未必能正常运行。
(二)异常处理 Java层出现异常,虚拟机会直接抛出异常,这是需要try..catch或者继续往外throw。但是对于JNI出现异常时,即执行到JNIEnv中某个函数异常时,并不会立即抛出异常来中断程序的执行,还可以继续执行内存之类的清理工作,直到返回到Java层时才会抛出相应的异常。
另外,Dalvik虚拟机有些情况下JNI函数出错可能返回NULL,但ART虚拟机在出错时更多的是抛出异常。这样导致的问题就可能是在Dalvik版本能正常运行的程序,在ART虚拟机上由于没有正确处理异常而崩溃。