插件化库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内部存储;
  • 加载可执行文件;
  • 调用具体的方法执行业务逻辑;

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

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

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

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

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中不支持动态申请权限。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,529评论 5 475
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,015评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,409评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,385评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,387评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,466评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,880评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,528评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,727评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,528评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,602评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,302评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,873评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,890评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,132评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,777评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,310评论 2 342

推荐阅读更多精彩内容