cloud.tencent.com/developer/a… juejin.cn/post/710795…
一 背景
随着我们业务的增加,我们的包体积越来越大,然而占用包体积最多的是so文件,正常来说我们app内会打入 armeabi,armeabi-v7a,arm64-v8a,x86(当然这个是模拟器的,一般不打入),有的app为了减少包体积只打入armeabi 或者是 armeabi-v7a,Google Play 商店自从2019.8.1已强制要求支持64位架构 ,并且现在市面上的手机90%都已经是64位的处理器了(这个可以在你的项目中查看占比)。正常的做法是为客户端里打包两套架构,系统自动选择,但这样会使包体积接近翻倍。让支持64位的手机下载或者更新64位的包,仅支持32位的手机下载或者更新32位的包。但是,还不能完全解决so库的大小问题,因此我们考虑动态加载so,使之减少包体积。
二 系统具体加载的流程
- 安装app的时候,PMS会把指定架构的so库,拷贝到 data/data/[包名]/lib 下面
- 启动app的时候,会把系统的so文件夹,以及 安装包的so文件夹位置 给 BaseDexClassLoader 中的属性DexPathList 下面属性的 nativeLibraryDirectories 和 systemNativeLibraryDirectories 两个File集合
- 调用及使用 调用:System.loadLibrary("xxx")
看一下tinker的so加载流程
2.1 加载so流程 下面都是android29的源码
一般通过两种方式加载so
- System.loadLibrary
只能加载data/data/[包名]/lib 下 ,或者是一些系统的so(system/lib,system/product/lib,不同版本,不同产商,有可能不一样),系统的可以通过System.getProperty("java.library.path")得到。一般用的都是这种加载so库,不带lib 和 so后缀 - System.load
加载绝对路径下的so,文件名参数必须是完整的路径名且带文件so后缀
public final class System {
//加载特殊文件夹的so库,data/data/[包名]/lib 下 ,或者是一些系统的so(system/lib,system/product/lib都是系统规定好的一些文件夹,,不带lib 和 so后缀
public static void loadLibrary(String libname) {
Runtime.getRuntime().loadLibrary0(Reflection.getCallerClass(), libname);
}
// 加载任意文件夹的so,指定全路径,文件名参数必须是完整的路径名且带文件so后缀
public static void load(String filename) {
Runtime.getRuntime().load0(Reflection.getCallerClass(), filename);
}
}
2.2 看一下 Runtime 下的loadLibrary0()
public class Runtime {
private synchronized void loadLibrary0(ClassLoader loader, Class<?> callerClass, String libname) {
// 校验名字不能有 “/”
if (libname.indexOf((int)File.separatorChar) != -1) {
throw new UnsatisfiedLinkError(
"Directory separator should not appear in library name: " + libname);
}
String libraryName = libname;
// BootClassLoader 的 findLibrary 是null,所以这里处理一下
if (loader != null && !(loader instanceof BootClassLoader)) {
//从 ClassLoader 里面去寻找,即 从 BaseDexClassLoader 里面找
String filename = loader.findLibrary(libraryName);
if (filename == null) {
// 没有找到so 抛异常
throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
System.mapLibraryName(libraryName) + "\"");
}
// 真正加载so,Native方法
String error = nativeLoad(filename, loader);
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
return;
}
// 当 loader 为null ,或者是 BootClassLoader,就从系统的 so文件夹集合中找
// 拿到 系统的so文件夹集合
getLibPaths();
// 把 名字 改为 lib[name].so的形式
String filename = System.mapLibraryName(libraryName);
// 真正加载
String error = nativeLoad(filename, loader, callerClass);
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
}
}
上面我们知道 System.loadLibrary() 是不用带lib以及.so后缀的。最终都会调用到底层nativeLoad加载
分两种情况:
- 当 ClassLoader 不为 null 且 不是 BootClassLoader的时候,直接回从 BaseDexClassLoader 中调用 findLibrary,此时会返回 这个so的全路径,并且也会拼上 lib[name].so
- 反之 会从 系统的文件夹集合里面去找,也会拼上 lib[name].so
2.3 看一下 BaseDexClassLoader findLibrary()
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
@Override
public String findLibrary(String name) {
return pathList.findLibrary(name);
}
}
这里很简单,直接调用的是 DexPathList 的 findLibrary 方法,下面我们看一下 DexPathList 的源码
final class DexPathList {
/** List of application native library directories. */
// app 内部的 so文件夹
private final List<File> nativeLibraryDirectories;
/** List of system native library directories. */
// 系统的so文件夹地址
private final List<File> systemNativeLibraryDirectories;
// 寻找so,就是从这里找的
NativeLibraryElement[] nativeLibraryPathElements;
// 构造方法
public DexPathList(ClassLoader definingContext, ByteBuffer[] dexFiles) {
// 拿到app内部的so文件夹集合
this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
// 拿到系统的so文件夹集合
this.systemNativeLibraryDirectories =
splitPaths(System.getProperty("java.library.path"), true);
// 把 app内的so文件夹集合 和 系统so文件夹 合并
List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);
// 合成 NativeLibraryElement的数组
this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories);
// 开始遍历nativeLibraryPathElements,拿到so的全路径
public String findLibrary(String libraryName) {
// 把名字拼接起来 lib[name].so
String fileName = System.mapLibraryName(libraryName);
for (NativeLibraryElement element : nativeLibraryPathElements) {
String path = element.findNativeLibrary(fileName);
if (path != null) {
return path;
}
}
return null;
}
}
由上面可以看出 通过 DexPathList 中的 nativeLibraryDirectories 和systemNativeLibraryDirectories两个文件夹集合,生成一个NativeLibraryElement[],然后从这里面找对应的so,返回全路径。从以前的热修复技术可以联想到,我们可以hook这里的api 加入咱们自己定义的文件夹
2.4 最后都会走到 Native方法 Runtime.nativeLoad()
然后我们看一下C的代码,Runtime的 包名是java.lang,所以我们直接搜索 java_lang_Runtime.cc 中的 Runtime_nativeLoad 方法。里面的实现其实跟加载Class 差不多,先判断是否加载过这个,如果加载过,然后在比较加载的ClassLoader是否一样,如果都一样直接算加载完成,如果没加载过,再去加载 因为C的代码我也看不懂,因为自从api23之后就没有更新过java_lang_Runtime文件。有兴趣可以看一下Android 热修复方案Tinker(五) SO补丁加载和Dalvik虚拟机JNI方法的注册过程分析
三 参考Tinker的实现方法 、 demo 以及 demo地址
Tinker 就是 hook了DexPathList 中的 nativeLibraryDirectories,在这个文件夹集合中又添加一个咱们自己定义的文件夹。比如下面的代码,根据不同的版本对应的处理逻辑。TinkerLoadLibrary.installNativeLibraryPath(loader,自定义文件夹),就可以实现动态加载so库了,下面就是摘自 Tinker的TinkerLoadLibrary稍微处理了一下,不依赖Tinker也能用。
public class TinkerLoadLibrary {
private static final String TAG = "Tinker.LoadLibrary";
public static void installNativeLibraryPath(ClassLoader classLoader, File folder)
throws Throwable {
if (folder == null || !folder.exists()) {
ShareTinkerLog.e(TAG, "installNativeLibraryPath, folder %s is illegal", folder);
return;
}
// android o sdk_int 26
// for android o preview sdk_int 25
if ((Build.VERSION.SDK_INT == 25 && Build.VERSION.PREVIEW_SDK_INT != 0)
|| Build.VERSION.SDK_INT > 25) {
try {
V25.install(classLoader, folder);
} catch (Throwable throwable) {
// install fail, try to treat it as v23
// some preview N version may go here
ShareTinkerLog.e(TAG, "installNativeLibraryPath, v25 fail, sdk: %d, error: %s, try to fallback to V23",
Build.VERSION.SDK_INT, throwable.getMessage());
V23.install(classLoader, folder);
}
} else if (Build.VERSION.SDK_INT >= 23) {
try {
V23.install(classLoader, folder);
} catch (Throwable throwable) {
// install fail, try to treat it as v14
ShareTinkerLog.e(TAG, "installNativeLibraryPath, v23 fail, sdk: %d, error: %s, try to fallback to V14",
Build.VERSION.SDK_INT, throwable.getMessage());
V14.install(classLoader, folder);
}
} else if (Build.VERSION.SDK_INT >= 14) {
V14.install(classLoader, folder);
} else {
V4.install(classLoader, folder);
}
}
private static final class V4 {
private static void install(ClassLoader classLoader, File folder) throws Throwable {
String addPath = folder.getPath();
Field pathField = ShareReflectUtil.findField(classLoader, "libPath");
final String origLibPaths = (String) pathField.get(classLoader);
final String[] origLibPathSplit = origLibPaths.split(":");
final StringBuilder newLibPaths = new StringBuilder(addPath);
for (String origLibPath : origLibPathSplit) {
if (origLibPath == null || addPath.equals(origLibPath)) {
continue;
}
newLibPaths.append(':').append(origLibPath);
}
pathField.set(classLoader, newLibPaths.toString());
final Field libraryPathElementsFiled = ShareReflectUtil.findField(classLoader, "libraryPathElements");
final List<String> libraryPathElements = (List<String>) libraryPathElementsFiled.get(classLoader);
final Iterator<String> libPathElementIt = libraryPathElements.iterator();
while (libPathElementIt.hasNext()) {
final String libPath = libPathElementIt.next();
if (addPath.equals(libPath)) {
libPathElementIt.remove();
break;
}
}
libraryPathElements.add(0, addPath);
libraryPathElementsFiled.set(classLoader, libraryPathElements);
}
}
private static final class V14 {
private static void install(ClassLoader classLoader, File folder) throws Throwable {
final Field pathListField = ShareReflectUtil.findField(classLoader, "pathList");
final Object dexPathList = pathListField.get(classLoader);
final Field nativeLibDirField = ShareReflectUtil.findField(dexPathList, "nativeLibraryDirectories");
final File[] origNativeLibDirs = (File[]) nativeLibDirField.get(dexPathList);
final List<File> newNativeLibDirList = new ArrayList<>(origNativeLibDirs.length + 1);
newNativeLibDirList.add(folder);
for (File origNativeLibDir : origNativeLibDirs) {
if (!folder.equals(origNativeLibDir)) {
newNativeLibDirList.add(origNativeLibDir);
}
}
nativeLibDirField.set(dexPathList, newNativeLibDirList.toArray(new File[0]));
}
}
private static final class V23 {
private static void install(ClassLoader classLoader, File folder) throws Throwable {
final Field pathListField = ShareReflectUtil.findField(classLoader, "pathList");
final Object dexPathList = pathListField.get(classLoader);
final Field nativeLibraryDirectories = ShareReflectUtil.findField(dexPathList, "nativeLibraryDirectories");
List<File> origLibDirs = (List<File>) nativeLibraryDirectories.get(dexPathList);
if (origLibDirs == null) {
origLibDirs = new ArrayList<>(2);
}
final Iterator<File> libDirIt = origLibDirs.iterator();
while (libDirIt.hasNext()) {
final File libDir = libDirIt.next();
if (folder.equals(libDir)) {
libDirIt.remove();
break;
}
}
origLibDirs.add(0, folder);
final Field systemNativeLibraryDirectories = ShareReflectUtil.findField(dexPathList, "systemNativeLibraryDirectories");
List<File> origSystemLibDirs = (List<File>) systemNativeLibraryDirectories.get(dexPathList);
if (origSystemLibDirs == null) {
origSystemLibDirs = new ArrayList<>(2);
}
final List<File> newLibDirs = new ArrayList<>(origLibDirs.size() + origSystemLibDirs.size() + 1);
newLibDirs.addAll(origLibDirs);
newLibDirs.addAll(origSystemLibDirs);
final Method makeElements = ShareReflectUtil.findMethod(dexPathList,
"makePathElements", List.class, File.class, List.class);
final ArrayList<IOException> suppressedExceptions = new ArrayList<>();
final Object[] elements = (Object[]) makeElements.invoke(dexPathList, newLibDirs, null, suppressedExceptions);
final Field nativeLibraryPathElements = ShareReflectUtil.findField(dexPathList, "nativeLibraryPathElements");
nativeLibraryPathElements.set(dexPathList, elements);
}
}
private static final class V25 {
private static void install(ClassLoader classLoader, File folder) throws Throwable {
final Field pathListField = ShareReflectUtil.findField(classLoader, "pathList");
final Object dexPathList = pathListField.get(classLoader);
final Field nativeLibraryDirectories = ShareReflectUtil.findField(dexPathList, "nativeLibraryDirectories");
List<File> origLibDirs = (List<File>) nativeLibraryDirectories.get(dexPathList);
if (origLibDirs == null) {
origLibDirs = new ArrayList<>(2);
}
final Iterator<File> libDirIt = origLibDirs.iterator();
while (libDirIt.hasNext()) {
final File libDir = libDirIt.next();
if (folder.equals(libDir)) {
libDirIt.remove();
break;
}
}
origLibDirs.add(0, folder);
final Field systemNativeLibraryDirectories = ShareReflectUtil.findField(dexPathList, "systemNativeLibraryDirectories");
List<File> origSystemLibDirs = (List<File>) systemNativeLibraryDirectories.get(dexPathList);
if (origSystemLibDirs == null) {
origSystemLibDirs = new ArrayList<>(2);
}
final List<File> newLibDirs = new ArrayList<>(origLibDirs.size() + origSystemLibDirs.size() + 1);
newLibDirs.addAll(origLibDirs);
newLibDirs.addAll(origSystemLibDirs);
final Method makeElements = ShareReflectUtil.findMethod(dexPathList, "makePathElements", List.class);
final Object[] elements = (Object[]) makeElements.invoke(dexPathList, newLibDirs);
final Field nativeLibraryPathElements = ShareReflectUtil.findField(dexPathList, "nativeLibraryPathElements");
nativeLibraryPathElements.set(dexPathList, elements);
}
}
}
3.2 用mmkv测试动态加载so
- 我们把so拷贝到assets目录下面,就当是从网上下载了
- 我们在build.gradle中把需要打包的时候排除so库
- 然后在Application中 把so拷贝到一个内部的文件夹中。然后 用 Tinker的TinkerLoadLibrary 加载这个文件夹就可以了
3.2.1 排出so
在app的Build.gradlw中排出so
android {
packagingOptions {
//去除SO
exclude 'lib/x86/libmmkv.so'
exclude 'lib/armeabi-v7a/libmmkv.so'
exclude 'lib/arm64-v8a/libmmkv.so'
}
}
3.2.2 在Application中把assets对应的arm拷贝到本地文件夹
注意这里模拟的下载对应的ABI,正常应该先从nativeLibraryDirectories读取现在系统运行的abi,然后去下载这个abi类型。然后复制到本地文件夹中
String assetsPath = "";
String[] supportAbis = Build.SUPPORTED_ABIS;
// 这是demo ,所以选择最合适的去复制,线上需要判断apk中的so文件夹,对应的去复制
// 比如线上so的文件夹只有arm ,就只复制arm,从dexPathList中的nativeLibraryDirectories中查看
// 比如线上只有arm64
Log.d(TAG, "最适合的架构类型是:" + supportAbis[0]);
assetsPath = supportAbis[0];
File fileso = new File(getFilesDir().getAbsolutePath() + "/solib");
if (!fileso.exists()) {
fileso.mkdirs();
}
if (fileso.list().length <= 0) {
FileUtil.doCopy(this, assetsPath, fileso.getAbsolutePath());
}
try {
TinkerLoadLibrary.installNativeLibraryPath(getClassLoader(), fileso);
} catch (Throwable throwable) {
Log.e(MainActivity.TAG, throwable.getMessage());
}
3.2.3 ASM替换 System.loadLibrary 为 SoLoader.loadLibrary
package com.nzy.sodemo;
import android.util.Log;
/**
* @author niezhiyang
* since 2024/3/26
*/
public class SoLoader {
private final static String TAG = "SoLoader";
public static void loadLibrary(String libname) {
Log.d(TAG, "拦截到了 SoLoader " + libname + "------");
System.loadLibrary(libname);
StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < stackTraceElements.length; i++) {
sb.append(stackTraceElements[i]);
sb.append("\n");
}
Log.d(TAG, "拦截到了 堆栈信息 " + sb);
}
}
定义 com.nzy.plugin.SoPlugin
根据Transform遍历所有的jar和class,使用asm去更改方法
private byte[] referHackClass(byte[] inputStream) {
ClassReader classReader = new ClassReader(inputStream);
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
// ClassVisitor cv = new AutoClassVisitor(Opcodes.ASM6, classWriter);
// ClassVisitor cv = new ThreadClassVisitor(Opcodes.ASM6, classWriter);
ClassVisitor cv = new SoClassVisitor(Opcodes.ASM6, classWriter);
classReader.accept(cv, ClassReader.EXPAND_FRAMES);
return classWriter.toByteArray();
}
替换
public class SoMethodVisitor extends AdviceAdapter {
private String mMethodOwner = "java/lang/System";
private String mMethodName = "loadLibrary";
private String mMethodDesc = "(Ljava/lang/String;)V";
private final int newOpcode = INVOKESTATIC;
private final String newOwner = "com/nzy/sodemo/SoLoader";
private final String newMethodName = "loadLibrary";
private String clazzName;
protected SoMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor, String clazzName) {
super(api, methodVisitor, access, name, descriptor);
// 拿到当类名,如果是咱们的SoLoader类,就不去替换
this.clazzName = clazzName;
}
@Override
public void visitMethodInsn(int opcodeAndSource, String owner, String name, String descriptor, boolean isInterface) {
LoggerUtil.e("clazzName=" + clazzName + "---mMethodName=" + name + "---mMethodOwner=" + owner + "---" + (Objects.equals(mMethodOwner, owner)) + "---" + (Objects.equals(name, mMethodName)) + "----descriptor" + descriptor);
if (Objects.equals(mMethodOwner, owner) && Objects.equals(name, mMethodName) && !Objects.equals(clazzName, newOwner)) {
if (Objects.equals(descriptor, mMethodDesc)) {
super.visitMethodInsn(newOpcode, newOwner, newMethodName, descriptor, false);
return;
}
}
super.visitMethodInsn(opcodeAndSource, owner, name, descriptor, isInterface);
}
}
具体demo地址
参考
Android 热修复方案Tinker(五) SO补丁加载
Dalvik虚拟机JNI方法的注册过程分析
动态下发 so 库在 Android APK 安装包瘦身方面的应用