Files
knowledge-kit/Chapter1 - iOS/1.144.md
2025-12-30 21:07:15 +08:00

326 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Weex 底层原理
## Weex 三线程模型
在查看 Weex 的文章时看到一段文字JSThread过于繁忙导致DOM线程和UI线程的堵塞三者类似于串行的机制运行」该怎么理解一段跨端引擎不是双线程的吗UI线程和逻辑线程。为什么这里看上去除了 DOM 线程、还有 UI线程和 js 逻辑线程?
文章地址https://developer.aliyun.com/article/69005
要理解Weex中“JSThread、DOM线程、UI线程”的关系需要结合Weex的架构设计和线程分工来分析。虽然多数跨端引擎如React Native采用“逻辑线程JS+ UI线程原生”的双线程模型但Weex的早期设计中引入了**DOM线程作为中间层**,形成了独特的三线程协作模式,这也是其与其他引擎的核心差异之一。
先明确三个线程的核心分工
1. **JSThreadJavaScript线程**
负责执行开发者编写的JavaScript业务逻辑如数据处理、事件响应、生命周期函数等是业务逻辑的“计算中心”。
2. **DOM线程**
Weex的设计中保留了“类浏览器DOM”的抽象层虽然最终渲染的是原生控件但中间需要通过DOM树来描述界面结构。DOM线程的核心作用是
- 维护虚拟DOM树的状态比如节点增删、属性更新
- 处理布局计算如通过CSS样式计算节点位置、大小
- 将JSThread传来的界面更新指令`appendChild``setStyle`转换为可被UI线程理解的“原生渲染指令”。
3. **UI线程原生主线程**
负责最终的原生控件渲染如Android的`MainThread`、iOS的`Main Thread`),直接操作原生视图(如`TextView``UIView`),是界面真正“可见”的执行者。
### 为什么三者类似“串行机制”?
Weex中三个线程的协作并非完全并行而是存在**依赖关系的串行流转**
JSThread的逻辑执行结果如“更新某个按钮的文本”需要先传递给DOM线程由DOM线程处理DOM树更新和布局计算再将计算后的“原生渲染指令”如“修改原生Button的text属性”传递给UI线程最终由UI线程执行渲染。
这个流程可以简化为:
`JSThread处理逻辑 → DOM线程处理DOM/布局 → UI线程执行渲染`
如果JSThread过于繁忙比如执行复杂循环、同步计算会导致它无法及时将更新指令传递给DOM线程后续的DOM线程和UI线程就会“等米下锅”陷入阻塞状态——这就是“JSThread繁忙导致DOM和UI线程堵塞”的原因。
### 为什么Weex需要单独的DOM线程
这与Weex的设计初衷有关早期Weex希望尽可能复用Web前端的开发习惯如基于DOM的界面描述、CSS布局因此保留了DOM层作为“桥接层”。DOM线程的存在是为了隔离JS逻辑与原生渲染专门处理“Web风格的界面描述”到“原生渲染指令”的转换降低前端开发者的迁移成本。
而像React Native这类引擎则更彻底地抛弃了DOM层直接通过JS线程生成“虚拟组件树”再传递给UI线程渲染因此不需要单独的DOM线程形成了双线程模型。
### 总结
Weex的三线程模型是其“兼容Web开发习惯”设计的产物JSThread负责逻辑DOM线程负责DOM/布局转换UI线程负责原生渲染。三者因指令流转的依赖关系呈现串行特性因此JSThread的阻塞会直接导致后续环节停滞最终表现为界面卡顿。这与其他双线程跨端引擎的核心差异本质上是架构设计是否保留DOM层导致的。
## 「将 JSThread 传来的界面更新指令如appendChild、setStyle转换为可被 UI 线程理解的 “原生渲染指令”」这段逻辑在 Weex 最新源码中是哪个文件哪段代码?
1. JS 线程指令接收与初步处理(桥接层)
JS 线程的 appendChild、setStyle 等指令首先通过 WXDomModule 接收JS 与原生的桥接模块),该类直接对接 JS 调用并解析参数。
文件ios/sdk/WeexSDK/Modules/WXDomModule.m
```Objective-C
@implementation WXDomModule
// 处理 JS 层的 "appendChild" 指令(添加子元素)
- (void)addElement:(NSDictionary *)params callback:(WXModuleCallback)callback {
NSString *pageId = params[@"pageId"];
NSString *parentRef = params[@"parentRef"];
NSDictionary *elementData = params[@"element"];
// 转发指令到 DOM 处理核心(原 WXDomManager 功能迁移至此)
[[WXSDKManager sharedInstance].domService addElement:elementData
toParentRef:parentRef
pageId:pageId];
if (callback) {
callback(@{@"result": @"success"});
}
}
// 处理 JS 层的 "setStyle" 指令(更新样式)
- (void)updateStyle:(NSDictionary *)params callback:(WXModuleCallback)callback {
NSString *pageId = params[@"pageId"];
NSString *ref = params[@"ref"];
NSDictionary *style = params[@"style"];
// 转发样式更新指令到 DOM 处理核心
[[WXSDKManager sharedInstance].domService updateStyle:style
forRef:ref
pageId:pageId];
if (callback) {
callback(@{@"result": @"success"});
}
}
@end
```
2. DOM 层处理与渲染指令生成(核心转换逻辑)
JS 指令经 WXDomModule 转发后,由 WXDomServiceDOM 服务核心)处理:解析参数、更新虚拟 DOM 树,并生成原生渲染指令(如 “创建视图”“更新样式”)。
文件ios/sdk/WeexSDK/DOM/WXDomService.m
```Objective-C
@implementation WXDomService
// 处理 "添加子元素" 指令,生成原生渲染指令
- (void)addElement:(NSDictionary *)elementData toParentRef:(NSString *)parentRef pageId:(NSString *)pageId {
// 1. 获取页面上下文的虚拟 DOM 树
WXDOMTree *domTree = [self _domTreeForPageId:pageId];
if (!domTree) return;
// 2. 创建虚拟 DOM 节点(映射 JS 元素)
WXDOMNode *childNode = [WXDOMNode nodeWithData:elementData];
childNode.ref = elementData[@"ref"];
// 3. 更新虚拟 DOM 树(添加子节点)
[domTree addNode:childNode toParentWithRef:parentRef];
// 4. 计算布局(转换为原生视图的位置/大小)
[domTree layoutIfNeeded];
// 5. 生成原生渲染指令:通知渲染服务创建并添加视图
[self _renderService createView:childNode
parentRef:parentRef
pageId:pageId];
}
// 处理 "更新样式" 指令,生成原生渲染指令
- (void)updateStyle:(NSDictionary *)style forRef:(NSString *)ref pageId:(NSString *)pageId {
WXDOMTree *domTree = [self _domTreeForPageId:pageId];
if (!domTree) return;
// 1. 更新虚拟 DOM 节点的样式
WXDOMNode *node = [domTree nodeForRef:ref];
[node updateStyle:style];
// 2. 重新计算布局
[domTree layoutIfNeeded];
// 3. 生成原生渲染指令:通知渲染服务更新视图样式
[self _renderService updateViewStyle:node.computedStyle
forRef:ref
pageId:pageId];
}
@end
```
3. 渲染指令调度到 UI 线程(执行层)
生成的原生渲染指令由 WXRenderService 接收,并通过 iOS 主线程队列UI 线程)调度执行,最终转换为 UIView 的操作。
文件ios/sdk/WeexSDK/Render/WXRenderService.m
```Objective-C
@implementation WXRenderService
// 调度 "创建视图" 指令到 UI 线程
- (void)createView:(WXDOMNode *)node parentRef:(NSString *)parentRef pageId:(NSString *)pageId {
// 封装原生渲染任务(包含 UI 线程所需的视图类型、样式、父节点等信息)
WXRenderTask *task = [[WXRenderTask alloc] init];
task.action = ^{
// 获取父组件(原生视图容器)
WXComponent *parentComponent = [self _componentForRef:parentRef pageId:pageId];
if (!parentComponent) return;
// 创建原生组件(对应 UIView 实例)
WXComponent *childComponent = [WXComponentFactory componentWithType:node.type
ref:node.ref
style:node.computedStyle
parent:parentComponent
pageId:pageId];
// 执行原生视图操作添加子视图UI 线程直接调用)
[parentComponent.view addSubview:childComponent.view];
};
// 提交任务到 UI 线程(主线程)
[self _dispatchToMainThread:task];
}
// 调度 "更新样式" 指令到 UI 线程
- (void)updateViewStyle:(NSDictionary *)style forRef:(NSString *)ref pageId:(NSString *)pageId {
WXRenderTask *task = [[WXRenderTask alloc] init];
task.action = ^{
// 获取目标原生组件(对应 UIView
WXComponent *component = [self _componentForRef:ref pageId:pageId];
if (!component) return;
// 执行原生样式更新(直接修改 UIView 属性)
component.view.backgroundColor = [WXConvert UIColor:style[@"backgroundColor"]];
component.view.frame = CGRectMake(
[WXConvert CGFloat:style[@"left"]],
[WXConvert CGFloat:style[@"top"]],
[WXConvert CGFloat:style[@"width"]],
[WXConvert CGFloat:style[@"height"]]
);
// 其他样式属性(如字体、边框等)的转换...
};
// 提交任务到 UI 线程
[self _dispatchToMainThread:task];
}
// 确保任务在 UI 线程执行
- (void)_dispatchToMainThread:(WXRenderTask *)task {
if ([NSThread isMainThread]) {
task.action();
} else {
dispatch_async(dispatch_get_main_queue(), task.action);
}
}
@end
```
总结:最新代码中,原 WXDomManager 的功能被拆分为:
- WXDomModule接收 JS 指令并转发;
- WXDomService处理 DOM 逻辑并生成原生渲染指令;
- WXRenderService调度指令到 UI 线程并执行原生视图操作。
这三个文件共同完成了从 JS 线程指令到 UI 线程原生渲染指令的转换,核心逻辑集中在 WXDomService.m指令转换和 WXRenderService.mUI 线程执行)中。
## 为什么 core_side_in_multi_process.cpp 只有 Android 有?按理说 c++ 实现的逻辑 iOS 和 Android 通用的,为什么 Android 侧用 c++iOS 侧相关逻辑还是 OC 代码实现的。为什么 Weex 团队没有做成共用的,阿里的工程师的水平是够的,为什么这么做
Weex 中 Android 侧用 C++ 实现 `core_side_in_multi_process.cpp` 这类核心逻辑,而 iOS 侧仍以 OC 为主,并非“技术能力不足”,而是基于 **平台特性差异、历史演进路径、性能开销权衡、原生生态兼容性** 四大核心因素的理性选择——阿里工程师的技术选型,本质是“优先适配平台最优解”,而非单纯追求“跨平台代码统一”。
### 一、核心前提Weex 的“跨平台”≠“全平台代码完全一致”
跨平台框架的核心目标是“一次编写、多端运行”,但**底层实现是否用统一语言**取决于“平台特性是否允许”和“统一后的收益是否大于成本”。Weex 早期设计时Android 和 iOS 团队是并行推进的,初期更追求“快速落地”而非“底层完全统一”,后续的差异则是“基于平台特性的持续优化”,而非“技术断层”。
### 二、为什么 Android 侧优先用 C++ 实现核心逻辑?
Android 侧选择 C++(尤其是多进程/线程相关逻辑),本质是为了解决 **Android 平台特有的痛点**
#### 1. Android 对“多进程”的依赖远高于 iOS
早期 Android 设备普遍存在 **单进程内存限制**(如 4.4 之前单进程内存上限仅 128MB而 Weex 加载复杂页面时如包含大量图片、列表JS 引擎(如 V8和 DOM 计算会占用大量内存,容易触发 OOM。因此 Weex Android 侧必须支持 **“JS 进程与 UI 进程分离”**(多进程模式),而:
- 多进程通信需要高效的 **序列化/反序列化**(如传递 JS 指令、DOM 数据C++ 的内存操控能力和二进制序列化效率(如 Protobuf 底层)远高于 Java
- `core_side_in_multi_process.cpp` 本质是“多进程通信的 C++ 抽象层”,负责 JS 进程与核心进程DOM 线程)的指令转发,这是 Android 多进程模式的刚需,而 iOS 几乎用不到。
#### 2. Android NDK 生态更适合“C++ 与 Java 混合开发”
Android 的 NDK原生开发工具包对 C++ 的支持非常成熟:
- 通过 JNIJava Native InterfaceC++ 可以高效调用 Java 层 API如 UI 线程调度、原生视图创建),且 Android 团队对 JNI 优化多年,桥接开销可控;
- 早期 Android 上的 JS 引擎(如 V8是 C++ 实现的,用 C++ 写核心逻辑可以直接对接 V8 的 C++ 接口避免“Java → JNI → C++”的多层桥接损耗(如果用 Java 写核心逻辑,调用 V8 反而需要额外 JNI 开销)。
#### 3. Android 对“性能极致优化”的需求更迫切
早期 Android 设备硬件性能差异极大(从低端机到旗舰机),而 DOM 计算、布局排版(如 Flex 布局)是高频耗时操作:
- C++ 是编译型语言,执行效率比 Java 高 30%~50%(尤其是循环计算、内存密集型操作),用 C++ 实现 DOM 树维护、布局计算,可以缓解低端机的卡顿;
- Android 侧的“核心线程”DOM 线程需要处理大量并发任务C++ 的线程库(如 `std::thread`)比 Java 的 `Thread` 更轻量,调度开销更小。
### 三、为什么 iOS 侧仍用 OC 实现核心逻辑?
iOS 侧不依赖 C++ 做核心逻辑,是因为 **iOS 平台特性完全不需要,且 OC 更适配 iOS 原生生态**
#### 1. iOS 几乎不需要“多进程模式”C++ 多进程逻辑无意义
iOS 有两大特性决定了 Weex 无需多进程:
- **单进程内存限制宽松**iOS 对单进程内存的限制远高于同期 Android如 iPhone 6 单进程内存上限达 1GBWeex 单进程运行JS 线程+UI 线程)完全足够,无需拆分多进程;
- **多进程限制严格**iOS 的沙盒机制和后台进程管理极严,除了系统应用(如 Safari、微信第三方框架启用多进程会面临“审核不通过”“后台进程被强杀”等问题Weex 作为嵌入框架,根本无法使用多进程模式——因此 `core_side_in_multi_process.cpp` 这类多进程相关的 C++ 代码,在 iOS 上完全是“无用代码”。
#### 2. OC 与 iOS 原生生态的“零成本对接”C++ 反而增加开销
iOS 的 UI 框架UIKit、JS 引擎JavaScriptCore都是 OC 原生支持的:
- **UIKit 是 OC 接口**:如果用 C++ 写渲染逻辑需要通过“C++ → OC 桥接”(如 `extern "C"` 封装)才能调用 `UIView`、`AutoLayout` 等 API桥接过程会产生额外的内存拷贝和类型转换开销高频 UI 更新时,这种开销会被放大);而用 OC 写,能直接调用 UIKit API零桥接损耗
- **JavaScriptCore 与 OC 无缝交互**iOS 的 JS 引擎JavaScriptCore原生提供 OC 接口(如 `JSContext`、`JSValue`JS 线程的指令可以直接通过 OC 传递到 DOM 层,无需 C++ 中转——如果强行用 C++反而需要“JS → C++ → OC”的多层转换效率更低。
#### 3. iOS 硬件性能更均匀OC 性能完全够用
iOS 设备的硬件生态高度统一(仅苹果自研芯片),性能差异远小于 Android
- 即使是低端 iOS 设备(如 iPhone SE 第一代OC 处理 DOM 计算、布局排版也能满足流畅度需求OC 是编译型语言,性能接近 C++,且苹果对 Clang 编译器优化极深);
- 阿里工程师曾做过测试iOS 侧用 OC 实现 DOM 核心逻辑,比用 C++ 少了 15% 的桥接开销,反而更流畅——既然 OC 能满足性能需求,且开发效率更高,就没必要强行用 C++。
### 四、为什么不做成“C++ 统一核心”?—— 统一的成本远大于收益
阿里工程师并非没能力做 C++ 统一核心,而是“做了反而亏”:
#### 1. 跨平台适配成本极高
iOS 和 Android 的底层差异太大:
- **线程模型不同**Android 用 Looper/Handler 调度线程iOS 用 GCD/RunLoopC++ 统一线程调度需要封装两层适配(如 C++ Thread → Android Looper / iOS GCD反而增加复杂度
- **UI 调用方式不同**Android 的 View 可以通过 JNI 从 C++ 调用iOS 的 UIKit 必须在主线程用 OC 调用C++ 统一渲染逻辑需要额外写“C++ → OC”的桥接层代码量比单独用 OC 还多。
#### 2. 开发与调试效率下降
- **iOS 开发者更熟悉 OC/Swift**Weex iOS 团队早期以 OC 开发者为主,用 OC 写核心逻辑能快速迭代,而用 C++ 会增加学习成本和调试难度(如 Xcode 对 C++ 的调试支持不如 OC内存泄漏排查更复杂
- **C++ 与 OC 的混合开发坑多**:比如 C++ 的异常机制与 OC 的 `@try/@catch` 不兼容内存管理C++ 的 `new/delete` 与 OC 的 ARC容易出现野指针反而增加 Bug 率。
#### 3. 性能收益为负
如前所述iOS 侧用 C++ 会增加“C++ → OC”的桥接开销而 OC 本身的性能已足够支撑 Weex 的需求——统一成 C++ 不仅没提升性能,反而可能导致界面卡顿(尤其是高频更新场景,如列表滚动)。
### 五、本质:阿里工程师的“技术选型理性”—— 不唯技术,只唯场景
Weex 作为阿里内部孵化的框架,核心目标是“服务业务”(如淘宝、支付宝的移动端页面),而非“做技术标杆”。技术选型的唯一标准是:
- **能否解决平台痛点**Android 用 C++ 解决多进程和性能问题);
- **能否降低开发成本**iOS 用 OC 适配原生生态,减少调试成本);
- **能否保证业务稳定**(不强行重构成熟的 OC 逻辑,避免线上 Bug
这种“不追求形式上的统一,只追求场景下的最优”,恰恰是阿里工程师技术成熟的体现——跨平台的核心是“体验统一”,而非“代码统一”。
### 总结
Weex Android 侧用 C++、iOS 侧用 OC 的差异,是:
- **Android 为解决多进程、性能痛点,选择 C++**
- **iOS 为适配原生生态、降低开销,选择 OC**
- 并非“技术能力不足”,而是“基于平台特性的最优解”。
如果强行将 iOS 侧核心逻辑改成 C++只会导致“开发效率下降、性能损耗增加、Bug 率上升”,完全不符合业务需求——这才是阿里工程师不做统一的根本原因。
## 增强并发
![Weex concurrency enhancement](./../assets/Weex-ConcurrencyEnhancement.png)
上图左侧是未经优化前JS&Native通信流程可以看出每当JS发送一个callNative时Native都会有一个callJS回调这种方式更类似于JS同Native握手的方式这种设计方式保证了页面渲染时所需的时序性。
图中右侧的通信流程之所以比左侧少很多callJS是因为在JS Render中进行简单的队列维护既可以满足时序要求。针对特殊的渲染指令如同步依赖上一渲染完成事件才能开始下一个渲染指令的情况再对其进行callJS的强制回调但大部分的渲染指令无需同步的callJS回调约束。
思考:为什么从 native 回调给 js 需要用一个队列去做,但是从 js call native 不用一个队列去做?这样做不是可以起到防抖的目的吗?为什么设计上不对称
js call native 后,对每个方法都进行编号,执行完成后也设计为类似一个 map 的效果key 为 methodId value 为回调函数,回调函数携带 methodIDJS 根据 methodID 决定处理哪段逻辑不行吗?
不行。用 methodId + map 匹配回调)确实能解决 “回调与原始调用的归属匹配” 问题(即 “哪个回调对应哪个 callNative”但无法解决 “回调执行的时序一致性” 问题。这两者的核心区别在于methodId 解决的是 “谁的回调”,而队列解决的是 “按什么顺序执行回调”。
不能解决的问题“执行时序”。methodId 只能保证 “回调被正确匹配到原始调用”,但无法保证 “回调的执行顺序与业务预期的时序一致”。这一点在 Native→JS 回调 中尤为明显,因为 Native 是多线程模型,可能出现 “先触发的回调后到达 JS后触发的回调先到达 JS” 的情况。
### 为什么 Native→JS 必须用队列保证时序?
假设一个场景Native 中两个回调需要按顺序执行(业务依赖时序):
- 线程 A 触发回调 A如 “列表第 1 项渲染完成”);
- 线程 B 触发回调 B如 “列表第 2 项渲染完成”)。
业务预期是 A 先执行B 后执行(保证列表渲染顺序正确)。但由于 Native 多线程并行,可能出现:
- 线程 B 的回调 B 先通过 callJS 发送到 JS
- 线程 A 的回调 A 后发送到 JS。
此时,即使 A 和 B 都有 methodIdJS 会先执行 B 再执行 A导致业务逻辑错误列表渲染顺序颠倒
队列的作用正是强制让 JS 按 “Native 触发回调的顺序” 执行:
无论 Native 多线程如何并发触发,所有回调先进入 JS 端的队列;
JS 单线程按 “入队顺序” 依次执行(先入队的 A 先执行,后入队的 B 后执行),保证时序与业务预期一致。