第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>)。改一个图层属性的过程:
- 复制现有 Impl 得到新实例
- 在新实例上改值
- 冻结成不可变引用,替换
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 |