Introduction to the
For App, the so-called plug-in, personal understanding is a complete App host and plug-in split into two parts, we can App runs on the host dynamic load or replace part of the plug-in, plug-in is not only to the expansion of the host function but also can reduce the burden of the host, the host is to run the App, Plug-ins are apK files loaded by the host runtime, so the host-plug-in solution technology is probably pluginization.
Why plug-in?
- Decoupled, independent business modules become plug-ins, do not interfere with each other, plug and use, convenient development and maintenance. When the business is huge and tedious, whether there is a feeling of affecting the whole body, whether there is too complicated logic, high coupling degree, difficult to control the whole project.
- Speed up compilation. After each change, there is no need to re-edit the entire project, and a plug-in project can be compiled separately. For large projects, speed is the supreme martial power.
- Dynamic update. There is no need to re-download and install the APP. You can download a plug-in APK separately and load it directly. It feels like a good choice in terms of dynamic update, package volume and traffic.
- Module customization. Download whatever modules you need, you don’t need to make your app big, you need to get what you need.
The principle of plug-in is briefly described
The main solution of plug-in is probably the core problems of class loading, resource loading and component loading. The so-called principle is also discussed around these problems.
Android class loading
The ClassLoader of android class loading system can be roughly divided into BaseDexClassLoader and SecureClassLoader. As a plug-in, we only briefly analyze PathClassLoader and DexClassLoader, after all, the content of class loading is also a lot of, to write a lot of things 😝, first look at the Android class loading inheritance diagram:
- The PathClassLoader provides a simple classloader implementation that operates on the list of files and directories in the local file system, but does not attempt to load classes from the network. Android uses this class for its system class loader and application class loader (which simply loads installed APK). Here’s the official description:
Provides a simple ClassLoader implementation that operates on a list of files and directories in the local file system, but does not attempt to load classes from the network. Android uses this class for its system class loader and for its application class loader(s).
- DexClassLoader it loads classes containing the.dex entries from.jar and.apk files. This can be used to execute code that is not installed as part of the application (simply load uninstalled APK, which may be used for hot fixes and dynamic updates). Prior to API level 26, the class loader required an application-specific writable directory to cache the optimized classes. Use context.getCodecachedir () to create a directory that looks like this:
A class loader that loads classes from .jar and .apk files containing a classes.dex entry. This can be used to execute code not installed as part of an application. Prior to API level 26, this class loader requires an application-private, writable directory to cache optimized classes. Use Context.getCodeCacheDir() to create such a directory:
The above about android class loading is just an understatement, said for a long time, about the plug-in of course using DexClassLoader, let’s look at the implementation of DexClassLoader.
public class DexClassLoader extends BaseDexClassLoader { public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) { super(dexPath, new File(optimizedDirectory), librarySearchPath, parent); }}Copy the code
DexPath: string variable containing a list of JAR/APK files containing classes and resources, separated by file. pathSeparator, which defaults to “:” on Android. OptimizedDirectory (context.getCodecachedir) : optimizedDirectory (context.getCodecachedir) : optimizedDirectory (context.getCodecachedir) : optimizedDirectory (context.getCodecachedir) : optimizedDirectory (context.getCodecachedir) : optimizedDirectory (context.getCodecachedir) LibrarySearchPath: directory list containing native libraries, C/C++ store path, separated by File. PathSeparator; May be null. Parent: indicates the parent ClassLoader.
Look again at the constructor and core method of the BaseDexClassLoader, the parent class called
public class BaseDexClassLoader extends ClassLoader { private final DexPathList pathList; public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) { super(parent); this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory); } @Override protected Class<? > findClass(String name) throws ClassNotFoundException { List<Throwable> suppressedExceptions = new ArrayList<Throwable>(); Class c = pathList.findClass(name, suppressedExceptions);if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
@Override
protected URL findResource(String name) {
return pathList.findResource(name);
}
@Override
protected Enumeration<URL> findResources(String name) {
return pathList.findResources(name);
}
@Override
public String findLibrary(String name) {
returnpathList.findLibrary(name); }}Copy the code
When initializing the constructor, instantiate the DexPathList object. At the same time, BaseDexClassLoader overrides the parent findClass() method. When using the findClass() method to perform class lookup, The DexPathList findClass() method is delegated to the pathList object for the appropriate class lookup.
final class DexPathList { private Element[] dexElements; DexPathList(ClassLoader definingContext, String dexPath, String librarySearchPath, File optimizedDirectory, boolean isTrusted) { ... this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions, definingContext, isTrusted); . } private static Element[] makeDexElements(List<File> files, File optimizedDirectory, List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) { Element[] elements = new Element[files.size()]; int elementsPos = 0;for (File file : files) {
if (file.isDirectory()) {
elements[elementsPos++] = new Element(file);
} else if (file.isFile()) {
String name = file.getName();
DexFile dex = null;
if (name.endsWith(DEX_SUFFIX)) {
// Raw dex file (not inside a zip/jar).
try {
dex = loadDexFile(file, optimizedDirectory, loader, elements);
if(dex ! = null) { elements[elementsPos++] = new Element(dex, null); } } catch (IOException suppressed) { System.logE("Unable to load dex file: "+ file, suppressed); suppressedExceptions.add(suppressed); }}else {
try {
dex = loadDexFile(file, optimizedDirectory, loader, elements);
} catch (IOException suppressed) {
suppressedExceptions.add(suppressed);
}
if (dex == null) {
elements[elementsPos++] = new Element(file);
} else{ elements[elementsPos++] = new Element(dex, file); }}}else {
System.logW("ClassLoader referenced unknown path: "+ file); }}return elements;
}
public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if(dex ! = null) { Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);if(clazz ! = null) {returnclazz; }}}if(dexElementsSuppressedExceptions ! = null) { suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions)); }returnnull; }}Copy the code
When the DexPathList constructor is called, the dexPath is traversed through the makeDexElements method, loading each dex file in turn and storing it in the Element[] array. When the DexPathList findClass is called, By iterating through the dex file of Element[], the class is loaded through the loadClassBinaryName() of the DexFile class. If it is not empty, the class is successfully loaded and returns class; otherwise, null is returned. Let’s see how the base class ClassLoader is implemented
public abstract class ClassLoader { private final ClassLoader parent; protected Class<? > loadClass(String name, boolean resolve) throws ClassNotFoundException{ Class c = findLoadedClass(name);if (c == null) {
try {
if(parent ! = null) { c = parent.loadClass(name,false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if(c == null) { c = findClass(name); }}returnc; }}Copy the code
This is obviously a parent delegate model, and when a class is loaded, it looks first to see if the class has been loaded before, and if it has been loaded, it returns, otherwise it delegates to the parent class loader, and if the parent class loader can’t find it, it goes to the BootstrapClass, and it doesn’t find it, Well, then I’ll go out and find it myself. This avoids reloading and is more secure. Ok, summarize the loading process of DexClassLoader: loadClass->findClass->BaseDexClassLoader.findClass->DexPathList.findClass->loadDexFile->DexFile.loadClassBinaryName->Dex File.defineclass, that’s basically how it goes.
Resource to load
The Android system loads resources through the Resource object. Therefore, you only need to add the path of resources (that is, apK files) to the AssetManager to access the plug-in resources.
/**
* Create a new AssetManager containing only the basic system assets.
* Applications will not generally use this method, instead retrieving the
* appropriate asset manager with {@link Resources#getAssets}. Not for
* use by applications.
* @hide
*/
public AssetManager() {
final ApkAssets[] assets;
synchronized (sSync) {
createSystemAssetsInZygoteLocked();
assets = sSystemApkAssets;
}
mObject = nativeCreate();
if (DEBUG_REFS) {
mNumRefs = 0;
incRefsLocked(hashCode());
}
// Always set the framework resources.
setApkAssets(assets, false /*invalidateCaches*/);
}
Copy the code
The AssetManager constructor is @hide API, so it can’t be used directly. Android P does not limit the system’s hidden API. But I would say that most of the major plugins are already available on Android9.0, so don’t worry. The following is a brief post about the loading process of Android resources. For plug-in resource loading, please refer to didi VirtualApk resource loading idea (portal).
class ContextImpl extends Context { //... private ContextImpl(ContextImpl container, ActivityThread mainThread, LoadedApk packageInfo, IBinder activityToken, UserHandle user, boolean restricted, Display display, Configuration overrideConfiguration) { //.... Resources resources = packageInfo.getResources(mainThread); / /... } / /... }Copy the code
Instead of focusing on how packageInfo is generated, we can trace it directly to the following.
public final class LoadedApk { private final String mResDir; public LoadedApk(ActivityThread activityThread, ApplicationInfo aInfo, CompatibilityInfo compatInfo, ClassLoader baseLoader, boolean securityViolation, boolean includeCode, boolean registerPackage) { final int myUid = Process.myUid(); aInfo = adjustNativeLibraryPaths(aInfo); mActivityThread = activityThread; mApplicationInfo = aInfo; mPackageName = aInfo.packageName; mAppDir = aInfo.sourceDir; mResDir = aInfo.uid == myUid ? aInfo.sourceDir : aInfo.publicSourceDir; // Notice the sourceDir. This is the path of our host's APK package in the phone. The host's resources are loaded from this address. // The generation of this value involves PMS, so it is not analyzed for now. // Full path to the base APK for this application. //.... } / /... public Resources getResources(ActivityThread mainThread) { if (mResources == null) { mResources = mainThread.getTopLevelResources(mResDir, mSplitResDirs, mOverlayDirs, mApplicationInfo.sharedLibraryFiles, Display.DEFAULT_DISPLAY, null, this); } return mResources; } / /... }Copy the code
Into the ActivityThread. GetTopLevelResources () logic
public final class ActivityThread { Resources getTopLevelResources(String resDir, CompatibilityInfo compInfo) {AssetManager assets = new AssetManager(); If (assets. AddAssetPath (resDir) == 0) { Return null; The Resources object can be searched by AssetManager. } // Create the Resources object, which relies on the AssetManager class for resource lookup. r = new Resources(assets, metrics, getConfiguration(), compInfo); }}Copy the code
From the above code we know how our common Resources are generated, so in theory the plug-in can generate a Resources object for its own use in this way.
Component loading
Android has four components: Activity, Service, ContentProvider and BoradCastRecevier. The properties and life cycle of each component are also different. So with respect to the components loaded in the plug-in, you need to examine how each component is loaded separately.
Simply take the Activity component as an example, now some mainstream methods are basically through the idea of “pit”, the word is also said to have originated from 360. In general, the pit is occupied first, because the Manifest of our host app does not apply for the Activity in the plug-in, so I will occupy a pit first to cheat the system. Then replace it with the Activity in the plug-in. Multiple pits may be required because some resource properties can be configured dynamically. Examples include launchMode, Process, configChanges, Theme, and so on. Here we also need to understand the Activity startup process, here we can take a brief look.
@Override
public void startActivity(Intent intent, @Nullable Bundle options) {
if(options ! = null) { startActivityForResult(intent, -1, options); }else {
// Note we want to go through this call forcompatibility with // applications that may have overridden the method. startActivityForResult(intent, -1); }}Copy the code
As you can see, we usually start startActivity by calling startActivityForResult (), so let’s move on
public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
@Nullable Bundle options) {
if (mParent == null) {
options = transferSpringboardActivityOptions(options);
Instrumentation.ActivityResult ar =
mInstrumentation.execStartActivity(
this, mMainThread.getApplicationThread(), mToken, this,
intent, requestCode, options);
if(ar ! = null) { mMainThread.sendActivityResult( mToken, mEmbeddedID, requestCode, ar.getResultCode(), ar.getResultData()); }if (requestCode >= 0) {
mStartedActivity = true;
}
cancelInputsAndStartExitTransition(options);
// TODO Consider clearing/flushing other event sources and events for child windows.
} else {
if(options ! = null) { mParent.startActivityFromChild(this, intent, requestCode, options); }else {
// Note we want to go through this method forcompatibility with // existing applications that may have overridden it. mParent.startActivityFromChild(this, intent, requestCode); }}}Copy the code
We can see that the system’s Instrumentation class execStartActivity () is used to start the Activity. We can continue to see the following method:
public ActivityResult execStartActivity (,,,,,,, try {intent. MigrateExtraStreamToClipData (); intent.prepareToLeaveProcess(who); int result = ActivityManager.getService() .startActivity(whoThread, who.getBasePackageName(), intent, intent.resolveTypeIfNeeded(who.getContentResolver()), token, target ! = null ? target.mEmbeddedID : null, requestCode, 0, null, options); checkStartActivityResult(result, intent); } catch (RemoteException e) { throw new RuntimeException("Failure from system", e);
}
return null;
}
/**
* @hide
*/
public static IActivityManager getService() {
return IActivityManagerSingleton.get();
}
private static final Singleton<IActivityManager> IActivityManagerSingleton =
new Singleton<IActivityManager>() {
@Override
protected IActivityManager create() {
final IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE);
final IActivityManager am = IActivityManager.Stub.asInterface(b);
returnam; }};Copy the code
ActivityManager. GetService () to get the IActivityManager object, and then to call startActivity (), and IActivityManager just an abstract interface, the following see the implementation class
public abstract class ActivityManagerNative extends Binder implements IActivityManager
public final class ActivityManagerService extends ActivityManagerNative
implements Watchdog.Monitor, BatteryStatsImpl.BatteryCallback
class ActivityManagerProxy implements IActivityManager
Copy the code
You can see the two implementation classes ActivityManagerProxy and ActivityManagerService (AMP and AMS for short). AMP is only the local proxy object of AMS. Its startActivity method calls AMS’s startActivity method. Also note that the startActivity method passes the ApplicationThread object to the AMS process. AMS actually gets the ApplicationThread’s proxy object, ApplicationThreadProxy. AMS communicates with our App process through this proxy object. Since the verification of the existence of the Activity occurs at the AMS end, we should replace the Activity ComponentName with the name of the pit in advance before interacting with AMS. Select Hook Instrumentation or ActivityManagerProxy should be possible, then the Activity after the complex startup process will eventually execute the Instrumentation newActivity (), Here we can restore the Activity as a plug-in.
public Activity newActivity(Class<? > clazz, Context context, IBinder token, Application application, Intent intent, ActivityInfo info, CharSequence title, Activity parent, String id, Object lastNonConfigurationInstance) throws InstantiationException, IllegalAccessException { Activity activity = (Activity)clazz.newInstance(); ActivityThread aThread = null; // Activity.attach expects a non-null Application Object.if (application == null) {
application = new Application();
}
activity.attach(context, aThread, this, token, 0 /* ident */, application, intent,
info, title, parent, id,
(Activity.NonConfigurationInstances)lastNonConfigurationInstance,
new Configuration(), null /* referrer */, null /* voiceInteractor */,
null /* window */, null /* activityConfigCallback */);
return activity;
}
Copy the code
As for the loading principle of the four components of plug-in is too complicated, I only briefly describe the idea of plug-in. If you want to see the specific thought process, you can also check the component loading principle of Didi VirtualApk. The idea of plug-in has something in common (portal).
About the selection of plug-in scheme
If you are doing plug-in, or want to study plug-in, it doesn’t matter if you don’t understand the above, anyway, there are a lot of mature schemes in the market, here are a few better schemes selected from the thousands of schemes, so as to avoid more detdetments, after all, I also walked from the vast plug-in scheme.
- VirtualApk didi plug-in solution, very powerful, and strong compatibility, has been adapted to Android 9.0, if the project plug-in and host dependency is a good choice.
- DroidPlugin 360 is a plug-in solution, the biggest feature is plug-in independence, do not depend on the host, of course, there is no coupling
- RePlugin 360 is another plug-in solution, which represents two different directions from DroidPlugin. Each function module can be upgraded independently, but also requires some interaction and coupling between the host and the plug-in.
- Shadow Tencent has recently opened source plug-in scheme, the biggest feature is zero reflection, and the core library is implemented by Kotlin. Personally, I think it will be a good choice in the future, but because it has just opened source, it has not been tested by the public.
- VirtualApp is a sandbox product running on the Android system, which can be understood as a lightweight “Android virtual machine”, very cool, Widely used in plug-in development, no sense of hot update, cloud control automation, rolled, rent, mobile game, mobile game handle free activation, block chain, mobile office security, military government secrecy, mobile phone analog information, automation, automated test script, such as technology, the biggest characteristic app double opening and rolled, sandbox ability, internal and external isolation. But 2017 has been commercialized.
Didi plug-in taste fresh
How VirtualAPK works
VirtualAPK has no additional constraints on plug-ins, and native APK can be used as plug-ins. After the plug-in project is compiled and generated, the APK can be loaded through the host App. After each plug-in APK is loaded, a separate LoadedPlugin object will be created in the host. With these LoadedPlugin objects, as shown below, VirtualAPK can manage plug-ins and give them new meaning, making them behave like apps installed on your phone.
How to use
Step 1: Add build. Gradle to host Project
dependencies {
classpath 'com. Didi. Virtualapk: gradle: 0.9.8.6'
}
Copy the code
Step 2: Add build. Gradle to the host Moudle
apply plugin: 'com.didi.virtualapk.host'
implementation 'com. Didi. Virtualapk: core: 0.9.8'
Copy the code
Step 3: Add an initialization to the host app’s Applicaiton:
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
PluginManager.getInstance(base).init();
}
Copy the code
Step 4: Add confusion:
-keep class com.didi.virtualapk.internal.VAInstrumentation { *; }
-keep class com.didi.virtualapk.internal.PluginContentResolver { *; }
-dontwarn com.didi.virtualapk.**
-dontwarn android.**
-keep class android.** { *; }
Copy the code
Step 5: Use of hosts:
String pluginPath = Environment.getExternalStorageDirectory().getAbsolutePath().concat("/Test.apk");
File plugin = new File(pluginPath);
PluginManager.getInstance(base).loadPlugin(plugin);
// Given "com.didi.virtualapk.demo" is the package name of plugin APK,
// and there is an activity called `MainActivity`.
Intent intent = new Intent();
intent.setClassName("com.didi.virtualapk.demo"."com.didi.virtualapk.demo.MainActivity");
startActivity(intent);
Copy the code
Step 6: Build. Gradle configuration for the plugin’s Project:
dependencies {
classpath 'com. Didi. Virtualapk: gradle: 0.9.8.6'
}
Copy the code
Step 7: Build. Gradle for your plugin app:
apply plugin: 'com.didi.virtualapk.plugin'
virtualApk {
packageId = 0x6f // The package id of Resources.
targetHost='source/host/app' // The path of application module in host project.
applyHostMapping = true // [Optional] Default value is true.
}
Copy the code
Step 8: Run commands about compilation
Gradlew Clean assembleRelease plugin: Gradlew Clean assemblePluginCopy the code
The principle of
- Classloaders that merge hosts and plug-ins need to be careful that the classes in the plug-in do not duplicate the classes in the host
- Merge plug-in and host resources Resets the packageId of the plug-in resource to merge the plug-in and host resources
- The Gradle plugin removes references to the host’s code and resources during build time
The realization principle of the four components
- The Activity exploits the host manifest to bypass system validation and then loads the actual Activity.
- AMS intercepts Service requests and forwards them to The Service Runtime, which takes over all operations of the system.
- Receiver re-registers the statically registered Receiver in the plug-in.
- ContentProvider The dynamic agent IContentProvider intercepts provider-related requests and forwards them to the Provider Runtime, which takes over all operations of the system.
The following is the overall architecture of VirtualAPK, please read the source code for more details.
Problems encountered by even compiler running
- Plugins and hosts can be in or out of the same project, they are associated with targetHost, so they are very flexible and don’t have to worry about structure (normally plugins and hosts are different projects)
- Incompatible with Ali’s hotfix framework, probably related to initialization (temporarily removed)
- Incompatible with JobIntentService No such Service componentInfo
- Host jump plugin, discover the resource interface is still the host (resource ID cannot be the same as the host resource name)
- Plugin hosts need to rely on all com.android.support packages (plug-ins and hosts need to rely on com.android.support packages at the same version)
- IllegalStateException: You need to use a Theme.AppCompat Theme, please use (gradlew clean assemblePlugin)
- Found that the theme of the plug-in is not in effect (make sure that the host and plug-in use the same theme)
- The directory of host application doesn’t exist! (targetHost route force configuration error)
- When Tencent X5 browser fails to load, the default is the WebView of the system (check the so file to ensure that the CPU core of the host and plug-in is consistent).
The last
Pluggable simple research on android is sauce like probably, for the first time to try still feeling pretty good, but the biggest upset should be business how to break up the plugin, basic components, how to split from complex business thorns fight our way out, want to “all flowers, leaf don’t stick”, SAO years, I believe you can.