精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

Flutter 全埋點(diǎn)的實(shí)現(xiàn)

開(kāi)發(fā) 前端
使用 Dart AOP 實(shí)現(xiàn)的 Flutter App 全埋點(diǎn)功能具有多重優(yōu)勢(shì)。首先,它不依賴于業(yè)務(wù)層,可以在端上自動(dòng)采集并上報(bào)數(shù)據(jù),從而不會(huì)對(duì)業(yè)務(wù)代碼造成額外的負(fù)擔(dān)。

一、前言

目前,F(xiàn)lutter App(以下簡(jiǎn)稱(chēng) App)的全量日志的模塊埋點(diǎn)功能采用業(yè)務(wù)層手動(dòng)埋點(diǎn)的方式實(shí)現(xiàn),這種方式不僅增加了研發(fā)成本,同時(shí)也限制了后續(xù)的擴(kuò)展和維護(hù)。因此,可以基于 Dart AOP 實(shí)現(xiàn) Flutter 全埋點(diǎn)功能來(lái)補(bǔ)齊全量日志。該方式不依賴于業(yè)務(wù)層,可以在端上自動(dòng)采集并上報(bào)數(shù)據(jù),并通過(guò)一定規(guī)則篩選出所需數(shù)據(jù),用于分析和模擬用戶行為,幫助排查線上疑難問(wèn)題。這種方法不僅能夠提高我們的效率,而且能夠加快問(wèn)題的排查速度,從而提高 App 的穩(wěn)定性。

二、實(shí)現(xiàn)原理

隨著 App 的不斷迭代,項(xiàng)目復(fù)雜度也不斷提升。在該過(guò)程中,為了準(zhǔn)確找出問(wèn)題并排查,我們需要使用一些技術(shù)手段來(lái)輔助。在 Flutter 方面,Hook 能力是 App 缺少的基礎(chǔ)能力之一。因此,實(shí)現(xiàn)一套通用的 Dart AOP 基礎(chǔ)工具變得尤為重要。我們可以在關(guān)鍵的代碼調(diào)用點(diǎn)注入自定義邏輯,以實(shí)現(xiàn)數(shù)據(jù)收集、性能監(jiān)控等功能,這種切面編程的技術(shù)被稱(chēng)為 AOP(Aspect-Oriented Programming),它可以幫助我們更好地管理和組織代碼,提高代碼的可維護(hù)性和復(fù)用性。

前端編譯

要想實(shí)現(xiàn)  Flutter 側(cè) Hook 能力,首先要簡(jiǎn)單了解一下前端編譯。

圖片圖片

CFE(Common Front-End):通用前端編譯器,當(dāng)執(zhí)行 Dart 代碼時(shí),通過(guò)詞法分析(Scanner)和語(yǔ)法分析(parser)構(gòu)建一顆 AST(Component)樹(shù),再經(jīng)過(guò)一系列的 Transformer 優(yōu)化(TFA、Desugaring、Tree Shaking)后,將優(yōu)化后的 AST 樹(shù)二進(jìn)制寫(xiě)入到 Dill 文件中;

TFA(Type Flow Analysis):全局類(lèi)型流分析和相關(guān)轉(zhuǎn)換,比如簡(jiǎn)化參數(shù)傳遞等;

Desugaring:語(yǔ)法脫糖,比如將 Async/Await 轉(zhuǎn)換成基于 Future 實(shí)現(xiàn);

Tree Shaking:樹(shù)搖,從 Kernel 產(chǎn)物中摘除未使用的 Classes、Procedures、Fields等;

AST (Abstract Syntax Tree):抽象語(yǔ)法樹(shù),是一種用于表示源代碼結(jié)構(gòu)的樹(shù)形結(jié)構(gòu),每個(gè)節(jié)點(diǎn)代表一個(gè)語(yǔ)法單元,例如表達(dá)式、函數(shù)、變量等。它在編譯器和解釋器中扮演著非常重要的角色,是代碼優(yōu)化、代碼轉(zhuǎn)換和運(yùn)行的基礎(chǔ)。通過(guò)構(gòu)建 AST,我們可以對(duì)代碼的結(jié)構(gòu)和語(yǔ)義進(jìn)行全面的分析和處理,同時(shí)也為開(kāi)發(fā)人員提供了一種理解代碼表達(dá)方式和程序執(zhí)行方式的框架,簡(jiǎn)單看下 Component 結(jié)構(gòu)。Dart 2.18.6 AST 源碼點(diǎn)這里。

圖片圖片

frontend_server.dart 前端編譯關(guān)鍵偽代碼如下:

Future<bool> compile() {
// 1.kernelForProgram(source)源碼編譯為AST樹(shù)
// 詞法分析、語(yǔ)法分析、構(gòu)建AST Outline
 summaryComponent = await kernelTarget.buildOutlines(...);
// 構(gòu)建完整AST樹(shù)
 component = await kernelTarget.buildComponent(...);
// 2.運(yùn)行優(yōu)化transformer:TFA、Desugaring、Tree Shaking
 result = await runGlobalTransformations(component);
// 3. 序列化為二進(jìn)制
await writeDillFile(result);
}
  1. 執(zhí)行 Dart 代碼時(shí),先進(jìn)行詞法分析和語(yǔ)法分析來(lái)構(gòu)建 AST Outline,接著第二次會(huì)構(gòu)建完整 AST;
  2. 運(yùn)行語(yǔ)法糖脫糖、Tree-shaking 和 TFA 等來(lái)進(jìn)行優(yōu)化;
  3. 將優(yōu)化后的 AST 二進(jìn)制寫(xiě)入 Dill 文件中。

Dart AOP

設(shè)計(jì)思路

通過(guò)對(duì)前端編譯流程的簡(jiǎn)單梳理,我們已經(jīng)知道要想實(shí)現(xiàn)編譯期的 Dart 切面能力,需要在 Transfromer 優(yōu)化之前注入 AOP 能力,因?yàn)?Transfromer 優(yōu)化中會(huì)發(fā)生 Tree Shaking,如果在此之后才注入可能會(huì)因?yàn)闆](méi)有用到而被樹(shù)搖搖掉。設(shè)計(jì)流程如下:

圖片圖片

  1. Dart 編譯成 Kernel 前注入自定義 AopTransformer,通過(guò) AopTransformer 提取自定義注解信息,遍歷 AST 節(jié)點(diǎn),對(duì)注解中聲明的節(jié)點(diǎn)進(jìn)行修改;
  2. 編譯 host_release,生成新的 frontend_server.dart.snapshot 來(lái)替換 App 對(duì)應(yīng) SDK 的原前端編譯器快照;
  3. 針對(duì)原方法新建一個(gè)帶有切面注解信息的 Hook 方法,當(dāng)程序執(zhí)行到原方法時(shí),其實(shí)執(zhí)行的是對(duì)應(yīng)的樁方法。

注意:AOP 之前,B 方法調(diào)用 A 方法:B -> A。

圖片圖片

支持的 Hook 方式有兩種:

圖片圖片

閑魚(yú)有一套開(kāi)源的面向 Dart 的 AOP 框架 AspectD,不直接使用它的原因如下:

  • AspectD 支持的 SDK 版本過(guò)低且對(duì)外不再維護(hù),當(dāng) Flutter SDK 升級(jí)到 3.3.10 后,AST 中的部分 API 發(fā)生了較大變更,其中代碼生成相關(guān)邏輯需要進(jìn)行較大的調(diào)整來(lái)適配新 API,無(wú)法直接使用;
  • AspectD 沒(méi)有支持空安全(Null Safety)這個(gè)很重要的語(yǔ)法特性;
  • 缺少調(diào)用方的作用域能力:實(shí)際開(kāi)發(fā)中可能存在這樣一種場(chǎng)景,插件 A 和 插件 B 都有打印功能,只想 Hook 插件 B 的打印的話,目前缺少這個(gè)能力;
  • 方法調(diào)用替換會(huì)生成重復(fù)的樁方法:不同的調(diào)用方執(zhí)行同一個(gè)原始方法的調(diào)用替換(Call)時(shí),生成了多個(gè)重復(fù)的樁方法,應(yīng)只保留一個(gè)樁方法即可;
  • AspectD 使用 Flutter_tools 調(diào)用工具鏈較為繁瑣,可以直接編譯并替換前端編譯器快照,化繁為簡(jiǎn)。

方案描述可能比較抽象,可以參考以下 Demo 來(lái)加深理解。

分別使用 @Call 和 @Execute 注解對(duì) hello() 方法執(zhí)行切面操作:

圖片圖片

打印日志信息:

圖片圖片

圖片圖片

偽代碼如下:

圖片圖片

圖片圖片

技術(shù)難點(diǎn)

調(diào)用方的作用域能

App 中,插件 A 和插件 B 里都有打印功能,但若只想對(duì)插件 B 的打印進(jìn)行 hook,那就必須可精細(xì)化的控制 hook 范圍。根據(jù)上面的原理分析,@Execute 修改了原方法,插樁后只有一個(gè)變更點(diǎn),保證了所有方法都能被 hook 到,所以無(wú)法支持調(diào)用方的作用域能力,無(wú)法精準(zhǔn)控制 hook 范圍;而 @Call 不會(huì)修改原方法,只是替換了方法調(diào)用點(diǎn),即將原方法調(diào)用替換為 hook 方法調(diào)用,所以插樁 N 次就會(huì)生成 N 個(gè)變更點(diǎn)。因此,在方法調(diào)用替換前首先判斷當(dāng)前 class 的 uri,通過(guò)正則匹配定義的 scope,如果滿足,才可以進(jìn)行插樁。

可選參數(shù)的默認(rèn)值

在經(jīng)過(guò) AOP 之后,B 方法調(diào)用 A 方法時(shí)會(huì)經(jīng)過(guò)一層代理,也就是我們的 Hook 方法,然后才會(huì)調(diào)用到 A 方法,這個(gè)過(guò)程中就存在了對(duì)原方法參數(shù)的傳遞。

為了能夠把參數(shù)傳遞給原方法,在調(diào)用點(diǎn)進(jìn)行替換時(shí),會(huì)構(gòu)造一個(gè) PointCut 對(duì)象,將位置參數(shù)放入到 PointCut 對(duì)象的 List 屬性中,將命名參數(shù)放入到 PointCut 對(duì)象的 Map 屬性中,然后將 PointCut 對(duì)象作為參數(shù)傳遞給 Hook 方法。在替換方法調(diào)用時(shí),還會(huì)為 PointCut 生成一個(gè) Stub 樁方法,而這個(gè) Stub 方法則是調(diào)用原來(lái)的 A 方法,即通過(guò) A 方法參數(shù)列表定義,在 Stub 方法中分別取出 PointCut 對(duì)象的 List 屬性和 Map 屬性中存儲(chǔ)的實(shí)參,來(lái)拼接成 A 方法調(diào)用所需的 Arguments,然后在 Stub 方法中生成 A 方法調(diào)用的 Invocation。

所以,最終方法調(diào)用的實(shí)參都會(huì)存儲(chǔ)到 PointCut 對(duì)象的 List 屬性與 Map 屬性中,然后在 Stub 方法中取出并回調(diào)原方法。這種方式本身沒(méi)有問(wèn)題,但是當(dāng)參數(shù)是可選參數(shù)時(shí)就會(huì)出現(xiàn)問(wèn)題。假如 A 方法中的參數(shù) a 是可選參數(shù),默認(rèn)值是 "hello world",B 方法在調(diào)用 A 方法時(shí)并沒(méi)有為可選參數(shù) a 傳值,理論上可選參數(shù) a 的值是默認(rèn)值 "hello world",但是 Stub 方法生成 Invocation 時(shí),是通過(guò) A 方法的參數(shù)列表定義去拼接參數(shù)的,這里會(huì)存在一定變數(shù)。

由于 B 方法沒(méi)有傳入可選參數(shù) a,當(dāng) PointCut 對(duì)象構(gòu)造時(shí),Map 屬性中并沒(méi)有存入可選參數(shù) a,所以,Stub 方法在拼接參數(shù)時(shí),從 Map 屬性中獲取的可選參數(shù) a 的值將是 null,這個(gè) null 值是作為 Arguments 中的一員,這樣最終的 A 方法調(diào)用將會(huì)使用 null 值,而不是默認(rèn)值 "hello world"。

為了解決這個(gè)問(wèn)題,需要在 Stub 方法中生成 A 方法調(diào)用所需的 Arguments 時(shí),對(duì) PointCut 對(duì)象的 Map 屬性中的參數(shù)進(jìn)行判斷。通過(guò) A 方法參數(shù)列表定義從 Map 屬性中提取實(shí)參時(shí),先判斷對(duì)應(yīng)參數(shù)是否為可選參數(shù),如果是可選參數(shù),通過(guò) Map 的 containsKey() 方法來(lái)判斷 Map 屬性中是否存在該可選參數(shù)。假如這個(gè)參數(shù)是可選參數(shù),而且 Map 屬性中也不存在該參數(shù),那么我們接下來(lái)該怎么辦呢?其實(shí),我們?cè)诒闅v A 方法的參數(shù)列表定義時(shí),可以獲取到對(duì)應(yīng)參數(shù)的變量聲明,通過(guò)這個(gè)變量聲明可以獲取到對(duì)應(yīng)初始值的表達(dá)式。假如 Map 屬性中不包含對(duì)應(yīng)的可選參數(shù),我們可以使用對(duì)應(yīng)可選參數(shù)的初始值表達(dá)式拼接到 Arguments 中,這樣就保證了 Arguments 是固定的,也保證了可選參數(shù)在沒(méi)有傳值的情況下依舊可以使用到默認(rèn)值。

總結(jié):判斷 Map 屬性中是否存在可選參數(shù)時(shí),我們需要先構(gòu)造出 Map 對(duì)象的 containsKey() 的 Invocation,然后再構(gòu)建條件表達(dá)式(ConditionalExpression),將 containsKey() 的 Invocation 作為條件值,條件表達(dá)式兩個(gè)分支分別放入 Map 取值的表達(dá)式與可選參數(shù)初始值的表達(dá)式。

圖片圖片

重復(fù)的樁方法

方法調(diào)用替換時(shí),不同調(diào)用方執(zhí)行同一個(gè)原方法的調(diào)用替換時(shí),都會(huì)生成一個(gè) Stub 方法,以便 pointCut.proceed() 能夠通過(guò) Stub 方法來(lái)回調(diào)原方法。

假如,一個(gè)方法有 N 個(gè)調(diào)用點(diǎn),那么我們就要為每個(gè)調(diào)用點(diǎn)都生成一個(gè) Stub 方法,這顯然不合理,因?yàn)槎际菍?duì)同一個(gè)方法的調(diào)用,且方法調(diào)用所需的 Arguments 都是通過(guò) PointCut 對(duì)象的 List 屬性與 Map 屬性中取出來(lái)拼接的,所以眾多的方法調(diào)用其實(shí)都可以復(fù)用一個(gè) Stub 方法來(lái)完成原方法的回調(diào)。

圖片圖片

三、全埋點(diǎn)

用戶操作路徑

當(dāng)用戶觸發(fā)點(diǎn)擊事件時(shí),我們可以通過(guò)命中點(diǎn)擊的最小 Widget 來(lái)回溯出該 Widget 在樹(shù)中的層次結(jié)構(gòu);通過(guò)獲取到的層次結(jié)構(gòu),我們可以去除中間無(wú)效和冗余的組件路徑,并按照一定的拼接規(guī)則來(lái)獲取用戶的操作路徑。簡(jiǎn)言之,當(dāng)用戶點(diǎn)擊某個(gè) Widget 時(shí),我們可以追蹤到它在 Widget 樹(shù)中的位置,并根據(jù)這個(gè)位置信息剔除無(wú)效和重復(fù)的組件路徑,從而得到有效的用戶操作路徑。這種操作路徑的獲取方法可以幫助我們了解用戶在 App 中的具體操作流程,從而更好地理解和分析用戶行為,更準(zhǔn)確更及時(shí)的定位問(wèn)題。

路徑追蹤

關(guān)鍵字段的拼接規(guī)則如下:

  • 用戶操作路徑:控件類(lèi):Dart文件名:行數(shù):列數(shù);
  • 組件路徑 ID (從根節(jié)點(diǎn)到子節(jié)點(diǎn)):Widget 名字[位置]/ ... / Widget 名字[位置]。

源碼分析

BuildContext 定義了一些如獲取 State、Widget、RenderObject、父子 Element 等重要的接口;Element 實(shí)現(xiàn)了 BuildContext 中的關(guān)鍵方法,比如實(shí)現(xiàn)了 visitAncestorElements (訪問(wèn)祖先元素)方法等,且通過(guò) Element.Widget 獲取與之對(duì)應(yīng)的 Widget,根據(jù)此 Widget 可獲取到具體路徑;RenderObjectElement 繼承 Element,在 mount() 方法中初始化 _renderObject 對(duì)象;在 mount() 和 update() 方法中,通過(guò)斷言將當(dāng)前 Element 傳入到 renderObject 的 debugCreator 屬性中保存。因此,可以通過(guò) debugCreator 屬性獲取到對(duì)應(yīng)的 Element,再通過(guò) Element 獲取到對(duì)應(yīng)的 Widget。由于 debugCreator 屬性賦值定義在斷言中,只在Debug 模式時(shí)能獲取到 Widget,因此需要分別 Hook mount() 和 update() 方法來(lái)支持 Release 和 Profile 模式時(shí)獲取對(duì)應(yīng) Widget 信息的能力。

圖片圖片

關(guān)鍵實(shí)現(xiàn)

  • Release 和 Profile 模式創(chuàng)建 DebugCreator

圖片圖片

  • 組件路徑優(yōu)化

Widget_Inspctor 在 Debug 模式的編譯期間,通過(guò)一個(gè)特定的 Transform,讓最底層 Widget 實(shí)現(xiàn)了抽象類(lèi) xxHasCreationLocation,在 Widget 所有子類(lèi)的構(gòu)造方法中新增一個(gè) xxLocation 類(lèi)型的命名參數(shù),同時(shí)會(huì)修改對(duì)應(yīng)的構(gòu)造方法調(diào)用點(diǎn)即傳入 xxLocation 對(duì)象,最終可通過(guò) Widget 對(duì)象獲取到 Widget 構(gòu)造時(shí)所在文件路徑和代碼行數(shù)?;诖耍梢栽诜?Debug 模式復(fù)用此邏輯(為了保留 Debug 模式時(shí)本身支持的 Dev-Tools 能力,Debug 模式不做修改)

修改源碼 track_widget_constructor_locations.dart

圖片圖片

當(dāng)前 Element 是否添加到 Path 中,用于去除中間無(wú)效冗余的組件路徑:

圖片圖片

事件與手勢(shì)

理解手勢(shì)

PointerEvent(指針事件)表示用戶交互的原始觸摸數(shù)據(jù),例如 PointerDownEvent、PointerCancelEvent、PointerUpEvent 等;當(dāng)手指觸摸屏幕的時(shí)候,發(fā)生觸摸事件,F(xiàn)lutter 會(huì)確定觸發(fā)的位置上有哪些組件,并將觸摸事件交給最內(nèi)層的組件去響應(yīng),事件會(huì)從最內(nèi)層的組件開(kāi)始,沿著組件樹(shù)向根節(jié)點(diǎn)向上一級(jí)級(jí)冒泡分發(fā)。

處理 PointerEvent 是從 GestureBinding 的 handlePointerEvent() 方法開(kāi)始:

圖片圖片

  1. 創(chuàng)建 HitTestResult 對(duì)象:PointerEvent 為 PointerDownEvent、PointerSignalEvent、PointerHoverEvent、PointerPanZoomStartEvent 時(shí)創(chuàng)建 HitTestResult 對(duì)象,該對(duì)象內(nèi)部有一個(gè) _path 字段,表示 HitTestEntry 集合。
  2. 命中測(cè)試,調(diào)用 RendererBinding 的 hitTest() 方法:調(diào)用 hitTest() 方法進(jìn)行命中測(cè)試,該方法將自身作為參數(shù)創(chuàng)建 HitTestEntry 對(duì)象,然后將 HitTestEntry 對(duì)象添加到 HitTestResult 的 _path 中,HitTestEntry 中只有 HitTestTarget 屬性字段。即創(chuàng)建的 HitTestEntry 添加到 HitTestResult 的 _path 中,被當(dāng)做事件分發(fā)冒泡排序中的一個(gè)路徑節(jié)點(diǎn)。

圖片圖片

  1. 調(diào)用 RenderView 的 hitTest() 方法(從根節(jié)點(diǎn) RenderView 開(kāi)始命中測(cè)試);
  2. 調(diào)用父類(lèi)的 hitTest() 方法,即 GestureBinding 的 hitTest() 方法。
  1. 事件分發(fā):經(jīng)過(guò)一系列的 hitTest 后,調(diào)用到 GestureBinding 的 dispatchEvent() 方法。

圖片圖片

dispatchEvent() 方法遍歷 _path 中的每個(gè) HitTestEntry,取出其 target 進(jìn)行事件分發(fā),而 HitTestTarget 除了幾個(gè)Binding,其具體都是由 RenderObject 實(shí)現(xiàn)的,所以也就是對(duì)每個(gè) RenderObject 節(jié)點(diǎn)進(jìn)行事件分發(fā),也就是我們說(shuō)的“事件冒泡”,冒泡的第一個(gè)節(jié)點(diǎn)是最小 child 節(jié)點(diǎn)(最內(nèi)部的組件),最后一個(gè)是 GestureBinding。

所以,handlePointerEvent() 方法主要就是不斷通過(guò) hitTest() 方法計(jì)算出所需的 HitTestResult,然后再通過(guò) dispatchEvent() 對(duì)事件進(jìn)行分發(fā)。

關(guān)鍵實(shí)現(xiàn)

通過(guò)分析手勢(shì)事件,選擇以下兩個(gè)切入點(diǎn):

  • 獲取到點(diǎn)擊的控件:通過(guò)攔截 GestureBinding 的 dispatchEvent() 方法,獲取到傳給該方法的 PointerEvent 和 HitTestResult 參數(shù);
  • 攔截點(diǎn)擊事件:攔截 GestureRecognizer 中的 invokeCallback() 方法,可以通過(guò)傳遞的參數(shù),得到是不是點(diǎn)擊狀態(tài)(判斷 eventName == "onTap")。

圖片圖片

業(yè)務(wù)信息

即使我們獲取了用戶的操作路徑信息,如果缺少關(guān)鍵業(yè)務(wù)代碼,也無(wú)法快速排查問(wèn)題。因此,在全埋點(diǎn)中,我們需要上報(bào)與業(yè)務(wù)流程相關(guān)的日志。為了避免對(duì)業(yè)務(wù)層代碼的侵入,我們可以通過(guò) Hook 來(lái)獲取業(yè)務(wù)內(nèi)容,并將其上傳到全量日志。那么,如何獲取業(yè)務(wù)信息呢?

設(shè)計(jì)思路

以下敘述均以新版 Bloc 為例。

在 App 中,存在多種設(shè)計(jì)模式。以新版 Bloc 為例,與業(yè)務(wù)相關(guān)的信息保存在一個(gè) State 類(lèi)中。我們可以通過(guò)獲取當(dāng)前 State 對(duì)象中的所有信息來(lái)還原模擬用戶操作。然而,F(xiàn)lutter 缺少動(dòng)態(tài)能力,無(wú)法通過(guò)反射機(jī)制動(dòng)態(tài)獲取 State 對(duì)象的所有信息。因此,我們可以為每個(gè) State 對(duì)象生成 toString() 方法,以獲取對(duì)象中的所有信息(方法返回的是 Map 對(duì)象轉(zhuǎn)成的字符串)。然而,手動(dòng)編寫(xiě)大量的 toString() 代碼不僅侵入了業(yè)務(wù)層代碼,而且效率極低。為了解決這些問(wèn)題,我們可以嘗試在編譯期提前生成 State 對(duì)象的 toString() 方法,以更高效地獲取業(yè)務(wù)流程信息。當(dāng) Hook 方法被調(diào)用時(shí),我們可以通過(guò)調(diào)用 toString() 方法獲取到 State 對(duì)象所有信息并上報(bào)。

如何判斷當(dāng)前的類(lèi)是否為需要的 State 類(lèi)呢?

  1. 自定義 CreateToStringMethodVisitor 繼承 Transformer,重寫(xiě)訪問(wèn)實(shí)例調(diào)用(visitInstanceInvocation)方法;
  2. 遍歷 AST,獲取當(dāng)前實(shí)例調(diào)用 methodInvocation 的接口目標(biāo)引用(interfaceTargetReference)的節(jié)點(diǎn) node;
  3. 判斷該節(jié)點(diǎn)如果為 Procedure,獲取到它的 Class 和 Library,從而獲得 importUri、clsName、methodName;
  4. 由于 State 沒(méi)有明顯的繼承關(guān)系,無(wú)法直接判斷出一個(gè)類(lèi)是否為 State,所以從 Emit 方法調(diào)用點(diǎn)出發(fā),通過(guò) Emit 方法調(diào)用點(diǎn)傳入的參數(shù)來(lái)獲取 State 對(duì)應(yīng)的類(lèi),這么可分別對(duì)比 ImportUri、clsName、methodName 和新版 Bloc 的 Emit() 方法所在的類(lèi)、Import 名字 和 Call() 方法所在的類(lèi)、Import 名字,完全匹配則說(shuō)明找到了 State 類(lèi)的實(shí)力調(diào)用遍歷實(shí)例調(diào)用的位置參數(shù)列表中的表達(dá)式,根據(jù)表達(dá)式不同的類(lèi)型獲取到對(duì)應(yīng)的 state 的 Class;
  5. 遍歷 stateClass 的 Procedures,如果沒(méi)有 toStringProcedure,為當(dāng)前 StateClass 生成 toStringProcedure 并插入到 Procedures 中。

如何生成 toStringProcedure 呢?

  1. 初始化一個(gè)空數(shù)組,里面存放的是映射文字條目(MapLiteralEntry)。
  2. 遍歷 StateClass 的 Fields,根據(jù)當(dāng)前 Field 生成一個(gè) Key 為 Field 名字,Value 為 Field 表達(dá)式的 MapLiteralEntry,添加到 MapLiteralEntry 數(shù)組中。
  3. 如果 stateClass 有父類(lèi),需要循環(huán)向上找到 Field 并生成對(duì)應(yīng)的 MapLiteralEntry 添加到數(shù)組中。
  4. 數(shù)組 MapLiteralEntry 轉(zhuǎn)成 MapLiteral,創(chuàng)建 toStringMap實(shí)例調(diào)用 并包裝成帶有返回值的描述 Statement,通過(guò)這個(gè)描述 創(chuàng)建 FunctionNode,通過(guò) FunctionNode 創(chuàng)建 toStringProcedure,添加到 StateClass 的 Procedures 中。

注意:需要存在一個(gè) toStringProcedure 模版,不會(huì)憑空創(chuàng)建。

圖片圖片

關(guān)鍵實(shí)現(xiàn)

  1. 通過(guò)對(duì)象和屬性定義獲取對(duì)象屬性,即 StateClass 屬性保存的 Field 對(duì)象。
  2. 如果當(dāng)前 Field 對(duì)象是數(shù)組的話,打印出來(lái)的會(huì)是 Instance of xxxModel,我們需要獲取 xxxModel 內(nèi)部信息,所以需要對(duì) xxxModel 進(jìn)行 toJson()。
  3. 根據(jù)當(dāng)前 Field 生成一個(gè) Key 為 Field 名字,Value 為 Field 表達(dá)式的 MapLiteralEntry,添加到 MapLiteralEntry 數(shù)組中。
  4. 如果屬性定義對(duì)象為空,那么選擇以上生成的實(shí)例方法調(diào)用,否則使用 Field 對(duì)象即可。

圖片圖片

圖片圖片

最終效果

圖片圖片

圖片圖片

四、其他收益

Dart AOP 用途有很多,也可以解決疑難 Crash。比如前段時(shí)間,有一個(gè)線上疑難 Crash 問(wèn)題持續(xù)影響了多個(gè)版本。Bugly 出現(xiàn)堆棧信息為 Null check operator used on a null value 的異常問(wèn)題,最終定位的原因是 3.3.10 SDK 源碼里,TextSelectionOverlay 類(lèi)通過(guò)持有的 Context 對(duì)象尋找 RenderObject 時(shí),返回了Nil 值,在對(duì)其進(jìn)行強(qiáng)制解包時(shí)觸發(fā)了異常。因此,小組成員選擇 Hook 系統(tǒng) SelectionOverlay._buildToolbar() 方法,在其內(nèi)部判斷對(duì)應(yīng) Context 是否已經(jīng) unmount,如果是則直接返回一個(gè) Container。這么修改上線后問(wèn)題已解決。

雖然可以 Hook 系統(tǒng)方法來(lái)處理問(wèn)題或配置自定義內(nèi)容,但也需要選擇合理的合適的時(shí)機(jī)去觸發(fā),不可以過(guò)度使用。

五、總結(jié)

使用 Dart AOP 實(shí)現(xiàn)的 Flutter App 全埋點(diǎn)功能具有多重優(yōu)勢(shì)。首先,它不依賴于業(yè)務(wù)層,可以在端上自動(dòng)采集并上報(bào)數(shù)據(jù),從而不會(huì)對(duì)業(yè)務(wù)代碼造成額外的負(fù)擔(dān)。其次,通過(guò) AOP 的方式,我們可以在代碼中簡(jiǎn)單地插入埋點(diǎn)邏輯,而不需要修改原有代碼,從而大大縮短了開(kāi)發(fā)時(shí)間。此外,基于 AOP 的實(shí)現(xiàn)方式還能夠方便后期的維護(hù)工作,當(dāng)需要新增或修改埋點(diǎn)邏輯時(shí),只需修改 AOP 配置即可,而不需要對(duì)業(yè)務(wù)代碼進(jìn)行大規(guī)模的修改。因此,基于 Dart AOP 實(shí)現(xiàn)的 Flutter App 全埋點(diǎn)功能不僅能夠提升開(kāi)發(fā)效率,還能夠方便后期的維護(hù)工作,為項(xiàng)目的穩(wěn)定性和可維護(hù)性提供了有力支持,希望以后可以通過(guò) AOP 技術(shù)解決更多難題。

參考文獻(xiàn):https://juejin.cn/post/6892371163859976199

責(zé)任編輯:武曉燕 來(lái)源: 得物技術(shù)
相關(guān)推薦

2024-11-01 12:39:04

2025-11-06 01:45:00

2025-07-03 03:20:00

2020-04-29 16:24:55

開(kāi)發(fā)iOS技術(shù)

2023-02-08 19:37:37

大數(shù)據(jù)技術(shù)

2019-08-12 10:45:54

Flutter框架Native

2023-09-05 07:28:02

Java自動(dòng)埋點(diǎn)

2023-04-19 09:05:44

2017-12-28 14:54:04

Android代碼埋點(diǎn)全埋點(diǎn)

2022-08-31 07:54:08

采集sdk埋點(diǎn)數(shù)據(jù)

2016-12-12 13:42:54

數(shù)據(jù)分析大數(shù)據(jù)埋點(diǎn)

2025-07-11 09:09:00

2021-02-19 07:59:21

數(shù)據(jù)埋點(diǎn)數(shù)據(jù)分析大數(shù)據(jù)

2021-08-10 13:50:24

iOS

2023-01-10 09:08:53

埋點(diǎn)數(shù)據(jù)數(shù)據(jù)處理

2018-11-14 11:26:49

神策數(shù)據(jù)

2021-08-31 19:14:38

技術(shù)埋點(diǎn)運(yùn)營(yíng)

2023-11-21 07:14:43

埋點(diǎn)大數(shù)據(jù)

2016-08-12 00:30:45

互聯(lián)網(wǎng)數(shù)據(jù)埋點(diǎn)

2022-10-14 08:47:42

埋點(diǎn)統(tǒng)計(jì)優(yōu)化
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)

亚洲精品一区二区三区在线| eeuss影院www在线观看| 亚洲成人原创| 亚洲人线精品午夜| 91精品国产91久久久久久吃药| 亚洲熟妇一区二区| 免费高潮视频95在线观看网站| 2020国产精品| 亚洲一区二区在线| 午夜毛片在线观看| 天天做天天爱天天爽综合网| 精品国产成人系列| 精品久久久久久久无码 | 91美女在线视频| 国产精品免费看久久久香蕉| 国产亚洲欧美精品久久久久久 | 国产精品中文字幕日韩精品 | 黄色小视频在线免费看| 欧美人与牛zoz0性行为| 日韩一区二区三区在线视频| 无码无遮挡又大又爽又黄的视频| 欧美激情黑人| 久久综合久久综合久久综合| 亚洲淫片在线视频| 久久国产香蕉视频| 亚洲激情一区| 成人97在线观看视频| 亚洲一区二区三区四区五区六区| 国产精品成人3p一区二区三区| 欧美性精品220| 福利在线一区二区| av片哪里在线观看| 日本一区二区成人在线| 国产区一区二区三区| 国产又粗又长又黄| 日韩一区精品视频| 国产91精品网站| 日本一级黄色大片| 你懂的国产精品| 最好看的2019的中文字幕视频| 黄色性生活一级片| 高潮按摩久久久久久av免费| 在线不卡中文字幕| 中文字幕国产传媒| 色老头在线一区二区三区| 一区二区三区在线影院| 最新国产精品久久| 在线视频三区| 国产精品欧美一区喷水| 欧美日本亚洲| 日韩在线免费看| 99久久亚洲一区二区三区青草| 成人黄色短视频在线观看| 色婷婷久久综合中文久久蜜桃av| 午夜在线播放视频欧美| 91精品国产电影| 亚洲精品77777| 亚洲人妖在线| 欧美在线一区二区视频| 日韩伦人妻无码| 亚洲欧洲午夜| 欧美与欧洲交xxxx免费观看| 91精品国产乱码久久久张津瑜| 黄色成人av网站| 久久久久久com| 日本少妇裸体做爰| 久久精品九九| 国产美女扒开尿口久久久| 一二三区在线播放| 国产麻豆精品久久一二三| 亚洲最大的免费| www.色播.com| thepron国产精品| 欧美精品在线一区| 在线免费看av| 一区二区三区波多野结衣在线观看| 色哟哟免费网站| 男女视频在线| 欧美午夜精品在线| a在线观看免费视频| 青草综合视频| 欧美xxxxxxxx| 国产精品jizz| 91影院成人| 欧美激情在线有限公司| 欧美bbbbbbbbbbbb精品| 秋霞午夜av一区二区三区| 91久久在线播放| 免费观看的毛片| 国产丝袜欧美中文另类| 91九色国产ts另类人妖| 狠狠操一区二区三区| 欧美亚洲愉拍一区二区| aaaaa黄色片| 欧美**vk| 欧美精品第一页在线播放| 中文字幕视频网| 极品少妇一区二区| 久久久久久九九| 欧美被日视频| 欧美视频裸体精品| 国产精品久久久久久久99| 国产美女撒尿一区二区| 日韩中文字幕网站| 日韩大片免费在线观看| 久久超碰97中文字幕| 精品欧美一区二区三区久久久| 成年人在线看| 婷婷六月综合亚洲| 三日本三级少妇三级99| 国产成人影院| 欧美激情欧美激情在线五月| 中文字幕网址在线| 不卡视频在线看| 一区二区视频在线免费| 蜜臀国产一区| 精品国产精品一区二区夜夜嗨| 天堂av网手机版| 99国产精品久久久久久久成人热| 成人在线中文字幕| 久色视频在线| 图片区日韩欧美亚洲| 人人爽人人爽av| 欧洲毛片在线视频免费观看| 97视频在线观看免费| 国产老妇伦国产熟女老妇视频| 91免费观看视频| 成人在线播放网址| 精品国产亚洲一区二区在线观看| 国产亚洲精品久久久久久牛牛| 日本在线免费观看| 成人高清在线视频| 免费观看国产视频在线| 日韩午夜视频在线| 有码中文亚洲精品| 无码视频在线观看| 久久久午夜精品理论片中文字幕| 日韩日韩日韩日韩日韩| 我要色综合中文字幕| 另类天堂视频在线观看| 92久久精品一区二区| 国产蜜臀av在线一区二区三区| 国产精品97在线| 欧美尿孔扩张虐视频| 国内久久久精品| 亚洲国产精品二区| 亚洲午夜电影网| 日韩成人av影院| 欧美激情五月| 国产成人看片| 92久久精品| 亚洲精品videossex少妇| 国产在线成人精品午夜| 成人激情综合网站| 妞干网在线视频观看| 国产女人18毛片水真多18精品| 久久久人成影片一区二区三区观看| 精品国产无码一区二区| 一区二区三区在线观看欧美| 国产麻豆剧传媒精品国产| 欧美另类综合| 精品日产一区2区三区黄免费 | 亚洲高清免费视频| 中国xxxx性xxxx产国| 亚洲理伦在线| 日本在线观看不卡| 国产亚洲欧美日韩精品一区二区三区| 丝袜亚洲另类欧美重口| 国产wwwwwww| 一区二区三区在线播| 艳妇乳肉豪妇荡乳xxx| 一本色道88久久加勒比精品| 欧美日韩国产一二| 91成人抖音| 欧美成人激情在线| 欧美熟妇乱码在线一区 | 在线播放精品视频| 亚洲美女视频在线| 潘金莲一级淫片aaaaa| 91久久午夜| 午夜精品视频在线观看一区二区| 日本电影久久久| 久久99精品久久久久久琪琪| 天堂中文资源在线观看| 在线免费不卡电影| 综合五月激情网| caoporn国产精品| 久久久久国产一区| 亚洲先锋成人| 日本精品一区二区三区视频| 亚洲精品三区| 欧美一级淫片丝袜脚交| 一级毛片视频在线| 亚洲电影天堂av| 中文字幕日产av| 亚洲.国产.中文慕字在线| 影音先锋制服丝袜| 欧美三级理论片| 激情91久久| 亚洲国内在线| 精品三级av在线导航| 国产精品一香蕉国产线看观看| 国产盗摄精品一区二区酒店| 中文国产成人精品| 五月婷婷六月激情| 欧美一区二区三区色| 日韩在线视频不卡| 亚洲国产视频在线| 国产精品69久久久久孕妇欧美| 成人精品一区二区三区四区| 中文字幕第80页| 一本一本久久| 精品国产一区二区三区无码| 色欧美自拍视频| 久久久久久a亚洲欧洲aⅴ| 日韩精品一级| 国产精品爽黄69天堂a| 成人黄色动漫| 久久成人综合视频| www.av在线播放| 日韩国产激情在线| 精品国产无码一区二区| 欧美日韩高清一区二区不卡| 青青青国产在线| 图片区日韩欧美亚洲| 免费在线观看黄视频| 1000部国产精品成人观看| 久久国产柳州莫菁门| av在线不卡观看免费观看| 日批视频在线看| 国产麻豆一精品一av一免费| 在线免费观看视频黄| 日本特黄久久久高潮| 欧美性久久久久| 亚洲国产网站| 成人免费视频91| 欧美网站在线| 草草草视频在线观看| 中文无码久久精品| 福利在线小视频| 午夜国产精品视频| 青少年xxxxx性开放hg| 97久久视频| 97超碰免费观看| 亚洲综合五月| 伊人网在线免费| 欧美精品日韩| 日本男女交配视频| 国产真实久久| 欧美在线一区视频| 国产一区二区三区久久| www.com毛片| 日韩**一区毛片| 久久人妻精品白浆国产| 日韩电影免费在线看| 色播五月综合网| 久久精品国产999大香线蕉| 欧美性受xxxxxx黑人xyx性爽| 精品一区二区免费看| 天天爽夜夜爽视频| 成人小视频免费在线观看| 国产十八熟妇av成人一区| 99久久精品一区| 99久久久无码国产精品性| 国产女主播在线一区二区| 亚洲精品成人av久久| 日韩美女啊v在线免费观看| 国产乱国产乱老熟300| 亚洲国产一区二区三区青草影视| 成人免费看片98欧美| 色噜噜狠狠成人中文综合| 中文字幕av久久爽| 日韩一区二区三区视频在线| 日本高清视频免费看| 亚洲色图15p| 在线观看免费黄色| 欧美极度另类性三渗透| 在线观看涩涩| 国产日韩欧美黄色| 大陆精大陆国产国语精品| 欧美日韩免费精品| 亚洲欧美网站在线观看| 男女私大尺度视频| 日韩av中文字幕一区二区三区 | 成人免费视频caoporn| 亚洲中文字幕无码av| 国产精品色哟哟| 久久久久亚洲天堂| 在线一区二区三区四区五区| 国产www视频| 亚洲石原莉奈一区二区在线观看| 麻豆传媒在线免费看| 国产91精品不卡视频| 欧美大陆国产| 欧美激情论坛| 欧美精品九九| 性欧美1819| 99re在线视频这里只有精品| 亚洲人与黑人屁股眼交| 欧美三级免费观看| 国产成人精品亚洲精品色欲| 亚洲欧美日韩一区二区在线 | 日韩av手机在线观看| 久久9999免费视频| 色姑娘综合网| 一区二区三区四区五区精品视频| 最新天堂在线视频| 91网上在线视频| 免费视频网站www| 欧美日韩国产一区二区三区地区| 污污网站免费在线观看| 久久亚洲精品国产亚洲老地址| 伊人久久国产| 国产一区二区高清不卡| 亚洲午夜精品一区二区国产| 日韩一级免费在线观看| caoporn国产精品| 免费在线黄色片| 91麻豆精品国产91久久久久久久久| 日本在线一二三| 国模精品系列视频| 亚洲超碰在线观看| 男女h黄动漫啪啪无遮挡软件| 视频一区在线视频| 日韩少妇一区二区| 亚洲精品日韩一| 97成人在线观看| 中文字幕欧美在线| 日韩电影免费观看高清完整版| 国产原创精品| 亚洲国产一区二区三区高清| 超碰人人cao| 亚洲免费观看高清完整版在线观看| a片在线免费观看| 影音先锋日韩有码| 激情亚洲影院在线观看| 久久亚洲国产精品日日av夜夜| 亚洲黄色视屏| 无码任你躁久久久久久老妇| 亚洲一区二区在线播放相泽| 国产成人精品一区二区无码呦| 欧美成人午夜免费视在线看片 | 可以看av的网站久久看| 一区二区三区免费在线观看视频| 午夜av区久久| 香港一级纯黄大片| 欧美一二三视频| 亚洲69av| 久久久久人妻精品一区三寸| 久久日韩粉嫩一区二区三区| 天天干天天干天天操| 亚洲欧洲在线看| 国产一区二区三区影视| 亚洲韩国在线| 国产精品资源站在线| 久久精品视频8| 亚洲精品720p| 欧美黄色网页| 亚洲欧美精品在线观看| 久久精品国产第一区二区三区| 99精品久久久久| 精品日韩成人av| 筱崎爱全乳无删减在线观看| 欧美男人的天堂| 久久精品72免费观看| 欧洲第一无人区观看| 亚洲的天堂在线中文字幕| 欧美裸体视频| 日韩欧美三级一区二区| 精品一区二区三区免费毛片爱| 久久久精品国产sm调教| 亚洲精品720p| 国产精品传媒麻豆hd| 免费在线精品视频| av激情综合网| 波多野结衣视频免费观看| 精品国产一区二区三区四区在线观看 | 免费人成在线观看网站| 国产精品视频在线播放| 欧美一区91| 欧美 变态 另类 人妖| 欧美日韩一区精品| 日本小视频在线免费观看| 欧美极品jizzhd欧美| 精品一区二区三区在线视频| 国产无码精品视频| 一区二区三区国产视频| 涩爱av色老久久精品偷偷鲁| 欧美在线观看www| 亚洲欧洲日产国码二区| 色欲久久久天天天综合网| 国产美女精品免费电影| 亚洲高清资源| 免费看一级黄色| 日韩大片免费观看视频播放| 久久天堂影院| 国产视频九色蝌蚪| 中文字幕在线观看一区| 亚洲三区在线观看无套内射| 91中文字幕在线观看|