簡(jiǎn)介
先簡(jiǎn)單介紹下舆床,我們知道jni
是native
層與java
層交互的橋梁该溯,有了jni认轨,我們可以通過動(dòng)態(tài)或靜態(tài)的方式去加載so绅络,從而讀取so庫(kù)中的native
邏輯。
常用架構(gòu)
當(dāng)我們需要將native
代碼打包成so庫(kù)時(shí)嘁字,我們需要使用ndk-build
等命令去生成對(duì)應(yīng)的架構(gòu)so庫(kù)恩急,常用的架構(gòu)如下:
armeabi,armeabi-v7a纪蜒,x86衷恭,mips,arm64-v8a纯续,mips64随珠,x86_64
外部加載與內(nèi)部加載
- 打包在apk中的情況,不需要開發(fā)者自己去判斷ABI杆烁,Android系統(tǒng)在安裝APK的時(shí)候牙丽,不會(huì)安裝APK里面全部的so庫(kù)文件,而是會(huì)根據(jù)當(dāng)前CPU類型支持的ABI兔魂,從APK里面拷貝最合適的so庫(kù),并保存在APP的內(nèi)部存儲(chǔ)路徑的
libs
下面举娩。 - 動(dòng)態(tài)加載外部so的情況下析校,需要我們判斷
ABI
類型來加載相應(yīng)的so构罗,Android系統(tǒng)不會(huì)幫我們處理。
加載so的兩種方式
- System.load
參數(shù)必須為庫(kù)文件的絕對(duì)路徑
使用注意:只能將so放到內(nèi)部進(jìn)行絕對(duì)路徑加載智玻,而不能放置于sd卡遂唧,否則會(huì)拋異常
- System.loadLibrary
參數(shù)為庫(kù)文件名,不包含庫(kù)文件的擴(kuò)展名
插件中so加載問題定位
小編寫的《實(shí)戰(zhàn)插件化-MPlugin》使用到了如下加載so方式
// 步驟1:加載生成的so庫(kù)文件
// 注意要跟so庫(kù)文件名相同
static {
System.loadLibrary("hello_jni");
}
// 步驟2:定義在JNI中實(shí)現(xiàn)的方法
public native String getFromJNI();
如果僅僅是使用addDexPath
方法將插件dex插入到宿主dexElements
中吊奢,那么插件apk中存在加載so的話盖彭,是會(huì)拋經(jīng)典的UnsatisfiedLinkError
異常
通過閱讀android7.0源碼我們發(fā)現(xiàn),當(dāng)我們調(diào)用System.loadLibrary("hello_jni")
方法時(shí)页滚,會(huì)進(jìn)入如下源碼層
System.java
public static void loadLibrary(String libname) {
Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
}
Runtime.java
synchronized void loadLibrary0(ClassLoader loader, String libname) {
if (libname.indexOf((int)File.separatorChar) != -1) {
throw new UnsatisfiedLinkError(
"Directory separator should not appear in library name: " + libname);
}
String libraryName = libname;
if (loader != null) {
String filename = loader.findLibrary(libraryName);
if (filename == null) {
throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
System.mapLibraryName(libraryName) + "\"");
}
String error = doLoad(filename, loader);
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
return;
}
而通過如上源碼我們會(huì)發(fā)現(xiàn)當(dāng)filename
為null
時(shí)召边,會(huì)拋出經(jīng)典的UnsatisfiedLinkError
異常,所以我們繼續(xù)看loader.findLibrary(libraryName)
這個(gè)方法裹驰,源碼如下:
BaseDexClassLoader.java
@Override
public String findLibrary(String name) {
return pathList.findLibrary(name);
}
DexPathList.java
public String findLibrary(String libraryName) {
String fileName = System.mapLibraryName(libraryName);
for (Element element : nativeLibraryPathElements) {
String path = element.findNativeLibrary(fileName);
if (path != null) {
return path;
}
}
return null;
}
通過如上源碼我們大概知道了隧熙,System.loadLibrary
是通過BaseDexClassLoader
中的nativeLibraryPathElements
數(shù)組來遍歷查詢子element
來獲得librarypath
,所以我們定位到nativeLibraryPathElements
幻林,再來看nativeLibraryPathElements
是如何生成的贞盯,繼續(xù)往下看源碼
DexPathList.java
public DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory) {
········
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions, definingContext);
this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
this.systemNativeLibraryDirectories =
splitPaths(System.getProperty("java.library.path"), true);
List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);
this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories,
suppressedExceptions,
definingContext);
········
}
通過如上源碼我們可以知道原來dexElements
和nativeLibraryPathElements
是分開的,所以這就是為什么我們明明將插件apk中的dex插入到宿主apk的dexElements
中去運(yùn)行插件apk沪饺,而插件apk中因?yàn)榧虞d了so從而導(dǎo)致拋出經(jīng)典的UnsatisfiedLinkError
異常躏敢。
解決方案
- 第一種方式:通過
DexClassLoader
去load取插件apk,我們知道DexClassLoader
中還可以傳入libraryPath
整葡,該參數(shù)就是允許你指定so加載路徑父丰,所以實(shí)現(xiàn)方式如下:
String librarySearchPath = "/data/data/mplugindemo.shengyuan.com.mplugindemo/mplugin_demo/lib/";
DexClassLoader loader = new DexClassLoader(dexPath, mContext.getCacheDir().getAbsolutePath(),librarySearchPath, mContext.getClassLoader());
然后獲得如上loader
對(duì)象后,如果你的so加載邏輯是在fragment
中掘宪,而只是為了將插件中的fragment
載入到宿主容器中顯示蛾扇,如上方式就可以了,但是如果你是希望去啟動(dòng)插件Activity魏滚,由插件Activity去加載so的話镀首,還需要將LoadedApk
中的mClassLoader
對(duì)象替換成如上loader
對(duì)象。(不建議使用鼠次,因?yàn)楫?dāng)你使用插件apk跳轉(zhuǎn)到下一個(gè)頁(yè)面的時(shí)候更哄,會(huì)拋出找不到第三方公共庫(kù)的異常,除非你重寫startActivity腥寇,然后load插件dex中的class來啟動(dòng)對(duì)應(yīng)的activity頁(yè)面)
- 第二種方式:可以參考小編之前的《剖析ClassLoader深入熱修復(fù)原理》文章中提到的成翩,在
PathClassLoader
和BootClassLoader
之間插入一個(gè) 自定義的MyClassLoader
,然后在MyClassLoader
中重寫findLibrary
方法 - 第三種方式:通過如上閱讀定位我們知道赦役,核心點(diǎn)在
nativeLibraryPathElements
數(shù)組麻敌,因?yàn)槲覀冎?code>nativeLibraryPathElements數(shù)組是通過makePathElements方法構(gòu)建生成的,所以我們可以通過反射去調(diào)用makePathElements
方法掂摔,將librarySearchPath
路徑傳入术羔,從而獲得新的nativeLibraryPathElements
數(shù)組赢赊,然后將新舊合并。
實(shí)現(xiàn)方式如下:
public static void insertNativeLibraryPathElements(File soDirFile,Context context){
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
Object pathList = getPathList(pathClassLoader);
if(pathList != null) {
Field nativeLibraryPathElementsField = null;
try {
Method makePathElements;
Object invokeMakePathElements;
boolean isNewVersion = Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1;
//調(diào)用makePathElements
makePathElements = isNewVersion?pathList.getClass().getDeclaredMethod("makePathElements", List.class):pathList.getClass().getDeclaredMethod("makePathElements", List.class,List.class,ClassLoader.class);
makePathElements.setAccessible(true);
ArrayList<IOException> suppressedExceptions = new ArrayList<>();
List<File> nativeLibraryDirectories = new ArrayList<>();
nativeLibraryDirectories.add(soDirFile);
List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
//獲取systemNativeLibraryDirectories
Field systemNativeLibraryDirectoriesField = pathList.getClass().getDeclaredField("systemNativeLibraryDirectories");
systemNativeLibraryDirectoriesField.setAccessible(true);
List<File> systemNativeLibraryDirectories = (List<File>) systemNativeLibraryDirectoriesField.get(pathList);
Log.i("insertNativeLibrary","systemNativeLibraryDirectories "+systemNativeLibraryDirectories);
allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);
invokeMakePathElements = isNewVersion?makePathElements.invoke(pathClassLoader, allNativeLibraryDirectories):makePathElements.invoke(pathClassLoader, allNativeLibraryDirectories,suppressedExceptions,pathClassLoader);
Log.i("insertNativeLibrary","makePathElements "+invokeMakePathElements);
nativeLibraryPathElementsField = pathList.getClass().getDeclaredField("nativeLibraryPathElements");
nativeLibraryPathElementsField.setAccessible(true);
Object list = nativeLibraryPathElementsField.get(pathList);
Log.i("insertNativeLibrary","nativeLibraryPathElements "+list);
Object dexElementsValue = combineArray(list, invokeMakePathElements);
//把組合后的nativeLibraryPathElements設(shè)置到系統(tǒng)中
nativeLibraryPathElementsField.set(pathList,dexElementsValue);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
注意
需要先解壓插件apk级历,提取apk中的so庫(kù)释移,根據(jù)Build.CPU_ABI
來判斷當(dāng)前適用的so架構(gòu),然把對(duì)應(yīng)架構(gòu)的so庫(kù)復(fù)制到宿主apk對(duì)應(yīng)的data so目錄下(/data/data/mplugindemo.shengyuan.com.mplugindemo/mplugin168/lib/arm64-v8a
)
已在android7.0寥殖、8.0驗(yàn)證通過
實(shí)例地址:https://github.com/3332523marco/MPlugin