美文网首页Android 插件式Android
搞懂插件化,看这一篇就够了

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

作者: 尹学姐 | 来源:发表于2023-02-21 21:29 被阅读0次

    背景

    历史和现状

    发展历史

    • 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肩膀上,稳定且实现简单

    缺点

    • 插件和宿主一起打包,每次更新宿主,插件都要重新下载

    相关文章

      网友评论

        本文标题:搞懂插件化,看这一篇就够了

        本文链接:https://www.haomeiwen.com/subject/xpeokdtx.html