第02章 - 架构总览与模块划分

2.1 架构要回答的问题

打开 MapLibre Native 源码时最常遇到的困惑是:Map、Style、Renderer、RenderOrchestrator、TilePyramid、TransformState 谁调谁?一次缩放、一次样式改动,数据从哪进、从哪出?为什么 Android 的 MapView 和 iOS 的 MLNMapView 里看不到渲染代码?

答案落在一句话上:Map 对象所属的线程负责 API 调用、持有 Style、发起渲染;渲染在这个线程执行,但所有重计算(瓦片解析、文字排版、顶点构建)被派发到每 Style 4 个工作线程。平台壳只做三件事:开窗口、收手势、把帧交给 surface。

2.2 仓库分层

+---------------------------------------------------------------+
| platform/{android,ios,macos,darwin,linux,windows,node,qt,ohos} |  <- 平台壳
|   UI 控件、手势、生命周期、surface 绑定、语言绑定              |
+------------------------------+--------------------------------+
                               | 调用 C++ API
+------------------------------v--------------------------------+
| include/mln/  (公共头)     src/mln/  (实现)                    |
|                                                               |
|  map/       Map、Transform、TransformState                     |
|  style/     Style、Layer、Source、expression、parser           |
|  renderer/  Renderer、RenderOrchestrator、RenderTree、TilePyramid |
|  tile/      Tile、TileCache、VectorTile、GeometryTileWorker    |
|  layout/    SymbolLayout、SymbolInstance、clip_lines、merge_lines |
|  text/      Placement、CollisionIndex、GlyphManager、harfbuzz   |
|  gfx/ + gl/ + mtl/ + vulkan/ + webgpu/  渲染后端与 drawable     |
|  storage/   FileSource、DatabaseFileSource、PMTiles、MBTiles   |
|  actor/     跨线程 actor 框架                                   |
|  layermanager/  各图层类型的工厂与注册表                        |
+---------------------------------------------------------------+

ARCHITECTURE.md 说得很清楚:include 是公共核心 API,src 是实现私有头,二者都嵌在命名空间目录下,正确写法是 #include <mln/...>。注意该文档正文里还留着”nested under an mbgl directory”的旧描述,但下方示例已改成 mln——文档本身没同步干净,以目录为准。

平台职责边界:platform/darwin 是 iOS 与 macOS 共用的 Objective-C 层;platform/default 放跨平台默认实现(OpenGL 上下文、文件源、HTTP);platform/android/src/cpp/ 是 JNI 桥,把 Kotlin/Java 调用转成 mln:: 调用。main 分支还多了 platform/ohos(HarmonyOS)。

2.3 线程模型

ARCHITECTURE.md 的 Threading 一节给出运行时线程构成:

线程 数量 职责 禁止
主线程(创建 Map 的线程) 1 处理 Map API、持有 Style、执行渲染 大计算、阻塞 I/O
工作线程 每 Style 4 个 解析矢量瓦片、文字排版、生成顶点缓冲 触碰 Map 状态
FileSource 线程 1 网络请求、SQLite 读写 解析地图数据

一个常被忽略的事实:渲染在主线程上执行,不是独立渲染线程。Android 和 iOS 都是这样,所以主线程被业务代码占住时地图立即掉帧。反过来,瓦片解析在 4 个工作线程上,首屏加载时 CPU 占用接近 4 个核。

跨线程通信走 actor 框架(include/mln/actor/)。原则是任意两个线程之间不共享可变内存,参数按值传递或用 std::unique_ptr 转移所有权。发起跨线程调用返回 std::unique_ptr<AsyncTask>,销毁它表示”结果不再需要”,工作线程可中止。取消一次瓦片请求不需要显式 cancel —— 丢掉 AsyncTask 就够了。

2.4 不可变性与 style diffing

Style、Layer、Source 是可变对象,但其实现类 Layer::Impl、Source::Impl 不可变(Immutable<T>)。改一个图层属性的过程:

  1. 复制现有 Impl 得到新实例
  2. 在新实例上改值
  3. 冻结成不可变引用,替换 Layer 持有的旧引用

旧引用不修改,仍在其他线程上用旧值的代码不受影响。新值靠 style diffing 生效:RenderStyle 逐帧比较新旧 Impl 快照,指针相同就跳过,不同就重算。切换整个 style(亮色切暗色)能平滑过渡,原因就是 diff 不关心快照来自哪个样式文件。

实际影响:频繁调用 setPaintProperty 每次都产生新 Impl 与一次 diff,不会立即全量重算;真正昂贵的是”一帧内改大量图层”,diff 工作量会集中爆发。

2.5 一次渲染的调用链

平台壳 (MapView / MLNMapView)
   |  setStyle() / jumpTo() / 手势
   v
mln::Map  --------------------------------> style::Style
   |  update(UpdateParameters)                |  diff
   v                                          v
RendererFrontend (平台实现)  --->  Renderer::render()
                                      |
                                      v
                              Renderer::Impl
                                      |
                                      v
                        RenderOrchestrator::createRenderTree()
                                      |
                    +-----------------+------------------+
                    |                                    |
              RenderSource                        RenderLayer
              (TilePyramid)                       (bucket / drawable)
                    |                                    |
                    +-----------------+------------------+
                                      v
                                 RenderTree
                                      |
                                      v
                         gfx::RendererBackend
                    (OpenGL / Metal / Vulkan / WebGPU)

写自定义渲染或排查崩溃要用到两个构造签名:

// include/mln/map/map.hpp
explicit Map(RendererFrontend&,
             MapObserver&,
             const MapOptions&,
             const ResourceOptions&,
             const ClientOptions& = ClientOptions(),
             const util::ActionJournalOptions& = util::ActionJournalOptions());

// include/mln/renderer/renderer.hpp
Renderer(gfx::RendererBackend&,
         float pixelRatio,
         const std::optional<std::string>& localFontFamily = std::nullopt);

RendererFrontend 是平台与 Renderer 的桥。它的注释写明:隐藏线程细节,回调永远回到最初线程。接口只有三个纯虚函数——reset()、setObserver()、update(),平台各自实现(iOS 在 platform/darwin,Android 在 platform/android/src/cpp)。

2.6 TilePyramid:一处可见区域的瓦片集合

TilePyramid 在 src/mln/renderer/tile_pyramid.hpp(不是 src/mln/tile/),持有某个 source 需要的全部瓦片。核心成员:

// src/mln/renderer/tile_pyramid.hpp(结构示意)
class TilePyramid {
public:
    void update(const std::vector<Immutable<style::LayerProperties>>& visibleLayers,
                bool needsRendering, bool needsRelayout,
                const TileParameters&, const style::Source::Impl&,
                uint16_t tileSize, Range<uint8_t> zoomRange,
                std::optional<LatLngBounds> bounds,
                std::function<std::unique_ptr<Tile>(const OverscaledTileID&)> createTile);

    void handleWrapJump(float lng);   // 跨 ±180° 经线时处理坐标跳变
    void setCacheSize(size_t);
    void reduceMemoryUse();

private:
    std::map<OverscaledTileID, std::unique_ptr<Tile>> tiles;   // 已创建
    TileCache cache;                                           // 移出视野的缓存
    std::map<UnwrappedTileID, std::reference_wrapper<Tile>> renderedTiles;  // 本帧绘制
};

两个 ID 类型要分清:OverscaledTileID 带 overzoom 层级(渲染层高于数据层时复用父瓦片),UnwrappedTileID 去掉了世界环绕。

2.7 TransformState:相机快照

TransformState 是 Transform(负责动画与约束)算完之后交给渲染层的只读快照,构造函数默认值直接决定初始行为:

// src/mln/map/transform_state.hpp
TransformState(ConstrainMode = ConstrainMode::HeightOnly,
               ViewportMode = ViewportMode::Default);

它给两类矩阵:matrixFor(mat4&, const UnwrappedTileID&) 把瓦片坐标映射到屏幕,getProjMatrix(mat4&, uint16_t nearZ = 1, bool aligned = false) 给投影矩阵。nearZ 默认 1,倾斜时用于近平面裁剪;aligned=true 做像素对齐,避免标注抖动。

还有可写接口 setProperties(TransformStateProperties&),字段全是 std::optional,只更新传入项。这是给手势增量更新准备的——拖拽只改 x/y,不重建整个状态。

2.8 平台壳的公共面

各平台 API 不同,但概念一致:

能力 Android iOS
样式加载 map.setStyle(url) MLNMapView(styleURL:)
相机 map.cameraPosition = CameraPosition.Builder()... setCenter(_:zoomLevel:animated:)
图层 map.style?.addLayer(...) MLNStyle.addLayer(_:)
查询 map.queryRenderedFeatures(...) visibleFeatures(in:styleLayerIdentifiers:)
生命周期 手动转发 mapView.onStart()/onResume()/... UIViewController 自动管理

Android 需要手动转发生命周期,这是 SurfaceView 体系决定的;iOS 没有这个问题。

2.9 调试手段

开 debug 标志:Map::setDebug(MapDebugOptions) 可叠加 TileBorders(瓦片边界)、Timestamps(每帧耗时)、CollisionBoxes(标注碰撞框)。

拿帧耗时:实现 RendererObserver::onDidFinishRenderingFrame(RenderMode, repaint, placementChanged, frameEncodingTime, frameRenderingTime),后两个参数就是帧编码与渲染耗时。

崩溃在 worker 线程:先看 dumpDebugLogs()。Map::dumpDebugLogs() 与 Renderer::dumpDebugLogs() 会把当前样式、瓦片、渲染状态打进日志,通常能定位到是哪个 tile 的哪个阶段挂了。

样式改了不生效:先确认调用发生在创建 Map 的线程。Map 的 API 只能在同一线程调用,跨线程调用是未定义行为,表现通常是偶发崩溃而非明确报错。

2.10 生产建议

  • 主线程不要做重活:渲染就在主线程,任何超过 8ms 的主线程操作直接变掉帧。
  • 避免单帧大改:一帧内改 500 个图层属性,diff 会集中算完。分批改,或用图层可见性切换替代批量 set。
  • 调 TilePyramid 缓存:setCacheSize(size_t) 控制缓存瓦片数,低端机下调,否则内存会先于帧率出问题。
  • 跨重命名升级要全量替换:mbgl:: → mln::、include/mbgl/ → include/mln/,第三方插件类代码一处漏改就编译失败。

2.11 本章小结

要点 事实
分层 platform/ 平台壳 + include/mln、src/mln 核心
线程 主线程渲染 + 每 Style 4 个工作线程 + 1 个 FileSource 线程
跨线程 actor 框架,按值/所有权传参,销毁 AsyncTask 即取消
可变性 公共类可变、Impl 不可变,靠 style diffing 传递变化
渲染入口 Renderer::render() → Renderer::Impl → RenderOrchestrator::createRenderTree()
瓦片集合 TilePyramid 持有 tiles、cache、renderedTiles 三组容器
相机快照 TransformState 提供 matrixFor、getProjMatrix、setProperties