A background
With the increase of our business, the volume of our packages is getting bigger and bigger. However, so files occupy the most volume of packages. Normally, armeabi, Armeabi-V7A, ARM64-V8A and x86 will be entered into our app (of course, this is an emulator, so it is generally not entered). Some apps only tap into Armeabi or Armeabi-V7a to reduce package size, The Google Play Store has mandated support for 64-bit architecture since 2019 8.1, and 90% of phones on the market now have 64-bit processors (check out the percentages in your project). The normal practice is to pack two architectures for the client, which the system automatically selects, but this nearly doubles the package size. Enable 64-bit phones to download or update 64-bit packages, and only 32-bit phones to download or update 32-bit packages. However, the size problem of SO library cannot be completely solved, so we consider dynamic loading of SO to reduce the package volume.
The specific loading process of the system
- When installing the app, PMS will copy the so library of the specified schema to data/data/[package name]/lib
- When you start the app, you put the so folder in the system, As well as the installation package so the folder location to BaseDexClassLoader nativeLibraryDirectories and systemNativeLibraryDirectories DexPathList below attribute Two File collections
- Call and use calls: system.loadLibrary (” XXX “)
Take a look at Tinker’s so loading process
2.1 Loading so process below are the source code of Android29
So is typically loaded in two ways
- System.loadLibrary
Can only load data/data / / package/lib, or so some system (system/lib, the system/product/lib, different version, different producers, probably not), The System name can be obtained from System.getProperty(“java.library.path”). This is usually used to load the so library, without the lib and so suffixes
- System.load loads so in the absolute path. The filename argument must be a full pathname with the file so suffix
Public final class System {/ / load special folders so libraries, data/data / / package/lib, or so some System (System/lib, the System/product/lib is System prescribed some folders,, Public static void loadLibrary(String libname) {static void loadLibrary(String libname) { Runtime.getRuntime().loadLibrary0(Reflection.getCallerClass(), libname); } // Load any folder so, Public static void load(String filename) {public static void load(String filename) { Runtime.getRuntime().load0(Reflection.getCallerClass(), filename); }}Copy the code
LoadLibrary0 ()
public class Runtime {
private synchronized void loadLibrary0(ClassLoader loader, Class
callerClass, String libname) {
// Check name cannot have "/"
if (libname.indexOf((int)File.separatorChar) ! = -1) {
throw new UnsatisfiedLinkError(
"Directory separator should not appear in library name: " + libname);
}
String libraryName = libname;
BootClassLoader findLibrary is null, so handle it here
if(loader ! =null && !(loader instanceof BootClassLoader)) {
// Select BaseDexClassLoader from BaseDexClassLoader
String filename = loader.findLibrary(libraryName);
if (filename == null) {
// So throw exception not found
throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
System.mapLibraryName(libraryName) + "\" ");
}
// True load so Native method
String error = nativeLoad(filename, loader);
if(error ! =null) {
throw new UnsatisfiedLinkError(error);
}
return;
}
// If the loader is null, or BootClassLoader is BootClassLoader, check the so folder in the system
// Get the system so folder collection
getLibPaths();
// Change the name to the form lib[name].so
String filename = System.mapLibraryName(libraryName);
// Actually load
String error = nativeLoad(filename, loader, callerClass);
if(error ! =null) {
throw newUnsatisfiedLinkError(error); }}}Copy the code
System.loadlibrary () does not have lib and.so suffixes. Finally, the underlying nativeLoad will be called in two cases:
- When ClassLoader is not null and is not BootClassLoader, calling findLibrary directly from BaseDexClassLoader will return the full path of so and spell lib[name].so
- Otherwise, the system will find the folder collection, also will spell lib[name].so
BaseDexClassLoader findLibrary()
public class BaseDexClassLoader extends ClassLoader { private final DexPathList pathList; @Override public String findLibrary(String name) { return pathList.findLibrary(name); }}Copy the code
Here is very simple, direct call is DexPathList findLibrary method, let’s look at the source of DexPathList
final class DexPathList {
/** List of application native library directories. */
// So folder inside app
private final List<File> nativeLibraryDirectories;
/** List of system native library directories. */
// System so folder address
private final List<File> systemNativeLibraryDirectories;
// So, so, so, so
NativeLibraryElement[] nativeLibraryPathElements;
// constructor
public DexPathList(ClassLoader definingContext, ByteBuffer[] dexFiles) {
// Get the so folder set inside the app
this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
// Get the system so folder collection
this.systemNativeLibraryDirectories =
splitPaths(System.getProperty("java.library.path"), true);
// Merge app so folder set with system so folder
List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);
// Synthesize an array of NativeLibraryElement
this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories);
/ / traverse nativeLibraryPathElements, so the full path
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) {
returnpath; }}return null; }}Copy the code
Can be seen from above Through DexPathList nativeLibraryDirectories and systemNativeLibraryDirectories two folders in the collection, Generate a NativeLibraryElement[] and find the corresponding so in it to return the full path. From the previous hot repair technology can be associated, we can hook the API here to add our own defined folder
NativeLoad ()
The Runtime package name is java.lang, so we search for the Runtime_nativeLoad method in java_lang_runtime. cc directly. The implementation of this is basically the same as loading the Class, first check if it has been loaded, if it has been loaded, then compare whether the ClassLoader loaded is the same, if it is the same, then load it, if it has not been loaded, then load it because I can’t read the C code, The java_lang_Runtime file has not been updated since API23. Interested in Android hotfix Tinker(v) SO patch loading and Dalvik VIRTUAL machine JNI method registration process analysis
Refer to Tinker’s implementation method, Demo andThe demo address
Tinker is the hook of the nativeLibraryDirectories in DexPathList, add a folder we define in this folder set. For example, the following code, depending on the version of the corresponding processing logic. TinkerLoadLibrary. InstallNativeLibraryPath (loader, custom folders), can realize dynamic loading so library, the following is an excerpt from TinkerLoadLibrary dealt with a little bit about the Tinker, You can do it without 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); }}}Copy the code
3.2 Test dynamic loading of SO with baidu positioning SDK
- We copied so into Assets and downloaded it from the Internet at that time
- In build.gradle, we exclude the so library when we need to package
- Then copy so in Application to an internal folder. Then use Tinker’s TinkerLoadLibrary to load the folder
specificThe demo address
reference
Dalvik VIRTUAL machine JNI method registration process analysis dynamic delivery SO library in the Android APK installation package thin application