随着业务的增大,我们的业务代码也随之增多,包的大小是有增无减,所以适当的时候思考下:怎么做减法–减小包的体积。

结合最近在做的公司的项目,觉得动态加载so文件是一个很好精简apk包的方法。举个例子,视频播放器的SDK(如IJKplayer,VLC player),他们的各种视频的解码器一般都是通过C/C++编译的so文件,这些so文件其实都不小,这样导致我们从市场上下载的apk包很大,所以能不能让so文件不随apk一起发布呢,而是按需下载(只有当需要播放视频时才去服务器下载,然后再在本地load)。

为什么要动态加载

其实刚才已经解释了,可以有效避免apk安装包过大,因为这些so文件是依赖server的下发,本地只是load的过程。
其次,动态加载可以动态升级so文件,也是动态化的一部分。可以在不发版的情况下,升级so文件。

动态加载so文件,必须进行安全性校验,避免不必要的安全事故。

动态加载so文件

1. System.load(String filePath)

加载so文件分为动态加载和静态加载。

  1. 静态加载就是通过System.loadLibrary(Sting libname);来直接加载,对于一个app它只能加载system的和我们自己添加到jniLibs下的so文件。
    图2-1

    这个是我的demo项目的路径,静态加载回去这些路径下找到对应的库,否则抛出异常。

  2. 动态加载这是通过System.load(String filePath)来加载filePath对应路径下的so文件,这个路径不可以是外置SDcard等拓展路径,必须是/data/**{package}下。

所以下发的so没有权限放到图2-1下,只能通过加载的so文件路径的方式来动态加载so文件。

方案1: 将so文件copy到/data/**{package}下,system.load(filePath).

2. 支持静态加载

但是我们这样做还是解决不了问题,因为有些so文件加载的过程是放到sdkxia的,如百度地图sdk,已经封装了加载so文件(静态加载),即使你已经实现了方案1仍然扔出UnsatisfiedLinkError的异常。
要弄清这个过程,就必须了解so的加载过程,以我的本地的android skd(Android)为例。
System源码

1
2
3
public static void loadLibrary(String libname) {
Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
}

RunningTime

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

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) {
// 去loade中查找libraryName命令的library
String filename = loader.findLibrary(libraryName);
if (filename == null) {
// It's not necessarily true that the ClassLoader used
// System.mapLibraryName, but the default setup does, and it's
// misleading to say we didn't find "libMyLibrary.so" when we
// actually searched for "liblibMyLibrary.so.so".
throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
System.mapLibraryName(libraryName) + "\"");
}
String error = doLoad(filename, loader);
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
return;
}

String filename = System.mapLibraryName(libraryName);
List<String> candidates = new ArrayList<String>();
String lastError = null;
for (String directory : getLibPaths()) {
String candidate = directory + filename;
candidates.add(candidate);

if (IoUtils.canOpenReadOnly(candidate)) {
String error = doLoad(candidate, loader);
if (error == null) {
return; // We successfully loaded the library. Job done.
}
lastError = error;
}
}

if (lastError != null) {
throw new UnsatisfiedLinkError(lastError);
}
throw new UnsatisfiedLinkError("Library " + libraryName + " not found; tried " + candidates);
}

代码中的loader是ClassLoader的对象,对于Android实际上是PathClassLoader,这个意思就是当有classLoader时就通过PathClassLoaderfindLibrary(libraryName)来加载(这个好像加载class),若无classLoader就通过mapLibraryName1()

建议大家看下native层怎么实现的:深入理解 System.loadLibrary

我们加载so看classLoader是怎么实现的,Android 5.0的源码源码:
BaseDexClassLoader.java的源码

1
2
3
4
@Override
public String findLibrary(String name) {
return pathList.findLibrary(name);
}

pathList就是我们的DexPathList对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
final class DexPathList {

private static final String DEX_SUFFIX = ".dex";
/** class definition context */
private final ClassLoader definingContext;
/**
* List of dex/resource (class path) elements.
* Should be called pathElements, but the Facebook app uses reflection
* to modify 'dexElements' (http://b/7726934).
*/
private final Element[] dexElements;
/** List of native library directories. */
private final File[] nativeLibraryDirectories;
…………
public String findLibrary(String libraryName) {
String fileName = System.mapLibraryName(libraryName);
for (File directory : nativeLibraryDirectories) {
String path = new File(directory, fileName).getPath();
if (IoUtils.canOpenReadOnly(path)) {
return path;
}
}
return null;
}
…………
}

看到了吧,会先找system下的so文件,再找nativeLibraryDirectories下的,而这个nativeLibraryDirectories就是我们的自己项目中jniLibs下对应的so文件的路径。
当以当我们静态加载时,其实找的so文件就是nativeLibraryDirectories,所以我们可以以此作为突破口,利用反射,将这个nativeLibraryDirectories的开始处加上我们自己放so的文件夹下(感觉像QQ空间对class做patch的方式哦,其实替换旧的so文件这种可以可行的)。
开始hook啦。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
PathClassLoader pathClassLoader = (PathClassLoader) context.getApplicationContext().getClassLoader();
try {
Object pathList = getPathList(pathClassLoader);

// 拿到nativeLibraryDirectories的Field
Field nativeLibraryDirectoriesField = pathList.getClass().getDeclaredField("nativeLibraryDirectories");
nativeLibraryDirectoriesField.setAccessible(true);
File[] libPaths = (File[]) nativeLibraryDirectoriesField.get(pathList);
File[] envilLibPaths = new File[libPaths.length + 1];
// 将存放我们自己so的文件夹加到第一位
envilLibPaths[0] = dir;
// 将原来的路径追加到后面
for (int i = 0; i < libPaths.length; i++) {
envilLibPaths[i + 1] = libPaths[i];
}
// 将新的nativeLibraryDirectories设置给pathList
nativeLibraryDirectoriesField.set(pathList, envilLibPaths);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

这个代码是在14-22都是ok的,但是23源码不是这样滴,看源码吧:
23的源码先放这,hook起来也不难。

Android 23源码建议hook nativeLibraryPathElements这个而不是nativeLibraryDirectories;

方案2:Hook DexPathList的nativeLibraryPathElements或者nativeLibraryDirectories,将我们自定义存so文件的文件夹作为他们的第一个元素。

出现的问题

刚开始我把所有视频相关的so文件扔到本地的一个文件下,再copy到/data/**{package}下,居然报32-bit instead of 64-bit 这个错误,我把so再放到jniLibs/armeabi下再跑可以啊,后来google了下发现有人在动态化时也遇到了,其中Anjon-github提到了一个方案:只要找任意一个32位的so文件(当然越小越好了)放到主程序中即可,于是我找了个1k的so文件放到了项目的jniLibs/armeabi下居然真的可以,这个原因不知为何,这个涉及到native代码,本人技术有限暂时没找到答案,不知道大家是否更好的解答或者解决方法。