一、背景§
2022年1月,滴滴出行App货运业务线开始迁移至滴滴小程序方案,滴滴小程序在API层面基本上与微信对齐,但在实际迁移完成前,就有着用户反馈滴滴小程序框架地图性能问题,原版滴滴小程序地图使用的Web方案,通过JavaScript API实现地图渲染的,而经过分析,行业内其他几家小程序方案的地图实现都是基于Native实现,Native地图相比于Web API性能完全不在一个等级之上;外加滴滴货运业务场景相对来说更加复杂,且货运业务又是一个重地图的场景,因此地图已然成为了一个体验上的短板。
二、技术调研§
调研市场上大量的Hybrid App后,市面上调用Native地图的Web混合技术,主要分为两类:
- WebV与Native地图页面是独立的两个页面,从Web跳转至Native地图页面,后取用地图数据回到WebView中。
- Web页面与Native地图页面属于同一个页面,将整个屏幕划分为上下两部分,操作时数据会双向传递。
而目前为止滴滴货运已经是一个运营已久的稳定业务,很显然不可能临时修改设计稿和产品方案,需要做到与原有产品逻辑一致,所以第二类实现方案更合适。
其中第二类方案有以下几种实现方法:

- Native地图和WebView各占据屏幕一半。
- Native地图层铺满屏幕,上层覆盖WebView渲染。
- WebView铺满屏幕,上层覆盖Native地图。
- WebView铺满屏幕,上层覆盖Native层,将地图组件渲染在底层的WebView上。
1.业务情况匹配§
(1)Native与WebView各占一半§
这一类方案实现布局相对来说比较容易,但是实现后针对一些情况的适配,会变得极其复杂,譬如弹出软键盘后整个view上移情况,以及取消上移后,在低端机器上渲染错误的情况。每当业务迭代,都需要客户端开发人员进行修正;且地图方案计划落地在其他业务线。遂放弃该方案。
(2)Native铺满屏幕§
这一类方案是将整个Native铺满屏幕,上层覆盖上一层WebView来实现,WebView默认透明,透过WebView可看到最底层的Native View。通过手势分发层,对需要的手势进行分发,WebView与Native通过JSBridge进行数据传输,进行双向的交互操作,譬如在屏幕上加入跟车气泡等方案,都是通过JSBridge实现。
(3)WebView铺满屏幕§
这一方案是将WebView铺满屏幕,Native在上曾渲染,需要设置宽高,对屏幕比例适配来说极不友好,遂放弃。
(4)将Native渲染在WebView中§
将Native组件直接渲染到WebView中,这种情况下Native就已经不存在,Native组件将会被载入WebView节点上。可以像用非原生组件一样去使用Native组件,在小程序方案中可以用任意小程序view去覆盖原生组件,使用z-index,以及使用scroll-view等容器来包裹原生组件。
2.方案选择§
经过筛选,最终留下两种实现方案。滴滴小程序开发框架目前只针对内部使用,在设计上,Android主要是用了v8引擎作为渲染方案,iOS使用的是JSC。作为框架开发人员我们要做的是在不引入其他引擎,以最小成本实现整个方案,并做到双端一致。
(1)iOS§
滴滴小程序在iOS端使用的是WKWebView进行渲染,WKWebView的设计使用的是分层的方式来实现的渲染,其将WebKit内核生成的Compositing Layer渲染成iOS上的一个WKCompositingView,其本质是客户端的一个原生View,内核会将多个DOM节点渲染到一个Compositing Layer上,所以合成层与DOM节点之间不存在一对一映射关系。但将DOM节点的CSS属性设置为overflow:scroll时,WKWebView会生成一个WKChildScrollView,与DOM形成映射关系,成为一个原生UIScrollView的子类;这也就意味着WebView中滚动实质上是被原生侧的滚动组件来承载。WKWebView如此设计是为了让iOS的滚动体验更接近原生流畅,但也因此,暴露了这个特点。
因此iOS端的实现可以直接使用WKChildScrollView来实现,原生组件在attached后会载入到WKChildScrollView中,在这里可以直接将原生组件插入到WKChildScrollView中来实现将Native组件渲染到WebView的目的,最终也就是实现了第四种方案。
(2)Android§
而Android这边使用的是v8引擎进行渲染,在初期调研的情况下,Android使用了第二种方案来实现,将Native平铺于底部,在上层覆盖上透明的WebView,在小程序侧,对 <cover-view> 标签增加一些属性,来镂空并将操作传导到底部NativeView中,来实现“融合”的效果。
但是这个方案并不完美,更像是在WebView这一层打洞,来镂空一部分最后传导到下层,严格来说,这并不算“融合”,两边元素不是同一级的,且这种方案在别的场景,例如内嵌视频播放器,随着小程序滚动的情况下,会需要Web不断的将当前的位置告诉给Native,这也就是 <cover-view> 增加了一些属性值的原因,需要不断的去告诉Native位置信息。
之后再此基础上,在不修改业务逻辑的情况下,不在WebView下建立Native View,在WebView初始化同时添加一个子View覆盖在WebView正上方,显示Web内容,Native组件添加到WebView和单独子View之间,来达到同级渲染的效果。

在后续的业务迭代流程中,为了增加更多的原生小组件,发现了 <embed> 可以满足类似于iOS端的混合实现,。Chromium的Extension包含了一个Plugin体系,利用这个特点,可以用 <embed> 去载入一个自定义的Plugin,嘿嘿。这个自定义的Plugin通过RenderLayer绘制,客户端会实现是将内容绘制到上面RenderLayer所绑定的SurfaceTexture上,通过SurfaceTexture来接管Native绘制,通知compositor绘制Native到WebView。
3.事件分发§
对于这种方案,令人头疼的是,事件该如何分发,无论多么融合,Native都是一个Native,Native组件的事件与实践消费和Web的模型是不相通的,会存在众多的冲突。譬如焦点占用、事件差异、事件消费时的逻辑冲突。
设计逻辑上需要保证以下:
- 不能因为原生组件划过焦点就终断整个事件。
- 事件必须在WebView传递一层,不能再进入Container之前就把事件消费掉。
- 不能让事件没进入到WebView就被消费掉。
- 事件进入WebView的时候要把控制权递交出去给WKWebKit等,走正常的逻辑处理,保证容器中的事件模型和前端是对齐的。
4.实现§
说回地图,最终的实现上,按照上面所说实现后,放开了地图的API能力,这里会涉及到一些内部的数据,就不一一展开,单从设计上来说,一个对外API应该有着以下的特点:
- 简洁明了
- 接口数越少越好,流程越短越好
- 地图上兼容自定义地图组件(在滴滴的地图有多种混搭方案)
- 一个接口只做一件事
- 不用不初始化(可以配合初始化参数实现)
- 内部内存管理(增加复用机制以及独立的内存管理)
- 减少全局回调
- 异常情况回调
最后实现了一个可以自定义替换地图的地图Service,可以更替为内部其他地图方案,以及自定义Marker方案。