做移动端的地图渲染,最怕的不是功能多,而是选型错了后面一路返工。我最近一个项目需要在 iOS 和 Android 双端上线一套带矢量瓦片、离线包和自定义样式的完整地图能力,同时还要求后续能嵌入跨平台层统一维护。对比一圈下来,最终落在 MapLibre Native 上。这篇文章就围绕这个标题,把我在接入过程中踩过的坑、验证过的方案、以及跨平台集成的取舍完整梳理一遍,给同样在评估地图方案的团队一个参考。
这篇文章不会讲太多“MapLibre 是什么”的科普内容,因为官方文档已经写得很清楚了。我会把重点放在三件事上:第一,为什么在纯原生双端和跨平台框架混合的场景里选它;第二,iOS 和 Android 两个端真实的工程接入过程长什么样;第三,跨平台集成时架构和状态同步怎么设计才不会翻车。适合正在做地图选型、或者已经在接 MapLibre Native 但被某个环节卡住的开发者看。
1. 需求定型:为什么是 MapLibre Native,而不是其他地图方案
1.1 先搞清楚项目到底要什么
做技术选型的第一步,永远是先把需求边界列清楚。我手头那个项目,核心诉求其实很朴素:
- 双端必须有原生地图渲染能力,不能在 WebView 里糊弄,因为要长时间跑定位轨迹和大量标注点;
- 地图样式要能完全自定义,包括底图配色、道路层级、POI 显隐;
- 必须支持离线瓦片,业务现场经常没网;
- 未来可能要接入跨平台容器,所以要考虑桥接层怎么做;
- 项目预算里没有“地图授权费”这一项,所以商业 SDK 直接出局。
把需求列到这里,答案其实已经浮出水面了。商业地图 SDK 功能确实全,但授权费用和每年续费对中小团队来说是个不小的包袱。WebView 方案听起来简单,但我在之前一个项目里吃过亏:瓦片多了 WebView 内存直接爆,而且和原生手势、定位能力的交互非常别扭,后面越改越痛。
1.2 MapLibre Native 的核心优势
MapLibre Native 是 Mapbox GL Native 的一个社区延续分支。它保留了完整的矢量瓦片渲染能力,支持 OpenGL ES 和 Metal 渲染路径,而且 API 设计和旧版 Mapbox SDK 的熟悉度很高——如果你以前写过 Mapbox 的移动端 SDK,上手 MapLibre Native 几乎是无痛的。
它最打动我的一点是样式规范完全开放。MapLibre 遵循 MapLibre Style Specification,你可以用一套 JSON 样式文件同时驱动 iOS、Android、Web 甚至桌面端。这意味着设计侧出一次样式稿,五端都能复用,维护成本低得惊人。对需要多端统一视觉的团队来说,这个价值比单纯的“开源免费”重要得多。
另外,它的数据源体系也非常友好:支持矢量瓦片、栅格瓦片、GeoJSON,还有 MBTiles 离线包。尤其是 MBTiles,拿 SQLite 存瓦片,离线场景下非常稳。我后面会专门讲离线包的管理方式。
1.3 什么时候不该选它
我说话比较直,MapLibre Native 虽然好,但不是万能的。如果你的团队没有 Android/iOS 双端原生开发能力,或者地图需求只停留在“显示一张图 + 几个大头针”,那 MapLibre 反而是过度设计。这种情况下 Web 地图 + WebView 壳更快,成本更低。
另外,如果业务涉及非常复杂的轨迹平滑、导航路线引导、实时路况这种高德/百度级别的能力,MapLibre Native 需要你自己搭很多东西——它提供的是渲染引擎和基础位置能力,不是“导航 App”级别的开箱即用。我的态度是:它是“地图引擎”,不是“地图应用”。把这个定位搞清楚,才不会出现接完发现缺一堆功能的尴尬。
2. iOS 端从零接入:依赖配置、地图加载与交互细节
2.1 环境准备与依赖安装
iOS 端接 MapLibre Native 最常见的方式是 CocoaPods。直接在 Podfile 里加一行:
pod 'MapLibre'然后pod install就行。这里我强烈建议锁版本号,不要用不带版本的写法。MapLibre 的 API 还在快速演进期,有时候小版本升级就可能出编译错误,锁版本至少能保证团队其他人的环境和你一致。
如果你不想用 CocoaPods,也可以直接用 Swift Package Manager。选 SPM 时注意添加依赖的地址要用官方仓库的 HTTPS 地址,版本号选最新的稳定 release。我个人更习惯 CocoaPods,因为项目里其他第三方库都是用它管理,统一工具链少一套心智负担。
首次编译会比较久,因为 MapLibre Native 的核心渲染层是 C++ 写的,编译链接的时候要处理一堆底层依赖。第一次 pod install 之后跑起来,等MapLibre.framework构建完成,基本就通了。
2.2 地图初始化与样式加载
iOS 端核心类名是MLNMapView。初始化代码非常直接:
import MapLibre let styleURL = URL(string: "https://your-tile-server.example.com/styles/basic/style.json") let mapView = MLNMapView(frame: view.bounds, styleURL: styleURL) mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight] mapView.tintColor = .systemBlue mapView.logoView.isHidden = true view.addSubview(mapView)这里有个比较隐蔽的细节:styleURL如果传nil,MapLibre 会加载内置的默认样式,但内置样式依赖在线资源,如果你本来就要走自定义数据源,建议直接传自己的 style JSON 地址。加载远程样式时,一定要保证这个 URL 在公网可达,而且返回的 JSON 格式完全符合样式规范,否则地图会白屏或者只显示网格背景。
初始化完成后,等地图准备好再操作相机或添加数据。有个代理方法mapViewDidFinishLoadingMap(_:),正式项目里等这个回调再干活是最稳的:
extension MapViewController: MLNMapViewDelegate { func mapViewDidFinishLoadingMap(_ mapView: MLNMapView) { let camera = MLNMapCamera( lookingAtCenter: CLLocationCoordinate2D(latitude: 30.2, longitude: 120.1), fromDistance: 4000, pitch: 0, heading: 0 ) mapView.setCamera(camera, animated: true) } }坐标取值我这里用的是“某业务区域的中心点”,你在实际项目里换成自己业务的经纬度即可。相机参数里fromDistance的含义是“相机距离地面的高度”,类似无人机的飞行高度,数值越小地图越近。
2.3 标注、图层与手势交互
在地图上加一个点标注,最直接的方式是用MLNPointAnnotation:
let annotation = MLNPointAnnotation() annotation.coordinate = CLLocationCoordinate2D(latitude: 30.2, longitude: 120.1) annotation.title = "设备点" annotation.subtitle = "在线" mapView.addAnnotation(annotation)但我要提醒你:点标注虽简单,不适合一次性加几百个。遇到批量点位展示的场景,更好的做法是走 GeoJSON 数据源加 symbol 图层,这个后面跨平台章节我会专门讲性能问题。原生标注对象多了以后,iOS 上会出现明显的卡顿和内存上涨。
手势交互上 MapLibre 默认支持单指缩放、双击放大、双指倾斜旋转。有一点需要注意:如果你在 mapView 上层再叠加自己的 scrollView 或侧滑抽屉,容易出现手势冲突。我当时的处理方式是自定义UIGestureRecognizerDelegate,在gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:)里返回true,让地图和抽屉各自的手势互相不阻塞。这个细节不处理,用户在抽屉上滑动时会莫名其妙拖动地图。
2.4 iOS 特有坑位提醒
接入过程里,iOS 端我碰到最典型的问题是切后台回来后地图纹理花屏。后来定位到原因是 GPU 上下文在 app 进入后台时被系统回收,但 MapLibre 没有自动重建。解决办法是在AppDelegate的applicationDidEnterBackground/applicationWillEnterForeground回调里,显式暂停和恢复地图渲染。如果MLNMapView提供了对应的方法就用,没有的话可以把地图视图从窗口移除再重新添加,强制触发一次重新渲染。
另外,真机调试时如果发现地图字体非常模糊,检查一下 style JSON 里 symbol 图层的text-font字段是否包含中文资源。MapLibre 默认字体对中文支持有限,需要额外挂载中文字体库到样式里。这个不处理,整张地图上的中文地名全是方框。
3. Android 端接入全流程:Gradle 配置、生命周期与设备兼容
3.1 Gradle 依赖与权限配置
Android 端的入口是org.maplibre.android,在build.gradle里这样加:
repositories { mavenCentral() } dependencies { implementation 'org.maplibre.gl:android-sdk:你的版本号' }Android 端 MapLibre 的最低 API 版本建议设置到 21(Android 5.0)以上。低于这个版本,OpenGL ES 2.0 的兼容性会出各种奇怪问题。权限方面,如果只是显示地图,只需要网络权限;如果要显示用户当前位置,得加上定位权限。我一般在AndroidManifest.xml里这样配:
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />注意 Android 6.0 以上运行时权限得动态申请,不能只写在 Manifest 里。我用的是第三方权限库封装一下,省得在 Activity 里写一堆回调样板代码。
3.2 MapView 生命周期绑定
MapLibre 的MapView必须在 Activity 或 Fragment 的生命周期回调里手动同步调对应方法。这是一步都不能省的,否则会崩溃或者内存泄漏。我整理了一个标准模板,每个接入项目都照这个写:
override fun onCreate(savedInstanceState: Bundle?) { mapView.onCreate(savedInstanceState) } override fun onStart() { super.onStart() mapView.onStart() } override fun onResume() { super.onResume() mapView.onResume() } override fun onPause() { super.onPause() mapView.onPause() } override fun onStop() { super.onStop() mapView.onStop() } override fun onDestroy() { super.onDestroy() mapView.onDestroy() } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) mapView.onSaveInstanceState(outState) }有一个非常容易犯的错:Activity 旋转重建时,如果你没有调onSaveInstanceState,地图的相机位置和状态会全部丢失,用户一转屏就回到默认中心点。转屏后的体验会非常差。所以这个模板里所有方法都要在,一个都不能少。
拿地图实例用getMapAsync:
mapView.getMapAsync { map -> map.setCamera( CameraUpdateFactory.newCameraPosition( CameraPosition.Builder() .target(Point.fromLngLat(120.1, 30.2)) .zoom(12.0) .build() ) ) map.addMarker( MarkerOptions() .position(Point.fromLngLat(120.1, 30.2)) .title("设备点") ) }3.3 Android 端的高分屏适配
Android 设备碎片化严重,我在测试机上遇到过两类问题:低端机上地图拖动掉帧明显;部分国产 ROM 上地图出现绿色或黑色条纹。
掉帧问题多半是 style JSON 里图层效果太重,比如全局光晕、大量阴影、透明度叠加。实际调优时我把非核心图层的visibility关掉后,帧率有明显回升。
条纹问题比较复杂,网上讨论也比较多,根源是部分 GPU 驱动对 GL_SRGB8_ALPHA8 纹理格式支持不完整。规避手段有两个:一是升级到最新版本 MapLibre,新版本在渲染管线上修了很多驱动兼容问题;二是如果还复现,尝试在 style JSON 里避免使用带透明度混合的栅格图层,改用纯矢量图层。如果项目里的底图必须是用户上传的栅格影像,那这个问题需要拿到具体机型信息去 MapLibre 的 issue 区找对应修复记录。
3.4 Android 特有的内存问题
Android 上另一个容易踩的是OutOfMemoryError。地图纹理、缓存瓦片、GL 上下文都很吃内存。我处理过的一个案例:一个地图页面反复进出十几次,内存从 180MB 一路涨到 400MB,最后直接闪退。
检查下来,发现是MapView没有被真正销毁。很多开发者只调了onDestroy,但忘了把 mapView 从 ViewGroup 里移除并置空引用。我建议在页面销毁时彻底释放:
override fun onDestroy() { mapView.onDestroy() (mapView.parent as? ViewGroup)?.removeView(mapView) mapView = null super.onDestroy() }如果你用的是 Fragment,还要注意 Fragment 的 View 被 detach 时 mapView 也会被系统回收,此时不要尝试在 detach 之后继续调用地图方法,否则必崩。
4. 跨平台集成:共享状态层与 Flutter / React Native 桥接实践
4.1 先确认你想要的“跨平台”是哪种形态
我见过很多团队一谈跨平台就直接往 Flutter 或 React Native 上冲,结果项目做到一半发现原生能力桥接的成本比重新写两套还要高。“跨平台”其实分几种层次:
- 只共享业务逻辑层(网络请求、数据模型、状态管理),UI 各端原生;
- 用 Flutter / React Native 统一 UI 和业务层,地图能力走原生插件;
- 用 WebView 加载地图 JS,原生只做壳。
我实际项目中采用的是“共享状态层 + 双端原生 MapLibre”的组合。为什么不用 Flutter?因为团队当时没有足够 Flutter 经验,短期内硬切风险大。为什么不用 WebView?前面说了,性能没法满足长时间轨迹任务。所以“逻辑跨平台、视图端原生”反而是收益风险比最高的方案。
如果你团队已经以 Flutter 为主,那就可以考虑 MapLibre 的 Flutter 插件。项目里有社区维护的 map lib 组件,通过 method channel 桥接原生 MapLibre。React Native 也一样,有对应的原生 View 封装库。这类方案能用,但你要有心理准备:桥接层本身就是一层技术债,尤其是当 MapLibre 版本升级时,插件不跟上你就会被困在旧版本里。
4.2 地图状态同步设计:事件总线与单一数据源
跨平台集成最核心的问题,是地图状态如何同步。这里的“状态”指的是:中心点经纬度、缩放级别、旋转角度、倾斜角度、当前可见区域。如果双端各管各的,业务层拿到的数据可能不一致,后面很多坑都是从这小地方引发的。
我采用的做法是:建立一个统一的地图状态模型,双端通过事件总线发布和订阅变化。原生端每次相机移动结束,就发一个状态事件;业务层如果需要知道当前位置,只订阅这个事件,而不直接调用地图 API。好处是后端逻辑完全不用关心地图跑在哪个端上。
原生端的状态发布,可以用 MapLibre 相机移动回调:
map.addOnCameraIdleListener { val center = map.cameraPosition.target val zoom = map.cameraPosition.zoom val bearing = map.cameraPosition.bearing // 发布状态事件 stateBus.post(MapStateEvent(center, zoom, bearing)) }用OnCameraIdleListener而不是OnCameraMoveListener,是因为 move 回调触发频率太高,每帧都在跑,跨线程传输必然卡顿。idle 事件只在相机停止后触发一次,对业务层来说感知完全一致,性能却好得多。
4.3 React Native / Flutter 桥接的注意点
如果你最终还是决定走跨平台 UI 框架路线,有几个桥接层的问题必须提前设计好。
首先是视图生命周期不同步。原生 MapView 是在原生侧管理的,而 Flutter/RN 的页面生命周期和原生 View 的生命周期经常错位。常见问题是:页面已经销毁,但原生侧的地图还在跑定位回调,导致内存泄漏。所以桥接封装里一定要暴露dispose方法,并且在 UI 框架的页面销毁生命周期里调用它。
其次是手势冲突。Flutter 和 RN 都有自己的手势识别系统,地图的原生手势(缩放、旋转)和框架层手势(滑动列表、返回手势)会互相抢事件。处理方式是框架端手势失败后才把事件交给原生地图。这个优先级设置不对,用户体验就是“地图划不动”或者“列表滑不动”。
再然后是地图事件向业务层的回传。地图上的点击事件、标注点击事件,都需要从原生侧传到 Flutter/RN 业务层。这个通道的数据序列化,我建议全部用扁平 JSON 结构,字段名双端保持一致,不要让桥接层解析复杂嵌套对象。桥接层的序列化解析成本会被放大的。
4.4 状态透传的正确打开方式
我们当时还在双端之间抽了一个简单的状态透传协议。核心结构大概这样:
{ "type": "camera_changed", "payload": { "center": [120.1, 30.2], "zoom": 14.5, "bearing": 20.0, "pitch": 30.0, "timestamp": 1700000000000 } }业务层解析这个事件来驱动自己的 UI,比如列表项联动、地图外部的状态栏展示等。整个链路通讯频率做了节流,600ms 内只发一次有效事件。实测下来,三端(iOS、Android、业务层)状态不一致的概率从早期的 30% 降到了基本为 0。
这里有个经验:千万不要在地图的每次 move 回调里直接同步 UI,否则你会看到 UI 在疯狂跳变。正确做法是“移动过程中只更新本地地图,移动停止后再通知业务层”。低频通知比高频同步靠谱得多。
5. 性能与离线:生产环境里的地图优化经验
5.1 渲染性能核心指标与调优
地图的性能指标,最直接的是帧率和内存。MapLibre 用的是矢量瓦片渲染,在移动端帧率瓶颈通常不是 GPU 而是 CPU 端的样式解析和数据处理。
我优化性能时梳理了几条主线:
- 控制图层数量:一个 style JSON 里图层数超过 30 个,低端机帧率基本保不住。把不必要的图层直接删掉,比任何代码层面的调优都有效。
- 降低瓦片请求频率:MapLibre 的瓦片请求是按需发起的,当地图快速拖动时,会突然发出大量瓦片请求,导致网络层拥塞。可以通过设置
map.setMinZoom和map.setMaxZoom来限制比例尺范围,同时给瓦片服务端加一层代理缓存。 - 聚合点数据:几十个标注用原生 Marker 没问题,几百上千个必须走 GeoJSON 源 + style 图层渲染。这个逻辑有点像“用 CSS 批量画图标”而不是“每个图标创建一个 View 对象”,性能差异是数量级的。
- 关闭不必要的动画:全局
map.setStyle重载样式时,动画过渡会加重首帧负担,业务上可以接受的前提下建议直接关闭过渡动画。
5.2 离线包与缓存:MBTiles 管理和更新策略
离线地图是 MapLibre 的强项。标准做法是构建 MBTiles 文件,它本质是一个 SQLite 数据库,瓦片 PNG 数据直接存成 blob。MapLibre 原生支持加载 MBTiles 离线文件。
离线包的管理,我的经验是按“区域 + 版本 + 日期”规划文件命名。比如subway_route_v3_20240115.mbtiles。每个离线包对应一个业务区域,更新的时整包替换,不做局部增量。这样做最省心,也避开了增量更新的复杂性。
离线包更新要特别注意时机。用户可能正在看地图,你直接覆盖文件会导致 SQLite 连接冲突。常见做法是:先下载新包到临时目录,校验 MD5 通过后,再通过一个“手动切换”动作把当前地图的离线数据源切到新文件。切换时地图会短暂闪一下,但至少不会崩溃。
缓存策略上,MapLibre 默认会在应用沙盒里存瓦片缓存。线上我主动控制了缓存上限,一般项目里设置 200MB 到 500MB 是安全的。超出上限后清理最久未使用的瓦片,保证地图缓存不会无限膨胀。
5.3 大数据量点位展示的聚合方案
前面提到的 GeoJSON 源 + 图层方式,展开讲一下。点位数据在万级以下,直接加载全部 GeoJSON 到客户端画 symbol 图层是可行的。超过万级,就必须做聚合。
MapLibre 本身支持数据驱动的样式,你可以利用circle-radius和circle-opacity根据聚合数量动态渲染:
{ "id": "points-cluster", "type": "circle", "source": "points", "filter": ["has", "point_count"], "paint": { "circle-radius": ["interpolate", ["linear"], ["get", "point_count"], 1, 14, 100, 32], "circle-color": ["step", ["get", "point_count"], "#51bbd6", 10, "#f28cb1", 50, "#f1f075"] } }这是 MapLibre 风格聚合的原生 JSON 写法,源数据源开启聚类:
{ "type": "geojson", "data": "your-points.geojson", "cluster": true, "clusterMaxZoom": 14, "clusterRadius": 50 }这套配置写一次双端通用,真正做到了“一次样式,两处复用”。我实测 5 万条点位一次性加载,聚合渲染后帧率依然稳定在 30fps 以上,内存占用比逐个添加标注减少了至少 80%。
5.4 构建体积控制
只要你留意,MapLibre Native 添加后 App 体积增量是可以控制的。Android 端由于包含多个 ABI 架构的 so 库,单 ABI 增量大概在 20MB 左右。如果不需要在 x86 模拟器上跑,可以通过abiFilters只保留arm64-v8a和armeabi-v7a,体积能省不少。
iOS 端由于MapLibre.framework包含模拟器和真机两种架构,打包的时候注意用 Release 模式打出来的包系统会自动处理架构裁剪。实际增量大概在 40MB 上下,对地图引擎来说可以接受。
如果离线包是内置在 App 里的,一定要确认发布的时候有没有把 MBTiles 误打成资源包。我在一个项目里见过把 GB 级离线瓦片塞进 assets 目录最后商店审核超时的惨剧。大体积离线数据应该放在应用沙盒的 Library/Caches 或 Documents 目录,并通过应用内下载获取,不打进安装包。
6. 高频问题实录:地图黑屏、标注丢失与 GL 崩溃排查
6.1 地图区域全黑或白屏
现象描述:地图控件加载了,能看见网格或纯色背景,但是瓦片区域全黑。
排查顺序:
- 打开 style URL,直接在浏览器里访问,确认能正常返回 JSON;
- 确认瓦片服务地址没被防盗链限制,某些瓦片服务商会校验 HTTP Referer 头,移动端默认不带 Referer 会返回 404;
- 确认样式文件里的
sources字段定义的瓦片地址,是否用了file://本地路径但路径不存在; - 检查网络层是否开了代理抓包,代理对 HTTPS 证书校验失败也会导致瓦片拉不到。
最坑的是,MapLibre 加载失败时不会弹对话框,只在日志里打一条 error。所以上线前一定要留日志采集,否则排查这类问题只能靠瞎猜。
6.2 标注/图标不显示
标注不显示,大概率是图层顺序问题。MapLibre 的图层是有严格的 painter 顺序的,先画底图图层,再画标注图层。如果你的symbol图层在源数据里定义得很靠后,但 style JSON 里被排在了某个不透明底图图层之前,就会被遮住。
还有一种常见错误是缺少图标。symbol 图层可以通过icon-image引用样式内置的 sprite 图标,这个图标必须打包在 sprite 资源里。我遇到过 style JSON 里写了icon-image,但对应的 sprite 没上传,导致所有标注全部静默消失。检查方式:打开 style JSON 里的 sprite 地址,看能否访问到 PNG 和 JSON。
6.3 Android 切后台回来崩溃
场景:App 退到后台,过几分钟回来,地图区域显示空白或直接闪退。
原因基本是 GL 上下文失效。Android 系统在内存压力大时会回收后台 App 的 GPU 资源。MapView 重新回到前台时,如果没有正确恢复 GL 上下文,底层渲染就会异常。
排查和解决:
- 确保
onStop/onStart方法在 Activity 生命周期里被正确调用; - 升级到最新 MapLibre 版本,新版本对 GL 上下文恢复有更完善的处理;
- 如果自定义了 GLSurfaceView 相关的渲染扩展,检查是否和 MapView 冲突。
我建议做一次大范围的机型回归测试,重点覆盖国产主流厂商的旗舰和中低端机。不同厂商的系统对后台内存回收策略差异非常大。
6.4 地图拖动卡顿或出现残影
卡顿先看图层数量和瓦片请求频率,残影问题则多半是 GPU 渲染管线的 bug。MapLibre 在某些 Adreno GPU 驱动的机型上,快速拖动地图后边缘区域会出现“撕裂”或“拖影”。
这个问题的物理成因是GPU Tile Prerendering机制导致的纹理残留。临时方案是在地图的onCameraMoveEnd后强制一帧map.setStyle重绘,但这个方法只建议在问题机型上做降级处理。长期方案还是升版本,新版 MapLibre 对这块做了不少修复。
6.5 离线包无法加载
离线包加载失败,我遇到的最常见原因是:
- 文件名或路径中有中文字符,导致文件访问失败;
- MBTiles 文件损坏或格式版本不兼容;
- 离线包下载到一半就切换了网络,文件不完整。
排查方式很直接:用 SQLite 工具打开 MBTiles 文件,检查metadata表和tiles表是否存在。如果metadata表缺失,大概率是文件生成过程中出错。重下离线包通常能解决 90% 的问题。
7. 一点实用经验:先跑通最小验证,再铺开架构
最后分享一个我个人的实操经验。如果你正在评估 MapLibre Native,不要一上来就设计跨平台大架构。先花两三天时间,在 iOS 和 Android 两台真机上,分别接好 MapLibre、加载同一个 style JSON、显示一张可缩放的地图。这个小验证跑通了,后面所有的问题都只是工程量问题。
我当时就是这么做的。最开始还担心双端 API 差异会很大,实际上共同点远大于差异,两边只是换了一套包名和类名前缀。样式层完全共用一套 JSON,数据源完全共用一套 GeoJSON,这种“一次设计,双端复用”的开发体验,在真正做完一整个项目之后体会更深。
联动体验上,如果做到一半发现框架桥接层太复杂,大可以先退回“共享状态层 + 双端原生”的方式。跨平台 UI 是手段,不是目的。目的永远是业务迭代效率和用户体验的平衡。MapLibre Native 给了我很大的灵活度,也是我在后续项目里愿意继续用它的原因。