序言
随着应用在开发到了一定阶段以后,功能模块越来越多,代码越来越庞大,很难维护,项目中某一段代码牵涉模块越来越多,应对bug反应越来越慢;需求越来越多,某一模块的小改动都要重新发布版本,发布时间越来越不可控;APK安装包越来越大;用户在使用过程中没有办法选择性的加载自己需要的功能模块等很多难以突破的问题,这个时候可选的解决方案总结就下面三个:
1.用Html5代替部分逻辑
2.删除无用代码,减少方法数
3.插件化,将一个 App 划分为多个插件(Apk 或相关格式)
然而前两种方法在某种条件下可以解决问题,但是治标不治本,也正因如此插件化技术成为了多模块应用的开发必备技能。
TPF是集业界大部分插件框架所长,又在支持能力以及性能上优于业界大部分插件框架。
一、基于插件化技术架构的优势
- 解除单个dex函数不能超过 65535的限制
- 模块解耦
- 动态更新升级
- 高效开发(编译速度更快)
- 提高并行开发效率
1. 应用程序非常容易扩展,比如一个新的领域要加到旧的应用程序中,只需把这个新的领域做为一个插件,只开发这个小的app就可以了,旧的应用程序可能会原分不动,就连编译打包都不需要。
2. 下载更新特别省流量,假如一个应用程序有10M把它分成两个的话可能每次更新只需要花费5M或者更少的流量就可以更新完。
- 其他(按需加载、…)
二、TPF插件框架已支持的功能
1、插件包无需安装,由宿主程序动态加载运行。
2、支持fragment、activity(包括4个LaunchMode)、service、receiver、contentprovider、so、application、notification。
3、支持插件自定义控件。
4、开发插件apk对齐原生apk,相比原生apk开发无门槛。
5、插件中的组件拥有真正生命周期,完全交由系统管理、非反射代理。
6、支持插件共享宿主的代码和资源。
7、宿主 - ShareLib提供一套完整的控件库供插件开发者使用。
8、支持插件使用宿主主题、系统主题、插件自身主题以及style。
9、支持非独立插件和独立插件。
10、支持插件资源文件中直接通过@方式引用宿主共享资源
11、支持插件热更新。
12、支持第三方应用启动插件activity组件
13、支持插件service配置独立进程
三、宏观角度分析插件技术
基于插件框架的插件包其实也是一个apk包,基于这一点我们先来分析一下普通apk包内容结构:
1)assets目录:存放需要打包到APK中的静态文件,和res的不同点在于assets目录资源不会被编译
2) lib目录:存放应用程序依赖的native库文件
3) res目录:存放应用程序资源
4) META-INF目录:保存应用的签名信息
5) AndroidManifest.xml:应用程序配置文件
6) classes.dex 可执行文件
7) resources.arsc:资源配置文件,用来记录资源文件和资源ID之间的映射关系
从上面的内容结构可以知道,Android系统在处理apk[安装、运行、卸载]也就只处理三部分内容:解析程序配置文件、程序代码加载、程序资源加载,来看一下系统处理用于的示意图:
应用程序的安装、运行、卸载都是有AndroidOS[framework层]来承载,而插件应用则是由宿主提供的插件框架来承载,来看一下示意图:
因此,应用开发技术是插件技术的一个子集,插件技术除普通应用开发必备的知识外还需要具备对Android framework层了解,插件化技术与应用程序开发技术知识领域关系如下:
[插件技术&应用程序开发技术知识领域关系图]
从上面的关系图可以看出插件化技术和普通应用的开发多了一层对Android framework的了解掌握,也正是茶技术需要对Android framework的掌握使得插件化技术门槛高了不少。插件化技术听起来高深莫测,实际上要解决的也就三个问题[和AndroidOS处理应用程序一样]:
1. AndroidManifest解析
2. 代码加载
3. 资源加载
在TPF框架里面插件包就是一个“应用程序”,下面我们从这三个方向来解刨插件框架如何处理[插件应用程序]的。
1、AndroidManifest.xml解析
插件应用程的配置文件AndroidManifest.xml,和普通应用程序一样,该配置文件保存了程序的版本、SdkVersion、application、组件等信息,解析AndroidManifest.xml其实就是手机应用程序信息的过程。
2、代码加载
类的加载可以使用Java的ClassLoader机制:
但是对于Android来说,并不是说类加载进来就可以用了,很多组件都是有“生命”的;因此对于这些有血有肉的类,必须给它们注入活力,也就是所谓的组件生命周期管理(这个后面有内容单独讲解)
3、资源加载
资源加载方案使用的是AssetManager的隐藏方法addAssetPath。
但是,不同插件的资源如何管理是各个插件框架必须经历的纠结也必须做出二选一的决定:是公用一套资源?还是插件独立资源?如果是共用资源,得考虑如何避免资源冲突?
对于资源加载,有的方案共用一套资源并采用资源分段机制解决冲突(要么修改aapt要么添加编译插件、要么通过public.xml的public-padding来处理冲突);有的方案选择独立资源,不同插件管理自己的资源。
这种抉择由项目的性质来决定回好些,比如桌面选择独立资源、DeviceManager采用宿主和插件共用一套资源、...
四、插件框架的三个重要流程
- 插件框架的加载流程:
TPF框架加载的重点在维护支持组件或者功能的生命周期上,插件的构建和启动都得基于这一套的基础上:
- 插件应用的安装流程:
TPF在安装插件的过程中负责三件事,第一件:将插件包安全的进行合法校验,第二件:解析得到插件应用信息,第三件:拷贝资源文件到私有目录。
- 插件应用的启动流程:
TPF在这个过程负责创建插、代码加载以及资源加载。
五、TPF插件应用组件的完整生命周期
在《二、TPF支持的功能》第5点:插件中的组件拥有真正生命周期,完全交由系统管理、非反射代理。这里就拿最常用的activity来说一下TPF是如何做到的:
1、组件的声明
我们知道Android应用程序的application、activity、service、receiver[xml配置的]、provider这些组件需要在AndroidManifest.xml里面进行配置,这样系统才能知道应用拥有哪些组件(或者说:系统才会认可哪些组件)。然而插件应用是不走系统的安装流程,也就是系统默认情况下是不认可插件应用在AndroidManifest.xml里面配置的组件。
这点我们可以通过宿主来做桥梁,提前在宿主的AndroidManifest.xml里面注册号需要的组件,通过这些提前注册号的组件来桥接到插件组件上面去。
之前有套方案就是让插件组件通过实现框架提供的proxy来将宿主预先准备好的代理组件转接到插件组件上,这套方案的主要问题有两个:1、宿主接管了插件组件的生命周期,插件组件并没有自己的完整生命周期,2、对service等组件不能支持。然而TPF肯定不会是采用这种了,在《二、TPF支持的功能》第5点:插件中的组件拥有真正生命周期,完全交由系统管理、非反射代理。明确的说了,下面就详细道来。
2、插件组件的真正生命周期来源
来看一下activity生命周期
这张图,大家在熟悉不过了,可是在onCreate之前系统帮我们做了什么事呢
这个环节是解决插件组件activity完整生命周期的关键。我们来看一下这个过程:
上面就是组件activity生命周期onCreate之前系统默默帮我们进行处理的流程。
从开始执行execStartActivity到最终将Activity对象new出来这个过程,系统层会去校验需要启动的activity的合法性[就是是否有在某个应用的AndroidManifest.xml里面注册]以及按启动要求创建activity对象。清晰了这点我们就可以很好的绕过系统的约束,达到我们的目的:【插件中的组件拥有真正生命周期,完全交由系统管理、非反射代理】。
简单来说方案就两步:
Step1、在开始startActivity的时候将需要启动的插件组件替换成宿主预先声明号的。
public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
//如果启动的是插件的activity组件,这里面将会被替换成宿主预先声明的
PluginIntentResolver.resolveActivity(intent);
return hackInstrumentation.execStartActivity(who, contextThread, token, target, intent, requestCode, options);
}
Step2、在最终创建activity对象的时候改回成插件组件的。
@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException,
IllegalAccessException, ClassNotFoundException {
ClassLoader orignalCl = cl;
String orginalClassName = className;
String orignalIntent = intent.toString();
if (ProcessUtil.isPluginProcess()) {
// 将PluginStubActivity替换成插件中的activity
if (PluginManagerHelper.isStub(className)) {
String action = intent.getAction();
if (action != null && action.contains(PluginIntentResolver.CLASS_SEPARATOR)) {
String[] targetClassName = action.split(PluginIntentResolver.CLASS_SEPARATOR);
String pluginClassName = targetClassName[0];
final String pid = intent.getStringExtra(PluginIntentResolver.INTENT_EXTRA_PID).trim();
PluginDescriptor pluginDescriptor = TextUtils.isEmpty(pid) ? PluginManagerHelper.getPluginDescriptorByClassName(pluginClassName) : PluginManagerHelper.getPluginDescriptorByPluginId(pid);
Class<?> clazz = PluginLoader.loadPluginClassByName(pluginDescriptor, pluginClassName);
if (clazz != null) {
className = pluginClassName;
cl = clazz.getClassLoader();
intent.setExtrasClassLoader(cl);
if (targetClassName.length > 1) {
// 之前为了传递classNae,intent的action被修改过
// 这里再把Action还原到原始的Action
intent.setAction(targetClassName[1]);
} else {
intent.setAction(null);
}
// 添加一个标记符
intent.addCategory(RELAUNCH_FLAG + className);
} else {
throw new ClassNotFoundException("pluginClassName : " + pluginClassName, new Throwable());
}
} else if (PluginManagerHelper.isExact(className, PluginDescriptor.ACTIVITY)) {
// 这个逻辑是为了支持外部app唤起配置了stub_exact的插件Activity
PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByClassName(className);
if (pluginDescriptor != null) {
boolean isRunning = PluginLauncher.instance().isRunning(pluginDescriptor.getPackageName());
if (!isRunning) {
return waitForLoading(pluginDescriptor);
}
}
Class<?> clazz = PluginLoader.loadPluginClassByName(pluginDescriptor, className);
if (clazz != null) {
cl = clazz.getClassLoader();
} else {
throw new ClassNotFoundException("className : " + className, new Throwable());
}
} else {
// 进入这个分支可能是因为activity重启了,比如横竖屏切换,由于上面的分支已经把Action还原到原始到Action了
// 这里只能通过之前添加的标记符来查找className
boolean found = false;
Set<String> category = intent.getCategories();
if (category != null) {
Iterator<String> itr = category.iterator();
while (itr.hasNext()) {
String cate = itr.next();
if (cate.startsWith(RELAUNCH_FLAG)) {
className = cate.replace(RELAUNCH_FLAG, "");
PluginDescriptor pluginDescriptor = PluginManagerHelper.getPluginDescriptorByClassName(className);
if (pluginDescriptor != null) {
boolean isRunning = PluginLauncher.instance().isRunning(
pluginDescriptor.getPackageName());
if (!isRunning) {
return waitForLoading(pluginDescriptor);
}
}
Class<?> clazz = PluginLoader.loadPluginClassByName(pluginDescriptor, className);
cl = clazz.getClassLoader();
found = true;
break;
}
}
}
if (!found) {
throw new ClassNotFoundException(
"className : " + className + ", intent : " + intent.toString(), new Throwable());
}
}
} else {
if (cl instanceof PluginClassLoader) {
PluginIntentResolver.resolveActivity(intent);
} else {
// Do Nothing
}
}
}
try {
Activity activity = super.newActivity(cl, className, intent);
if (activity instanceof PluginContainer) {
((PluginContainer) activity).setPluginId(intent.getStringExtra(PluginContainer.FRAGMENT_PLUGIN_ID));
}
return activity;
} catch (ClassNotFoundException e) {
// 收集状态,便于异常分析
throw new ClassNotFoundException(" orignalCl : " + orignalCl.toString() + ", orginalClassName : "
+ orginalClassName + ", orignalIntent : " + orignalIntent + ", currentCl : " + cl.toString()
+ ", currentClassName : " + className + ", currentIntent : " + intent.toString() + ", process : "
+ ProcessUtil.isPluginProcess() + ", isStubActivity : "
+ PluginManagerHelper.isStub(orginalClassName) + ", isExact : "
+ PluginManagerHelper.isExact(orginalClassName, PluginDescriptor.ACTIVITY), e);
}
}
方案确实很简单,不过还有一些收尾工作,就是将创建好的[插件]组件进行一些必要的init操作,比如:在声明周期onCreate之前进行上下文替换等操作,这些都在插件框架提供的PluginInstrumentionWrapper里面进行完成的,看一下代码片段:
@Override
public void callActivityOnCreate(Activity activity, Bundle icicle) {
PluginInjector.injectActivityContext(activity);
Intent intent = activity.getIntent();
if (intent != null) {
intent.setExtrasClassLoader(activity.getClassLoader());
}
if (icicle != null) {
icicle.setClassLoader(activity.getClassLoader());
}
if (ProcessUtil.isPluginProcess()) {
installPluginViewFactory(activity);
if (activity instanceof WaitForLoadingPluginActivity) {
// NOTHING
} else {
}
if (activity.isChild()) {
// 修正TabActivity中的Activity的ContextImpl的packageName
Context base = activity.getBaseContext();
while (base instanceof ContextWrapper) {
base = ((ContextWrapper) base).getBaseContext();
}
if (HackContextImpl.instanceOf(base)) {
HackContextImpl impl = new HackContextImpl(base);
String packageName = PluginLoader.getApplication().getPackageName();
// String packageName1 = activity.getPackageName();
impl.setBasePackageName(packageName);
impl.setOpPackageName(packageName);
}
}
}
super.callActivityOnCreate(activity, icicle);
monitor.onActivityCreate(activity);
}
到这插件activity组件就被顺序的启动起来了,并且是系统在维护具备完整的生命周期。组件service、Receiver也是一样的,只是这两个组件的拦截点在ActivityThread的Handler成员的回调Callback里面进行的,其他的原理是一样的就不一一细说,详情请查阅https://github.com/rickdynasty/TwsPluginFramework。
六、plugin-display插件显示协议
经宿主插件框架加载的插件最终的目的是正常运行插件里面的功能。然而不管插件有没界面都必须提供一个启动的入口,比如:随宿主一起启动、在宿主HomeUI上显示一个图标或者按钮等,然而这个需求本来可以直接写两行代码在宿主里面,可是如果发布了后呢?如果插件有调整,宿主就得重新发布版本,这种耦合就成为了一个大浪费的源头。
解耦这一层关系的方案:将插件在宿主中的显示入口信息配置到AndroidManifest.xml里面统一由宿主的插件框架进行解析获取。下面的TPF Demo工程的配置:
这套协议支持插件显示入口的类型、位置[项目定制化]、多分辨率图标、多语言Title、页面Actionbar等配置。
七、第三方应用启动插件组件
TPF支持第三方应用启动插件的activity、service组件。
方案原理:在宿主预先声明固定规则[和插件组件一样的intent-filter]的组件,系统唤起后桥接到插件组件。这点和插件拥有完整生命周期是一样的。
附录
1、开源框架TwsPluginFramework github地址:
https://github.com/rickdynasty/TwsPluginFramework
2、框架使用注意事项
① 插件可配置单独进程,com.tws.plugin.util. ProcessUtil里面须指定插件进程的命名规则,方便匹配插件进程的判断,比如TPF:
// 这是定制化规则,插件的进程除PluginManagerProvider的标配外,其他的都统一规定前缀:
private static final String PLUGIN_MULTI_PROCESS_PREFIX = "com.tencent.tws.gdevicemanager:plugin";
② TPF提供了一套共享库,该共享库包含了TencentOs2.0所有控件,须在com.tencent.tws.sharelib.util. HostProxy里面指定宿主的packageName:
public class HostProxy {
private static String HOST_PACKAGE_NAME = "com.tencent.tws.gdevicemanager";
//...