zipflinger導(dǎo)致的UnsatisfiedLinkError的實(shí)例分析,相信很多沒有經(jīng)驗(yàn)的人對(duì)此束手無策,為此本文總結(jié)了問題出現(xiàn)的原因和解決方法,通過這篇文章希望你能解決這個(gè)問題。
創(chuàng)新互聯(lián)成立與2013年,是專業(yè)互聯(lián)網(wǎng)技術(shù)服務(wù)公司,擁有項(xiàng)目成都網(wǎng)站設(shè)計(jì)、做網(wǎng)站、成都外貿(mào)網(wǎng)站建設(shè)公司網(wǎng)站策劃,項(xiàng)目實(shí)施與項(xiàng)目整合能力。我們以讓每一個(gè)夢(mèng)想脫穎而出為使命,1280元赤壁做網(wǎng)站,已為上家服務(wù),為赤壁各地企業(yè)和個(gè)人服務(wù),聯(lián)系電話:18982081108
筆者在安卓源碼環(huán)境下做一些開發(fā)工作。幾日前碰到了一個(gè)奇怪的問題,預(yù)裝的APP突然報(bào)了一個(gè)UnsatisfiedLinkError的崩潰。查了一下最近的改動(dòng)記錄,只是將AGP(Androidd gradle plugin) 從3.6.1版本升級(jí)到了4.1.0版本。
源碼環(huán)境為Android 9.0,app預(yù)裝在 /system/priv-app下,且app中包含有so。為了簡(jiǎn)化問題,寫了一個(gè)極簡(jiǎn)的 Demo app,將這個(gè)app預(yù)裝在 /system/priv-app下,使用AGP 4.0及其以下的版本都正常,一旦使用AGP 4.1及其以上的版本打出來的apk包,就會(huì)報(bào) UnsatisfiedLinkError的錯(cuò)誤。
app預(yù)裝的配置
include $(CLEAR_VARS) LOCAL_MODULE := MyTestApp.apk LOCAL_SRC_FILES := $(LOCAL_MODULE).apk LOCAL_MODULE_CLASS := APPS LOCAL_MODULE_TAGS := optional LOCAL_MODULE_SUFFIX := $(COMMON_ANDROID_PACKAGE_SUFFIX) LOCAL_CERTIFICATE := PRESIGNED LOCAL_PRIVILEGED_MODULE := true LOCAL_DEX_PREOPT := nostripping include $(BUILD_PREBUILT)
報(bào)錯(cuò)信息
E AndroidRuntime: java.lang.UnsatisfiedLinkError: dlopen failed: library "/system/priv-app/MyTestApp/MyTestApp.apk!/lib/armeabi-v7a/libmytest.so" not found E AndroidRuntime: at java.lang.Runtime.loadLibrary0(Runtime.java:1016) E AndroidRuntime: at java.lang.System.loadLibrary(System.java:1669) ...
問題原因很明顯是由于升級(jí)了AGP到4.1導(dǎo)致的,不過查看了一下官方的changelog,沒發(fā)現(xiàn)有什么明顯的跟這個(gè)問題相關(guān)的改動(dòng)。 分析log發(fā)現(xiàn)是so加載失敗了,可是把MyTestApp.apk pull出來解壓,發(fā)現(xiàn)so文件是存在的,路徑也沒問題,那問題出現(xiàn)在哪呢,這個(gè)時(shí)候只能先分析一下系統(tǒng)加載so的流程,看看問題出在哪了。
So加載的流程網(wǎng)上文章很多,就不逐一分析了,這里列出調(diào)用棧
ojluni/src/main/java/java/lang/System.java --> System.loadLibrary ojluni/src/main/java/java/lang/Runtime.java --> Runtime.loadLibrary0 -> nativeLoad ojluni/src/main/native/Runtime.c --> Runtime_nativeLoad art/openjdkjvm/OpenjdkJvm.cc --> JVM_NativeLoad art/runtime/java_vm_ext.cc --> JavaVMExt::LoadNativeLibrary system/core/libnativeloader/native_loader.cpp --> OpenNativeLibrary bionic/libdl/libdl.cpp --> android_dlopen_ext bionic/linker/dlfcn.cpp --> __loader_android_dlopen_ext bionic/linker/dlfcn.cpp --> dlopen_ext bionic/linker/linker.cpp --> do_dlopen bionic/linker/linker.cpp --> find_library bionic/linker/linker.cpp --> find_libraries bionic/linker/linker.cpp --> find_library_internal bionic/linker/linker.cpp --> load_library bionic/linker/linker.cpp --> open_library bionic/linker/linker.cpp --> open_library_in_zipfile
經(jīng)過大量的debug,最終發(fā)現(xiàn)系統(tǒng)會(huì)使用 "!/" 這個(gè)分隔符來分隔路徑 /system/priv-app/MyTestApp/MyTestApp.apk!/lib/armeabi-v7a/libmytest.so,然后在 /system/priv-app/MyTestApp/MyTestApp.apk這個(gè)apk文件(apk其實(shí)就是一個(gè)zip文件)中搜索name為 lib/armeabi-v7a/libmytest.so的entry。這部分邏輯在 bionic/linker/linker.cpp --> open_library_in_zipfile中。 導(dǎo)致加載失敗的是以下條件 entry.offset % PAGE_SIZE != 0
if (entry.method != kCompressStored || (entry.offset % PAGE_SIZE) != 0) { close(fd); return -1; }
由此我們可以推測(cè)出問題應(yīng)該發(fā)生在zipalign相關(guān)的事情上。根據(jù)官方文檔的描述,zipalign的目的是確保所有未壓縮數(shù)據(jù)的開頭均相對(duì)于文件開頭部分執(zhí)行特定的對(duì)齊。具體來說,它會(huì)使 APK 中的所有未壓縮數(shù)據(jù)(例如圖片或原始文件)在 4 字節(jié)邊界上對(duì)齊。這樣一來,即可使用 mmap() 直接訪問所有部分,即使其中包含具有對(duì)齊限制的二進(jìn)制數(shù)據(jù)也沒關(guān)系。這樣做的好處是可以減少運(yùn)行應(yīng)用時(shí)消耗的 RAM 容量。
很顯然/system/priv-app/MyTestApp/MyTestApp.apk這個(gè)apk的對(duì)齊處理應(yīng)該是有問題的,我們來做一下驗(yàn)證。將這個(gè)apk pull出來,執(zhí)行以下命令,發(fā)現(xiàn)確實(shí)有問題。
xxx@debian:~/workspace$ zipalign -c -v -p 4 MyTestApp.apk Verifying alignment of out.apk (4)... 3964 lib/armeabi-v7a/libmytest.so (BAD - 3964) 108038 META-INF/CERT.SF (OK - compressed) 108568 AndroidManifest.xml (OK - compressed) 109583 META-INF/CERT.RSA (OK - compressed) 110676 res/layout/activity_main.xml (OK - compressed) 111012 res/mipmap-xhdpi-v4/ic_launcher.png (OK) 115704 resources.arsc (OK) 116722 META-INF/MANIFEST.MF (OK - compressed) 117196 classes.dex (OK) Verification FAILED
那么gradle打包生成的apk是否有問題呢,我們按照相同的方法驗(yàn)證一下源文件,發(fā)現(xiàn)是沒問題的!那么問題就很明顯了,安卓系統(tǒng)在編譯的時(shí)候一定是對(duì)這個(gè)apk做了一些處理,導(dǎo)致出現(xiàn)了問題。于是我們需要來看一下編譯相關(guān)的處理。
Android系統(tǒng)對(duì)應(yīng) BUILT_PREBUILT 的腳本在 build/core/prebuilt_internal.mk中,其中
ifeq (true, $(LOCAL_UNCOMPRESS_DEX)) $(uncompress-dexs) endif # LOCAL_UNCOMPRESS_DEX
也就是說如果LOCAL_UNCOMPRESS_DEX 為true,那么會(huì)對(duì)apk進(jìn)行一個(gè) uncompress-dexs 的處理,uncompress-dexs定義在 build/core/definitions.mk中
# Uncompress dex files embedded in an apk. # define uncompress-dexs $(hide) if (zipinfo $@ '*.dex' 2>/dev/null | grep -v ' stor ' >/dev/null) ; then \ tmpdir=$@.tmpdir; \ rm -rf $$tmpdir && mkdir $$tmpdir; \ unzip -q $@ '*.dex' -d $$tmpdir && \ zip -qd $@ '*.dex' && \ ( cd $$tmpdir && find . -type f | sort | zip -qD -X -0 ../$(notdir $@) -@ ) && \ rm -rf $$tmpdir; \ fi endef
分析發(fā)現(xiàn),這個(gè)處理就是判斷apk中的 dex 后綴的文件是否是壓縮存儲(chǔ)的,如果不是壓縮存儲(chǔ)的那么不做任何操作,如果是壓縮存儲(chǔ)的,那么將其變?yōu)椴粔嚎s存儲(chǔ)的方式。(zip文件中的文件項(xiàng)目的存儲(chǔ)方式分為不壓縮存儲(chǔ)(stored)和壓縮存儲(chǔ)(deflated))
繼續(xù)分析發(fā)現(xiàn)經(jīng)過uncompress-dexs之后,編譯系統(tǒng)對(duì)這個(gè)apk還進(jìn)行了一步 align-package的操作,定義還是在 build/core/definitions.mk中
# Align STORED entries of a package on 4-byte boundaries to make them easier to mmap. # define align-package $(hide) if ! $(ZIPALIGN) -c $(ZIPALIGN_PAGE_ALIGN_FLAGS) 4 $@ >/dev/null ; then \ mv $@ $@.unaligned; \ $(ZIPALIGN) \ -f \ $(ZIPALIGN_PAGE_ALIGN_FLAGS) \ 4 \ $@.unaligned $@.aligned; \ mv $@.aligned $@; \ fi endef
那現(xiàn)在問題比較明顯了,就是對(duì)于AGP4.1打出來的apk包,經(jīng)過uncompress-dexs操作后,再重新執(zhí)行zipalign,生成的apk文件的對(duì)齊是有問題的。為了方便debug,將uncompress-dexs對(duì)應(yīng)的操作寫了一個(gè)shell腳本 uncompress-dexs
那么為什么系統(tǒng)會(huì)對(duì)apk做這樣的處理呢,LOCAL_UNCOMPRESS_DEX 這個(gè)參數(shù)我們似乎也沒有定義呀,查看LOCAL_UNCOMPRESS_DEX這個(gè)參數(shù)的定義和用法,在 build/core/dex_preopt_odex_install.mk中
# We explicitly uncompress APKs of privileged apps, and used by # privileged apps LOCAL_UNCOMPRESS_DEX := false ifneq (true,$(DONT_UNCOMPRESS_PRIV_APPS_DEXS)) ifeq (true,$(LOCAL_PRIVILEGED_MODULE)) LOCAL_UNCOMPRESS_DEX := true else ifneq (,$(filter $(PRODUCT_LOADED_BY_PRIVILEGED_MODULES), $(LOCAL_MODULE))) LOCAL_UNCOMPRESS_DEX := true endif # PRODUCT_LOADED_BY_PRIVILEGED_MODULES endif # LOCAL_PRIVILEGED_MODULE endif # DONT_UNCOMPRESS_PRIV_APPS_DEXS
分析發(fā)現(xiàn),如果DONT_UNCOMPRESS_PRIV_APPS_DEXS為默認(rèn)值false,那么系統(tǒng)會(huì)對(duì)privileged app,也就是 /system/priv-app/下的app執(zhí)行uncompress-dexs操作。
那么現(xiàn)在就需要去調(diào)研AGP4.1到底有什么改動(dòng),導(dǎo)致uncompress-dexs這個(gè)操作會(huì)對(duì)zipalign造成影響。
經(jīng)過一些搜索最終發(fā)現(xiàn),google從AGP 3.6版本開始加入了一個(gè)新的打包工具zipflinger,不過只在構(gòu)建調(diào)試版本的時(shí)候生效,但是從AGP4.1開始,構(gòu)建release版本默認(rèn)也會(huì)啟用zipflinger。通過在gradle.properties中加入以下屬性可禁用zipflinger
android.useNewApkCreator=false
我們使用AGP4.1,加入這個(gè)配置打包測(cè)試,發(fā)現(xiàn)問題果然解決了。
雖然問題解決了,不過難道我們就不能在系統(tǒng)集成的時(shí)候,集成啟用了zipflinger工具打包的apk嗎?google既然推出了這個(gè)工具想必是做了充分的測(cè)試的吧。由于我們目前使用的是Android 9.0的源碼,那我們看看最新的master上的這部分代碼是如何處理的,查看代碼果然發(fā)現(xiàn)了一些改動(dòng)。在最新的aosp源碼中,uncompress-dexs的實(shí)現(xiàn)如下 鏈接
# Uncompress dex files embedded in an apk. # define uncompress-dexs if (zipinfo $@ '*.dex' 2>/dev/null | grep -v ' stor ' >/dev/null) ; then \ $(ZIP2ZIP) -i $@ -o $@.tmp -0 "classes*.dex" && \ mv -f $@.tmp $@ ; \ fi endef
我們發(fā)現(xiàn)google使用了一個(gè)新的工具zip2zip來處理apk中dex文件的解壓縮。我們找到這個(gè)工具的源碼,其實(shí)就是一個(gè)單獨(dú)的文件,使用go語言編寫的 zip2zip.go,我們將源碼下載下來,使用 go build
命令編譯成可執(zhí)行文件,這里我編譯了一個(gè) zip2zip,我們來使用這個(gè)工具對(duì)啟用zipflinger生成的apk進(jìn)行測(cè)試,發(fā)現(xiàn)果然沒問題
zip2zip -i MyTestApp.apk -o out.apk -0 classes.dex zipalign -f -p 4 out.apk MyTestApp.apk zipalign -c -v -p 4 MyTest.apk
至此這個(gè)問題終于算是解決了,總結(jié)來看就是一個(gè)舊版本的AOSP不兼容新的zipflinger打包工具的問題。根據(jù)分析過程解決辦法有如下幾個(gè):
解決辦法1
在BoardConfig.mk文件中聲明
DONT_UNCOMPRESS_PRIV_APPS_DEXS := true
(不推薦這種方式,只有在分區(qū)空間不足的情況下,才會(huì)聲明這個(gè)屬性,以犧牲一點(diǎn)dex的加載速度來換取空間)
解決辦法2
預(yù)裝APP的時(shí)候設(shè)置不為privileged app
LOCAL_PRIVILEGED_MODULE := false
這種處理方式不通用,有些app必須是privileged
解決辦法3
修改 build/core/definitions.mk 中的 uncompress-dexs方法,使用新的zip2zip方案來適配
這種方法可行,不過需要修改的地方有點(diǎn)多,需要更新很多AOSP的新代碼過去,比較麻煩
解決辦法4
回退AGP版本到4.0或其以下 (顯然這不是一個(gè)好辦法)
解決辦法5 (推薦方法)
在app工程的gradle.properties中聲明禁用zipflinger
android.useNewApkCreator=false
看完上述內(nèi)容,你們掌握zipflinger導(dǎo)致的UnsatisfiedLinkError的實(shí)例分析的方法了嗎?如果還想學(xué)到更多技能或想了解更多相關(guān)內(nèi)容,歡迎關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道,感謝各位的閱讀!