https://github.com/didi/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和主流开源框架的对比
如下是VirtualAPK和主流的插件化框架之间的对比。
特性 | DynamicLoadApk | DynamicAPK | Small | DroidPlugin | VirtualAPK |
---|---|---|---|---|---|
支持四大组件 | 只支持Activity | 只支持Activity | 只支持Activity | 全支持 | 全支持 |
组件无需在宿主manifest中预注册 | √ | × | √ | √ | √ |
插件可以依赖宿主 | √ | √ | √ | × | √ |
支持PendingIntent | × | × | × | √ | √ |
Android特性支持 | 大部分 | 大部分 | 大部分 | 几乎全部 | 几乎全部 |
兼容性适配 | 一般 | 一般 | 中等 | 高 | 高 |
插件构建 | 无 | 部署aapt | Gradle插件 | 无 | Gradle插件 |
通俗易懂地说
- 如果你是要加载微信、支付宝等第三方APP,那么推荐选择DroidPlugin;
- 如果你是要加载一个内部业务模块,并且这个业务模块很难从主工程中解耦,那么VirtualAPK是最好的选择。
抽象地说
- 如果你要加载一个插件,并且这个插件无需和宿主有任何耦合,也无需和宿主进行通信,并且你也不想对这个插件重新打包,那么推荐选择DroidPlugin;
- 除此之外,在同类的开源中,推荐大家选择VirtualAPK。
VirtualAPK的工作过程
VirtualAPK对插件没有额外的约束,原生的apk即可作为插件。插件工程编译生成apk后,即可通过宿主App加载,每个插件apk被加载后,都会在宿主中创建一个单独的LoadedPlugin对象。如下图所示,通过这些LoadedPlugin对象,VirtualAPK就可以管理插件并赋予插件新的意义,使其可以像手机中安装过的App一样运行。
va1.png
如何使用
第一步: 初始化插件引擎
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
PluginManager.getInstance(base).init();
}
第二步:加载插件
public class PluginManager {
public void loadPlugin(File apk);
}
当插件入口被调用后,插件的后续逻辑均不需要宿主干预,均走原生的Android流程。 比如,在插件内部,如下代码将正确执行:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_book_manager);
LinearLayout holder = (LinearLayout)findViewById(R.id.holder);
TextView imei = (TextView)findViewById(R.id.imei);
imei.setText(IDUtil.getUUID(this));
// bind service in plugin
Intent service = new Intent(this, BookManagerService.class);
bindService(service, mConnection, Context.BIND_AUTO_CREATE);
// start activity in plugin
Intent intent = new Intent(this, TCPClientActivity.class);
startActivity(intent);
}
探究原理
基本原理
- 合并宿主和插件的ClassLoader 需要注意的是,插件中的类不可以和宿主重复
- 合并插件和宿主的资源 重设插件资源的packageId,将插件资源和宿主资源合并
- 去除插件包对宿主的引用 构建时通过Gradle插件去除插件对宿主的代码以及资源的引用
四大组件的实现原理
- Activity 采用宿主manifest中占坑的方式来绕过系统校验,然后再加载真正的activity;
-
Service 动态代理AMS,拦截service相关的请求,将其中转给
Service Runtime
去处理,Service Runtime
会接管系统的所有操作; - Receiver 将插件中静态注册的receiver重新注册一遍;
-
ContentProvider 动态代理IContentProvider,拦截provider相关的请求,将其中转给
Provider Runtime
去处理,Provider Runtime
会接管系统的所有操作。
如下是VirtualAPK的整体架构图:
va.png
使用注意事项
运行时获取资源需要通过packageId来映射apk中的资源文件,不同apk的packageId值不能相同,所以插件的packageId范围是介于系统应用(0x01,0x02,...具体占用多少值视系统而定)和宿主(0x7F)之间。
宿主和插件同时依赖公共的本地jar文件或library module不支持自动去除,需要部署到maven或其它依赖管理服务器
插件安装使用so的abi必须与宿主保持一致,而宿主的abi类型在app安装时根据apk配置确定,并不再改变。请按以下步骤检查项目配置:
- 宿主apk中未使用so,则按设备默认abi类型安装。如果插件中使用so并报此错误,则需要在宿主中放一个占位的同abi类型so。
- 插件中使用的liba.so中直接引用了libb.so导出的符号,需要在加载a.so前显式加载b.so,否则可能会报找不到b.so的错误。详见Android中的System.loadLibrary对于依赖so的加载分析,如:
System.loadLibrary("b");
System.loadLibrary("a");
从Android 6.0开始,系统采用了新的权限机制,但是暂时不支持在插件中动态申请权限。
构建插件请使用gradle assemblePlugin,而不能直接通过AndroidStudio run出来一个插件apk。
资源id不能和宿主的资源重名,重名资源会在构建插件包时被自动剔除,导致插件内加载的是宿主资源而非自身资源
参数说明
virtualApk {
// 插件资源表中的packageId,需要确保不同插件有不同的packageId.
packageId = 0x6f
// 宿主工程application模块的路径,插件的构建需要依赖这个路径
targetHost = '../../VirtualAPK/app'
//默认为true,如果插件有引用宿主的类,那么这个选项可以使得插件和宿主保持混淆一致
applyHostMapping = true
}
如何开发插件?
在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中插件和宿主通信的基本方式。
然而,VirtualAPK仍然有一些小小的约束,如下注意事项,请务必仔细阅读。
目前暂不支持的特性
- 暂不支持Activity的一些不常用特性(比如process、configChanges等属性),但是支持theme、launchMode和screenOrientation属性;
- overridePendingTransition(int enterAnim, int exitAnim)这种形式的转场动画,动画资源不能使用插件的(可以使用宿主或系统的);
- 插件中弹通知,需要统一处理,走宿主的逻辑,通知中的资源文件不能使用插件的(可以使用宿主或系统的)。
- 插件的Activity中不支持动态申请权限。
插件中四大组件的已知约束
Activity,支持LaunchMode和theme
- 透明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);
Service,支持跨进程bind service
无约束
BroadcastReceiver
- 静态Receiver将被动态注册,当宿主停止运行时,外部广播将无法唤醒宿主;
- 由于动态注册的缘故,插件中的Receiver必须通过隐式调用来唤起。
ContentProvider,支持跨进程访问ContentProvider
1)分情况,插件调用自己的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);
2)插件调用宿主和外部的ContentProvider,无约束;
3)宿主调用插件的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文件的适配。
网友评论