天天看點

談談Android的so

原文連結:http://allenfeng.com/2016/11/06/what-you-should-know-about-android-abi-and-so/

一般情況下,我們不需要關心so。但是當APP使用的第三方SDK中包含了so檔案,或者自己需要使用NDK開發某些功能,就有必要去好好了解下so的一些知識。

什麼是ABI和so

早期的Android裝置隻支援ARMv5的CPU架構,随着Android系統的快速發展,搭載Android的硬體平台也早已多樣化了,又加入了ARMv7,x86,MIPS,ARMv8,MIPS64和x86_64。

每一種CPU架構,都定義了一種ABI(Application Binary Interface,應用二進制接口),ABI定義了其所對應的CPU架構能夠執行的二進制檔案(如.so檔案)的格式規範,決定了二進制檔案如何與系統進行互動。

談談Android的so

每一種ABI的詳細介紹可以參見官方的介紹ABI Management。

so(shared object,共享庫)是機器可以直接運作的二進制代碼,是Android上的動态連結庫,類似于Windows上的dll。每一個Android應用所支援的ABI是由其APK提供的.so檔案決定的,這些so檔案被打包在apk檔案的lib/目錄下,其中abi可以是上面表格中的一個或者多個。

例如,解壓一個apk檔案後,在lib目錄下可以看到如下檔案:

1
     
     
      2
     
     
      3
     
     
      4
     
     
      5
     
     
      6
     
     
      7
     
     
      8
     
     
      9
     
     
      10
           
lib
     
     
      |
     
     
      ├── armeabi
     
     
      │   └── libmath.so
     
     
      ├── armeabi-v7a
     
     
      │   └── libmath.so                                                            
     
     
      ├── mips
     
     
      │   └── libmath.so
     
     
      └── x86
     
     
          └── libmath.so
           

說明該應用所支援的ABI為armeabi, armeabi-v7a, mips, 和x86。

注:可以使用

aapt

指令快速檢視apk支援的abi

1
     
     
      2
           
~ aapt dump badging baidutieba.apk | grep abi
     
     
      native-code: 
      'armeabi' 
      'mips' 
      'x86'  
           

為什麼使用so

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

為指定的ABI生成so

預設情況下,NDK隻會為armeabi生成.so檔案,若需要生成支援其他ABI的.so檔案,可以在Application.mk檔案中指定

APP_ABI

參數:

1
           
APP_ABI := armeabi-v7a
           

APP_ABI

參數可以被指定多個值以支援多個ABI:

1
            
APP_ABI := armeabi armeabi-v7a x86
            

當然,你也可以使用

all

來生成支援所有ABI的so:

1
            
APP_ABI := all
            
談談Android的so

檢視Android系統的ABI支援

Android可以在運作期間确定目前系統所支援的ABI,這是由系統編譯時的具體參數指定的:

  • primary ABI

    (主ABI):對應目前系統中使用的機器碼類型
  • secondary ABI

    (副ABI):表示目前系統支援的其他ABI類型

許多手機支援不止一個ABI,比如,一個基于ARMv7的裝置會将armeabi-v7a定義為primary ABI,armeabi作為secondary ABI,意味着這台機器同時支援armeabi-v7a和armeabi。

許多基于x86的裝置也可以運作armeabi-v7a和armeabi的so,對于這些機器,primary ABI是x86,secondary ABI則是armeabi-v7a.

但是,為了能得到更好的性能表現,我們應該盡可能的直接提供primary ABI所對應的so檔案。比如,我們可以為x86手機直接提供x86的so檔案,而不是僅提供arm的so讓系統通過houdini去動态轉換arm指令,避免轉換過程中的性能損耗。

檢視Android系統支援的ABI有以下兩種方法:

使用adb指令

/system/build.prop

中指定了支援的ABI類型,在adb中,可使用如下指令檢視:

1
     
     
      2
     
     
      3
     
     
      4
     
     
      5
           
[email protected]:/ $ getprop | grep abilist
     
     
      [ro.product.cpu.abi]: [arm64-v8a]
     
     
      [ro.product.cpu.abilist32]: [armeabi-v7a,armeabi]
     
     
      [ro.product.cpu.abilist64]: [arm64-v8a]
     
     
      [ro.product.cpu.abilist]: [arm64-v8a,armeabi-v7a,armeabi]
           

使用API擷取

使用Build.SUPPORTED_ABIS可以擷取目前裝置支援的ABI清單:

1
     
     
      2
           
import android.os.Build;
     
     
      String supportedAbis = Build.SUPPORTED_ABIS;
           

x86手機對arm的支援

值得注意的是原本x86架構的CPU是不支援運作arm架構的native代碼的,但Intel和Google合作在x86機子的系統核心層之上加入了一個名為houdini的Binary Translator(二進制轉換中間層),這個中間層會在運作期間動态的讀取arm指令并将之轉換為x86指令去執行。

談談Android的so

是以能看到很多沒有提供x86對應so的應用(如新浪微網誌)也能夠運作在x86手機上。

apk安裝過程中對so的選擇

在Android上安裝應用程式時,Package Manager會掃描整個apk檔案,尋找符合下面檔案路徑格式的動态連接配接庫:

1
           
lib/<primary-abi>/lib<name>.so
           

在這裡,

primary-abi

是上面表中的abi的值,

name

對應的是我們在Android.mk中定義的LOCAL_MODULE的值,

如果在apk内并沒有找到适合目前機器primary-abi的so,Package Manager會嘗試尋找适合secondary-abi的so檔案:

1
           
lib/<secondary-abi>/lib<name>.so
           

即安裝應用時,系統會根據目前CPU架構選擇最優ABI适配,如果找到了合适的so檔案,包管理器會将該ABI檔案夾下所有so庫全部拷貝至應用的data目錄下:

data/data/<package_name>/lib/

注意:apk安裝過程對so選擇是基于整個ABI檔案夾的,而非以單個so檔案為粒度,也就是說把lib/armeabi 、lib/armeabi-v7a、lib/x86等等檔案夾的其中一個檔案夾内所有.so複制到應用的data目錄下。

如果我們在代碼中調用了某個so的功能,而最終拷貝的ABI檔案夾下并沒有提供這個檔案,apk的安裝過程中并不會報錯,但是運作時會遇到

java.lang.UnsatisfiedLinkError

so的加載

對于so的加載,Android在

System

類中提供了兩種方法:

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

System.loadLibrary

這是我們最常用的一個方法,

System.loadLibrary

隻需要傳入so在Android.mk中定義的LOCAL_MODULE的值即可,

系統會調用

System.mapLibraryName

把這個libName轉化成對應平台的so的全稱并去嘗試尋找這個so加載。

比如我們的so檔案全名為libmath.so,加載該動态庫隻需要傳入

math

即可:

1
           
System.loadLibrary(
      "math");
           

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時調用:

1
           
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

,再嘗試加載時,會得到如下錯誤:

1
           
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動辄幾十M或者更大,apk包大小的精簡成為了開發過程中的重要一環。通過上面的介紹,我們知道x86、x86_64、armeabi-v7a、arm64-v8a裝置都支援armeabi架構的so,是以,通過移除不必要的so來減小包大小是一個不錯的選擇。

按照ABI分别單獨打包APK

我們可以選擇在Google Play上傳指定ABI版本的APK,生成不同ABI版本的APK可以在build.gradle中進行如下配置:

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

隻提供

armabi

的so

上面的方法需要應用市場提供使用者裝置CPU類型更識别的支援,在國内并不是一個十分适用的方案。常用的處理方式是利用gradle中的abiFilters配置。

首先配置修改主工程

build.gradle

下的

abiFilters

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

abiFilters後面的ABI類型即為要打包進apk的ABI類型,除此以外都不打包進apk裡。

然後在項目的根目錄下的

gradle.properties

(沒有的話建立一個)中加入下面這行:

1
           
android.useDeprecatedNdk=
      true
           

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

進階版方案

如果進一步,會發現上面的方案并不完美。首先是性能問題:使用相容模式去運作arm架構的so,會丢失專門為目前ABI優化過的性能;其次還有相容性問題,雖然x86裝置能相容arm類型的函數庫,但是并不意味着100%的相容,某些情況下還是會發生crash,是以x86的arm相容隻是一個折中方案,為了最好的利用x86自身的性能和避免相容性問題,我們最好的做法仍是專為

x86

提供對應的so。

針對這些問題,我們可以采用一個相對更好的方案:讓所有so都來自于網路,應用下載下傳伺服器上的so庫後,利用

System.load

方法動态加載目前裝置對應的so.

需要注意的問題

不要把so放錯地方

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

盡可能為所有ABI提供so

理想狀況下,應該盡可能為所有ABI都提供對應的so,這一點的好處我們已經在上面讨論過了:在可以發揮更好性能的同時,還能減少由于相容帶來的某些crash問題。當然,這一點要結合實際情況(如SDK提供的so不全、晶片市場占有率、apk包大小等)去考量,如果使用的so本身就很小,我們大可為盡可能多的ABI都提供so。

若是局限于包大小等因素,可以結合通過精簡so來減小包大小一節中提供的第三個方案來調整so的使用政策。

所有ABI檔案夾提供的so要保持一緻

這是一個十分容易出現的錯誤。

如果我們的應用選擇了支援多個ABI,要十分注意:對于每個ABI下的so,但要麼全部支援,要麼都不支援。不應該混合着使用,而應該為每個ABI目錄提供對應的.so檔案。

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

1
     
     
      2
     
     
      3
     
     
      4
     
     
      5
     
     
      6
     
     
      7
     
     
      8
     
     
      9
     
     
      10
     
     
      11
     
     
      12
     
     
      13
     
     
      14
     
     
      15
     
     
      16
           
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:

1
     
     
      2
     
     
      3
     
     
      4
     
     
      5
     
     
      6
           
lib
     
     
      |
     
     
      ├── armeabi
     
     
      │   └── libImages.so
     
     
      └── armeabi-v7a
     
     
          └── libImages.so
           

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

1
     
     
      2
     
     
      3
     
     
      4
     
     
      5
     
     
      6
     
     
      7
     
     
      8
     
     
      9
     
     
      10
     
     
      11
     
     
      12
     
     
      13
     
     
      14
     
     
      15
     
     
      16
     
     
      17
     
     
      18
           
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
           

參考上面apk安裝過程中對so的選擇一節,假設目前裝置是x86機器,包管理器會先去lib/x86下尋找,發現該檔案夾是存在的,是以最終隻有lib/x86下的so–即隻有libBugtags.so會被安裝。當嘗試在運作期間加載

libImages.so

時,就會遇上下面常見的UnsatisfiedLinkError錯誤:

1
     
     
      2
           
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"
     
     
      E/xxx   (10674):     at java.lang.Runtime.loadLibrary(Runtime.java:366)
           

是以,我們需要遵循這樣的準則:

  • 對于so開發者:支援所有的平台,否則将會搞砸你的使用者。
  • 對于so使用者:要麼支援所有平台,要麼都不支援。

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

一種可行的處理方案是:取你所有的so庫所支援的ABI的交集,移除其他(可以通過上面介紹的

abiFilters

來實作)。

如上面的例子,最終生成的apk可以是:

1
      
      
       2
      
      
       3
      
      
       4
      
      
       5
      
      
       6
      
      
       7
      
      
       8
            
lib
      
      
       |
      
      
       ├── armeabi
      
      
       │   ├── libBugtags.so
      
      
       │   └── libImages.so
      
      
       └── armeabi-v7a
      
      
           ├── libBugtags.so
      
      
           └── libImages.so