公司要实现一个这样的效果,类似于qq游戏大厅,我们可以下载斗地主,可以下载保皇,下载完成直接就可以玩,不需要安装,也就是说我们的这一款软件里面可以装载多款软件.
公司项目是一套系统管理软件,它里面包括了五款软件,用户可以选择付费选择购买这其中的任一款软件,需求是说为了用户体验好,不让用户购买一款软件就要在桌面上安装一个软件(一共五款,想想用户界面会不会特别乱).
想了想总结一下需要解决以下几个问题:
1.应用占用内存大小,不能让应用占手机内存特别大(五款系统,资源文件不能都放在一个应用里面,那样下载大小太大了~)
2.手机桌面看不到下载的五款应用图标,就像QQ游戏一样,只能看到一个主应用的图标
3.应用之间数据互通(五个应用之间不用使用一个软件就去重新登录,重新获取数据,一方面浪费流量,一方面用户体验也不好)
于是乎开始花了一上午的时间在网上查了一下,得到了三种解决办法 分别如下:
1.插件化,把一款应用分成多个插件, 就是说做一个宿主的主应用,然后里面的五款软件分别拆分成五个插件,当用户购买应用下载时候,其实是把插件下载下来了,直接调用进入就行,
2.设置AndroidManifest.xml里面的主activity <category android:name="android.intent.category.LAUNCHER" />这一行里面的LAUNCHER设置成DEFAULT,也有说直接删除的,就可以让他在桌面上不存在图标了,等等关于他的一系列说法,本人亲测一切都没有用,对,,,没有用 随着安卓版本的更新,这种方法早已经被淘汰了
3.热更新热修复,各大互联网公司基本都有自己的技术,可以很快集成到自己的项目,但是呢~~必须把自己的代码挂到人家那上面 发布版本也是要把代码往人家上面放(..想想...自己代码都给人家了..自己还玩个P啊对吧...)
所以只剩下一个解决办法了 就是使用插件化 恩...下面开始介绍本人插件化的坎坷之路
首先我选用的是滴滴开源的VirtualAPK框架,下面是关于他跟其他插件化框架的不同之处
特性 DynamicLoadApk DynamicAPK Small DroidPlugin VirtualAPK 支持四大组件 只支持Activity 只支持Activity 只支持Activity 全支持 全支持 组件无需在宿主manifest中预注册 √ × √ √ √ 插件可以依赖宿主 √ √ √ × √ 支持PendingIntent × × × √ √ Android特性支持 大部分 大部分 大部分 几乎全部 几乎全部 兼容性适配 一般 一般 中等 高 高 插件构建 无 部署aapt Gradle插件 无 Gradle插件
可以看到真的很牛逼的样子
接下来是,集成到项目,开始写代码
一定注意文中所提到的版本 以及gradle版本号 严格规定,只能用这个版本!
环境准备
- Gradle版本需要为2.14.1,可以使用
gradle -v
查看环境中配置的Gradle版本号。也可以使用工程中gradlew
来编译,可以在gradle/wrapper/gradle-wrapper.properties
中更改版本号distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip
- com.android.tools.build的版本号为2.1.3
宿主工程接入
Host宿主工程接入需要以下6步
- 在宿主工程根目录的build.gradle添加依赖
dependencies { classpath 'com.didi.virtualapk:gradle:0.9.0' }
- 在App的工程模块的build.gradle添加使用gradle插件
apply plugin: 'com.didi.virtualapk.host'
- 添加VirtualAPK SDK compile依赖
dependencies { compile 'com.didi.virtualapk:core:0.9.0' }
- MyApplication类是继承了Application,覆写attachBaseContext函数,进行插件SDK初始化工作
@Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); PluginManager.getInstance(base).init(); }
- 在使用插件之前加载插件,可以根据具体业务场景选择合适时机加载,我是在MainActivity的onCreate时机加载
protected void onCreate(Bundle savedInstanceState) { // 加载plugin.apk插件包 PluginManager pluginManager = PluginManager.getInstance(this); File apk = new File(getExternalStorageDirectory(), "plugin.apk"); if (apk.exists()) { try { pluginManager.loadPlugin(apk); } catch (Exception e) { e.printStackTrace(); } } }
经过上述6步后,VirtualAPK插件功能就集成到宿主中了,宿主打包和运行方式没有任何改变。接下来看下插件工程如何集成和构建的。
插件工程接入
ImageBrowser插件工程接入分为3步:
- ImageBrowser工程根目录的build.gradle添加依赖
dependencies { classpath 'com.didi.virtualapk:gradle:0.9.0' }
在App的工程模块的build.gradle添加使用gradle插件和插件配置信息,信息需要放在文件最下面
apply plugin: 'com.didi.virtualapk.plugin' ... ... // 插件配置信息,放在文件最下面 virtualApk { packageId = 0x6f // 插件资源id,避免资源id冲突 targetHost='../host/app' // 宿主工程的路径 applyHostMapping = true // 插件编译时是否启用应用宿主的apply mapping }
解释一下上面3个参数的作用
- packageId用于定义每个插件的资源id,多个插件间的资源Id前缀要不同,避免资源合并时产生冲突
- targetHost指明宿主工程的应用模块,插件编译时需要获取宿主的一些信息,比如mapping文件、依赖的SDK版本信息、R资源文件,一定不能填错,否则在编译插件时会提示找不到宿主工程。
- applyHostMapping表示插件是否开启apply mapping功能。当宿主开启混淆时,一般情况下插件就要开启applyHostMapping功能。因为宿主混淆后函数名可能有fun()变为a(),插件使用宿主混淆后的mapping映射来编译插件包,这样插件调用fun()时实际调用的是a(),才能找到正确的函数调用。
最后一步生成插件,需要使用Gradle命令
gradle clean assemblePlugin 或者 ./gradlew clean assemblePlugin
强调一下如果构建时确保Gradle版本需要为2.14.1,否则构建可能发生错误。构建成功后在build/outputs/apk 或者plugin目录中查看插件,plugin目录和apk目录中插件的区别在于plugin将插件以packageName_timestamp格式重命名,DEMO中的插件构建成功后才3KB。
官方WIKI中还说明了:
- 插件包均是Release包,不支持debug模式的插件包
- 如果存在多个productFlavors,那么将会构建出多个插件包
前面说到VirtualAPK是对耦合型业务有很好的支持,对于我们的DEMO来说,宿主和插件都用到了Picasso库,我们反编译插件包后看一下里面包含的内容如下图所示。
可以看到,插件包中没有Picasso库的相关源码,构建时VirtualAPK已经帮我们移除了。需要注意,宿主和插件包中依赖的SDK版本需要完全一致时才会被移除。
运行插件
因为宿主中代码写的是从SD卡根目录加载plugin.apk插件,所以我们需要将生成的插件重命名后放到指定位置。
adb push 插件路径 /sdcard/plugin.apk
然后启动宿主程序后点击"查看更多美图",此时加载的Activity来自于插件中,启动代码如下所示
@Override
public void onClick(View v) {
if (PluginManager.getInstance(this).getLoadedPlugin(PLUGIN_PKG_NAME) == null) {
Toast.makeText(getApplicationContext(),
"插件未加载,请尝试重启APP", Toast.LENGTH_SHORT).show();
return;
}
Intent intent = new Intent();
intent.setClassName("com.virtualapk.imageplugin", "com.virtualapk.imageplugin.ImageBrowserActivity");
startActivity(intent);
}
OK,插件就么运行起来了,可以看到在开发宿主和插件时,几乎没有侵入,在生成插件时需要一些命令操作,可以通过脚本实现编译插件+Push到SD卡中,提高开发效率。
DEMO比较简单只包含了Activity,后面会扩展到其他组件的使用,大家可以仔细看下官方WIKI-插件开发指南,里面介绍了插件中四大组件的使用、so库的加载、已知的约束和插件加载时机的选取问题。
其他注意点
- 要想清楚宿主和插件的业务边界很重要,才能找到插件的入口点。Demo中是以ImageBrowserActivity为边界,从这个Activity之后的功能来来自于插件中。其实我们可以把插件看成一个类提供者,可以使用Class.forName()这种方式使用插件中的类,所以不是只能从Activity/Fragment作为入口点。
- 编译宿主工程时会生成一些信息(在build/VAHost文件夹下),插件构建时会读取这些信息,所以要确保运行的宿主和插件基于相同信息构建的,宿主变化时请重新构建插件。
资源互相调用也是可以的只要做一套公共的model,然后他们都公用同一个model里面的资源文件,资源ID冲突的问题暂时还没发现,他的生成规则是从头开始按照顺序来的,DEMO比较简单只包含了Activity,但是已经可以满足使用,大家可以仔细看下官方WIKI-插件开发指南,
按照上面说的就可以完成,下面附上DEMO
>点我下载DEMO<