溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

zipflinger導(dǎo)致的UnsatisfiedLinkError的實(shí)例分析

發(fā)布時(shí)間:2021-12-24 14:27:46 來(lái)源:億速云 閱讀:304 作者:柒染 欄目:互聯(lián)網(wǎng)科技

zipflinger導(dǎo)致的UnsatisfiedLinkError的實(shí)例分析,相信很多沒(méi)有經(jīng)驗(yàn)的人對(duì)此束手無(wú)策,為此本文總結(jié)了問(wèn)題出現(xiàn)的原因和解決方法,通過(guò)這篇文章希望你能解決這個(gè)問(wèn)題。

筆者在安卓源碼環(huán)境下做一些開(kāi)發(fā)工作。幾日前碰到了一個(gè)奇怪的問(wèn)題,預(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)化問(wèn)題,寫(xiě)了一個(gè)極簡(jiǎn)的 Demo app,將這個(gè)app預(yù)裝在 /system/priv-app下,使用AGP 4.0及其以下的版本都正常,一旦使用AGP 4.1及其以上的版本打出來(lái)的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)
...

問(wèn)題原因很明顯是由于升級(jí)了AGP到4.1導(dǎo)致的,不過(guò)查看了一下官方的changelog,沒(méi)發(fā)現(xiàn)有什么明顯的跟這個(gè)問(wèn)題相關(guān)的改動(dòng)。 分析log發(fā)現(xiàn)是so加載失敗了,可是把MyTestApp.apk pull出來(lái)解壓,發(fā)現(xiàn)so文件是存在的,路徑也沒(méi)問(wèn)題,那問(wèn)題出現(xiàn)在哪呢,這個(gè)時(shí)候只能先分析一下系統(tǒng)加載so的流程,看看問(wèn)題出在哪了。

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)過(guò)大量的debug,最終發(fā)現(xiàn)系統(tǒng)會(huì)使用 "!/" 這個(gè)分隔符來(lái)分隔路徑 /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è)出問(wèn)題應(yīng)該發(fā)生在zipalign相關(guān)的事情上。根據(jù)官方文檔的描述,zipalign的目的是確保所有未壓縮數(shù)據(jù)的開(kāi)頭均相對(duì)于文件開(kāi)頭部分執(zhí)行特定的對(duì)齊。具體來(lái)說(shuō),它會(huì)使 APK 中的所有未壓縮數(shù)據(jù)(例如圖片或原始文件)在 4 字節(jié)邊界上對(duì)齊。這樣一來(lái),即可使用 mmap() 直接訪(fǎng)問(wèn)所有部分,即使其中包含具有對(duì)齊限制的二進(jìn)制數(shù)據(jù)也沒(méi)關(guān)系。這樣做的好處是可以減少運(yùn)行應(yīng)用時(shí)消耗的 RAM 容量。

很顯然/system/priv-app/MyTestApp/MyTestApp.apk這個(gè)apk的對(duì)齊處理應(yīng)該是有問(wèn)題的,我們來(lái)做一下驗(yàn)證。將這個(gè)apk pull出來(lái),執(zhí)行以下命令,發(fā)現(xiàn)確實(shí)有問(wèn)題。

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是否有問(wèn)題呢,我們按照相同的方法驗(yàn)證一下源文件,發(fā)現(xiàn)是沒(méi)問(wèn)題的!那么問(wèn)題就很明顯了,安卓系統(tǒng)在編譯的時(shí)候一定是對(duì)這個(gè)apk做了一些處理,導(dǎo)致出現(xiàn)了問(wèn)題。于是我們需要來(lái)看一下編譯相關(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

也就是說(shuō)如果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)過(guò)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)在問(wèn)題比較明顯了,就是對(duì)于AGP4.1打出來(lái)的apk包,經(jīng)過(guò)uncompress-dexs操作后,再重新執(zhí)行zipalign,生成的apk文件的對(duì)齊是有問(wèn)題的。為了方便debug,將uncompress-dexs對(duì)應(yīng)的操作寫(xiě)了一個(gè)shell腳本 uncompress-dexs

那么為什么系統(tǒng)會(huì)對(duì)apk做這樣的處理呢,LOCAL_UNCOMPRESS_DEX 這個(gè)參數(shù)我們似乎也沒(méi)有定義呀,查看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)過(guò)一些搜索最終發(fā)現(xiàn),google從AGP 3.6版本開(kāi)始加入了一個(gè)新的打包工具zipflinger,不過(guò)只在構(gòu)建調(diào)試版本的時(shí)候生效,但是從AGP4.1開(kāi)始,構(gòu)建release版本默認(rèn)也會(huì)啟用zipflinger。通過(guò)在gradle.properties中加入以下屬性可禁用zipflinger

android.useNewApkCreator=false

我們使用AGP4.1,加入這個(gè)配置打包測(cè)試,發(fā)現(xiàn)問(wèn)題果然解決了。

雖然問(wèn)題解決了,不過(guò)難道我們就不能在系統(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來(lái)處理apk中dex文件的解壓縮。我們找到這個(gè)工具的源碼,其實(shí)就是一個(gè)單獨(dú)的文件,使用go語(yǔ)言編寫(xiě)的 zip2zip.go,我們將源碼下載下來(lái),使用 go build 命令編譯成可執(zhí)行文件,這里我編譯了一個(gè) zip2zip,我們來(lái)使用這個(gè)工具對(duì)啟用zipflinger生成的apk進(jìn)行測(cè)試,發(fā)現(xiàn)果然沒(méi)問(wè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è)問(wèn)題終于算是解決了,總結(jié)來(lái)看就是一個(gè)舊版本的AOSP不兼容新的zipflinger打包工具的問(wèn)題。根據(jù)分析過(guò)程解決辦法有如下幾個(gè):

  • 解決辦法1

    在BoardConfig.mk文件中聲明

    	DONT_UNCOMPRESS_PRIV_APPS_DEXS := true


    (不推薦這種方式,只有在分區(qū)空間不足的情況下,才會(huì)聲明這個(gè)屬性,以犧牲一點(diǎn)dex的加載速度來(lái)?yè)Q取空間)

  • 解決辦法2

    預(yù)裝APP的時(shí)候設(shè)置不為privileged app

    	LOCAL_PRIVILEGED_MODULE := false


    這種處理方式不通用,有些app必須是privileged

  • 解決辦法3

    修改 build/core/definitions.mk 中的 uncompress-dexs方法,使用新的zip2zip方案來(lái)適配

    這種方法可行,不過(guò)需要修改的地方有點(diǎn)多,需要更新很多AOSP的新代碼過(guò)去,比較麻煩

  • 解決辦法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)注億速云行業(yè)資訊頻道,感謝各位的閱讀!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI