简介
先简单介绍下,我们知道jni
是native
层与java
层交互的桥梁,有了jni,我们可以通过动态或静态的方式去加载so,从而读取so库中的native
逻辑。
常用架构
当我们需要将native
代码打包成so库时,我们需要使用ndk-build
等命令去生成对应的架构so库,常用的架构如下:
armeabi,armeabi-v7a,x86,mips,arm64-v8a,mips64,x86_64
外部加载与内部加载
- 打包在apk中的情况,不需要开发者自己去判断ABI,Android系统在安装APK的时候,不会安装APK里面全部的so库文件,而是会根据当前CPU类型支持的ABI,从APK里面拷贝最合适的so库,并保存在APP的内部存储路径的
libs
下面。 - 动态加载外部so的情况下,需要我们判断
ABI
类型来加载相应的so,Android系统不会帮我们处理。
加载so的两种方式
- System.load
参数必须为库文件的绝对路径
使用注意:只能将so放到内部进行绝对路径加载,而不能放置于sd卡,否则会抛异常
- System.loadLibrary
参数为库文件名,不包含库文件的扩展名
插件中so加载问题定位
小编写的《实战插件化-MPlugin》使用到了如下加载so方式
// 步骤1:加载生成的so库文件
// 注意要跟so库文件名相同
static {
System.loadLibrary("hello_jni");
}
// 步骤2:定义在JNI中实现的方法
public native String getFromJNI();
如果仅仅是使用addDexPath
方法将插件dex插入到宿主dexElements
中,那么插件apk中存在加载so的话,是会抛经典的UnsatisfiedLinkError
异常
通过阅读android7.0源码我们发现,当我们调用System.loadLibrary("hello_jni")
方法时,会进入如下源码层
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;
}
而通过如上源码我们会发现当filename
为null
时,会抛出经典的UnsatisfiedLinkError
异常,所以我们继续看loader.findLibrary(libraryName)
这个方法,源码如下:
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
数组来遍历查询子element
来获得librarypath
,所以我们定位到nativeLibraryPathElements
,再来看nativeLibraryPathElements
是如何生成的,继续往下看源码
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
中去运行插件apk,而插件apk中因为加载了so从而导致抛出经典的UnsatisfiedLinkError
异常。
解决方案
- 第一种方式:通过
DexClassLoader
去load取插件apk,我们知道DexClassLoader
中还可以传入libraryPath
,该参数就是允许你指定so加载路径,所以实现方式如下:
String librarySearchPath = "/data/data/mplugindemo.shengyuan.com.mplugindemo/mplugin_demo/lib/";
DexClassLoader loader = new DexClassLoader(dexPath, mContext.getCacheDir().getAbsolutePath(),librarySearchPath, mContext.getClassLoader());
然后获得如上loader
对象后,如果你的so加载逻辑是在fragment
中,而只是为了将插件中的fragment
载入到宿主容器中显示,如上方式就可以了,但是如果你是希望去启动插件Activity,由插件Activity去加载so的话,还需要将LoadedApk
中的mClassLoader
对象替换成如上loader
对象。(不建议使用,因为当你使用插件apk跳转到下一个页面的时候,会抛出找不到第三方公共库的异常,除非你重写startActivity,然后load插件dex中的class来启动对应的activity页面)
- 第二种方式:可以参考小编之前的《剖析ClassLoader深入热修复原理》文章中提到的,在
PathClassLoader
和BootClassLoader
之间插入一个 自定义的MyClassLoader
,然后在MyClassLoader
中重写findLibrary
方法 - 第三种方式:通过如上阅读定位我们知道,核心点在
nativeLibraryPathElements
数组,因为我们知道nativeLibraryPathElements
数组是通过makePathElements方法构建生成的,所以我们可以通过反射去调用makePathElements
方法,将librarySearchPath
路径传入,从而获得新的nativeLibraryPathElements
数组,然后将新旧合并。
实现方式如下:
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;
//调用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设置到系统中
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库,根据Build.CPU_ABI
来判断当前适用的so架构,然把对应架构的so库复制到宿主apk对应的data so目录下(/data/data/mplugindemo.shengyuan.com.mplugindemo/mplugin168/lib/arm64-v8a
)
已在android7.0、8.0验证通过
实例地址:https://github.com/3332523marco/MPlugin