Android 源码分析实战 - 动态加载修复 so 库
1. 需求背景
俗话说养兵千日用兵一时,学习源码分析到底有什么用呢?我们遇到的所有问题,都能通过分析源码解决;看似无法实现的功能,都能通过源码分析找到思路......。这些都是之前无数次给大家洗脑的概念,我们来看一下实际的开发需求,我带大家来动手实战几次。之前还在有信时,我们做的是一个音频直播的项目,后面由于这一块业务一直上不去,老板要我们在里面做一个 3D 的玩法,也就是采用 Unity + Android 的开发方式。Unity 导出的资源大概有 200M 左右,直接集成到 Android 肯定会增大包体积。而包体积太大,会影响我们的安装速度和启动速度等等,这个在之前就分析过源码原理,不是我们的重点。我们的重点是需求得实现但不能增大包体积,后面我们做出来的效果是包体积只增大了 50K 。由于实现了 Unity 与 Android 项目交互的从 0 到 1,实现了 Unity 资源的动态加载,我被评为了公司的优秀员工。后来需求搞完部门就合并了,大家走的走散的散,业务起不来我们也没了激情。再后来在面试简历上也是简单的写上了一笔,迷迷糊糊就进了腾讯,幸福也是来得太突然。其实大公司也很悲催 996 压力大,只是一般人我不好意思告诉他。
2. 需求分析
Unity 导出的资源大致分为三个部分,一个部分是 jar 包 50K 左右,第二部分是 so 库 30M 左右,第三部分是 assets 资源 100M 左右。首先是 jar 包,我们写的 Activity 需要继承自 jar 里面的 UnityPlayerActivity,而且 jar 包也并不大,因此我们不做动态加载应该没问题;其次是 assets 资源,这个也是很容易处理的;难就难在 so 的动态加载,当然有的同学肯定会认为这有什么难的,不就是:
static {
System.load("下载好的 so 目录全路径");
}
问题是 unity 导出的 jar 包中是这样写的
static {
(new k()).a();
o = false;
o = loadLibraryStatic("main");
}
protected static boolean loadLibraryStatic(String var0) {
try {
System.loadLibrary(var0);
return true;
} catch (UnsatisfiedLinkError var1) {
com.unity3d.player.e.Log(6, "Unable to find " + var0);
return false;
} catch (Exception var2) {
com.unity3d.player.e.Log(6, "Unknown error " + var2);
return false;
}
}
如果只是这样也还好,我们只要把这个 jar 里的源码,修改成 load 这种方案就好了,但比较坑的就是 libmain.so 在 C++ 层,还会动态的加载另外的两个 so 库文件,当时我是一晚上没睡好呀。晚上实在睡不着,我就开始翻 so 加载流程的源码。
3. 源码分析
public static void loadLibrary(String libname) {
Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
}
synchronized void loadLibrary0(ClassLoader loader, String libname) {
String libraryName = libname;
if (loader != null) {
// 通过 libname 从 ClassLoader 去找到 filename
String filename = loader.findLibrary(libraryName);
if (filename == null) {
throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
System.mapLibraryName(libraryName) + "\"");
}
String error = nativeLoad(filename, loader);
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
return;
}
...
}
@Override
public String findLibrary(String name) {
return pathList.findLibrary(name);
}
public String findLibrary(String libraryName) {
String fileName = System.mapLibraryName(libraryName);
// 通过 nativeLibraryPathElements 来遍历查找返回的
for (Element element : nativeLibraryPathElements) {
String path = element.findNativeLibrary(fileName);
if (path != null) {
return path;
}
}
return null;
}
源码其实还是比较简单的,最终的 so 文件是通过 DexPathList 遍历 nativeLibraryPathElements 来查找返回的,那么我们是不是可以往 nativeLibraryPathElements 的最前面插入一个 Element 呢?显然这是可以的,而且我们早在三年前的视频中就开始用这种套路,来动态加载修复 class 类了,难度系数并不大。
4. 版本适配
封装写完代码后,我开始迫不及待的炫耀了一番,转手就拿给同事去集成了:
集成效果.gif这才意识到自己是新手上路,于是我把 Android 4.0 - 9.0 的源码统统翻了个遍。
6.0 源码
private final Element[] nativeLibraryPathElements;
static class Element {
private final File dir;
private final boolean isDirectory;
private final File zip;
private final DexFile dexFile;
private ZipFile zipFile;
public Element(File dir, boolean isDirectory, File zip, DexFile dexFile) {
this.dir = dir;
this.isDirectory = isDirectory;
this.zip = zip;
this.dexFile = dexFile;
}
}
8.0 源码
private final NativeLibraryElement[] nativeLibraryPathElements;
static class NativeLibraryElement {
private final String zipDir;
private boolean initialized;
public NativeLibraryElement(File dir) {
this.path = dir;
this.zipDir = null;
}
public NativeLibraryElement(File zip, String zipDir) {
this.path = zip;
this.zipDir = zipDir;
if (zipDir == null) {
throw new IllegalArgumentException();
}
}
}
5.0 的源码
private final File[] nativeLibraryDirectories;
很多同学在开发的过程中,可能下意识的会想到大厂的一些第三方框架,但其实很多功能他们也未必有现成的实现,其次很多东西我们未必用得上,最重要的是自己写的未必就不行。
最终效果视频地址:https://pan.baidu.com/s/1tQ7omRNg8BgldnkjdlBPlw
视频密码:6hlc