当前位置:   article > 正文

滴滴插件化方案 VirtualApk 完全解析(一) 初识基本用法

滴滴virtualapk插件的.so文件,宿主app如何调用

转载请注明出处:https://juejin.im/post/5b61076851882519f6478156

本文出自 容华谢后的博客

1.介绍

VirtualApk GitHub地址

VirtualAPK是滴滴在2017年6月开源的一款插件化框架,支持Android四大组件,以及几乎所有的Android特性,通过Gradle来构建插件,集成与构建十分便捷,目前已经应用在 滴滴出行 App上,兼容市面上几乎所有的Android设备。

VirtualAPK支持的Android版本:Android 4.0.3(API 15) - Android P(API P)

什么是插件化?插件化的优势在哪里?

在开发的过程中,一个工程通常会被分为多个Module,用来区分不同的业务模块,一个主Module下面有多个业务Module,也就是我们常说的Library,发布的时候打成一个apk,所有的逻辑都在这一个apk中,当版本更新或者某一个Module出现问题时,只能是全量更新这个apk,如果过于频繁,用户肯定会不爽,然后给你个差评。

插件化的出现正好解决了这一难题,主Module不变(宿主),业务Module被分成一个个单独的工程,不再和主Module一起打包,而是分别打包成apk(插件),宿主启动后,动态的去加载插件。当某一个业务模块需要更新时,直接更新插件apk就可以了,全程在后台进行,不需要用户参与操作,但这样做对用户有一定风险,App通过审核后,有可能在后台加载一些非法插件,所以Google Play是禁止插件化App上线的,有海外市场的项目要注意下。

在插件化开发中,每个人负责不同的插件模块,插件之间完全解耦,开发完成后,再进行集成测试。一个宿主可以拥有多个插件,一个插件也可以为多个宿主服务。举个栗子,同一个公司,A项目需要集成一个第三方登录模块,B项目也需要,那么就可以把这个登录模块做成通用插件,供两个项目同时使用。

注意:集成插件化框架的APP不能在Google Play发布。

2.集成

注意:目前VirtualApk支持的gradle插件最新版本为3.0.0,若有更新请参考官方Demo。

宿主

  • 1.在项目根目录的build.gradle文件中加入VirtualAPK依赖:
  1. dependencies {
  2. classpath 'com.didi.virtualapk:gradle:0.9.8.4'
  3. }
  4. 复制代码
  • 2.在app根目录的buil.gradle文件中应用VirtualAPK host插件:
  1. apply plugin: 'com.didi.virtualapk.host'
  2. 复制代码
  • 3.在app根目录的buil.gradle文件中引用VirtualAPK远程库:
  1. dependencies {
  2. implementation 'com.didi.virtualapk:core:0.9.6'
  3. }
  4. 复制代码
  • 4.在项目Application中初始化插件:
  1. public class VirtualAPKHostApplication extends Application {
  2. @Override
  3. protected void attachBaseContext(Context base) {
  4. super.attachBaseContext(base);
  5. // 初始化VirtualAPK
  6. PluginManager.getInstance(base).init();
  7. }
  8. @Override
  9. public void onCreate() {
  10. super.onCreate();
  11. // 加载存储根目录的插件apk,实际项目中按需保存
  12. String pluginPath = Environment.getExternalStorageDirectory().getAbsolutePath().concat("/plugin.apk");
  13. File plugin = new File(pluginPath);
  14. if (plugin.exists()) {
  15. try {
  16. PluginManager.getInstance(this).loadPlugin(plugin);
  17. } catch (Exception e) {
  18. e.printStackTrace();
  19. }
  20. }
  21. }
  22. }
  23. 复制代码

不要忘了在清单文件中配置Application:

  1. <application
  2. android:name=".VirtualAPKHostApplication">
  3. </application>
  4. 复制代码
  • 5.调用插件

com.yl.plugin是插件工程的包名,com.yl.plugin.PluginActivity是插件工程中的类,插件工程的包名可以和宿主工程相同,但是相同包名下的类名不能相同,资源名称也不能相同。

  1. public class MainActivity extends AppCompatActivity implements View.OnClickListener {
  2. @Override
  3. protected void onCreate(Bundle savedInstanceState) {
  4. super.onCreate(savedInstanceState);
  5. setContentView(R.layout.activity_main);
  6. findViewById(R.id.btn_start_plugin_activity).setOnClickListener(this);
  7. }
  8. @Override
  9. public void onClick(View view) {
  10. if (PluginManager.getInstance(this).getLoadedPlugin("com.yl.plugin") == null) {
  11. Toast.makeText(this, "Plugin is not loaded!", Toast.LENGTH_SHORT).show();
  12. } else {
  13. Intent intent = new Intent();
  14. intent.setClassName("com.yl.plugin", "com.yl.plugin.PluginActivity");
  15. startActivity(intent);
  16. }
  17. }
  18. }
  19. 复制代码
  • 6.权限
  1. <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
  2. <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
  3. 复制代码
  • 7.混淆配置
  1. -keep class com.didi.virtualapk.internal.VAInstrumentation { *; }
  2. -keep class com.didi.virtualapk.internal.PluginContentResolver { *; }
  3. -dontwarn com.didi.virtualapk.**
  4. -dontwarn android.**
  5. -keep class android.** { *; }
  6. 复制代码

插件

  • 1.在项目根目录的build.gradle文件中加入VirtualAPK依赖:
  1. dependencies {
  2. classpath 'com.didi.virtualapk:gradle:0.9.8.4'
  3. }
  4. 复制代码
  • 2.在app根目录的buil.gradle文件中应用VirtualAPK plugin插件:
  1. apply plugin: 'com.didi.virtualapk.plugin'
  2. 复制代码
  • 3.在app根目录的buil.gradle文件中配置VirtualAPK:

需要在buil.gradle文件中的最后位置进行此配置

  1. virtualApk {
  2. // 插件资源表中的packageId,需要确保不同插件有不同的packageId
  3. // 范围 0x1f - 0x7f
  4. packageId = 0x6f
  5. // 宿主工程application模块的路径,插件的构建需要依赖这个路径
  6. // targetHost可以设置绝对路径或相对路径
  7. // ../VirtualAPKHostDemo/app 代表 VirtualAPKDemo/VirtualAPKHostDemo/app
  8. targetHost = '../VirtualAPKHostDemo/app'
  9. // 默认为true,如果插件有引用宿主的类,那么这个选项可以使得插件和宿主保持混淆一致
  10. applyHostMapping = true
  11. }
  12. 复制代码

3.构建宿主与插件

宿主

宿主的构建和正常apk的构建方式是相同的,可以通过Build > Generate Signed APK的方式,也可以通过下面的命令:

  1. gradlew clean assembleRelease
  2. 复制代码

如果不想输入命令,还可以这样:

构建完成的apk在app > build > outputs > apk > release目录下。

插件

插件采用下面的命令进行构建:

  1. gradlew clean assemblePlugin
  2. 复制代码

如果不想输入命令,还可以这样:

构建完成的apk在app > build > outputs > plugin > release目录下。

注意:因为assemblePlugin依赖于assembleRelease,所以插件包均是Release包,不支持debug模式的插件包。

到这里,宿主和插件就构建完成了,将插件apk拷贝至存储设备根目录,安装运行宿主apk,看下效果:

4.插件与宿主进行交互

插件和宿主通过引用相同依赖库的方式来进行交互,比如,宿主工程中引用了A库,

  1. dependencies {
  2. implementation 'com.x.x.x.A'
  3. }
  4. 复制代码

插件工程中如果也需要访问A库中的类和资源,那么可以在插件工程中同样引用A库,这样就可以和宿主工程共用A库了,插件构建的过程中会自动将A库从apk中剔除。

以一个全局变量举例:

A库中有一个全局变量V = false,如果在插件中将此变量设置为true,那么在宿主中获取到的V值则为true。

5.插件目前暂不支持的特性

以下内容来自官方WiKi

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

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

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

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

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

以下内容来自官方WiKi

Activity,支持LaunchMode和theme

  • 透明Activity,不能有启动模式,并且主题中必须含有android:windowIsTranslucent属性;
  1. <style name="AppTheme.Transparent">
  2. <item name="android:windowBackground">@android:color/transparent</item>
  3. <item name="android:windowIsTranslucent">true</item>
  4. </style>
  5. 复制代码
  • 插件中调用宿主的四大组件,请注意Intent中的包名。

VirtualAPK对Intent的处理遵循Android规范,插件之间乃至插件和宿主之间,包名是区分它们的唯一标识。

在下面的例子中,假如宿主的包名是"com.didi.virtualapk",然后在插件中启动一个宿主Activity,下面分别是错误和正确的示范:

  1. // 错误的用法,因为此时intent中的包名是插件的包名
  2. Intent intent = new Intent(this, HostActivity.class);
  3. startActivity(intent);
  4. // 正确的用法
  5. Intent intent = new Intent();
  6. intent.setClassName("com.didi.virtualapk", "com.didi.virtualapk.HostActivity");
  7. startActivity(intent);
  8. 复制代码

但是,如果想在插件中去访问插件的四大组件,那么就没有任何要求了,下面的代码会在插件Activity中尝试启动另一个插件Activity:

  1. // 正确的用法,因为此时intent中的包名是插件的包名
  2. Intent intent = new Intent(this, PluginActivity.class);
  3. startActivity(intent);
  4. 复制代码

Service,支持跨进程bind service

无约束

BroadcastReceiver

  • 静态Receiver将被动态注册,当宿主停止运行时,外部广播将无法唤醒宿主;

  • 由于动态注册的缘故,插件中的Receiver必须通过隐式调用来唤起。

ContentProvider,支持跨进程访问ContentProvider

1)分情况,插件调用自己的ContentProvider,如果需要用到call方法,那么需要将provider的uri放到bundle中,否则调用不生效;

  1. Uri bookUri = Uri.parse("content://com.didi.virtualapk.demo.book.provider/book");
  2. Bundle bundle = PluginContentResolver.getBundleForCall(bookUri);
  3. getContentResolver().call(bookUri, "testCall", null, bundle);
  4. 复制代码

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

3)宿主调用插件的ContentProvider,需要将provider的uri包装一下,通过PluginContentResolver.wrapperUri方法,如果涉及到call方法,参考1)中所描述的;

  1. String pkg = "com.didi.virtualapk.demo";
  2. LoadedPlugin plugin = PluginManager.getInstance(this).getLoadedPlugin(pkg);
  3. Uri bookUri = Uri.parse("content://com.didi.virtualapk.demo.book.provider/book");
  4. bookUri = PluginContentResolver.wrapperUri(plugin, bookUri);
  5. Cursor bookCursor = getContentResolver().query(bookUri,
  6. new String[]{"_id", "name"}, null, null, null);
  7. 复制代码

Fragment

推荐大家在Application启动的时候去加载插件,不然的话,请注意插件的加载时机。考虑一种情况,如果在一个较晚的时机去加载插件并且去访问插件中的资源,请注意当前的Context。比如在宿主Activity(MainActivity)中去加载插件,接着在MainActivity去访问插件中的资源(比如Fragment),需要做一下显示的hook,否则部分4.x的手机会出现资源找不到的情况。

  1. String pkg = "com.didi.virtualapk.demo";
  2. PluginUtil.hookActivityResources(MainActivity.this, pkg);
  3. 复制代码

so文件的加载

为了提升性能,VirtualAPK在加载一个插件时并不会主动去释放插件中的so,除非你在插件apk的manifest中显式地指定VA_IS_HAVE_LIB为true,如下所示:

  1. <application
  2. android:name=".VAApplication"
  3. android:allowBackup="true"
  4. android:icon="@mipmap/ic_launcher"
  5. android:label="@string/app_name"
  6. android:supportsRtl="true"
  7. android:theme="@style/HostTheme">
  8. <meta-data
  9. android:name="VA_IS_HAVE_LIB"
  10. android:value="true" />
  11. ...
  12. </application>
  13. 复制代码

7.写在最后

到这里VirtualAPK的基本用法就介绍完了,如有错误或者遗漏的地方可以给我留言评论,谢谢!

代码已上传至GitHub,欢迎Star、Fork!

GitHub地址:https://github.com/alidili/Demos/tree/master/VirtualAPKDemo

本文Demo的Apk下载地址:

宿主:https://github.com/alidili/Demos/raw/master/VirtualAPKDemo/host.apk

插件:https://github.com/alidili/Demos/raw/master/VirtualAPKDemo/plugin.apk

后续会有系列文章对VirtualAPK的源码进行分析和学习,敬请期待!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/秋刀鱼在做梦/article/detail/923310
推荐阅读
相关标签
  

闽ICP备14008679号