插件化库 VirtualAPK 详解,你头秃都没想到还能这样吧
1.1、宿主工程引入 VirtualApk
在项目 Project 的 build.gradle 中添加依赖
dependencies {classpath 'com.didi.virtualapk:gradle:0.9.8.6'}
在宿主 app 的 build.gradle 中引入 VirtualApk 的 host 插件
apply plugin: 'com.didi.virtualapk.host'
在 app 中添加依赖
dependencies {implementation 'com.didi.virtualapk:core:0.9.8'}
在 Application 中完成 PluginManager 的初始化
public class VirtualApplication extends Application {@Overrideprotected void attachBaseContext(Context base) {super.attachBaseCon
text(base);PluginManager.getInstance(base).init();}}
1.2 插件开发
插件 APK 的配置
在插件 project 中配置
classpath 'com.didi.virtualapk:gradle:0.9.8.6'
在插件 app 的 build.gradle 中引入 plugin 插件
apply plugin: 'com.didi.virtualapk.plugin'
配置插件信息和版本
virtualApk{// 插件资源表中的 packageId,需要确保不同插件有不同的 packageId// 范围 0x1f - 0x7fpackageId = 0x6f// 宿主工程 application 模块的路径,插件的构建需要依赖这个路径// targetHost 可以设置绝对路径或相对路径 targetHost = '../../../VirtualAPkDemo/app'// 默认为 true,如果插件有引用宿主的类,那么这个选项可以使得插件和宿主保持混淆一致//这个标志会在加载插件时起作用 applyHostMapping = true}
设置签名(Virtual 仅支持 Release,host 项目和 plugin 项目签名一致)
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 中插件和宿主通信的基本方式。
插件中四大组件的已知约束
透明 Activity,不能有启动模式,并且主题中必须含有 android:windowIsTranslucent 属性;
<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
静态 Receiver 将被动态注册,当宿主停止运行时,外部广播将无法唤醒宿主;
由于动态注册的缘故,插件中的 Receiver 必须通过隐式调用来唤起。
ContentProvider,支持跨进程访问 ContentProvider
分情况,插件调用自己的 ContentProvider,如果需要用到 call 方法,那么需要将 provider 的 uri 放到 bundle 中,否则调用不生效;
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,如下所示:
<applicationandroid:name=".VAApplication"android:allowBackup="true"
评论