Android插件化开发 - 动态加载

之前接触小程序引擎的开发,对于应用安装包轻量化这个概念一直 耿耿于怀。相对于性能以及表现更加突出的原生安卓,为什么不能做成和小程序一样的,只在我需要的时候,才把功能包下载到本地并且呈现出来?毕竟相对来说,这种做法:

  • 安装的主apk包会小好多
  • 给开发者提供了业务功能扩展,并且不需要用户进行更新
  • 在非主apk包中的功能出现BUG时,可以及时修复
  • 用户不需要的功能,完全就不会出现在系统里面,减轻设备的负担

极端情况就是,主apk就只有一个套壳Activity,剩下的,全靠线上扩展。(过分了,和谐和谐)

慢慢的,就接触到了插件化这么个概念。要说这个概念提出,好像也是几年前了,论技术,也算是老技术了。于是乎,本着学习研究的态度,把前辈的路再走一遍吧。

回归正题,讲到这个插件化,得先熟悉一个东西 - ClassLoader

一、插件化开发的前提 - ClassLoader

Decomile
如上图所示,将一个apk反编译出来,可以看到,所有的类都被打包到一个一个的dex中。那么,是不是有办法,将这些类解析出来,然后作为目标Activity拉起来呢?好,下面就有请本段的主角 - ClassLoader

    大家好,我叫“ClassLoader”,我的作用,通俗点讲,就是将字节码文件(.class)加载到JVM(Java虚拟机)中。也就是这种方式,才使得甲骨文宣称的:

Write once, run anywhere

万能的谷歌,自然也不会想不到这点,于是给开发者提供了专门的加载Dex包的类 - DexClassLoader。关于DexClassLoader的用法,这边就不需要赘述了,网上多得是,并且可以参考Android Developer - DexClassLoader

二、插件化开发的优化 - 代理模式

如果,所有的插件apk中,都是以Fragment作为界面单元,那么可以忽略这部分内容。如果打算和常规application一样,使用Activity作为界面单元,那么,你就需要了解代理模式了。
下面,我分两点说明一下,为什么需要这个代理模式:

1. Manifest注册的问题

由于插件化包中的Activity类不可能存在于主apk包中,那么,主apk包中的AndroidManifest必然不会注册这个Activity。有人问,我注册一个不就好了?没错,你注册一个是好了,但是这是不是符合插件化的初衷?难道说,每新增一个Activity,就要去更新一下主apk包中的Manifest,然后让用户更新吗?不存在的~

2. Activity上下文等问题

先抛开Manifest这个问题,还有一个需要去考虑的问题:通过DexClassLoader加载的类,不过是一个Java的普通类,他不存在像正常Activity一样的上下文对象以及生命周期等等。

上述两个问题,就像是两把刀,直接把Activity杀死了。那么,如何让这个Activity重新活起来?
来,一起请出今天的第二位嘉宾,代理模式:

    我是代理模式,我只会去处理核心的事情,比如演唱会的时候上台唱歌,而不会去和运营公司打交道,而这些事情,我都习惯交给我的经纪人去做。

听完他的自我介绍,感觉好像有了一点思路:如果,在主工程里面建立一个套壳Activity(以下称之为“HostActivity”),里面基本不干啥事,然后每一个插件化工程里面使用代理的Activity(以下使用“RemoteActivity”代指),其中,HostActivity中的业务逻辑代理给RemoteActivity去做,而我们最终加载的是HostActivity,这样不就行了吗?既获得到了HostActivity中的上下文对象和生命周期等正常Activity所拥有的东西,也能够运行到RemoteActivity中想要的布局以及业务逻辑。

呼,这么说来,感觉自己就要救活这个身上插着两把刀的Activity了。

学医救不了中国人,但是可以救活Activity。 —— 鲁迅

三、插件化开发的实践

理论感觉差不多了,下面就开始实践吧。
首先,建立如下图所示的工程结构:
ProjectStructure
其中,app就是主application工程;base是存放基类Activity以及一些基础库的工程;bindview大家不用管,这是方便开发做的,类似于butterknife的框架;part1part2就是两个插件化工程。目的就是在app中加载这两个未安装的apk。

主界面代码如下所示,就是两个按钮,分别加载两个apk。

@RootView(R.layout.activity_main)
public class MainActivity extends AbsBaseActivity {
    @OnClick(R.id.loaderPart1)
    public void loadPart1() {
        final String apkName = "part1.apk";
        final String hostClass = "com.joelzhu.part1.MainActivity";

        startHostActivity(apkName, hostClass);
    }

    @OnClick(R.id.loaderPart2)
    public void loadPart2() {
        final String apkName = "part2.apk";
        final String hostClass = "com.joelzhu.part2.MainActivity";

        startHostActivity(apkName, hostClass);
    }

    private void startHostActivity(final String apkName, final String hostClass) {
        Intent intent = new Intent(this, HostActivity.class);
        intent.putExtra(IntentKey.APK_NAME, apkName);
        intent.putExtra(IntentKey.HOME_CLASS, hostClass);
        startActivity(intent);
    }
}

加载apk包的代码如下所示:

private void loadApk(final String homeClassName, final String apkPath) {
    // Create AssetManager.
    try {
        assetManager = AssetManager.class.newInstance();
        Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
        addAssetPath.invoke(assetManager, apkPath);
    } catch (IllegalAccessException | InstantiationException | NoSuchMethodException | InvocationTargetException e) {
        LogUtil.e(TAG, "Create AssetManager failed.");
        LogUtil.e(TAG, e.getMessage());
        return;
    }

    // Create Resources.
    final DisplayMetrics displayMetrics = super.getResources().getDisplayMetrics();
    final Configuration configuration = super.getResources().getConfiguration();
    resources = new Resources(assetManager, displayMetrics, configuration);

    // Create loader to load the entrance in uninstalled-apk.
    DexClassLoader loader = new DexClassLoader(apkPath, null, null, getClassLoader());
    try {
        Class homeClass = loader.loadClass(homeClassName);
        if (AbsRemoteActivity.class.isAssignableFrom(homeClass)) {
            remoteActivity = (AbsRemoteActivity) homeClass.newInstance();
        } else {
            LogUtil.e(TAG, String.format("Illegal home activity, %s", homeClass.getSimpleName()));
        }
    } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
        LogUtil.e(TAG, "Create instance of home activity failed.");
        LogUtil.e(TAG, e.getMessage());
    }
}

值得注意的是,在RemoteActivity被加载之前,通过反射的方式,调用AssetManager中的addAssetPath方法,将apk中的资源加载到Resources对象中,然后当RemoteActivity真正被加载的时候,使用这里的AssetManagerResources等去加载资源,否则会找不到插件化工程中的资源。

再来看一下RemoteActivity的基类:

public abstract class AbsRemoteActivity extends AbsBaseActivity {
    private Activity hostActivity;

    @Override
    public void setContentView(int layoutResID) {
        hostActivity.setContentView(layoutResID);
    }

    @Override
    public Window getWindow() {
        return hostActivity.getWindow();
    }

    @Override
    public <T extends View> T findViewById(int id) {
        return hostActivity.findViewById(id);
    }

    public void attachActivity(Activity activity) {
        this.hostActivity = activity;
    }
}

在HostActivity被加载的时候,调用attachActivity方法,就能将HostActivity中存在的Window对象等有用的内容,保存到RemoteActivity中。

最后,在HostActivity的生命周期中,委托RemoteActivity去做就好了:

@Override
public void createActivity() {
    if (remoteActivity != null) {
        remoteActivity.createActivity();
    }
}

@Override
public void resumeActivity() {
    if (remoteActivity != null) {
        remoteActivity.resumeActivity();
    }
}

@Override
public void startActivity() {
    if (remoteActivity != null) {
        remoteActivity.startActivity();
    }
}

......

然后,将打包出来的两个插件化apk推到手机中,存放路径完全可以自定义,不过我一般喜欢放到应用目录中,getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
运行一下,大功告成。
Result
至此,插件化开发的要点,基本就差不多了。
当然,这个只是demo,再完善一下,比如动态化配置插件应用等等。

完整的代码,请移步我的Github

彩蛋: 这种方式也是有其局限性,因为在插件化的工程里面,要求Activity必须要继承RemoteActivity,这对于有些特殊的业务逻辑或者是其他有基类Activity的框架来说,可能就行不通了。并且对于RemoteActivity这个代理类来说,他是以一个伪Activity的身份而存在,不是通过ActivityThread真正创建出来的Activity,其必然会在某些场景受限。

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值