写点什么

高德地图:崩溃率从万分之八降到十万分之八的架构奥秘

  • 2019-08-15
  • 本文字数:6318 字

    阅读完需:约 21 分钟

高德地图:崩溃率从万分之八降到十万分之八的架构奥秘

近几年来,高德地图业务发展迅猛,团队规模迅速扩张,代码体量急剧增加,为了提高团队高效并行作战的能力,端上做了一系列架构升级。2018 年通过双端融合、组件化、研发平台搭建等技术实践,使得发版效率提升 50%, App 崩溃率从万分之八降到十万分之八。本文整理自ArchSummit 全球架构师峰会(深圳站)2019 峰会演讲,主要分享在一系列架构升级改进中,高德地图的具体做法、经验和思考。


大家好,我是来自高德地图的郝仁杰,本次分享的主题是“高德地图 App 架构演化与实践”。2018 年,我们通过架构的演进将发版周期缩短至一半,整个 App 的崩溃率从万分之八降低至十万分之八。在正式开始介绍之前,我先来简单介绍下高德。

背景介绍

高德是国内数字地图内容、导航和位置服务解决方案提供商。目前在端上,分为手机和车机两条主线。近年来,高德业务迅猛发展,人员规模迅速扩张,代码体量急剧增加,为了提高团队高效并行作战的能力,端上做了 C++多端融合和动态化能力建设。


回顾近几来高德地图 App 架构的演进历程:2014 年,手机端上只有几十个研发, Android 和 iOS 端由原生单体架构实现;2015 年,地图引擎下沉 C++,实现了手机和车机的多端融合;2016 年,端上启动了动态 UI 框架的开发,为未来业务的动态化铺路;2017 年,动态 UI 框架建设完成,具备了运行静态页面的能力;到了 2018 年,手机端已经成长为拥有数百研发规模的团队,双端代码量也已经达到数百万行,架构要如何继续演化来提高团队高效并行作战的能力,来支撑并赋能业务快速发展呢?

问题现状

为了让业务开发有节奏的进行,项目上每年会制定一些公车计划。公车就是每个 App 版本,版本里带的产品功能就是公车上的货物,公车计划即每年的发版计划。按照计划,公车会在指定的时间把组装好的货物拉走。


2018 年初,由于双端代码差异较大、耦合严重、复用率低、职责不清晰、平台工具简陋等问题,公车无法按照计划拉走货物。工具落后,货物组装慢且质量差,无法如期交货,迫使公车等待,导致整个发版周期长达 3 个月,崩溃率高达万分之八,公车变成了伪公车。



为了解决这些问题,使伪公车变为真公车,需要做到稳定、并行和高效。端上通过以下三种方式达到该目的,一是双端融合,如上图,蓝色部分上漂动态 UI,下沉 C++,以及 Android、iOS 双端拉齐,减少差异,提高可维护性;二是选择组件化方案,分而治之,解除耦合,提高复用率,做到并行、高效;三是搭建研发中台,工具升级,流程自动化以及风险质量管控,提升效率和稳定性。

执行方案

双端融合

2015 年,我们通过地图引擎下沉 C++,实现了手机、车机的多端融合,同理,可将部分功能下沉 C++;通过 2017 年建成的动态 UI 框架,可将部分业务上漂到动态 UI;对于既不能上漂也不能下沉的,通过双端拉齐做到融合。


那么,什么样的场景适合下沉到 C++呢?一,需要有稳定的逻辑,不经常变化;二是不强依赖原生;三,对性能要求较高。举例来说,导航逻辑,地图从开始建立到现在已经打磨出一套非常核心稳固的逻辑,这部分逻辑可以下沉到 C++。


哪些场景又适合上漂到动态 UI 呢?一,对性能要求不高;二,经常易变的业务代码,比如产品的 UI 需求;三,不强依赖于原生能力。


对于既不能下沉,也不能上漂的功能,选择双端拉齐:对性能有一定要求;强依赖原生的能力;需要支撑一些原生业务。例如,高德地图的页面框架,虽然 Android 和 iOS 端有原生的页面框架,但地图类应用和普通应用不太一样,地图类应用的主要功能是围绕着一张地图进行,这张地图上面的元素非常丰富,数据量非常庞大,内存占用较大,如果采用原生页面框架进行开发,就意味着每切换一个页面就得创建一张新地图,这对手机端这种资源紧缺的环境来说是非常浪费的,对于低端机型来说是不可接受的。


另外,地图应用从一个页面切换到另一个页面,或者从一个场景切换到另一个场景,并不是完全不同的两张图切换,而仅仅是一张地图的不同状态转换,此时,如果额外创建一张新的地图,显然是极大的浪费。所以,对于地图类应用,我们建设了自己的页面框架:以单系统页面控制器多视图切换的方式实现。由于原来都是单体开发,Android 和 iOS 只关注自身特性,两边的实现不太一样,跳转规则、功能特性均有差异,我们通过分析双端的规则、特性,借鉴双端各自的优点,设计了一套统一的规则、特性,实现了双端融合。


下面简单介绍高德地图页面框架的融合方案:



如上图,左边的 Activity 是 Android 的系统页面控制器,右边的 UIViewController 是 iOS 的系统页面控制器,通过虚线连接比较,我们发现两端的页面状态设计基本相同。所以,我们在设计自己的页面框架时沿用了这些系统页面状态,同时从命名上也保持一致,这样可以让 Android 和 iOS 原生开发的同学更容易理解和上手。


此外,我们吸取了双端各自的优点。比如,Android 端页面有四种启动模式,但是 iOS 端并没有这些,我们就把 Android 的四种启动模式运用到了 iOS 端;iOS 端有 Present 特性,但是 Android 端没有,那么也把这种特性融合到 Android 端的页面框架中;最后,还有一些小设计,比如 Android 的 onResult 设计,也可以借鉴融合到 iOS 端。



首先,介绍下四种启动模式,这是安卓特有的。第一种是 Standard 模式,这个模式和栈的行为是一样的,就是标准的 Push 和 Pop;第二种是 SingleTop 模式,当向一个页面栈压入另一个页面时,如果该页面已经在栈顶,那么将不会创建一个新的页面实例 C 放到栈顶,不会变成 ABCC 这样的方式,而仅仅是通知当前栈顶的 C 页面做一个数据更新;第三种是 SingleInstance 模式,当以 SingleInstance 模式 Push 一个页面时,如果该页面已经在栈中,那么就把它从栈中带到栈顶;最后一种是 SingleTask 的模式,这和原生系统略有差别,因为我们目前是基于单页面控制器的方式实现的,当以 SingleTask 的方式 Push 一个页面到页面栈时,如果该页面已经在栈中,页面框架会把其之上的所有页面全部清除出栈,使其成为新的栈顶,这就是四种启动模式。



接下来,简单介绍 iOS 的 Present 特性。当页面栈顶的 C 页面 Present D 页面时,D 页面并没有被加入到 ABC 页面栈中,而是变成了 C 页面的一个附属,当 D 页面要消失时,同样也是通过 C 页面的 dismiss 移除掉。这里有些限制,每个页面仅可以 Present 一个页面,这个页面可以是一个普通页面,也可以是一个导航页面,那么导航页面是什么呢?大家可以理解成一个新的页面栈(功能类似 UINavigationController),其上可以添加其它页面。如果 D 页面是导航页面,就可以在其上 Push 其它页面,如果业务流程有一个主流程,一个分支流程,就可以采用这种方式实现。



最后是 Android 的 onResult 特性,实现了页面间数据返回的解耦,如上示例代码就是大致的实现原理。具体来说,从 A 页面跳转到 B 页面,那么 B 页面执行了一段逻辑之后,A 希望得到执行结果,如果按照原来 iOS 的实现方式,只能通过监听 Listener 或 Delegate 等方式将 B 页面的执行结果返回给 A 页面。当 iOS 的页面框架实现了这个特性, A 页面就不需要额外注册 B 页面的 Listener 或 Delegate 了,只需重写自己的 onResult 方法并处理结果即可,这样既可以实现页面解耦,又方便了业务同学开发。



接下来,举个高德地图手机端上的具体实例。


有这样一个搜索场景,从一个具体地理位置详情页可以跳转到以它为中心的搜周边页,在搜周边页中又可以跳转到另一个具体地理位置详情页,接着可以跳转到新的搜周边页,以此递归循环,但是返回时,产品希望仅返回到之前搜索过的具体地理位置详情页,略去搜周边页。如上右侧图片展示,查询顺序是:7 天优品酒店详情页 -> 7 天优品酒店搜周边页 -> 火驴火烧肉亭详情页 -> 火驴火烧肉亭搜周边页;返回顺序是:火驴火烧肉亭搜周边页 -> 火驴火烧肉亭详情页 -> 7 天优品酒店详情页。中间的 7 天优品酒店搜周边页被去掉了。


在 iOS 页面框架未实现 launch mode 前,火驴火烧肉亭详情页在跳转到火驴火烧肉亭搜周边页前,需要自行遍历当前页面栈,将 7 天优品酒店搜周边页从页面栈中移出后,再跳转到火驴火烧肉亭搜周边页,以此保证产品逻辑的正确性。在实现了 launch mode 之后,火驴火烧肉亭详情页仅需以 SingleInstance 的方式打开火驴火烧肉亭搜周边页即可,页面框架会自动将之前的 7 天优品酒店搜周边页调到栈顶,并将该搜周边页的内容刷新为火驴火烧肉亭搜周边。极大简化了 iOS 端业务同学的开发成本,规范了 iOS 页面跳转规范,结束了由业务自行操作页面栈的混乱时代,同时双端技术能力的融合也为上层动态 UI 业务提供了一致性的体验。


上面,我们介绍了双端融合方案的三种方式,也举例说明了其带来的效果。下沉 C++,实现两套代码合一,解决了一致性问题,提高了性能,但同时也提高了开发门槛,适用于多年沉淀的核心逻辑;上漂动态 UI,同样解决了双端一致性问题,性能会稍有损失,但降低了开发门槛,使得开发速度得到提升,适用于频繁变动的业务场景;双端拉齐则是借鉴了双端优势,做到互相融合。

组件化

我们做了一些团队组件化方案的选型和参考,例如手淘的 Atlas、Beehive,网易的 LDBusMediator 等,由于这些组件化方案都比较成熟,这里不再赘述。它们都包含五个概念:容器、模块、生命周期、页面路由和对外服务(通信),我们重新命名了这些概念使其更加形象化。



容器,负责管理模块;模块,是一个独立的功能单元,可以独立编译;微应用,管理模块的生命周期,对于一个手机操作系统,是为每个应用派发生命周期,对于一个单独的应用,是为每个模块派发生命周期,就像一个应用管理着很多微应用一样,因此我们取了这个形象的名字;页面路由,负责进行 URL 的解析和页面跳转;微服务,模块中的逻辑功能,同时提供对外服务。


我们对容器在设计进行了一些改造,如上右半边图,模块被虚化了,被定义成了一个物理概念(即一个独立代码仓库),逻辑上拆分为微应用、微服务和页面路由,容器不再管理模块,而是直接管理这三个元素。之所以这样做,是因为我们希望业务更关注自身需要的服务是什么,而不是它在哪个模块,这些也是借鉴了安卓的组件化思想。



接下来,我们详细介绍下微应用生命周期的设计,如上图,微应用在 iOS 端参考的是 UIApplicationDelegate 的生命周期,而在 Android 端参考的是 Activity 的生命周期。做这样的参考选择,原因有三:一,高德地图内的应用场景大都依赖前后台切换的事件做一些逻辑处理;二,iOS 的 UIApplicationDelegate 作为应用的生命周期,同时支持前后台切换,完全吻合高德地图的场景;三,Android 选择 Activity 是因其组件化的思想,在 Android 的设计中,Application 已经弱化成了一个特殊进程的概念,并不能代表一个应用,且高德地图是基于单 Activity 实现的(上面介绍页面框架时提到过),通过 Activity 的 onStop,onRestart 生命周期中做些逻辑处理,即可判断出应用是否为前后台切换。这样,去除图中虚线框中的生命周期后,双端得到了统一的生命周期,如下左半部分图:



对于虚线中差异化的部分(如上右半部分图),设计为扩展的生命周期,做到抽象相同、扩展差异,既统一了通用生命周期,也支持了双端各自的特性。


对于微服务,我们定义了一个通信规范,只能通过接口方法,不能直接调用实现。定义微服务主要是希望 UI 展现与业务逻辑能够分离,并让业务逻辑服务化,不仅服务于当前页面,也能够服务更多页面,提高代码的复用率,降低维护成本。


有了容器框架,代码便可以抽成一个个独立的模块单元,但模块应该放在那里,上下依赖关系是什么,还需要对模块进行分层、分组,下图为分层、分组后的整体架构:



通过容器建设,架构分层、分组,我们实现了组件化,解除了模块间耦合,提高了代码复用率,为后面的高效并行打好基础。分而治之的思想,组件化的“分”也是为后面的“治”做好铺垫。

搭建研发中台

研发中台应该有哪些功能,可以结合组件化和公车流程来分解,如下图:



主流程是公车流程,分为:需求收集、需求串讲、开发、合版、提测、灰度发布和正式上线。开发流程可以分解为更细的建立迭代、选择模块、功能开发、模块构建和安装包构建。这里解释下迭代的概念,即一个发版周期内的功能开发。组件化实现了功能解耦,使得不同业务团队可以在开发阶段创建自己的迭代并行开发,开发完成后在规定的时间段进行合版。提测流程可以分成模块集成、安装包构建和集成测试,其中模块集成是以产物的方式进行集成。测试通过后,通过客户端发布流程,进行灰度发布验证,灰度通过后,再进行正式上线,上线之后,我们会对崩溃、性能等维度进行监控。通过流程拆解,我们整理出了研发中台的完整功能:



研发中台建设完成后,我们实现了研发流程、测试流程以及发布流程的自动化,提高了人效。另外,通过质量管控,提高了稳定性;通过流程管控,约束了可能产生的风险。

主副收益

首先,通过双端融合、组件化、中台建设提升了代码稳定性,实现了流程自动化,做到了开发阶段的并行,使发版周期缩短到原来的一半,从伪公车变成真公车。


其次,通过质量优化,让崩溃率从万分之八降低到十万分之八:双端融合减少了一致性问题;架构合理化提高了可维护性;关键流程管控,减少了风险源头;通过质量扫描,解决了头部质量问题,通过崩溃监控,解决头部崩溃问题。


然后,通过升级编译脚本,支持并行编译;通过模块化,基于产物构建安装包,大大降低编译时长,从原来的 40 多分钟降至现在的 8 分钟。


最后是包大小优化,iOS 端从 146M 减到 123M,纯减量达 48M,这主要是通过编译优化,资源云化,功能合并(分层、分组),svg 替代 png 小图标,删除无用图片和代码实现等手段实现。其中,资源云化主要是指将启动时的非必要资源放在云端,需要时再进行动态加载。

经验教训

在组件化以后,编译模式发生了一些变化,模块在集成前提前生成了产物,这些变化同时带来了一些问题,比如二进制兼容问题。以枚举功能为例,在模块化后,A 模块依赖 B 模块中定义的枚举,在 A 模块生成产物后,B 模块的枚举定义发生了变化,A 中使用的枚举值含义可能发生变化,如下图:



为了解决该问题,我们制定了一些开发规范:对于枚举的定义,不允许删除任何已定义的枚举值,不允许从中间插入任何枚举值,如果一定要添加,只能在末尾添加,以此来解决二进制兼容性问题。当然,除了枚举的问题,还有宏定义等引起的二进制兼容性问题,此处不一一详述。


此外,Android 端还可能出现代码注解丢失问题。编译期注解仅存在于编译阶段,模块化后,产物中无法保存注解信息,导致产物集成时,由于找不到注解信息而无法进行全局注册。为此,我们做了一些自定义 APT 插件,在注解处 阶段生成 Java 数据类的同时也存储一份注解信息,这样在集成阶段就可以根据注解信息进行全局注册。

未来展望

2018 年,高德客户端通过一系列架构治理,从伪公车变成了真公车,但这只是近几年架构演进的一个阶段性成果。未来,我们要发挥动态 UI 的优势,让业务真正动态化起来,从公车时代跨入到 Feature Team 时代,让公车变成一条条公路,每个 Feature Team 就是一个小汽车,按照自己的节奏装好货物后,就可以在修好的公路上自由的行驶,更好地做到灵活、并行和高效。


嘉宾介绍


郝仁杰,高德地图无线开发专家。十余年移动客户端开发经验,曾深度参与 Nokia S60 Contacts,YY 语音,360 手机卫士的研发维护工作,对 Symbian、Android、iOS 系统有较深的理解。目前在高德地图负责 Android、iOS 端的基础架构,2018 年,带领团队实现了端上的 Bundle 化等一系列架构升级的开发工作。


除了分享大公司的技术实践之外,会议上还邀请业界关于架构前沿的探索话题,欢迎继续关注Archsummit 全球架构师峰会北京 2019,限时 7 折大力优惠名额,有任何问题欢迎联系票务小姐姐灰灰:15600537884 (微信同号)


2019-08-15 08:0226067

评论 5 条评论

发布
用户头像
开发应用真不容易啊。👍
2023-07-11 14:14 · 日本
回复
用户头像
最后是包大小优化,iOS 端从 146M 减到 123M,纯减量达 48M。这个是打错了?
2019-09-06 16:42
回复
用户头像
但是现在打车位置不准了
2019-08-25 10:20
回复
用户头像
双端融合那里做的太漂亮了,一般双端都存在的项目偶尔会遇到某端实现复杂度太高,双端功能的最终交互不一致的现象,融合后这类问题也会大大减小了。
2019-08-17 23:40
回复
感谢观点分享~
2019-08-19 13:06
回复
没有更多了
发现更多内容

架构实战营 - 模块五作业

思梦乐

【Vue2.x 源码学习】第三十七篇 - 组件部分 - 组件的合并

Brave

源码 vue2 8月日更

高并发中,那些不得不说的线程池与ThreadPoolExecutor类

华为云开发者联盟

Java 线程 高并发 线程池 ThreadPoolExecutor类

随机字符串,随机密码生成器

入门小站

工具

可视化接口管理平台 YApi,让你轻松搞定 API 的管理问题

xcbeyond

工具 接口管理 YAPI 8月日更

【Flutter 专题】68 图解基本约束 Box (三)

阿策小和尚

Flutter 小菜 0 基础学习 Flutter Android 小菜鸟 8月日更

模块五作业

秀聪

架构训练营

企业研发效能提升之道 —— 管中窥豹,窥一斑而知全豹

在天涯的海角

研发效能

LeetCode题解:220. 存在重复元素 III,暴力法,JavaScript,详细注释

Lee Chen

算法 大前端 LeetCode

Seata TCC模式原理与实战

码农参上

分布式事务 seata SpringCloud Alibaba 8月日更

docker的使用

Rubble

8月日更

没有银弹

escray

学习 极客时间 如何落地业务建模 8月日更

10篇校招/社招面经请你查收~

王知无

架构实战营毕业设计

林子钧

架构实战营 毕业设计

架构实战营毕业总结

林子钧

架构实战营 毕业总结

从0开始的TypeScriptの九:接口Interfaces · 中

空城机

typescript 大前端 8月日更

Spark RDD模型

Geek_qsftko

spark

你真的了解 fail-fast 和 fail-safe 吗

4ye

Java 后端 并发 map 8月日更

讲透学烂二叉树(三):二叉树的遍历图解算法步骤及JS代码

zhoulujun

二叉树 二叉树遍历 前序遍历 中序遍历 后续遍历

讲透学烂二叉树(四):二叉树的存储结构—建堆-搜索-排序

zhoulujun

二叉树 堆排序 二叉堆 二叉堆排序 二叉树排序

传统企业数字化转型的三大技术误区

码猿外

数字化转型 敏捷精益

Linux之scp命令

入门小站

Linux

TypeScript那些最佳实践

思诚^_^

typescript

讲透学烂二叉树(五):分支平衡—AVL树与红黑树伸展树自平衡

zhoulujun

二叉树 平衡二叉树 红黑树

讲透学烂二叉树(六):二叉树的笔试题:翻转|宽度|深度

zhoulujun

二叉树 二叉树遍历 二叉树翻转

【设计模式】备忘录模式

Andy阿辉

C# 编程 后端 设计模式 8月日更

JVM空间分配担保机制

W🌥

Java JVM 8月日更

架构实战营 毕业设计项目

梦寻解语花

架构实战营

《社会心理学》-怎样说服他人?

箭上有毒

8月日更

毕业总结

梦寻解语花

架构实战营

悄悄学习Doris,偷偷惊艳所有人 | Apache Doris四万字小总结

王知无

高德地图:崩溃率从万分之八降到十万分之八的架构奥秘_语言 & 开发_郝仁杰_InfoQ精选文章