從 Vue 組件庫(Vue Lib)到 Weex 渲染為 iOS 原生 UIKit 元素,核心是 “Virtual DOM → 跨線程通信 → Native DOM 構建 → 佈局計算 → 原生 View 渲染 → 事件反向綁定” 的完整鏈路。Weex 作為中間層,主要完成 7 大核心工作,接下去看看詳細內容
一、核心步驟
從 Vue Lib 到 Weex 原生 UIKit 元素的完整流程。
首先,用户的需求是 “列舉 Weex 做的事情” 並 “結合最新源碼標位置”,所以思考過程要先拆解流程:從 Vue 編譯產物處理、JS-Native 通信、Virtual DOM 解析、DOM 構建與佈局計算、渲染指令生成、原生 View 創建與更新,這幾個核心步驟。每個步驟都要對應 Weex 的具體工作,還要找到最新源碼中的對應類和方法。
第一步,應該是 Vue 編譯產物的適配。因為 Vue 寫的代碼要被 Weex 識別,Weex 需要處理 Vue 的模板編譯結果(比如 render 函數生成的 Virtual DOM),還要注入 Weex 的 JS Runtime API。這裏要找最新源碼中 JS Runtime 的初始化類,比如 WXJSContext 或 WXBridgeCore,因為 JS 環境的搭建是第一步。
第二步,JS-Native 通信的建立。Vue 的 Virtual DOM 變化需要傳遞給 Native,所以 Weex 的 Bridge 核心要註冊通信方法(比如 callNative),這裏對應 WXBridgeCore 的 invokeNative 方法,之前討論過 bridge 隊列,這裏要結合最新源碼中通信任務的調度。
第三步,Virtual DOM 的解析與 DOM 樹構建。Weex 需要把 Vue 的 Virtual DOM(JSON 結構)解析成 Native 側的 DOM 節點(WXDOMNode),這部分現在在 WXDOMCore 中,比如 createNode 方法,處理節點創建、屬性更新,還要維護 DOM 樹結構。
第四步,樣式解析與 Yoga 佈局計算。Vue 的樣式(比如 flex)要被 Weex 解析,轉換為 Yoga 的佈局屬性,然後在 dom 線程計算佈局,這部分在 WXDOMCore 的_scheduleLayout 方法,調用 Yoga 的 calculateLayout。
第五步,渲染指令生成與傳遞。DOM 和佈局計算完成後,要把結果封裝成渲染指令(節點 ID、frame、類型等),通過主線程傳遞給渲染核心,這裏是 WXDOMCore 的_notifyRender 方法,切換到 main queue。
第六步,原生 UIKit 元素的創建與更新。渲染核心(WXRenderCore)接收指令,創建對應 UIKit 控件(UILabel、UIImageView 等),或複用已有 View,更新 frame 和樣式,這部分在 WXRenderCore 的 render 方法,還有具體的 View 工廠類(比如 WXViewFactory)。
第七步,事件綁定與反向通信。原生 UI 的事件(比如點擊)要傳遞迴 Vue,Weex 需要註冊 Native 事件,通過 Bridge 回調 JS,這裏對應 WXEventDispatcher 和 WXBridgeCore 的 callbackJS 方法。
具體的邏輯不做深入探討,可以查看源碼研究。
二、Yoga
1. 支持"增量佈局更新"
當組件樣式變化時,Yoga 僅重新計算受影響的組件樹分支,而非全量重算,大幅減少 RN 應用的佈局耗時和卡頓』Yoga 是如何實現僅計算受影響的組件樹分支的?類似有個打標記,標記為 dirty?
Yoga 實現增量佈局的核心就是 「Dirty 標記機制」+「組件樹依賴傳播」—— 通過標記 “受影響的節點”,並僅處理這些節點及其關聯分支,避免全量重算。
1. YogaNode 與 Dirty 狀態標識
Yoga 中每個組件對應一個 YogaNode(佈局計算的最小單元),每個節點都包含 3 個關鍵狀態標記(用於判斷是否需要重算):
-
dirtyFlags(核心標記):記錄節點的 “髒狀態類型”,主要分兩類:
- LAYOUT_DIRTY:節點自身樣式(如 width、flex)或子節點佈局變化,需要重新計算自身佈局;
- MEASURE_DIRTY:節點的測量相關屬性(如 measureFunction 自定義測量邏輯)變化,需要先重新測量尺寸,再計算佈局。
- isLayoutClean:布爾值,快速判斷節點是否 “乾淨”(無髒狀態),避免重複檢查 dirtyFlags;
- childCount + children 指針:維護子節點列表,用於後續遍歷依賴分支。
2. 髒狀態觸發與傳播:從 “變化節點” 到 “根節點” 的冒泡
當組件樣式變化時(如 RN 中修改 style={{ flex: 2 }}),Yoga 會觸發以下流程:
- 步驟 1:標記自身為 Dirty
直接修改變化節點的 dirtyFlags |= LAYOUT_DIRTY(或 MEASURE_DIRTY),同時設置 isLayoutClean = false。 - 步驟 2:向上冒泡通知父節點
由於父節點的佈局(如尺寸、位置)依賴子節點的佈局結果(比如父節點是 flex:1,子節點尺寸變化會影響父節點的剩餘空間分配),因此會遞歸向上遍歷父節點,直到根節點,將所有 “依賴節點” 都標記為 LAYOUT_DIRTY。
關鍵優化:父節點僅標記 “需要重算”,但不會立即計算,避免中途重複觸發計算。 - 步驟 3:跳過已標記的節點
若某個節點已被標記為 Dirty,後續重複觸發時會直接跳過(避免重複冒泡),提升效率。
3. 佈局計算階段:只處理 Dirty 分支,跳過乾淨節點(DFS)
當 Yoga 觸發佈局計算(如 RN 渲染幀觸發、組件掛載完成)時,會從根節點開始遍歷組件樹,但僅處理 “Dirty 節點及其子樹”:
- 步驟 1:根節點判斷狀態
若根節點是乾淨的(isLayoutClean = true),直接終止計算(全量跳過);若為 Dirty,進入分支處理。 - 步驟 2:遞歸處理 Dirty 分支
對每個節點,先檢查自身狀態: - 若干淨:直接複用上次緩存的佈局結果(x/y/width/height),不重算;
-
若 Dirty:
- 先處理子節點:如果子節點是 Dirty,先遞歸計算子節點佈局(保證父節點計算時依賴的子節點數據是最新的);
- 再計算自身佈局:根據 Flex 規則(如 flexDirection、justifyContent)和子節點佈局結果,計算自身的最終尺寸和位置;
- 清除 Dirty 標記:計算完成後,設置 dirtyFlags = 0、isLayoutClean = true,標記為乾淨。
- 步驟 3:增量更新的核心效果
比如修改一個列表項的 margin,只會標記該列表項 → 父列表容器 → 根節點為 Dirty,其他列表項、頁面其他組件均為乾淨,會直接跳過計算,僅重算 “列表項→父容器” 這一小分支。
2. Flex 佈局邏輯如何到 Native 系統
Flex 佈局邏輯,或者説 DSL,是如何翻譯為 iOS 的 AutoLayout 和 Android 的 LayoutParams 的?
Yoga 先將 Flex DSL 解析為統一的「佈局計算結果」(節點的 x/y/width/height、間距、對齊方式等),再根據平台差異,將計算結果 “映射” 為對應平台的原生布局規則——iOS 映射為 AutoLayout 約束,Android 映射為 LayoutParams + 原生布局容器屬性。
1. 第一步:通用前置流程(跨平台統一)
無論 iOS 還是 Android,Yoga 都會先完成以下步驟,屏蔽 Flex DSL 的解析差異:
- 解析 Flex 樣式:將上層框架的 Flex 配置(如 RN 的 StyleSheet、Weex 的模板樣式)解析為 YogaNode 的屬性(如 flexDirection、justifyContent、margin、padding 等);
- 執行佈局計算:通過 Flexbox 算法(基於 Web 標準),計算出每個 YogaNode 的最終佈局數據:
- 固定屬性:width/height(含 auto/flex 計算後的具體數值)、x/y(相對父節點的座標);
- 間距屬性:marginLeft/Top/Right/Bottom、paddingLeft/Top/Right/Bottom;
- 對齊屬性:alignItems、justifyContent 對應的節點相對位置關係;
- 輸出標準化佈局數據:將上述結果封裝為平台無關的結構體,供後續平台映射使用。
2. 第二步:iOS 端:映射為 AutoLayout 約束(NSLayoutConstraint)
AutoLayout 的核心是「基於約束的關係描述」(而非直接設置座標),因此 Yoga 會將 “計算出的具體尺寸 / 位置” 轉化為 UIView 的約束(NSLayoutConstraint),核心映射規則如下:一一翻譯 css 規則到 iOS AutoLayout 寫法:
| Flex 核心屬性 | 對應的 AutoLayout 約束邏輯 |
|---|---|
width: 100 |
映射為 view.widthAnchor.constraint(equalToConstant: 100) |
height: auto |
先通過 Yoga 計算出具體高度(如文字高度、子節點包裹高度),再映射為 heightAnchor 約束;若為 flex:1,則映射為 heightAnchor.constraint(equalTo: superview.heightAnchor, multiplier: 1)(佔滿父容器剩餘高度) |
marginLeft: 20 |
映射為 view.leadingAnchor.constraint(equalTo: superview.leadingAnchor, constant: 20) |
marginTop: 15 |
映射為 view.topAnchor.constraint(equalTo: superview.topAnchor, constant: 15) |
justifyContent: center(父節點 flexDirection: row) |
父節點約束:view.centerXAnchor.constraint(equalTo: superview.centerXAnchor);若有多個子節點,通過調整子節點間的 spacing 約束實現均勻分佈 |
alignItems: center(父節點 flexDirection: column) |
子節點約束:view.centerYAnchor.constraint(equalTo: superview.centerYAnchor) |
flex: 1(子節點) |
映射為 view.widthAnchor.constraint(equalTo: superview.widthAnchor, multiplier: 1)(橫向佔滿)+ 父節點的 distribution 約束(分配剩餘空間) |
補充信息:
- Yoga 會為每個
UIView關聯一個YogaNode,佈局計算完成後,通過YogaKit(或上層框架如 RN 的原生層)自動生成約束; - 支持 “約束優先級” 適配:比如
flex:1對應的約束優先級會高於固定尺寸約束,確保 Flex 規則優先生效; - 混合佈局兼容:若原生視圖已有部分 AutoLayout 約束,Yoga 會生成 “補充約束”,避免衝突(通過
active屬性控制約束啓用 / 禁用)。
三、Weex 剖析
下面針對核心機制詳解與源碼定位
1. 編譯階段:從 Vue 到 Virtual DOM
- 處理 Vue 單文件:開發者的
.vue文件通過 Webpack 和weex-loader編譯成 JavaScript Bundle。這個 Bundle 包含了渲染頁面所需的所有信息 - 生成Virtual DOM:在JS運行時,Vue.js(或 Rax)的渲染函數會生成一棵 Virtual DOM樹(VNode)。Weex 的 JS Framework 會攔截常規的 DOM 操作,將其導向 Weex 的渲染管道
源碼相關:編譯過程主要涉及 weex-loader (在 weex-toolkit 項目中),而 JS Framework 對 VNode 的處理在 js-framework 目錄下。重點關注 src/framework.js 中的 Document 和 Element 類,它們模擬了 DOM 結構
2. 指令生成與通信
-
序列化為渲染指令(json 數據):JS-Framework 不會直接操作 Dom,而是把對 Dom 的操作,描述成對 VNode 對象的創建、更新、刪除等,序列化成一種特殊的 JSON 格式的渲染指令。比如
{ "module": "dom", "method": "createBody", "args": [{"ref": "1", "type": "div", "style": {...}}] } - JS-Native 橋接:這些指令通過 callNative 方法,從 JS 端發送到 Native 端,同時 Native 端也可以通過 callJS 方法向 JS 端發送事件(比如用户點擊)
3. 原生端渲染
- 指令解析與組件渲染:Native 端的渲染引擎(如 Android 的 WXRenderManger 和 iOS 的 WXComponentManager)接收並解析 JS 指令。Weex 維護了一個從 JS 組件到原生 UI 組件的映射表。(例如 <text> 映射到 iOS 的 UILabel)
- 佈局與樣式:Weex 使用的 Flexbox 佈局模型做為統一的佈局方案,Native 端需要將 JS 傳遞的 css 樣式屬性,轉換為原生組件能夠理解的佈局參數與樣式屬性。
- 多線程模型:為了保證 UI 流暢,Weex 採用了多線程模型。DOM 操作和佈局計算通常在單獨的 DOM 線程進行,而最終創建和更新原生視圖的操作必須在 UI 主線程上進行
4. 拓展機制
- 模塊(Module):用於暴露原生能力(如網絡、存儲)給前端調用,通過 callNative 觸發,支持回調
- 組件(Component):拓展自定義 UI 組件,允許開發者創建自定義的原生 UI 組件,並在 JSX 中使用
- 適配器(Adapter):提供可替換的實現,如圖片下載器
四、為什麼自定義 Component 都需要繼承自 WXComponent?
比如下面的代碼
[self registerComponent:@"image" withClass:NSClassFromString(@"WXImageComponent") withProperties:nil];
@interface WXImageComponent : WXComponent
@end
答:自定義原生組件必須繼承自 WXComponent,本質是複用 Weex 封裝的「JS - 原生交互、生命週期、樣式佈局、渲染基礎」等通用能力,確保組件能接入 Weex 運行時生態。
Weex Module 與 Componet 的區別
| 類型 | 核心作用 | 基類 | 示例 |
|---|---|---|---|
| Component | 原生 UI 渲染(有視圖) | WXComponent |
WXImageComponent(圖片)、WXTextComponent(文本)、自定義按鈕組件 |
| Module | 功能擴展(無視圖) | WXModule |
WXNavigatorModule(導航)、WXStorageModule(存儲)、自定義工具模塊 |
實現 JS 與原生組件的「數據同步」(屬性、事件、方法)
Weex 的核心是「JS 控制原生組件」,而 WXComponent 封裝了 JS 與原生之間的通信協議,無需自定義組件手動處理:
-
屬性同步(Props):JS 端通過
<my-component prop1="xxx" prop2="yyy">傳遞的屬性,WXComponent 會自動解析、類型轉換(如 JS 字符串 → 原生 NSString/NSNumber),並通過setter方法同步到自定義組件。示例:WXImageComponent 繼承
WXComponent後,只需重寫-setSrc:(NSString*)src方法,就能接收 JS 傳的src屬性,無需關心「JS 如何把值傳給原生」。 -
事件分發(Events):原生組件的交互事件(如點擊、加載完成),
WXComponent會按照 Weex 協議回傳給 JS 端(如@emit('click'))示例:自定義按鈕組件繼承後,只需調用
[self fireEvent:@"click" params:@{@"x": @100, @"y": @200}],JS 端就能通過@onclick接收事件,無需自己實現事件通信。 -
方法調用(Methods):JS 端通過
this.$refs.myComponent.callMethod('xxx', params)調用原生組件方法,WXComponent會解析方法名和參數,反射調用自定義組件的對應方法。
示例:自定義播放器組件繼承後,只需暴露
-play方法,JS 就能直接調用,WXComponent負責方法查找和參數傳遞。
五、JS 數據變化是如何驅動 Native UI 更新的
純 Web 端的數據變化會通過 Proxy 去驅動關聯的 UI 更新,這也是 Vue3 的工作原理,那麼 JS 端的數據變化是如何驅動 Native UI 組件的更新的?
所有的 Native UI Component 都繼承自 WXComponent,所以可以直接給 WXComponent 添加一個實現 DataBinding 的 Category,這就是 Weex 最新源碼中的 WXComponent+DataBinding.mm
核心是:解析 JS 端傳遞的「綁定表達式」(如 {{a + b}}),編譯為原生可執行的回調 Block,當 JS 數據變化時,通過 Block 計算出組件所需的新值,自動更新組件的屬性、樣式、事件,或處理列表(v-for)、條件(v-if)、一次性綁定(v-once)等邏輯
可能有些人要問了:為什麼當 js 數據變化時,需要讓 Native 計算組件所需的新值?這不就是 Native 做了一遍 Vue 響應式的邏輯嗎?這種重複邏輯的價值是什麼?
Vue3 的 Proxy 只負責「JS 端數據變化的監聽 + 依賴收集 + 觸發更新通知」—— 它是 “響應式的觸發器”,而非 “UI 更新的執行者”
而 Weex 之所以需要 Native 託管,核心是因為「繼承自 WXComponent 的 UI 組件是 Native 側的原生組件,而非 DOM 組件」,JS 端沒有任何能力(API)去訪問、操作他們,Proxy 再強大,它也只是 Native 側(Weex)和 Web 端(Vue)負責“喊一聲,哎,數據變了,你們誰需要的自助,自己去處理感興趣的 UI”,卻摸不到 UI 組件,Web 端由 DOM API 去渲染繪製,Native 端更觸碰不到,必須由 Native 自己來完成:聽到通知 -> 計算新值 -> 更新控件的流程。
1. Proxy 都做了些什麼?
Vue3 的核心實現裏 Proxy 做了3件事:全程在 JS 側,不涉及任何 UI 操作
監聽數據操作:通過 Proxy 代理對象攔截數據的 getter、setter
- 通過 getter 收集依賴關係:當組件渲染時觸發 getter,Proxy 會記錄這個組件依賴了這個數據
- 通過 setter 觸發更新通知:當數據被修改時觸發 setter,Proxy 會告訴 Vue 運行時,“user.name” 變了,所有依賴它的組件該更新了
Proxy(代理)是 ES6 新增的內置對象,用於創建一個對象的代理副本,並通過「陷阱(Trap)」攔截對原對象的基本操作(如屬性訪問、賦值、刪除等),從而自定義這些操作的行為。
const proxy = new Proxy(target, handler);
target:被代理的原始對象(可以是對象、數組,甚至函數);handler:配置對象,包含多個「陷阱方法」(如get、set),用於定義攔截邏輯;proxy:代理對象,後續對原始對象的操作需通過代理對象進行,才能觸發攔截。
| 陷阱方法 | 作用 | 觸發場景 |
|---|---|---|
get(target, key, receiver) |
攔截「屬性訪問」 | proxy.key 或 proxy[key] |
set(target, key, value, receiver) |
攔截「屬性賦值」 | proxy.key = value 或 proxy[key] = value |
deleteProperty(target, key) |
攔截「屬性刪除」 | delete proxy.key |
has(target, key) |
攔截「in 運算符判斷」 |
key in proxy |
Tips: Proxy 代理的是「整個對象」,而非單個屬性,且攔截的是「操作行為」(如 “訪問屬性” 這個動作),而非屬性本身。
Vue 核心流程:創建代理 → 依賴收集 → 數據修改 → 觸發更新。
1. 創建代理(reactive 函數的核心)
reactive 函數接收一個原始對象,返回其 Proxy 代理對象,同時配置 get、set 等陷阱方法,為後續依賴收集和更新做準備
function reactive(target) {
return new Proxy(target, {
// 攔截屬性訪問
get(target, key, receiver) {
// 1. 先獲取原始屬性值
const value = Reflect.get(target, key, receiver);
// 2. 收集依賴(關鍵:記錄“誰在訪問這個屬性”)
track(target, key);
// 3. 若訪問的是嵌套對象,遞歸創建代理(懶代理,優化性能)
if (typeof value === 'object' && value !== null) {
return reactive(value);
}
return value;
},
// 攔截屬性賦值
set(target, key, value, receiver) {
// 1. 先設置原始屬性值
const oldValue = Reflect.get(target, key, receiver);
const success = Reflect.set(target, key, value, receiver);
// 2. 若值發生變化,觸發依賴更新
if (success && oldValue !== value) {
trigger(target, key);
}
return success;
},
// 攔截屬性刪除
deleteProperty(target, key) {
const success = Reflect.deleteProperty(target, key);
if (success) {
trigger(target, key); // 刪除屬性也觸發更新
}
return success;
}
});
}
- 用
Reflect操作原始對象,Reflect 是 ES6 新增的內置對象,提供了與 Proxy 陷阱對應的方法,比如Relect.get、Reflect.set確保操作原始對象的行為一直,同時避免直接操作 target 所產生的問題 - 嵌套對象懶代理:Proxy 僅代理當前層級對象,當訪問嵌套對象 (proxy.user.name)時,才遞歸對 user 對象創建代理,避免初始化時遞歸遍歷所有屬性,優化性能
2. 依賴收集
Vue3 用「三層映射」存儲依賴,確保精準定位
// WeakMap:key 是被代理的原始對象(target),value 是該對象的屬性-依賴映射
const targetMap = new WeakMap();
function track(target, key) {
// 1. 若沒有當前目標對象的映射,創建一個(Map:key 是屬性名,value 是依賴集合)
if (!targetMap.has(target)) {
targetMap.set(target, new Map());
}
const depsMap = targetMap.get(target);
// 2. 若沒有當前屬性的依賴集合,創建一個(Set:存儲依賴函數,去重)
if (!depsMap.has(key)) {
depsMap.set(key, new Set());
}
const deps = depsMap.get(key);
// 3. 將當前活躍的依賴函數(effect)添加到集合中
if (activeEffect) {
deps.add(activeEffect);
}
}
會產生一個這樣的結構
{
""
}
3. 數據修改(觸發 set/deleteProperty 的陷阱)
當通過代理對象修改屬性(如 proxy.name = 'newName')或刪除屬性(如 delete proxy.age)時,會觸發對應的 Proxy 陷阱(set 或 deleteProperty)。
陷阱函數會先更新原始對象的屬性值,再判斷值是否真的發生變化(避免無效更新)
4. 觸發更新 (tigger 函數)
function trigger(target, key) {
// 1. 從 targetMap 中獲取當前對象的屬性-依賴映射
const depsMap = targetMap.get(target);
if (!depsMap) return;
// 2. 獲取當前屬性的所有依賴
const deps = depsMap.get(key);
if (!deps) return;
// 3. 執行所有依賴函數(觸發更新)
deps.forEach(effect => effect());
}
2. Proxy 不做的事情
- 不計算表達式(比如 user.name + "後綴"的結果,Proxy 不管)
- 不操作 UI(不管是 DOM 和 Native 控件,Proxy 都不碰)
- 不跨端通信
為什麼 Native 組件不能讓 Proxy “解決”?
核心矛盾:渲染載體不同。Proxy 之所以在 Web 端能 “間接驅動 UI”,是因為 Web 端有個「中間橋樑」—— DOM,且 JS 端有完整的 DOM API(比如 document.getElementById、element.style.setProperty):
Web 端完整鏈路:Proxy 觸發更新 → Vue 運行時計算表達式 → 虛擬 DOM diff → 調用 DOM API 操作 DOM → UI 更新
- JS 端沒有操作 Native 控件的 API:瀏覽器給 JS 暴露了 DOM API,但 iOS/Android 系統不會給 JS 引擎暴露 “修改
UILabel文本”“設置UIImageView圖片” 的 API —— JS 端連 Native 控件的 “引用” 都拿不到,更別説更新了; - Native 控件不在 JS 運行時的內存空間:JS 引擎(如 V8、JSC)和 Native 應用是兩個獨立的 “進程 / 虛擬機”,內存不共享 —— Proxy 所在的 JS 內存裏,根本沒有 Native 控件的實例,想操作都無從下手
Weex 的設計優雅之處在於:Native 託管“執行層”,Proxy 保留“觸發層”。響應式工作繼續複用現有邏輯,由 Proxy 完成,最後的執行層由 Native 實現,也就是 WXComponent+DataBinding
- 響應式系統(Proxy)的核心是 “發現變化”:不管是 Web 還是 Weex,Proxy 都只幹這件事;
- UI 更新的核心是 “操作渲染載體”:Web 端操作 DOM(JS 端能做),Weex 端操作 Native 控件(只能 Native 端做);
- WXComponent+DataBinding 的角色是 “Native 端的 UI 執行器”:它不是替代 Proxy,而是 Proxy 觸發更新後,負責把 “更新通知” 落地到 Native 控件上的唯一途徑
六、Weex 自定義組件是如何工作的
上面分析了自定義組件的數據變化和表達式運算是 Native 負責的,執行層也就是 WXComponent+DataBinding.mm 這個類。
一言以蔽之就是:把 JS 端傳遞的“原始數據”,通過預編譯的綁定規則(Block)計算出 Native 組件需要的最終值,並自動更新 UI 組件,同時適配長列表組件等複雜場景的 UI 優化。
該分類為所有繼承自 WXComponent 的組件,注入“數據綁定能力”,無需手動實現。
1. 綁定規則的“編譯存儲”,把 JS 表達式轉換為 Native 可執行的 block
數據綁定的「前置準備」:在組件初始化時,解析 JS 端傳遞的綁定規則(如 [[user.name]]、[[repeat]]),編譯為 Native 可執行的 WXDataBindingBlock(代碼塊),並存儲到組件的綁定映射表中(_bindingProps/_bindingStyles/_bindingEvents 等)
- (void)_storeBindingsWithProps:(NSDictionary *)props styles:(NSDictionary *)styles attributes:(NSDictionary *)attributes events:(NSDictionary *)events;
接收組件的 props/attrbutes/styles/events 中的綁定規則,解析並存儲為可執行的 block。
- 識別綁定表達式:判斷是否包含
WXBindingIdentify(@"@binding")標記,比如{"src": {"@binding": "user.name"}}; - AST 解析:通過
WXJSASTParser把綁定表達式字符串(如"user.name + '後綴'")解析為 AST 節點(WXJSExpression); - 生成執行 Block:調用
-bindingBlockWithExpression:把 AST 節點轉成WXDataBindingBlock(後續數據變化時直接執行該 Block 計算結果); -
分類存儲:按綁定類型(屬性 / 樣式 / 事件 / 特殊綁定)存入對應的映射表:
_bindingProps:屬性綁定(如src);_bindingStyles:樣式綁定(如fontSize);_bindingEvents:事件綁定(如onClick參數);- 特殊綁定:
_bindingRepeat([[repeat]]對應v-for)、_bindingMatch([[match]]對應v-if)、_dataBindOnce([[once]]對應v-once)。
2. WXComponentManager 都做了什麼
WXComponentManager 是 Weex iOS 端的 組件全生命週期與任務調度核心,所有與 Native 組件相關的操作(創建、更新、佈局、銷燬、事件綁定)都由它統一管理,同時承擔「線程分工協調、UI 任務批量處理、性能監控」等關鍵職責,是連接 JS 指令、Native 組件、佈局引擎和 UI 渲染的 “中樞大腦”。
1. 組件線程管理
組件業務的 “專屬執行環境”,作為組件線程的「創建者和維護者」,WXComponentManager 確保所有組件核心操作都在全局唯一的組件線程中執行,避免線程安全問題和主線程阻塞。
核心工作:
- 懶加載創建全局組件線程(
+componentThread),啓動 RunLoop 確保線程常駐(_runLoopThread) - 提供線程調度接口:
WXPerformBlockOnComponentThread(異步)、WXPerformBlockSyncOnComponentThread(同步),讓外部模塊(如WXBridgeManager)能將組件任務提交到組件線程 - 線程斷言約束:所有組件核心方法(如
createBody、updateStyles)開頭都有WXAssertComponentThread,強制組件操作在組件線程執
2. 組件樹構建與管理:組件的 “增刪改查” 全生命週期
核心工作:
-
創建組件
- 根組件創建(
createBody:):接收 JS 端根組件指令,創建頁面根組件(如<div>根節點),綁定到頁面根視圖; - 子組件創建(
addComponent:type:parentRef:):根據 JS 端指令,創建子組件並關聯父組件,存入_indexDict(組件 ref → 實例映射,快速查找)。
- 根組件創建(
-
更新組件關係
- 移動組件(
moveComponent:toSuper:atIndex:):調整組件在組件樹中的位置,同步更新視圖層級; - 刪除組件(
removeComponent:):從組件樹和索引字典中移除組件,遞歸刪除子組件,釋放視圖資源。
- 移動組件(
-
組件查詢與遍歷
- 按 ref 查找組件(
componentForRef:):供 JS 端this.$refs訪問原生組件實例; - 遍歷組件樹(
enumerateComponentsUsingBlock:):支持遞歸遍歷所有組件(如性能統計、全局樣式更新)
- 按 ref 查找組件(
3. 數據綁定輔助:綁定規則的提取與存儲
配合 WXComponent+DataBinding 模塊,WXComponentManager 在組件創建時,從 JS 端傳遞的 props/styles/attributes 中提取「綁定表達式配置」,為響應式更新鋪路。核心工作:
-
提取綁定規則:
_extractBindings::從樣式 / 屬性中提取[[repeat]]/{"@binding": "expr"}等綁定配置,移除原始字典中的綁定字段(避免干擾普通屬性處理)_extractBindingEvents::從事件數組中提取綁定參數(如onClick的回調錶達式);_extractBindingProps::提取組件自定義 props 綁定(@componentProps)。
- 存儲綁定規則:調用組件的
_storeBindingsWithProps:styles:attributes:events:,將提取的綁定配置存入組件實例,後續數據變化時觸發表達式計算。
4. 組件更新調度:樣式 / 屬性 / 事件的 “同步與執行”
當 JS 端觸發組件更新(如修改樣式、屬性、綁定事件)時,WXComponentManager 負責「跨線程調度、數據預處理、UI 同步」,確保更新流程高效且安全。
-
樣式更新(
updateStyles:forComponent:)- 組件線程:過濾無效樣式(如空值),更新組件實例的樣式數據,觸發佈局計算;
- 主線程:通過
_addUITask將樣式更新任務(如設置CALayer.backgroundColor、UILabel.font)批量調度到主線程執行。
- 屬性更新(
updateAttributes:forComponent:):類似樣式更新,組件線程處理數據邏輯,主線程更新原生組件屬性(如UIImageView.image、UIScrollView.contentOffset)。 -
事件綁定 / 解綁
- 組件線程:維護組件的事件列表(如
click/scroll); - 主線程:綁定 / 移除原生手勢識別器(如
UITapGestureRecognizer),捕獲用户交互。
- 組件線程:維護組件的事件列表(如
- 批量更新優化:通過
performBatchBegin/performBatchEnd標記批量更新範圍,合併多個 UI 任務,減少主線程調度次數(提升性能)。
5. 佈局調度與 UI 同步:從佈局計算到 UI 渲染
Weex 採用 Flex 佈局引擎(Yoga),WXComponentManager 負責佈局計算的觸發、組件 frame 分配、UI 任務批量執行,確保組件按預期位置渲染。
- 觸發佈局計算:組件更新、根視圖尺寸變化(
rootViewFrameDidChange:)時,調用_layoutAndSyncUI觸發WXCoreBridge執行 Yoga 佈局計算,得到所有組件的 frame。 - 分配組件 frame:
layoutComponent:frame:isRTL:innerMainSize:將計算後的 frame 分配給組件,若為根組件,同步更新頁面根視圖尺寸(適配wrap_content模式)。 - UI 任務同步:
_syncUITasks批量執行_uiTaskQueue中的 UI 任務(如addSubview、setFrame),異步調度到主線程,避免頻繁主線程切換導致掉幀。 - 幀率同步:通過
WXDisplayLinkManager監聽屏幕刷新率(60fps),確保佈局更新與幀率同步,提升渲染流暢度。
6. 生命週期與資源釋放:頁面卸載時的 “清理工作”
當 Weex 頁面銷燬(WXSDKInstance 卸載)時,WXComponentManager 負責清理組件資源,避免內存泄漏。
核心工作(unload 方法):
- 停止佈局調度:調用
_stopDisplayLink,停止幀率監聽和佈局計算; - 解綁渲染資源:遍歷所有組件,解除與底層渲染對象(
RenderObject)的綁定; - 釋放 UI 資源:調度到主線程,銷燬所有組件的原生視圖(
_unloadViewWithReusing:); - 清空狀態:清空
_indexDict、_uiTaskQueue、_fixedComponents等容器,解除與WXSDKInstance的綁定。 - 清除事件綁定:清除所有的事件、手勢等邏輯
七、WXModule 的註冊機制及其調用流程
1. WXModule 的註冊分為 Naitve 註冊和 JS 註冊
- Native 註冊:在 Native 端,調用
[WXSDKEngine registerModule:withClass:]方法(在 iOS 中) ,這個過程會將自定義 Module 的類和一個模塊名稱(例如TestModule)建立映射關係,並生成一個ModuleFactory存儲在一個全局的 Map(例如sModuleFactoryMap)中。同時,如果該 Module 被標記為全局(global),SDK 會立即創建一個實例並緩存起來。 - JS 註冊:Native 註冊完成後,Weex 會將所有已註冊 Module 的模塊名稱及其暴露給 JS 的方法名列表,通過
WXBridge(JS-Native 通信橋樑)傳遞給 JS 引擎。這樣,JS 端就知道存在哪些模塊以及每個模塊有哪些方法可以調用。
2. 當 JS 調用 Module 方法時
- JS 發起調用:在 JS 代碼中,通過
weex.requireModule('moduleName')獲取模塊實例 。然後弔影其方法,比如 'staream.fetch()options, callack)' - Bridge 橋接:JS 引擎通過 JSBridge 將這次調用(包括模塊名、方法名、參數等信息)傳遞給 Native 段
- Native 端查找與執行:Native 端的 WXModuleManager 根據模塊名從之前註冊的工廠中獲取創建的 Module 實例,並根據方法名找到對應的 MethodInvoker。MethodInvoker 會通過反射手段調用具體的 Native 方法
- 結果回調:如果有需要,Native 可以通過 WXModuleCallBack 或者 WXModuleKeepAliveCallBack 將結果回調給 JS。WXModuleCallback 只能回調1次,而 WXModuleKeepAliveCallback 可以多次回調
3. WXModuleProtocol 的作用
WXModuleProtocol 是一個協議,定義了 Module 的行為規範。你的自定義 Module 必須遵循此協議。它聲明瞭 Module 需要實現的方法或屬性,例如如何暴露方法給 JS(通過 WX_EXPORT_METHOD 宏)、方法在哪個線程執行(通過實現特定的方法返回目標線程,例如 targetExecuteThread)、以及如何通過 weexInstance 屬性弱引用持有它的 WXSDKInstance 實例。
通過遵循 WXModuleProtocol,你自定義的 Module 就能被 Weex SDK 正確識別和調
4. WXModuleFactory 的作用
- 存儲配置:在註冊階段,它會緩存 Module 的配置信息,例如模塊名和對應的工廠類(
WXModuleConfig)。 - 方法解析:通過反射,解析 Module 類中所有通過
WX_EXPORT_METHOD或WX_EXPORT_METHOD_SYNC宏暴露的方法,並生成方法名與MethodInvoker(封裝了反射調用邏輯)的映射關係。 - 提供實例:當 JS 調用 Module 方法時,
WXModuleManager會通過WXModuleFactory根據模塊名獲取或創建 Module 實例,以及對應方法的MethodInvoker。
八、Weex 分為幾個線程
1. 主線程
核心定位:應用的 UI 線程(與原生 App 主線程同源),負責 UI 渲染、用户交互響應,禁止耗時操作。
核心職責:
- 承載 Weex 頁面的 原生渲染容器(如 Android 的
WXFrameLayout、iOS 的WXSDKInstanceView),執行視圖佈局、繪製、動畫觸發; - 處理用户交互事件(點擊、滑動、輸入等),並將事件轉發給 JS 線程(如需要 JS 邏輯響應時);
- 執行原生模塊的 主線程方法(通過
@WXModuleAnnotation(runOnUIThread = true)標記的方法,如彈 Toast、更新 UI 的原生能力); - 接收 JS 線程下發的 UI 操作指令(如創建視圖、修改樣式、更新屬性),並映射為原生視圖操作;
關鍵約束:所有直接操作原生視圖的邏輯必須在主線程執行,否則會導致 UI 錯亂或崩潰
2. JS 線程
核心定位:Weex 的 “業務邏輯線程”,獨立於主線程,專門運行 JavaScript 代碼,避免阻塞 UI。
核心職責:
- 加載並執行 Weex 業務代碼(
.we編譯後的 JS bundle),包括 Vue/React 組件初始化、數據綁定、生命週期管理; - 處理 JS 層面的業務邏輯(事件響應、數據計算、接口請求預處理);
- 調用原生模塊時,通過 JSBridge 轉發請求(區分同步 / 異步,同步請求會短暫阻塞 JS 線程,需謹慎使用);
- 生成 UI 操作指令(如
createElement、updateStyle),通過跨線程通信發送給主線程執行; - 接收主線程轉發的用户交互事件(如點擊回調),執行對應的 JS 事件處理函數;
關鍵優化:最新版本中,JS 線程支持 Bundle 預加載、懶加載組件**,減少啓動耗時;同時通過 JSContext隔離多個 Weex 實例,避免線程內資源競爭。
3. 耗時線程
1. 網絡線程
核心定位:Weex 框架封裝的 專用網絡線程(跨端統一調度),避免網絡請求阻塞主線程或 JS 線程。
核心職責:
- 處理 Weex 內置的網絡請求(如
weex.requireModule('stream')發起的 HTTP/HTTPS 請求); - 負責 JS Bundle 的下載(首次加載或更新時),支持斷點續傳、緩存管理;
- 處理網絡請求的攔截、重試、超時控制(框架層統一實現,無需業務關心);
- 將網絡響應結果通過 JSBridge 回傳給 JS 線程;
設計亮點:與原生系統的網絡庫解耦,但對外暴露統一的 JS API,線程調度由框架內部管理,業務無需手動切換線程
2. 圖片下載線程
核心定位:專門處理 Weex 圖片的異步加載、解碼,避免佔用主線程資源導致 UI 卡頓。
核心職責:
- 加載網絡圖片、本地圖片(通過
img標籤或weex.requireModule('image')); - 圖片解碼、壓縮(適配視圖尺寸,減少內存佔用);
- 圖片緩存管理(內存緩存 + 磁盤緩存,框架層統一維護);
- 加載完成後,將圖片 bitmap 提交到主線程渲染;
iOS 側圖片加載線程的核心管理類是 WXImageComponent。
Weex 線程職責邊界清晰:UI 操作歸主線程,JS 邏輯歸 JS 線程,耗時操作歸工作線程 / 網絡線程,避免跨線程直接操作資源
九、JS 和 Native 通信
1. callJS 和 callNative
| 通信方向 | 發起方 | 接收方 | 核心目的 | 典型場景 |
|---|---|---|---|---|
callNative |
JS | Native | JS 調用 Native 的模塊 / 組件接口 | 渲染組件、彈 Toast、獲取設備信息 |
callJS |
Native | JS | Native 觸發 JS 的回調函數 | 組件事件回調(如按鈕點擊)、數據同步(如網絡請求結果) |
兩者的底層依賴 同一個 JS Bridge 通道,只是「發起方」和「數據格式」不同,Weex 已封裝好統一的通信框架,開發者無需關心底層傳輸細節
2. callNative 實現
callNative 是 JS 主動調用 Native 接口的過程,核心流程:JS 構造標準化指令 → 序列化 JSON → 橋接通道發送 → Native 解析指令 → 執行對應接口 → 響應結果回傳。
怎麼樣?是不是感覺似曾相識,早期做 Hybrid 的時候,JS 和 Native 的通信也是一樣的流程,感興趣的可以查看這篇文章。
是的,通信要解決的問題一直不變,所以方案也不變。
1. 標準化指令格式
為了讓 Native 能統一解析,Weex 規定 callNative 的指令必須包含 4 個核心字段(JS 端構造):
const callNative指令 = {
module: "component", // 模塊名(如 component/modal/device)
method: "create", // 方法名(如 create/toast/getInfo)
params: {}, // 入參(如組件樣式、Toast 內容)
callbackId: "cb_123" // 回調 ID(用於 Native 回傳結果)
};
module+method:定位 Native 端的具體接口(如modal.toast對應 Native 的「彈 Toast」接口);params:JS 傳遞給 Native 的數據(需是 JSON 兼容類型);callbackId:唯一標識當前請求,Native 執行完成後通過該 ID 找到對應的 JS 回調函數。
2. JS 端實現
JS 側調用 Native 的核心是3個實例方法,對應3類場景
| 方法名 | 用途 | 對應 Native 接口 |
|---|---|---|
callModule |
調用 Native 普通模塊(如 modal/storage) |
global.callNativeModule |
callComponent |
調用 Native 自定義組件方法 | global.callNativeComponent |
callDOM |
調用 DOM 相關 Native 方法(如創建元素) | global.callAddElement 等獨立方法 |
這3個方法都會通過 Native 注入的全局函數(global 上的方法)將調用傳遞給 Native 層
這3個方法在源碼最後
// 調用 DOM 相關 Native 方法
callDOM (action, args) {
return this[action](this.instanceId, args)
}
// 調用 Native 自定義組件方法
callComponent (ref, method, args, options) {
return this.componentHandler(this.instanceId, ref, method, args, options)
}
// 調用 Native 普通模塊方法(最常用,對應原 callNative)
callModule (module, method, args, options) {
return this.moduleHandler(this.instanceId, module, method, args, options)
}
1. 普通模塊調用 callModule → moduleHandler
moduleHandler 是普通模塊調用的最終轉發函數,源碼中通過 global.callNativeModule 對接 Native:
proto.moduleHandler = global.callNativeModule ||
((id, module, method, args) =>
fallback(id, [{ module, method, args }]))
- 正常情況(客户端環境):
global.callNativeModule是 Native 注入到 JS 全局的函數(iOS/Android 原生實現),直接接收instanceId、模塊名、方法名、參數,傳遞給 Native 層。 - 降級情況(無 Native 橋接):調用
fallback函數(初始化時由sendTasks參數傳入,通常用於調試 / 模擬)。
2. 自定義組件調用 callComponent → componentHandler
邏輯與 moduleHandler 一致,對接 global.callNativeComponent:
proto.componentHandler = global.callNativeComponent ||
((id, ref, method, args, options) =>
fallback(id, [{ component: options.component, ref, method, args }]))
3. DOM 方法調用 callDOM → 獨立全局函數映射
DOM 相關的 Native 方法(如 addElement/updateStyle)被單獨映射到 global 上的獨立函數(而非統一的 callNative),源碼通過 init 函數初始化映射:
// 源碼第 116-138 行:DOM 方法與 Native 全局函數的映射
export function init () {
const DOM_METHODS = {
createFinish: global.callCreateFinish,
addElement: global.callAddElement, // DOM 創建元素 → Native 的 callAddElement
removeElement: global.callRemoveElement, // DOM 刪除元素 → Native 的 callRemoveElement
updateAttrs: global.callUpdateAttrs, // 更新屬性 → Native 的 callUpdateAttrs
// ... 其他 DOM 方法
}
const proto = TaskCenter.prototype
// 給 TaskCenter 原型掛載 DOM 方法,直接調用 Native 注入的全局函數
for (const name in DOM_METHODS) {
const method = DOM_METHODS[name]
proto[name] = method ?
(id, args) => method(id, ...args) : // 正常情況:調用 Native 全局函數
(id, args) => fallback(...) // 降級情況
}
}
例如調用 callDOM('addElement', args) 時,最終會執行 global.callAddElement(instanceId, ...args),直接對接 Native 的 DOM 模塊。其實是注入到 JSContext 裏的方法對象。
在 Weex 的 JS 運行環境中,global 是 JS 全局對象(Global Object)—— 它是所有 JS 代碼的 “頂層容器”,所有未被定義在局部作用域的變量、函數,最終都會掛載到 global 上(類似瀏覽器環境的 window,Node.js 環境的 global)
Native 向 JS 引擎的 “全局上下文” 注入 callAddElement 函數時,該函數會自動成為 global 對象的屬性——JS 側的 global.callAddElement,本質就是訪問這個被 Native 注入到全局的函數。
QA:global 是什麼?
是 JS 全局對象。不管是瀏覽器、Node.js 還是 Weex 的 JS 引擎(JavaScriptCore/QuickJS),都有一個 全局對象(Global Object):
- 它是 JS 運行環境的 “根”,所有全局變量、函數都是它的屬性;
-
不同環境的全局對象名稱不同:
- 瀏覽器環境:叫
window(比如window.alert、window.document); - Node.js 環境:叫
global(比如global.console、global.setTimeout); - Weex 環境:叫
global(因為 Weex 不依賴瀏覽器,沒有window,直接用 JS 引擎原生的全局對象global)。
- 瀏覽器環境:叫
// WXJSCoreBridge.mm
- (void)registerCallAddElement:(WXJSCallAddElement)callAddElement
{
id callAddElementBlock = ^(JSValue *instanceId, JSValue *ref, JSValue *element, JSValue *index, JSValue *ifCallback) {
NSString *instanceIdString = [instanceId toString];
WXSDKInstance *instance = [WXSDKManager instanceForID:instanceIdString];
if (instance.unicornRender) {
JSValueRef args[] = {instanceId.JSValueRef, ref.JSValueRef, element.JSValueRef, index.JSValueRef};
[WXCoreBridge callUnicornRenderAction:instanceIdString
module:"dom"
method:"addElement"
context:[JSContext currentContext]
args:args
argCount:4];
return [JSValue valueWithInt32:0 inContext:[JSContext currentContext]];
}
NSDictionary *componentData = [element toDictionary];
NSString *parentRef = [ref toString];
NSInteger insertIndex = [[index toNumber] integerValue];
if (WXAnalyzerCenter.isInteractionLogOpen) {
WXLogDebug(@"wxInteractionAnalyzer : [jsengin][addElementStart],%@,%@",instanceIdString,componentData[@"ref"]);
}
return [JSValue valueWithInt32:(int32_t)callAddElement(instanceIdString, parentRef, componentData, insertIndex) inContext:[JSContext currentContext]];
};
_jsContext[@"callAddElement"] = callAddElementBlock;
}
在 js 側是通過 TaskCenter.js 的 init 方法中定義的,存在映射關係, addElement: global.callAddElement,
3. callJS 實現
WXReactorProtocol 協議:
- 定義 Native 調用 JS 的「標準接口」(如觸發回調、發送事件),不關心底層用哪種 JS 引擎(JavaScriptCore / 其他);
- 具體的橋接類(如
WXJSCoreBridge)遵守這個協議,實現接口方法 —— 即使未來替換 JS 引擎,只要遵守協議,上層代碼(如 Native 模塊、組件)無需修改。
@class JSContext;
@protocol WXReactorProtocol <NSObject>
@required
/**
Weex should register a JSContext to reactor
*/
- (void)registerJSContext:(NSString *)instanceId;
/**
Reactor execute js source
*/
- (void)render:(NSString *)instanceId source:(NSString*)source data:(NSDictionary* _Nullable)data;
- (void)unregisterJSContext:(NSString *)instanceId;
/**
When js call Weex NativeModule, invoke callback function
@param instanceId : weex instance id
@param callbackId : callback function id
@param args : args
*/
- (void)invokeCallBack:(NSString *)instanceId function:(NSString *)callbackId args:(NSArray * _Nullable)args;
/**
Native event to js
@param instanceId : instance id
@param ref : node reference
@param event : event type
@param args : parameters in event object
@param domChanges : dom value changes, used for two-way data binding
*/
- (void)fireEvent:(NSString *)instanceId ref:(NSString *)ref event:(NSString *)event args:(NSDictionary * _Nullable)args domChanges:(NSDictionary * _Nullable)domChanges;
@end
Native 模塊(Module)/組件(Component) 完成任務後 -> WXBridgeManager.callBack(...) → 構造 JS 腳本(調用 TaskCenter.callback) → WXJSCoreBridge.executeJavascript(...) → JS 引擎執行 → TaskCenter.callback 響應
WXJSCoreBridge 本身不直接拼接回調腳本,而是提供 executeJavascript: 方法(源碼第 102 行),作為 JS 腳本執行的底層入口;真正的腳本構造,在 WXBridgeManager 中
WXBridgeManager 事件回調
- (void)fireEvent:(NSString *)instanceId ref:(NSString *)ref type:(NSString *)type params:(NSDictionary *)params
{
[self fireEvent:instanceId ref:ref type:type params:params domChanges:nil];
}
- (void)fireEvent:(NSString *)instanceId ref:(NSString *)ref type:(NSString *)type params:(NSDictionary *)params domChanges:(NSDictionary *)domChanges
{
[self fireEvent:instanceId ref:ref type:type params:params domChanges:domChanges handlerArguments:nil];
}
- (void)fireEvent:(NSString *)instanceId ref:(NSString *)ref type:(NSString *)type params:(NSDictionary *)params domChanges:(NSDictionary *)domChanges handlerArguments:(NSArray *)handlerArguments
{
// ...
WXCallJSMethod *method = [[WXCallJSMethod alloc] initWithModuleName:nil methodName:@"fireEvent" arguments:[WXUtility convertContainerToImmutable:args] instance:instance];
[self callJsMethod:method];
}
- (void)callJsMethod:(WXCallJSMethod *)method
{
if (!method || !method.instance) return;
__weak typeof(self) weakSelf = self;
WXPerformBlockOnBridgeThreadForInstance(^(){
WXBridgeContext* context = method.instance.useBackupJsThread ? weakSelf.backupBridgeCtx : weakSelf.bridgeCtx;
[context executeJsMethod:method];
}, method.instance.instanceId);
}
WXBridgeContext.m 代碼如下:
- (void)executeJsMethod:(WXCallJSMethod *)method {
// ...
[sendQueue addObject:method];
[self performSelector:@selector(_sendQueueLoop) withObject:nil];
}
- (void)_sendQueueLoop {
if ([tasks count] > 0 && execIns) {
WXSDKInstance * execInstance = [WXSDKManager instanceForID:execIns];
NSTimeInterval start = CACurrentMediaTime()*1000;
if (execInstance.instanceJavaScriptContext && execInstance.bundleType) {
[self callJSMethod:@"__WEEX_CALL_JAVASCRIPT__" args:@[execIns, [tasks copy]] onContext:execInstance.instanceJavaScriptContext completion:nil];
} else {
[self callJSMethod:@"callJS" args:@[execIns, [tasks copy]]];
}
// ...
}
}
- (void)callJSMethod:(NSString *)method args:(NSArray *)args {
if (self.frameworkLoadFinished) {
[self.jsBridge callJSMethod:method args:args];
} else {
[_methodQueue addObject:@{@"method":method, @"args":args}];
}
}
再到 WXJSCoreManager
- (JSValue *)callJSMethod:(NSString *)method args:(NSArray *)args {
WXLogDebug(@"Calling JS... method:%@, args:%@", method, args);
WXPerformBlockOnMainThread(^{
[[WXBridgeManager sharedManager].lastMethodInfo setObject:method ?: @"" forKey:@"method"];
[[WXBridgeManager sharedManager].lastMethodInfo setObject:args ?: @[] forKey:@"args"];
});
return [[_jsContext globalObject] invokeMethod:method withArguments:[args copy]];
}
其實不管是 CallJS 還是 CallNative,通信的技術方案設計和 Hybrid 的設計一致,都需要在 JavascriptCore 的 global 對象上掛載一個方法。比如 Native 註冊了一個 WXComponent 之後,Weex 側用 Vue 語法寫完了個頁面,呈現在用户手機上,用户點擊頁面上的按鈕之後,Native 再將事件回調給 Weex 側,Weex 再去處理後續邏輯。
4. WXAssertComponentThread 斷言
WXAssertComponentThread 的核心作用是 強制約束組件相關操作在「組件專屬線程」執行,本質是為了解決「線程安全」和「性能穩定性」問題
iOS 開發的核心線程規則是「UI 操作必須在主線程」,但 Weex 組件的工作流程(綁定解析、數據計算、佈局計算、子組件管理)包含大量「非 UI 操作」—— 如果這些操作都在主線程執行,會阻塞主線程(比如長列表數據解析、複雜表達式計算),導致 UI 卡頓(比如滑動掉幀)
因此 Weex 設計了線程分工
| 線程類型 | 負責的操作 |
|---|---|
| 組件專屬線程 | 綁定規則解析(_storeBindings)、表達式計算(bindingBlockWithExpression)、數據更新(updateBindingData)、佈局計算(calculateLayout) |
| 主線程 | 最終 UI 渲染(如 UIImageView 設圖、UILabel 設文本)、子視圖增刪(insertSubview) |
1. 避免「線程安全問題」,防止崩潰 / 數據錯亂
組件的核心數據(如 _bindingProps、_subcomponents、_flexCssNode)都是「非線程安全的」(沒有加鎖保護)—— 如果多個線程同時讀寫這些數據,會導致:
- 數據競爭:比如主線程讀取
_subcomponents遍歷,組件線程同時修改_subcomponents(增刪子組件),導致數組越界崩潰; - 數據不一致:比如組件線程更新
_bindingProps的值,主線程同時讀取該值用於 UI 更新,導致顯示錯誤的舊值; - 野指針:比如組件線程銷燬子組件,主線程還在訪問該子組件的
view。
線程斷言通過「強制所有組件核心操作在同一線程執行」,從根源上避免了這些跨線程問題 —— 同一時間只有一個線程操作組件數據,無需複雜鎖機制(鎖會降低性能)。
2. 簡化調試,快速定位線程問題
如果沒有線程斷言,跨線程操作組件可能導致「偶現崩潰」(比如 100 次操作出現 1 次),難以復現和排查(日誌中看不到線程上下文)。而線程斷言會在「違規線程調用時直接崩潰」,並明確提示「必須在組件線程執行」,開發者能立刻定位到違規代碼(比如在主線程調用了 updateBindingData),大幅降低調試成本。
3. 保證操作順序一致性
組件的更新流程是「解析綁定 → 計算表達式 → 更新屬性 → 佈局計算 → UI 渲染」—— 這些步驟必須按順序執行。如果分散在多個線程,可能出現「佈局計算還沒完成,UI 已經開始渲染」的情況(導致佈局錯亂)。組件專屬線程保證了所有操作串行執行,順序不會亂。
5. WXJSASTParser 的工作原理
WXJSASTParser 如何把表達式字符串解析為 AST 節點?
WXJSASTParser 是 Weex 自定義的「輕量 JS 表達式解析器」—— 核心是「按 JS 語法規則,把字符串拆分為結構化的 AST 節點」,全程不依賴完整 JS 引擎(如 JSC/V8),只支持綁定表達式需要的基礎語法(標識符、成員訪問、二元運算等),兼顧性能和體積。
整個解析過程分 3 步:詞法分析 → 語法分析 → AST 節點封裝,和編譯器的前端流程一致,以下結合示例("user.name + '?size=100'")拆解:
先明確:AST 是什麼?
AST(抽象語法樹)是「用樹形結構表示代碼語法」的中間結構 —— 比如表達式 user.name + '?size=100',AST 會拆分為:
根節點:BinaryExpression(運算符 '+')
├─ 左子節點:MemberExpression(成員訪問)
│ ├─ object:Identifier(標識符 'user')
│ └─ property:Identifier(標識符 'name')
└─ 右子節點:StringLiteral(字符串字面量 '?size=100')
這種結構能被程序快速遍歷和計算(比如之前講的生成 WXDataBindingBlock 時,遞歸遍歷節點執行運算)。
1.詞法分析(Lexical Analysis)
拆分為詞法單元(Token)。詞法分析是「把表達式字符串拆分為最小的、有意義的語法單元」,忽略空格、換行等無關字符。核心是「按 JS 語法規則匹配字符序列」。
表達式 "user.name + '?size=100'"` 詞法分析後得到的 Token 序列:
| Token 類型 | Token 值 | 説明 |
|---|---|---|
IDENTIFIER |
user |
標識符(變量名 / 屬性名) |
DOT |
. |
成員訪問運算符 |
IDENTIFIER |
name |
標識符 |
PLUS |
+ |
二元運算符(加法 / 拼接) |
STRING_LITERAL |
?size=100 |
字符串字面量(去掉引號) |
詞法分析的實現邏輯(簡化):
- 初始化一個「字符指針」,從表達式字符串開頭遍歷;
- 遇到字母 / 下劃線 → 繼續往後讀,直到非字母 / 數字 / 下劃線 → 識別為
IDENTIFIER(如user); - 遇到
+/-/*///>/=等 → 識別為對應運算符(如+→PLUS); - 遇到
"或'→ 繼續往後讀,直到下一個相同引號 → 識別為STRING_LITERAL(去掉引號); - 遇到
.→ 識別為DOT(成員訪問); - 遇到空格 / 製表符 → 直接跳過(無意義字符);
- 遇到無法識別的字符(如
#/@)→ 拋出語法錯誤(WXLogError)。
Weex 的 WXJSASTParser 內部會維護一個「Token 流」(數組),詞法分析後把 Token 按順序存入流中,供下一步語法分析使用。
2. 語法分析(Syntactic Analysis)
語法分析是「根據 JS 表達式語法規則,把 Token 流組合為樹形 AST 節點」—— 核心是「驗證 Token 序列是否符合語法,並構建層級關係」。
Weex 支持的 JS 表達式語法子集(核心):
- 標識符:
user、imageUrl(對應WXJSIdentifier); - 成員訪問:
user.name、list[0](對應WXJSMemberExpression); - 字面量:字符串(
'abc')、數字(123)、布爾(true)、null(對應WXJSStringLiteral/WXJSNumericLiteral等); - 二元運算:
a + b、age > 18、a === b(對應WXJSBinaryExpression); - 條件運算:
age > 18 ? 'adult' : 'teen'(對應WXJSConditionalExpression); - 數組表達式:
[a, b, c](對應WXJSArrayExpression)。
示例:Token 流 → AST 節點的構建過程
Token 流:IDENTIFIER(user) → DOT → IDENTIFIER(name) → PLUS → STRING_LITERAL(?size=100)
- 語法分析器先讀取前 3 個 Token(
user→.→name),匹配「成員訪問語法規則」(IDENTIFIER . IDENTIFIER)→ 構建WXJSMemberExpression節點(左子節點user,右子節點name); - 接着讀取
PLUS(二元運算符),再讀取後面的STRING_LITERAL(?size=100)→ 匹配「二元運算語法規則」(Expression + Expression); - 把之前構建的
WXJSMemberExpression作為「左子節點」,STRING_LITERAL作為「右子節點」,PLUS作為「運算符」→ 構建根節點WXJSBinaryExpression; - 最終生成 AST 樹(如之前的結構)。
語法分析的實現邏輯(簡化):
Weex 採用「遞歸下降分析法」(最適合手工實現的語法分析方法):
- 為每種表達式類型定義一個「解析函數」(如
parseMemberExpression解析成員訪問、parseBinaryExpression解析二元運算); - 解析函數遞歸調用:比如
parseBinaryExpression會調用parseMemberExpression解析左右操作數,parseMemberExpression會調用parseIdentifier解析標識符; - 語法校驗:如果 Token 序列不符合規則(如
user.name +缺少右操作數),會拋出「語法錯誤」日誌,終止解析。
3. AST 節點封裝
轉為 Weex 自定義的 WXJSExpression。語法分析生成的是「抽象語法樹結構」,Weex 會把這個結構封裝為自定義的 WXJSExpression 子類(對應不同表達式類型),每個子類存儲該節點的關鍵信息(如運算符、子節點),供後續生成 WXDataBindingBlock 使用。
示例封裝:
WXJSMemberExpression類:存儲object(子節點,如user)、property(子節點,如name)、computed(是否是計算屬性,如list[0]為YES,user.name為NO);WXJSBinaryExpression類:存儲left(左子節點)、right(右子節點)、operator_(運算符字符串,如"+");- 字面量類(如
WXJSStringLiteral):存儲value(字面量值,如?size=100)。
這些類的定義在 Weex 源碼的 WXJSASTParser.h 中,本質是「數據容器」,把 AST 結構轉化為 Objective-C 代碼可訪問的對象。
WXJSASTParser 本質:它不是完整的 JS 解析器(不支持 function、for 等複雜語法),而是「專門為 Weex 綁定表達式設計的輕量解析器」—— 只解析需要的 JS 表達式子集,把字符串轉為結構化的 AST 節點,最終目的是「讓 Native 代碼能遞歸遍歷節點,計算出表達式結果」(如 user.name + '?size=100' → avatar.png?size=100)。
這種「自定義輕量解析器」的設計,既避免了依賴完整 JS 引擎的體積和性能開銷,又能精準適配 Weex 的綁定需求,是跨端框架的常見優化思路。
十、值得借鑑的地方
1. WXThreadSafeMutableDictionary 線程安全字典
Weex 中的 WXThreadSafeMutableDictionary 提供了一個線程安全的字典,其本質是通過加 pthread_muext_t 鎖來維護內部的一個字典的。
比如下面的代碼
初始化鎖相關的配置
@interface WXThreadSafeMutableDictionary ()
{
NSMutableDictionary* _dict;
pthread_mutex_t _safeThreadDictionaryMutex;
pthread_mutexattr_t _safeThreadDictionaryMutexAttr;
}
@end
@implementation WXThreadSafeMutableDictionary
- (instancetype)initCommon
{
self = [super init];
if (self) {
pthread_mutexattr_init(&(_safeThreadDictionaryMutexAttr));
pthread_mutexattr_settype(&(_safeThreadDictionaryMutexAttr), PTHREAD_MUTEX_RECURSIVE); // must use recursive lock
pthread_mutex_init(&(_safeThreadDictionaryMutex), &(_safeThreadDictionaryMutexAttr));
}
return self;
}
- (instancetype)init
{
self = [self initCommon];
if (self) {
_dict = [NSMutableDictionary dictionary];
}
return self;
}
在字典操作的地方使用鎖
- (void)setObject:(id)anObject forKey:(id<NSCopying>)aKey
{
id originalObject = nil; // make sure that object is not released in lock
@try {
pthread_mutex_lock(&_safeThreadDictionaryMutex);
originalObject = [_dict objectForKey:aKey];
[_dict setObject:anObject forKey:aKey];
}
@finally {
pthread_mutex_unlock(&_safeThreadDictionaryMutex);
}
originalObject = nil;
}
這麼寫的價值:解鎖邏輯「絕對執行」,徹底避免死鎖
這是 @try-finally 最核心的價值 ——無論 try 塊內發生什麼(正常執行、提前 return、拋異常),finally 塊的解鎖邏輯一定會執行
對比無 try-finally 的寫法
// Bad: 若setObject拋異常,unlock不會執行→死鎖
pthread_mutex_lock(&_mutex);
[_dict setObject:anObject forKey:aKey];
pthread_mutex_unlock(&_mutex);
問題:[_dict setObject:anObject forKey:aKey] 可能拋異常(比如 aKey = nil 時會觸發 NSInvalidArgumentException),若沒有 finally,鎖會被永久持有→其他線程調用 lock 時死鎖,整個字典無法再操作。
設計優點:
@try-finallly:即使 try 內邏輯出錯,finally 也會執行 pthread_mutex_unlock,保證鎖最終釋放,這是線程安全的「兜底保障」- 注意,不是
try...catch...finally: 如果加了 catch 邏輯,則字典的 key 為 nil 產生的崩潰也會被捕獲掉,這屬於不符合預期的行為。因為 key 為 nil 產生的原因太多了,可能是業務代碼異常,也可能是數據異常,也可能是邏輯錯誤,如果一刀切直接用try...catch...finally捕獲了異常,但是沒有配置異常的收集、上報、處理邏輯,屬於邊界不清晰,本質是為了解決加解鎖不匹配而可能帶來的線程安全問題,卻"多管閒事",把字典 key 為 nil 本該向上跑的異常而卡住了(這個問題不再贅述,是一個經典的策略問題,端上的異常發生時,安全氣墊的“做與不做”問題)
延伸:聊聊類似網易的大白解決方案或者業界其他公司中,安全氣墊雖然保證了代碼不 crash,影響用户體驗,但是比如數組本該越界,現在卻不越界:
- 唯一能做的就是返回一個錯誤的值,比如數組長度為3,訪問4,現在不 crash,返回了 0 的值,那是不是產生了業務異常?比如商品價格
- 不 crash,也不返回錯誤位置的值,類似給一個回調,告訴業務方出現了異常,可以做一些業務層面的提醒或者配置(比如開發階段商品卡片的價格 Label 顯示:商品價格獲取錯誤,數組越界),同時產生的異常案發現場信息和其他的一些數據會上報,用於 APM 平台去分析和定位。
但這也產生一個問題,類似數組越界的場景,可能10000次裏面9999次都正常,只有1次異常,業務開發為了這萬分之一出現的異常,還需要寫一些異常處理的邏輯(比如商品卡片展示價格獲取錯誤,數組越界)。那字典的 key 為 nil 呢?除法的分母為0呢?諸如此類,類似樂觀鎖和悲觀鎖的場景
相關問題的思考可以查看這篇文章:安全氣墊
- WXHandlerFactory:Weex 核心的「處理器工廠」,負責管理所有協議(如圖片加載、網絡請求、存儲等)的實現類註冊 / 查找;
- WXImgLoaderProtocol:Weex 定義的「圖片加載協議」,僅聲明接口(下載、取消、緩存等),不包含具體實現。
Weex 支持業務層自定義圖片加載邏輯(比如統一用項目的圖片緩存庫、添加下載攔截、埋點等),此時自定義實現類會替代默認實現,成為下載執行者:
步驟 1:業務層創建類(如 MyCustomImgLoader),遵循 WXImgLoaderProtocol,實現 wx_loadImageWithURL: 等協議方法(內部可調用 SDWebImage/AFNetworking 等完成下載);
步驟 2:將自定義類註冊到 WXHandlerFactory:
[WXHandlerFactory registerHandler:[MyCustomImgLoader new] forProtocol:@protocol(WXImgLoaderProtocol)];
步驟 3:此時 [WXHandlerFactory handlerForProtocol:@protocol(WXImgLoaderProtocol)] 會返回 MyCustomImgLoader 實例,所有圖片下載由該類負責
2. 設計分層合理
Weex 最核心的設計是將整個框架清晰地分為:語法層(DSL)、中間層(JS Framework)和渲染層(Native SDK)
這種渲染引擎和語法層 DSL 分離的設計,可以使得上層 DSL 方便拓展 Vue、Rax 寫法,下層渲染引擎可以保持較好的穩定性。為了生態的拓展提供了極大的便攜性。
3. 可擴展的組件與模塊系統
Weex 通過WXSDKEngine.registerComponent() 和 registerModule() 方法,允許開發者擴展原生組件 (UI Component)和模塊(Login Module)。這套機制設計得足夠底層和通用,使得 Weex 可以由開發者來註冊,由公司內的體驗設計中心規範來落地的組件。以及一些基礎能力。這樣子 Weex 官方已經提供了一些功能強大的筋骨,我們在其之上可以提供更符合需求的外表和更有力量的一塊手臂肌肉。
雖然事後視角來看,Weex、RN、Flutter,甚至是更早的、設計完善的 Hybrid 都有該能力。但這對於遠古時期的 Weex 來説,還是可圈可點的。
4. 輕量 JSBundle + 增量更新支持
Weex 的 JSBundle 僅包含業務邏輯和組件描述,框架代碼(Vue 內核、Weex 基礎 API)內置在原生 SDK 中,因此 Bundle 體積極小;同時支持將 Bundle 拆分為 “基礎包(公共邏輯)+ 業務包(頁面邏輯)”,實現增量更新。
解決了跨端框架 “首屏加載慢” 的痛點(小 Bundle 加載更快),同時增量更新降低了發佈成本。
十一、Weex APM
1. 歷史背景
Weex 是諸多年前的產物,部分業務線用 Weex 寫了部分功能模塊,或者是某幾個頁面,或者是某個二級、三級業務 SDK 的頁面。但可以確定的是:
- 21年就完成了 Flutter 的基建開發(對齊 Native 的 UI 組件庫,遵循體驗設計平台產出的集團 UI 標準;做了 Flutter 的大量 plugin、打包構建平台、日誌庫、網絡庫、探照燈、APM SDK、熱修復能力等)。新業務的實現只會在 Native 和 Flutter 上考慮
- Weex 業務代碼基本上是存量的
- Weex 代碼沒有 bug 就不去修改;有版本迭代,之前是 Weex 實現的,本次只做簡單 UI 增刪或字段調整,也是會修改一下。初次之外不修改 Weex 代碼
所以像 Native 一樣去全面監控性能、網絡、crash、異常、白屏、頁面加載耗時等維度的話,ROI 是很低的。那麼就需要制定一些策略去有針對性的監控高優問題。
Weex 的異常比較有特點,比如在頁面的模版代碼中綁定了 data 中的一個對象,此時對象可能並沒有值,而是依賴後續的網絡請求完成,對象才有了具體的值 data 改變,數據驅動,頁面再次 render。所以監控代碼會認為第一次 render 的時候訪問對象不存在的屬性。
真正有問題的代碼和不影響業務的異常信息,都會被 Vue 官方認為是異常。基於這樣的背景,我們無法 pick 出真正異常或者是開發者判空代碼沒寫好的問題。基於此,我們需要做一些約定和標準。
2. 優先級權衡標準
這時候就需要摒棄程序員視角(不然會陷入啥數據都想統計,可能是潔癖、可能是追求),但從 ROI 角度出發,我們就需要切換到用户視角。
假設你是一個用户,什麼樣的情況代表業務異常,對我們的用户來説比較痛呢?
- 頁面白屏了,看都看不到了,別説你們的 App 為我賦能解決用户痛點了
- 稍微好點,可以看到頁面了,但是某一個區域是白屏的。比如:該頁面大部分在展示商品價格、商品數量、商品折扣價、商品折扣信息、下面應該是有個“確認支付”按鈕,但是此處就是空白,點也點不了。
- 情況再好點。可以看到全部的頁面了,但是點擊後無響應。比如:該頁面大部分在展示商品價格、商品數量、商品折扣價、商品折扣信息、下面有個“確認支付”按鈕。用户在考慮再三,本着理性購物後,發現是剛需品,咬緊牙要付款了,此時點擊“確認支付”按鈕了,但是頁面沒有任何反應。用户也是“見多識廣”的體面人,猜測可能是網絡不好的情況,所以等了1分鐘,他很有耐心。切換了 WI-FI 到 5G 後,繼續點擊,依舊沒反應。一怒之下點了10次,等了2分鐘,還是沒反應。他奔潰了,卸載了 App
上述幾種情況,總結為:按照異常等級,可以劃分為影響業務和不影響業務。什麼叫“影響業務”?這是我們自己定義的標準,影響用户是否正常操作 App。比如:頁面白屏(頁面全部白屏、頁面部分白屏)、點擊某個按鈕無響應,這些叫做“影響業務”,屬於 Error 級別。其他的一些輕微異常,不影響用户使用 App 功能,不影響業務,屬於 Warning 級別。
3. UI 顯示異常
1. 部分白屏:註冊的 Component 使用異常
這種情況就屬於頁面部分白屏。因為某個哪個 Compoent 會鋪滿頁面,基本類似 iOS UI 控件一樣組合使用。就像上文描述的「該頁面大部分在展示商品價格、商品數量、商品折扣價、商品折扣信息、下面應該是有個“確認支付”按鈕,但是此處就是空白」這個空白粗,理應顯示一個 Native 註冊的 Button,但是沒有顯示出來,造成業務的阻塞。
.vue(或 Weex 專屬.we)文件內基於 Vue 擴展的 Weex 跨平台模板 DSL 代碼,在前端構建階段會先由 Webpack 的weex-loader觸發編譯流程:首先通過 Weex 核心編譯器@weex-cli/compiler(複用並擴展vue-template-compiler)將模板 DSL 解析為模板 AST(抽象語法樹);接着由 Weex 自定義 Babel 插件(如babel-plugin-transform-weex-template)將模板 AST 轉換為標準化的 JS AST,並針對 iOS/Android 跨平台特性做屬性、樣式、事件的適配處理(如樣式單位歸一化、事件名標準化);最終生成包含_h(即 Weex 運行時的$createElement,等價於 Vue 的createElement)調用的render函數,該函數會被 Webpack 打包到最終的 Weex JS Bundle 中。
_c('color-button',
{
staticStyle: {
width: "400px",
height: "40px",
marginBottom: "20px"
},
attrs: {
"title": "點擊計算10+20",
"bgColor": "#FF6600",
"message": "hello"
},
on: {
"click": _vm.handleButtonClick
}
},
// 如果有 children 就是 children 信息
)
在 App 運行階段,Weex 的 JS 引擎(iOS 端為 JSCore、Android 端為 V8)加載 JS Bundle 後,執行組件的render函數,通過調用 _h 函數將模板描述轉換為跨平台的虛擬 DOM(VNode),VNode 會被序列化為 JSON 格式,最終通過 JS Bridge 傳遞給 Native 端(iOS/Android)用於原生視圖渲染。
Weex 的 Component 相關邏輯都由 WXComponentManager 負責。頁面在構建展示的時候,會調用 _buildComponent 方法,其內部會調用 WXComponentFactory 的能力(configWithComponentName),根據 ComponentName 獲取 Component。
configWithComponentName 是 Weex iOS 側 WXComponentFactory(組件工廠類)的核心方法之一,核心作用是:根據傳入的組件名稱(如 color-button/div/text),查找該組件對應的 Native 側配置(WXComponentConfig);若找不到對應配置,則降級使用基礎容器組件 div 的默認配置,並輸出警告日誌。
- (WXComponentConfig *)configWithComponentName:(NSString *)name
{
WXAssert(name, @"Can not find config for a nil component name");
WXComponentConfig *config = nil;
[_configLock lock];
config = [_componentConfigs objectForKey:name];
if (!config) {
WXLogWarning(@"No component config for name:%@, use default config", name);
config = [_componentConfigs objectForKey:@"div"];
}
[_configLock unlock];
return config;
}
UI Component 做的比較隨意,認為顯示問題降級用 div 就可以了。做為 SDK 這麼設計也似乎可以接受,但作為業務方,我們必須收集統計這種異常情況。
所以此處我們可以收集案發現場數據,進行上報。我們發現 Weex 自己封裝了 WXExceptionUtils類,暴露了 commitCriticalExceptionRT 接口,用於收集致命問題。
+ (void)commitCriticalExceptionRT:(WXJSExceptionInfo *)jsExceptionInfo{
WXPerformBlockOnComponentThread(^ {
id<WXJSExceptionProtocol> jsExceptionHandler = [WXHandlerFactory handlerForProtocol:@protocol(WXJSExceptionProtocol)];
if ([jsExceptionHandler respondsToSelector:@selector(onJSException:)]) {
[jsExceptionHandler onJSException:jsExceptionInfo];
}
if ([WXAnalyzerCenter isOpen]) {
[WXAnalyzerCenter transErrorInfo:jsExceptionInfo];
}
});
}
可以看到會判斷是否存在可以處理 exception 遵循 WXJSExceptionProtocol 的 handler。所以我們新增一個 WXExceptionReporter 類(遵循 WXJSExceptionProtocol 協議),用於收集異常,然後用於統一的上報,內部提供基礎數據的組裝、字段解析功能。
效果如下:
2. 全部白屏
根據 Weex 的工作原理可以知道,頁面需要展示肯定要根據 url 去獲取 JS Bundle 內容,然後解析成 VNode 最後通過 JSBridge 去調用 Native 的 UI Component 去展示 UI,那麼整個流程幾個重要的環節都可能出錯,導致頁面白屏。
1. 資源請求失敗
JS Bundle 資源請求失敗,存在 Error,此時是無法去展示 Weex 頁面的。這種情況就是 HTTP 狀態碼非200的情況。
每個 Weex 頁面都由 WXSDKInstance 負責下載 JS Bundle 資源,所以下載的邏輯在 WXSDKInstance 裏。
- (void)_renderWithRequest:(WXResourceRequest *)request options:(NSDictionary *)options data:(id)data;
{
_mainBundleLoader.onFinished = ^(WXResourceResponse *response, NSData *data) {
NSError *error = nil;
if ([response isKindOfClass:[NSHTTPURLResponse class]] && ((NSHTTPURLResponse *)response).statusCode != 200) {
error = [NSError errorWithDomain:WX_ERROR_DOMAIN
code:((NSHTTPURLResponse *)response).statusCode
userInfo:@{@"message":@"status code error."}];
if (strongSelf.onFailed) {
strongSelf.onFailed(error);
}
}
if (error) {
[WXExceptionUtils commitCriticalExceptionRT:strongSelf.instanceId
errCode:[NSString stringWithFormat:@"%d", WX_KEY_EXCEPTION_JS_DOWNLOAD]
function:@"_renderWithRequest:options:data:"
exception:[NSString stringWithFormat:@"download bundle error :%@",[error localizedDescription]]
extParams:nil];
return;
}
if (!data) {
NSString *errorMessage = [NSString stringWithFormat:@"Request to %@ With no data return", request.URL];
WX_MONITOR_FAIL_ON_PAGE(WXMTJSDownload, WX_ERR_JSBUNDLE_DOWNLOAD, errorMessage, strongSelf.pageName);
[WXExceptionUtils commitCriticalExceptionRT:strongSelf.instanceId
errCode:[NSString stringWithFormat:@"%d", WX_KEY_EXCEPTION_JS_DOWNLOAD]
function:@"_renderWithRequest:options:data:"
exception:errorMessage
extParams:nil];
return;
}
};
}
模擬 JS Bundle 下載錯誤,效果如下:
下載 JS Bundle 網絡請求完成後,如果出現 Error,則會調用 WXExceptionUtils 的能力,將異常交給 WXExceptionReporter 去處理。
2. 資源請求成功,數據為空
還有一種情況就是:JSBundle 下載請求在 HTTP 層面 “成功完成”(狀態碼 200),但返回的二進制數據 data 為 nil 或空(長度為 0)
可能你會好奇,怎麼可能有空的 JSBundle,什麼場景下會產生這種情況?
凡是正常寫代碼都符合預期就沒有任何 bug 和故障了,所以利用悲觀策略,將各種可能出現問題的地方都監控到,因為只要 JSBundle 為空,頁面肯定是白屏,對於用户側來説都是致命的。
- 服務器/CDN 返回“空響應”:後端 / CDN 配置異常:請求的 JSBundle URL 有效,HTTP 狀態碼返回 200,但響應體(Body)為空(比如靜態 JS 文件被刪除、CDN 緩存失效且源站無數據、後端接口邏輯錯誤未寫入響應內容);
- 下載過程中數據傳輸截斷 / 丟失
- 網絡波動:下載請求已收到服務器的 “響應完成” 信號,但數據傳輸過程中因網絡中斷、超時等導致 NSData 未完整接收(僅 HTTP 頭成功接收,體數據為空);
- Weex 加載器(mainBundleLoader)異常:加載器在將響應數據轉為 NSData 時出現底層錯誤(如內存不足、數據解碼失敗),導致 data 被置為 nil。
Mock:將 data 設為 nil。效果如下:
可以看到 Weex 也會把這種錯誤進行收集,調用 WXExceptionUtils commitCriticalExceptionRT,所以我們添加的 Analyzer 是可以監控到這種異常的。
效果如下:
3. 資源請求成功,數據無法解析
還有一種特殊的情況就是:下載的 JSBundle 二進制數據雖非空,但因無法以 UTF-8 編碼解碼為字符串,導致 Weex 實例無法加載執行該數據,最終頁面 UI 無法正常展示。比如下面的情況:
和上面的情況類似,這種都屬於概率較小的問題,但也要監控和預防。
一些可能的情況:
- JSBundle 文件編碼非 UTF-8。 Weex 要求:JS Bundle 文件必須採用 UTF-8 編碼(無 BOM)以保證跨平台兼容性,非 UTF-8 編碼(如 GBK、UTF-16)可能導致 iOS/Android 平台解析失敗
-
數據損壞/包含非法 UTF-8 字節
- 下載截斷:UTF-8 是「多字節編碼」(比如中文佔 3 字節),若下載過程中數據末尾的字符字節不完整(如只下了 2 字節),解碼時會因 “字節序列不合法” 失敗;
- 數據篡改:CDN / 網關 / 代理在傳輸中混入非 UTF-8 字節(如 0xFF、0xFE、0x00 等無效字節),破壞編碼結構;
- 文件損壞:JSBundle 文件打包 / 上傳時出錯(如壓縮後未正確解壓),包含亂碼 / 二進制碎片
-
請求到非文本數據(URL 錯誤)。請求的 JS Bundle 返回的不是 JS 文本,而是二進制:
- URL 配置錯誤:指向圖片(png/jpg)、壓縮包(zip)、二進制協議數據(如 protobuf)、可執行文件等
- 後端接口錯誤:原本應返回 JS 文本的接口,異常時返回二進制格式的錯誤信息(而非文本錯誤)
- 緩存污染:Weex 本地緩存的 JSBundle 被其他二進制文件覆蓋(如緩存路徑衝突)
-
特殊字符/編碼溢出
- JSBundle 中包含 UTF-8 無法表示的「無效 Unicode 碼點」(如超出 U+10FFFF 範圍,或保留的未定義碼點)
- 數據量過大:極大型 JSBundle 解碼時因內存不足 / 系統限制,導致解碼接口返回 nil(iOS 中 NSString 對單字符串長度有隱性限制)
這種情況,Weex 官方是怎麼做的?
NSString *jsBundleString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (!jsBundleString) {
WX_MONITOR_FAIL_ON_PAGE(WXMTJSDownload, WX_ERR_JSBUNDLE_STRING_CONVERT, @"data converting to string failed.", strongSelf.pageName)
[strongSelf.apmInstance setProperty:KEY_PROPERTIES_ERROR_CODE withValue:[@(WX_ERR_JSBUNDLE_STRING_CONVERT) stringValue]];
return;
}
可以看到,這種情況沒有被 Weex 沒有視為“致命問題”進行上報。只是進行了簡單打印。嘗試站在框架角度想問題,從 SDK Owner 角度歸因:
- HTTP 狀態碼錯誤/無數據:Weex 認為這類錯誤是「外部不可控故障」(網絡、CDN、服務端宕機),會影響大批量實例,屬於 “框架級致命異常”,必須通過 WXExceptionUtils 上報(觸發全局異常統計、告警)
- 編碼轉換失敗:可能是分批多次打包,前幾次都是 UTF-8 格式,只是這次編碼錯誤,是可以定位的。Weex 認為這類錯誤是「內部可控問題」(前端打包時未按 UTF-8 規範輸出、URL 配置錯誤指向二進制文件),屬於 “業務側錯誤”,框架只需記錄監控(提醒開發者修復),無需升級為 “框架級致命異常”。
但從業務方角度出發,不光頁面是 Weex、Native、Flutter、H5,只要是影響了用户體驗,都屬於致命問題,尤其這種整個頁面都是白屏的情況。所以我們需要修改源碼,去上報致命異常。調用 WXExceptionUtils commitCriticalExceptionRT 的能力。
效果如下:
4. 邏輯異常
1. JS 側 require Module 失敗
在 Native [WXSDKEngine registerModule:@"logicCalculation" withClass:[WXLogicCalculationModule class]] 正常註冊的 Module,名字叫 logicCalculation。在 js 側使用的時候不小心寫成 const logicCalculation = weex.requireModule('logicCalculation1'),測試又沒回歸到,問題逃逸到線上,可能就是邏輯問題。Weex 官方的做法就是在 Xcode 打印 log。
所以作為 APM 側,我們要定位和收集到該問題,進行問題上報。
想辦法知道哪裏報錯,requireModule 不是原生寫法,這肯定是 JS 側封裝的,查看 Weex 源碼
// Weex JS Framework 核心源碼(簡化)
WeexInstance.prototype.requireModule = function requireModule(moduleName) {
// 1. 基礎校驗:Weex實例是否有效(比如是否已銷燬)
var id = getId(this); // 獲取當前Weex實例ID
if (!(id && this.document && this.document.taskCenter)) {
console.error("[JS Framework] Failed to requireModule(\"" + moduleName + "\"), instance doesn't exist.");
return;
}
// 2. 關鍵校驗:檢查Module是否在Native側註冊過
if (!isRegisteredModule(moduleName)) {
console.warn("[JS Framework] using unregistered weex module \"" + moduleName + "\"");
return;
}
// 3. 核心:創建Module代理對象(並非真實對象,僅封裝橋接調用)
var moduleProxy = {};
// 獲取該Module在Native側註冊的所有方法(提前從Native同步到JS的方法映射表)
var moduleMethods = getRegisteredMethods(moduleName);
// 4. 為代理對象綁定方法:調用方法時觸發JS-Native橋接
moduleMethods.forEach(function(methodName) {
moduleProxy[methodName] = function() {
// 封裝調用參數:實例ID、Module名、方法名、參數、回調
var args = Array.prototype.slice.call(arguments);
var callback = null;
// 提取最後一個參數作為回調(Weex約定)
if (typeof args[args.length - 1] === 'function') {
callback = args.pop();
}
// 5. 核心:通過taskCenter(橋接核心)調用Native
this.document.taskCenter.sendNative('callNative', {
instanceId: id,
module: moduleName,
method: methodName,
params: args,
callback: callback ? generateCallbackId(callback) : null
});
}.bind(this);
}, this);
// 6. 返回代理對象給JS側使用
return moduleProxy;
};
- 返回的不是真實的 Module 實例,而是代理對象(Proxy) —— 所有方法調用都會被攔截,轉而通過橋接發送到 Native;
- isRegisteredModule 校驗:JS 側會緩存一份「Native 已註冊 Module 列表」(Native 初始化時同步到 JS),避免無效橋接。
方案一:Weex 由於安全設計,沒辦法直接注入 JS。也就是説想通過“切面”思想,hook JS 側 requireModule 是行不通的。這種方案,代碼如下
// 備份原生requireModule方法
const originalRequireModule = WeexInstance.prototype.requireModule;
// 重寫requireModule,在錯誤觸發時主動上報Native
WeexInstance.prototype.requireModule = function (moduleName) {
// 先執行原生判斷邏輯
const id = getId(this);
if (!(id && this.document && this.document.taskCenter)) {
const errorMsg = "[JS Framework] Failed to requireModule(\"" + moduleName + "\"), instance (" + id + ") doesn't exist anymore.";
// 主動上報“實例不存在”錯誤到Native
this.document.taskCenter.sendNative('__weex_apm_report', {
type: 'module_require_failed',
subType: 'instance_not_exist',
moduleName: moduleName,
message: errorMsg,
instanceId: id
});
console.error(errorMsg);
return;
}
// 核心:攔截“未註冊Module”判斷
if (!isRegisteredModule(moduleName)) {
const warnMsg = "[JS Framework] using unregistered weex module \"" + moduleName + "\"";
// 主動上報“Module未註冊”錯誤到Native(關鍵)
this.document.taskCenter.sendNative('__weex_apm_report', {
type: 'module_not_registered',
moduleName: moduleName,
message: warnMsg,
instanceId: id,
timestamp: Date.now()
});
// 保留原生warn日誌(不影響原有邏輯)
console.warn(warnMsg);
return;
}
// 執行原生邏輯
return originalRequireModule.call(this, moduleName);
};
方案二:Native 側攔截 JS 的 console.warn 調用(無 JS 侵入)
寫法1:Weex JS 側的 console.warn 最終會通過 WXBridgeContext 的 handleJSLog 方法傳遞到 Native,無需解析最終日誌,直接 Hook 該方法攔截 warn 信息,精準匹配 Module 未註冊錯誤
#import <objc/runtime.h>
@implementation NSObject (WXJSLogHook)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 獲取WXBridgeContext類(無需頭文件)
Class bridgeContextClass = NSClassFromString(@"WXBridgeContext");
if (!bridgeContextClass) return;
// Hook處理JS日誌的核心方法:handleJSLog:
SEL handleJSLogSel = NSSelectorFromString(@"handleJSLog:");
Method originalMethod = class_getInstanceMethod(bridgeContextClass, handleJSLogSel);
if (!originalMethod) return;
SEL swizzledSel = NSSelectorFromString(@"weex_apm_handleJSLog:");
Method swizzledMethod = class_getInstanceMethod(self, swizzledSel);
class_addMethod(bridgeContextClass, swizzledSel, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
method_exchangeImplementations(originalMethod, swizzledMethod);
});
}
// Hook後的handleJSLog方法:攔截JS側的warn日誌
- (void)weex_apm_handleJSLog:(NSDictionary *)logInfo {
// 1. 先執行原方法,保留原有日誌輸出邏輯
[self weex_apm_handleJSLog:logInfo];
// 2. 解析JS日誌信息(logInfo格式:{level: 'warn', msg: 'xxx', ...})
NSString *logLevel = logInfo[@"level"];
NSString *logMsg = logInfo[@"msg"];
// 3. 精準匹配“未註冊Module”的warn
if ([logLevel isEqualToString:@"warn"] && [logMsg containsString:@"using unregistered weex module"]) {
// 提取Module名稱
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"using unregistered weex module \"(.*?)\"" options:0 error:nil];
NSTextCheckingResult *match = [regex firstMatchInString:logMsg options:0 range:NSMakeRange(0, logMsg.length)];
NSString *moduleName = match ? [logMsg substringWithRange:match.rangeAtIndex(1)] : @"";
// 4. 構造APM數據上報
NSDictionary *apmData = @{
@"error_type": @"weex_module_not_registered",
@"module_name": moduleName,
@"message": logMsg,
@"source": @"js_console_warn", // 標記來源:JS console.warn
@"timestamp": @([[NSDate date] timeIntervalSince1970] * 1000)
};
// 調用 APM SDK 接口,數據先落庫,後續統一按照數據上報策略,從本地 DB 撈取、聚合、上報
// [YourAPMManager reportWeexError:apmData];
}
}
@end
核心優勢
- 無侵入:無需修改 / 注入 JS 代碼,純 Native 側實現;
- 精準:攔截的是 JS 側傳遞到 Native 的原始日誌數據(而非最終打印的字符串),無格式誤差;
- 覆蓋全:所有 JS 側的console.warn都會經過此方法,100% 覆蓋 Module 未註冊場景
寫法二:由於 Weex 代碼是大量的存量業務代碼,很穩定。而且 Weex 官方好幾年不更新,所以我們內部私有化 Weex SDK,也就沒有采取 Hook 手段。而是直接修改源碼,WXBridgeContext.m 的 + (void)handleConsoleOutputWithArgument:(NSArray *)arguments logLevel:(WXLogFlag)logLevel 方法。比如:
+ (void)handleConsoleOutputWithArgument:(NSArray *)arguments logLevel:(WXLogFlag)logLevel
{
NSMutableString *string = [NSMutableString string];
[string appendString:@"jsLog: "];
[arguments enumerateObjectsUsingBlock:^(JSValue *jsVal, NSUInteger idx, BOOL *stop) {
[string appendFormat:@"%@ ", jsVal];
if (idx == arguments.count - 1) {
if (logLevel) {
if (WXLogFlagWarning == logLevel || WXLogFlagError == logLevel) {
if ([string containsString:@"using unregistered weex module"]) {
// 提取Module名稱
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"using unregistered weex module \"(.*?)\"" options:0 error:nil];
NSTextCheckingResult *match = [regex firstMatchInString:string options:0 range:NSMakeRange(0, string.length)];
NSString *moduleName = match ? [string substringWithRange:[match rangeAtIndex:1]] : @"";
// 接入收口工具類
NSString *exceptionMsg = [NSString stringWithFormat:@"JS require未註冊模塊:%@,原始日誌:%@", moduleName, string];
NSDictionary *customExt = @{@"moduleName": moduleName};
NSString *instanceId = [WXSDKEngine topInstance].instanceId ?: @"";
NSString *bundleUrl = [WXSDKEngine topInstance].scriptURL.absoluteString ?: @"";
[[WXExceptionReporter sharedInstance] reportExceptionWithCode:WXCustomExceptionCode_Module_NotRegistered
exceptionType:WXCustomExceptionType_Module
instanceId:instanceId
function:@"handleConsoleOutputWithArgument:logLevel:"
exceptionMsg:exceptionMsg
bundleUrl:bundleUrl
customExtParams:customExt];
}
id<WXAppMonitorProtocol> appMonitorHandler = [WXSDKEngine handlerForProtocol:@protocol(WXAppMonitorProtocol)];
if ([appMonitorHandler respondsToSelector:@selector(commitAppMonitorAlarm:monitorPoint:success:errorCode:errorMsg:arg:)]) {
[appMonitorHandler commitAppMonitorAlarm:@"weex" monitorPoint:@"jswarning" success:NO errorCode:@"99999" errorMsg:string arg:[WXSDKEngine topInstance].pageName];
}
}
WX_LOG(logLevel, @"%@", string);
} else {
[string appendFormat:@"%@ ", jsVal];
WXLogInfo(@"%@", string);
}
}
}];
}
2. JS 調用 Moudle 方法失敗
Native 註冊了一個負責邏輯的 Module,但是在 JS 側使用的時候,要麼方法名寫錯了,要麼參數少傳了,都可能導致預期的邏輯執行錯誤,發生不符合預期的行為。
1. 點擊事件工作原理
核心問題:點擊事件發生時,如何根據 Component 的點擊事件定位到該 Component 在 Vue DSL 中聲明的事件?
第一步:頁面初始化時,JS 側構建事件映射表。
Weex 頁面渲染時,會為每個組件做2件事情:
- 生成組件唯一標識:每個組件都有
ref/componentId/docId,類似組件身份證 - 綁定事件與方法:解析
@click="handleButtonClick"時,JS 會將「組件 ID + 事件類型(click)」作為 key,handleButtonClick作為 value,一起存進組件實例的映射表裏,(對應下面的this.event[type])
第二步:Native 側捕獲點擊,攜帶關鍵信息調用 fireEvent。
Native 側能拿到 componentId,是因為渲染組件時,JS 側會把組件 ID 同步給 Native 渲染引擎(WXComponent),Native 控件和 JS 組件實例通過 ID 一一綁定
第三步:JS 側調用 fireEvent 方法,其內部通過 ID + 事件類型 找方法。
- 定位組件實例:JS 通過 componentID(代碼裏的 this.ref)找到組件實例。
- 查找事件映射:從組件實例的
this.event里根據 type (如 click)找到具體的 eventDesc(包含具體的 handler) - 發起調用
handler.call
/**
* Fire an event manually.
* @param {string} type type
* @param {function} event handler
* @param {boolean} isBubble whether or not event bubble
* @param {boolean} options
* @return {} anything returned by handler function
*/
Element.prototype.fireEvent = function fireEvent (type, event, isBubble, options) {
var result = null;
var isStopPropagation = false;
var eventDesc = this.event[type];
if (eventDesc && event) {
var handler = eventDesc.handler;
event.stopPropagation = function () {
isStopPropagation = true;
};
if (options && options.params) {
result = handler.call.apply(handler, [ this ].concat( options.params, [event] ));
}
else {
result = handler.call(this, event);
}
}
if (!isStopPropagation
&& isBubble
&& (BUBBLE_EVENTS.indexOf(type) !== -1)
&& this.parentNode
&& this.parentNode.fireEvent) {
event.currentTarget = this.parentNode;
this.parentNode.fireEvent(type, event, isBubble); // no options
}
return result
};
2. JS 調用 module 方法,方法名錯誤
Native 註冊的 Module 方法名為 multiply:num2:callback:,而在 JS 側調用的時候方法名多加了幾個字符,造成方法名對不上,方法調用失敗的問題。
用户點擊屏幕上的 UI 控件(此處就是註冊 Component [WXSDKEngine registerComponent:@"color-button" withClass:[WXColorButtonComponent class]])。
Weex 統一給 Comonent 添加了分類來負責事件的處理。WXComponent+Events。源碼中 addClickEvent 就是添加了點擊事件的監聽。當發生點擊後會計算點擊事件的座標和時間戳信息,最後封裝一個 WXCallJSMethod 對象,方法名固定為 fireEvent。如下堆棧所示:
由於 logicCalculation 沒有對應的 multiplyWith 方法,所以會報錯,被 JS 的 try...catch... 捕獲後,通過 console.error 的方式輸出異常信息。但是 console.error 被 Native 接管了。所以我們可以在 Native 接管的地方統一攔截處理。只要日誌包含 Failed to invoke the event handler 就可以認為是因為方法名問題,導致調用方法出錯
代碼如下:
+ (void)handleConsoleOutputWithArgument:(NSArray *)arguments logLevel:(WXLogFlag)logLevel
{
// ...
if ([string containsString:@"Failed to invoke the event handler"]) {
// 原有解析邏輯保留
NSString *errorMethodName = @"";
NSString *eventType = @"";
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"'\\.\\.\\.(?:logicCalculation\\.)([a-zA-Z0-9_]+)\\.\\.\\." options:0 error:nil];
NSTextCheckingResult *match = [regex firstMatchInString:string options:0 range:NSMakeRange(0, string.length)];
if (match) {
errorMethodName = [string substringWithRange:[match rangeAtIndex:1]];
}
NSRegularExpression *eventRegex = [NSRegularExpression regularExpressionWithPattern:@"event handler of \"([^\"]+)\"" options:0 error:nil];
NSTextCheckingResult *eventMatch = [eventRegex firstMatchInString:string options:0 range:NSMakeRange(0, string.length)];
if (eventMatch) {
eventType = [string substringWithRange:[eventMatch rangeAtIndex:1]];
}
// 接入收口工具類
NSString *exceptionMsg = [NSString stringWithFormat:@"Module方法名錯誤:%@,事件類型:%@,原始日誌:%@", errorMethodName, eventType, string];
NSDictionary *customExt = @{
@"moduleName": @"logicCalculation",
@"methodName": errorMethodName,
@"eventType": eventType
};
NSString *instanceId = [WXSDKEngine topInstance].instanceId ?: @"";
NSString *bundleUrl = [WXSDKEngine topInstance].scriptURL.absoluteString ?: @"";
[[WXExceptionReporter sharedInstance] reportExceptionWithCode:WXCustomExceptionCode_Module_MethodNotFound
exceptionType:WXCustomExceptionType_Module
instanceId:instanceId
function:@"handleConsoleOutputWithArgument:logLevel:"
exceptionMsg:exceptionMsg
bundleUrl:bundleUrl
customExtParams:customExt];
}
// ...
}
效果如下
3. JS 調用 module 方法,方法參數個數不匹配
上面已經講了點擊事件的工作流程,調用方法時,除了調用了不存在的方法或者方法名寫錯了,還有一種情況就是參數個數不匹配。
這種情況如何識別並監控?
JS 的事件處理函數裏,調用註冊的 Module 和對應的方法,會統一走到 WXJSCoreBridge.mm 的 - (void)registerCallNativeModule:(WXJSCallNativeModule)callNativeModuleBlock 給當前的 JSContext 註冊好的 callNativeModule 回調裏。_jsContext[@"callNativeModule"] = ^JSValue *(JSValue *instanceId, JSValue *moduleName, JSValue *methodName, JSValue *args, JSValue *options) 可以拿到模塊名、方法名、參數個數、instanceID 等。拿到實際傳遞的方法參數列表,再通過模塊名根據 ModuleFactory 找到模塊類對象,然後利用 runtime 能力,遍歷類對象的方法列表,找到對應的 SEL,判斷其預期的方法參數個數,然後再和實際傳遞過來的方法參數個數做比較即可
- (void)registerCallNativeModule:(WXJSCallNativeModule)callNativeModuleBlock
{
// JS 調用 Native 的方法都會走這裏。可以解析到:模塊名、方法名、參數數組等信息。可以在這裏判斷方法參數個數是否相同。
_jsContext[@"callNativeModule"] = ^JSValue *(JSValue *instanceId, JSValue *moduleName, JSValue *methodName, JSValue *args, JSValue *options) {
// ...
};
}
在其 callNativeModule 的 block 裏,增加一個方法,專門用來判斷和檢查方法參數個數是否匹配的問題
// 輔助方法:校驗Module方法參數個數
- (void)checkModuleParamCount:(NSString *)moduleName
methodName:(NSString *)methodName
actualParams:(NSArray *)actualParams
instanceId:(NSString *)instanceId {
// 1. 跳過空值/系統模塊(避免無意義校驗)
if (!moduleName || !methodName || actualParams.count < 0) return;
Class moduleClass = [WXModuleFactory classWithModuleName:moduleName];
if (!moduleClass) return;
// 2. 拼接完整的方法選擇器(Weex Module方法名帶冒號,需補全,如multiply→multiply:num2:callback:)
// 注:若方法名規則固定,可通過模塊類的方法列表獲取所有selector,匹配前綴
SEL targetSel = nil;
unsigned int methodCount = 0;
Method *methods = class_copyMethodList(moduleClass, &methodCount);
for (int i = 0; i < methodCount; i++) {
Method method = methods[i];
SEL sel = method_getName(method);
NSString *selStr = NSStringFromSelector(sel);
// 匹配前綴(如multiply開頭的方法)
if ([selStr hasPrefix:methodName]) {
targetSel = sel;
break;
}
}
free(methods);
if (!targetSel) return;
// 3. 解析方法簽名,計算預期參數個數(減self/_cmd)
NSMethodSignature *methodSig = [moduleClass instanceMethodSignatureForSelector:targetSel];
NSInteger weexParamCount = methodSig.numberOfArguments - 2;
// 4. 判斷參數個數是否不匹配
if (actualParams.count != weexParamCount) {
// 構造錯誤信息
NSString *errorMsg = [NSString stringWithFormat:@"Module:%@ 方法:%@ 參數個數不匹配,預期%ld個,實際%ld個",
moduleName, methodName, weexParamCount, actualParams.count];
WXLogError(@"[WeexParamError] %@", errorMsg);
// 5. 上報APM(核心:生產環境監控)
NSDictionary *apmData = @{
@"error_type": @"weex_module_param_count_mismatch",
@"module_name": moduleName,
@"method_name": methodName,
@"expected_count": @(weexParamCount),
@"actual_count": @(actualParams.count),
@"actual_params": actualParams,
@"instance_id": instanceId ?: @"",
@"timestamp": @([[NSDate date] timeIntervalSince1970] * 1000),
@"message": errorMsg
};
// APM:異步上報,避免阻塞JS橋接
NSLog(@"APM 數據上報通道,【JS 通過 Module 調用 Native 方法,參數個數不匹配】:%@", apmData);
}
}
效果如下:
5. Vue 層面異常
Weex 底層依靠 Vue 實現,差異化就是 VM 去通過 Bridge 在 WeexSDK Native 去做繪製。異常方面除了常規的 JS 運行時異常(如語法錯誤、類型錯誤等 7 種),Vue 框架自身的邏輯層、編譯層、響應式系統、組件生命週期 等環節會拋出專屬異常,這些異常必須通過 Vue.config.errorHandler 兜底。
分析 Weex 源碼中:packages/weex-js-framework/index.js/
function handleError (err, vm, info) {
if (vm) {
var cur = vm;
while ((cur = cur.$parent)) {
var hooks = cur.$options.errorCaptured;
if (hooks) {
for (var i = 0; i < hooks.length; i++) {
try {
var capture = hooks[i].call(cur, err, vm, info) === false;
if (capture) { return }
} catch (e) {
globalHandleError(e, cur, 'errorCaptured hook');
}
}
}
}
}
globalHandleError(err, vm, info);
}
function globalHandleError (err, vm, info) {
if (config.errorHandler) {
try {
return config.errorHandler.call(null, err, vm, info)
} catch (e) {
logError(e, null, 'config.errorHandler');
}
}
logError(err, vm, info);
}
源碼中 nextTick、Vue.prototype.$emit、callHook、Watcher.prototype.get、Watcher.prototype.run、renderRecyclableComponentTemplate、Vue.prototype._render 等等都調用了 handleError 方法。
Vue 內部對部分異常做了封裝/攔截,避免直接冒泡到全局(防止阻斷應用整體運行),但會通過 errorHandler 暴露出來。
舉個例子,WeexAPM 類可以封裝為:
/**
* APM
*/
class WeexAPM {
/**
* 獲取當前的葉子節點
* @param {*} Vue vm
* @returns 當前組件名稱
*/
formatComponentName (vm) {
if (vm.$root === vm) return 'root'
var name = vm._isVue
? (vm.$options && vm.$options.name) ||
(vm.$options && vm.$options._componentTag)
: vm.name
return (
(name ? 'component <' + name + '>' : 'anonymous component') +
(vm._isVue && vm.$options && vm.$options.__file
? ' at ' + (vm.$options && vm.$options.__file)
: '')
)
}
/**
* 處理Vue錯誤提示
*/
monitor (Vue) {
if (!Vue) {
return
}
// 錯誤處理
Vue.config.errorHandler = (err, vm, info) => {
let componentName = 'unknown'
if (vm) {
componentName = this.formatComponentName(vm)
}
let errorInfo = {
name: err.name,
reason: err.message,
callStack: err.stack,
componentName: componentName,
info: info,
level: 'VUE_ERROR'
}
try {
const weexAPMUploader = weex.requireModule('weexAPMUploader')
weexAPMUploader.uploadException(errorInfo)
} catch (error) {
console.error('APMMonitor 能力有問題,請檢查是否註冊了weexAPMUploader模塊' + error)
}
}
}
}
export default WeexAPM
在捕獲到 Vue 層面的異常時,可以調用註冊好的 weexAPMUploader module 能力,將數據傳輸到 Native 側,由 Native 側進行統一的參數組裝,最後調用 APM SDK 的能力進行數據寫入數據庫、按照策略上報到 APM 服務端進行消費。
模擬產生 Vue 層級的錯誤:給一個字符串類型的數據,在計算屬性裏調用 toFixed 方法。按鈕的點擊事件裏將數據改為字符串,則會報錯。
可以看到被 Vue.config.errorHandler 捕獲了,後續交給 Native 處理即可。