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个。

插件化的原理

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

  • 通过DexClassLoader加载。
  • 代理模式添加生命周期。
  • 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());

}

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

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的启动流程。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值