background

History and Present Situation

The development history

  • In 2012, tu yimin of dianping launched AndroidDynamicLoader framework, which can be regarded as the first plug-in framework.
  • At the beginning of 2014, an Ali employee made a technical sharing, specifically talking about Taobao’s Altas technology and the general direction of this technology. But many of the technical details are not shared or open source.
  • At the end of 2014, Ren Yugang released an Android plug-in project named Dynamic-load-APk. It did not Hook too many low-level methods of the system, but solved the problem from the upper layer, namely the App application layer, and created a ProxyActivity class inherited from Activity. Then make all activities in the plug-in inherit from ProxyActivity and rewrite all Activity methods.
  • In August 2015, Zhang Yong released DroidPlugin, which has many hooks and can load any App into the host. It is not maintained at present, but many ideas are worth learning from.
  • Atlas went open source in March 2017.
  • In June 2017, VirtualAPK is didi’s open source plug-in framework that supports almost all Android features, four major components, etc.
  • In 2018, Google announced the Android App Bundle at IO conference, which is an official plug-in solution that relies on The GooglePlay Service. At the same time, it began to add system-level support for Android P and above. However, since there is no PlayService in China, we need to make magic changes based on it.
  • In June 2019, Qigsaw was officially open source, based on Android App Bundle. At the same time, its API is compatible with the official, that is, it can adopt plug-in mode in China and GooglePlay channel in overseas, which is a very good idea.

The status quo

  • Each plug-in framework is developed based on the business of its own App, with more or less different goals, so it is difficult to have a plug-in framework that can solve all problems in a unified way.
  • Android each version upgrade will bring a lot of impact to each plug-in framework, all have to work hard to adapt some, not to mention the domestic manufacturers on the ROM customization, as VirtualAPK author Ren Yugang said: It’s not that hard to Demo a plug-in framework, but it’s not that easy to develop a complete plug-in framework.
  • In 2020, Android plug-in is still a high-risk technology, involving various Android SDK versions and OEM compatibility issues.

General classification

Free installation type

  • The host and plug-in compile separately.
  • Load an independent APK that has no business relationship with the main APP to realize the function that can be used without installation.
  • VirtualApk
  • DroidPlugin

Since the decoupling model

  • The host and plug-in compile together.
  • They tend to separate “components” from the main app into “plug-ins” on the basis of “componentization”.
  • Atlas
  • Qigsaw

Plug-in concerns

  • Load & manage plug-ins
  • ClassLoader
  • Resources
  • Four major components
  • Compile the package
  • Host & plugin upgrades

VirtualApk source

VirtualApk as a pioneering plug-in framework, the source code is very reference. Below from the perspective of VirtualApk source code, from all aspects to discuss how to implement a plug-in framework.

Load & manage plug-ins

  • PluginManager: Responsible for loading and managing plug-ins and storing some global information
  • LoadedPlugin: After the plug-in is loaded, all information about the plug-in is saved

Loading process

PluginManager

public void loadPlugin(File apk) throws Exception {
    / /... Check whether the APK file exists
    / / create LoadedPlugin
    LoadedPlugin plugin = createLoadedPlugin(apk);
    
    // Cache to mPlugins map
    this.mPlugins.put(plugin.getPackageName(), plugin);
    synchronized (mCallbacks) {
        for (int i = 0; i < mCallbacks.size(); i++) {
            // Plug-in Load callback successfullymCallbacks.get(i).onAddedLoadedPlugin(plugin); }}}Copy the code

LoadedPlugin creation process:

public LoadedPlugin(PluginManager pluginManager, Context context, File apk) throws Exception {

    . / / reflection calls PackageParser parsePackage parsing apk that get Package object
    this.mPackage = PackageParserCompat.parsePackage(context, apk, PackageParser.PARSE_MUST_BE_APK);
    this.mPackage.applicationInfo.metaData = this.mPackage.mAppMetaData;
   
    // Construct the PackageInfo object
    this.mPackageInfo = new PackageInfo();
    / /... Copy the contents of package to PackageInfo
    
    // Construct PluginPacakgeManager objects
    this.mPackageManager = createPluginPackageManager();
    
    // Construct PluginContext
    this.mPluginContext = createPluginContext(null);
    
    // Construct the Resources object
    this.mResources = createResources(context, getPackageName(), apk);
    
    / / this construction
    this.mClassLoader = createClassLoader(context, apk, this.mNativeLibDir, context.getClassLoader());
    
    // Copy the so library. Copy the SO corresponding to the CPU-ABI to the host directory.
    tryToCopyNativeLib(apk);

    / / the cache Manifest the Activities/Services/Content Provider
    // ...
   
    // Change the static broadcast to dynamic
    // ...
   
    // Instantiate the plug-in's Application and call onCreate
    invokeApplication();
}
Copy the code

The specific practices

parsePacakge

// Different Android versions hook in different ways
static final PackageParser.Package parsePackage(Context context, File apk, int flags) throws Throwable {
    PackageParser parser = new PackageParser();
    / / the resolution from the apk Package, including the packageName/versionCode/versionName/information such as the four major components
    PackageParser.Package pkg = parser.parsePackage(apk, flags);
    // Get application signature information mSignatures via the collectCertificates method
    Reflector.with(parser)
        .method("collectCertificates", PackageParser.Package.class, int.class)
        .call(pkg, flags);
    return pkg;
}
Copy the code

Create the PackageManager for the plug-in

/ / create PluginPackageManager
protected class PluginPackageManager extends PackageManager {
    @Override
    public PackageInfo getPackageInfo(String packageName, int flags) throws NameNotFoundException {
        // Get the plugin LoadedPlugin from PluginManager using the package name
        LoadedPlugin plugin = mPluginManager.getLoadedPlugin(packageName);
        if (null! = plugin) {// Get the plug-in PackageInfo
            return plugin.mPackageInfo;
        }
        // If not, use the host's PackageInfo
        return this.mHostPackageManager.getPackageInfo(packageName, flags);
    }
    
    @Override
    public ActivityInfo getActivityInfo(ComponentName component, int flags) throws NameNotFoundException {
        LoadedPlugin plugin = mPluginManager.getLoadedPlugin(component);
        if (null! = plugin) {// Get ActivityInfo from LoadedPlugin
            return plugin.mActivityInfos.get(component);
        }
        return this.mHostPackageManager.getActivityInfo(component, flags);
    }
    
    // ...
}
Copy the code

Create the plug-in Context

class PluginContext extends ContextWrapper{

    private final LoadedPlugin mPlugin;

    public PluginContext(LoadedPlugin plugin) {
        super(plugin.getPluginManager().getHostContext());
        this.mPlugin = plugin;
    }
    
    @Override
    public ClassLoader getClassLoader(a) {
        // Get the plugin ClassLoader
        return this.mPlugin.getClassLoader();
    }
    
    @Override
    public PackageManager getPackageManager(a) {
        // Get the plug-in PackageManager
        return this.mPlugin.getPackageManager();
    }

    @Override
    public Resources getResources(a) {
        // Get the Resources of the plug-in
        return this.mPlugin.getResources();
    }

    @Override
    public AssetManager getAssets(a) {
        // Get the AssetManager for the plug-in
        return this.mPlugin.getAssets();
    }

    @Override
    public void startActivity(Intent intent) {
        // Start the plugin's activity
        ComponentsHandler componentsHandler = mPlugin.getPluginManager().getComponentsHandler();
        componentsHandler.transformIntentToExplicitAsNeeded(intent);
        super.startActivity(intent); }}Copy the code

The purpose of creating the PackageManger and Context for the plugin is to make it easier to use the plugin’s ClassLoader, Resources and other Resources. For example, Hook the Context after creating an Activity.

Resources

What does packing aAPT do

  • Generate a resource ID constant for each resource in the Assets RES directory and store the mapping between the ID value and the resource name in the resources.arsc file
  • Define these resource ID constants in the R.Java file
  • Converting textual XML to binary XML files takes up less space and is faster to parse

The runtime retrieves resources

  • During the running process, resources are obtained through Resource, and Resource files packaged into APK are read by AssetManager inside Resource.
  • Add resources to AssetManager by calling addAssetPath of AssetManager and passing resDir in

VirtualApk provides two ways to handle resources

protected Resources createResources(Context context, String packageName, File apk) throws Exception {
    if (Constants.COMBINE_RESOURCES) {
        // Merge host and plug-in resources
        return ResourcesManager.createResources(context, packageName, apk);
    } else {
        Resources hostResources = context.getResources();
        // Create an assetManager
        AssetManager assetManager = createAssetManager(context, apk);
        // Initialize the resource using the new assetManager and the host screen parameters (DPI, screen width).
        return newResources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration()); }}Copy the code
  • If the COMBINE_RESOURCES flag is set, the resources of the host and plug-in are merged. The host and plug-in can access each other, and the plug-in can access resources of other plug-ins (not recommended).
  • Otherwise, plug-ins use separate Resources, with hosts and plug-ins inaccessible to each other
1. Resource independence
protected AssetManager createAssetManager(Context context, File apk) throws Exception {
    AssetManager am = AssetManager.class.newInstance();
    // Call addAssetPath to add resources from plug-in APk to assetManager
    Reflector.with(am).method("addAssetPath", String.class).call(apk.getAbsolutePath());
    return am;
}
Copy the code
2. Merge resources

ResourceManager.createResources

public static synchronized Resources createResources(Context hostContext, String packageName, File apk) throws Exception {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        // hook mSplitResDirs directly after N
        return createResourcesForN(hostContext, packageName, apk);
    }
    
    // create newResources after the merged resource
    Resources resources = ResourcesManager.createResourcesSimple(hostContext, apk.getAbsolutePath());
    // Replace the host mResources
    ResourcesManager.hookResources(hostContext, resources);
    return resources;
}
Copy the code

ResourceManager.createResourcesSimple

private static Resources createResourcesSimple(Context hostContext, String apk) throws Exception {
    Resources hostResources = hostContext.getResources();
    Reflector reflector = Reflector.on(AssetManager.class).method("addAssetPath", String.class);
    // Get the host's AssetManager
    AssetManager assetManager = hostResources.getAssets();
    reflector.bind(assetManager);
    // Call addAssetPath to add plug-in resources
    final int cookie2 = reflector.call(apk);
    // Add resources for all plug-ins
    List<LoadedPlugin> pluginList = PluginManager.getInstance(hostContext).getAllLoadedPlugins();
    for (LoadedPlugin plugin : pluginList) {
        final int cookie3 = reflector.call(plugin.getLocation());
    }
    / / create newResources
    Resources newResources = newResources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration()); an// Update Resources for all plug-ins
    for (LoadedPlugin plugin : pluginList) {
        plugin.updateResources(newResources);
    }
    
    return newResources;
}
Copy the code

ResourceManager.hookResources

// Replace the host's Resources with newResources
public static void hookResources(Context base, Resources resources) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        return;
    }
    try {
        // Replace mReources in the host context
        Reflector reflector = Reflector.with(base);
        reflector.field("mResources").set(resources);
        // Replace mResources in host PackageInfo
        Object loadedApk = reflector.field("mPackageInfo").get();
        Reflector.with(loadedApk).field("mResourtces").set(resources);

        Object activityThread = ActivityThread.currentActivityThread();
        Object resManager;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            resManager = android.app.ResourcesManager.getInstance();
        } else {
            resManager = Reflector.with(activityThread).field("mResourcesManager").get();
        }
        // Replace the Resource cache in ResourceManager and put a weak reference to newResources in the map. The newly created context will use newResources
        Map<Object, WeakReference<Resources>> map = Reflector.with(resManager).field("mActiveResources").get();
        Object key = map.keySet().iterator().next();
        map.put(key, new WeakReference<>(resources));
    } catch(Exception e) { Log.w(TAG, e); }}Copy the code

Create context, is called the ResourceManager. GetTopLevelResources () to obtain the Resources, all in the context of Resources come from here.

public Resources getTopLevelResources(String resDir, int displayId, Configuration overrideConfiguration, CompatibilityInfo compatInfo, IBinder token) {  
    final float scale = compatInfo.applicationScale;  
    / / create a key
    ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfiguration, scale, token);  
    Resources r;  
    synchronized (this) {  
        // Whether mActiveResources existsWeakReference<Resources> wr = mActiveResources.get(key); r = wr ! =null ? wr.get() : null;  
        if(r ! =null && r.getAssets().isUpToDate()) {  
            returnr; }}/ / create the Resources
    AssetManager assets = new AssetManager();  
    r = new Resources(assets, dm, config, compatInfo, token);  
  
    synchronized (this) {  
        WeakReference<Resources> wr = mActiveResources.get(key);  
        // Store weak references to newly created Resources into ActiveResources
        mActiveResources.put(key, new WeakReference<Resources>(r));
        returnr; }}Copy the code

Problems merging resources

1. The resource ID is duplicate

When a resource is packaged, a unique Id is assigned to the resource file in the RES directory.

  • The first two PP values of Id are Package IDS, indicating the application type. System applications, third-party applications, Instant Apps, or Dynamic features.
  • TT in the middle of the Id indicates Type, indicating the resource Type. Drawable, layout, string, etc.
  • The last four digits of EE indicate Entry, which indicates the resource sequence.

Solution: Rewrite the AAPT command, during the plug-in APK packaging process, by specifying the prefix of the resource ID PP field, to ensure that the host and plug-in resource ids will never conflict.

2. Host upgrade

When the host is upgraded, the plug-in of the old version matches the host of the new version. Ensure that the id of the resource called by the original plug-in cannot be changed. Otherwise, the loaded plug-in still gets the ID of the resource of the old version after the host is upgraded. Therefore, the resources in the host used by the plug-in should ensure that:

  • Old resources cannot be deleted
  • Need to keep the id of the old version of the resource unchanged (see Tinker’s implementation)

ClassLoader

Class-loaded source code

loadClass

// Parent delegate mechanism
protectedClass<? > loadClass(String name,boolean resolve)
    throws ClassNotFoundException
{
        // First, check that the class is loadedClass<? > c = findLoadedClass(name);if (c == null) {
            try {
                if(parent ! =null) {
                    c = parent.loadClass(name, false);
                } else{ c = findBootstrapClassOrNull(name); }}catch (ClassNotFoundException e) {
                // parent Class not found
            }

            if (c == null) {
                // Load the class yourselfc = findClass(name); }}return c;
}
Copy the code

findClass

protectedClass<? > findClass(String name)throws ClassNotFoundException {
    List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
    // Use DexPathList to find classes
    Class c = pathList.findClass(name, suppressedExceptions);
    // ...
    return c;
}
Copy the code
public Class findClass(String name, List<Throwable> suppressed) {
    // look through dexElements
    for (Element element : dexElements) {
        DexFile dex = element.dexFile;

        if(dex ! =null) {
            Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
            if(clazz ! =null) {
                returnclazz; }}}return null;
}
Copy the code

DexClassLoader is different from PathClassLoader

  • DexClassLoader and PathClassLoader both inherit from BaseDexClassLoader.
  • If optmizedDirectory is not empty, the user-defined directory is used as the storage directory of the optimized DEX file. Odex; if optmizedDirectory is empty, the default /data/dalvik-cache/ directory is used.
  • The specified optimizedDirectory must be an internal store

BaseDexClassLoader constructor:

// dex file path /odex file output directory/dynamic library path /parent classLoader
public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent) {
    this(dexPath, librarySearchPath, parent, null.false);
}
Copy the code

The PathClassLoader constructor:

public PathClassLoader(String dexPath, ClassLoader parent) {
    super(dexPath, null.null, parent);
}
    
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
    super(dexPath, null, librarySearchPath, parent);
}
Copy the code

The DexClassLoader constructor

public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {
        super(dexPath, newFile(optimizedDirectory), libraryPath, parent); }}Copy the code

Multiple ClassLoader & single ClassLoader

More than this

Multi-classloader schemes can also be subdivided into two types: One is that the parent of each custom ClassLoader is the ClassLoader of the current host application, that is, PathClassLoader. In this scheme, the host is regarded as the operating environment, plug-ins need to rely on the host to run, and plug-ins are isolated from each other, as shown in the following figure:

One is that the parent of each custom ClassLoader is BootClassLoader. This scheme is similar to the scheme of native application isolation, where the host and plug-in and plug-in are independent from each other, as shown in the following figure:

Single this

Dex is delegated to the application’s PathClassLoader, and the host and plug-in share the same ClassLoader. When a BaseDexClassLoader is constructed, it creates a DexPathList, and DexPathList has an array called dexElements inside it. What we need to do is to insert the dex file into this dexElements array. When a class is found in the PathClassLoader, the DexFile information in this array is traversed to complete the loading of the plug-in class.

VirtualApk plugin ClassLoader creation

protected ClassLoader createClassLoader(Context context, File apk, File libsDir, ClassLoader parent) throws Exception {
    File dexOutputDir = getDir(context, Constants.OPTIMIZE_DIR);
    String dexOutputPath = dexOutputDir.getAbsolutePath();
    // dex File path Optimized odex file path Dynamic library path Host classloader
    DexClassLoader loader = new DexClassLoader(apk.getAbsolutePath(), dexOutputPath, libsDir.getAbsolutePath(), parent);

    if (Constants.COMBINE_CLASSLOADER) {
        // merge to the host, which has access to the plug-in's classes
        DexUtil.insertDex(loader, parent, libsDir);
    }

    return loader;
}
Copy the code
Merge DexElements

DexUtils.insertDex

public static void insertDex(DexClassLoader dexClassLoader, ClassLoader baseClassLoader, File nativeLibsDir) throws Exception {
    // Host's dexElements
    Object baseDexElements = getDexElements(getPathList(baseClassLoader));
    / / the plugin dexElements
    Object newDexElements = getDexElements(getPathList(dexClassLoader));
    // After merging, the host's dexElements precede the plug-in
    Object allDexElements = combineArray(baseDexElements, newDexElements);
    // Set the merged dexElements into the host PathClassloader
    Object pathList = getPathList(baseClassLoader);
    Reflector.with(pathList).field("dexElements").set(allDexElements);
    
    // Insert the so library
    insertNativeLibrary(dexClassLoader, baseClassLoader, nativeLibsDir);
}
Copy the code
conclusion
  • If COMBINE_CLASSLOADER is configured, the host can access the plug-in Class. If COMBINE_CLASSLOADER is not configured, the host cannot access the plug-in Class
  • Plug-ins can and have priority access to the host’s Class because the host’s classLoader is the parent
  • Single ClassLoader and multiple ClassLoaders coexist, which is more flexible. If you don’t know which plug-in the class belongs to, use the PathClassLoader to load it. If you know which plug-in belongs to, it is more efficient to load it directly using the plug-in’s ClassLoader.

Four major components

The four major components are not registered in the host Manifest, so some Hook operations need to be done to bypass system checks.

The Activity start

A few questions:

  • The plug-in Activity is not registered in the Manifest will quote ActivityNotFoundException abnormalities, how to solve?
  • Plug-in Activity creation and resource issues after creation
  • What about LaunchMode?

Hook procedure

  • Before the request to start an Activity reaches AMS, it is replaced with an Activity registered in the Manifest to pass THE AMS check
  • On the path where the AMS call comes back, replace the Activity back
Activity Startup process

To lift the hooks
  // Dynamic proxy
  public static void hookActivityManagerService(a) throws Reflector.ReflectedException{
    Object gDefaultObj = null;
    . / / API hook after 29 and android app. ActivityTaskManager. IActivityTaskManagerSingleton
    . / / API hook 26 and after android app. ActivityManager. IActivityManagerSingleton
    / / API hook before 25 android. App. ActivityManagerNative. GDefault
    if(Build.VERSION.SDK_INT >= 29){
      gDefaultObj = Reflector.on("android.app.ActivityTaskManager").field("IActivityTaskManagerSingleton").get();
    }else if(Build.VERSION.SDK_INT >= 26){
      gDefaultObj = Reflector.on("android.app.ActivityManager").field("IActivityManagerSingleton").get();
    }else{
      gDefaultObj = Reflector.on("android.app.ActivityManagerNative").field("gDefault").get();
    }
    Object amsObj = Reflector.with(gDefaultObj).field("mInstance").get();
    // Local class loader;
    // The interface that the object of the proxy Class inherits (represented by the Class array, supporting multiple interfaces)
    // The actual logic of the proxy class is encapsulated in the New InvocationHandler
    Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
        amsObj.getClass().getInterfaces(), new IActivityManagerHandler(amsObj));
    Reflector.with(gDefaultObj).field("mInstance").set(proxy);
  }
Copy the code

IActivityManagerHandler

public class IActivityManagerHandler implements InvocationHandler {
    Object mBase;
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Log.d(TAG, "invoke " + method.getName());
        // If the Activity is started, replace the Intent
        if("startActivity".equals(method.getName())){
          hookStartActivity(args);
          return method.invoke(mBase, args);
        }else if("startService".equals(method.getName())){
          // Intercept all operations, change them to startService, and distribute them in onStartCommand
        }
        returnmethod.invoke(mBase, args); }}Copy the code

Replace the Activity

Private void hookStartActivity(Object[] args){int index = getIntentIndex(args); Intent intent = (Intent) args[index]; ComponentName Component = intent.getComponent(); // IntentName = intent.getComponent(); // Component is empty, And the host if (component = = null) {/ / host resolveinfo null resolveinfo info = mPluginManager. ResolveActivity (intent); if(info ! = null && info.activityInfo ! = null){ component = new ComponentName(info.activityInfo.packageName, info.activityInfo.name); intent.setComponent(component); Intent.getcomponent (); intent.getComponent(); = null && ! intent.getComponent().getPackageName().equals(mPluginManager.getHostContext().getPackageName())){ Intent newIntent = new  Intent(); String stubPackage = mPluginManager.getHostContext().getPackageName(); ComponentName = New ComponentName(stubPackage, mPluginManager.getComponentsHandler().getStubActivityClass(intent)); newIntent.setComponent(componentName); PutExtra (Constants.KEY_IS_PLUGIN, true); // Store the previous intent. newIntent.putExtra(Constants.EXTRA_TARGET_INTENT, intent); args[index] = newIntent; Log.d(TAG, "hook succeed"); }}Copy the code
Return the hooks
 // mH in Hook ActivityThread
 public void hookActivityThreadCallback(a) throws Exception {
    ActivityThread activityThread = ActivityThread.currentActivityThread();
    Handler handler = Reflector.with(activityThread).field("mH").get();
    Reflector.with(handler).field("mCallback").set(new ActivityThreadHandlerCallback(handler));
 }
Copy the code

ActivityThreadHandlerCallback

@Override public boolean handleMessage(@NonNull Message msg) { Log.d(TAG, "handle Message " + msg.what); If (what == 0){try{// Hook the value of EXECUTE_TRANSACTION is ActivityThread ActivityThread = ActivityThread.currentActivityThread(); Handler handler = Reflector.with(activityThread).field("mH").get(); what = Reflector.with(handler).field("EXECUTE_TRANSACTION").get(); }catch (Reflector.ReflectedException e){ e.printStackTrace(); what = EXECUTE_TRANSACTION; If (MSG. What == what){handleLaunchActivity(MSG); } return false; }Copy the code
private void handleLaunchActivity(Message msg){ try{ List list = Reflector.with(msg.obj).field("mActivityCallbacks").get(); if(list == null || list.isEmpty()) return; Class<? > launchActivityItemClz = Class.forName("android.app.servertransaction.LaunchActivityItem"); If (launchActivityItemClz. IsInstance (list. Get (0))) {/ / get to start from the LaunchActivityItem intent intent intent = Reflector.with(list.get(0)).field("mIntent").get(); / / to start the intent of saving target, plug-in is the activity of information intent target = intent. GetParcelableExtra (the EXTRA_TARGET_INTENT); if(target ! = null){// Replace the original activity intent.setComponent(target.getComponent()); } } }catch (Reflector.ReflectedException e){ e.printStackTrace(); }catch (ClassNotFoundException e){ e.printStackTrace(); }}Copy the code
Creating a plug-in Activity

VAInstrumentation

@Override public Activity newActivity(ClassLoader cl, String className, Intent intent) throws ClassNotFoundException, IllegalAccessException, InstantiationException {try {// Host classloader.loadClass (className); } catch (ClassNotFoundException e) { ComponentName component = intent.getComponent(); if (component ! = null) { String targetClassName = component.getClassName(); LoadedPlugin loadedPlugin = mPluginManager.getLoadedPlugin(component.getPackageName()); if (loadedPlugin ! = null) {/ / use the plug-in load this Activity Activity. = mBase newActivity (loadedPlugin. GetClassLoader (), targetClassName, intent); return activity; } } } return super.newActivity(cl, className, intent); }Copy the code

Replace Resources & Context in the Activity

@Override
public void callActivityOnCreate(Activity activity, Bundle icicle) {
    injectActivity(activity);
    mBase.callActivityOnCreate(activity, icicle);
}
Copy the code
protected void injectActivity(Activity activity) {
    final Intent intent = activity.getIntent();
    if (PluginUtil.isIntentFromPlugin(intent)) {
        Context base = activity.getBaseContext();
        try {
            LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent);
             // Replace the mResources of the plug-in Activity context
            Reflector.with(base).field("mResources").set(plugin.getResources());
            Reflector reflector = Reflector.with(activity);
            // Replace the Context of the plug-in Activity
            reflector.field("mBase").set(plugin.createPluginContext(activity.getBaseContext()));
            // Replace the Application of the plug-in Activity
            reflector.field("mApplication").set(plugin.getApplication());

            // set screenOrientation
            ActivityInfo activityInfo = plugin.getActivityInfo(PluginUtil.getComponent(intent));
            if(activityInfo.screenOrientation ! = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) { activity.setRequestedOrientation(activityInfo.screenOrientation); }// for native activity
            ComponentName component = PluginUtil.getComponent(intent);
            Intent wrapperIntent = new Intent(intent);
            wrapperIntent.setClassName(component.getPackageName(), component.getClassName());
            activity.setIntent(wrapperIntent);
            
        } catch (Exception e) {
            Log.w(TAG, e);
        }
    }
}co
Copy the code
LaunchMode

Register various LaunchMode activities in the Manifest and match the different StubActivities in order according to the LaunchMode

BroadcastReceiver

  • Dynamic registration does not require special processing and can be used normally
  • Static registrations are converted to dynamic registrations in loadPlugin
// BroadcastReceiver goes from static to dynamic, registering the static receiver of the add-on to host
Map<ComponentName, ActivityInfo> receivers = new HashMap<>();
for(PackageParser.Activity receiver : this.mPackage.receivers){
  receivers.put(receiver.getComponentName(), receiver.info);
  BroadcastReceiver br = BroadcastReceiver.class.cast(
      // Use the plugin's classloader to load it
      getClassLoader().loadClass(receiver.getComponentName().getClassName()).newInstance());
      for(PackageParser.ActivityIntentInfo aii : receiver.intents){ mHostContext.registerReceiver(br, aii); }}Copy the code
  • Problem: A plug-in’s static broadcast is treated as dynamic and will never be triggered if the plug-in is not running (i.e., no plug-in process is running). This means that plugins cannot be pulled up by listening for system events

conclusion

advantages

  • The plug-in is free to add four components, independent of the host.
  • The COMBINE_RESOURCE and COMBINE_CLASSLOADER options are available, meeting more application scenarios.

disadvantages

  • Hook points, every new Version of Android needs to be re-adapted

Qigsaw

General situation of

Rely on Dynamic Features & Split Apks

Method of installing split APKs

  • adb install-multiple [base.apk, split.apk]
  • PackageInstaller, the authorization window is displayed. After the installation is complete, it takes effect again by default. SetDontKillApp (system Api) determines whether to kill the application process after APK is installed
  • The experience of third-party applications using PackageInstaller to install Split APKs is not friendly, and some domestic mobile phones do not fully support split APKs function, so Qigsaw finally installed and loaded Split APKs in a general plug-in way.

Differences from other frameworks

Four major components

  • At packaging time, a Manifest merge is performed, merging the SPLIT APK’s Manifest into the Base APK
  • Split APK’s four major components cannot be updated dynamically

ClassLoader

  • Single ClassLoader and multiple ClassLoaders are optional
  • In multi-classLoader mode, each plug-in generates a SplitDexClassLoader. Plug-ins can access the host’s classes, but the host cannot access the plug-in’s classes

Resources

  • When packaging, Android Gradle Plugin separates the resource ID of split APks from the ID of base APK without conflict, also by custom PP field
  • Use, using ASM insert SplitInstallHelper. Where getResource loadResources (), merge all plug-in resources.
  • The host cannot access the resources of the plug-in by default and needs to be whitelisted. Once whitelisted, use ASM to insert resource into the target Activity.
/**
 * Activities of base apk which would load split's fragments or resources.
 */
baseContainerActivities = [

    // gamecenterplugin
    "com.yxcorp.gifshow.gamecenter.cloudgame.ZtGameCloudPlayActivity"
]
Copy the code

Multiprocess problem

The child needs to initialize Qigsaw, but the child has not loaded the plug-in Split Apks.

Qigsaw’s solution:

  • All the installed Monos will be loaded when the child process starts
  • Change the ClassLoader findClass and load all the installed Monos if ClassNotFound is found. Because a new plug-in might be loaded while the process is running.
privateClass<? > onClassNotFound(String name) {// All the installed Monos have been loaded
    SplitLoadManagerService.getInstance().loadInstalledSplits();
    ret = findClassInSplits(name);
    if(ret ! =null) {
        SplitLog.i(TAG, "Class %s is found in Splits after loading all installed splits.", name);
        return ret;
    }
    return null;
}
Copy the code

The plugin updates

  • The plugin cannot be fully updated dynamically. Tinker hotfix is available
  • Each time the host is upgraded, the plug-in is re-downloaded
  • Optimization scheme: so do not download

conclusion

advantages

  • Can seamlessly switch between domestic and foreign releases
  • Standing on Google’s shoulders is stable and easy to implement

disadvantages

  • The plug-in is packaged with the host, and each time the host is updated, the plug-in is re-downloaded