SoLoaderfacebook出品的一款小巧的用于加载so库文件的开源项目,主要作用是自动检查和加载多个有依赖关系的so库文件。在Android平台下React-Native项目大量使用了动态链接库,即JNI技术,作为JavaJavascript两种程序语言之间的通信桥梁。

解压一个React-Native项目的安装包apk文件,我们可以看到一共有15so库文件,其中libreactnativejni.soJNI桥梁的入口。

libreactnativejni.so又依赖于以下12so文件:

libfb.so
libfbjni.so
libfolly_json.so
libjsc.so
libglog.so
libgnustl_shared.so
libandroid.so
liblog.so
libstdc++.so
libm.so
libc.so
libdl.so

其中,前6个是React-Native自身的动态链接库,后6个则是Android系统的动态链接库,所以如果想要加载libreactnativejni.so库,必须要先加载其依赖的这12个库文件。后6个系统的库文件是由系统预先加载到Dalvik虚拟机里面的,可以不用处理,但前6个必须手动预先加载!可是如果其中有库文件又依赖于其它的库文件,那么在加载其自身前又必须加载其依赖的库文件。

这样,其实就是一个递归加载依赖的过程,如果是由人工来维护这种依赖,首先极其繁琐,其次代码的可维护性也大大降低了。好在Android 4.3版本(包括)之后,会自动检查和加载依赖库,但是React-Native是兼容到Android 4.1版本的,所以SoLoader就是一种兼容4.3以下版本的技术解决方案。

SoLoader很轻巧,一种只有不到20个文件,可以直接用在任何的Android项目中,而不限于React-Native

github开源地址:https://github.com/facebook/SoLoader


我们来研究以下SoLoader的实现原理。

首先SoLoader加载库文件之前,需要初始化,主要目的是将所有so库文件(系统+项目自身)所在目录预先全部收集起来,方面后面加载时查找。

来看一下com.facebook.soloader.SoLoaderinit方法。

第一步:收集系统库文件

   ArrayList<SoSource> soSources = new ArrayList<>();

   String LD_LIBRARY_PATH = System.getenv("LD_LIBRARY_PATH");
   if (LD_LIBRARY_PATH == null) {
       LD_LIBRARY_PATH = "/vendor/lib:/system/lib";
   }

   String[] systemLibraryDirectories = LD_LIBRARY_PATH.split(":");
   for (int i = 0; i < systemLibraryDirectories.length; ++i) {
        File systemSoDirectory = new File(systemLibraryDirectories[i]);
        soSources.add(new DirectorySoSource(systemSoDirectory,DirectorySoSource.ON_LD_LIBRARY_PATH));
    }

系统的库文件主要是在两个目录下面,/vendor/lib目录和/system/lib目录,先将这两个DirectorySoSource收集起来。

第二步:收集当前应用的库文件

int ourSoSourceFlags = 0;

// On old versions of Android, Bionic doesn't add our library directory to its internal
// search path, and the system doesn't resolve dependencies between modules we ship.  On
// these systems, we resolve dependencies ourselves.  On other systems, Bionic's built-in
// resolver suffices.

if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR1) {
    ourSoSourceFlags |= DirectorySoSource.RESOLVE_DEPENDENCIES;
}

SoSource ourSoSource = new DirectorySoSource(new File(applicationInfo.nativeLibraryDir), ourSoSourceFlags);

soSources.add(0, ourSoSource);

nativeLibraryDir指向的是/data/app-lib/[package-name]-n目录。由于Android 4.2版本及之上,是会自动处理依赖关系的,所以这里添加一个标志RESOLVE_DEPENDENCIES,表示加载库文件时直接加载,不需要查找依赖。

第三步:加载库文件前解析依赖关系

这个过程是最为复杂的一步,涉及到ELF文件的解码。ELF文件原名为Executable and Linking Format,大致包括三种:可重定位文件、动态链接库文件、可执行文件。Android系统中常用的so扩展名文件,就是指其中的动态链接库文件。

ELF文件的相关资料,详细可以参考下面两篇文章:
http://blog.chinaunix.net/uid-21273878-id-1828736.html
http://blog.chinaunix.net/uid-72446-id-2060531.html

下面来说一下ELF文件的解码,主要目的是查找到动态链接库的依赖库。代码位于com.facebook.soloader.MinElfextract_DT_NEEDED方法,代码比较长,我们一步步来看:

    ByteBuffer bb = ByteBuffer.allocate(8 /* largest read unit */);

    // Read ELF header.

    bb.order(ByteOrder.LITTLE_ENDIAN);
    if (getu32(fc, bb, Elf32_Ehdr.e_ident) != ELF_MAGIC) {
      throw new ElfError("file is not ELF");
    }

首先读取ELF文件的头部信息,ELF_MAGIC的值为0x464c457f,表示这是一个ELF格式文件,其中45表示字符E4c表示字符L46表示字符F

boolean is32 = (getu8(fc, bb, Elf32_Ehdr.e_ident + 0x4) == 1);
if (getu8(fc, bb, Elf32_Ehdr.e_ident + 0x5) == 2) {
   bb.order(ByteOrder.BIG_ENDIAN);
}

5个字节表示文件类型,取值有0(非法目标文件)、1(32位目标文件)、2(64位目标文件)。

6个字节表示编码格式,取值有0(非法编码格式)、1(小端编码格式)、2(大端编码格式)。

    long e_phoff = is32
        ? getu32(fc, bb, Elf32_Ehdr.e_phoff)
        :  get64(fc, bb, Elf64_Ehdr.e_phoff);

    long e_phnum = is32
        ? getu16(fc, bb, Elf32_Ehdr.e_phnum)
        : getu16(fc, bb, Elf64_Ehdr.e_phnum);

    int e_phentsize = is32
        ? getu16(fc, bb, Elf32_Ehdr.e_phentsize)
        : getu16(fc, bb, Elf64_Ehdr.e_phentsize);

    if (e_phnum == PN_XNUM) { // Overflowed into section[0].sh_info

      long e_shoff = is32
          ? getu32(fc, bb, Elf32_Ehdr.e_shoff)
          : get64(fc, bb, Elf64_Ehdr.e_shoff);

      long sh_info = is32
          ? getu32(fc, bb, e_shoff + Elf32_Shdr.sh_info)
          : getu32(fc, bb, e_shoff + Elf64_Shdr.sh_info);

      e_phnum = sh_info;
    }

这一步的作用是为了获取程序头部表的数目e_phnum,然后遍历每个头部表信息,通过其p_type的值找到动态链接库依赖项所在区域的起始位置。

    long dynStart = 0;
    long phdr = e_phoff;

    for (long i = 0; i < e_phnum; ++i) {
      long p_type = is32
          ? getu32(fc, bb, phdr + Elf32_Phdr.p_type)
          : getu32(fc, bb, phdr + Elf64_Phdr.p_type);

      if (p_type == PT_DYNAMIC) {
        long p_offset = is32
            ? getu32(fc, bb, phdr + Elf32_Phdr.p_offset)
            : get64(fc, bb, phdr + Elf64_Phdr.p_offset);

        dynStart = p_offset;
        break;
      }

      phdr += e_phentsize;
    }

描述着动态链接库的程序头部表的p_type值为PT_DYNAMIC,紧挨在其4个字节后面的是相对文件偏移p_offset

接下来开始计算其依赖的动态链接库的数量,主要是通过解析dynamic section,它里面包含了一个数组:

typedef struct { 
Elf32_Sword d_tag; 
union { 
Elf32_Sword d_val; 
Elf32_Addr d_ptr; 
} d_un; 
} Elf32_Dyn; 

d_tag的值为DT_NEEDED1,表示有一个动态链接库依赖。d_tag的值为DT_STRTAB5,表示这是描述着动态链接库信息的表的偏移索引。

    long d_tag;
    int nr_DT_NEEDED = 0;
    long dyn = dynStart;
    long ptr_DT_STRTAB = 0;

    do {
      d_tag = is32
          ? getu32(fc, bb, dyn + Elf32_Dyn.d_tag)
          : get64(fc, bb, dyn + Elf64_Dyn.d_tag);

      if (d_tag == DT_NEEDED) {
        if (nr_DT_NEEDED == Integer.MAX_VALUE) {
          throw new ElfError("malformed DT_NEEDED section");
        }

        nr_DT_NEEDED += 1;
      } else if (d_tag == DT_STRTAB) {
        ptr_DT_STRTAB = is32
            ? getu32(fc, bb, dyn + Elf32_Dyn.d_un)
            : get64(fc, bb, dyn + Elf64_Dyn.d_un);
      }

      dyn += is32 ? 8 : 16;
    } while (d_tag != DT_NULL);

这样有了动态链接依赖库的数量和描述这动态链接库信息表的偏移索引,就能读取到其依赖的库的名字了。

第四步:加载库文件

SoLoaderloadLibrary的方法加载动态链接库,比如:

SoLoader.loadLibrary("reactnativejni");

最终调用的是DirectorySoSourceloadLibrary,由于libreactnativejni.so是当前应用程序的动态链接库,所以DirectorySoSourcesoDirectory指向的就是/data/app-lib/[package-name]-n

public int loadLibrary(String soName, int loadFlags) throws IOException {
    File soFile = new File(soDirectory, soName);
    if (!soFile.exists()) {
      return LOAD_RESULT_NOT_FOUND;
    }

    if ((loadFlags & LOAD_FLAG_ALLOW_IMPLICIT_PROVISION) != 0 &&
        (flags & ON_LD_LIBRARY_PATH) != 0) {
      return LOAD_RESULT_IMPLICITLY_PROVIDED;
    }

    if ((flags & RESOLVE_DEPENDENCIES) != 0) {
      String dependencies[] = MinElf.extract_DT_NEEDED(soFile);
      for (int i = 0; i < dependencies.length; ++i) {
        String dependency = dependencies[i];
        if (dependency.startsWith("/")) {
          continue;
        }

        SoLoader.loadLibraryBySoName(
            dependency,
            (loadFlags | LOAD_FLAG_ALLOW_IMPLICIT_PROVISION));
      }
    }

    System.load(soFile.getAbsolutePath());
    return LOAD_RESULT_LOADED;
  }

通过MinElf.extract_DT_NEEDED方法解析出其依赖库文件,然后递归调用SoLoader.loadLibraryBySoName方法加载依赖库文件。

如果依赖的库文件是系统的动态链接库,比如libandroid.so,由于其位于/system/lib下面,则调用相应的DirectorySoSource去加载,这种场景下由于这个库已经由系统加载了,则自动返回LOAD_RESULT_IMPLICITLY_PROVIDED

如果依赖的库文件也是当前应用的动态链接库,比如libfb.so,则先去解析libfb.so的依赖库文件,重复以上过程。直到最后一个被依赖的动态链接库加载完成,最后调用系统的System.load方法加载自身。


总结

国内的Android应用程序很少有超过5个动态链接库的,即使有各个动态链接库也是相互独立的(来自国内各大服务提供商,比如百度地图等),而像React-Native这种严重依赖JNI技术的应用程序毕竟少见,但是如果有的话,使用SoLoader可以大大提高程序的代码质量!


本博客不定期持续更新,欢迎关注和交流:

http://blog.csdn.net/megatronkings

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐