前语

无论是 Android 开发者仍是 Java 工程师应该都有运用过 JNI 开发,但对于 JVM 怎么加载 so、Android 体系怎么加载 so,或许鲜有时间了解。

本文经过代码、流程解释,带大家快速了解其加载原理,扫清困惑。

1. System#load() + loadLibrary()

1.1 load()

System 供给的 load() 用于指定 so 的完好的途径名且带文件后缀并加载,等同于调用 Runtime 类供给的 load()。

If the filename argument, when stripped of any platform-specific library prefix, path, and file extension, indicates a library whose name is, for example, L, and a native library called L is statically linked with the VM, then the JNI_OnLoad_L function exported by the library is invoked rather than attempting to load a dynamic library.

Eg.

System.load("/sdcard/path/libA.so")

进程简述:

  1. 经过 Reflection 获取调用来历的 Class 实例

  2. 接着调用 Runtime 的 load0() 完结

    • load0() 首要获取体系的 SecurityManager

    • 当 SecurityManager 存在的话查看方针 so 文件的拜访权限:权限缺乏的话打印拒绝信息、抛出 SecurityException ,假如 name 参数为空,抛出 NullPointerException

    • 假如 so 文件名非绝对途径的话,并不支撑,并抛出 UnsatisfiedLinkError,message 为:

      Expecting an absolute path of the library: xxx

    • 针对 so 文件的权限查看和称号查看均经过的话,持续调用 ClassLoader 的 loadLibrary() 完结,需求留意的是绝对途径参数为 true

// java/lang/System.java
    public static void load(String filename) {
        Runtime.getRuntime().load0(Reflection.getCallerClass(), filename);
    }
// java/lang/Runtime.java
    synchronized void load0(Class<?> fromClass, String filename) {
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkLink(filename);
        }
        if (!(new File(filename).isAbsolute())) {
            throw new UnsatisfiedLinkError(
                "Expecting an absolute path of the library: " + filename);
        }
        ClassLoader.loadLibrary(fromClass, filename, true);
    }

1.2 loadLibrary()

System 类供给的 loadLibrary() 用于指定 so 的称号并加载,等同于调用 Runtime 类供给的 loadLibrary()。在 Android 平台体系会主动去体系目录(/system/lib64/)、应用 lib 目录(/data/app/xxx/lib64/)下去找 libname 参数拼接了 lib 前缀的库文件。

The libname argument must not contain any platform specific prefix, file extension or path.

If a native library called libname is statically linked with the VM, then the JNI_OnLoad_libname function exported by the library is invoked.

Eg.

System.loadLibrary("A")

进程简述:

  1. 同样经过 Reflection 获取调用来历的 Class 实例

  2. 接着调用 Runtime 的 loadLibrary0() 完结

    • loadLibrary0() 首要获取体系的 SecurityManager,并查看方针 so 文件的拜访权限:权限缺乏或文件名为空的话和上面相同抛出 Exception

    • 保证 so 称号不包含 /,反之,抛出 UnsatisfiedLinkError,message 为:

      Directory separator should not appear in library name: xxx

    • 查看经过后,同样调用 ClassLoader 的 loadLibrary() 完结持续下一步,只不过绝对途径参数为 false

// java/lang/System.java
    public static void loadLibrary(String libname) {
        Runtime.getRuntime().loadLibrary0(Reflection.getCallerClass(), libname);
    }
// java/lang/Runtime.java
    synchronized void loadLibrary0(Class<?> fromClass, String libname) {
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkLink(libname);
        }
        if (libname.indexOf((int)File.separatorChar) != -1) {
            throw new UnsatisfiedLinkError(
    "Directory separator should not appear in library name: " + libname);
        }
        ClassLoader.loadLibrary(fromClass, libname, false);
    }

2. ClassLoader#loadLibrary()

上面的调用栈能够看到无论是 load() 仍是 loadLibrary() 终究都是调用 ClassLoaderloadLibrary(),首要差异在于 name 参数是 lib 完好途径、仍是 lib 称号,以及是否是绝对途径参数。

  1. 首要经过 getClassLoader() 取得加载源所属的 ClassLoader 实例

  2. 保证寄存 libraries 途径的字符串数组 sys_paths 不为空

    • 尚且为空的话,调用 initializePath(“java.library.path”) 先初始化 usr 途径字符串数组,再调用 initializePath(“sun.boot.library.path”) 初始化 system 途径字符串数组。initializePath() 详细见下章节
  3. 依据是否 isAbsolute 决议是否直接加载 library

    • name 是绝对途径的话,直接创立 File 实例,调用 loadLibrary0(),持续加载该文件。详细见下章节

      • 查看 loadLibrary0 的成果:true:即表明加载成功,完毕;false:即表明加载失利,抛出 UnsatisfiedLinkError

        Can’t load xxx

    • name 非绝对途径而且获取的 ClassLoader 存在的话,经过 findLibrary() ,依据 so 称号取得 lib 绝对途径,并创立指向该途径的 File 实例 libfile

      • 并保证该文件的途径是绝对途径。反之,抛出 UnsatisfiedLinkError

        ClassLoader.findLibrary failed to return an absolute path: xxx

      • 尔后也是调用 loadLibrary0() 持续加载该文件,并查看 loadLibrary0 的成果,处理同上

  4. 假使 ClassLoader 不存在:遍历 system 途径字符串数组的元素,

    • 经过 mapLibraryName() 别离将 lib name 映射到平台相关的 lib 完好称号并回来,详细见下章节

    • 创立当时遍历的 path 下 libfile 实例

    • 调用 loadLibrary0() 持续加载该文件,并查看成果:

      • true 则直接完毕

      • false 的话,经过 mapAlternativeName() 获取该 lib 或许存在的替代文件名,比方将后缀替换为 jnilib

        • 假如再度 map 后的 libfile 不为空,调用 loadLibrary0() 再度加载该文件并查看成果,true 则直接完毕;反之,进入下一次循环
  5. 至此,假如仍未成功找到 library 文件,则在 ClassLoader 存在的情况下,到 usr 途径字符串数组中查找

    • 遍历 usr 途径字符串数组的元素
      • 后续逻辑和上述一致,仅仅 map 时分的前缀不同,是 usr_paths 的元素
  6. 终究进行默许处理,即抛出 UnsatisfiedLinkError,提示在 java.library.path propery 代表的途径下也未找到 so 文件

no xx in java.library.path

// java/lang/ClassLoader.java
    static void loadLibrary(Class<?> fromClass, String name,
                            boolean isAbsolute) {
        ClassLoader loader =
            (fromClass == null) ? null : fromClass.getClassLoader();
        if (sys_paths == null) {
            usr_paths = initializePath("java.library.path");
            sys_paths = initializePath("sun.boot.library.path");
        }
        if (isAbsolute) {
            if (loadLibrary0(fromClass, new File(name))) {
                return;
            }
            throw new UnsatisfiedLinkError("Can't load library: " + name);
        }
        if (loader != null) {
            String libfilename = loader.findLibrary(name);
            if (libfilename != null) {
                File libfile = new File(libfilename);
                if (!libfile.isAbsolute()) {
                    throw new UnsatisfiedLinkError(...);
                }
                if (loadLibrary0(fromClass, libfile)) {
                    return;
                }
                throw new UnsatisfiedLinkError("Can't load " + libfilename);
            }
        }
        for (int i = 0 ; i < sys_paths.length ; i++) {
            File libfile = new File(sys_paths[i], System.mapLibraryName(name));
            if (loadLibrary0(fromClass, libfile)) {
                return;
            }
            libfile = ClassLoaderHelper.mapAlternativeName(libfile);
            if (libfile != null && loadLibrary0(fromClass, libfile)) {
                return;
            }
        }
        if (loader != null) {
            for (int i = 0 ; i < usr_paths.length ; i++) {
                File libfile = new File(usr_paths[i],
                                        System.mapLibraryName(name));
                if (loadLibrary0(fromClass, libfile)) {
                    return;
                }
                libfile = ClassLoaderHelper.mapAlternativeName(libfile);
                if (libfile != null && loadLibrary0(fromClass, libfile)) {
                    return;
                }
            }
        }
        // Oops, it failed
        throw new UnsatisfiedLinkError("no " + name + " in java.library.path");
    }

3. ClassLoader#initializePath()

System 中获取对应 property 代表的 path 到数组中。

  1. 先调用 getProperty() 从 JVM 中取出配置的途径,默许的是 “”

    • 其中的 checkKey() 将查看 key 称号是否合法,null 的话抛出 NullPointerException

      key can’t be null

      假如为"",抛出 IllegalArgumentException

      key can’t be empty

    • 后面经过 getSecurityManager() 获取 SecurityManager 实例,查看是否存在该 property 的拜访权限

  2. 假如允许引证途径元素而且 \ 存在的话,将途径字符串的 char 取出进行拼接、计算得到途径字符串数组

  3. 反之经过 indexOf(/) 统计 / 出现的次数,并创立一个 / 次数 + 1 的数组

  4. 遍历该途径字符串,经过 substring() 将各 / 的中间 path 内容提取到上述数组中

  5. 终究回来得到的 path 数组

// java/lang/ClassLoader.java
    private static String[] initializePath(String propname) {
        String ldpath = System.getProperty(propname, "");
        String ps = File.pathSeparator;
        ...
        i = ldpath.indexOf(ps);
        n = 0;
        while (i >= 0) {
            n++;
            i = ldpath.indexOf(ps, i + 1);
        }
        String[] paths = new String[n + 1];
        n = i = 0;
        j = ldpath.indexOf(ps);
        while (j >= 0) {
            if (j - i > 0) {
                paths[n++] = ldpath.substring(i, j);
            } else if (j - i == 0) {
                paths[n++] = ".";
            }
            i = j + 1;
            j = ldpath.indexOf(ps, i);
        }
        paths[n] = ldpath.substring(i, ldlen);
        return paths;
    }

4. ClassLoader#findLibrary()

findLibrary() 将到 ClassLoader 中查找 lib,取决于各 JVM 的详细完结。比方能够看看 Android 上的完结。

  1. DexPathList 的详细完结中调用
  2. 首要经过 System 类的 mapLibraryName() 中取得 mapping 后的 lib 全名,细节见下章节
  3. 遍历寄存 native lib 途径元素数组 nativeLibraryPathElements
  4. 逐一调用各元素的 findNativeLibrary() 完结去寻觅
  5. 一经找到立即回来,遍历完毕仍未发现的话回来 null
// android/libcore/dalvik/src/main/java/dalvik/system/
// BaseDexClassLoader.java
   public String findLibrary(String name) {
        return pathList.findLibrary(name);
    }
// android/libcore/dalvik/src/main/java/dalvik/system/
// DexPathList.java
    public String findLibrary(String libraryName) {
        // 到 System 中取得 mapping 后的 lib 全名
        String fileName = System.mapLibraryName(libraryName);
        // 到寄存 native lib 途径数组中遍历
        for (NativeLibraryElement element : nativeLibraryPathElements) {
            String path = element.findNativeLibrary(fileName);
            // 一旦找到立即回来并完毕,反之进入下一次循环
            if (path != null) {
                return path;
            }
        }
        // 途径中全找遍了,仍未找到则回来 null
        return null;
    }

4.1 System#mapLibraryName()

mapLibraryName() 的效果很简单,行将 lib 称号 mapping 到完好格式的称号,比方输入 opencv 得到的是 libopencv.so。假如遇到称号为空或者长度超上限 240 的话,将抛出相应 Exception。

// java/lang/System.java
public static native String mapLibraryName(String libname);

其是 native 办法,详细完结位于 JDK Native Source Code 中,可在如下网站中看到:

  • hg.openjdk.java.net/jdk8/jdk8/j…
// native/java/lang/System.c
#define JNI_LIB_PREFIX "lib"
#define JNI_LIB_SUFFIX ".so"
Java_java_lang_System_mapLibraryName(JNIEnv *env, jclass ign, jstring libname)
{
    // 界说终究称号的 Sring 长度变量
    int len;
    // 并获取 lib 前缀、后缀的字符串常量的长度
    int prefix_len = (int) strlen(JNI_LIB_PREFIX);
    int suffix_len = (int) strlen(JNI_LIB_SUFFIX);
    // 界说暂时的寄存终究称号的 char 数组
    jchar chars[256];
    // 假如 libname 参数为空,抛出 NPE
    if (libname == NULL) {
        JNU_ThrowNullPointerException(env, 0);
        return NULL;
    }
    // 获取 libname 长度
    len = (*env)->GetStringLength(env, libname);
    // 假如大于 240 的话抛出 IllegalArgumentException
    if (len > 240) {
        JNU_ThrowIllegalArgumentException(env, "name too long");
        return NULL;
    }
    // 将前缀 ”lib“ 的字符拷贝到暂时的 char 数组头部
    cpchars(chars, JNI_LIB_PREFIX, prefix_len);
    // 将 lib 称号从字符串里拷贝到 char 数组的 “lib” 后面
    (*env)->GetStringRegion(env, libname, 0, len, chars + prefix_len);
    // 更新称号长度为:前缀+ lib 称号
    len += prefix_len;
    // 将后缀 ”.so“ 的字符拷贝到暂时的 char 数组里的 lib 称号后
    cpchars(chars + len, JNI_LIB_SUFFIX, suffix_len);
    // 再次更新称号长度为:前缀+ lib 称号 + 后缀
    len += suffix_len;
    // 从 char 数组里提取当时长度的复数 char 成员到新创立的 String 目标中回来
    return (*env)->NewString(env, chars, len);
}
static void cpchars(jchar *dst, char *src, int n)
{
    int i;
    for (i = 0; i < n; i++) {
        dst[i] = src[i];
    }
}

逻辑很清晰,查看 lib 称号参数是否合法,之后便是将称号别离加上前后缀到暂时字符数组中,终究转为字符串回来。

4.2 nativeLibraryPathElements()

nativeLibraryPathElements 数组来历于获取到的一切 native Library 目录后转化而来。

// android/libcore/dalvik/src/main/java/dalvik/system/
// DexPathList.java
    public DexPathList(ClassLoader definingContext, String librarySearchPath) {
        ...
        this.nativeLibraryPathElements = makePathElements(getAllNativeLibraryDirectories());
    }

一切 native Library 目录除了包含应用自身的 library 目录列表以外,还包括了体系的列表部分。

// android/libcore/dalvik/src/main/java/dalvik/system/
// DexPathList.java
    private List<File> getAllNativeLibraryDirectories() {
        List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
        allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);
        return allNativeLibraryDirectories;
    }
    /** List of application native library directories. */
    private final List<File> nativeLibraryDirectories;
    /** List of system native library directories. */
    private final List<File> systemNativeLibraryDirectories;

应用自身的 library 目录列表来自于 DexPathList 初始化时传入的 librarySearchPath 参数,splitPaths() 担任去该 path 下遍历各级目录得到对应数组。

// android/libcore/dalvik/src/main/java/dalvik/system/
// DexPathList.java
    public DexPathList(ClassLoader definingContext, String librarySearchPath) {
        ...
        this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
    }
    private static List<File> splitPaths(String searchPath, boolean directoriesOnly) {
        List<File> result = new ArrayList<>();
        if (searchPath != null) {
            for (String path : searchPath.split(File.pathSeparator)) {
                if (directoriesOnly) {
                    try {
                        StructStat sb = Libcore.os.stat(path);
                        if (!S_ISDIR(sb.st_mode)) {
                            continue;
                        }
                    } catch (ErrnoException ignored) {
                        continue;
                    }
                }
                result.add(new File(path));
            }
        }
        return result;
    }

体系列表则来自于体系的 path 途径,调用 splitPaths() 的第二个参数不同,促使其在分割的时分只处理目录类型的部分,纯文件的话跳过。

// android/libcore/dalvik/src/main/java/dalvik/system/
// DexPathList.java
    public DexPathList(ClassLoader definingContext, String librarySearchPath) {
        ...
        this.systemNativeLibraryDirectories =
                splitPaths(System.getProperty("java.library.path"), true);
        ...
    }

拿到 path 文件列表之后便是调用 makePathElements 转成对应元素数组。

  1. 按照列表长度创立等长的 Element 数组
  2. 遍历 path 列表
  3. 假如 path 包含 “!/” 的话,将其拆分为 path 和 zipDir 两部分,并创立 NativeLibraryElement 实例
  4. 反之,假如是目录的话,直接用 path 创立 NativeLibraryElement 实例,zipDir 参数则为空
// android/libcore/dalvik/src/main/java/dalvik/system/
// DexPathList.java
    private static NativeLibraryElement[] makePathElements(List<File> files) {
        NativeLibraryElement[] elements = new NativeLibraryElement[files.size()];
        int elementsPos = 0;
        for (File file : files) {
            String path = file.getPath();
            if (path.contains(zipSeparator)) {
                String split[] = path.split(zipSeparator, 2);
                File zip = new File(split[0]);
                String dir = split[1];
                elements[elementsPos++] = new NativeLibraryElement(zip, dir);
            } else if (file.isDirectory()) {
                // We support directories for looking up native libraries.
                elements[elementsPos++] = new NativeLibraryElement(file);
            }
        }
        if (elementsPos != elements.length) {
            elements = Arrays.copyOf(elements, elementsPos);
        }
        return elements;
    }

4.3 findNativeLibrary()

findNativeLibrary() 将先保证当 zip 目录存在的情况下内部处理 zip 的 ClassPathURLStreamHandler 实例履行了创立。

  • 假如 zip 目录不存在(一般情况下都是不存在的)直接判断该途径下 lib 文件是否可读,YES 则回来 path/name、反之回来 null
  • zip 目录存在而且 ClassPathURLStreamHandler 实例也创立完毕的话,查看该 name 的 zip 文件的存在。并在 YES 的情况下,在 path 和 name 之间跟上 zip 目录并回来,即:path/!/zipDir/name
// DexPathList.java
// android/.../libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
    private static final String zipSeparator = "!/";
    static class NativeLibraryElement {
        public String findNativeLibrary(String name) {
            // 保证 element 初始化完结
            maybeInit();
            if (zipDir == null) {
                // 假如 zip 目录为空,则直接创立该 path 下该文件的 File 实例
                // 可读的话则回来
                String entryPath = new File(path, name).getPath();
                if (IoUtils.canOpenReadOnly(entryPath)) {
                    return entryPath;
                }
            } else if (urlHandler != null) {
                // zip 目录而且 urlHandler 都存在
                // 创立该 zip 目录下 lib 文件的完好称号
                String entryName = zipDir + '/' + name;
                // 假如该称号的压缩包是否存在的话
                if (urlHandler.isEntryStored(entryName)) {
                    // 回来:途径/zip目录/lib 称号的成果出去
                    return path.getPath() + zipSeparator + entryName;
                }
            }
            return null;
        }
        // 首要是保证在 zipDir 不为空的情况下
        // 内部处理 zip 的 urlHandler 实例已经创立完毕
        public synchronized void maybeInit() {
            ...
        }
    }

5. ClassLoader#loadLibrary0()

  1. 调用静态内部类 NativeLibrary 的 native 办法 findBuiltinLib() 查看是否是内置的动态链接库,细节见如下章节

    • 假如不是内置的 library,经过 AccessController 查看该 library 文件是否存在
      • 不存在则加载失利并完毕
      • 存在则到本 ClassLoader 已加载 library 的 nativeLibraries Vector 或体系 class 的已加载 library Vector systemNativeLibraries 中查找是否加载过
        • 已加载过则完毕
        • 反之,持续加载的使命
  2. 到一切 ClassLoader 已加载过的 library Vector loadedLibraryNames 里再次查看是否加载过,假如不存在的话,抛出 UnsatisfiedLinkError:

    Native Library xxx already loaded in another classloader

  3. 到正在加载/卸载 library 的 nativeLibraryContext Stack 中查看是否已经处理中了

    • 存在而且 ClassLoader 来历匹配,则完毕加载

    • 存在但 ClassLoader 来历不同,则抛出 UnsatisfiedLinkError:

      Native Library xxx is being loaded in another classloader

    • 反之,持续加载的使命

  4. 依据 ClassLoader、library 称号、是否内置等信息,创立 NativeLibrary 实例并入 nativeLibraryContext 栈

  5. 尔后,交由 NativeLibrary load,细节亦见如下章节,并在 load 后出栈

  6. 终究依据 load 的成果决议是否将加载记录到对应的 Vector 傍边

// java/lang/ClassLoader.java
    private static boolean loadLibrary0(Class<?> fromClass, final File file) {
        // 获取是否是内置动态链接库
        String name = NativeLibrary.findBuiltinLib(file.getName());
        boolean isBuiltin = (name != null);
        if (!isBuiltin) {
            // 不是内置的话,查看文件是否存在
            boolean exists = AccessController.doPrivileged(
                new PrivilegedAction<Object>() {
                    public Object run() {
                        return file.exists() ? Boolean.TRUE : null;
                    }})
                != null;
            if (!exists) {
                return false;
            }
            try {
                name = file.getCanonicalPath();
            } catch (IOException e) {
                return false;
            }
        }
        ClassLoader loader =
            (fromClass == null) ? null : fromClass.getClassLoader();
        Vector<NativeLibrary> libs =
            loader != null ? loader.nativeLibraries : systemNativeLibraries;
        synchronized (libs) {
            int size = libs.size();
            // 查看是否已经加载过
            for (int i = 0; i < size; i++) {
                NativeLibrary lib = libs.elementAt(i);
                if (name.equals(lib.name)) {
                    return true;
                }
            }
            synchronized (loadedLibraryNames) {
                // 再次查看一切 library 加载前史中是否存在
                if (loadedLibraryNames.contains(name)) {
                    throw new UnsatisfiedLinkError(...);
                }
                int n = nativeLibraryContext.size();
                // 查看是否已经在加载中了
                for (int i = 0; i < n; i++) {
                    NativeLibrary lib = nativeLibraryContext.elementAt(i);
                    if (name.equals(lib.name)) {
                        if (loader == lib.fromClass.getClassLoader()) {
                            return true;
                        } else {
                            throw new UnsatisfiedLinkError(...);
                        }
                    }
                }
                // 创立 NativeLibrary 实例持续加载
                NativeLibrary lib = new NativeLibrary(fromClass, name, isBuiltin);
                // 并在加载前后压栈和出栈
                nativeLibraryContext.push(lib);
                try {
                    lib.load(name, isBuiltin);
                } finally {
                    nativeLibraryContext.pop();
                }
                // 加载成功的将该 library 称号缓存到 vector 中
                if (lib.loaded) {
                    loadedLibraryNames.addElement(name);
                    libs.addElement(lib);
                    return true;
                }
                return false;
            }
        }
    }

5.1 findBuiltinLib()

  1. 首要一如既往地先查看 library name 是否为空,为空则抛出 Error

    NULL filename for native library

  2. 将 string 类型的称号转为 char 指针,失利的话抛出 OutOfMemoryError

  3. 查看称号长度是否短于最起码的 lib.so 几位,失利的话回来 NULL 完毕

  4. 创立 library 称号指针 libName 并分配内存

  5. 从 char 指针提取 libxxx.so 中 xxx.so 部分到 libName 中

  6. 将 libName 中 .so 的 . 位置替换成 \0

  7. 调用 findJniFunction() 依据 handle 指针,library 称号查看该 library 的 JNI_OnLoad() 是否存在

    • 存在则开释 libName 内存并回来该函数地址
    • 反之,开释内存并回来 NULL 完毕
// native/java/lang/ClassLoader.c
Java_java_lang_ClassLoader_00024NativeLibrary_findBuiltinLib
  (JNIEnv *env, jclass cls, jstring name)
{
    const char *cname;
    char *libName;
    ...
    // 查看称号是否为空
    if (name == NULL) {
        JNU_ThrowInternalError(env, "NULL filename for native library");
        return NULL;
    }
    procHandle = getProcessHandle();
    cname = JNU_GetStringPlatformChars(env, name, 0);
    // 查看 char 称号指针是否为空
    if (cname == NULL) {
        JNU_ThrowOutOfMemoryError(env, NULL);
        return NULL;
    }
    // 查看称号长度
    len = strlen(cname);
    if (len <= (prefixLen+suffixLen)) {
        JNU_ReleaseStringPlatformChars(env, name, cname);
        return NULL;
    }
    // 提取 library 称号(取出前后缀)
    libName = malloc(len + 1); //+1 for null if prefix+suffix == 0
    if (libName == NULL) {
        JNU_ReleaseStringPlatformChars(env, name, cname);
        JNU_ThrowOutOfMemoryError(env, NULL);
        return NULL;
    }
    if (len > prefixLen) {
        strcpy(libName, cname+prefixLen);
    }
    JNU_ReleaseStringPlatformChars(env, name, cname);
    libName[strlen(libName)-suffixLen] = '\0';
    // 查看 JNI_OnLoad() 开释存在
    ret = findJniFunction(env, procHandle, libName, JNI_TRUE);
    if (ret != NULL) {
        lib = JNU_NewStringPlatform(env, libName);
        free(libName);
        return lib;
    }
    free(libName);
    return NULL;
}

5.2 findJniFunction()

findJniFunction() 用于到 library 指针、已加载/卸载的 JNI 数组中查找该 library 称号所对应的 JNI_ONLOAD、JNI_ONUNLOAD 的函数地址。

// native/java/lang/ClassLoader.c
static void *findJniFunction(JNIEnv *env, void *handle,
                                    const char *cname, jboolean isLoad) {
    const char *onLoadSymbols[] = JNI_ONLOAD_SYMBOLS;
    const char *onUnloadSymbols[] = JNI_ONUNLOAD_SYMBOLS;
    void *entryName = NULL;
    ...
    // 假如是加载,则到 JNI_ONLOAD_SYMBOLS 中获取函数数组和长度
    if (isLoad) {
        syms = onLoadSymbols;
        symsLen = sizeof(onLoadSymbols) / sizeof(char *);
    } else {
        // 反之,则到 JNI_ONUNLOAD_SYMBOLS 中获取卸载函数数组和长度
        syms = onUnloadSymbols;
        symsLen = sizeof(onUnloadSymbols) / sizeof(char *);
    }
    // 遍历该数组,调用 JVM_FindLibraryEntry()
    // 逐一查找 JNI_On(Un)Load<_libname> function 是否存在
    for (i = 0; i < symsLen; i++) {
        // cname + sym + '_' + '\0'
        if ((len = (cname != NULL ? strlen(cname) : 0) + strlen(syms[i]) + 2) >
            FILENAME_MAX) {
            goto done;
        }
        jniFunctionName = malloc(len);
        if (jniFunctionName == NULL) {
            JNU_ThrowOutOfMemoryError(env, NULL);
            goto done;
        }
        buildJniFunctionName(syms[i], cname, jniFunctionName);
        entryName = JVM_FindLibraryEntry(handle, jniFunctionName);
        free(jniFunctionName);
        if(entryName) {
            break;
        }
    }
 done:
    // 假如没有找到,默许回来 NULL
    return entryName;
}

5.3 JVM_FindLibraryEntry()

JVM_FindLibraryEntry() 调用的是平台相关的 dll_lookup(),依据 library 指针和 function 称号。

// vm/prims/jvm.cpp
JVM_LEAF(void*, JVM_FindLibraryEntry(void* handle, const char* name))
  JVMWrapper2("JVM_FindLibraryEntry (%s)", name);
  return os::dll_lookup(handle, name);
JVM_END

6. NativeLibrary#load()

NativeLibrary 是界说在 ClassLoader 内的静态内部类,其代表着已加载 library 的实例,包含了该 library 的指针、所需的 JNI 版本、加载的 Class 来历、称号、是否是内置 library、是否加载过重要信息。

以及核心的加载 load、卸载 unload native 完结。

// java/lang/ClassLoader.java
    static class NativeLibrary {
        long handle;
        private int jniVersion;
        private final Class<?> fromClass;
        String name;
        boolean isBuiltin;
        boolean loaded;
        native void load(String name, boolean isBuiltin);
        native void unload(String name, boolean isBuiltin);
        static native String findBuiltinLib(String name);
        ...
    }

本章节咱们侧重看下 load() 的要害完结:

  1. 首要调用 initIDs() 初始化 ID 等根本数据

    • 假如 ClassLoader$NativeLibrary 内部类、handle 等特点有一不存在的话,回来 FALSE 并完毕加载
    • 经过查看的话初始化 procHandle 指针
  2. 其次经过 JNU_GetStringPlatformChars() 将 String 类型的 library 称号转为 char 类型,假如称号为空的话完毕加载

  3. 假如不是内置的 so,需求调用 JVM_LoadLibrary() 加载得到指针(见下章节),反之沿袭上述的 procHandle 指针即可

  4. 假如 so 指针存在的话,经过 findJniFunction() 和指针参数获取 JNI_OnLoad() 的地址

    • 假如 JNI_OnLoad() 获取成功,则调用它并得到该 so 要求的 jniVersion

    • 反之设置为默许值 0x00010001,即 JNI_VERSION_1_1,1.1

    • 接着调用 JVM_IsSupportedJNIVersion() 查看 JVM 是否支撑该版本,调用的是 Threads 的 is_supported_jni_version_including_1_1()

      • 假如不支撑或者是内置 so 一起版本低于 1.8,抛出 UnsatisfiedLinkError:

        unsupported JNI version xxx required by yyy

      • 反之表明加载成功

  5. 反之,抛出反常 ExceptionOccurred

// native/java/lang/ClassLoader.c
Java_java_lang_ClassLoader_00024NativeLibrary_load
  (JNIEnv *env, jobject this, jstring name, jboolean isBuiltin)
{
    const char *cname;
    ...
    void * handle;
    if (!initIDs(env)) return;
    cname = JNU_GetStringPlatformChars(env, name, 0);
    if (cname == 0) return;
    handle = isBuiltin ? procHandle : JVM_LoadLibrary(cname);
    if (handle) {
        JNI_OnLoad_t JNI_OnLoad;
        JNI_OnLoad = (JNI_OnLoad_t)findJniFunction(env, handle,
                                               isBuiltin ? cname : NULL,
                                               JNI_TRUE);
        if (JNI_OnLoad) {
            ...
            jniVersion = (*JNI_OnLoad)(jvm, NULL);
        } else {
            jniVersion = 0x00010001;
        }
        ...
        if (!JVM_IsSupportedJNIVersion(jniVersion) ||
            (isBuiltin && jniVersion < JNI_VERSION_1_8)) {
            char msg[256];
            jio_snprintf(msg, sizeof(msg),
                         "unsupported JNI version 0x%08X required by %s",
                         jniVersion, cname);
            JNU_ThrowByName(env, "java/lang/UnsatisfiedLinkError", msg);
            if (!isBuiltin) {
                JVM_UnloadLibrary(handle);
            }
            goto done;
        }
        (*env)->SetIntField(env, this, jniVersionID, jniVersion);
    } else {
        cause = (*env)->ExceptionOccurred(env);
        if (cause) {
            (*env)->ExceptionClear(env);
            (*env)->SetLongField(env, this, handleID, (jlong)0);
            (*env)->Throw(env, cause);
        }
        goto done;
    }
    (*env)->SetLongField(env, this, handleID, ptr_to_jlong(handle));
    (*env)->SetBooleanField(env, this, loadedID, JNI_TRUE);
 done:
    JNU_ReleaseStringPlatformChars(env, name, cname);
}
static jboolean initIDs(JNIEnv *env)
{
    if (handleID == 0) {
        jclass this =
            (*env)->FindClass(env, "java/lang/ClassLoader$NativeLibrary");
        if (this == 0)
            return JNI_FALSE;
        handleID = (*env)->GetFieldID(env, this, "handle", "J");
        if (handleID == 0)
            return JNI_FALSE;
        ...
        procHandle = getProcessHandle();
    }
    return JNI_TRUE;
}

7. JVM_LoadLibrary()

JVM_LoadLibrary() 是 JVM 这层加载 library 的终究一个完结,详细进程如下:

  1. 界说 1024 长度的 char 数组和接收加载成果的指针
  2. 调用 dll_load() 加载 library,其细节见下章节
  3. 加载失利的话,打印 library 称号和错误 message
  4. 一起抛出 UnsatisfiedLinkError
  5. 反之将加载成果回来
// vm/prims/jvm.cpp
JVM_ENTRY_NO_ENV(void*, JVM_LoadLibrary(const char* name))
  JVMWrapper2("JVM_LoadLibrary (%s)", name);
  char ebuf[1024];
  void *load_result;
  {
    ThreadToNativeFromVM ttnfvm(thread);
    load_result = os::dll_load(name, ebuf, sizeof ebuf);
  }
  if (load_result == NULL) {
    char msg[1024];
    jio_snprintf(msg, sizeof msg, "%s: %s", name, ebuf);
    Handle h_exception =
      Exceptions::new_exception(...);
    THROW_HANDLE_0(h_exception);
  }
  return load_result;
JVM_END

8. dll_load()

dll_load() 的完结跟平台相关,比方 bsd 平台便是调用标准库的 dlopen(),而其终究的成果来自于 do_dlopen(),其将经过 find_library() 得到 soinfo 实例,内部将履行 to_handle() 得到 library 的指针。

// bionic/libdl/libdl.cpp
void* dlopen(const char* filename, int flag) {
  const void* caller_addr = __builtin_return_address(0);
  return __loader_dlopen(filename, flag, caller_addr);
}
void* __loader_dlopen(const char* filename, int flags, const void* caller_addr) {
  return dlopen_ext(filename, flags, nullptr, caller_addr);
}
static void* dlopen_ext(...) {
  ScopedPthreadMutexLocker locker(&g_dl_mutex);
  g_linker_logger.ResetState();
  void* result = do_dlopen(filename, flags, extinfo, caller_addr);
  if (result == nullptr) {
    __bionic_format_dlerror("dlopen failed", linker_get_error_buffer());
    return nullptr;
  }
  return result;
}
void* do_dlopen(...) {
  ...
  if (si != nullptr) {
    void* handle = si->to_handle();
    si->call_constructors();
    failure_guard.Disable();
    return handle;
  }
  return nullptr;
}

9. JNI_OnLoad()

JNI_OnLoad() 界说在 jni.h 中,当 library 被 JVM 加载时会回调,该办法内一般会经过 registerNatives() 注册 native 办法并回来该 library 所需的 JNI 版本。

该头文件还界说了其他函数和常量,比方 JNI 1.1 等数值。

// jni.h
...
struct JNIEnv_ {
    ...
    jint RegisterNatives(jclass clazz, const JNINativeMethod *methods,
                         jint nMethods) {
        return functions->RegisterNatives(this,clazz,methods,nMethods);
    }
    jint UnregisterNatives(jclass clazz) {
        return functions->UnregisterNatives(this,clazz);
    }
}
...
/* Defined by native libraries. */
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved);
#define JNI_VERSION_1_1 0x00010001
...

结语

一文了解 Java 中 so 文件的加载原理
整体流程能够归纳如下:

  1. System 类供给的 load() 加载 so 的完好的途径名且带文件后缀,等同于直接调用 Runtime 类供给的 load();loadLibrary() 用于加载指定 so 的称号,等同于调用 Runtime 类供给的 loadLibrary()。
    • 两者都将经过 SecurityManager 查看 so 的拜访权限以及称号是否合法
  2. 之后调用 ClassLoader 类的 loadLibrary() 完结,差异在于前者指定的是否是绝对途径的 isAbsolute 参数是否为 true
  3. ClassLoader 首要需求经过 System 供给的 getProperty() 获取 JVM 配置的寄存 usrsystem library 途径字符串数组
  4. 假如 library name 非绝对途径,需求先调用 findLibrary() 获取该 name 对应的完好 so 文件,之后再调用 loadLibrary0() 持续
    • ClassLoader 不存在,别离到 system、usr 字符串数组中查找该 so 是否存在
  5. loadLibrary0() 将调用 native 办法 findBuiltinLib() 查看是否是内置的动态链接库,并到加载过 vector、加载中 context 中查找是否已经加载过、加载中
  6. 经过查看的话调用 NativeLibrary 静态内部类持续,事实上是调用 ClassLoader.cload()
  7. 其将调用 jvm.cppJVM_LoadLibrary() 进行 so 的加载取得指针
  8. 依据 OS 的完结,dll_load() 经过 dlopen() 履行 so 的打开和地址回来
  9. 终究经过 findJniFunction() 获取 JNI_OnLoad() 地址进行 native 办法的注册和所需 JNI 版本的搜集。

源码地址

  • BaseDexClassLoader.java
  • System.c
  • ClassLoader.c
  • jvm.cpp
  • libdl.cpp
  • jni.h

参阅资料

写作本文的时分参阅了很多文章的内容,也有部分内容论述了 JNI 原理以外的东西,大家能够结合着一起看看。

  • Hotspot JNI库文件加载源码解析
  • JNI开发之JNI原理
  • Android JNI:深入分析安卓JNI原理
  • JVM的启动进程
  • 深入理解JNI
  • JNI/NDK入门指南之JavaVM和JNIEnv
  • Android 源码中的 JNI,到底是怎么运用的?
  • Java System.load() 与 System.loadLibrary() 差异解析
  • so加载 – Linker跟NameSpace知识
  • Android NDK开发:JNI基础篇
  • Android NDK开发:JNI实战篇
  • JNI 从入门到实践,万字爆肝详解!
  • 怎么查看JVM的源码
  • Where to find source code for java.lang native methods?
  • Java native method source code