搞懂插件化,看这一篇就够了

背景

历史和现状

发展历史

  • 2012年,大众点评的屠毅敏推出了AndroidDynamicLoader框架,可以算是第一个插件化框架。
  • 2014年初,阿里一位员工做了一次技术分享,专门讲淘宝的Altas技术,以及这项技术的大方向。但是很多技术细节没有分享,也没有开源。
  • 2014年底,任玉刚发布了一个Android插件化项目,起名为dynamic-load-apk,它没有Hook太多的系统底层方法,而是从上层,即App应用层解决问题,创建一个继承自Activity的ProxyActivity类,然后让插件中的所有Activity都继承自ProxyActivity,并重写Activity所有的方法。
  • 2015年8月,张勇发布DroidPlugin,有非常多的Hook,能把任意的App都加载到宿主里。目前已经不维护了,但很多思路值得借鉴。
  • 2017年3月,Atlas开源。
  • 2017年6月,VirtualAPK 是滴滴开源的一套插件化框架,支持几乎所有的 Android 特性,四大组件等。
  • 2018年,Google在IO大会上发布了Android App Bundle方案,是一个依赖于GooglePlay Service的官方插件化方案,同时在Android P及以上系统中开始加入了系统级的支持。不过因为国内没有PlayService服务,我们需要在其基础上进行魔改。
  • 2019年6月,Qigsaw正式开源,基于Android App Bundle,同时其API与官方兼容,即在国内可以走插件化方式,在海外走GooglePlay渠道,思路非常不错。

现状

  • 各插件框架,都是基于自身App的业务来开发的,目标或多或少都有区别,所以很难有一个插件框架能一统江湖解决所有问题。
  • Android每次版本升级都会给各个插件化框架带来不少冲击,都要费劲心思适配一番,更别提国内各个厂商对在ROM上做的定制了,正如VirtualAPK的作者任玉刚所说:完成一个插件化框架的 Demo 并不是多难的事儿,然而要开发一款完善的插件化框架却并非易事。
  • 在2020年,Android插件化依旧是一个高风险的技术,涉及到各个Android SDK版本、各个OEM厂商的兼容性适配问题。

笼统的分类

免安装型

  • 宿主和插件单独编译。
  • 加载一个与主app无业务关系的独立apk,实现不安装即可使用功能。

自解耦型

  • 宿主和插件共同完成编译。
  • 偏向于在“组件化”的基础上,将“组件”从主app中剥离为“插件”。

插件化关注点

  • 加载&管理插件
  • ClassLoader
  • Resources
  • 四大组件
  • 编译打包
  • 宿主&插件升级

VirtualApk源码

VirtualApk作为一个开创性的插件化框架,源码非常具有借鉴意义。下面从VirtualApk源码的角度,从各个方面来讨论,如何实现一个插件化框架。

加载&管理插件

  • PluginManager:负责加载和管理插件,保存一些全局信息
  • LoadedPlugin:加载插件后,保存了插件的全部信息

加载流程

PluginManager

public void loadPlugin(File apk) throws Exception {
    // ... 检查apk文件是否存在
    // 创建LoadedPlugin
    LoadedPlugin plugin = createLoadedPlugin(apk);
    
    // 缓存到mPlugins map 中
    this.mPlugins.put(plugin.getPackageName(), plugin);
    synchronized (mCallbacks) {
        for (int i = 0; i < mCallbacks.size(); i++) {
            // 插件Load成功回调
            mCallbacks.get(i).onAddedLoadedPlugin(plugin);
        }
    }
}

创建LoadedPlugin大致流程:

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

    // 反射调用PackageParser.parsePackage解析apk,获取Package对象
    this.mPackage = PackageParserCompat.parsePackage(context, apk, PackageParser.PARSE_MUST_BE_APK);
    this.mPackage.applicationInfo.metaData = this.mPackage.mAppMetaData;
   
    // 构造PackageInfo对象
    this.mPackageInfo = new PackageInfo();
    // ... 将package中内容复制到PackageInfo中
    
    // 构造PluginPacakgeManager对象
    this.mPackageManager = createPluginPackageManager();
    
    // 构造PluginContext对象
    this.mPluginContext = createPluginContext(null);
    
    // 构造Resources对象
    this.mResources = createResources(context, getPackageName(), apk);
    
    // 构造ClassLoader
    this.mClassLoader = createClassLoader(context, apk, this.mNativeLibDir, context.getClassLoader());
    
    // 拷贝so库。将CPU-ABI对应的SO拷贝到宿主的目录下。
    tryToCopyNativeLib(apk);

    // 缓存Manifest中的Activities/Services/Content Provider
    // ...
   
    // 将静态广播转为动态 
    // ...
   
    // 实例化插件的Application,并调用onCreate
    invokeApplication();
}

具体做法

parsePacakge

// 不同的Android版本,hook的方式不一样
static final PackageParser.Package parsePackage(Context context, File apk, int flags) throws Throwable {
    PackageParser parser = new PackageParser();
    // 从apk中解析出Package,包含packageName/versionCode/versionName/四大组件等信息
    PackageParser.Package pkg = parser.parsePackage(apk, flags);
    // 通过collectCertificates方法获取应用的签名信息mSignatures
    Reflector.with(parser)
        .method("collectCertificates", PackageParser.Package.class, int.class)
        .call(pkg, flags);
    return pkg;
}

创建插件的PackageManager

// 创建PluginPackageManager
protected class PluginPackageManager extends PackageManager {
    @Override
    public PackageInfo getPackageInfo(String packageName, int flags) throws NameNotFoundException {
        // 使用包名从PluginManager中获取插件LoadedPlugin
        LoadedPlugin plugin = mPluginManager.getLoadedPlugin(packageName);
        if (null != plugin) {
            // 获取插件PackageInfo
            return plugin.mPackageInfo;
        }
        // 如果没找到,则使用宿主的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) {
            // 从LoadedPlugin获取ActivityInfo
            return plugin.mActivityInfos.get(component);
        }
        return this.mHostPackageManager.getActivityInfo(component, flags);
    }
    
    // ...
}

创建插件的Context

class PluginContext extends ContextWrapper{

    private final LoadedPlugin mPlugin;

    public PluginContext(LoadedPlugin plugin) {
        super(plugin.getPluginManager().getHostContext());
        this.mPlugin = plugin;
    }
    
    @Override
    public ClassLoader getClassLoader() {
        // 获取插件ClassLoader
        return this.mPlugin.getClassLoader();
    }
    
    @Override
    public PackageManager getPackageManager() {
        // 获取插件PackageManager
        return this.mPlugin.getPackageManager();
    }

    @Override
    public Resources getResources() {
        // 获取插件的Resources
        return this.mPlugin.getResources();
    }

    @Override
    public AssetManager getAssets() {
        // 获取插件的AssetManager
        return this.mPlugin.getAssets();
    }

    @Override
    public void startActivity(Intent intent) {
        // 启动插件的activity
        ComponentsHandler componentsHandler = mPlugin.getPluginManager().getComponentsHandler();
        componentsHandler.transformIntentToExplicitAsNeeded(intent);
        super.startActivity(intent);
    }
}

创建插件的PackageManger和Context的用处,是为了在后续的使用中,更方便得使用插件的ClassLoader,Resources等资源。比如创建Activity后Hook掉Context。

Resources

打包aapt做了什么

image
  • 为assets res目录的每个资源,生成一个资源id常量,把id值和资源名称的对应关系,存放在resources.arsc文件中
  • 把这些资源id常量,都定义在R.java文件中
  • 将文本的xml转化成二进制xml文件,占用空间更小,解析更快

运行时获取资源

  • 运行过程中通过Resource来获取资源,Resource内部通过AssetManager来读取打包到apk中的资源文件。
  • 调用AssetManager的addAssetPath,将resDir传入,将资源加入到assetmanager中

VirtualApk提供两种资源处理方式

protected Resources createResources(Context context, String packageName, File apk) throws Exception {
    if (Constants.COMBINE_RESOURCES) {
        // 合并宿主和插件资源
        return ResourcesManager.createResources(context, packageName, apk);
    } else {
        Resources hostResources = context.getResources();
        // 新创建一个assetmanager
        AssetManager assetManager = createAssetManager(context, apk);
        // 使用新的assetmanager和宿主的屏幕参数信息(DPI、屏幕宽高等),初始化resource。
        return new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());
    }
}
  • 如果设置了COMBINE_RESOURCES标志,会将宿主与插件的资源合并,宿主和插件可以互相访问,插件也可访问其他插件的资源(不推荐)
  • 否则,插件使用独立的Resources,宿主和插件,插件和插件之间都无法互相访问
1. 资源独立
protected AssetManager createAssetManager(Context context, File apk) throws Exception {
    AssetManager am = AssetManager.class.newInstance();
    // 调用addAssetPath将插件apk的资源加入到assetManager
    Reflector.with(am).method("addAssetPath", String.class).call(apk.getAbsolutePath());
    return am;
}
2. 资源合并

ResourceManager.createResources

public static synchronized Resources createResources(Context hostContext, String packageName, File apk) throws Exception {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        // N之后可以直接hook mSplitResDirs,原理一样
        return createResourcesForN(hostContext, packageName, apk);
    }
    
    // 创建合并资源后的newResources
    Resources resources = ResourcesManager.createResourcesSimple(hostContext, apk.getAbsolutePath());
    // 替换宿主的mResources
    ResourcesManager.hookResources(hostContext, resources);
    return resources;
}

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);
    // 获取宿主的AssetManager
    AssetManager assetManager = hostResources.getAssets();
    reflector.bind(assetManager);
    // 调用addAssetPath添加插件资源
    final int cookie2 = reflector.call(apk);
    // 添加所有插件的资源
    List<LoadedPlugin> pluginList = PluginManager.getInstance(hostContext).getAllLoadedPlugins();
    for (LoadedPlugin plugin : pluginList) {
        final int cookie3 = reflector.call(plugin.getLocation());
    }
    // 创建newResources
    Resources newResources = new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());an
    // 更新所有插件的Resources
    for (LoadedPlugin plugin : pluginList) {
        plugin.updateResources(newResources);
    }
    
    return newResources;
}

ResourceManager.hookResources

// 用newResources替换宿主的Resources
public static void hookResources(Context base, Resources resources) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        return;
    }
    try {
        // 替换宿主context中的mReources
        Reflector reflector = Reflector.with(base);
        reflector.field("mResources").set(resources);
        // 替换宿主PackageInfo中的mResources
        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();
        }
        // 替换ResourceManager中的resource缓存,将newResources的弱引用放入map中,新创建的context会使用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);
    }
}

创建context的时候,会调用ResourceManager.getTopLevelResources()来获取Resources,所有context里面的资源都来自于此处。

public Resources getTopLevelResources(String resDir, int displayId, Configuration overrideConfiguration, CompatibilityInfo compatInfo, IBinder token) {  
    final float scale = compatInfo.applicationScale;  
    // 创建key
    ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfiguration, scale, token);  
    Resources r;  
    synchronized (this) {  
        // 是否在mActiveResources存在
        WeakReference<Resources> wr = mActiveResources.get(key);  
        r = wr != null ? wr.get() : null;  
        if (r != null && r.getAssets().isUpToDate()) {  
            return r;  
        }  
    }  
  
    // 创建Resources
    AssetManager assets = new AssetManager();  
    r = new Resources(assets, dm, config, compatInfo, token);  
  
    synchronized (this) {  
        WeakReference<Resources> wr = mActiveResources.get(key);  
        // 将新创建的Resources的弱引用存入ActiveResources
        mActiveResources.put(key, new WeakReference<Resources>(r));
        return r;  
    }  
}  

合并资源的问题

1. 资源id重复

资源打包时,会对res目录下资源文件分配一个唯一Id。


image
  • Id前两位PP为Package Id,代表应用类型。是系统应用、第三方应用、Instant App或Dynamic Feature等。
  • Id中间两位TT为Type,代表资源类型。是drawable、layout或string等。
  • Id后四位EE为Entry,代表该资源顺序。

解决方法:
重写AAPT命令,在插件apk打包过程中,通过指定资源id的前缀PP字段,来保证宿主和插件的资源id永远不会冲突。

2. 宿主升级

宿主升级,旧版本插件配新版本宿主,需要保证原来插件调用的资源id不能改变,否则宿主升级后,加载的插件还是拿取的旧版本资源id,会导致资源找不到和错乱情况。
所以,宿主中被插件使用的资源要保证:

  • 旧的资源不能删除
  • 需要保持旧版本资源的id不变(参考Tinker的实现方案)

ClassLoader

类加载源码

loadClass

// 双亲委派机制
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) {
                // parent未找到类
            }

            if (c == null) {
                // 自己去加载类
                c = findClass(name);
            }
        }
        return c;
}

findClass

protected Class<?> findClass(String name) throws ClassNotFoundException {
    List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
    //通过DexPathList查找类
    Class c = pathList.findClass(name, suppressedExceptions);
    // ...
    return c;
}
public Class findClass(String name, List<Throwable> suppressed) {
    // 遍历dexElements查找
    for (Element element : dexElements) {
        DexFile dex = element.dexFile;

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

DexClassLoader & PathClassLoader 区别

  • DexClassLoader和PathClassLoader都继承自BaseDexClassLoader。
  • optmizedDirectory 不为空时,使用用户定义的目录作为 DEX 文件优化后产物 .odex 的存储目录,为空时,会使用默认的 /data/dalvik-cache/ 目录。
  • 指定的optimizedDirectory必须是内部存储

BaseDexClassLoader构造函数:

// dex文件路径/odex文件输出目录/动态库路径/parent classloader
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
        String librarySearchPath, ClassLoader parent) {
    this(dexPath, librarySearchPath, parent, null, false);
}

PathClassLoader构造函数:

public PathClassLoader(String dexPath, ClassLoader parent) {
    super(dexPath, null, null, parent);
}
    
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
    super(dexPath, null, librarySearchPath, parent);
}

DexClassLoader构造函数

public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}

多ClassLoader & 单ClassLoader

多ClassLoader

多 ClassLoader 的方案,还可以细分为两种:一种是每个自定义 ClassLoader 的 parent 为当前宿主应用的 ClassLoader 即是 PathClassLoader,这种方案将宿主视为运行环境,插件需依赖宿主运行,插件之间互相隔离,如下图:


image

一种是每个自定义 ClassLoader 的 parent 为 BootClassLoader,这种方案类似原生应用隔离的方案,宿主与插件、插件与插件互相独立,如下图:


image
单ClassLoader

这种方案是委托给应用的PathClassLoader加载.dex,宿主与插件共享同一个 ClassLoader。 BaseDexClassLoader 在构造时生创建一个DexPathList,而DexPathList内部有一个叫做dexElements数组,我们要做的就是将 dex 文件插入到这个dexElements数组中,在 PathClassLoader 中查找类时,就会遍历这个数组中 DexFile 的信息,完成插件类的加载。

VirtualApk插件ClassLoader的创建

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文件路径 优化后的odex文件路径 动态库路径 宿主的classloader
    DexClassLoader loader = new DexClassLoader(apk.getAbsolutePath(), dexOutputPath, libsDir.getAbsolutePath(), parent);

    if (Constants.COMBINE_CLASSLOADER) {
        // 合并到宿主, 宿主能访问插件的类
        DexUtil.insertDex(loader, parent, libsDir);
    }

    return loader;
}
合并DexElements

DexUtils.insertDex

public static void insertDex(DexClassLoader dexClassLoader, ClassLoader baseClassLoader, File nativeLibsDir) throws Exception {
    // 宿主的dexElements
    Object baseDexElements = getDexElements(getPathList(baseClassLoader));
    // 插件dexElements
    Object newDexElements = getDexElements(getPathList(dexClassLoader));
    // 合并后,宿主的dexElements在插件前面
    Object allDexElements = combineArray(baseDexElements, newDexElements);
    // 将合并后的dexElements设置到宿主PathClassloader中
    Object pathList = getPathList(baseClassLoader);
    Reflector.with(pathList).field("dexElements").set(allDexElements);
    
    // 插入so库
    insertNativeLibrary(dexClassLoader, baseClassLoader, nativeLibsDir);
}
总结
  • 如果设置COMBINE_CLASSLOADER,宿主可以访问插件的Class,不设置,则宿主无法访问插件的Class
  • 插件可以且优先访问宿主的Class,因为将宿主的classLoader作为parent
  • 单ClassLoader和多ClassLoader并存的方式,更灵活。如果不知道class属于哪个插件,使用PathClassLoader加载。如果知道属于哪个插件,直接使用插件的ClassLoader加载,效率更高。

四大组件

四大组件没有在宿主的Manifest中注册,所以需要做一些Hook操作来绕过系统的检查。

Activity启动

几个问题:

  • 插件Activity没有在Manifest中注册,会报ActivityNotFoundException异常,怎么解决?
  • 插件Activity创建 以及 创建完之后资源问题
  • LaunchMode如何处理?

Hook流程

  • 在启动Activity的请求到达AMS前,替换成已在Manifest中注册的Activity,来通过AMS的检查
  • 在AMS调用回来的路径上,将Activity替换回来
Activity启动流程
image
去程Hook
  // 动态代理
  public static void hookActivityManagerService() throws Reflector.ReflectedException{
    Object gDefaultObj = null;
    // API 29 及以后hook android.app.ActivityTaskManager.IActivityTaskManagerSingleton
    // API 26 及以后hook android.app.ActivityManager.IActivityManagerSingleton
    // API 25 以前hook 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();
    // 本地的类加载器;
    // 代理类的对象所继承的接口(用Class数组表示,支持多个接口)
    // 代理类的实际逻辑,封装在new出来的InvocationHandler内
    Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
        amsObj.getClass().getInterfaces(), new IActivityManagerHandler(amsObj));
    Reflector.with(gDefaultObj).field("mInstance").set(proxy);
  }

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());
        // 如果是启动Activity,替换Intent
        if("startActivity".equals(method.getName())){
          hookStartActivity(args);
          return method.invoke(mBase, args);
        }else if("startService".equals(method.getName())){
          // 将所有的操作进行拦截,都改为startService,然后统一在onStartCommand中分发
        }
        return method.invoke(mBase, args);
    }
}

替换Activity

// 替换为占位Activity
private void hookStartActivity(Object[] args){
    int index = getIntentIndex(args);
    Intent intent = (Intent) args[index];
    
    // 将插件的隐式intent转化为显式intent,host的intent不变
    ComponentName component = intent.getComponent();
    // component为空,且非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);
      }
    }
    
    // Component不为空,且非host
    if(intent.getComponent() != null
        && !intent.getComponent().getPackageName().equals(mPluginManager.getHostContext().getPackageName())){
      Intent newIntent = new Intent();
      String stubPackage = mPluginManager.getHostContext().getPackageName();
      // 占位Activity的名称
      ComponentName componentName = new ComponentName(stubPackage,
          mPluginManager.getComponentsHandler().getStubActivityClass(intent));
      newIntent.setComponent(componentName);
    
      // 将之前的intent存起来
      newIntent.putExtra(Constants.KEY_IS_PLUGIN, true);
      newIntent.putExtra(Constants.EXTRA_TARGET_INTENT, intent);
      args[index] = newIntent;
      Log.d(TAG, "hook succeed");
    }
}
回程Hook
 // Hook ActivityThread 中的 mH
 public void hookActivityThreadCallback() throws Exception {
    ActivityThread activityThread = ActivityThread.currentActivityThread();
    Handler handler = Reflector.with(activityThread).field("mH").get();
    Reflector.with(handler).field("mCallback").set(new ActivityThreadHandlerCallback(handler));
 }

ActivityThreadHandlerCallback

@Override
public boolean handleMessage(@NonNull Message msg) {
    Log.d(TAG, "handle Message " + msg.what);
    if(what == 0){
      try{
        // Hook获取到EXECUTE_TRANSACTION的值
        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;
      }
    }
    // 如果是EXECUTE_TRANSACTION
    if(msg.what == what){
      handleLaunchActivity(msg);
    }
    return false;
}
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))) {
        // 从LaunchActivityItem中获取到待启动的intent
        Intent intent = Reflector.with(list.get(0)).field("mIntent").get();
        // 待启动的intent中保存的target,就是插件activity的信息
        Intent target = intent.getParcelableExtra(Constants.EXTRA_TARGET_INTENT);
        if(target != null){
          // 替换回原来的activity
          intent.setComponent(target.getComponent());
        }
      }
    }catch (Reflector.ReflectedException e){
      e.printStackTrace();
    }catch (ClassNotFoundException e){
      e.printStackTrace();
    }
}
创建插件Activity

VAInstrumentation

@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent)
  throws ClassNotFoundException, IllegalAccessException, InstantiationException {
    try {
      // 宿主的ClassLoader
      cl.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) {
          // 使用插件的classLoader加载
          Activity activity =
              mBase.newActivity(loadedPlugin.getClassLoader(), targetClassName, intent);
          return activity;
        }
      }
    }
    return super.newActivity(cl, className, intent);
}

替换Activity中的Resources & Context

@Override
public void callActivityOnCreate(Activity activity, Bundle icicle) {
    injectActivity(activity);
    mBase.callActivityOnCreate(activity, icicle);
}
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);
             // 替换插件Activity context的mResources
            Reflector.with(base).field("mResources").set(plugin.getResources());
            Reflector reflector = Reflector.with(activity);
            // 替换插件Activity的Context
            reflector.field("mBase").set(plugin.createPluginContext(activity.getBaseContext()));
            // 替换插件Activity的Application
            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
LaunchMode

在Manifest中注册各种LaunchMode的Activity,根据LaunchMode来按顺序匹配到不同的StubActivity

image

BroadcastReceiver

  • 动态注册不需要特殊处理,可以正常使用
  • 静态注册在loadPlugin时会被转化为动态注册
// BroadcastReceiver静态转动态,将插件的静态receiver动态注册到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(
      // 用插件的classloader去加载
      getClassLoader().loadClass(receiver.getComponentName().getClassName()).newInstance());
      for(PackageParser.ActivityIntentInfo aii : receiver.intents){
        mHostContext.registerReceiver(br, aii);
      }
}
  • 问题:插件的静态广播会被当作动态处理,如果插件没有运行(即没有插件进程运行),其静态广播也永远不会被触发。意思就是,不能通过监听系统的一些事件把插件的相关功能拉起

总结

优点

  • 插件可自由增加四大组件,不受宿主约束。
  • 提供COMBINE_RESOURCE和COMBINE_CLASSLOADER两种选择,满足更多应用场景。

缺点

  • Hook点多,每发新的Android版本都需要重新适配

Qigsaw

概况

依赖于Dynamic Features & Split Apks

image

安装split apks的方法

  • adb install-multiple [base.apk, split.apk]
  • PackageInstaller,会弹出授权窗,安装完成后,默认再次启动生效。setDontKillApp(系统Api)可决定当APK安装完成后是否杀死应用进程
  • 第三方应用利用PackageInstaller安装split APKs体验不友好,且某些国产手机对split APKs功能支持不完善,所以Qigsaw最终还是按照一般插件化方式安装加载split APKs。

与其他框架的区别

四大组件

  • 打包的时候,会进行Manifest合并,将split apk的manifest合并到base apk中
  • split apk的四大组件不能动态更新
image

ClassLoader

  • 可选单ClassLoader和多ClassLoader
  • 多ClassLoader模式下,每个插件会生成一个SplitDexClassLoader。插件可以访问宿主的类,宿主不可访问插件的类
image

Resources

  • 打包时,Android Gradle Plugin会将split apks的资源id与base apk的id分开,不会产生冲突,也是通过自定义PP字段
  • 使用时,使用ASM在getResource的地方插入SplitInstallHelper.loadResources(),合并所有插件的资源。
  • 宿主默认不能访问插件的资源,需要加白名单。加入白名单后,使用ASM将resource插入目标Activity。
/**
 * Activities of base apk which would load split's fragments or resources.
 */
baseContainerActivities = [

    // gamecenterplugin
    "com.yxcorp.gifshow.gamecenter.cloudgame.ZtGameCloudPlayActivity"
]

多进程问题

子进程需要初始化qigsaw,但是子进程未加载过插件Split Apks。

qigsaw的解决方案:

  • 子进程启动时,加载所有已安装的splits
  • 修改ClassLoader findClass,如果出现ClassNotFound,加载所有已安装的splits。因为可能在进程运行的过程中,加载了新的插件。
private Class<?> onClassNotFound(String name) {
    // 加载所有已安装的splits
    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;
}

插件更新

  • 插件不能完全动态更新,可接Tinker热修复
  • 每次升级宿主,都有重新下载插件

总结

优点

  • 可以无缝切换国内和国外发布
  • 站在Google肩膀上,稳定且实现简单

缺点

  • 插件和宿主一起打包,每次更新宿主,插件都要重新下载
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容