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编程的一般步骤

  1. 在java层定义需要的native方法
  2. 通过javah 包名+类名生成.h头文件
  3. 根据这个头文件写对应的C/C++代码
  4. 编写Android.mkApplication.mk文件
  5. 通过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步:

  1. 获取到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。
  2. 调用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。

  1. /vendor/lib
  2. /system/lib
  3. /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中,它主要做了以下一些事情:

  1. 调用dlopen()打开一个so文件,创建一个handle。
  2. 调用dlsym()函数,查找到so文件中的JNI_OnLoad()这个函数的函数指针。
  3. 执行上一步找到的那个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虚拟机上由于没有正确处理异常而崩溃。

results matching ""

    No results matching ""