前篇文章《Android组件化和插件化开发》主要介绍了Android组件化和插件化的架构特点、两者的对比分析以及推荐了学习组件化的相关文章,本编主要介绍下目前插件化开源库的使用情况,以及着重介绍下VirtualAPK库,供大家参考。
插件化主要就是利用动态加载技术
通过服务器配置一些参数,Android APP获取这些参数再做出相应的逻辑,这是常有的事,比如现在大部分APP都有一个启动页面,如果到了一些重要的节日,APP的服务器会配置一些与时节相关的图片,APP启动时候再把原有的启动图换成这些新的图片,这样就能提高用户的体验了。
再则,早期个人开发者在安卓市场上发布应用的时候,如果应用里包含有广告,那么有可能会审核不通过,那么就通过在服务器配置一个开关,审核应用的时候先把开关关闭,这样应用就不会显示广告了;安卓市场审核通过后,再把服务器的广告开关给打开,以这样的手段规避市场的审核。所以现在安卓市场开始扫描APK里面的Manifest甚至dex文件,查看开发者的APK包里是否有广告的代码,如果有就有可能审核不通过。通过服务器怕配置开关参数的方法行不通了,开发者们开始想,“既然这样,能不能先不要在APK写广告的代码,在用户运行APP的时候,再从服务器下载广告的代码运行,再实现广告呢?”。答案是肯定的,这就是动态加载。
在程序运行的时候,加载一些程序自身原本不存在的可执行文件并运行这些文件里的代码逻辑。
看起来就像是应用从服务器下载了一些代码,然后再执行这些代码!
使用动态加载技术,一般来说会使得Android开发工作变得更加复杂,这种开发方式不是官方推荐的,不是目前主流的Android开发方式,Github 和 StackOverflow 上面外国的开发者也对此不是很感兴趣,外国相关的教程更是少得可怜,目前只有在大天朝才有比较深入的研究和应用,特别是一些SDK组件项目和 BAT家族的项目上,Github上的相关开源项目基本是国人在维护。
动态加载的大致过程就是:
特性 | DynamicLoadApk | DynamicAPK | Small | DroidPlugin | VirtualAPK |
---|---|---|---|---|---|
支持四大组件 | 只支持Activity | 只支持Activity | 只支持Activity | 全支持 | 全支持 |
组件无需在宿主 manifest中预注册 | √ | × | √ | √ | √ |
插件依赖宿主 | √ | √ | √ | × | √ |
支持PendingIntent | × | × | × | √ | √ |
Android特性支持 | 大部分 | 大部分 | 大部分 | 几乎全部 | 几乎全部 |
兼容性适配 | 一般 | 一般 | 中等 | 高 | 高 |
插件构建 | 无 | 部署aapt | Gradle插件 | 无 | Gradle插件 |
阿里的atlas:Atlas 是伴随着手机淘宝不断发展而衍生出来的一个运行于 Android 系统上的插件化框架,也可以叫动态组件化框架,主要提供了解耦化、组件化、动态性的支持。是目前比较成熟的方案,功能强大,但相对的,使用和集成的难度也比较大。
腾讯的Shadow:Shadow是一个腾讯自主研发的Android插件框架,并且一直在维护中,但使用和集成难道稍大,有兴趣的可以研究下。
Github地址:https://github.com/didi/VirtualAPK
dependencies {
classpath 'com.didi.virtualapk:gradle:0.9.8.6'
}
apply plugin: 'com.didi.virtualapk.host'
dependencies {
implementation 'com.didi.virtualapk:core:0.9.8'
}
public class VirtualApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
PluginManager.getInstance(base).init();
}
}
classpath 'com.didi.virtualapk:gradle:0.9.8.6'
apply plugin: 'com.didi.virtualapk.plugin'
virtualApk{
// 插件资源表中的packageId,需要确保不同插件有不同的packageId
// 范围 0x1f - 0x7f
packageId = 0x6f
// 宿主工程application模块的路径,插件的构建需要依赖这个路径
// targetHost可以设置绝对路径或相对路径
targetHost = '../../../VirtualAPkDemo/app'
// 默认为true,如果插件有引用宿主的类,那么这个选项可以使得插件和宿主保持混淆一致
//这个标志会在加载插件时起作用
applyHostMapping = true
}
signingConfigs {
release {
storeFile file('/Users/wuliangliang/AndroidSubjectStudyProject/PluginProject/VirtualAPkDemo/keystore/keystore')
storePassword '123456'
keyAlias = 'key'
keyPassword '123456'
}
}
buildTypes {
release {
signingConfig signingConfigs.release
}
}
在VirtualAPK中,插件开发等同于原生Android开发,因此开发插件就和开发APP一样。
通过compile相同aar的方式来交互。 比如,宿主工程中compile了如下aar:
compile 'com.didi.foundation:sdk:1.2.0'
compile 'com.didi.virtualapk:core:[newest version]'
compile 'com.android.support:appcompat-v7:22.2.0'
但是插件工程需要访问宿主sdk中的类和资源,那么可以在插件工程中同样compile sdk的aar,如下:
compile 'com.didi.foundation:sdk:1.2.0'
这样一来,插件工程就可以正常地引用sdk了,类似宿主和插件共用了一个功能库来进行交流。并且,插件构建的时候会自动将这个aar从apk中剔除。上述就是VirtualAPK中插件和宿主通信的基本方式。
<style name="AppTheme.Transparent">
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowIsTranslucent">true</item>
</style>
插件中调用宿主的四大组件,请注意Intent中的包名
VirtualAPK对Intent的处理遵循Android规范,插件之间乃至插件和宿主之间,包名是区分它们的唯一标识。
为了兼容宿主与插件之间的activity互调的场景,我们弱化了插件的包名,在插件中通过context.getPackageName()取到的仍然是宿主的包名。因此在下面的例子中,假如宿主的包名是"com.didi.virtualapk",然后在插件中启动一个宿主Activity,仍然可正确的调用:
// 兼容方式
Intent intent = new Intent(this, HostActivity.class);
startActivity(intent);
// 显式指定包名的方式
Intent intent = new Intent();
intent.setClassName("com.didi.virtualapk", "com.didi.virtualapk.HostActivity");
startActivity(intent);
如果想在插件中去访问插件的四大组件,那么就没有任何要求了,下面的代码会在插件Activity中尝试启动另一个插件Activity:
// 正确的用法,因为此时intent中的包名是插件的包名
Intent intent = new Intent(this, PluginActivity.class);
startActivity(intent);
BroadcastReceiver
ContentProvider,支持跨进程访问ContentProvider
Uri bookUri = Uri.parse("content://com.didi.virtualapk.demo.book.provider/book");
Bundle bundle = PluginContentResolver.getBundleForCall(bookUri);
getContentResolver().call(bookUri, "testCall", null, bundle);
插件调用宿主和外部的ContentProvider,无约束;
宿主调用插件的ContentProvider,需要将provider的uri包装一下,通过PluginContentResolver.wrapperUri方法,如果涉及到call方法,参考1)中所描述的;
String pkg = "com.didi.virtualapk.demo";
LoadedPlugin plugin = PluginManager.getInstance(this).getLoadedPlugin(pkg);
Uri bookUri = Uri.parse("content://com.didi.virtualapk.demo.book.provider/book");
bookUri = PluginContentResolver.wrapperUri(plugin, bookUri);
Cursor bookCursor = getContentResolver().query(bookUri, new String[]{"_id", "name"}, null, null, null);
Fragment
推荐大家在Application启动的时候去加载插件,不然的话,请注意插件的加载时机。 考虑一种情况,如果在一个较晚的时机去加载插件并且去访问插件中的资源,请注意当前的Context。比如在宿主Activity(MainActivity)中去加载插件,接着在MainActivity去访问插件中的资源(比如Fragment),需要做一下显示的hook,否则部分4.x的手机会出现资源找不到的情况。
String pkg = "com.didi.virtualapk.demo";
PluginUtil.hookActivityResources(MainActivity.this, pkg);
so文件的加载
为了提升性能,VirtualAPK在加载一个插件时并不会主动去释放插件中的so,除非你在插件apk的manifest中显式地指定VA_IS_HAVE_LIB为true,如下所示:
<application
android:name=".VAApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/HostTheme">
<meta-data
android:name="VA_IS_HAVE_LIB"
android:value="true" />
...
</application>
为了通用性,在armeabi路径下放置对应的so文件即可满足需求。如果考虑性能请做好各种so文件的适配。
adb push ./app/build/outputs/plugin/release/com.alex.kotlin.virtualplugin_20190729172001.apk /sdcard/plugin_test.apk
注意问题
- 要先构建一次宿主app,才可以构建plugin,否则异常
- 插件布局文件中要设置资源的ID,否则异常:Cannot get property ‘id’ on null objectplugin
- 增加 gradle.properties 文件并配置android.useDexArchive=false,否则异常
在宿主App中加载插件apk
private void loadApk() {
File apkFile = new File(Environment.getExternalStorageDirectory(), "Test.apk");
if (apkFile.exists()) {
try {
PluginManager.getInstance(this).loadPlugin(apkFile);
} catch (Exception e) {
e.printStackTrace();
}
}
}
在插件下载或安装到设备后,获取插件的文件,调用PluginManager.loadPlugin()加载插件,PluginManager会完成所有的代码解析和资源加载;2. 执行界面跳转至插件中
final String pkg = "com.alex.kotlin.virtualplugin”; //插件Plugin的包名
Intent intent = new Intent();
intent.setClassName(pkg, "com.alex.kotlin.virtualplugin.MainPluginActivity”); //目标Activity的全路径
startActivity(intent);
virtualApk {
packageId = 0x6f
targetHost = '../../VirtualAPK/app' // 检测这个路径是否正确,相对路径或者绝对路径都行
applyHostMapping = true
}