插件化框架VirtualAPK基本使用

VirtualAPK的特性

VirtualAPK是滴滴出行自研的一款优秀的插件化框架,主要有如下几个特性。

功能完备
  • 支持几乎所有的Android特性;
  • 四大组件方面
四大组件均不需要在宿主manifest中预注册,每个组件都有完整的生命周期。
  1. Activity:支持显示和隐式调用,支持Activity的theme和LaunchMode,支持透明主题;
  2. Service:支持显示和隐式调用,支持Service的start、stop、bind和unbind,并支持跨进程bind插件中的Service;
  3. Receiver:支持静态注册和动态注册的Receiver;
  4. 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和主流的插件化框架之间的对比。

特性DynamicLoadApkDynamicAPKSmallDroidPluginVirtualAPK
支持四大组件只支持Activity只支持Activity只支持Activity全支持全支持
组件无需在宿主manifest中预注册×
插件可以依赖宿主×
支持PendingIntent×××
Android特性支持大部分大部分大部分几乎全部 几乎全部
兼容性适配一般一般中等
插件构建部署aaptGradle插件Gradle插件
为什么选择VirtualAPK?

已经有那么多优秀的开源的插件化框架,滴滴为什么要重新造一个轮子呢?

  1. 大部分开源框架所支持的功能还不够全面 除了DroidPlugin,大部分都只支持Activity。
  2. 兼容性问题严重,大部分开源方案不够健壮 由于国内Rom尝试深度定制Android系统,这导致插件框架的兼容性问题特别多,而目前已有的开源方案中,除了DroidPlugin,其他方案对兼容性问题的适配程度是不足的。
  3. 已有的开源方案不适合滴滴的业务场景 虽然说DroidPlugin从功能的完整性和兼容性上来看,是一款非常完善的插件框架,然而它的使用场景和滴滴的业务不符。
    DroidPlugin侧重于加载第三方独立插件,比如微信,并且插件不能访问宿主的代码和资源。而在滴滴打车中,其他业务模块均需要宿主提供的订单、定位、账号等数据,因此插件不可能和宿主没有交互。
    其实在大部分产品中,一个业务模块实际上并不能轻而易举地独立出来,它们往往都会和宿主有交互,在这种情况下,DroidPlugin就有点力不从心了。
    基于上述几点,我们只能重新造一个轮子,它不但功能全面、兼容性好,还必须能够适用于有耦合的业务插件,这就是VirtualAPK存在的意义。
在加载耦合插件方面,VirtualAPK是开源方案的首选,推荐大家使用。
通俗易懂地说
  1. 如果你是要加载微信、支付宝等第三方APP,那么推荐选择DroidPlugin;
  2. 如果你是要加载一个内部业务模块,并且这个业务模块很难从主工程中解耦,那么VirtualAPK是最好的选择。
抽象地说
  1. 如果你要加载一个插件,并且这个插件无需和宿主有任何耦合,也无需和宿主进行通信,并且你也不想对这个插件重新打包,那么推荐选择DroidPlugin;
  2. 除此之外,在同类的开源中,推荐大家选择VirtualAPK。

VirtualAPK的工作过程

VirtualAPK对插件没有额外的约束,原生的apk即可作为插件。插件工程编译生成apk后,即可通过宿主App加载,每个插件apk被加载后,都会在宿主中创建一个单独的LoadedPlugin对象。如下图所示,通过这些LoadedPlugin对象,VirtualAPK就可以管理插件并赋予插件新的意义,使其可以像手机中安装过的App一样运行。

基本使用

VirtualAPK 对插件没有额外的约束,原生的apk即可作为插件。插件工程编译生成apk后,即可通过宿主App加载,每个插件apk被加载后,都会在宿主中创建一个单独的LoadedPlugin对象。通过这些LoadedPlugin对象,VirtualAPK就可以管理插件并赋予插件新的意义,使其可以像手机中安装过的App一样运行。
在这里插入图片描述

宿主项目的配置

1.在项目根目录的build.gradle文件中加入VirtualAPK依赖:

dependencies {
    classpath 'com.didi.virtualapk:gradle:0.9.8.6'
}

2.在app根目录的buil.gradle文件中应用VirtualAPK host插件:

apply plugin: 'com.didi.virtualapk.host'

3.在app根目录的buil.gradle文件中引用VirtualAPK远程库:

dependencies {
    implementation 'com.didi.virtualapk:core:0.9.6'
}

4.在项目Application中初始化插件:

public class BaseApplication extends Application {

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        // 初始化VirtualAPK
        PluginManager.getInstance(base).init();
    }

    @Override
    public void onCreate() {
        super.onCreate();
        // 加载存储根目录的插件apk,实际项目中按需保存
        File plugin = new File(Environment.getExternalStorageDirectory(), "plugin.apk");
        if (plugin.exists()) {
            try {
                PluginManager.getInstance(this).loadPlugin(plugin);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

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

<application
    android:name=".BaseApplication">
</application>

5.调用插件
com.lwx.plugin_login是插件工程的包名,com.lwx.plugin_login.LoginActivity是插件工程中的类,插件工程的包名可以和宿主工程相同,但是相同包名下的类名不能相同,资源名称也不能相同。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        findViewById(R.id.btn_main_activity_host).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        if (PluginManager.getInstance(MainActivity.this).getLoadedPlugin("com.lwx.plugin_login") == null) {
            Toast.makeText(MainActivity.this, "Plugin is not loaded!", Toast.LENGTH_SHORT).show();
        } else {
            Intent intent = new Intent();
            intent.setClassName("com.lwx.plugin_login", "com.lwx.plugin_login.LoginActivity");
            intent.putExtra("name","hehe");
            intent.putExtra("pwd","123456");
            startActivity(intent);
        }
    }
});
    }
}

6.权限(6.0以上手机请动态申请,或手动在设置中打开权限)

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

7.混淆配置

-keep class com.didi.virtualapk.internal.VAInstrumentation { *; }
-keep class com.didi.virtualapk.internal.PluginContentResolver { *; }

-dontwarn com.didi.virtualapk.**
-dontwarn android.**
-keep class android.** { *; }
插件项目的配置

1.在项目根目录的build.gradle文件中加入VirtualAPK依赖(如果插件和宿主在同一个项目中,则不需要再重复添加):

dependencies {
    classpath 'com.didi.virtualapk:gradle:0.9.8.6'
}

2.在app根目录(或插件module)的buil.gradle文件中应用VirtualAPK plugin插件:

apply plugin: 'com.didi.virtualapk.plugin'

3.在app根目录(或插件module)的buil.gradle文件中配置VirtualAPK:
需要在buil.gradle文件中的最后位置进行此配置

virtualApk {
    // 插件资源表中的packageId,需要确保不同插件有不同的packageId
    // 范围 0x1f - 0x7f
    packageId = 0x6f

    // 宿主工程application模块的路径,插件的构建需要依赖这个路径
    // targetHost可以设置绝对路径或相对路径
    // ../VirtualAPKHostDemo/app 代表 VirtualAPKDemo/VirtualAPKHostDemo/app
    targetHost = '../VirtualAPKDemo/app'

    // 默认为true,如果插件有引用宿主的类,那么这个选项可以使得插件和宿主保持混淆一致
    applyHostMapping = true
}
构建宿主与插件
宿主

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

gradlew clean assembleRelease

如果不想输入命令,还可以这样:
在这里插入图片描述

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

插件

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

gradlew clean assemblePlugin

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

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

注意:因为assemblePlugin依赖于assembleRelease,所以插件包均是Release包,不支持debug模式的插件包。(出现找不到插件的问题时,先检查是否给插件配置签名文件)

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

插件与宿主进行交互

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

dependencies {
    implementation 'com.x.x.x.A'
}

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

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

探究原理

基本原理
  • 合并宿主和插件的ClassLoader 需要注意的是,插件中的类不可以和宿主重复
  • 合并插件和宿主的资源 重设插件资源的packageId,将插件资源和宿主资源合并
  • 去除插件包对宿主的引用 构建时通过Gradle插件去除插件对宿主的代码以及资源的引用
四大组件的实现原理
  • Activity 采用宿主manifest中占坑的方式来绕过系统校验,然后再加载真正的activity;
  • Service 动态代理AMS,拦截service相关的请求,将其中转给Service Runtime去处理,Service Runtime会接管系统的所有操作;
  • Receiver 将插件中静态注册的receiver重新注册一遍;
  • ContentProvider 动态代理IContentProvider,拦截provider相关的请求,将其中转给Provider Runtime去处理,Provider Runtime会接管系统的所有操作。
    如下是VirtualAPK的整体架构图,更详细的内容请大家阅读源码。
    在这里插入图片描述

原理详解可以看大神的博客:https://blog.csdn.net/lmj623565791/article/details/75000580
demo地址

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值