【開發必備】今天我們來談談Android NDK動態鏈接庫(so文件)的一些見解


一、寫在前面

      直到現在,基本我寫的每一個項目都會用到NDK動態鏈接庫的知識,可見這個也的確十分常用。那么,今天,咱們就來談談它。

二、什么是ABI和so

1、發展

      早起的Android系統幾乎只支持ARMv5的CPU架構,而現在卻發展到了7種:ARMv5,ARMv7 (從2010年起),x86 (從2011年起),MIPS (從2012年起),ARMv8,MIPS64和x86_64 (從2014年起),每一種都關聯着一個相應的ABI。每一種ABI的詳細介紹可以參見官方的介紹ABI Management

2、關系

      我們可以通過Build.SUPPORTED_ABIS得到根據偏好排序的設備支持的ABI列表。但你不應該從你的應用程序中讀取它,因為Android包管理器安裝APK時,會自動選擇APK包中為對應系統ABI預編譯好的.so文件,如果在對應的lib/ABI目錄中存在.so文件的話。

ABI(橫向)和cpu(縱向) armeabi armeabi-v7a arm64-v8a mips mips64 x86 x86_64
ARMv5 支持            
ARMv7 支持 支持          
ARMv8 支持 支持 支持        
MIPS       支持      
MIPS64       支持 支持    
x86 支持(3) 支持(2)       支持(1)  
x86_64 支持         支持 支持

     解析: x86設備上,libs/x86目錄中如果存在.so文件的話,會被安裝,如果不存在,則會選擇armeabi-v7a中的.so文件,如果也不存在,則選擇armeabi目錄中的.so文件。

x86設備能夠很好的運行ARM類型函數庫,但並不保證100%不發生crash,特別是對舊設備。

64位設備(arm64-v8a, x86_64, mips64)能夠運行32位的函數庫,但是以32位模式運行,在64位平台上運行32位版本的ART和Android組件,將丟失專為64位優化過的性能(ART,webview,media等等)。

三、我什么我們要關注so

  • so機制讓開發者最大化利用已有的C和C++代碼,達到重用的效果,利用軟件世界積累了幾十年的優秀代碼;
  • so是二進制,沒有解釋編譯的開消,用so實現的功能比純java實現的功能要快;
  • so內存分配不受Dalivik/ART的單個應用限制,減少OOM;
  • 相對於java代碼,二進制代碼的反編譯難度更大,一些核心代碼可以考慮放在so中。

四、NDK的兼容性

     使用NDK時,你可能會傾向於使用最新的編譯平台,但事實上這是錯誤的,因為NDK平台不是后向兼容(兼容過去的版本)的,而是前向兼容(兼容將來的版本)的。推薦使用app的minSdkVersion對應的編譯平台。

     這也意味着當你引入一個預編譯好的.so文件時,你需要檢查它被編譯所用的平台版本。

五、一個法則

    處理.so文件時有一條簡單卻並不知名的重要法則。

    你應該盡可能的提供專為每個ABI優化過的.so文件,但要么全部支持,要么都不支持:你不應該混合着使用。你應該為每個ABI目錄提供對應的.so文件。

六、so文件的加載

    對於so文件的加載,Android在System類中提供了下面兩種方法。

 1 /**
 2   * See {@link Runtime#loadLibrary}.
 3   */
 4  public static void loadLibrary(String libName) {
 5      Runtime.getRuntime().loadLibrary(libName, VMStack.getCallingClassLoader());
 6  }
 7 /**
 8   * See {@link Runtime#load}.
 9   */
10  public static void load(String pathName) {
11      Runtime.getRuntime().load(pathName, VMStack.getCallingClassLoader());
12  }

    1、System.loadLibrary

    這是我們最常用的一個方法,System.loadLibrary只需要傳入so在Android.mk中定義的LOCAL_MODULE的值即可,系統會調用System.mapLibraryName把這個libName轉化成對應平台的so的全稱並去嘗試尋找這個so加載。比如我們的so文件全名為libmath.so,加載該動態庫只需要傳入math即可:

System.loadLibrary("math");

     2、System.load

     對於System.load方法,官方是這樣介紹的: 

Loads a code file with the specified filename from the local file system as a dynamic library.
The filename argument must be a complete path name.

     所以它為動態加載非apk打包期間內置的so文件提供了可能,也就是說可以使用這個方法來指定我們要加載的so文件的路徑來動態的加載so文件。
     比如我們在打包期間並不打包so文件,而是在應用運行時將當前設備適用的so文件從服務器上下載下來,放在/data/data/<package-name>/mydir下,然后在使用so時調用:   

System.load("/data/data/<package-name>/mydir/libmath.so");

     即可成功加載這個so,開始調用本地方法了。

     其實loadLibrary和load最終都會調用nativeLoad(name, loader, ldLibraryPath)方法,只是因為loadLibrary的參數傳入的僅僅是so的文件名,所以,loadLibrary需要首先找到這個文件的路徑,然后加載這個so文件。
     而load傳入的參數是一個文件路徑,所以它不需要去尋找這個文件路徑,而是直接通過這個路徑來加載so文件。

     但是當我們把需要加載的so文件放在SdCard中,會發生什么呢?把上面so的路徑改成/mnt/sdcard/libmath.so,再嘗試加載時,會得到如下錯誤: 

java.lang.UnsatisfiedLinkError: dlopen failed: couldn't map "/mnt/sdcard/libmath.so" segment 1: Permission denied

     這是因為SD卡等外部存儲路徑是一種可拆卸的(mounted)不可執行(noexec)的儲存媒介,不能直接用來作為可執行文件的運行目錄,使用前應該把可執行文件復制到APP內部存儲下再運行。所以使用System.load加載so時要注意把so拷貝至/data/data/<package-name>/下。

七、通過精簡so來減小apk大小

     1、為什么

     現在的apk動輒幾十M或者更大,apk包大小的精簡成為了開發過程中的重要一環。通過上面的介紹,我們知道x86、x86_64、armeabi-v7a、arm64-v8a設備都支持armeabi架構的so,因此,通過移除不必要的so來減小包大小是一個不錯的選擇。

     2、按照ABI分別單獨打包APK

     我們可以選擇在Google Play上傳指定ABI版本的APK,生成不同ABI版本的APK可以在build.gradle中進行如下配置:(引自別處,未考證) 

 1 android {
 2     // Some other configuration here...
 3     splits {
 4         abi {
 5             enable true
 6             reset()
 7             include 'x86', 'armeabi', 'armeabi-v7a', 'mips' //select ABIs to build APKs for
 8             universalApk false // generate an additional APK that contains all the ABIs
 9         }
10     }
11 }

     3、只提供armabi的so

     上面的方法需要應用市場提供用戶設備CPU類型更識別的支持,在國內並不是一個十分適用的方案。常用的處理方式是利用gradle中的abiFilters配置。
首先配置修改主工程build.gradle下的abiFilters

1 android {
2     // Some other configuration here...
3     defaultConfig {
4         ndk {
5             abiFilters 'armeabi'
6         }
7     }
8 }

     abiFilters后面的ABI類型即為要打包進apk的ABI類型,除此以外都不打包進apk里。然后在項目的根目錄下的gradle.properties(沒有的話新建一個)中加入下面這行:

android.useDeprecatedNdk=true

     通過上面方法減少的apk體積是十分可觀的,也是目前比較主流的處理方案。

    4、進階版方案

     如果進一步,會發現上面的方案並不完美。首先是性能問題:使用兼容模式去運行arm架構的so,會丟失專門為當前ABI優化過的性能;其次還有兼容性問題,雖然x86設備能兼容arm類型的函數庫,但是並不意味着100%的兼容,某些情況下還是會發生crash,所以x86的arm兼容只是一個折中方案,為了最好的利用x86自身的性能和避免兼容性問題,我們最好的做法仍是專為x86提供對應的so。
     針對這些問題,我們可以采用一個相對更好的方案:讓所有so都來自於網路,應用下載服務器上的so庫后,利用System.load方法動態加載當前設備對應的so.

八、需要注意的問題

     1、不要把so放錯地方

     首先要注意的是不要把另一個ABI下的so文件放在另一個ABI文件夾下(每個ABI文件夾下的so文件名是相同的,有可能會搞錯)。

     2、盡可能為所有ABI提供so

     理想狀況下,應該盡可能為所有ABI都提供對應的so,這一點的好處我們已經在上面討論過了:在可以發揮更好性能的同時,還能減少由於兼容帶來的某些crash問題。當然,這一點要結合實際情況(如SDK提供的so不全、芯片市場占有率、apk包大小等)去考量,如果使用的so本身就很小,我們大可為盡可能多的ABI都提供so。
若是局限於包大小等因素,可以結合通過精簡so來減小包大小一節中提供的第三個方案來調整so的使用策略。

     3、所有ABI文件夾提供的so要保持一致

     這是一個十分容易出現的錯誤。
     如果我們的應用選擇了支持多個ABI,要十分注意:對於每個ABI下的so,但要么全部支持,要么都不支持。不應該混合着使用,而應該為每個ABI目錄提供對應的.so文件。

     先舉個例子,Bugtags的so支持所有的ABI:

libs
|
├── arm64-v8a
│   └── libBugtags.so
├── armeabi
│   └── libBugtags.so
├── armeabi-v7a
│   └── libBugtags.so
├── mips
│   └── libBugtags.so
├── mips64
│   └── libBugtags.so
├── x86
│   └── libBugtags.so
└── x86_64
    └── libBugtags.so

    但不是所有開發者提供的so都支持所有ABI: 

lib
|
├── armeabi
│   └── libImages.so
└── armeabi-v7a
    └── libImages.so

      如果不做任何設置,最終打出來的apk的lib目錄會是這樣的:

lib
|
├── arm64-v8a
│   └── libBugtags.so
├── armeabi
│   ├── libBugtags.so
│   └── libImages.so
├── armeabi-v7a
│   ├── libBugtags.so
│   └── libImages.so
├── mips
│   └── libBugtags.so
├── mips64
│   └── libBugtags.so
├── x86
│   └── libBugtags.so
└── x86_64
    └── libBugtags.so

      假設當前設備是x86機器,包管理器會先去lib/x86下尋找,發現該文件夾是存在的,所以最終只有lib/x86下的so–即只有libBugtags.so會被安裝。當嘗試在運行期間加載libImages.so時,就會遇上下面常見的UnsatisfiedLinkError錯誤:

1 E/xxx   (10674): java.lang.UnsatisfiedLinkError: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/xxx-2/base.apk"],nativeLibraryDirectories=[/data/app/xxx-2/lib/x86, /vendor/lib, /system/lib]]] couldn't find "libImages.so"
2 E/xxx   (10674):     at java.lang.Runtime.loadLibrary(Runtime.java:366)

    所以,我們需要遵循這樣的准則

  • 對於so開發者:支持所有的平台,否則將會搞砸你的用戶。
  • 對於so使用者:要么支持所有平台,要么都不支持。

    然而,因為種種原因(遺留so、芯片市場占有率、apk包大小等),並不是所有人都遵循這樣的原則。

    一種可行的處理方案是:取你所有的so庫所支持的ABI的交集,移除其他(可以通過上面介紹的abiFilters來實現)。
如上面的例子,最終生成的apk可以是:

lib
|
├── armeabi
│   ├── libBugtags.so
│   └── libImages.so
└── armeabi-v7a
    ├── libBugtags.so
    └── libImages.so

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM