您好,登錄后才能下訂單哦!
zipflinger導致的UnsatisfiedLinkError的實例分析,相信很多沒有經驗的人對此束手無策,為此本文總結了問題出現的原因和解決方法,通過這篇文章希望你能解決這個問題。
筆者在安卓源碼環境下做一些開發工作。幾日前碰到了一個奇怪的問題,預裝的APP突然報了一個UnsatisfiedLinkError的崩潰。查了一下最近的改動記錄,只是將AGP(Androidd gradle plugin) 從3.6.1版本升級到了4.1.0版本。
源碼環境為Android 9.0,app預裝在 /system/priv-app下,且app中包含有so。為了簡化問題,寫了一個極簡的 Demo app,將這個app預裝在 /system/priv-app下,使用AGP 4.0及其以下的版本都正常,一旦使用AGP 4.1及其以上的版本打出來的apk包,就會報 UnsatisfiedLinkError的錯誤。
app預裝的配置
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)
報錯信息
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) ...
問題原因很明顯是由于升級了AGP到4.1導致的,不過查看了一下官方的changelog,沒發現有什么明顯的跟這個問題相關的改動。 分析log發現是so加載失敗了,可是把MyTestApp.apk pull出來解壓,發現so文件是存在的,路徑也沒問題,那問題出現在哪呢,這個時候只能先分析一下系統加載so的流程,看看問題出在哪了。
So加載的流程網上文章很多,就不逐一分析了,這里列出調用棧
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
經過大量的debug,最終發現系統會使用 "!/" 這個分隔符來分隔路徑 /system/priv-app/MyTestApp/MyTestApp.apk!/lib/armeabi-v7a/libmytest.so,然后在 /system/priv-app/MyTestApp/MyTestApp.apk這個apk文件(apk其實就是一個zip文件)中搜索name為 lib/armeabi-v7a/libmytest.so的entry。這部分邏輯在 bionic/linker/linker.cpp --> open_library_in_zipfile 中。 導致加載失敗的是以下條件 entry.offset % PAGE_SIZE != 0
if (entry.method != kCompressStored || (entry.offset % PAGE_SIZE) != 0) { close(fd); return -1; }
由此我們可以推測出問題應該發生在zipalign相關的事情上。根據官方文檔的描述,zipalign的目的是確保所有未壓縮數據的開頭均相對于文件開頭部分執行特定的對齊。具體來說,它會使 APK 中的所有未壓縮數據(例如圖片或原始文件)在 4 字節邊界上對齊。這樣一來,即可使用 mmap() 直接訪問所有部分,即使其中包含具有對齊限制的二進制數據也沒關系。這樣做的好處是可以減少運行應用時消耗的 RAM 容量。
很顯然/system/priv-app/MyTestApp/MyTestApp.apk這個apk的對齊處理應該是有問題的,我們來做一下驗證。將這個apk pull出來,執行以下命令,發現確實有問題。
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是否有問題呢,我們按照相同的方法驗證一下源文件,發現是沒問題的!那么問題就很明顯了,安卓系統在編譯的時候一定是對這個apk做了一些處理,導致出現了問題。于是我們需要來看一下編譯相關的處理。
Android系統對應 BUILT_PREBUILT 的腳本在 build/core/prebuilt_internal.mk 中,其中
ifeq (true, $(LOCAL_UNCOMPRESS_DEX)) $(uncompress-dexs) endif # LOCAL_UNCOMPRESS_DEX
也就是說如果LOCAL_UNCOMPRESS_DEX 為true,那么會對apk進行一個 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
分析發現,這個處理就是判斷apk中的 dex 后綴的文件是否是壓縮存儲的,如果不是壓縮存儲的那么不做任何操作,如果是壓縮存儲的,那么將其變為不壓縮存儲的方式。(zip文件中的文件項目的存儲方式分為不壓縮存儲(stored)和壓縮存儲(deflated))
繼續分析發現經過uncompress-dexs之后,編譯系統對這個apk還進行了一步 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
那現在問題比較明顯了,就是對于AGP4.1打出來的apk包,經過uncompress-dexs操作后,再重新執行zipalign,生成的apk文件的對齊是有問題的。為了方便debug,將uncompress-dexs對應的操作寫了一個shell腳本 uncompress-dexs
那么為什么系統會對apk做這樣的處理呢,LOCAL_UNCOMPRESS_DEX 這個參數我們似乎也沒有定義呀,查看LOCAL_UNCOMPRESS_DEX這個參數的定義和用法,在 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
分析發現,如果DONT_UNCOMPRESS_PRIV_APPS_DEXS為默認值false,那么系統會對privileged app,也就是 /system/priv-app/下的app執行uncompress-dexs操作。
那么現在就需要去調研AGP4.1到底有什么改動,導致uncompress-dexs這個操作會對zipalign造成影響。
經過一些搜索最終發現,google從AGP 3.6版本開始加入了一個新的打包工具zipflinger,不過只在構建調試版本的時候生效,但是從AGP4.1開始,構建release版本默認也會啟用zipflinger。通過在gradle.properties中加入以下屬性可禁用zipflinger
android.useNewApkCreator=false
我們使用AGP4.1,加入這個配置打包測試,發現問題果然解決了。
雖然問題解決了,不過難道我們就不能在系統集成的時候,集成啟用了zipflinger工具打包的apk嗎?google既然推出了這個工具想必是做了充分的測試的吧。由于我們目前使用的是Android 9.0的源碼,那我們看看最新的master上的這部分代碼是如何處理的,查看代碼果然發現了一些改動。在最新的aosp源碼中,uncompress-dexs的實現如下 鏈接
# 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
我們發現google使用了一個新的工具zip2zip來處理apk中dex文件的解壓縮。我們找到這個工具的源碼,其實就是一個單獨的文件,使用go語言編寫的 zip2zip.go,我們將源碼下載下來,使用 go build
命令編譯成可執行文件,這里我編譯了一個 zip2zip,我們來使用這個工具對啟用zipflinger生成的apk進行測試,發現果然沒問題
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
至此這個問題終于算是解決了,總結來看就是一個舊版本的AOSP不兼容新的zipflinger打包工具的問題。根據分析過程解決辦法有如下幾個:
解決辦法1
在BoardConfig.mk文件中聲明
DONT_UNCOMPRESS_PRIV_APPS_DEXS := true
(不推薦這種方式,只有在分區空間不足的情況下,才會聲明這個屬性,以犧牲一點dex的加載速度來換取空間)
解決辦法2
預裝APP的時候設置不為privileged app
LOCAL_PRIVILEGED_MODULE := false
這種處理方式不通用,有些app必須是privileged
解決辦法3
修改 build/core/definitions.mk 中的 uncompress-dexs方法,使用新的zip2zip方案來適配
這種方法可行,不過需要修改的地方有點多,需要更新很多AOSP的新代碼過去,比較麻煩
解決辦法4
回退AGP版本到4.0或其以下 (顯然這不是一個好辦法)
解決辦法5 (推薦方法)
在app工程的gradle.properties中聲明禁用zipflinger
android.useNewApkCreator=false
看完上述內容,你們掌握zipflinger導致的UnsatisfiedLinkError的實例分析的方法了嗎?如果還想學到更多技能或想了解更多相關內容,歡迎關注億速云行業資訊頻道,感謝各位的閱讀!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。