VirtualAPK

2022-04-17  本文已影响0人  小鱼你好

https://github.com/didi/VirtualAPK

功能完备

四大组件均不需要在宿主manifest中预注册,每个组件都有完整的生命周期。

  1. Activity:支持显示和隐式调用,支持Activity的themeLaunchMode,支持透明主题;
  2. Service:支持显示和隐式调用,支持Service的startstopbindunbind,并支持跨进程bind插件中的Service;
  3. Receiver:支持静态注册和动态注册的Receiver;
  4. ContentProvider:支持provider的所有操作,包括CRUDcall方法等,支持跨进程访问插件中的Provider。

优秀的兼容性

入侵性极低

VirtualAPK和主流开源框架的对比

如下是VirtualAPK和主流的插件化框架之间的对比。

特性 DynamicLoadApk DynamicAPK Small DroidPlugin VirtualAPK
支持四大组件 只支持Activity 只支持Activity 只支持Activity 全支持 全支持
组件无需在宿主manifest中预注册 ×
插件可以依赖宿主 ×
支持PendingIntent × × ×
Android特性支持 大部分 大部分 大部分 几乎全部 几乎全部
兼容性适配 一般 一般 中等
插件构建 部署aapt Gradle插件 Gradle插件

通俗易懂地说

  1. 如果你是要加载微信、支付宝等第三方APP,那么推荐选择DroidPlugin;
  2. 如果你是要加载一个内部业务模块,并且这个业务模块很难从主工程中解耦,那么VirtualAPK是最好的选择。

抽象地说

  1. 如果你要加载一个插件,并且这个插件无需和宿主有任何耦合,也无需和宿主进行通信,并且你也不想对这个插件重新打包,那么推荐选择DroidPlugin;
  2. 除此之外,在同类的开源中,推荐大家选择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);
}

探究原理

基本原理

四大组件的实现原理

使用注意事项

运行时获取资源需要通过packageId来映射apk中的资源文件,不同apk的packageId值不能相同,所以插件的packageId范围是介于系统应用(0x01,0x02,...具体占用多少值视系统而定)和宿主(0x7F)之间。
宿主和插件同时依赖公共的本地jar文件或library module不支持自动去除,需要部署到maven或其它依赖管理服务器

插件安装使用so的abi必须与宿主保持一致,而宿主的abi类型在app安装时根据apk配置确定,并不再改变。请按以下步骤检查项目配置:

  1. 宿主apk中未使用so,则按设备默认abi类型安装。如果插件中使用so并报此错误,则需要在宿主中放一个占位的同abi类型so。
  2. 插件中使用的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仍然有一些小小的约束,如下注意事项,请务必仔细阅读。

目前暂不支持的特性

  1. 暂不支持Activity的一些不常用特性(比如process、configChanges等属性),但是支持theme、launchMode和screenOrientation属性;
  2. overridePendingTransition(int enterAnim, int exitAnim)这种形式的转场动画,动画资源不能使用插件的(可以使用宿主或系统的);
  3. 插件中弹通知,需要统一处理,走宿主的逻辑,通知中的资源文件不能使用插件的(可以使用宿主或系统的)。
  4. 插件的Activity中不支持动态申请权限。

插件中四大组件的已知约束

Activity,支持LaunchMode和theme

<style name="AppTheme.Transparent">
    <item name="android:windowBackground">@android:color/transparent</item>
    <item name="android:windowIsTranslucent">true</item>
</style>

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

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文件的适配。

上一篇下一篇

猜你喜欢

热点阅读