一文了解 Java 中 so 文件的加载原理

Posted TechMerger

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一文了解 Java 中 so 文件的加载原理相关的知识,希望对你有一定的参考价值。

前言

无论是 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 中,可在如下网站中看到:

// 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 (path08-SO加载解析过程

android的java层so加载解析

从源码入手,一文带你读懂Spring AOP面向切面编程

安卓开发之so库加载使用的那些坑

一文了解git底层原理

一文看透Java高并发:Synchronized锁的性质原理及其缺陷