动态加载步骤

  1. 通过资源打包 so 到 APK,或者下载 so 文件到手机存储。
  2. 将 so 文件拷贝到 app 的私有目录下。
  3. 修改 nativeLibrary 加载路径。
  4. 通过 System.Load 或 System.loadLibrary 加载 so。

注意事项

  1. so 必须拷贝到私有目录,若直接加载 SD 卡文件,则会提示“Permission denied”。
  2. 要注意加载 so 与 app 运行的进程位数一致(32/64)。
  3. 部分机型必须修改 nativeLibrary 加载路径。(如 Android5.1 加载 64位 so,不修改路径情况下,可能导致C# dllimport 失败、so的依赖库加载失败等问题)
  4. nativeLibrary 加载路径存储在 DexPathList 中,在 Android 不同系统中,数据结构不同,需要进行兼容适配。

源码分析:nativeLibrary 路径

nativeLibrary 路径主要涉及两个类: dalvik.system.BaseDexClassLoader 和 dalvik.system.DexPathList。

BaseDexClassLoader 中持有一个 DexPathList 类型的 pathList 成员,so 加载时,均通过这个 pathList 去查找路径。DexPathList 定义并存储了 nativeLibrary 的路径信息。

Android 版本 nativeLibrary 路径定义 初始化方式 更新方法
Android 4.0-5.1 [14, 23) File[] nativeLibraryDirectories 直接赋值
Android 6.0-7.1 [23, 25) Element[] nativeLibraryPathElements Element[] makePathElements(List, File, List)
Android 8.0-12 [25, 31] NativeLibraryElement[] nativeLibraryPathElements NativeLibraryElement[] makePathElements(List) addNativePath

Android 4.0-5.1 [14, 23)

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
final class DexPathList {

private final File[] nativeLibraryDirectories;

// 构造函数,初始化 nativeLibraryDirectories
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
// 代码略
this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
}

// 查找 SO 绝对路径(遍历 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;
}

// 获取 SO 加载路径(返回 nativeLibraryDirectories, 在 BaseDexClassLoader.getLdLibraryPath 调用)
public File[] getNativeLibraryDirectories() {
return nativeLibraryDirectories;
}
}

Android 6.0-7.1 [23, 25)

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
final class DexPathList {

private final Element[] nativeLibraryPathElements;
private final List<File> nativeLibraryDirectories;
private final List<File> systemNativeLibraryDirectories;

public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {

// Native libraries may exist in both the system and application library paths
// 1. This class loader's library path for application libraries (libraryPath):
// 1.1. Native library directories
// 1.2. Path to libraries in apk-files
// 2. The VM's library path from the system property for system libraries (java.library.path)
this.nativeLibraryDirectories = splitPaths(libraryPath, false);
this.systemNativeLibraryDirectories =
splitPaths(System.getProperty("java.library.path"), true);
List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);

this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories, null, suppressedExceptions);

// 代码略
}

// 生成 nativeLibraryPathElements
private static Element[] makePathElements(List<File> files, File optimizedDirectory, List<IOException> suppressedExceptions) {
List<Element> elements = new ArrayList<>();

for (File file : files) {
File zip = null;
File dir = new File("");

if (file.isDirectory()) {
// We support directories for looking up resources and native libraries.
// Looking up resources in directories is useful for running libcore tests.
elements.add(new Element(file, true, null, null));
} else {
// 代码略
}

if ((zip != null)) {
elements.add(new Element(dir, false, zip, dex));
}
}

return elements.toArray(new Element[elements.size()]);
}

// 查找 SO 绝对路径(遍历 nativeLibraryPathElements)
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;
}
}

Android 8.0-12 [25, 31]

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
public final class DexPathList {

NativeLibraryElement[] nativeLibraryPathElements;
private final List<File> nativeLibraryDirectories;
private final List<File> systemNativeLibraryDirectories;

// 组合 nativeLibraryDirectories 和 systemNativeLibraryDirectories
private List<File> getAllNativeLibraryDirectories() {
List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);
return allNativeLibraryDirectories;
}

// 生成 nativeLibraryPathElements
private static NativeLibraryElement[] makePathElements(List<File> files) {
NativeLibraryElement[] elements = new NativeLibraryElement[files.size()];
int elementsPos = 0;
for (File file : files) {
String path = file.getPath();

if (path.contains(zipSeparator)) {
String split[] = path.split(zipSeparator, 2);
File zip = new File(split[0]);
String dir = split[1];
elements[elementsPos++] = new NativeLibraryElement(zip, dir);
} else if (file.isDirectory()) {
// We support directories for looking up native libraries.
elements[elementsPos++] = new NativeLibraryElement(file);
}
}
if (elementsPos != elements.length) {
elements = Arrays.copyOf(elements, elementsPos);
}
return elements;
}

// 构造函数,初始化 nativeLibraryDirectories、systemNativeLibraryDirectories、nativeLibraryPathElements
public DexPathList(ClassLoader definingContext, String librarySearchPath) {
if (definingContext == null) {
throw new NullPointerException("definingContext == null");
}

this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
this.systemNativeLibraryDirectories = splitPaths(System.getProperty("java.library.path"), true);
this.nativeLibraryPathElements = makePathElements(getAllNativeLibraryDirectories());
}

// 查找 SO 绝对路径(遍历 nativeLibraryPathElements)
public String findLibrary(String libraryName) {
String fileName = System.mapLibraryName(libraryName);

for (NativeLibraryElement element : nativeLibraryPathElements) {
String path = element.findNativeLibrary(fileName);

if (path != null) {
return path;
}
}

return null;
}

// 添加 SO 查找路径(更新 nativeLibraryPathElements))
public void addNativePath(Collection<String> libPaths) {
if (libPaths.isEmpty()) {
return;
}
List<File> libFiles = new ArrayList<>(libPaths.size());
for (String path : libPaths) {
libFiles.add(new File(path));
}
ArrayList<NativeLibraryElement> newPaths =
new ArrayList<>(nativeLibraryPathElements.length + libPaths.size());
newPaths.addAll(Arrays.asList(nativeLibraryPathElements));
for (NativeLibraryElement element : makePathElements(libFiles)) {
if (!newPaths.contains(element)) {
newPaths.add(element);
}
}
nativeLibraryPathElements = newPaths.toArray(new NativeLibraryElement[newPaths.size()]);
}
}

代码片段

修改 nativeLibrary 路径

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
public static void loadAssetLibrary(Context context, String libName, String assertPath) {
try {
File dirs = context.getDir("libs", Context.MODE_PRIVATE);
String soName = System.mapLibraryName(libName);
String soFrom = assertPath + File.separator + soName;
String soTo = dirs.getAbsolutePath() + File.separator + soName;

if (!FileUtils.isFileExist(soName, dirs)) {
InputStream inStream = context.getAssets().open(soFrom);
FileOutputStream outStream = new FileOutputStream(soTo);
FileUtils.copyFile(inStream, outStream);
}

final int sdkInt = Build.VERSION.SDK_INT;
if (sdkInt >= 25) {
V25.install(classLoader, folder);
} else if (sdkInt >= 23) {
V23.install(classLoader, folder);
} else if (sdkInt >= 14) {
V14.install(classLoader, folder);
}

Log.d(TAG, "loadAssetLibrary form: " + soFrom + ", to: " + soTo);
System.load(soTo);

} catch (Throwable throwable) {
Log.e(TAG, "loadAssetLibrary error " + throwable.getMessage());
}
}

Android 4.0-5.1 [14, 23)

1
2
3
4
5
6
7
8
private static final class V14 {
private static void install(ClassLoader classLoader, File folder) throws Throwable {
Field pathListField = ReflectUtil.findField(classLoader, "pathList");
Object dexPathList = pathListField.get(classLoader);

ReflectUtil.expandFieldArray(dexPathList, "nativeLibraryDirectories", new File[]{folder});
}
}

Android 6.0-7.1 [23, 25)

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
private static final class V23 {
private static void install(ClassLoader classLoader, File folder) throws Throwable {
Field pathListField = ReflectUtil.findField(classLoader, "pathList");
Object dexPathList = pathListField.get(classLoader);

Field nativeLibraryDirectories = ReflectUtil.findField(dexPathList, "nativeLibraryDirectories");
List<File> libDirs = (List<File>) nativeLibraryDirectories.get(dexPathList);

//去重
if (libDirs == null) {
libDirs = new ArrayList<>(2);
}
final Iterator<File> libDirIt = libDirs.iterator();
while (libDirIt.hasNext()) {
final File libDir = libDirIt.next();
if (folder.equals(libDir) || folder.equals(lastSoDir)) {
libDirIt.remove();
break;
}
}

libDirs.add(0, folder);
Field systemNativeLibraryDirectories = ReflectUtil.findField(dexPathList, "systemNativeLibraryDirectories");
List<File> systemLibDirs = (List<File>) systemNativeLibraryDirectories.get(dexPathList);

if (systemLibDirs == null) {
systemLibDirs = new ArrayList<>(2);
}

Method makePathElements = ReflectUtil.findMethod(dexPathList, "makePathElements", List.class, File.class, List.class);
ArrayList<IOException> suppressedExceptions = new ArrayList<>();
libDirs.addAll(systemLibDirs);

Object[] elements = (Object[]) makePathElements.invoke(dexPathList, libDirs, null, suppressedExceptions);
Field nativeLibraryPathElements = ReflectUtil.findField(dexPathList, "nativeLibraryPathElements");
nativeLibraryPathElements.setAccessible(true);
nativeLibraryPathElements.set(dexPathList, elements);
}
}

Android 8.0-12 [25, 31]

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
private static final class V25 {
private static void install(ClassLoader classLoader, File folder) throws Throwable {

Field pathListField = ReflectUtil.findField(classLoader, "pathList");
Object dexPathList = pathListField.get(classLoader);

Field nativeLibraryDirectories = ReflectUtil.findField(dexPathList, "nativeLibraryDirectories");
List<File> libDirs = (List<File>) nativeLibraryDirectories.get(dexPathList);

//去重
if (libDirs == null) {
libDirs = new ArrayList<>(2);
}
final Iterator<File> libDirIt = libDirs.iterator();
while (libDirIt.hasNext()) {
final File libDir = libDirIt.next();
if (folder.equals(libDir) || folder.equals(lastSoDir)) {
libDirIt.remove();
break;
}
}

libDirs.add(0, folder);
Field systemNativeLibraryDirectories = ReflectUtil.findField(dexPathList, "systemNativeLibraryDirectories");
List<File> systemLibDirs = (List<File>) systemNativeLibraryDirectories.get(dexPathList);

if (systemLibDirs == null) {
systemLibDirs = new ArrayList<>(2);
}

Method makePathElements = ReflectUtil.findMethod(dexPathList, "makePathElements", List.class);
libDirs.addAll(systemLibDirs);

Object[] elements = (Object[]) makePathElements.invoke(dexPathList, libDirs);
Field nativeLibraryPathElements = ReflectUtil.findField(dexPathList, "nativeLibraryPathElements");
nativeLibraryPathElements.setAccessible(true);
nativeLibraryPathElements.set(dexPathList, elements);
}
}

反射工具

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
public class ReflectUtil {

public static Field findField(Object instance, String name) throws NoSuchFieldException {
for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
try {
Field field = clazz.getDeclaredField(name);
if (!field.isAccessible()) {
field.setAccessible(true);
}
return field;
} catch (NoSuchFieldException e) {
}
}

throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass());
}

public static Field findField(Class<?> originClazz, String name) throws NoSuchFieldException {
for (Class<?> clazz = originClazz; clazz != null; clazz = clazz.getSuperclass()) {
try {
Field field = clazz.getDeclaredField(name);
if (!field.isAccessible()) {
field.setAccessible(true);
}
return field;
} catch (NoSuchFieldException e) {
}
}
throw new NoSuchFieldException("Field " + name + " not found in " + originClazz);
}

public static Method findMethod(Object instance, String name, Class<?>... parameterTypes)
throws NoSuchMethodException {
for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
try {
Method method = clazz.getDeclaredMethod(name, parameterTypes);
if (!method.isAccessible()) {
method.setAccessible(true);
}
return method;
} catch (NoSuchMethodException e) {
}
}

throw new NoSuchMethodException("Method "
+ name
+ " with parameters "
+ Arrays.asList(parameterTypes)
+ " not found in " + instance.getClass());
}

public static void expandFieldArray(Object instance, String fieldName, Object[] extraElements)
throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
Field jlrField = findField(instance, fieldName);

Object[] original = (Object[]) jlrField.get(instance);
Object[] combined = (Object[]) Array.newInstance(original.getClass().getComponentType(), original.length + extraElements.length);

// NOTE: changed to copy extraElements first, for patch load first

System.arraycopy(extraElements, 0, combined, 0, extraElements.length);
System.arraycopy(original, 0, combined, extraElements.length, original.length);

jlrField.set(instance, combined);
}

public static void reduceFieldArray(Object instance, String fieldName, int reduceSize)
throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
if (reduceSize <= 0) {
return;
}
Field jlrField = findField(instance, fieldName);
Object[] original = (Object[]) jlrField.get(instance);
int finalLength = original.length - reduceSize;
if (finalLength <= 0) {
return;
}
Object[] combined = (Object[]) Array.newInstance(original.getClass().getComponentType(), finalLength);
System.arraycopy(original, reduceSize, combined, 0, finalLength);
jlrField.set(instance, combined);
}
}

文件工具

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
47
48
49
50
51
public class FileUtils {

public static boolean isFileExist(String name, File dirs) {

boolean isExist = false;
File[] currentFiles = dirs.listFiles();
if (currentFiles == null) {
return false;
}

for (int i = 0; i < currentFiles.length; i++) {
if (currentFiles[i].getName().contains(name)) {
isExist = true;
}
}
return isExist;
}

public static int copyFile(String fromFile, String toFile) {
try {
FileInputStream fileInput = new FileInputStream(fromFile);
FileOutputStream fileOutput = new FileOutputStream(toFile);
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
byte[] buffer = new byte[1024 * 1];
int len = -1;
while ((len = fileInput.read(buffer)) != -1) {
byteOut.write(buffer, 0, len);
}
// 从内存到写入到具体文件
fileOutput.write(byteOut.toByteArray());
// 关闭文件流
byteOut.close();
fileOutput.close();
fileInput.close();
return 0;
} catch (Exception ex) {
return -1;
}
}

public static void copyFile(InputStream inStream, FileOutputStream outStream) throws IOException {
//int bytesum = 0;
int byteread;
byte[] buffer = new byte[1444];
while ( (byteread = inStream.read(buffer)) != -1) {
//bytesum += byteread;
outStream.write(buffer, 0, byteread);
}
outStream.flush();
}
}

判断加载32位或64位库

Android 进程位数选择

一般来说,32位进程会默认加载32位的库,64位进程加载64位库。可以通过以下方法判断当前 app 进程需要加载的库:

  1. 通过反射获取 nativeLibraryDirectories 中路径(参考前面章节)判断。若路径包含 system/lib64、system_ext/lib64、vendor/lib64 则当前应用加载 64位 so。若路径包含 system/lib、system_ext/lib、vendor/lib 则当前应用加载 32位 so。
  2. 通过 ApplicationInfo.nativeLibraryDir 中的路径判断。若路径包含 xxxx/lib/arm64 则当前应用加载 64位 so。若路径包含 xxxx/lib/arm 则当前应用加载 32位 so。

平台限制

  • 从 Android 9(API 级别 28)开始,Android 平台对应用能使用的非 SDK 接口实施了限制。DexPathList 中的接口被列入灰名单(可以调用,运行时会有告警日志)。

参考文档

  1. Android 源码检索 (Android Code Search)
  2. Android 源码索引 (AndroidXRef)
  3. Android 针对非 SDK 接口的限制说明
  4. https://github.com/AnyMarvel/ManPinAPP