0X0 前言java
在 Android 系統中,當咱們安裝apk文件的時候,lib 目錄下的 so 文件會被解壓到 app 的原生庫目錄,通常來講是放到 /data/data/<package-name>/lib 目錄下,而根據系統和CPU架構的不一樣,其拷貝策略也是不同的,在咱們測試過程當中發現不正確地配置了 so 文件,好比某些 app 使用第三方的 so 時,只配置了其中某一種 CPU 架構的 so,可能會形成 app 在某些機型上的適配問題。因此這篇文章主要介紹一下在不一樣版本的 Android 系統中,安裝 apk 時,PackageManagerService 選擇解壓 so 庫的策略,並給出一些 so 文件配置的建議。架構
0x1 Android4.0之前app
當 apk 被安裝時,執行路徑雖然有差異,但最終要調用到的一個核心函數是 copyApk,負責拷貝 apk 中的資源。ide
參考2.3.6的 Android 源碼,它的 copyApk 其內部函數一段選取原生庫 so 邏輯:函數
public static int listPackageNativeBinariesLI(ZipFile zipFile, List> nativeFiles) throws ZipException, IOException { }
篇幅有限,這裏還有下面全部的源代碼沒法展現出來,若是要查看完整源碼的版本,請戳這裏性能
這段代碼中的 Build.CPU_ABI 和 "ro.product.cpu.abi2" 分別爲手機支持的主 abi 和次 abi 屬性字符串,abi 爲手機支持的指令集所表明的字符串,好比 armeabi-v7a、armeabi、x86、mips 等,而主 abi 和次 abi 分別表示手機支持的第一指令集和第二指令集。代碼首先調用 listPackageSharedLibsForAbiLI 來遍歷主 abi 目錄。當主 abi 目錄不存在時,纔會接着調用 listPackageSharedLibsForAbiLI 遍歷次 abi 目錄。測試
private static int listPackageSharedLibsForAbiLI(ZipFile zipFile, String cpuAbi, List> libEntries) throws IOException, ZipException { }
listPackageSharedLibsForAbiLI 中判斷當前遍歷的 apk 中文件的 entry 名是否符合 so 命名的規範且含相應 abi 字符串名。若是符合則規則則將 so 的 entry 名加入 list,若是遍歷失敗或者規則不匹配則返回相應錯誤碼。ui
拷貝 so 策略:spa
遍歷 apk 中文件,當 apk 中 lib 目錄下主 abi 子目錄中有 so 文件存在時,則所有拷貝主 abi 子目錄下的 so;只有當主 abi 子目錄下沒有 so 文件的時候即 PACKAGE_INSTALL_NATIVE_ABI_MISMATCH 的狀況,纔會拷貝次 ABI 子目錄下的 so 文件。code
策略問題:
當 so 放置不當時,安裝 apk 時會致使拷貝不全。好比 apk 的 lib 目錄下存在 armeabi/libx.so , armeabi/liby.so , armeabi-v7a/libx.so 這3個 so 文件,那麼在主 ABI 爲 armeabi-v7a 且系統版本小於4.0的手機上, apk 安裝後,按照拷貝策略,只會拷貝主 abi 目錄下的文件即 armeabi-v7a/libx.so,當加載 liby.so 時就會報找不到 so 的異常。另外若是主 abi 目錄不存在,這個策略會遍歷2次 apk,效率偏低。
0x2 Android 4.0-Android 4.0.3
參考4.0.3的 Android 源碼,同理,找處處理 so 拷貝的核心邏輯( native 層):
static install_status_t iterateOverNativeFiles(JNIEnv *env, jstring javaFilePath, jstring javaCpuAbi, jstring javaCpuAbi2, iterFunc callFunc, void* callArg) { ... }
拷貝 so 策略:
遍歷 apk 中全部文件,若是符合 so 文件的規則,且爲主 ABI 目錄或者次 ABI 目錄下的 so,就解壓拷貝到相應目錄。
策略問題:
存在同名 so覆蓋,好比一個 app 的 armeabi 和 armeabi-v7a 目錄下都含同名的 so,那麼就會發生覆蓋現象,覆蓋的前後順序根據 so 文件對應 ZipFileR0 中的 hash 值而定,考慮這樣一個例子,假設一個 apk 同時有 armeabi/libx.so 和 armeabi-v7a/libx.so,安裝到主 ABI 爲 armeabi-v7a 的手機上,拷貝 so 時根據遍歷順序,存在一種可能即 armeab-v7a/libx.so 優先遍歷並被拷貝,隨後 armeabi/libx.so 被遍歷拷貝,覆蓋了前者。原本應該加載 armeabi-v7a 目錄下的 so,結果按照這個策略拷貝了 armeabi 目錄下的 so。
apk 中文件 entry 的散列計算函數以下:
unsigned int ZipFileRO::computeHash(const char* str, int len) { }
0x3 Android 4.0.4之後
以4.1.2系統爲例,遍歷選擇 so 邏輯以下:
static install_status_t iterateOverNativeFiles(JNIEnv *env, jstring javaFilePath, jstring javaCpuAbi, jstring javaCpuAbi2, iterFunc callFunc, void* callArg) { }
拷貝 so 策略:
遍歷 apk 中文件,當遍歷到有主 Abi 目錄的 so 時,拷貝並設置標記 hasPrimaryAbi 爲真,之後遍歷則只拷貝主 Abi 目錄下的 so,當標記爲假的時候,若是遍歷的 so 的 entry 名含次 abi 字符串,則拷貝該 so。
策略問題:
通過實際測試, so 放置不當時,安裝 apk 時存在 so 拷貝不全的狀況。這個策略想解決的問題是在 4.0 ~ 4.0.3 系統中的 so 隨意覆蓋的問題,即若是有主 abi 目錄的 so 則拷貝,若是主 abi 目錄不存在這個 so 則拷貝次 abi 目錄的 so,但代碼邏輯是根據 ZipFileR0 的遍歷順序來決定是否拷貝 so,假設存在這樣的 apk, lib 目錄下存在 armeabi/libx.so , armeabi/liby.so , armeabi-v7a/libx.so 這三個 so 文件,且 hash 的順序爲 armeabi-v7a/libx.so 在 armeabi/liby.so 以前,則 apk 安裝的時候 liby.so 根本不會被拷貝,由於按照拷貝策略, armeabi-v7a/libx.so 會優先遍歷到,因爲它是主 abi 目錄的 so 文件,因此標記被設置了,當遍歷到 armeabi/liby.so 時,因爲標記被設置爲真, liby.so 的拷貝就被忽略了,從而在加載 liby.so 的時候會報異常。
0x4 64位系統支持
Android 在5.0以後支持64位 ABI,以5.1.0系統爲例:
public static int copyNativeBinariesWithOverride(Handle handle, File libraryRoot, String abiOverride) { }
copyNativeBinariesWithOverride 分別處理32位和64位 so 的拷貝,內部函數 copyNativeBinariesForSupportedAbi 首先會根據 abilist 去找對應的 abi。
public static int copyNativeBinariesForSupportedAbi(Handle handle, File libraryRoot, String[] abiList, boolean useIsaSubdir) throws IOException { }
findSupportedAbi 內部實現是 native 函數,首先遍歷 apk,若是 so 的全路徑中含 abilist 中的 abi 字符串,則記錄該 abi 字符串的索引,最終返回全部記錄索引中最靠前的,即排在 abilist 中最前面的索引。
static int findSupportedAbi(JNIEnv *env, jlong apkHandle, jobjectArray supportedAbisArray) { }
舉例說明,在某64位測試手機上的abi屬性顯示以下,它有2個 abilist,分別對應該手機支持的32位和64位 abi 的字符串組。
當處理32位 so 拷貝時, findSupportedAbi 索引返回以後,若返回爲0,則拷貝 armeabi-v7a 目錄下的 so,若是爲1,則拷貝 armeabi 目錄下 so。
拷貝 so 策略:
分別處理32位和64位 abi 目錄的 so 拷貝, abi 由遍歷 apk 結果的全部 so 中符合 abilist 列表的最靠前的序號決定,而後拷貝該 abi 目錄下的 so 文件。
策略問題:
策略假定每一個 abi 目錄下的 so 都放置徹底的,這是和2.3.6同樣的處理邏輯,存在遺漏拷貝 so 的可能。
0x5 建議
針對 Android 系統的這些拷貝策略的問題,咱們給出了一些配置 so 的建議:
1. 1)針對 armeabi 和 armeabi-v7a 兩種 ABI
方法1:因爲 armeabi-v7a 指令集兼容 armeabi 指令集,因此若是損失一些應用的性能是能夠接受的,同時不但願保留庫的兩份拷貝,能夠移除 armeabi-v7a 目錄和其下的庫文件,只保留 armeabi 目錄;好比 apk 使用第三方的 so 只有 armeabi 這一種 abi 時,能夠考慮去掉 apk 中 lib 目錄下 armeabi-v7a 目錄。
方法2:在 armeabi 和 armeabi-v7a 目錄下各放入一份 so;
2. 2)針對x86
目前市面上的x86機型,爲了兼容 arm 指令,基本都內置了 libhoudini 模塊,即二進制轉碼支持,該模塊負責把 ARM 指令轉換爲 X86 指令,因此若是是出於 apk package大小的考慮,而且能夠接受一些性能損失,能夠選擇刪掉 x86 庫目錄, x86 下配置的 armeabi 目錄的 so 庫同樣能夠正常加載使用;
3. 3)針對64位 ABI
若是 app 開發者打算支持64位,那麼64位的 so 要放全,不然能夠選擇不單獨編譯64位的 so,所有使用32位的 so,64位機型默認支持32位 so 的加載。好比 apk 使用第三方的 so 只有32位 abi 的 so,能夠考慮去掉 apk 中 lib 目錄下的64位 abi 子目錄,保證 apk 安裝後正常使用。
0x6 備註
其實本文是由於在 Android 的 so 加載上遇到不少坑,相信不少朋友都遇到過 UnsatisfiedLinkError 這個錯誤,反應在用戶的機型上也是千差萬別,可是有沒有想過,可能不是 apk 邏輯的問題,而是 Android 系統在安裝 APK 的時候,因爲 PackageManager 的問題,並無拷貝相應的 SO 呢?能夠參考下面第4個連接,做者給出瞭解決方案,就是當出現 UnsatisfiedLinkError 錯誤時,手動拷貝 so 來解決的。
因爲oschina篇幅有限,更詳細文章請點擊原文Android 系統安裝 apk 時解壓 so 的邏輯問題