轉(zhuǎn)自
http://blog.csdn.net/jiangwei0910410003/article/details/52312451
Android中有時(shí)候?yàn)榱诵室约捌脚_(tái)開(kāi)發(fā)庫(kù)的支持,難免會(huì)用到NDK開(kāi)發(fā),那么都會(huì)產(chǎn)生一個(gè)so文件,通過(guò)native方法進(jìn)行調(diào)用,開(kāi)發(fā)和調(diào)用步驟很簡(jiǎn)單,這里就不多說(shuō)了,本文主要來(lái)介紹,我們?cè)谑褂胹o的時(shí)候總是會(huì)出現(xiàn)一些常見(jiàn)的問(wèn)題,而現(xiàn)在插件化開(kāi)發(fā)也很普遍了,有時(shí)候插件中也會(huì)包含一些so文件,需要加載,這時(shí)候也會(huì)出現(xiàn)一些問(wèn)題。本文就來(lái)詳細(xì)總結(jié)一下這些問(wèn)題出現(xiàn)的原因,以及解決方法,主要還是通過(guò)源碼來(lái)分析。
因?yàn)楸疚闹饕ㄟ^(guò)分析源碼來(lái)分析so使用的知識(shí)點(diǎn)和問(wèn)題總結(jié),所以涉及到了很多的源碼類(lèi),這里就現(xiàn)提供一下:
1、PackageManagerService.Java
+setNativeLibraryPaths:設(shè)置應(yīng)用的native庫(kù)路徑
+scanPackageDirtyLI:掃描包內(nèi)容初始化應(yīng)用信息
2、ActivityManagerService.java
+startProcessLocked:發(fā)送命令給Zygote進(jìn)程啟動(dòng)一個(gè)虛擬機(jī)
3、NativeLibraryHelper.java
底層實(shí)現(xiàn)類(lèi):com_android_internal_content_NativeLibraryHelper.cpp
+copyNativeBinariesWithOverride:釋放apk中的so文件到本地目錄
+findSupportedAbi:遍歷apk中的so文件結(jié)合abiList值得到應(yīng)用支持的abi類(lèi)型索引值
4、LoadApk類(lèi)和ApplicationLoaders類(lèi)
5、VMRuntime.java
底層實(shí)現(xiàn)類(lèi):dalvik_system_VMRuntime.c
+getInstructionSet:獲取虛擬機(jī)的指令集類(lèi)型
+is64BitAbi:判斷VM是否為64位
6、Runtime.java
底層實(shí)現(xiàn)類(lèi):dalvik/vm/native/java_lang_Runtime.cpp,dalvik/vm/Native.cpp
+nativeLoad:加載so文件
Android中在進(jìn)行NDK開(kāi)發(fā)的時(shí)候,都知道因?yàn)闄C(jī)型雜而多的原因,沒(méi)有一個(gè)大的標(biāo)準(zhǔn),所以很多廠商都會(huì)采用不同型號(hào)的cpu,那么在編譯so文件的時(shí)候,就需要進(jìn)行交叉編譯出多個(gè)cpu平臺(tái)版本,現(xiàn)在主流的cpu架構(gòu)版本:
armeabi/armeabi-v7a:這個(gè)架構(gòu)是arm類(lèi)型的,主要用于Android4.0之后的,cpu值32位的
x86/x86_64:這個(gè)架構(gòu)是x86類(lèi)型的,有32位和64位,占用的設(shè)備比例比較小
arm64-v8:這個(gè)架構(gòu)是arm類(lèi)型,主要用于Android5.0之后,cpu是64位的
這里可以看到,其中arm類(lèi)型的是往下兼容策略,比如arm64-v8a肯定兼容armeabi/armeabi-v7a,也就是說(shuō)armeabi/armeabi-v7a架構(gòu)的so文件可以用在arm64-v8a的設(shè)備中的,而armeabi-v7a也是兼容armeabi的,但是因?yàn)閏pu型號(hào)不同,所以arm體系和x86體系之間是不能相互兼容的。
在Android中如果想使用so的話,首先得先加載,加載現(xiàn)在主要有兩種方法,一種是直接System.loadLibrary方法加載工程中的libs目錄下的默認(rèn)so文件,這里的加載文件名是xxx,而整個(gè)so的文件名為:libxxx.so。還有一種是加載指定目錄下的so文件,使用System.load方法,這里需要加載的文件名是全路徑,比如:xxx/xxx/libxxx.so。
上面的兩種加載方式,在大部分場(chǎng)景中用到的都是第一種方式,而第二種方式用的比較多的就是在插件中加載so文件了。
不管是第一種方式還是第二種方式,其實(shí)到最后都是調(diào)用了Runtime.java類(lèi)的加載方法doLoad:
這里會(huì)先從類(lèi)加載中獲取到nativeLib路徑,然后在調(diào)用native方法nativeLoad(java_lang_Runtime.cpp):
這里調(diào)用了一個(gè)核心的方法dvmLoadNativeCode(dalvik/vm/Native.cpp):
注意:
這里有一個(gè)檢測(cè)異常的代碼,而這個(gè)錯(cuò)誤,是我們?cè)谑褂貌寮_(kāi)發(fā)加載so的時(shí)候可能會(huì)遇到的錯(cuò)誤,比如現(xiàn)在我們使用DexClassLoader類(lèi)去加載插件,但是因?yàn)槲覀優(yōu)榱瞬寮軌驅(qū)崟r(shí)更新,所以每次都會(huì)賦值新的DexClassLoader對(duì)象,但是第一次加載so文件到內(nèi)存中了,這時(shí)候退出程序,但是沒(méi)有真正意義上的退出,只是關(guān)閉了Activity了,這時(shí)候再次啟動(dòng)又會(huì)賦值新的加載器對(duì)象,那么原先so已經(jīng)加載到內(nèi)存中了,但是這時(shí)候是新的類(lèi)加載器那么就報(bào)錯(cuò)了,解決辦法其實(shí)很簡(jiǎn)單,主要有兩種方式:
第一種方式:在退出程序的時(shí)候采用真正意義上的退出,比如調(diào)用System.exit(0)方法,這時(shí)候進(jìn)程被殺了,加載到內(nèi)存的so也就被釋放了,那么下次賦值新的類(lèi)加載就在此加載so到內(nèi)存了,
第二種方式:就是全局定義一個(gè)static類(lèi)型的類(lèi)加載DexClassLoader也是可以的,因?yàn)閟tatic類(lèi)型是保存在當(dāng)前進(jìn)程中,如果進(jìn)程沒(méi)有被殺就一直存在這個(gè)對(duì)象,下次進(jìn)入程序的時(shí)候判斷當(dāng)前類(lèi)加載器是否為null,如果不為null就不要賦值了,但是這個(gè)方法有一個(gè)弊端就是類(lèi)加載器沒(méi)有從新賦值,如果插件這時(shí)候更新了,但是還是使用之前的加載器,那么新插件將不會(huì)進(jìn)行加載。
繼續(xù)往下看:
這里主要調(diào)用了兩個(gè)核心的系統(tǒng)方法,dlopen和dlsym,這兩個(gè)方法用途還是很多的,一般是先加載so文件,然后得到指定函數(shù)的指針,最后直接調(diào)用即可,主要用于調(diào)用動(dòng)態(tài)的調(diào)用so中的指定函數(shù)功能。而且這里注意到了最開(kāi)始先調(diào)用so中的JNI_OnLoad函數(shù),這個(gè)函數(shù)是so被加載之后調(diào)用的第一個(gè)方法。
到這里我們就總結(jié)一下Android中加載so的流程:
1、調(diào)用System.loadLibrary和System.load方法進(jìn)行加載so文件
2、通過(guò)Runtime.java類(lèi)的nativeLoad方法進(jìn)行最終調(diào)用,這里需要通過(guò)類(lèi)加載器獲取到nativeLib路徑。
3、到底層之后,就開(kāi)始使用dlopen方法加載so文件,然后使用dlsym方法調(diào)用JNI_OnLoad方法,最終開(kāi)始了so的執(zhí)行。
五、Android中類(lèi)加載器關(guān)聯(lián)so路徑
上面分析so加載過(guò)程中可以發(fā)現(xiàn)有一個(gè)地方,就是通過(guò)類(lèi)加載器來(lái)獲取到so的路徑,那么Android中的主要類(lèi)加載器有兩個(gè),一個(gè)是PathClassLoader和DexClassLoader,關(guān)于這兩個(gè)類(lèi)加載不多說(shuō)了,網(wǎng)上資料很多可以自行查找閱讀。而PathClassLoader是我們Android中默認(rèn)的類(lèi)加載器,也就是apk文件就是由他來(lái)加載的,我們可以通過(guò)查看源碼得知,Android中加載apk的類(lèi)加載可以從LoadApk.java類(lèi)查找到:
注意:
這個(gè)類(lèi)很重要的,而這個(gè)類(lèi)加載器也是我們?cè)谧霾寮臅r(shí)候,需要做一些操作,比如需要把加載插件的DexClassLoader類(lèi)給添加到這個(gè)系統(tǒng)加載器中,就可以解決插件中組件的生命周期問(wèn)題。
看看這個(gè)類(lèi)加載器在哪里賦值的:
去看看ApplicationLoaders.java類(lèi):
看到了,這里就是定義了PathClassLoader類(lèi)了,所以我們Android中應(yīng)用的默認(rèn)加載器是PathClassLoader,再去看看這個(gè)類(lèi)加載器的nativeLib是哪里:
我們?cè)谑褂肧ystem.loadLibrary加載so的時(shí)候,傳遞的是so文件的libxxx.so中的xxx部分,那么系統(tǒng)是如何找到這個(gè)so文件然后進(jìn)行加載的呢?這個(gè)就要先從apk文件安裝時(shí)機(jī)說(shuō)起。
我們?nèi)绻€沒(méi)有分析源碼之前,大致能夠猜想到的流程是:
在安裝apk的時(shí)候,系統(tǒng)解析apk文件,因?yàn)閟o文件肯定是存放在libs下指定平臺(tái)目錄中的,而apk文件本身就是一個(gè)壓縮文件,所以可以進(jìn)行解壓,然后讀取libs目錄下的so文件,進(jìn)行本地釋放解壓到指定目錄,然后在加載的時(shí)候就先拼接so文件的全路徑,最后在進(jìn)行加載工作即可。
通過(guò)猜想,下面就通過(guò)源碼來(lái)分析一下流程,系統(tǒng)在安裝apk的時(shí)候,是調(diào)用系統(tǒng)類(lèi):PackageManagerService.java類(lèi):
主要的核心方法是scanPackageDirtyLI:
這個(gè)方法主要通過(guò)傳遞的pkg變量,開(kāi)始構(gòu)造applicationInfo信息。我們往下面看,找到設(shè)置nativeLib信息的代碼:
這里注意有一個(gè)判斷,是不是多平臺(tái)架構(gòu)的應(yīng)用:
所以,我們看看info.flags有沒(méi)有設(shè)置這個(gè)標(biāo)志,我們看到上面的pkg變量是通過(guò)解析apk文件的類(lèi)PackageParser.java類(lèi)中獲取到的,所以可以去這個(gè)類(lèi)中找這個(gè)標(biāo)志位的設(shè)置。
這里看到了,如果在AndroidManifest.xml中設(shè)置了Application中的multiArch屬性值的話就有,但是我們默認(rèn)都沒(méi)有設(shè)置這個(gè)屬性值,那么就是false,也就是說(shuō)一般應(yīng)用都不是多平臺(tái)的。所以上面的isMultiArch方法就返回false,代碼就走到了這里:
在這里就有很多知識(shí)點(diǎn)了,而這里可以看到,就涉及到了so文件的釋放工作了,主要是在NativeLibraryHelper類(lèi)中,但是這里看到首先獲取abiList值:
通過(guò)Build.SUPPORTED_ABIS來(lái)獲取到的:
最終是通過(guò)獲取系統(tǒng)屬性:ro.product.cpu.abilist的值來(lái)得到的,我們可以使用getprop命令來(lái)查看這個(gè)屬性值:
這里獲取到的值是:arm64-v8a,armeabi-v7a,armeabi,我用的是64位的cpu設(shè)備,所以可以看到他有多個(gè)cpu架構(gòu)可選,而且看到這個(gè)順序會(huì)想到,這個(gè)順序正好是向下兼容的順序。
現(xiàn)在去看看NativeLibraryHelper類(lèi)的copyNativeBinariesForSupportedAbi方法:
這個(gè)方法中主要干了三件事:
第一件事是獲取應(yīng)用所支持的arch架構(gòu)類(lèi)型
第二件事是通過(guò)架構(gòu)類(lèi)型獲取so釋放的目錄
第三件事是native層中釋放apk中的指定架構(gòu)的so到設(shè)備目錄中
第一件事:獲取應(yīng)用所支持的arch架構(gòu)類(lèi)型
NativeLibraryHelper類(lèi)的findSupportedAbi方法,其實(shí)這個(gè)方法就是查找系統(tǒng)當(dāng)前支持的架構(gòu)型號(hào)索引值:
看看native方法的實(shí)現(xiàn):
這里看到了,會(huì)先讀取apk文件,然后遍歷apk文件中的so文件,得到全路徑然后在和傳遞進(jìn)來(lái)的abiList進(jìn)行比較,得到合適的索引值,其實(shí)實(shí)現(xiàn)邏輯很簡(jiǎn)單:abiList是:arm64-v8a,armeabi-v7a,armeabi,然后就開(kāi)始比例apk中有沒(méi)有這些架構(gòu)平臺(tái)的so文件,如果有,就直接返回abiList中的索引值即可,比如說(shuō)apk中的libs結(jié)構(gòu)如下:
那么這時(shí)候返回來(lái)的索引值就是0,代表的是arm64-v8a架構(gòu)的。如果apk文件中沒(méi)有arm64-v8a目錄的話,那么就返回1,代表的是armeabi-v7a架構(gòu)的。依次類(lèi)推。得到應(yīng)用支持的架構(gòu)索引之后就可以獲取so釋放到設(shè)備中的目錄了。
這里主要通過(guò)VMRuntime.java中的getInstructionSet方法:
這里調(diào)用了一個(gè)map結(jié)構(gòu)值:
這里的arch架構(gòu)和目錄對(duì)應(yīng)關(guān)系,如果arch是arm64-v8a的話,那么目錄就是arm64了。
直接調(diào)用的是native層方法iterateOverNativeFiles:
好了到這里就講完了上面的三件事了,而這三件事做完之后,apk中的so文件就會(huì)被釋放到本地設(shè)備中的指定目錄中了,當(dāng)然這里系統(tǒng)會(huì)根據(jù)abiList中的值以及apk中包含的arch類(lèi)型的so來(lái)決定釋放哪個(gè)目錄中的so文件,比如這里通過(guò)ApplicationInfo類(lèi)來(lái)打印當(dāng)前應(yīng)用的nativeLibraryDir值:
打印的結(jié)果:
看到了,因?yàn)槭莂rm64-v8a類(lèi)型的,所以目錄是arm64的,而且可以看到這個(gè)應(yīng)用不是多平臺(tái)的。
我們可以看到Android中是如何釋放apk中的so文件到本地目錄的:
1、通過(guò)遍歷apk文件中的so文件的全路徑,然后和系統(tǒng)的abiList中的類(lèi)型值進(jìn)行比較,如果匹配到了就返回arch類(lèi)型的索引值
2、得到了應(yīng)用所支持的arch類(lèi)型之后,就開(kāi)始獲取創(chuàng)建本地釋放so的目錄
3、然后開(kāi)始釋放so文件
我們?cè)赑ackageMangerService類(lèi)中繼續(xù)往下看:
這里還要保存上面獲取到應(yīng)用支持的arch類(lèi)型值,我們可以使用反射打印這個(gè)值:
打印結(jié)果:
這個(gè)值在后面應(yīng)用創(chuàng)建VM的時(shí)候會(huì)用到。
接著開(kāi)始設(shè)置應(yīng)用的nativeLib路徑了:
看看這個(gè)方法的實(shí)現(xiàn):
這里先判斷是不是64位:
通過(guò)arch類(lèi)型對(duì)應(yīng)的目錄來(lái)判斷的:
這里如果是64位,目錄就是lib,如果是32位就是lib64:
這樣就和我們上面釋放so文件的目錄保持一致了,所以這里的ApplicationInfo類(lèi)中的lib路徑就是我們上面釋放so之后的路徑了。
在之前說(shuō)到了類(lèi)加載器中的lib路徑,我們可以打印一下庫(kù)路徑的,這里直接使用getClassLoader得到加載器打印即可:
這里看到Library的目錄包含很多路徑。
七、Android中64位系統(tǒng)如何兼容32位的so
上面分析完了,so文件的釋放工作,下面繼續(xù)來(lái)看一下如果一個(gè)64位系統(tǒng)的Android設(shè)備如何做到能夠運(yùn)行32位的so文件,這個(gè)就需要從應(yīng)用的啟動(dòng)說(shuō)起了,那么這個(gè)類(lèi)就是ActivityManagerService.java,有一個(gè)核心的方法:startProcessLocked,這個(gè)方法就是向Zygote進(jìn)程發(fā)送一個(gè)消息,為這個(gè)應(yīng)用創(chuàng)建虛擬機(jī)開(kāi)始運(yùn)行程序了:
這里在發(fā)送消息給Zygote進(jìn)程,看到這里通過(guò)ApplicationInfo中的primaryCpuAbi類(lèi)型告訴Zygote改創(chuàng)建多少位的虛擬機(jī),我們查看系統(tǒng)啟動(dòng)文件init.rc內(nèi)容:
這里會(huì)啟動(dòng)一個(gè)64位的Zygote進(jìn)程
然后啟動(dòng)一個(gè)32位的Zygot進(jìn)程
所以這里應(yīng)該就可以想明白了,原來(lái)系統(tǒng)啟動(dòng)的時(shí)候,如果是64位的系統(tǒng)設(shè)備,會(huì)啟動(dòng)兩個(gè)Zygote進(jìn)程用來(lái)兼容32位類(lèi)型的應(yīng)用,我們可以使用ps命令查看進(jìn)程:
看到了,這里果然啟動(dòng)了兩個(gè)Zygote進(jìn)程,一個(gè)64位的,一個(gè)是32位的。所以兼容功能的大致流程圖應(yīng)該是這樣的:
上層啟動(dòng)應(yīng)用的時(shí)候會(huì)把應(yīng)用的abi類(lèi)型帶過(guò)來(lái),然后這里會(huì)根據(jù)這個(gè)類(lèi)型發(fā)送給具體的Zygote進(jìn)程消息,來(lái)創(chuàng)建虛擬機(jī)開(kāi)始運(yùn)行程序,這樣就做到了兼容。
有時(shí)候我們?cè)陂_(kāi)發(fā)插件的時(shí)候,可能會(huì)調(diào)用so文件,一般來(lái)說(shuō)有兩種方案:
一種是在加載插件的時(shí)候,先把插件中的so文件釋放到本地目錄,然后在把目錄設(shè)置到DexClassLoader類(lèi)加載器的nativeLib中。
一種在插件初始化的時(shí)候,釋放插件中的so文件到本地目錄,然后使用System.load方法去全路徑加載so文件
這兩種方式的區(qū)別在于,第一種方式的代碼邏輯放在了宿主工程中,同時(shí)so文件可以放在插件的任意目錄中,然后在解壓插件文件找到這個(gè)so文件釋放即可。第二種方式的代碼邏輯是放在了插件中,同時(shí)so文件只能放在插件的assets目錄中,然后通過(guò)把插件文件設(shè)置到程序的AssetManager中,最后通過(guò)訪問(wèn)assets中的so文件進(jìn)行釋放。
上面就全部分析完了Android中關(guān)于so加載的相關(guān)內(nèi)容:
1、so編譯平臺(tái)問(wèn)題
2、so加載流程分析
3、so文件釋放功能分析
4、so文件兼容功能分析
5、插件中so文件調(diào)用功能分析
第一個(gè)問(wèn)題:Could not find libxxx.so
這個(gè)問(wèn)題看上去很好理解,就是在調(diào)用加載so的方法的時(shí)候,到底層使用dlopen方法打開(kāi)so文件,發(fā)現(xiàn)找不到這個(gè)so文件,那么這個(gè)問(wèn)題產(chǎn)生的原因主要有兩個(gè):
第一個(gè)是我們的確忘了在工程的libs下存放so文件了;
第二個(gè)是我們把so文件放錯(cuò)目錄了;
第一個(gè)原因就不多說(shuō)了,主要來(lái)看第二原因:
有時(shí)候我們?cè)陂_(kāi)發(fā)項(xiàng)目的時(shí)候,可能會(huì)放多個(gè)架構(gòu)類(lèi)型的so文件,那么現(xiàn)在假如我的設(shè)備是arm64-v8a類(lèi)型的,我的項(xiàng)目中有三個(gè)so文件,比如叫做AAA.so,BBB.so,CCC.so,然后我再arm64-v8a目錄中放了AAA.so,BBB.so,而CCC.so忘了放了,但是會(huì)放到armeabi-v7a和armeabi目錄中,那么這時(shí)候就會(huì)發(fā)生找不到CCC.so的錯(cuò)誤,原因很簡(jiǎn)單:
上面分析了apk中so文件的釋放邏輯,系統(tǒng)會(huì)先遍歷apk中所有so文件的全路徑,然后在結(jié)合abiList的值來(lái)決定最終釋放哪個(gè)目錄中的so文件,那么現(xiàn)在系統(tǒng)是arm64-v8a了,而apk中的libs下也有arm64-v8a,所以這里就會(huì)把a(bǔ)pk中的libs\arm64-v8a中的所有so文件釋放解壓到本地目錄中,而不會(huì)在去釋放armeabi/armeabi-v7a了。因?yàn)閍rm64-v8a中沒(méi)有CCC.so文件,所以最終釋放到本地目錄中也是沒(méi)有這個(gè)so文件的,所以加載時(shí)找不到文件了。
解決辦法:就是在使用so文件的時(shí)候,需要確定在每個(gè)架構(gòu)類(lèi)型目錄中都要有相同的so文件即可。
第二個(gè)問(wèn)題:32-bit instead of 64-bit
這個(gè)問(wèn)題的原因主要是因?yàn)?4位的Zygote進(jìn)程創(chuàng)建的虛擬機(jī)中加載了32位的so文件,這個(gè)問(wèn)題的產(chǎn)生原因主要有兩個(gè):
第一個(gè)是我們把不同架構(gòu)類(lèi)型的so文件放錯(cuò)目錄了,比如armeabi/armeabi-v7a的so文件放到了arm64-v8a中了
第二個(gè)是我們?cè)陂_(kāi)發(fā)插件的過(guò)程中,宿主工程中有arm64-v8a目錄,但是插件中加載so卻是armeabi/armeabi-v7a類(lèi)型的
第一個(gè)原因就不多說(shuō)了,主要是因?yàn)閟o放錯(cuò)目錄了,來(lái)看一下第二個(gè)原因,我們?cè)陂_(kāi)發(fā)插件的時(shí)候有時(shí)候需要在插件中去加載so文件,一般都是使用System.load方式去加載全路徑的so文件,那么這里就可能存在一個(gè)問(wèn)題,比如宿主工程中,放了所有架構(gòu)的目錄,包括了64位的,因?yàn)榭紤]插件的大小,所以在插件中只放了armeabi-v7a目錄的so文件,如果設(shè)備是64位的系統(tǒng),那么這時(shí)候插件加載so文件就會(huì)報(bào)錯(cuò)。原因就在于上面分析的so兼容問(wèn)題中說(shuō)到了,因?yàn)樗拗鞴こ讨邪?4位的架構(gòu)arm64-v8a類(lèi)型,系統(tǒng)的abiList中也有arm64-v8a類(lèi)型,所以這時(shí)候應(yīng)用的ApplicationInfo的abi就是arm64-v8a了,那么就會(huì)發(fā)送消息給Zygote64的進(jìn)程,創(chuàng)建的也是64位的虛擬機(jī)了,而最后插件中加載so的類(lèi)型是32位的armeabi-v7a,那么就會(huì)報(bào)錯(cuò)了,因?yàn)?2位的so文件不能運(yùn)行在64位的虛擬機(jī)中的。
解決辦法:宿主工程和插件工程中的so文件的架構(gòu)類(lèi)型保持一致,這個(gè)將會(huì)帶來(lái)一個(gè)很大的問(wèn)題,就是插件包會(huì)變得很大,因?yàn)樗拗鞴こ虨榱思嫒荻鄶?shù)機(jī)型,加入了多個(gè)類(lèi)型的架構(gòu)so文件,但是插件為了減小包大小,就放了指定類(lèi)型的so文件,但是最終會(huì)存在這種問(wèn)題,所以這個(gè)解決辦法就要看項(xiàng)目需要了。
還有一個(gè)類(lèi)似的問(wèn)題:64-bit instead of 32-bit:
原理都是一樣的,32位的虛擬機(jī)中加載了64位的so文件問(wèn)題導(dǎo)致的。
第三個(gè)問(wèn)題:Shared library already opened
這個(gè)問(wèn)題在上面介紹so加載流程中已經(jīng)介紹過(guò)了,原因主要是因?yàn)橹笆褂肈exClassLoader加載so之后,so沒(méi)有釋放還在內(nèi)存中,而在此啟動(dòng)有弄了一個(gè)新的DexClassLoader對(duì)象去加載so問(wèn)題,就出錯(cuò)了。
我們使用DexClassLoader類(lèi)去加載插件,但是因?yàn)槲覀優(yōu)榱瞬寮軌驅(qū)崟r(shí)更新,所以每次都會(huì)賦值新的DexClassLoader對(duì)象,但是第一次加載so文件到內(nèi)存中了,這時(shí)候退出程序,但是沒(méi)有真正意義上的退出,只是關(guān)閉了Activity了,這時(shí)候再次啟動(dòng)又會(huì)賦值新的加載器對(duì)象,那么原先so已經(jīng)加載到內(nèi)存中了,但是這時(shí)候是新的類(lèi)加載器那么就報(bào)錯(cuò)了。
解決辦法:
第一種方式:在退出程序的時(shí)候采用真正意義上的退出,比如調(diào)用System.exit(0)方法,這時(shí)候進(jìn)程被殺了,加載到內(nèi)存的so也就被釋放了,那么下次賦值新的類(lèi)加載就在此加載so到內(nèi)存了。
第二種方式:就是全局定義一個(gè)static類(lèi)型的類(lèi)加載DexClassLoader也是可以的,因?yàn)閟tatic類(lèi)型是保存在當(dāng)前進(jìn)程中,如果進(jìn)程沒(méi)有被殺就一直存在這個(gè)對(duì)象,下次進(jìn)入程序的時(shí)候判斷當(dāng)前類(lèi)加載器是否為null,如果不為null就不要賦值了,但是這個(gè)方法有一個(gè)弊端就是類(lèi)加載器沒(méi)有從新賦值,如果插件這時(shí)候更新了,但是還是使用之前的加載器,那么新插件將不會(huì)進(jìn)行加載。
本文主要介紹了Android中關(guān)于so的相關(guān)知識(shí),主要包括so編譯多架構(gòu)問(wèn)題,so加載流程問(wèn)題,so釋放問(wèn)題,so系統(tǒng)兼容問(wèn)題以及插件中加載so文件的功能解析,看完本文之后,我們需要了解到的知識(shí)點(diǎn):
1、在NDK開(kāi)發(fā)時(shí),可以指定多種架構(gòu)類(lèi)型編譯出多種類(lèi)型的so文件。
2、so的加載流程主要是System類(lèi)中的兩個(gè)加載方法,最終都會(huì)調(diào)用Runtime中的nativeLoad的native方法,而這個(gè)native方法最終會(huì)調(diào)用dlopen來(lái)打開(kāi)so文件,然后在調(diào)用dlsym方法調(diào)用so的JNI_OnLoad方法。
3、關(guān)于apk文件在安裝的時(shí)候釋放so文件到本地目錄中,主要是結(jié)合當(dāng)前設(shè)備的abiList信息(這個(gè)信息主要是通過(guò)系統(tǒng)屬性:ro.product.cpu.abilist值來(lái)獲取的)和apk中不同類(lèi)型架構(gòu),來(lái)決定最終釋放哪個(gè)類(lèi)型目錄中的so文件,釋放完成之后,還需要設(shè)置應(yīng)用的nativeLib路徑,以及應(yīng)用的abi信息,因?yàn)檫@個(gè)abi信息在后面啟動(dòng)虛擬機(jī)的時(shí)候需要用到。
4、因?yàn)楝F(xiàn)在有很多設(shè)備已經(jīng)是64位系統(tǒng)了,但是為了兼容32位的so文件,所以這些64位系統(tǒng)就會(huì)在系統(tǒng)啟動(dòng)的時(shí)候創(chuàng)建兩個(gè)Zygote進(jìn)程,一個(gè)是64位的,一個(gè)是32位的,當(dāng)一個(gè)應(yīng)用啟動(dòng)的時(shí)候,需要?jiǎng)?chuàng)建虛擬機(jī),那么這時(shí)候就會(huì)把應(yīng)用的架構(gòu)類(lèi)型傳遞過(guò)去,系統(tǒng)會(huì)根據(jù)這個(gè)類(lèi)型來(lái)交給哪個(gè)Zygote進(jìn)程來(lái)處理這個(gè)應(yīng)用啟動(dòng)事件。這樣就可以做到so調(diào)用的兼容問(wèn)題了。
5、插件中加載so文件現(xiàn)階段主要有兩種方式,一種是先釋放插件中的so文件到本地目錄,然后設(shè)置DexClassLoader的nativeLib路徑;還有一種方式是先釋放插件中的so文件,然后調(diào)用System.load來(lái)加載全局路徑的so文件。
本文還總結(jié)了在使用so文件的時(shí)候,會(huì)遇到的一些問(wèn)題,主要是三個(gè)問(wèn)題:
1、so文件找不到問(wèn)題
這個(gè)問(wèn)題一般是因?yàn)槲覀兺浄帕藄o文件,或者是so文件沒(méi)有放置全部,也就是沒(méi)有在libs目錄中所有的架構(gòu)類(lèi)型目錄中放置。
2、不同位數(shù)的虛擬機(jī)運(yùn)行了不同位數(shù)的so文件
這個(gè)問(wèn)題一般是因?yàn)槲覀冊(cè)趌ibs目錄中把so文件放錯(cuò)目錄了,或者是宿主工程和插件工程中的so文件架構(gòu)類(lèi)型目錄沒(méi)有保持一致。
3、類(lèi)加載器加載so文件再次加載
這個(gè)問(wèn)題一般是因?yàn)椴寮_(kāi)發(fā)中使用了不同的DexClassLoader去加載多次相同的so文件導(dǎo)致的。
我們?cè)陂_(kāi)發(fā)的過(guò)程中有時(shí)候想知道系統(tǒng)的位數(shù),那么這里網(wǎng)上告知說(shuō)有好幾種方法,其實(shí)那些都是忽悠人的,特別是在使用這個(gè)api的時(shí)候:android.os.Build.CPU_ABI,我就是在項(xiàng)目中被這個(gè)方法坑爹了,這個(gè)方法其實(shí)不是獲取系統(tǒng)的位數(shù),而是獲取當(dāng)前應(yīng)用的架構(gòu)類(lèi)型位數(shù),就是我們前面分析的ApplicationInfo中的abi信息,我們可以查看一下源碼:
這里可以看到,這個(gè)字段已經(jīng)被廢棄了,因?yàn)樗豢孔V呀,這個(gè)字段在Build類(lèi)的static塊中進(jìn)行賦值的:
這里會(huì)通過(guò)VMRuntime類(lèi)的is64Bit方法來(lái)判斷當(dāng)前虛擬機(jī)的位數(shù),來(lái)獲取這個(gè)值
這里還有兩個(gè)系統(tǒng)屬性:
ro.product.cpu.abilist32是32位的所有arch架構(gòu)類(lèi)型
ro.product.cpu.abilist64是64位的所有arch架構(gòu)類(lèi)型
而這兩個(gè)字段值的合集就是前面的ro.product.cpu.abilist屬性值。
而VMRuntime的is64Bit方法是native方法,實(shí)現(xiàn)如下:
看到了,這里得到的是虛擬機(jī)的位數(shù),那么就是上面的Zygote進(jìn)程的位數(shù)了。那么問(wèn)題就來(lái)了,假如我的設(shè)備是64位的,但是我的項(xiàng)目中沒(méi)有arm64-v8a類(lèi)型的so文件,這時(shí)候在解析apk進(jìn)行釋放so文件的時(shí)候,就會(huì)得知架構(gòu)類(lèi)型是armeabi/armeabi-v7a了,因?yàn)楸闅vapk文件,沒(méi)有找到arm64-v8a類(lèi)型的so文件,這時(shí)候應(yīng)用的abi類(lèi)型就是armeabi/armeabi-v7a了,這就是32位的了,就會(huì)通知32位的Zygote進(jìn)程創(chuàng)建了一個(gè)32位的虛擬機(jī),那么此時(shí)我的項(xiàng)目中通過(guò)Build.CPU_ABI得到的系統(tǒng)位數(shù)就是32了,那么完全不是我們想要的了。
所以正確的獲取系統(tǒng)位數(shù)的方法是:
Android5.0系統(tǒng)之后,可以通過(guò)ro.product.cpu.abilist屬性字段值來(lái)判斷,如果這個(gè)字段值中包含了64的話,那么就是64位系統(tǒng)了
Android5.0系統(tǒng)之前,需要通過(guò)ro.product.cpu.abi屬性字段值來(lái)判斷,不過(guò)5.0系統(tǒng)之前都是32位的,還沒(méi)有出現(xiàn)64位呢。
十三、選擇適當(dāng)架構(gòu)類(lèi)型減小包大小
我們上面分析之后可以看到,如果想做到萬(wàn)無(wú)一失即,項(xiàng)目不報(bào)錯(cuò),而且so運(yùn)行效率也是非常高的話,就需要把那幾個(gè)架構(gòu)類(lèi)型的so文件都要在項(xiàng)目中放一遍,那么這個(gè)問(wèn)題就來(lái)了,如果so文件較大的話,apk包最終也是很大的,所以這里就需要做一次選擇了。
1、我們?cè)陂_(kāi)發(fā)一個(gè)項(xiàng)目的時(shí)候因?yàn)?,整個(gè)項(xiàng)目的so文件結(jié)構(gòu)我們可以控制,所以為了防止apk包增大,我們可以考慮只放幾個(gè)架構(gòu)類(lèi)型的so文件,比如最好的是放armeabi類(lèi)型的,因?yàn)槭紫痊F(xiàn)在大部分設(shè)備采用cpu型號(hào)都是arm的,少數(shù)采用x86或者是mips類(lèi)型的,其次是防止了armeabi類(lèi)型之后,對(duì)于armeabi-v7a和arm64-v8a就可以兼容了,不會(huì)存在報(bào)錯(cuò)問(wèn)題。但是因?yàn)橄到y(tǒng)需要兼容所以就會(huì)出現(xiàn)so運(yùn)行效率的問(wèn)題了,最好的效率就是指定架構(gòu)類(lèi)型的so運(yùn)行在對(duì)應(yīng)架構(gòu)類(lèi)型的設(shè)備中。因?yàn)楝F(xiàn)在大部分的設(shè)備系統(tǒng)版本都是4.0以上了,所以armeabi-v7a架構(gòu)類(lèi)型用的比較多了,所以有時(shí)候?yàn)榱诵蕟?wèn)題,項(xiàng)目中只放了這個(gè)架構(gòu)類(lèi)型的so文件,那么像老版本的手機(jī)armeabi的話就會(huì)報(bào)錯(cuò)了,當(dāng)然這個(gè)錯(cuò)誤是可以接受的即可。
2、有時(shí)候像x86和mips等少數(shù)類(lèi)型架構(gòu)的設(shè)備,開(kāi)發(fā)程序的時(shí)候會(huì)單獨(dú)出一個(gè)版本比如叫做xxx應(yīng)用x86版本
3、在開(kāi)發(fā)SDK的時(shí)候,因?yàn)殚_(kāi)發(fā)之后的SDK包是給其他app接入的,而對(duì)于接入的app,我們不能做太多的限制,所以理論上應(yīng)該把所有架構(gòu)類(lèi)型的so都要提供,這樣給需要接入的app進(jìn)行選擇即可,比如像百度地圖SDK:
本文主要是介紹了Android中關(guān)于so的相關(guān)知識(shí),而這些知識(shí)點(diǎn)都是在使用so文件中會(huì)經(jīng)常用到的,同時(shí)一些問(wèn)題也是我們會(huì)遇到的,這里只是做了一個(gè)總結(jié),同時(shí)也給出了插件中加載so文件的方案已經(jīng)遇到的問(wèn)題解決思路等內(nèi)容。