注:本文是针对virtualAPK来说的,不同的组件化框架会有不同的实现方式和一些定义名称请不要混淆;
vertualAPK:git:https://github.com/didi/VirtualAPK
vertualAPK的一些限制和问题:
https://github.com/didi/VirtualAPK/wiki/VirtualAPK-%E6%8F%92%E4%BB%B6%E5%BC%80%E5%8F%91%E6%8C%87%E5%8D%97
一:首先概括的说一下virtualAPK的相关,怎么集成:
1.virtualAPK相关:
virtualAPK是滴滴程序组件化开发的一个产物,是一个动态加载框架;
app程序组件化开发用滴滴的说法分为宿主和插件;
宿主:宿主就是主程序,需要在各个平台发布然后在手机上下载安装后使用的程序,即用户在屏幕上看到应用图标在设置中应用程序里可以找到的那个程序;
插件:插件是相对宿主来说的,插件是宿主的完善和扩展,一般情况下插件都应该是一个相对独立的功能。插件开发过程中除去添加virtualAPK框架的依赖外其余的开发过程是和普通APP开发相同的;插件使用的时候只需要下载到指定的地址就可以;
总的来说如果插件中没有集成特殊的内容那兼容性还是比较好的,如果插件集成了特殊的三方或是依赖那就要看具体的内容了,尤其是插件中三方或依赖需要用到applicationID进行认证的情况;
virtualAPK是通过识别插件和宿主之间包名将宿主和插件关联在一起;
VirtualAPK 特性
功能完备
支持几乎所有的 Android 特性;
四大组件方面:四大组件均不需要在宿主manifest中预注册,每个组件都有完整的生命周期。
Activity:支持显示和隐式调用,支持 Activity 的 theme 和 LaunchMode,支持透明主题;
Service:支持显示和隐式调用,支持 Service 的 start、stop、bind 和 unbind,并支持跨进程 bind 插件中的 Service;
Receiver:支持静态注册和动态注册的 Receiver;
ContentProvider:支持 provider的所有操作,包括 CRUD 和 call 方法等,支持跨进程访问插件中的 Provider。
自定义View:支持自定义 View,支持自定义属性和 style,支持动画;
PendingIntent:支持 PendingIntent 以及和其相关的 Alarm、Notification 和AppWidget;
支持插件 Application 以及插件 manifest 中的 meta-data;
支持插件中的so。
优秀手机兼容性
兼容市面上几乎所有的 Android 手机,这一点已经在滴滴出行客户端中得到验证;
资源方面适配小米、Vivo、Nubia 等,对未知机型采用自适应适配方案;
极少的 Binder Hook,目前仅仅 hook 了两个 Binder:AMS 和 IContentProvider,Hook过程做了充分的兼容性适配;
插件运行逻辑和宿主隔离,确保框架的任何问题都不会影响宿主的正常运行。
入侵性极低
插件开发等同于原生开发,四大组件无需继承特定的基类;
精简的插件包,插件可以依赖宿主中的代码和资源,也可以不依赖;
插件的构建过程简单,通过Gradle插件来完成插件的构建,整个过程对开发者透明。
VirtualAPK 对插件没有额外的约束,原生的 apk 即可作为插件。插件工程编译生成 apk 后,即可通过宿主 App 加载,每个插件 apk 被加载后,都会在宿主中创建一个单独的 LoadedPlugin 对象。如下图所示,通过这些 LoadedPlugin 对象,VirtualAPK 就可以管理插件并赋予插件新的意义,使其可以像手机中安装过的App一样运行。
2.怎么集成:
基本操作:
集成分为两部分:宿主和插件;
A.宿主:
宿主项目的gradle中添加:
dependencies {
classpath 'com.didi.virtualpk:gradle:0.9.8.6'
}
在app module 的gradle中添加:
apply plugin: 'com.didi.virtualapk.host'
compile 'com.didi.virtualapk:core:0.9.8'
以上是宿主添加的gradle设置,
然后是在混淆设置中添加virtualAPK的混淆设置:
-keep class com.didi.virtualapk.internal.VAInstrumentation { *; }
-keep class com.didi.virtualapk.internal.PluginContentResolver { *; }
-dontwarn com.didi.virtualapk.**
-dontwarn android.**
-keep class android.** { *; }
以上就是宿主的集成设置;
B.插件:
插件的集成就相对简单一些:
在项目的gradle中添加如下内容:
dependencies {
classpath 'com.didi.virtualapk:gradle:0.9.8.6'
}
在module的gradle中添加如下内容:
apply plugin: 'com.didi.virtualapk.plugin'
virtualApk {
packageId = 0x6f // the package id of Resources.
targetHost = '../../CashLoan/app' // the path of application module in host project.
applyHostMapping = true //optional, default value: true.
}
packageID是为了区分资源等的值,这个值在插件和插件之间不能相同,而且有的时候插件的id会和宿主相同,会报异常就需要调整这个值,这个值的范围在系统和宿主的之间即大于0x02,小于0x7f;
targetHost:标明宿主的application module 也就是app module 的路径,所以一般会把插件放到宿主的一级目录下这样这个路径就可以是相对路径就可以了,这样简单一点,这路径每次打包的时候都会检测;
applyHostMapping:默认为true,如果插件有引用宿主的类,那么这个选项可以使得插件和宿主保持混淆一致;
以上插件的配置内容;
二.插件和宿主的相互调用:
A.宿主:宿主调用插件会比较麻烦一点,如下:
首先需要在代码中对PluginManager进行初始化:在application中的attachBaseContext方法中,如下:
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
PluginManager.getInstance(base).init();
}
检测apk是否存在:
File plugin = new File(Environment.getExternalStorageDirectory(),"your_apk_name.apk");
if (!plugin.exists()){
return;
}
在调用处获取PluginManager对象加载apk文件:
PluginManager pluginManager = PluginManager.getInstance(StartActivity.this);
try {
pluginManager.loadPlugin(plugin);
} catch (Exception e) {
e.printStackTrace();
}
检测插件包名是否与调用目标符合:
final String packageName = "xxx.xxx.xxx";
if (PluginManager.getInstance(context).getLoadedPlugin(packageName ) == null) {
return;
}
调用插件activity:
Intent intent = new Intent();
intent.setClassName(context, "com.xxx.xxx.xxx.xxxx.activityname");//需要完整包名,这个包名是插件的包名
intent.putExtra("test0","hello,I am a survey who the number is zero,i come with greeting");//可以利用intent进行传值
startActivityForResult(intent,1000);//可以通过result回去返回值
B.插件:
插件调用有两种,一是插件调用自己,二是插件调用宿主:
调用自己:正常调用即可,没有特殊要求;
调用宿主:使用intent显示包名调用;
三.打包:
打包通过命令行来执行打包:
宿主:gradle clean assembleRelease 命令来打包;
插件:gradle clean assemblePlugin命令来打包;
apk文件都位于各自的app/build文件下;
支持生成多渠道apk(通过配置productFlavors可以和通过androidstudio打包一样的效果);
host文件:插件第一次打包,插件需要放置到宿主一级目录下,因为宿主需要在插件中生成一个host文件夹,这个文件夹只有插件放在宿主文件中的时候才会在宿主打包的时候生成;
按照网络上的说法这种打包方式plugin会依赖relaease从而plugin不会生成debug包;
四.资源
android 查找资源生成resource文件是通过一个AssetManager类来实现的,virtualAPK也是通过操作这个类来把插件的资源添加和调用,这个时候就会用到之前设置的packageID这个参数了,因为资源id默认会生成0x7f开头的id,如果不设置那宿主和插件就很可能会重复了,所以插件的id开头会是以设置的packID为开头的id。
资源的详细解说https://juejin.im/entry/5969e457f265da6c261d6c5f:建议不要使用合并资源的方式对维护要求较高;
除去上面说的图片多媒体资源外,理论上vritualAPK中支持宿主和插件依赖aar这些三方的共用,但是需要在宿主和插件中同时添加相应的设置,在打包的时候插件会自动去除重复的三方(本地Jar和library是不支持去重的),比较特殊的是基础的support包宿主和插件必须是一样的。
五.通信:
git上virtualAPK文档推荐的是使用共同的arr来进行通信(这种方式没有进行验证),大概方法是在arr中定义静态方法和静态对象;
此外virtualAPK对四大组件都是支持的,可以使用BroadCastReceiver来进行通信;但BroadCastReceiver和contentProvider的注册和使用有一定的限制,
详情参考:
https://github.com/didi/VirtualAPK/wiki/VirtualAPK-%E6%8F%92%E4%BB%B6%E5%BC%80%E5%8F%91%E6%8C%87%E5%8D%97
我在实践过程中使用的是intentForResult来做的简单通信,只要使用显示调用就可以达到和非virtualAPK同样的效果。
六.要点:
1.几个不同:
a.宿主和插件及不同插件之间的包名;
b.宿主和插件及不同插件之间的applicationID;
c.插件之间的packageID;
2.virtualAPK对包名的处理:
virtualAPK会弱化插件的包名,当在运行的时候插件中通过getPackageName()获得包名是宿主,而且经过验证插件中第三方获取的包名同样是宿主的;因为android在打包的时候会将包名更换为applicationID(如果你没有设置那默认是和包名一样的),所以applicationID应当设置成和packageName同样的值,如果不一样打包的时候是会报错的;那么结果就是在开发的过程中即使是插件中用到的三方需要applicationID 和 packageName的话也需要用宿主的进行注册;
3.插件的更新:
根据virtualAPK的说明(其实大部分动态加载都是如此)不支持运行中更新,我测试的时候是需要关闭宿主然后再打开才会执行新的插件的;
深入了解virtualAPK涉及到的一些加载机制参考:https://blog.csdn.net/yyh352091626/article/details/74852390