Android插件化技术入门

插件化概述

提到插件化,就不得不提起方法数超过65535的问题,我们可以通过Dex分包来解决,同时也可以通过使用插件化开发来解决。插件化的概念就是由宿主APP去加载以及运行插件APP。

下面是一些插件化的优势:

  • 在一个大的项目里面,为了明确的分工,往往不同的团队负责不同的插件APP,这样分工更加明确。
  • 各个模块封装成不同的插件APK,不同模块可以单独编译,提高了开发效率。
  • 解决了上述的方法数超过限制的问题。
  • 可以通过上线新的插件来解决线上的BUG,达到“热修复”的效果。
  • 减小了宿主APK的体积。

下面是插件化开发的缺点:

  • 插件化开发的APP不能在Google Play上线,也就是没有海外市场。

综上所述,如果您的APP不需要支持海外的话,还是可以考虑插件化开发的。

插件化、热修复(思想)的发展历程

  • 2012年7月,AndroidDynamicLoader,大众点评,陶毅敏:思想是通过Fragment以及schema的方式实现的,这是一种可行的技术方案,但是还有限制太多,这意味这你的activity必须通过Fragment去实现,这在activity跳转和灵活性上有一定的不便,在实际的使用中会有一些很奇怪的bug不好解决,总之,这还是一种不是特别完备的动态加载技术。
  • 2013年,23Code,自定义控件的动态下载:主要利用 Java ClassLoader 的原理,可动态加载的内容包括 apk、dex、jar等。
  • 2014年初,Altas,阿里伯奎的技术分享:提出了插件化的思想以及一些思考的问题,相关资料比较少。
  • 2014年底,Dynamic-load-apk,任玉刚:动态加载APK,通过Activity代理的方式给插件Activity添加生命周期。
  • 2015年4月,OpenAltas/ACCD:Altas的开源项目,一款强大的Android非代理动态部署框架,目前已经处于稳定状态。
  • 2015年8月,DroidPlugin,360的张勇:DroidPlugin 是360手机助手在 Android 系统上实现了一种新的插件机制:通过Hook思想来实现,它可以在无需安装、修改的情况下运行APK文件,此机制对改进大型APP的架构,实现多团队协作开发具有一定的好处。
  • 2015年9月,AndFix,阿里:通过NDK的Hook来实现热修复。
  • 2015年11月,Nuwa,大众点评:通过dex分包方案实现热修复。
  • 2015年底,Small,林光亮:打通了宿主与插件之间的资源与代码共享。
  • 2016年4月,ZeusPlugin,掌阅:ZeusPlugin最大特点是:简单易懂,核心类只有6个,类总数只有13个。

下面是插件化框架的一些对比,下面引用https://github.com/wequick/Small/blob/master/Android/COMPARISION.md

插件化框架对比.png
插件化框架对比.png
[1] 独立插件:一个完整的apk包,可以独立运行。比如从你的程序跑起淘宝、QQ,但这加载起来是要闹哪样?
     非独立插件:依赖于宿主,宿主是个壳,插件可使用其资源代码并分离之以最小化,这才是业务需要嘛。
     -- “所有不能加载非独立插件的插件化框架都是耍流氓”。
[2] ACDD加载.so用了Native方法(libdexopt.so),不是Java层,源码见dexopt.cpp。
[3] Service更新频度低,可预先注册在宿主的manifest中,如果没有很好的理由说服我,现不支持。
[4] 要实现宿主、各个插件资源可互相访问,需要对他们的资源进行分段处理以避免冲突。
[5] 这些框架修改aapt源码、重编、覆盖SDK Manager下载的aapt,我只想说_“杀(wan)鸡(de)焉(kai)用(xin)牛(jiu)刀(hao)”。Small使用gradle-small-plugin,在后期修改二进制文件,实现了PP_段分区。
[6] 使用public-padding对资源id的_TT_段进行分区,分开了宿主和插件。但是插件之间无法分段。
[7] 除了宿主提供一些公共资源与代码外,我们仍需封装一些业务层面的公共库,这些库被其他插件所依赖。公共插件打包的目的就是可以单独更新公共库插件,并且相关插件不需要动到。
[8] AppCompat: Android Studio默认添加的主题包,Google主推的Metrial Design包也依赖于此。大势所趋。
[9] 联调插件:使用Android Studio调试宿主时,可直接在插件代码中添加断点调试。
插件化框架对比.png

插件化的原理

通过上面的框架介绍,插件化的原理无非就是这些:

  1. 通过DexClassLoader加载。
  2. 代理模式添加生命周期。
  3. Hook思想跳过清单验证。

插件化需要掌握一些系统底层的知识,比如说IPC,Android系统、APP、四大组件的启动过程,APK的安装过程。

插件化实战体验

通过DexClassLoader加载这个插件APK

下面写一个简单的例子,仅起到抛砖引玉的作用。

首先我们需要有一个插件APK,我们在里面放入一个类:

package com.nan.plugin;

/**
 * Created by huannan on 2017/6/20.
 */

public class Bean {

    private String name = "璐宝宝";

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

然后在宿主APP里面,通过DexClassLoader加载这个插件APK,并且通过反射实例化Bean并调用Bean的方法。

public class MainActivity extends AppCompatActivity {

    private ClassLoader mPluginClassLoader;

    @Override
    protected void attachBaseContext(Context newBase) {
        super.attachBaseContext(newBase);

        try {
            //把Assets里面的文件复制到 /data/data/包名/files 目录下
            //注意:不同手机厂商可能目录不一样
            Utils.extractAssets(newBase, "plugin-debug.apk");
        } catch (Throwable throwable) {

        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //插件APK路径
        //  /data/user/0/com.nan.dynalmic/files/plugin-debug.apk
        String dexPath = getFileStreamPath("plugin-debug.apk").getAbsolutePath();
        //DexClassLoader加载的时候Dex文件释放的路径
        //  /data/user/0/com.nan.dynalmic/app_dex
        String fileReleasePath = getDir("dex", Context.MODE_PRIVATE).getAbsolutePath();

        Log.e("acy", dexPath);
        Log.e("acy", fileReleasePath);

        //通过DexClassLoader加载插件APK
        mPluginClassLoader = new DexClassLoader(dexPath, fileReleasePath, null, getClassLoader());

        //通过反射调用插件的代码
        findViewById(R.id.btn_1).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                try {
                    Class<?> beanClass = mPluginClassLoader.loadClass("com.nan.plugin.Bean");
                    Object beanObject = beanClass.newInstance();

                    Method setNameMethod = beanClass.getMethod("setName", String.class);
                    setNameMethod.setAccessible(true);
                    Method getNameMethod = beanClass.getMethod("getName");
                    getNameMethod.setAccessible(true);

                    setNameMethod.invoke(beanObject, "huannan");
                    String name = (String) getNameMethod.invoke(beanObject);

                    Toast.makeText(MainActivity.this, name, Toast.LENGTH_SHORT).show();

                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

这里需要注意的一点就是,我们最好先把经过验证的插件APK复制到宿主APP的files目录下面,这样保证了APK的安全性。然后通过DexClassLoader进行加载的时候,需要指定插件APK的路径以及解压之后的dex存放路径。

通过面向接口(抽象)编程调用插件的代码

上文介绍了通过反射调用插件的代码,为了简化代码提高可读性,这里引入面向接口(抽象)编程的思想。

首先我们需要添加一个pluginlibrary,我们的app以及plugin模块都要引用这个库pluginlibrary,如下图所示:

项目架构

可以看到,我们在pluginlibrary里面添加了IBean接口:

public interface IBean {

    String getName();

    void setName(String name);

}

然后plugin里面的Bean类实现这个接口,最后在宿主加载的时候,直接把创建的对象转换为这个接口就可以,省去了反射的一系列繁琐操作,这也就是一种面向接口(抽象)编程的思想:

//通过面向接口编程调用插件的代码

Class<?> beanClass = mPluginClassLoader.loadClass("com.nan.plugin.Bean");
IBean bean = (IBean) beanClass.newInstance();

bean.setName("test");
Toast.makeText(MainActivity.this, bean.getName(), Toast.LENGTH_SHORT).show();

通过面向切面程调用插件中的带回调方法

比如说现在插件里面有一个方法methodWithCallback,它被调用的时候,最终会回调宿主APP。

先在pluginlibrary添加一个接口专门用于宿主与插件的交互的:

public interface IDynamic {

    void methodWithCallback(Callback callback);

}

其中的Callback是自定义的一个简单的接口:

public interface Callback {

    void callback(IBean bean);

}

这个IDynamic的实现类由插件来实现:

public class Dynamic implements IDynamic {

    @Override
    public void methodWithCallback(Callback callback) {
        Bean bean = new Bean();
        bean.setName("璐宝宝");

        //回调宿主APP的方法
        callback.callback(bean);
    }

}

这样我们就可以通过回调的方式实现了插件调用宿主的方法了。最终宿主的调用如下:

Class<?> dynamicClass = mPluginClassLoader.loadClass("com.nan.plugin.Dynamic");
IDynamic dynamic = (IDynamic) dynamicClass.newInstance();

dynamic.methodWithCallback(new Callback() {
    @Override
    public void callback(IBean bean) {
        //插件回调宿主
        Toast.makeText(MainActivity.this, bean.getName(), Toast.LENGTH_SHORT).show();
    }
});

宿主访问插件的资源文件

如果我们直接去加载插件的资源的话,就会报如下错误:

android.content.res.Resources$NotFoundException: String resource ID #0x7f060022

因为插件的资源没有被Android系统加载进来,那么我们就需要手动加载资源,主要是重写下面三个方法:

@Override
public AssetManager getAssets() {
    return mAssetManager == null ? super.getAssets() : mAssetManager;
}

@Override
public Resources getResources() {
    return mResources == null ? super.getResources() : mResources;
}

@Override
public Resources.Theme getTheme() {
    return mTheme == null ? super.getTheme() : mTheme;
}

然后在合适的时机调用loadPluginResources方法来加载插件的资源:

/**
 * 加载插件的资源:通过AssetManager添加插件的APK资源路径
 */
protected void loadPluginResources() {
    //反射加载资源
    try {
        AssetManager assetManager = AssetManager.class.newInstance();
        Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
        addAssetPath.invoke(assetManager, mDexPath);
        mAssetManager = assetManager;
    } catch (Exception e) {
        e.printStackTrace();
    }
    Resources superRes = super.getResources();
    mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
    mTheme = mResources.newTheme();
    mTheme.setTo(super.getTheme());

}
注:有关Android资源加载机制的可以参考《Android源码与设计模式》这本书。

最后,我们就可以访问到插件的资源了(这里只给出核心代码):

Class<?> dynamicClass = mPluginClassLoader.loadClass("com.nan.plugin.Dynamic");
IDynamic dynamic = (IDynamic) dynamicClass.newInstance();

String res = dynamic.methodWithResources(MainActivity.this);
Log.e(TAG, res);

无需验证启动Activity

我们可以利用Hook机制来启动一个没有在清单文件中注册的插件Activity。但是需要我们熟悉Activity的启动流程。

相关的文章:http://www.jianshu.com/p/69bfbda302df

写在最后

通过上面的例子我们体验了插件式开发的精髓,学习这些例子是为了更好的研究市面上的插件化框架,了解它们实现原理,明白这些框架对项目以及插件的侵入性、修改这些框架以适应自己的项目等。当然有兴趣的可以自己做一个。

在学习插件化的时候,需要掌握Android系统的一些Framework层面的知识以及一些编程相关的知识,其中包括:

  • Binder机制
  • Android系统、APP、Activity等四大组件的启动流程
  • APK安装过程
  • Android资源的加载过程
  • Hook机制
  • 面向接口(抽象)编程
  • 面向切面编程
  • 等等

相关的参考资料有:

如果觉得我的文字对你有所帮助的话,欢迎关注我的公众号:

公众号:Android开发进阶

我的群欢迎大家进来探讨各种技术与非技术的话题,有兴趣的朋友们加我私人微信huannan88,我拉你进群交(♂)流(♀)

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

推荐阅读更多精彩内容