VirtualAPK:滴滴 Android 插件化的实践之路

article/2025/10/20 12:07:39

作者简介: 任玉刚,滴滴出行 Android 技术专家,《Android 开发艺术探索》作者,插件化框架 dynamic-load-apk 的发起者,CSDN 移动开发博客专家,曾当选 CSDN 2014、2015年度十大博客之星。热爱技术,热爱开源,凡事喜欢刨根问底,长期活跃在 CSDN 和 GitHub。目前就职于滴滴出行 App 架构组,从事热修复和插件化相关的开发工作。
博客地址:http://blog.csdn.net/singwhatiwanna;
GitHub:https://github.com/singwhatiwanna。
本文为 CSDN 首发,欢迎技术投稿、约稿,给文章纠错,请发送邮件至mobile@csdn.net。

一、前言

在 Android 插件化技术日新月异的今天,开发并落地一款插件化框架到底是简单还是困难,这个问题不同人会有不同的答案。但是我相信,完成一个插件化框架的 Demo 并不是多难的事儿,然而要开发一款完善的插件化框架却并非易事,尤其在国内,各大 ROM 厂商都对 Android 系统做了一定程度的定制,这更进一步加剧了 Android 本身的碎片化问题。

滴滴出行在插件化上的探索起步较晚,由于业务发展较快,迭代占据了大量的时间,这使得我们在2016年才开始研究这方面的技术。经过半年的开发、测试、适配和线上验证,今天,我们正式推出一款较为完善的插件化框架——VirtualAPK。之所以现在推出,是因为 VirtualAPK 在内部已经得到了很好的验证,我们在迭代过程中不断地做机型适配和细节特性的支持,目前已达到一个非常稳定的状况,足以支撑滴滴部分乃至全部业务的动态发版需求。目前滴滴出行最新版本(v5.0.4)上,小巴和接送机业务均为插件,大家可以去体验。

二、插件化的现状

到目前为止,业界已经有很多优秀的开源项目,比如早期的基于静态代理思想的 DynamicLoadApk,随后的基于占坑思想的 DynamicApk、Small,还有360手机助手的 DroidPlugin。它们都是优秀的开源项目,很大程度上促进了国内插件化技术的发展。

尽管有如此多的优秀框架存在,但是兼容性问题仍然是制约插件化发展的一大难题。一款插件化框架,也许可以在一款手机上完美运行,但是在数以千万的设备上却总是容易存在这样那样的兼容性问题。我相信上线过插件化的工程师应该深有体会。滴滴为什么还要自研一款新的插件化框架?因为我们需要一款功能完备、兼容性优秀、适用于滴滴业务的插件化框架,目前市面上的开源不能满足我们的需求,所以必须重新造轮子,于是 VirtualAPK 诞生了。

三、VirtualAPK 的诞生

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

1. 功能完备

  • 支持几乎所有的 Android 特性;
  • 四大组件方面:四大组件均不需要在宿主manifest中预注册,每个组件都有完整的生命周期。

    • Activity:支持显示和隐式调用,支持 Activity 的 themeLaunchMode,支持透明主题;
    • Service:支持显示和隐式调用,支持 Service 的 startstopbindunbind,并支持跨进程 bind 插件中的 Service;
    • Receiver:支持静态注册和动态注册的 Receiver;
    • ContentProvider:支持 provider的所有操作,包括 CRUDcall 方法等,支持跨进程访问插件中的 Provider。
  • 自定义View:支持自定义 View,支持自定义属性和 style,支持动画;

  • PendingIntent:支持 PendingIntent 以及和其相关的 AlarmNotificationAppWidget
  • 支持插件 Application 以及插件 manifest 中的 meta-data
  • 支持插件中的so

2. 优秀的兼容性

  • 兼容市面上几乎所有的 Android 手机,这一点已经在滴滴出行客户端中得到验证;
  • 资源方面适配小米、Vivo、Nubia 等,对未知机型采用自适应适配方案;
  • 极少的 Binder Hook,目前仅仅 hook 了两个 Binder:AMSIContentProvider,Hook过程做了充分的兼容性适配;
  • 插件运行逻辑和宿主隔离,确保框架的任何问题都不会影响宿主的正常运行。

3. 入侵性极低

  • 插件开发等同于原生开发,四大组件无需继承特定的基类;
  • 精简的插件包,插件可以依赖宿主中的代码和资源,也可以不依赖;
  • 插件的构建过程简单,通过Gradle插件来完成插件的构建,整个过程对开发者透明。

四、VirtualAPK 的工作过程

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

VirtualAPK

1. VirtualAPK 的运行形态

我们计划赋予 VirtualAPK 两种工作形态,耦合形态和独立形态。目前 VirtualAPK 对耦合形态已经有了很好的支持,接下来将计划支持独立形态。

  • 耦合形态:插件对宿主可以有代码或者资源的依赖,也可以没有依赖。这种模式下,插件中的类不能和宿主重复,资源 id 也不能和宿主冲突。这是 VirtualAPK 的默认形态,也是适用于大多数业务的形态。
  • 独立形态:插件对宿主没有代码或者资源的依赖。这种模式下,插件和宿主没有任何关系,所以插件中的类和资源均可以和宿主重复。这种形态的主要作用是用于运行一些第三方 apk。

2. 如何使用

第一步: 初始化插件引擎

@Override
protected void attachBaseContext(Context base) {super.attachBaseContext(base);PluginManager.getInstance(base).init();
}

第二步:加载插件

public class PluginManager {public void loadPlugin(final File apk);public void loadPlugin(final String moduleCode);
}

我们对上述加载过程进行了一些封装,通过如下方式即可异步地去加载一个插件。

// 示例:启动插件中的Activity
DownloadManager downloadManager = DownloadManager.getInstance(this);
downloadManager.loadModule("com.ryg.test", true, this, new ILoadListener() {@Overridepublic void onLoadEnd(int resultCode) {if (resultCode == ILoadListener.LOAD_SUCCESS) {Intent intent = new Intent();intent.setClassName("com.ryg.test", "com.ryg.test.MainActivity");startActivity(intent);} else {// todo load plugin failed}}
});

当插件入口被调用后,插件的后续逻辑均不需要宿主干预,均走原生的 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 pluginIntent service = new Intent(this, BookManagerService.class);bindService(service, mConnection, Context.BIND_AUTO_CREATE);// start activity in pluginIntent intent = new Intent(this, TCPClientActivity.class);startActivity(intent);
}

五、探究原理

1. 基本原理

  • 合并宿主和插件的ClassLoader:需要注意的是,插件中的类不可以和宿主重复;
  • 合并插件和宿主的资源:重设插件资源的packageId,将插件资源和宿主资源合并;
  • 去除插件包对宿主的引用:构建时通过 Gradle 插件去除插件对宿主的代码以及资源的引用。

2. 四大组件的实现原理

VirtualAPK

  • Activity:采用宿主 manifest 中占坑的方式来绕过系统校验,然后再加载真正的 Activity;
  • Service:动态代理 AMS,拦截 Service 相关的请求,将其中转给一个虚拟空间(Matrix)去处理,Matrix 会接管系统的所有操作;
  • Receiver:将插件中静态注册的 Receiver 重新注册一遍;
  • ContentProvider:动态代理 IContentProvider,拦截 Provider 相关的请求,将其中转给一个虚拟空间(Matrix)去处理,Matrix 会接管系统的所有操作。

以下是 VirtualAPK 的整体结构图。

VirtualAPK

六、填坑之路

在实践中我们遇到了很多很多的问题,比如机型适配、API 版本适配、Binder Hook 的稳定性保证等问题,这里拿一个典型的资源适配问题来说明。

其实这是一个很无奈的问题,由于国内各大 ROM 厂商喜欢深度定制 Android 系统,所以就出现了这种适配问题。

正常情况下我们通过如下代码去创建插件的 Resources 对象:

Resources newResources = new Resources(assetManager,hostResources.getDisplayMetrics(), hostResources.getConfiguration());

然后在 Vivo 手机上,竟然出现了如下的类型转换错误,看起来是 Vivo 自己派生了 Resources 的子类。

java.lang.RuntimeException: Unable to start activity ComponentInfo{com.sdu.didi.psnger/com.didi.virtualapk.core.A$1}: java.lang.ClassCastException: android.content.res.Resources cannot be cast to android.content.res.VivoResourcesat android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2196)at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2245)at android.app.ActivityThread.access$800(ActivityThread.java:140)at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1202)at android.os.Handler.dispatchMessage(Handler.java:102)at android.os.Looper.loop(Looper.java:136)at android.app.ActivityThread.main(ActivityThread.java:5143)at java.lang.reflect.Method.invokeNative(Native Method)at java.lang.reflect.Method.invoke(Method.java:515)at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:786)at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:602)at dalvik.system.NativeStart.main(Native Method)
Caused by: java.lang.ClassCastException: android.content.res.Resources cannot be cast to android.content.res.VivoResourcesat android.app.ResourcesManager.getTopLevelResources(ResourcesManager.java:236)at android.app.ContextImpl.<init>(ContextImpl.java:2057)at android.app.ContextImpl.createActivityContext(ContextImpl.java:2008)at android.app.ActivityThread.createBaseContextForActivity(ActivityThread.java:2207)at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2140)... 11 more

于是反编译了下 Vivo 的 Framework 代码,果不其然,在如下代码中进行了类型转换,所以在加载插件资源的时候就报错了。

@VivoHook(hookType = VivoHookType.NEW_METHOD)public Resources getTopLevelResources(String pkgName, String resDir, int displayId, Configuration overrideConfiguration, CompatibilityInfo compatInfo, IBinder token) {Resources resources = getTopLevelResources(resDir, displayId, overrideConfiguration, compatInfo, token);if (resources != null) {((VivoResources) resources).init(pkgName);}return resources;}

为了解决这个问题,我们分析了 VivoResources 的代码实现,然后在创建插件资源的时候,采用了如下的代码。

private static final class VivoResourcesCompat {private static Resources createResources(Resources hostResources, AssetManager assetManager) throws Exception {Class resourcesClazz = Class.forName("android.content.res.VivoResources");Resources newResources = (Resources)ReflectUtil.invokeConstructor(resourcesClazz,new Class[]{AssetManager.class, DisplayMetrics.class, Configuration.class},new Object[]{assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration()});return newResources;}
}

除了 Vivo 以外,有类似问题的还有 MIUI、Nubia 以及其他不知名的机型。而且在 Vivo 手机上,除了类型转换错误的问题,还有其他很坑的问题。

事实上我们还处理了很多其他的坑,这里无法一一说明,所以说如何保证插件化的稳定性是一件很有技术挑战的事情。

七、一些暂时不支持的特性

由于种种原因,VirtualAPK 目前未能支持所有的 Android 的特性,已知的有如下几点:

  • 不支持 Activity 的部分属性,比如 processconfigChanges 等;
  • 暂不支持 overridePendingTransition(int enterAnim, int exitAnim) 这种形式的转场动画;
  • 插件中弹通知,不能使用插件中的资源,比如图片。

八、开源计划

我们的目标是打造一款功能完备的插件化框架,使得各个业务线都能以插件的形式集成,从而实现 Android App 的热更新能力。

目前 VirtualAPK 还有一些特性需要进一步完善,待完善后,将会进行开源计划。我们期望 VirtualAPK 开源后,可以让其他 App 能够无缝集成,无需考虑细节实现和兼容性问题即可轻松拥有热更新能力。


了解最新移动开发相关信息和技术,请关注 mobilehub 公众微信号(ID: mobilehub)。

mobilehub


http://chatgpt.dhexx.cn/article/JvYSVC0s.shtml

相关文章

Unity刚体

1、Dynamic&#xff1a;动态类型 受重力和力的影响移动和旋转 Material&#xff1a; 物理材质&#xff0c;在刚体上设置了物理材质&#xff0c;如果子物体有碰撞器但是没有设置材质则会通用刚体的物理材质 如果不设置&#xff0c;将使用在Physics 2D窗口中设置的默认材质(Physi…

android view 的测量过程

参考任玉刚主席的《android开发艺术探索》所写。

2017年终总结,开始写博客的第一年

结束校园生活&#xff0c;开始工作 今年是我开始工作的第一年&#xff0c;前半个学期实习阶段&#xff0c;开发了三个上线的应用&#xff0c;之前在学校的时候&#xff0c;做过不少练手的项目&#xff0c;有时心血来潮&#xff0c;还做了一些自己认为有趣的项目&#xff0c;但是…

阿里组织新调整:张勇兼任云业务总裁“敏捷组织”是内核

雷递网 雷建平 12月29日 岁末年初之际&#xff0c;在小米进行管理层调整后&#xff0c;阿里巴巴也进行了新一轮调整。 12月29日&#xff0c;阿里巴巴集团董事会主席兼CEO张勇发布内部邮件&#xff0c;宣布张建锋不再担任阿里云总裁&#xff0c;继续担任阿里达摩院院长&#xff…

鲲鹏院武志强课题组诚聘博士后

诚聘英才 Recruitment Season 佛山鲲鹏现代农业研究院 研究院简介 佛山鲲鹏现代农业研究院&#xff08;以下简称“鲲鹏院”&#xff09;成立于2021年&#xff0c;由中国农业科学院与佛山市人民政府、南海区人民政府依托中国农业科学院&#xff08;深圳&#xff09;农业基因组研…

刚体

概述 在整个 Unity 物理系统中&#xff0c;最重要概念就是刚体 Rigidbody。 刚体是物理学中的概念&#xff0c;它是指在运动中和受力后&#xff0c;形状和大小不变&#xff0c;并且内部各点相对位置不变的物体。刚体是一种为了方便物理计算而提出的理想化模型&#xff0c;在不…

技术人员如何从容转型项目经理?

科技在发展&#xff0c;时代在进步&#xff0c;项目越来越复杂&#xff0c;越来越多的技术人员面临技术转型&#xff0c;那么&#xff0c;如何成功转型成了众多技术人员面临的新挑战。 首先我们必须要搞清楚&#xff0c;为什么需要技术人员转型项目经理&#xff1f;项目经理不…

阿里云杨国彦:云上护航,陪伴成长

以下整理自杨国彦在云栖大会“云上成就创新梦想”论坛中发表的《云上护航&#xff0c;陪伴成长》的主题演讲&#xff09; 11月5日&#xff0c;以“云上成就创新梦想”为主题的中小企业云上创新论坛在浙江杭州云栖小镇举行&#xff0c;论坛聚集政府、资本、媒体、机构等相关领导…

滴滴技术专家任玉刚:让你的职业迷茫从哪来回哪去

今天给大家推荐的是《Android开发艺术探索》的作者&#xff08;滴滴技术专家&#xff09;任玉刚老师的直播课&#xff0c;感兴趣的同学可以通过文末的方式参与本次活动&#xff08;喜欢的同学欢迎转发&#xff09;。 我是任玉刚&#xff0c; 我来我想说。 01 收到过无数同学的…

从菜鸟到资深工程师的进阶之路

专访任玉刚&#xff1a;从菜鸟到资深工程师的进阶之路 发表于 2015-12-22 08:25| 11131次阅读| 来源 CSDN| 31 条评论| 作者 夏夏 专访 iOS Android 任玉刚 开发者 CSDN博客 allowtransparency"true" frameborder"0" scrolling"no" src"h…

kafka sasl_ssl配置

一、切换到存储证书的路径 我这里在家目录中的创建了ssl文件夹 mkdir ssl && cd ssl 二、生成服务端密钥库 keytool -keystore server.keystore.jks -alias localhost -validity 365 -genkey验证证书&#xff1a; keytool -list -v -keystore server.keystore.jks …

KAFKA SASL配置 记录

kafka配置SASL 第1步 将kafka_client_jaas.conf/kafka_server_jaas.conf/kafka_zoo_jaas.conf三个文件放入kafka的config文件夹中&#xff0c;文件中配置用户&#xff0c;superadmin用户必须配置。 kafka_client_jaas.conf内容如下 KafkaClient { …

WIN10 VS2019 编译Cyrus SASL

环境 下载安装Visual Studio 2019 安装时在【工作负载】必须勾选【使用C的桌面开发】下载cyrus-sasl源码 从Github上clone或者下载zip包&#xff0c;我本来是需要2.1.26&#xff0c;但是从从https://www.cyrusimap.org/releases/下载对应版本的源码包编译都有问题&#xff0c;…

Kafka3.0 SASL安全认证

下面主要介绍Kafka两种认证方式 kafka验证方式&#xff1a; SASL/PLAIN&#xff1a;不能动态添加用户配置文件写死账号密码 SASL/SCRAM&#xff1a; 可以动态的添加用户 SASL/PLAIN方式 cd /usr/local/kafka/kafka_2.12-3.0.1/bin/ ## 复制一份saslcp kafka-server-start.…

集成OpenLDAP与Kerberos实现统一认证(三):基于SASL/GSSAPI深度集成

文章目录 1. 写作背景2. 既定目标3. 重要概念3.1 SASL3.2 GSSAPI3.3 SASL与GSSAPI的关系3.4 saslauthd3.5 Kerberos化 4. 核心原理4.1 基于SASL/GSSAPI实现Kerberos账号登录OpenLDAP4.2 基于olcAuthzRegexp规则映射Kerberos与OpenLDAP账号4.3 基于saslauthd进行委托认证 5. 安装…

kafka sasl java_Kafka安装及开启SASL_PLAINTEXT认证(用户名和密码认证)

前些日子要封装一个kafka的客户端驱动&#xff0c;配置了下kafka环境&#xff0c;发现配置复杂度完爆rabbitmq很多倍啊&#xff0c;而且发布订阅模式使用起来也很麻烦&#xff0c;可能就胜在分布式了吧。 kafka需要java环境&#xff0c;自行安装java sdk 1.8. 官方加载安装包&a…

go kafka 配置SASL认证及实现SASL PLAIN认证功能

用户认证功能&#xff0c;是一个成熟组件不可或缺的功能。在0.9版本以前kafka是没有用户认证模块的&#xff08;或者说只有SSL&#xff09;&#xff0c;好在kafka0.9版本以后逐渐发布了多种用户认证功能&#xff0c;弥补了这一缺陷&#xff08;这里仅介绍SASL&#xff09;。 本…

kafka集群开启sasl认证

kafka集群开启sasl认证 sasl认证 sasl 是扩展C/S模式验证能力的一种认证机制。它可以规范客户端和服务端传输应答和传输内容编码&#xff0c;简而言之sasl决定了认证的规则&#xff0c;即客户端如何存储身份证书、客户端与服务端如何校验密码都由sasl决定。当我们的客户端通过…

mysql sasl_SASL认证失败的原因(authentication failed)

SASL认证失败的原因(authentication failed) (2012-06-15 00:45:43) 标签&#xff1a; 杂谈 authentication failed) SASL认证失败的原因可分为如下几个可能的方面&#xff1a; Permission问题&#xff1a;对系统用户的SASL Auth尤其重要&#xff0c;要保证postfix用户(smtpd)对…

Kafka安全(以SASL+ACL为例)

目录 1 Security2 SASLACL实现用户及权限认证2.1 下载2.2 Kafka服务配置2.3 修改Kafka 服务启动脚本2.4 配置server.properties2.5 启动Zookeeper2.6 启动Kafka 集群2.7 ACL2.7.1 admin2.7.2 生产者2.7.3 消费者2.7.4 sharga用户2.7.5 shargb用户2.7.6 说明 2.8 生产者客户端代…