滴滴国际化目前有着一些不同于国内打车的特殊场景——国内用户拿着国产手机出国打车。国内地图、Google 地图均没法用;手机移动漫游网络太慢;同时需要对接不同合作公司的司机运力,这是国际化客户端项目面临的主要问题。本文为滴滴出行技术专家 吴更新在 MDCC 2016 移动开发者大会上的演讲,主要介绍了滴滴国际化在地图选型、地图扩展适配、网络相关优化、项目整体技术拆分、演进方面的经验。
PPT 下载地址:https://github.com/MDCC2016/Android-Session-Slides。
视频观看地址:http://edu.csdn.net/course/detail/3094
目前大家用滴滴 App 在美国可以直接打车,不用下载新的 App,现在的滴滴 App 在美国打开就会自动显示海外打车页面。但是,国际化在技术上有一定的特殊性,滴滴国际化业务主要服务于国内用户在国外打车的场景,因此会涉及到与国内业务不太相同的地方,主要体现在地图、网络、运力来源三个方面。前两者很好理解,运力来源中的运力主要是指司机。在国内,个人可以注册滴滴司机,但是在国外我们是与合作伙伴合作,由合作方提供运力,所以在不同的国家,会与不同的合作伙伴实现接入。具体如下:
以上的三个特殊性决定着我们需要在技术上的差异,接下来将围绕地图模块、漫游网络、多业务接入项目演进详细展开。
这部分主要包括两大问题:地图选型、地图切换。
滴滴是个重度依赖地图的 App,而目前我们的友商及大部分国内地图提供商都没有海外的路网数据。我们前期针对的场景是国内用户海外打车,Google Map 依赖 Google Play Service,国内手机几乎都没有这个 Service,即便安装了 Google Play Service 部分手机也无法运行,另外即便都有了,漫游网络也不能访问 Google Map,所以最靠谱的 Google Map 一开始便被排除。
另外国内有些 App 在海外用了 Google Map,不过是通过中转下发地图切片的方式完成的,但滴滴对于地图各方面的要求都很高,我们必须找到一个合适的国外地图。
(1) 海外地图选型考察点
我们对地图强依赖,有些定制需求,如:很多 Marker 并且添加后需要修改、画圆并可以动态调整半径等等。
国外可用地图数据源主要有 OpenStreetMap、Here、Tomtom,OpenStreetMap 是个开源的地图数据源,类似维基百科的模式,所以数据很全很新,甚至超过 Google Map,但不可避免会有些脏数据,前期的话主要是针对大城市,OpenStreetMap 的数据可以满足需求。但因为涉及到异地跨时区沟通,所以希望技术支持力度够大。
性能方面则包括地图启动时间、渲染速度、前端响应速度、后端响应速度。
在开始国际化前,当时滴滴的安装包就已经很大了,基本是国内主流 App 之首(当然现在滴滴 App 已经挺小了),所以我们希望新的地图够小。
(2) 海外地图全面对比
这次我们调研了 Mapbox、Nutiteq、Here、Tomtom、Bing 共五款海外地图。其中:
所以我们重点集成和测试的是 Mapbox 和 Nutiteq 这两家地图供应商。
Mapbox 和 Nutiteq 的功能和性能都满足我们需求,地图数据源也都是以 OSM(OpenStreetMap) 为主。Mapbox 的 API 设计和国内地图类似,都是向 Google Map 靠拢,所以上手简单,并且整个 SDK 都是开源的,地图的样式也更美观些,而 Nutiteq 的地图底层设计比较独特,API 用法很不寻常,这也给我们接入带来了很大的麻烦。
Mapbox 有众多的 Web 用户,包括访问量都不低的 Foursquare、Pinterest 等,但 Android 端用户并不多;Nutiteq 的 Android 用户多些,但整体量也不是很大,不过我们并没有更好的选择,而且前期我们的量也不会很大,所以他们都在可接受范围内。
综合下来看的话,我们是更倾向于 Mapbox,不过 Mapbox 只能通过 GitHub Issues 和邮件反馈问题,反应很慢;Nutiteq 可以 Skype 沟通,效率很高。为了保险起见,Mapbox 和 Nutiteq 都做了全面接入和测试,最终证明这样是有用的。
跟多数 App 一样,为了使得包更小,我们的主工程配置了 abiFilter “armeabi”,仅打 armabi 的 so,而 Mapbox 的 armeabi so 无法跑在 armv7 机器上,前期集成测试我们通过修改 Gradle 脚本在编译时 copy so 的方式让测试通过,而 Mapbox 一直不愿意改,国内市场又不支持 Google 的 Apk Splits 机制,所以最终放弃选择 Nutiteq。
后话:Mapbox 最新版已经解决了这个问题,而且国内有相关的市场人员,沟通起来也顺畅多了。
用不了 Google Map 带来一个要求,我们选择的地图必须支持多国家,并且在设计时要支持以后不同地图任意切换。是的,即地图和 App 弱依赖。针对这个问题我们设计了地图隔离层。总体设计如下:
上图第二层 MapSDK 是地图的标准 API 层,App 只与此层打交道,标准层的 API 设计以 Google Map API 为标准。
第三层 Adapter 层是具体地图到标准 API 的适配实现层。每个地图都有个 Adapter,负责将地图 API 转换成标准 API。
将原来的 App 与三方地图直接依赖改为 App 依赖表示标准 API 的 MapSDK 层,由 MapSDK 通过具体的 Adapter 调用三方 SDK,这样地图切换只需要替换依赖的 Adapter 即可,其他地方无需改动。
新的设计后编译依赖关系如下:
App 依赖 Map Adapter,Map Adapter 依赖我们的 MapSDK 和三方的 Map SDK。当我们需要更换三方地图 SDK 时,仅需更换对应的 Map Adapter 即可。对于 Android,build.gradle 中更换依赖即可。
还有如 MapSDK 的 API 规范前面已经介绍过以 Google Map API 为标准。另 Adapter 有具体的开发规范要求。
前面介绍过我们初期针对的是国内用户海外打车场景,这时用户是用国内手机和运营商海外漫游接入网络,所以需要针对网络访问进行优化。
一般漫游网络流程如下图:
用户由海外运营商接入国内运营商,再通过公网(有墙)访问 Web。我们的服务器部署在 AWS 上,用户海外漫游打车网络流程如下图:
由于公网访问 AWS 非常慢,我们添加了海外专线,优化后用户海外漫游打车网络流程如下图:
用户先访问到国内的中转服务器,中转服务器再通过海外专线访问 AWS。
这个过程中客户端要做的工作包括:
之前国际化业务的工程是很简单的方式,所有业务、组件、工具放在一起,根据具体包名划分:
这个在早期问题不大,并且开发起来快速方便,但随着更多业务接入,如我们前面说过的新的国家运力接入,问题就日益明显,包括:
将原工程整体拆分为业务工程和 SDK 工程,单业务工程直接依赖 SDK,可独立开发、独立运行、独立打包。如下:
这样在接入新的业务后,总体项目结构如下图:
每个业务作为单独工程,共用组件、工具、业务统一到 SDK 层中。集成工程负责集成 Lyft、Ola、GrabTaxi 项目,所有业务项目提供 AAR,由集成工程整体打包对外发布。
为了解决组件之间耦合,防止后续问题加剧,同时方便协同开发和更好的复用,将 SDK 工程组件化拆分如下:
SDK 整体拆分为 Business Library 和 Util Library 两大部分,主要依据是是否可以独立于我们业务,他们间不允许反向依赖。每个部分包含若干组件,每个组件都以 Module 形式存在。
Business Library 为通用业务层,包含通用业务组件,如平滑移动、上车点、定位、地理信息、打点、网络封装。其中 CommonBusiness 存放暂时通用、但尚不足以作为一个单独组件的公共业务,以后可能独立出来,注意包名规范方便未来独立。Util Library 为工具库,大致分为 View 和 Util,DidiSDK 为滴滴 App 整体通用组件包,包含通用的图片缓存、网络请求、基础登陆组件等等。
通过上图我们可以发现即便只是 Business Library 层,组件也根据依赖关系划分为明显的上下层。
(1) 单一及开闭原则
每个模块只代表一个功能模块或一个公共业务,对于个性化或定制功能以接口形式对外开放。
注:目前 CommonBusiness 模块暂时作为国际化 SDK 整体集成打包的模块,即国际化 SDK 项目中的 sdk Module,后续当其中某个公共业务足够成为一个模块时可继续拆分出来。
(2) 拆分粒度
项目的演进是不断进行的,没必要将每个细小组件都拆分出来,这样不仅增加了项目的复杂度,同时也会影响编译时间。
先根据实际需要拆分必要的组件,太小暂不足以独立的组件可以在以后不断进行的重构中根据需要拆分。如上面的 CommonBusiness 模块,当然需要保持一定的规范方便以后拆分。
(3) 依赖关系
通过依赖图整理依赖关系,防止重复依赖,同时看出沉淀关系。
(4) 开发规范
为了保证扩展性及方便以后继续拆分:
com.didi.{xx}.sdk.{businessName}
开头;com.didi.{xx}.sdk.util.{utilName}
开头;com.didi.{xx}.sdk.view.{viewName}
开头;(5) 组件间通信
放弃原来造成耦合严重的 EventBus,改用原生的通信方式,包括原生 (startActivityForResult) 、内部广播、回调等。
其中虚线部分为 SDK 层。