Android动态加载So以及仿照Tinker实现

4,927 阅读8分钟

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,使之减少包体积。

二 系统具体加载的流程

  1. 安装app的时候,PMS会把指定架构的so库,拷贝到 data/data/[包名]/lib 下面
  2. 启动app的时候,会把系统的so文件夹,以及 安装包的so文件夹位置 给 BaseDexClassLoader 中的属性DexPathList 下面属性的 nativeLibraryDirectories 和 systemNativeLibraryDirectories 两个File集合
  3. 调用及使用 调用: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加载
分两种情况:

  1. 当 ClassLoader 不为 null 且 不是 BootClassLoader的时候,直接回从 BaseDexClassLoader 中调用 findLibrary,此时会返回 这个so的全路径,并且也会拼上 lib[name].so
  2. 反之 会从 系统的文件夹集合里面去找,也会拼上 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

  1. 我们把so拷贝到assets目录下面,就当是从网上下载了
  2. 我们在build.gradle中把需要打包的时候排除so库
  3. 然后在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 安装包瘦身方面的应用