React-Native系列Android——SoLoader載入動態連結程式庫

來源:互聯網
上載者:User

標籤:

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

React-Native系列Android——SoLoader載入動態連結程式庫

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.