React-Native系列Android——SoLoader加载动态链接库

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

时间: 2024-11-05 16:05:04

React-Native系列Android——SoLoader加载动态链接库的相关文章

【REACT NATIVE 系列教程之七】统一ANDROID与IOS两个平台的程序入口&&区分平台的组件简介

本站文章均为 李华明Himi 原创,转载务必在明显处注明: 转载自[黑米GameDev街区] 原文链接: http://www.himigame.com/react-native/2260.html       本篇介绍两个细节:       1. 关于如何将index.android.js 与index.ios.js统一管理起来.       2.  Platform 组件的简单介绍与使用   一:将index.android.js 与index.ios.js统一管理起来. 由于React本身

【REACT NATIVE 系列教程之十三】利用LISTVIEW与TEXTINPUT制作聊天/对话框&&获取组件实例常用的两种方式

本站文章均为 李华明Himi 原创,转载务必在明显处注明: 转载自[黑米GameDev街区] 原文链接: http://www.himigame.com/react-native/2346.html 本篇Himi来利用ListView和TextInput这两种组件实现对话.聊天框. 首先需要准备的有几点:(组件的学习就不赘述了,简单且官方有文档) 1. 学习下 ListView: 官方示例:http://reactnative.cn/docs/0.27/tutorial.html#content

【React Native开发】React Native For Android环境配置以及第一个实例

转载请标明出处: http://blog.csdn.net/developer_jiangqq/article/details/50456967 本文出自:[江清清的博客] (一)前言 FaceBook早期开源发布了React Native For IOS,终于在2015年9月15日也发布了ReactNative for Android,虽然Android版本的项目发布比较迟,但是也没有阻挡了广大开发者的热情.可以这样讲在2015年移动平台市场上有两个方向技术研究比较火,第一种为阿里,百度,腾讯

Android动态加载jar/dex

http://www.cnblogs.com/over140/archive/2011/11/23/2259367.html 前言 在目前的软硬件环境下,Native App与Web App在用户体验上有着明显的优势,但在实际项目中有些会因为业务的频繁变更而频繁的升级客户端,造成较差的用户体验,而这也恰恰是Web App的优势.本文对网上Android动态加载jar的资料进行梳理和实践在这里与大家一起分享,试图改善频繁升级这一弊病. 声明 欢迎转载,但请保留文章原始出处:) 博客园:http:/

React Native嵌入Android原生应用中

开发环境准备 首先你要搭建好React Native for Android开发环境, 没有搭建好的可以参考:React Native for Android Windows环境搭建 用Android Studio新建Android原生项目 我创建了一个名叫ReactNativeDemo的原生项目. 把React Native集成到原生项目当中 利用Windows命令行在项目根目录(ReactNativeDemo文件夹)下执行下面三行命令: npm init npm install –save

Unity3D的坑系列:动态加载dll

Unity3D的坑系列:动态加载dll 我现在参与的项目是做MMO手游,目标平台是Android和iOS,iOS平台不能动态加载dll(什么原因找乔布斯去),可以直接忽略,而在Android平台是可以动态加载dll的,有了这个就可以实现代码更新,不过实际上,在unity里要用上动态加载dll是有很多限制的(不了解的话就是坑). 限制1:在Android手机里动态加载dll不能使用Assembly.LoadFile(string path),只能使用Assembly.Load(byte[] raw

Android动态加载那些事儿

基础 1.Java 类加载器 类加载器(class loader)是 Java?中的一个很重要的概念.类加载器负责加载 Java 类的字节代码到 Java 虚拟机中.本文首先详细介绍了 Java 类加载器的基本概念,包括代理模式.加载类的具体过程和线程上下文类加载器等,接着介绍如何开发自己的类加载器,最后介绍了类加载器在 Web 容器和 OSGi?中的应用. 2.反射原理 Java 提供的反射機制允許您於執行時期動態載入類別.檢視類別資訊.生成物件或操作生成的物件,要舉反射機制的一個應用實例,就

React Native For Android 架构初探

Facebook 在2015.9.15发布了 React Native for Android,把JavaScript 开发技术扩展到了Android平台.React Native 让开发者使用 JavaScript 和 React 编写应用,利用相同的核心代码就可以创建 基于Web,iOS 和 Android 平台的原生应用.本文将浅析Android React的架构及相关基础知识.环境搭建及调试相关知识参考官网文档即可,本文不再赘述. 一.React架构分析 1.层次架构: Java层:ja

菜鸟窝React Native 系列教程-1.移动端开发趋势与未来

菜鸟窝React Native 系列教程-1.移动端开发趋势与未来 课程持续更新中..... 我是RichardCao,现任新美大酒店旅游事业群的Android Developer.如果你也有兴趣录制RN视频,请加入下面QQ群找我. 下载地址:https://pan.baidu.com/s/1c1XmE56 密码:shhw 首发地址:菜鸟窝-ReactNative学习板块 交流QQ群:576089067 课程目录:菜鸟窝React Native 系列教程