写点什么

插件化库 VirtualAPK 详解

发布于: 2021 年 04 月 23 日
插件化库VirtualAPK详解

前篇文章《Android组件化和插件化开发》主要介绍了 Android 组件化和插件化的架构特点、两者的对比分析以及推荐了学习组件化的相关文章,本编主要介绍下目前插件化开源库的使用情况,以及着重介绍下 VirtualAPK 库,供大家参考。

插件化的技术背景

插件化主要就是利用动态加载技术


通过服务器配置一些参数,Android APP 获取这些参数再做出相应的逻辑,这是常有的事,比如现在大部分 APP 都有一个启动页面,如果到了一些重要的节日,APP 的服务器会配置一些与时节相关的图片,APP 启动时候再把原有的启动图换成这些新的图片,这样就能提高用户的体验了。


再则,早期个人开发者在安卓市场上发布应用的时候,如果应用里包含有广告,那么有可能会审核不通过,那么就通过在服务器配置一个开关,审核应用的时候先把开关关闭,这样应用就不会显示广告了;安卓市场审核通过后,再把服务器的广告开关给打开,以这样的手段规避市场的审核。所以现在安卓市场开始扫描 APK 里面的 Manifest 甚至 dex 文件,查看开发者的 APK 包里是否有广告的代码,如果有就有可能审核不通过。通过服务器怕配置开关参数的方法行不通了,开发者们开始想,“既然这样,能不能先不要在 APK 写广告的代码,在用户运行 APP 的时候,再从服务器下载广告的代码运行,再实现广告呢?”。答案是肯定的,这就是动态加载。


在程序运行的时候,加载一些程序自身原本不存在的可执行文件并运行这些文件里的代码逻辑。


看起来就像是应用从服务器下载了一些代码,然后再执行这些代码!


使用动态加载技术,一般来说会使得 Android 开发工作变得更加复杂,这种开发方式不是官方推荐的,不是目前主流的 Android 开发方式,Github 和 StackOverflow 上面外国的开发者也对此不是很感兴趣,外国相关的教程更是少得可怜,目前只有在大天朝才有比较深入的研究和应用,特别是一些 SDK 组件项目和 BAT 家族的项目上,Github 上的相关开源项目基本是国人在维护。


动态加载的大致过程就是


  • 把可执行文件(.so/dex/jar/apk)拷贝到应用 APP 内部存储;

  • 加载可执行文件;

  • 调用具体的方法执行业务逻辑;

几个主流插件化开源框架的对比


阿里的 atlas:Atlas 是伴随着手机淘宝不断发展而衍生出来的一个运行于 Android 系统上的插件化框架,也可以叫动态组件化框架,主要提供了解耦化、组件化、动态性的支持。是目前比较成熟的方案,功能强大,但相对的,使用和集成的难度也比较大。


腾讯的 Shadow:Shadow 是一个腾讯自主研发的 Android 插件框架,并且一直在维护中,但使用和集成难道稍大,有兴趣的可以研究下。

VirtualAPK 的接入

Github 地址:https://github.com/didi/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 {   @Override   protected void attachBaseContext(Context base) {      super.attachBaseContext(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 - 0x7f    packageId = 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


  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);
复制代码


  1. 插件调用宿主和外部的 ContentProvider,无约束;

  2. 宿主调用插件的 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 文件的适配。

1.3、执行生成插件 Plugin

  • 执行 assemablePlugin

  • 产生 Plugin 文件将插件 Plugin 安装到手机中


adb push ./app/build/outputs/plugin/release/com.alex.kotlin.virtualplugin_20190729172001.apk  /sdcard/plugin_test.apk
复制代码


注意问题

  1. 要先构建一次宿主 app,才可以构建 plugin,否则异常

  2. 插件布局文件中要设置资源的 ID,否则异常:Cannot get property 'id' on null objectplugin

  3. 增加 gradle.properties 文件并配置 android.useDexArchive=false,否则异常

1.4、在宿主程序中使用插件 Plugin

在宿主 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 需要注意的问题

集成开发问题

  1. 注意宿主工程的 application 模块的路径是否正确


virtualApk {    packageId = 0x6f    targetHost = '../../VirtualAPK/app' // 检测这个路径是否正确,相对路径或者绝对路径都行    applyHostMapping = true}
复制代码


  1. packageId:


  • 运行时获取资源需要通过 packageId 来映射 apk 中的资源文件,不同 apk 的 packageId 值不能相同,所以插件的 packageId 范围是介于系统应用(0x01,0x02,...具体占用多少值视系统而定)和宿主(0x7F)之间。

  • 多个插件的 packageId 和 packageName 一样,在宿主中需要确保是唯一的。


  1. com.android.tools.build:gradle“最高支持到 3.1.4,即在 virtualApk 工程中最高只能用 classpath 'com.android.tools.build:gradle:3.1.4'

  2. 目前是不支持 androidx 库的

  3. 插件中的 buildToolsVersion 似乎只能到 27.0.3,其他的会出错

  4. 宿主中和插件中的资源文件不能重复(如布局文件,资源 id 等)

  5. 插件依赖的所有 com.android.support 包在宿主都有显式依赖,并且版本和宿主保持一致

  6. 宿主和插件同时依赖公共的本地 jar 文件或 library module,在构建插件时并不会自动剔除:构建插件的依赖自动剔除功能仅支持内容稳定不变,路径稳定的资源,而本地的 jar 或其它资源的路径和内容都是可变更的,因此无法直接自动剔除,如果需要剔除,请将资源打包导出部署到 maven 或其它依赖管理服务器。如果资源不可公开发布,可在内网部署私有 maven 服务。

目前暂不支持的特性

  • 暂不支持 Activity 的一些不常用特性(比如 process、configChanges 等属性),但是支持 theme、launchMode 和 screenOrientation 属性;

  • overridePendingTransition(int enterAnim, int exitAnim)这种形式的转场动画,动画资源不能使用插件的(可以使用宿主或系统的);

  • 插件中弹通知,需要统一处理,走宿主的逻辑,通知中的资源文件不能使用插件的(可以使用宿主或系统的)。

  • 插件的 Activity 中不支持动态申请权限。

用户头像

还未添加个人签名 2020.10.09 加入

Android领域,打工人

评论

发布
暂无评论
插件化库VirtualAPK详解