之前接触小程序引擎的开发,对于应用安装包轻量化这个概念一直 耿耿于怀。相对于性能以及表现更加突出的原生安卓,为什么不能做成和小程序一样的,只在我需要的时候,才把功能包下载到本地并且呈现出来?毕竟相对来说,这种做法:
- 安装的主apk包会小好多
- 给开发者提供了业务功能扩展,并且不需要用户进行更新
- 在非主apk包中的功能出现BUG时,可以及时修复
- 用户不需要的功能,完全就不会出现在系统里面,减轻设备的负担
极端情况就是,主apk就只有一个套壳Activity,剩下的,全靠线上扩展。(过分了,和谐和谐)
慢慢的,就接触到了插件化这么个概念。要说这个概念提出,好像也是几年前了,论技术,也算是老技术了。于是乎,本着学习研究的态度,把前辈的路再走一遍吧。
回归正题,讲到这个插件化,得先熟悉一个东西 - ClassLoader
。
一、插件化开发的前提 - ClassLoader
如上图所示,将一个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。 —— 鲁迅
三、插件化开发的实践
理论感觉差不多了,下面就开始实践吧。
首先,建立如下图所示的工程结构:
其中,app
就是主application工程;base
是存放基类Activity以及一些基础库的工程;bindview
大家不用管,这是方便开发做的,类似于butterknife的框架;part1
和part2
就是两个插件化工程。目的就是在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真正被加载的时候,使用这里的AssetManager
、Resources
等去加载资源,否则会找不到插件化工程中的资源。
再来看一下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)
。
运行一下,大功告成。
至此,插件化开发的要点,基本就差不多了。
当然,这个只是demo,再完善一下,比如动态化配置插件应用等等。
完整的代码,请移步我的Github
彩蛋: 这种方式也是有其局限性,因为在插件化的工程里面,要求Activity
必须要继承RemoteActivity,这对于有些特殊的业务逻辑或者是其他有基类Activity
的框架来说,可能就行不通了。并且对于RemoteActivity这个代理类来说,他是以一个伪Activity
的身份而存在,不是通过ActivityThread
真正创建出来的Activity
,其必然会在某些场景受限。