Android插件化之旅

作者: CKTim | 来源:发表于2018-10-30 15:58 被阅读12次

    一、概述

    Android插件化技术一直是安卓开发中一个重要的方向,大概12年就被提出,发展至今已逐渐趋于成熟,很多大厂都有自己的一套插件化方案,诸如淘宝的Atlas,滴滴的VirtualAPK,360的RePlugin等。插件化技术的发展得益于业务的不断新增,诸如淘宝APP,里面有聚划算,拍卖,饿了么,淘票票等业务功能模块(这里只考虑原生界面),如果今天饿了么有个Bug要修复发版,明天淘票票想加多个功能,是否每次都需要去更新淘宝客户端?这个代价未免太大,同时,作为淘宝的开发人员,我是否还需要帮忙去维护饿了么的第三方业务代码?而作为饿了么开发人员,我自己又要维护自己客户端的代码,又要维护在你淘宝上的代码吗?在这种拥有众多业务的大厂里,插件化技术就应运而生。

    二、概念区分

    近年来,除了插件化技术,组件化技术,热修复等也同样广受关注,这里主要做一下概念的区分:
    插件化:也叫动态加载技术,分宿主APK和插件APK,宿主APK可以理解为就是安装到手机的主APK(诸如手机淘宝),各个功能模块抽取变成插件APK(诸如饿了么,淘票票),这些插件APK可以随着宿主APK一起编译打包安装到手机上,也可以变成远程APK放在服务器,按需下载安装,实现功能的动态配置。从广义上理解,可以把Android系统当成一个宿主APK,各个安装到手机上的软件当成插件APK,从而组成一个插件化系统。
    组件化:组件化技术实现了在Debug调试阶段,每个功能模块可以独立变成APP调试,但在打包编译阶段,其最终还是将所有模块打包成一个APK。
    热修复:热修复技术有助于我们在用户无感知的时候修复APK,悄无声息的将Bug修复掉,我们希望热修复它是不新增资源文件,四大组件等操作,只是单纯的解决代码逻辑上的Bug,可以简单理解插件化技术是热修复的高级版

    三、插件化的优缺点

    优点

    • 让用户无需安装APK就能升级应用功能,减少发版频率,增加用户体验
    • 按需编译加载,有效减小主APK体积,实现功能的灵活配置
    • 模块化,降低耦合性,有利于多人合作开发同一个项目

    缺点

    • Android上的黑科技越来越不被Android新系统待见,诸如Android 9.0系统已禁止非 SDK 接口的调用,而插件化技术中又或多或少使用了一些反射。这会使得插件化技术在新系统的表现上存在一些欠缺。
    • 项目的构建过程变得复杂

    四、插件化技术中的两个主要问题

    正常情况下,apk被安装后,apk里面的代码和资源会被存放到系统的某处,以便系统能找到它。而插件APK未被安装,系统是找不到它里面的代码和资源的,所以如何加载插件APK中的代码和资源就成为了主要问题。针对这两个问题,下面主要介绍一种经典思路,达到抛砖引玉,有助于我们对插件化有个更好认识

    如何加载插件APK中的Java代码?

    Android中两个主要的Classloader,PathClassLoader和DexClassLoader,都是继承自BaseDexClassLoader:
    DexClassLoader:可以加载包含classes.dex实体的.jar或.apk文件
    PathClassLoader:只能加载已安装APK的dex文件
    显然DexClassLoader可以满足我们插件化中对Java代码的动态加载,如下代码所示可以通过传入APK路径获取相应的DexClassLoader,接着通过调用DexClassLoader的loadClass方法获取相应的类实例:

    //dexPath传入当前插件APK在SD卡中的路径
    DexClassLoader pluginDexClassLoader = new DexClassLoader(dexPath, context.getDir("dex", Context.MODE_PRIVATE).getAbsolutePath(), null, context.getClassLoader());
    //根据类名获取字节码对象
    Class<?> mClass=pluginDexClassLoader loadClass("这里传入需要加载的完整路径类名");
    //通过字节码对象创建类的实例
    Object newInstance = mClass.newInstance();
    

    类的实例可以通过上述拿到,然而这又会出现另外一个问题:已知Android系统中Activity页面的生命周期是由系统控制的,如果单纯使用DexClassLoader加载插件APK中的Activity,加载出来的也只是一个普通的对象,不具备页面的生命周期,曾看到过一个很生动的比喻:如果说系统创建的Activity是一个拥有四肢能动能跳的人的话,那么我们手动创建的Activity只是一个人偶,这个人偶虽然也有四肢,但是他动不了,因为他没有对应的掌控者。
    针对这个问题,可以使用代理来实现,就如为了让这个木偶动起来,可以将这个木偶绑到活人身上,当活人动的时候,木偶也能跟着动。

    具体的思路
    如何使用代理模式?可以先在宿主APK中注册好一个空的代理Activity页面,这个代理Activity拥有正常的生命周期,然后将插件Activity代理Activity绑定起来,当代理Activity触发某一个生命周期的时候,也去通知插件Activity,让插件Activity拥有一个伪生命周期。
    之前人们的采用的方法是使用反射去管理代理Activity的生命周期,但这样存在一些不便,比如反射代码写起来复杂,并且过多使用反射有一定的性能开销,后来采用了一种更为优雅的方式,就是采用接口机制,将代理Activity的生命周期提取出来作为一个接口,暂命名为PluginInterface,然后让插件Activity实现他:

    public interface PluginInterface {
    
        void onCreate(Bundle saveInstance);
    
        void attachContext(Activity context);
    
        void onStart();
    
        void onResume();
    
        void onRestart();
    
        void onDestroy();
    
        void onStop();
    
        void onPause();
    }
    

    接着回到代理Activity,第一步,当调用插件Activity的时候,实际是调用了代理Activity,在代理Activity的onCreate生命周期里,使用之前说的加载类的方法创建插件Activity类实例,然后在代理Activity的各个生命周期动态的调用插件Activity的伪生命周期,以此达到同步效果,代理Activity的具体代码如下:

    public class ProxyActivity extends Activity {
    
        private PluginInterface pluginInterface;
    
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            //拿到要启动的Activity
            String className = getIntent().getStringExtra("className");
            try {
                //加载该Activity的字节码对象
                Class<?> aClass = PluginManager.getInstance().getPluginDexClassLoader().loadClass(className);
                //创建该Activity的示例
                Object newInstance = aClass.newInstance();
                //面向接口编程,插件Activity需要实现PluginInterface接口
                if (newInstance instanceof PluginInterface) {
                    pluginInterface = (PluginInterface) newInstance;
                    //将代理Activity的实例传递给插件Activity,以此让插件APK用于宿主的上下文
                    pluginInterface.attachContext(this);
                    //创建bundle用来与插件apk传输数据
                    Bundle bundle = new Bundle();
                    //将当前生命周期同步给插件Activity
                    pluginInterface.onCreate(bundle);
                }
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    
        @Override
        public void onStart() {
            pluginInterface.onStart();
            super.onStart();
        }
    
        @Override
        public void onResume() {
            pluginInterface.onResume();
            super.onResume();
        }
    
        @Override
        public void onRestart() {
            pluginInterface.onRestart();
            super.onRestart();
        }
    
        @Override
        public void onDestroy() {
            pluginInterface.onDestroy();
            super.onDestroy();
        }
    
        @Override
        public void onStop() {
            pluginInterface.onStop();
            super.onStop();
        }
    
        @Override
        public void onPause() {
            pluginInterface.onPause();
            super.onPause();
        }
    
       /**
         * 在插件APK中,插件Activity调起其本身的Activity,实际还是一直调用代理Activity,不断重复上述流程
         */
        @Override
        public void startActivity(Intent intent) {
            Intent newIntent = new Intent(this, ProxyActivity.class);
            newIntent.putExtra("className", intent.getComponent().getClassName());
            super.startActivity(newIntent);
        }
    }
    

    如何加载插件APK中的资源文件?

    宿主APK中是没有插件APK中的资源的,如果在代理Activity中直接像平时一样使用R.来引用插件APK中的资源的话是会报错的。Activity中有两个系统方法是和加载资源有关,我们需要在代理Activity中重写这两个方法,返回相应插件APK的Resource对象,这样才能顺利引用插件APK中的资源。

    /** Return an AssetManager instance for your application's package. */
    public abstract AssetManager getAssets();
    /** Return a Resources instance for your application's package. */
    public abstract Resources getResources();
    

    AssetManager 中有一个addAssetPath方法,该方法可以通过传入指定的APK路径然后获取该APK的AssetManager,但这个方法是一个隐藏方法,需要通过反射来获取,紧接着将获取到的AssetManager传入Resources构造方法中,以此拿到相应插件APK中的Resources对象,示例代码如下:

     //dexPath是Plugin的路径,
    //optimizedDirectory是Plugin的缓存路径,
    //libraryPath可以为null,
    //parent为父类加载器
     pluginDexClassLoader = new DexClassLoader(dexPath, context.getDir("dex", Context.MODE_PRIVATE).getAbsolutePath(), null, context.getClassLoader());
     pluginPackageArchiveInfo = context.getPackageManager().getPackageArchiveInfo(dexPath, PackageManager.GET_ACTIVITIES);
     {
          AssetManager assets = null;
           try {
               assets = AssetManager.class.newInstance();
               Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
               addAssetPath.invoke(assets, dexPath);
           } catch (InstantiationException e) {
               e.printStackTrace();
           } catch (IllegalAccessException e) {
               e.printStackTrace();
           } catch (NoSuchMethodException e) {
               e.printStackTrace();
           } catch (InvocationTargetException e) {
               e.printStackTrace();
           }
          pluginResources = new Resources(assets, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());
    

    接下来重写代理Activity中的getResources()方法,返回刚才新创建的Resources方法

    /**
     * 注意:三方调用拿到对应加载的三方Resources
     * @return
     */
    @Override
    public Resources getResources() {
        return pluginResources;
    }
    

    五、市场上的插件化框架

    名称 团队 Github
    DroidPlugin 奇虎360 DroidPlugin
    PluginManager 个人开发者 PluginManager
    AndroidDynamicLoader 个人开发者 AndroidDynamicLoader
    dynamic-load-apk 任玉刚 dynamic-load-apk
    Small 开源组织Wequick Small
    DynamicAPK 携程 DynamicAPK
    VirtualAPK 滴滴 VirtualAPK
    RePlugin 奇虎360 RePlugin
    Atlas 手机淘宝 Atlas

    其中任玉刚的dynamic-load-apk插件化框架就是采用了上述所说的代理思路,上诉有些框架已经很久没有维护了,现在比较热门且还在维护的应属360的RePlugin,嘀嘀的VirtualAPK,手机淘宝的Atlas以及Small框架,其中Small框架支持Android和ios,较为轻量,但似乎还没办法做到按需加载。而淘宝Atlas框架相比其他具有更丰富的功能,除了可以按需加载相应的功能模块外,还具备热修复功能。

    六、是否使用插件化技术的思考:

    • 是否存在版本较多需要不断更新发版的情况?
    • 是否有较多的业务模块?
    • 是否开发人员众多?
    • .....

    Demo地址:https://github.com/CKTim/DynamicAPKTest

    相关文章

      网友评论

      本文标题:Android插件化之旅

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