干货 | 携程酒店安卓地图开发实践

作者简介

亦枫,携程资深软件工程师,负责酒店业务 Android 客户端的相关研发工作。

当前大多数移动互联网 App 都会存在地图相关功能,尤其是 LBS(基于位置服务)相关的业务,依赖性更强,携程 App 的酒店业务更是如此。这篇文章将围绕携程酒店 App 安卓地图功能,分别从产品业务背景、代码开发模块架构和遇到的典型产品技术问题等方面,描述我们这一路的开发实践经验,希望能够帮助到正在从事相关业务开发的同行们,大家相互交流,共同探讨。

 

一、携程酒店地图业务介绍

 

可视化的地图交互界面能够给到用户最直观的地理位置信息感受,根据用户主观心理选择的价格高低、距离远近、交通方式、生活购物等多个因素,帮助用户迅速找到目的地区域最适合自己的酒店。无论是直观体验上,还是用户习惯上,地图功能都是携程酒店整体业务不可或缺的一部分。

 

携程酒店业务涉及地图开发的地方目前主要有三个模块,酒店列表页小地图、酒店列表大地图和酒店详情页地图。

干货 | 携程酒店安卓地图开发实践_第1张图片

1)列表页小地图

 

提供与主列表数据联动的地图打点功能,方便用户浏览酒店列表时能够实时查看当前选中酒店的地图位置信息。

 

地图 Marker 覆盖物与列表数据一致,同时根据当前列表分页,展示当前页与上一页的酒店数据,并突出选中当前酒店。

 

2)列表页大地图

 

包括顶部标题栏筛选项等辅助信息,中间地图背景信息和底部选中酒店卡片信息三部分,用户可通过筛选项、拖动地图自动加载当前地图屏幕内酒店等功能,实现通过地图订酒店的功能。

 

之所以称之为列表页大地图,是因为当前页面与酒店列表页(包括小地图)自由切换,并记住当前用户行为,保存酒店数据信息。

 

3)详情页的地图

 

酒店详情地图主要提供导航路线、酒店周边相关 POI 内容,帮助用户了解所在酒店的具体地图信息。

 

相比而言,酒店详情页的地图业务相对比较简单,这里的简单更多是针对开发层面,因为该页面的数据较为独立,不存在与其他页面有太多联动相关的业务。

 

二、酒店地图相关架构设计

 

大家知道,提供地图工具的第三方服务不止一家,常见如国内有百度、高德、腾讯地图,国外有谷歌、苹果地图服务。

 

为了保证携程 App 内地图统一性和更换地图的高效可维护性,携程各业务部门所用到的地图由携程公共无线部门收口,进行封装对接。各业务部门可根据自己的实际业务需要再进行自定义处理,酒店部门也是如此。

 

为了方便酒店三大模块的地图业务统一性,酒店安卓这边自定义一个HotelMapView继承自公共提供的CtripMapView 来共具体业务使用,并将 Marker 打点、地图围栏、生命周期处理等通过接口形式抽象进来。

干货 | 携程酒店安卓地图开发实践_第2张图片

 

1)详情页地图架构设计

 

前面说到,详情页由于自身的数据独立性和业务功能的简介,相对而言,开发层面的代码逻辑还是简单清晰的,我们使用Android 开发传统的MVC 架构加上适当的抽象封装就能轻松应对。

 

稍微复杂一些的逻辑可能在于ABTest 上面,通用逻辑还是在Activity 层面提供的Controller 控制层,类似底部周边POI 的处理逻辑抽取到独立的Holder 辅助类中。

 

相比而言,详情页由于业务的界面独立性也不会有太多的技术性坑存在,唯一可能需要注意的是就是各种经纬度坐标系类型的转换处理,这个在导航至第三方地图时尤其需要引起重视。

 

目前市场上常见的经纬度坐标系,以及主流第三方地图服务商的对应使用关系为:

 

WGS84:国际通用标准经纬度坐标系,GPS坐标,也称地球坐标系。谷歌地图目前使用的就是 WGS84 坐标系(中国区除外);

 

GCJ02:中国国家测绘局制订的坐标系,有WGS84 坐标系加密而成,也称火星坐标系,谷歌中国区地图、高德地图的使用者;

 

BD09:即百度坐标系,由GCJ02坐标系进一步加密而成,百度地图独家使用的经纬度坐标系。

 

2)列表页地图架构设计

 

列表页大小地图业务逻辑错综复杂,不可能把所有的业务都集中在Activity 或Fragment 中处理,无论是多人开发的效率还是后续的可维护性,都比较差,传统的MVC 架构显然已经不太适合。这里我们间接采用Android 系统推荐的MVP 架构和页面模块化来分离实现。

干货 | 携程酒店安卓地图开发实践_第3张图片

根据列表页大地图业务业务分类,大致分为上图这些核心模块,以及其他诸如动画、遮罩浮层Tips 等其他附加模块。

 

这样,原本臃肿耦合的 Fragment 和 Activity 控制层就没有那么多耦合性强的代码,只是提供各个具象 Presenter 的注册业务,具体逻辑和生命周期均交到业务模块 Presenter 中间层,Presenter 持有View 和Model 的引用,以及Context 生命周期的回调,可独立实现自己的功能,高内聚、低耦合的特点更加利于扩展维护。

 

事实上,列表页大小地图在产品业务不停迭代的过程中,大小地图位于两个 Activity 内独立维护已经不能满足产品需求,譬如大小地图来回切换时,两个页面的 MapView 很难做到动画的无缝过渡,交互体验难有充分自由的发挥;再譬如,顶部 ToolBar 和筛选项部分,无法共用,维护起来也非常费劲。经过讨论,进行技改,代码重构,合并列表大小地图。

干货 | 携程酒店安卓地图开发实践_第4张图片

可以看到,这种架构下,不用再单独开发与酒店住列表相同的模块内容,只需要定义大小地图相关的 onShow、onHide等生命周期方法,实现 IHotelListMapView 接口,相关模块依然是低耦合的。同时,由于共用的是同一个 MapView,也在同一个Activity 当中,切换动画、处理 CacheBean 数据、以及共享 Hotel Service 都是非常方便的。

 

三、遇到问题以及解决方案

 

酒店列表页大小地图由于数据依赖和同步联动的关系,开发过程中会存在线程同步等各种各样的问题。

 

1)酒店数据与地图数据模型转换

 

列表页酒店数据的 JSON 结构是非常复杂的,而且不能直接用于地图打点使用,需要转换为地图不同覆盖物需要的 Model 数据结构。同时,这个转换过程相对 UI 线程而言,算是一个耗时操作,所以所有地图打点渲染前的数据转换都需要放置到子线程中单独处理,否则就会造成 UI 卡顿。

 

解决方案有很多,这里我们选择 Android 系统提供的 AsyncTask 类,来实现 UI 线程与子线程的切换与数据通信问题。

 

2)酒店 List 的线程同步问题

 

地图数据来源于 HotelListCacheBean 共享内存中的酒店主列表,由于转换过程放置在子线程中,而且不能通过加锁阻断主列表的用户操作,那么必然存在同一进程不同线程的数据同步问题。

 

对于小地图来说,用户滑动列表或修改筛选项的操作是随意而为的,短时间内可以不停操作,如果在子线程中多次对CacheBean 中的可变数据产生依赖,就会造成前后不一致问题,引发多线程并发造成的List 集合异常,如:

 

- ConcurrentModificationException

 

- ArrayIndexOutOfBoundsException

 

为了规避这种问题,应减少线程处理过程中的多次依赖,而是在线程开始执行前,复制目标数据源,供子线程处理。

 

3)酒店地图数据的一致性问题

 

前面说到,借助 AsyncTask 实现在子线程中转换数据模型,然后切换到 UI 线程中渲染地图,理想状态下,是没有什么问题的。当多线程并发执行,CacheBean 中的酒店数据可能与地图 Marker 数据不同步,实际渲染地图的数据可能与酒店列表的数据对应关系错乱,导致用户操作发生异常。比如用户选择某个 Marker 点联动的酒店列表或底部酒店卡片不是实际想要的酒店信息。这就需要开发这边保证数据转换与地图渲染单次流程的完整性,类似多线程中的原子性问题。

 

这里我们采取的解决方案是,使用Java Atomic 包提供的线程安全类AtomicBoolean 创建一个 Flag 标识位,根据标识位来控制不同批次数据处理的完整性,类似这样:

 

private val isMapTaskDoing : AtomicBoolean =AtomicBoolean(false)
 
override fun doInBackground(vararg params: String):Void? {
    if(isMapTaskDoing.compareAndSet(false, true)) {
        //converse data
    }
    return null
}
 
override fun onPostExecute(result: Void?) {
    // render ui
   isMapTaskDoing.set(false)
}

4)小地图Marker 分页问题

 

列表页小地图的酒店等数据来源有很多,比如用户在大地图过滤的酒店数据也可能转换到小地图来展示。这些不同场景CacheBean 中保存的酒店数据分页Size 标准是不一样的。

 

根据产品需求,小地图展示的是当前页与上一页的酒店数据,为了保证 Marker 数据分页的统一性与连贯性,可将CacheBean 中的多页酒店数据视为一个整体,再根据 Marker 自身定义的分页标准进行分页。对于用户而言,当有多页数据出现时,用户是不知道当前选中酒店位于哪一页,这也不是用户关心的问题,技术能做的就是确保用户体验是一致的。

 

5)列表页地图屏幕半径问题

 

列表页大地图拥有一个搜索屏幕内酒店的功能,这里需要获取当前屏幕内最小展示区域。一开始的时候,计算使用的是地图矩形展示区域内的最小圆经纬度坐标(由地图边界距离屏幕中心点最短半径决定),传递给 service 端,获取圆内酒店list 数据。

 

事实上,圆形在业务上覆盖区域比矩形区域要小(少了四个角落的数据),在技术上,也比矩形区域计算效率低(涉及弧度等数学公式),改为矩形区域计算更具优势。

干货 | 携程酒店安卓地图开发实践_第5张图片

四、酒店地图业务开发总结

  

业务上,携程酒店借助地图给用户带来更优的使用体验,未来能够探索的功能创新点也非常多。开发上,Map 架构也会随着业务不断迭代变化,而遇到的问题也是各种各样,甚至那种实现代码与产品功能冲突类的问题也会存在,希望这些内容或经验能够帮助到大家。我们也在不断优化,持续升级的前进过程中。

【推荐阅读】

  • Kotlin/Native 异步并发模型初探

  • 携程机票 App Kotlin Multiplatform 初探

  • 携程无线APM升级实践

  • 携程酒店iOS动态View的探索

  • 《携程技术2019年度合辑》,送给爱学习的你

干货 | 携程酒店安卓地图开发实践_第6张图片

 “携程技术”公众号

  分享,交流,成长

你可能感兴趣的:(干货 | 携程酒店安卓地图开发实践)