本期作者
罗星
哔哩哔哩资深开发工程师
一、前言
从产品运营角度来说,功能的用户触达是实现用户价值转化的最基本前提。所以如何快速将一个新的功能触达到用户,同时减少触达过程中对运营推广、用户带来额外的成本就成了一个必须被重视的课题。对于APP而言,以一个相对固定的节奏进行版本迭代,在一个迭代周期内,新版本基本可以达到一个相对较高的覆盖率。但对SDK而言,因为其并非直接面向用户,更像是一个ToB类产品,快速的版本迭代显然不是一个有效的途径,游戏SDK也不例外。
二、背景与目标
2.1 “三座大山”
  • 周期长,游戏SDK的功能触达用户需要一个月左右的时间。一个功能从需求到用户触达要经历SDK开发->SDK测试→游戏接入游戏测试→版本发布一个漫长的周期。另外,游戏的强更与非强更也直接影响到最终的触达时效。同时,为积极响应国家对个人信息保护的政策持续升级,长迭代周期的业务模式也成了必须“翻越”的首座“高峰”。
  • 成本高,目前接入哔哩哔哩游戏SDK的游戏成百上千款,发布一个版本,如果要同时覆盖全部游戏,中间对于运营来说是一个巨大的沟通和推广成本。就算全部通知沟通到位,众多的游戏在接入过程中,因为引擎差异、游戏研发人员对Native开发知识的掌握程度不同、兼容性问题等,导致在技术支持上也要付出相当大的人力成本。
  • 灰度能力不完善,为了保证一个新版本的稳定性,我们一般会通过一两款游戏接入上线进行灰度验证。一方面沟通游戏接入需要花费一定的成本。另一方面,因为不同游戏之间因为引擎、玩法、接入第三方库等的差异,在整体灰度效果上,覆盖的场景比较局限。无法以一定采样率进行全平台的灰度。
 2.2 目标
  • 实现功能快速触达;
  • 减少触达过程中对运营、游戏研发、技术支持以及用户带来额外的成本;
  • 具备全平台灰度的能力,进一步提升线上业务的稳健性。
如何翻越“三座大山”,实现业务价值转化呢?是否能将APP中惯用的热更新能力迁移到SDK场景中来使用呢?
三、热更新在游戏SDK场景下的思考
3.1 游戏SDK业务模式简介
因为游戏SDK与移动开发小伙伴们平时工作中接入的一些第三方SDK还是存在一定的差异。所以在分析具体业务与选型前,我先介绍一下游戏SDK的业务模式。
首先,应用场景不同。游戏SDK并非应用于原生APP,而是游戏。而不同游戏因开发引擎(Unity、Cocos、UE4等)的差异,在调用SDK相关API时需要封装一层Bridge。同时因为游戏开发者对Native开发的技术数量程度参差不齐,在SDK的接入过程中,需要花费较大的人力成本去提供技术支持。
其次,业务流程不同。游戏SDK主要是为游戏提供统一的登录、支付等体系化功能,在流程上存在一些线性的固定逻辑。比如以登录为例,原生APP的登录基本是用户点击登录→提交账号信息→服务端验证→返回登录结果→登录成功/失败。
但游戏的登录在这个过程中会增加很多强校验子流程,具体为:用户点击登录→提交账号信息→服务端验证→返回登录结果三无账号检测→ 防沉迷检测 → 游客升级 → 实名认证检测 → 防沉迷提醒→ 登录成功/失败。这些额外的固定流程在每次登录都必须强校验,且流程不可阻塞。任何一个环节检测不通过都会导致登录失败。
最后,界面交互不同。因为游戏SDK的所有功能界面,都是展示在游戏场景之上,所以所设计的页面都是以弹窗的形式进行展示。
下面是一个登录入口的截图,希望可以帮助您对游戏SDK业务场景有一个大概的了解。
图3.1 SDK登录入口截图
3.2 常用的热更新方案
目前行业内比较常用具备热更新能力的方案大致可以分为四类:动态化布局、虚拟运行环境、业务插件化和Web容器增强。
图3.2 不同方案的代表性框架
3.3 业务场景分析
具备热更新能力的方案百花齐放,我们该如何选择呢?鲁迅说,没有最好的技术方案,只有最契合业务的方案。
所以我们首先要做的就是对业务场景进行审视。我们将业务大概划分为三类:
  • 核心业务 —— 对于游戏SDK而言,核心业务就是指登录和支付。这部分业务是基础也是重点,对性能和稳定性有着极高的要求。
  • 附加业务—— 比如修改密码、账号升级等。生命周期长,需要长期维护;同时需求整体稳定、沉淀性高;业务逻辑结构相似,提炼性强;用户触发频率相对较低,对性能有一定的敏感性,但不追求极致。
  • 展示型业务—— 比如活动、公告等。内容调整频繁,整体逻辑性不强以展示为主。有一定的可沉淀性,对性能极致性不敏感,更多的是追求对需求和内容变化的快速响应。
3.3 技术选型
明确了不同业务场景的特征与需求重心,回到问题本身,我们如何选择合适的技术方案呢?
布局动态化方案通过布局引擎,对动态下发的布局模板进行解析,底层通过Native组件进行承载,保证了一定的动态化能力,同时充分发挥Native极致性能。但一个适配双端的页面模板的开发需要一定的配套设施开发,方案落地有一定的成本。对于需求相对稳定、业务逻辑可抽象性强的附加业务类型匹配度较高。
web容器增强方案通过提供一个高性能WebView容器,同时沉淀大量通用性bridge来承载html页面,实现和移动开发框架的有效结合,开发成本低,整体性能与Native有一定差距。对内容变更频繁但对性能要求不追求极致的展示型业务比较符合。
业务插件化是提供宿主APP,通过远程下载代表各个功能模块的插件,实现资源和代码的动态加载。整体开发模式与现有Native迭代没有太大差异,性能和稳定性最高,但仅支持Android平台。对登录、支付等核心业务插件化后业务价值较大。
图3.3 不同方案优缺点与适用场景对比
不同技术方案各有优劣,适用场景也有所差异,很难通过一种方案完全匹配业务需求。所以我们索性就采用新的方式——多种方案相结合,将SDK容器化,根据不同的需求类型和业务场景选择最佳的容器来承载,打造一个集多种容器于一身的复合型热更方案。
四、方案设计
 4.1 整体架构
本方案整体可以分为服务端、SDK、发布运维系统和质效保障四大部分。其中:
  • SDK:采用分层化设计,自上而下将SDK分为应用层、协议层、容器层、框架层、组件层、基建层和系统层。应用层主要是为游戏研发提供相应的API能力,如登录、支付等。协议层是建立统一的路由协议,对业务模块进行模块化拆分和路优化改造后,方便进行统一的调度和交互;容器层是本案的核心,即建立多种类型的容器承载不同类型和场景的业务需求;框架层主要是对容器层能力的支撑和管理,如插件管理、引擎管理、模板管理等。组件层是将不同的业务模块进行组件化拆分,化整为零实现模块解耦;基建层主要承载网络、图片、APM等通用能力;系统层主要是指Android和iOS双端的系统服务;
  • 服务端:主要负责根据不同用户的信息状态(设备信息、游戏信息、用户信息等)进行筛选匹配,完成产物的动态下发。
  • 发布运维系统:负责从发布、灰度配置、相关性能指标收集、全流程性能监控等工作。
  • 质效保障:建立完善的设计和编码规范,结合自动化检测能力,确保开发质量可靠;沉淀统一的工程手脚架、模板和组件提升开发效率。
通过对SDK的分层化设计改造,以多容器化模式承载不同的业务场景;建立统一的业务路由协议,对业务模块进行路优化改造,实现多容器之间的调度与交互;通用组件和基架做下沉处理,实现多容器多业务服务用;最后建立完善的发布运维系统,保障动态化产物从构建→ 配置→ 白名单内测→ 灰度→发布等全流程的跟踪监控,结合完整的流程规范与能效工具,保障整个系统的高效性与稳定性。 
图4.1 方案架构图 
4.2 子系统设计
4.2.1 动态化路由协议
4.2.1.1 整体思路
动态化路由总体设计思路是通过对业务组件进行组件化拆分,指定路由入口。然后根据业务流程,通过服务端动态下发相应的业务路由配置表。
当新增业务模块或现有业务模块有调整时,直接动态修改路由配置,即可完成新业务的调度与新流程的调整。有两个核心接口,通过RouteMapProcessService定义完整业务流程的路由表。通过不用业务模块实现RouteProcessService并完成相应的状态和逻辑处理后,实现流程的流转。
图4.2 RouteMapProcessService接口
图4.3 RouteProcessService接口
4.2.1.2 路由协议规则
路由协议整体采用Scheme URL的格式,主要分为Scheme、group、path、固定参数和自定义参数几个部分,其中:
Scheme:自定义协议,主要是方便内部和游戏研发侧对路由协议的调用
Group和Path:模块名和路径名,按需加载路由映射表
固定参数:一些与路由流程调度相关的必要参数,如:allow_failure,标识该流程是否为强校验流程。
自定义参数:不同业务场景所需特殊字段,如:通用web容器路由需要配置url参数
图4.4 路由协议结构示例
4.2.2 多容器
4.2.2.1 插件化容器
插件化容器我们采用的是VirtualAPK方案(此处手动鸣谢VirtualAPK开源项目组)。
其基本原理主要有三部分:
  • 合并素组和插件的ClassLoader;
  • 合并插件和宿主的资源;
  • 去除插件包对宿主的引用。
整体功能比较完备,支持Android四大组件,四大组件实现原理:
  • Activity:采用宿主manifest中的占坑方式绕过系统校验,然后再加载真正的Activity;
  • Service:动态代理AMS,拦截Service相关请求,将其中转给一个虚拟空间(Matrix)去处理,Matrix会结果系统的所有操作;
  • BroadcastReceiver:将插件中静态注册的广播重新注册一遍;
  • ContentProvider:动态代理IContentProvider,拦截Provider的相关请求,将其中转给Matrix,由Matrix接管。
具备较好的兼容性和较低的侵入性。整体框架如下图所示(图片来源于网络):
图4.5 VirtualAPK架构图
因为VirtualAPI从2017年开始已经停止维护,所以我们针对整个框架做了大量的升级和兼容工作,主要包括:
  • 升级对高系统版本的支持(目前已完美适配android 11)
  • 升级对对androidx的支持
  • 升级对gradle plugin高版本的兼容
  • 优化插件资源加载(包括res、layout、theme、so等)
  • 优化对四大组件中Service、Broadcast、ContentProvider的支持能力
  • 优化对类加载器、插件包安装等方面能力
  • 还有一些机型适配等方面的兼容性问题
4.2.2.2 动态化模板容器
动态化模板容器我们采用的是基于FlexboxLayout渲染引擎(谷歌开源框架)的Native + DSL方案。动态化能力建设主要可以分为四个部分:DSL定义、动态视图管理、视图构造和数据绑定。
  • DSL定义:基于Flexbox布局系统进行视图布局,采用JSON语言描述视图结构,使用属性区分视图控件类型。其中常用的属性大概有布局属性、视图属性、数据属性、交互属性等。
  • 动态视图管理:基于DSL语言进行页面样式编写→ 进入相应的动态化页面时请求服务端样式接口→ 服务端根据客户端相关信息(设备信息、游戏信息、用户信息等)进行策略匹配后下发模板→ 客户端接受到模板信息后进行校验、缓存、解析和渲染。
  • 视图构建:客户端为每一个UI组件创建对应的解析器,当客户端获取到动态模板后,由解析器觉得采用什么原生控件进行承载。因为业务场景的特殊性,目前在组件支持上主要以ImageView、TextView、Button、EditText等基础组件为主。
  • 数据绑定:对于一些接口数据,直接通过NodeJs服务在下发的页面模板中进行相应属性的配置。对于本地资源则在编写模板时进行指定。解析器完成对控件相应参数的设置。
整体架构如下图所示:
图4.6 动态化模板方案架构图
4.2.2.3 通用web容器
对于通用web容器相信很多app和场景下都会用到,就容器而言整体实现方式也大同小异。这里主要介绍以下我们SDK容器的一些差异点。
一方面因为游戏SDK本身场景的特殊性,页面基本以弹窗形式展示。所以我们对于整个容器的样式做了大量的个性化配置能力。
包括主题(原生主题、游戏主题)、样式(全屏、横屏、竖屏、特定大小、特定比例、自定义等样式)最大限度去保证不同游戏,不同需求在配置时的需要。另一方面就是JsBridge能力,我们梳理了目前业务场景中可能用到的一些bridge,包括(登录态、网络、埋点、设备信息、参数传递、页面跳转、双向交互等等)。
4.2.3 发布系统
对于发布系统,首先采用基于jenkins实现插件、动态化模板等动态化产物的自动、持续构建。然后,设计了一个从灰度配置、过滤配置和排除配置三个步骤的灵活发布方案。
  • 灰度配置:主要分为基于用户进行灰度分桶和基于设备进行灰度分桶两个维度。支持千分之一级别的灰度粒度。主要针对新功能热更时按0.1% -> 100%施行逐渐放量发布,最大限度降低可能发生的异常事件造成线上事故的损失。
  • 过滤配置:主要从SDK版本、游戏、渠道、ROM版本、网络环境、设备品牌、设备型号等维度进行灵活的配置,同时支持用户白名单配置。主要针对一些定制化需求和定向化优化需求。
  • 排除配置:排除配置从维度上与过滤配置相似,也是从SDK版本、游戏、渠道、ROM版本、网络环境、设备品牌、设备型号等维度进行配置,同时支持配置多组。但作用有所差异,主要用于线上偶发性缺陷和一些兼容适配问题的降级。通过灵活的配置方案,结合对热更新产物从下发、下载、应用等全生命周期的数据打点,对异常情况进行实时监控,发现问题快速进行配置兼容、回滚、修复,确保线上业务稳定运行。
通过灰度配置,实现灰度逐渐放量能力;通过过滤配置,可多维度灵活配置灰度范围;通过排除配置,规避一些可能发生的线上兼容性问题。三管齐下建立一个相对完善的灰度发布系统,实现全平台灰度能力。
五、实践
5.1 不同方案在业务侧的尝试
插件化方案目前主要应用与登录及其子流程(实名认证、防沉迷、未成年人保护等)、悬浮球(截屏、自动录屏发布、一键直播等)。
选择这两个场景主要处于两点考虑:一方面插件化本质上采用的是Native开发的方式,在整体性能和稳定性上可以得到有效保障,同时对端能力的依赖上没有任何瓶颈。另一方面从业务场景上来说,这两个场景可能发生功能拓展的可能性较高,作为插件化改造,后续的版本迭代上适用性更强。
动态化模板方案目前主要应用于账号系统(修改密码、找回密码等)二级页面。这些页面与Native之间有较多的逻辑交互,但交互的方式可以被很好的抽象封装成通用能力。具体场景如下图所示:
图5.1 动态化模板配置页面一
图5.2 动态化模板配置页面二
通用web容器,目前主要应用于双协议(用户协议、隐私协议)和一些游戏系统公告等场景。这些场景具有重展示轻交互的特点。
内容上有较高的动态化需求,但没有太复杂的逻辑,性能上也没有过高要求。具体场景如下图所示:
图5.3 Web容器应用场景 —— 隐私协议
图5.4  Web容器应用场景 ——  公告
 5.2 上线效果
5.2.1 产物下发及应用情况
以插件为例,图5.1 为"get_config_success",即成功获取插件配置信息数据;图5.2 为"download_success",即成功对插件包进行下载的数据;图5.3为"install_success",即成功完成对插件包的安装情况数据。
其中配置下发->成功下载转化率为97.24%(其中绝大部分为网络异常,此部分有进一步优化空间)
下载成功-> 安装成功转化率为99.99%,整体框架稳定性和兼容性得到有效验证。
图5.5 成功获取插件包配置信息数据走势图
图5.6 成功完成插件包下载数据走势图
图5.7 成功完成插件包安装数据走势图
5.2.2 紧急回滚
同样以插件为例,2021-11-02 11:00 点开始对插件进行回滚。回滚后下载和安装数据断崖式归零,同时成功卸载的日志大量产生。具体数据情况如下图:
图5.8 插件回滚后成功下载数据走势图
图5.9 插件回滚后成功安装数据走势图
图5.10 插件回滚后本地卸载成功数据走势图
六、 避坑指南
1)R8兼容:插件框架在gradle版本升级后,低版本R8生成的mapping.txt文件存在大量重复的映射记录,这样的mapping文件直接进行applaymapping时解析过程会因报错而终止插件构建,无法构建成功。直接使用高版本R8对低版本进行覆盖构建结果中,插件功能存在一些不可预见的异常,需要在构建时,先对mapping.txt进行指定,然后再构建过程中applymapping指定的mapping文件,确保构建和功能正常。
图6.1 mapping内容示例
图6.2 动态处理mapping文件脚本
2)混淆mapping处理:游戏SDK是以aar形象进行交付,本身在交付前进行了一次混淆。同时不同的游戏完成SDK接入后,会再一次进行混淆,不同游戏的混淆结果不同,且无法将混淆结果回调给SDK,比如交付的A.class,游戏打包后变成了B.class或者C.class。
如果要求游戏接入方不做混淆处理,对游戏和SDK都存在一定的安全隐患。如果SDK对可能被插件调用的API做特殊处理不进行混淆,则需要大量的人工介入操作,且无法完全预计后续业务迭代过程中可能需要用到的API。可以采用在构建混淆结束后,读取混淆结果,输出混淆结果为proguard-rules规则,确保游戏二次混淆后,游戏SDK不会被再次混淆。
图6.3  输出混淆规则示例
七、未来发展与规划
1)建立完善的监控预警机制,实现自动通知、降级、回滚的分级预警的干预措施。
2)基于大数据和用户标签,实现千人千面的个性化动态下发,提供精细化运营能力。
八、总结
本案为我们在游戏SDK侧的一点尝试,以动态化路由协议为媒介,多种容器并行的热更新方案。很多地方有优化的空间,但从效果上也基本达到了预期。感谢您能阅读到最后,希望在某些设计思路上能让您有所收获。如有不足之处,欢迎在下方进行评论指教。
写在最后,“路漫漫其修远兮”,我们一直走在前进的路上,加油!
以上是今天的分享内容,如果你有什么想法或疑问,欢迎大家在留言区与我们互动,如果喜欢本期内容的话,欢迎点个“在看”吧!
往期精彩指路
继续阅读
阅读原文