Android插件化之旅

一、概述

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容