The introduction

In the last stage, we started to enter the field of plug-in/thermal repair, and understood the past and present life of thermal repair. Now we will learn the ClassLoader scheme design in thermal repair.

The ClassLoader is mainly used to load plug-ins. Before starting the plug-in, the plug-in should be loaded in the first place. Let’s analyze different solutions to understand the different loading posture ~

Solution 1: Combine Dex (Hook mode)

Who used this scheme?

QQ team space skin function

The principle of

Merge our plugin dex and the host apK’s class.dex into the host dexElements array. The App is loaded from this array every time it starts.

Actual combat process

1) Get host, dexElements

2) Get the plug-in, dexElements

3) Merge two dexElements

4) Assign the new dexElements to the host dexElements

code

Class<? > clazz = Class.forName("dalvik.system.BaseDexClassLoader"); Field pathListField = clazz.getDeclaredField("pathList"); pathListField.setAccessible(true); Class<? > dexPathListClass = Class.forName("dalvik.system.DexPathList"); Field dexElementsField = dexPathListClass.getDeclaredField("dexElements"); dexElementsField.setAccessible(true); // Host ClassLoader ClassLoader pathClassLoader = context.getClassLoader(); HostPathList = pathListField.get(pathClassLoader); hostPathList = pathListField.get(pathClassLoader); // hostDexElements Object[] hostDexElements = (Object[]) dexelementsfield. get(hostPathList); DexClassLoader = new dexClassLoader (apkPath, context.getcacheDir ().getabsolutePath (),null, pathClassLoader); PluginPathList = pathListField.get(dexClassLoader); // pluginDexElements = (Object[]) dexElementsField. Get (pluginPathList); Create a new array Object[] newDexElements = (Object[]) Array.newInstance(hostDexElements.getClass().getComponentType(),hostDexElements.length + pluginDexElements.length); Arraycopy (hostDexElements, 0, newDexElements,0, hostDexElements. Length); System.arraycopy(pluginDexElements, 0,newDexElements,hostDexElements.length, pluginDexElements.length); // Assign dexelementsfield. set(hostPathList, newDexElements);Copy the code

The characteristics of

This is a single ClassLoader solution. All the classes of the plug-in and the host program are loaded by the host ClasLoader. The problem is that “if the class libraries used between plug-ins or between plug-ins and the host are the same, the loading will be out of order.”

Option 2: Replace the parent of the PathClassloader

Who used this scheme?

Micro shops, Instant – Run

Knowledge base

The ClassLoader link relationship of the APK (host) installed in the mobile phone

1) Code:

ClassLoader classLoader = getClassLoader();
ClassLoader parentClassLoader = classLoader.getParent();
ClassLoader pParentClassLoader = parentClassLoader.getParent();
Copy the code

2) Relationship:

= = this = = : dalvik. System. PathClassLoader

= = parentClassLoader = = : Java. Lang. BootClassLoader

= = pParentClassLoader = = : null

The current classLoader is PathClassLoader, the parent’s classLoader is BootClassLoader, and the BootClassLoader does not have the parent’s classLoader

Implement ideas

How to use the above host link basic design principles?

The ClassLoader constructor has an argument called parent; If we replace the parent of PathClassLoader with our plugin’s classLoader==; Set the parent== of the plugin’s classLoader to BootClassLoader; Add the parent delegate mechanism and the process of finding the plug-in class becomes: BootClassLoader->== plug-in classLoader==->PathClassLoader

Code implementation

public static void loadApk(Context context, String apkPath) { File dexFile = context.getDir("dex", Context.MODE_PRIVATE); File apkFile = new File(apkPath); // find PathClassLoader ClassLoader ClassLoader = context.getClassLoader(); // Build the plugin's ClassLoader //PathClassLoader's father passed the plugin's ClassLoader // here, in the order: BootClassLoader-> plug-in classLoader DexClassLoader DexClassLoader = new DexClassLoader(apkFile.getAbsolutePath(),dexFile.getAbsolutePath(), null,classLoader.getParent()); Try {// The father of the PathClassLoader is set to the plugin's ClassLoader // in the following order: BootClassLoader - > plugins this - > PathClassLoader Field fieldClassLoader. = this class. The getDeclaredField (" parent "); if (fieldClassLoader ! = null) { fieldClassLoader.setAccessible(true); fieldClassLoader.set(classLoader, dexClassLoader); } } catch (Exception e) { e.printStackTrace(); }}Copy the code

The characteristics of

This is a single ClassLoader solution. All the classes of the plug-in and the host program are loaded by the host ClasLoader. The problem is that “if the class libraries used between plug-ins or between plug-ins and the host are the same, the loading will be out of order.”

Scheme 3: Use the caching mechanism of LoadedApk

Who used this scheme?

360的DroidPlugin

Realize the principle of

java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);
Copy the code

The code above does two things:

1) system with a packageInfo. GetClassLoader () to load the installed app Activity

2) Instantiated activities

PackageInfo is LoadedApk, which is the representation of APK files in memory. Information related to APK files, such as the code and resources of APK files, and even information about activities, services and other components in the code can be obtained through this object.

How is packageInfo generated? By reading the source code:

1) Obtain it from the mPackages cache of ActivityThread (Map, key, LoadedApk)

2) If the cache does not have one, new LoadedApk generates one and places it in the cache mPackages

Based on the principle of the above system, the key steps are as follows:

1) Build the plugin ApplicationInfo information

ApplicationInfo applicationInfo = (ApplicationInfo) generateApplicationInfoMethod.invoke(packageParser,packageObj, 0, defaultPackageUserState);
String apkPath = apkFile.getPath();
applicationInfo.sourceDir = apkPath;
applicationInfo.publicSourceDir = apkPath;
Copy the code

2) Build CompatibilityInfo

Class<? > compatibilityInfoClass = Class.forName("android.content.res.CompatibilityInfo"); Field defaultCompatibilityInfoField = compatibilityInfoClass.getDeclaredField("DEFAULT_COMPATIBILITY_INFO"); defaultCompatibilityInfoField.setAccessible(true); Object defaultCompatibilityInfo = defaultCompatibilityInfoField.get(null);Copy the code

3) Build loadedApk for the plug-in according to ApplicationInfo and CompatibilityInfo

Class<? > activityThreadClass = Class.forName("android.app.ActivityThread"); Method getPackageInfoNoCheckMethod = activityThreadClass.getDeclaredMethod("getPackageInfoNoCheck", ApplicationInfo.class, compatibilityInfoClass); Object loadedApk = getPackageInfoNoCheckMethod.invoke(currentActivityThread, applicationInfo, defaultCompatibilityInfo);Copy the code

4) Build the plugin’s ClassLoader and replace it with loadedApk’s ClassLoader

String odexPath = Utils.getPluginOptDexDir(applicationInfo.packageName).getPath();
String libDir = Utils.getPluginLibDir(applicationInfo.packageName).getPath();
ClassLoader classLoader = new DexClassLoader(apkFile.getPath(), odexPath, libDir, ClassLoader.getSystemClassLoader());
Field mClassLoaderField = loadedApk.getClass().getDeclaredField("mClassLoader");
mClassLoaderField.setAccessible(true);
mClassLoaderField.set(loadedApk, classLoader);
Copy the code

5) Add loadedApk to ActivityThread’s mPackages

// Get the current ActivityThread Class<? > activityThreadClass = Class.forName("android.app.ActivityThread"); Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread"); currentActivityThreadMethod.setAccessible(true); Object currentActivityThread = currentActivityThreadMethod.invoke(null); / / get mPackages this static member variables, here the cache dex package information Field mPackagesField = activityThreadClass. GetDeclaredField (" mPackages "); mPackagesField.setAccessible(true); Map mPackages = (Map) mPackagesField.get(currentActivityThread); // Since it is a weak reference, we must save a copy somewhere, otherwise it will be easily GC; Disqualified. SLoadedApk. Put (applicationInfo packageName, loadedApk); WeakReference weakReference = new WeakReference(loadedApk); mPackages.put(applicationInfo.packageName, weakReference);Copy the code

6) Bypass system checks and make the system feel that the plug-in is already installed on the system

Private static void hookPackageManager () throws the Exception {/ / this step because initializeJavaContextClassLoader This method inadvertently checks if the package is installed on the system // if it is not installed, it will throw an exception, so you need to Hook the PMS temporarily to bypass this check. Class<? > activityThreadClass = Class.forName("android.app.ActivityThread"); Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread"); currentActivityThreadMethod.setAccessible(true); Object currentActivityThread = currentActivityThreadMethod.invoke(null); // Get the original sPackageManagerField sPackageManagerField = in ActivityThread activityThreadClass.getDeclaredField("sPackageManager"); sPackageManagerField.setAccessible(true); Object sPackageManager = sPackageManagerField.get(currentActivityThread); // Prepare the proxy object to replace the original Class<? > iPackageManagerInterface = Class.forName("android.content.pm.IPackageManager"); Object proxy = Proxy.newProxyInstance(iPackageManagerInterface.getClassLoader(), new Class<? >[] { iPackageManagerInterface }, new IPackageManagerHookHandler(sPackageManager)); SPackageManager sPackageManagerField. Set (currentActivityThread, proxy); // 1. Replace sPackageManager sPackageManagerField. }Copy the code

The characteristics of

1) Custom plug-in ClassLoader, and bypass the Framework detection

2) Hook place is also a bit much: not only need Hook AMS and H, but also need Hook ActivityThread mPackages and PackageManager!

3) Multi-classloader architecture, each plug-in has its own ClassLoader, good isolation, if different plug-ins use different versions of the same library, they are safe

4) Actually hot loading code!

The plugin needs to be upgraded by creating a custom ClassLoader to load the new plugin and then replacing the original version.

The implementation of a single ClassLoader is very troublesome and may need to restart the process.

Scheme 4: Customize the ClassLoader logic

Who with?

Tencent Video and other business groups in the Shadow hot repair framework

Realize the principle of

1) Learn about the ClassLoader link of the host (App already installed) : BootClassLoader -> PathClassLoader

2) The plugin can load the host class implementation:

Build the plugin’s ClassLoader, named ApkClassLoader, where the parent loader passes the host ClassLoader, the code snippet is:

class ApkClassLoader extends DexClassLoader { static final String TAG = "daviAndroid"; private ClassLoader mGrandParent; private final String[] mInterfacePackageNames; @Deprecated ApkClassLoader(InstalledApk installedApk, ClassLoader parent,////parent = host ClassLoader String[] mInterfacePackageNames, int grandTimes) { super(installedApk.apkFilePath, installedApk.oDexPath, installedApk.libraryPath, parent);Copy the code

BootClassLoader -> PathClassLoader -> ApkClassLoader (parent delegate)

3) The plugin does not need to load the host class implementation:

class ApkClassLoader extends DexClassLoader { ............ //1) Class<? > clazz = findLoadedClass(className); If (clazz == null) {//2) find clazz = findClass(className); If (clazz == null) {//3) find from parent's parent (BootClassLoader) ClassLoader. clazz = mGrandParent.loadClass(className); }}... }Copy the code

This logic plug-ins do not need to load the host class, so will not to load the host load logic class (that is, through PathClassLoader), in this case, even if plug-in and the host USES the same class, so the plugin loading time won’t because the consignment loading mechanism and the load of the host, causes the plug-in load wrong;

Code implementation

class ApkClassLoader extends DexClassLoader { private ClassLoader mGrandParent; private final String[] mInterfacePackageNames; ApkClassLoader(InstalledApk installedApk, ClassLoader parent, String[] mInterfacePackageNames, int grandTimes) { super(installedApk.apkFilePath, installedApk.oDexPath, installedApk.libraryPath, parent); ClassLoader grand = parent; for (int i = 0; i < grandTimes; i++) { grand = grand.getParent(); } mGrandParent = grand; this.mInterfacePackageNames = mInterfacePackageNames; } @Override protected Class<? > loadClass(String className, boolean resolve) throws ClassNotFoundException { String packageName; int dot = className.lastIndexOf('.'); if (dot ! = -1) { packageName = className.substring(0, dot); } else { packageName = ""; } boolean isInterface = false; for (String interfacePackageName : mInterfacePackageNames) { if (packageName.equals(interfacePackageName)) { isInterface = true; break; } } if (isInterface) { return super.loadClass(className, resolve); } else { Class<? > clazz = findLoadedClass(className); if (clazz == null) { ClassNotFoundException suppressed = null; try { clazz = findClass(className); } catch (ClassNotFoundException e) { suppressed = e; } if (clazz == null) { try { clazz = mGrandParent.loadClass(className); } catch (ClassNotFoundException e) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { e.addSuppressed(suppressed); } throw e; } } } return clazz; }} /** * read the implementation of the interface from apk ** @param clazz interface class * @param className className of the implementation class * @param <T> interface type * @return required interface * @throws Exception */ <T> T getInterface(Class<T> clazz, String className) throws Exception { try { Class<? > interfaceImplementClass = loadClass(className); Object interfaceImplement = interfaceImplementClass.newInstance(); return clazz.cast(interfaceImplement); } catch (ClassNotFoundException | InstantiationException | ClassCastException | IllegalAccessException e) { throw new Exception(e); }}}Copy the code

This code implements abnormal parent-delegate logic, both to isolate class loads (and hosts) from parent and to reuse some of the host’s classes via whitelist

The characteristics of

1) Belongs to multi-classloader scheme

2) Plugins can choose to load host classes and bypass host loading

At the end

Haha, that’s all for this article (systematic learning and growing together)