Flutter 異常監控方案與實踐

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"前言","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"錯誤監控是維護App穩定的重要手段,通過對線上問題的實時監控,來觀察App是否出現異常狀況,以便快速解決問題以及制定修復方案.對於集成了Flutter的App,除了需要提供crash崩潰監控,還需要對Flutter異常進行監控.","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一般來說,監控系統都會包含問題的實時收集上報、問題的歸類識別(聚合)以及自動分配、問題定位、實時報警等幾個模塊.要做一套Flutter異常監控也不例外,圖中是貝殼在Flutter異常監控的整套方案.首先在端上收集異常並進行初步處理,持續集成平臺會處理各平臺的app符號信息並管理app相關的基礎信息,在監控後臺,系統主要處理異常日誌數據源,並經過預處理、解析、構建多緯度統計數據、最終展示到前端平臺、並會根據一些閾值配置進行異常報警.","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文主要圍繞其中移動端Flutter異常處理、監控後臺異常預處理、監控後臺異常的解析處理三部分來介紹貝殼在Flutter異常監控的實踐與沉澱.","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/c7/c774a41012f0cf53029fde4153674441.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule","attrs":{}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"一、移動端Flutter異常處理","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在介紹Flutter異常處理前,我們先了解下Flutter異常.","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"1.1. Flutter異常","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Flutter異常是指程序中Dart代碼運行時拋出的錯誤事件.一般來說,異常種類主要分爲Exception和Error,以及它們的子類型.當然開發者也可以自定義非null的錯誤類型.Dart支持程序拋出非空類型的各種錯誤,如下代碼所示:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"void main(){\n // 可以拋出任意非空的異常\n throw \"自定義錯誤\";\n throw Error();\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於Flutter應用來說,當程序出現異常時,通常情況程序不會崩潰退出,這點不同於java或者Objective-C這種編程語言.拿Android Java應用舉例,當異常發生並且沒有被捕獲,那麼默認的","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"uncaughtException","attrs":{}}],"attrs":{}},{"type":"text","text":"方法就會捕獲到異常並且執行","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"System.exit()","attrs":{}}],"attrs":{}},{"type":"text","text":"殺掉程序,或者異常觸發系統底層的異常進程也會直接被殺掉.","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是Flutter的處理方式則不一樣,異常即使沒有被我們主動捕獲,系統的默認處理方式也只是print,或者替換錯誤widget,通常在App上表現爲頁面白屏(紅屏)、用戶操作不響應等,這也是爲什麼我們在崩潰監控之外需要通過額外的監控平臺能力去處理Flutter異常.","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在Flutter運行過程中,採用了事件循環的機制來運行任務(https://dart.cn/articles/archive/event-loop),如下圖所示,其中有兩個不同優先級的隊列,每當有事件任務觸發,都會被放到其中一個隊列中,其中運行的各個任務是互相獨立的.當某個任務出現異常,會導致任務的後續代碼不會繼續執行,但不會影響其他任務的執行.","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/51/51b9956c358077ad3c5aded43be5d93f.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"1.2. Flutter異常捕獲","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"和java類似,Flutter也可以通過try-catch機制捕獲,但是try-catch只能捕獲同步代碼塊的異常,對於future異步代碼塊拋出的錯誤,需要採用future提供的catchError語句捕獲,如下代碼:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"void main() {\n // 使用try-catch捕獲同步代碼塊異常\n try {\n throw AssertionError('throw AssertionError');\n } catch (e) {\n print(e);\n }\n\n // 使用catchError捕獲異步代碼塊異常\n Future.delayed(Duration(microseconds: 0))\n .then((e) => throw AssertionError('throw AssertionError'))\n .catchError((e) => print(e));\n\n // 異步代碼塊通過try-catch捕獲不到,下面catch邏輯不會執行\n try {\n Future.delayed(Duration(microseconds: 0))\n .then((e) => throw AssertionError('throw AssertionError'));\n } catch (e) {\n print(\"不會執行\");\n }\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"知道如何捕獲錯誤後,只需再找到合適的地方去捕獲Flutter錯誤,下文分爲3個部分去介紹異常捕獲.","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"1.2.1. Flutter框架異常捕獲","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Flutter框架本身已經捕獲了許多dart拋出的異常,包括構建期間、佈局期間和繪製期間的異常.它通過 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"FlutterError.reportError","attrs":{}}],"attrs":{}},{"type":"text","text":"統一處理,如下面代碼:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"// 框架先通過try-catch捕獲錯誤,然後發送到reportError去統一處理\nvoid performRebuild() {\n ...\n try {\n ...\n } catch (e, stack) {\n built = ErrorWidget.builder(\n // 方法對中調到reportError\n _debugReportException(\n ErrorDescription('building $this'),\n e,\n stack,\n informationCollector: () sync* {\n yield DiagnosticsDebugCreator(DebugCreator(this));\n },\n ),\n );\n }\n ...\n }\n\nstatic void reportError(FlutterErrorDetails details) {\n ...\n if (onError != null)\n onError(details);\n }\n \n // 系統提供的默認實現方式,輸出到控制檯,重寫方法可以實現自己的處理邏輯.\nstatic FlutterExceptionHandler onError = dumpErrorToConsole;\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們可以","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"main","attrs":{}}],"attrs":{}},{"type":"text","text":"方法中重寫","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"onError","attrs":{}}],"attrs":{}},{"type":"text","text":"方法去實現我們自己的邏輯,如下代碼所示:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"void main() {\n // 重寫onError方法,實現自定義邏輯\n FlutterError.onError = (details) {\n print(details);\n };\n \n runApp(MyApp());\n}\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"1.2.2. 其它dart異常捕獲","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於其它未被Flutter框架捕獲的Dart異常,比如Future中的異常等,會被錯誤發生所在","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"Zone","attrs":{}}],"attrs":{}},{"type":"text","text":"捕獲,","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"Zone","attrs":{}}],"attrs":{}},{"type":"text","text":"表示一個代碼執行的上下文,給異步代碼和同步代碼提供了一個穩定的運行環境,可以簡單理解爲一個沙盒,其對於內部發生且未被主動捕獲的異常的默認處理方式也是打印輸出錯誤.初始","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"main","attrs":{}}],"attrs":{}},{"type":"text","text":"函數就在默認區域 ( ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"Zone.root","attrs":{}}],"attrs":{}},{"type":"text","text":" )的上下文中運行,我們可以通過將","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"runApp()","attrs":{}}],"attrs":{}},{"type":"text","text":"包裹到自定義的","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"Zone","attrs":{}}],"attrs":{}},{"type":"text","text":"裏,重寫捕獲異常的方法","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"onError","attrs":{}}],"attrs":{}},{"type":"text","text":",如下代碼所示:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"void main(){\n runZoned(() {\n runApp(MyApp());\n }, onError: (error, stackTrace) {\n // 自定義處理錯誤\n print(error);\n });\n}\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"1.2.3. 白屏(紅屏)異常捕獲","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上文說到,Flutter框架會捕獲到一部分的dart異常,除了統一的回調處理,還對一部分導致頁面白屏問題的異常,進行了替換錯誤widget的處理,","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"void performRebuild() {\n ...\n try {\n ...\n } catch (e, stack) {\n // ErrorWidget.builder回調方法替換錯誤頁面\n built = ErrorWidget.builder(\n _debugReportException(\n ErrorDescription('building $this'),\n e,\n stack,\n informationCollector: () sync* {\n yield DiagnosticsDebugCreator(DebugCreator(this));\n },\n ),\n );\n }\n ...\n }\n //默認的處理方式,我們也可以在main中覆蓋處理\n static ErrorWidgetBuilder builder = _defaultErrorWidgetBuilder;\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如上面代碼所示,Flutter框架通過","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"ErrorWidgetBuilder builder","attrs":{}}],"attrs":{}},{"type":"text","text":"對頁面渲染失敗異常進行統一替換widget的處理,我們通過對其覆蓋重寫,就能在衆多的異常中捕獲到頁面渲染的異常,方便後面對異常進行分級分類處理.","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"注意,官方邏輯中,回調替換widget的地方也同樣上報到了","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"reportError","attrs":{}}],"attrs":{}},{"type":"text","text":",我們可以通過aop的方式將邏輯替換,否則對上報錯誤數量有一定影響.","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"到這裏,捕獲Flutter異常已經完成,最終使用了三個hook點去上報異常,爲後續的後端服務解析處理做好了源數據準備.當然,光在這些地方收集異常還是不夠的,還需要一些異常封裝處理,來補充異常運行的狀態信息.","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"1.3. Flutter異常封裝處理","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"異常信息的封裝主要分爲兩個步驟:異常信息的提取處理、添加附加信息.","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"1.3.1. Flutter異常提取處理","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先是異常種類的提取,一般通過","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"runtimeType","attrs":{}}],"attrs":{}},{"type":"text","text":"就能獲取到異常類型;但是要注意的是,之前hook上報的地方,有些異常被封裝成","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"FlutterErrorDetails","attrs":{}}],"attrs":{}},{"type":"text","text":",所以需要對其exception進行判斷.","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":" const FlutterErrorDetails({\n this.exception, //真實的異常\n this.stack,\n this.library = 'Flutter framework',\n this.context,\n this.stackFilter,\n this.informationCollector,\n this.silent = false,\n });\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"再者就是對異常的概述提取,我們通過使用Flutter框架中的一個函數","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"exceptionAsString","attrs":{}}],"attrs":{}},{"type":"text","text":"來獲取,如下面代碼:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"String exceptionAsString() {\n String? longMessage;\n if (exception is AssertionError) {\n final Object? message = exception.message;\n final String fullMessage = exception.toString();\n if (message is String && message != fullMessage) {\n if (fullMessage.length > message.length) {\n final int position = fullMessage.lastIndexOf(message);\n if (position == fullMessage.length - message.length &&\n position > 2 &&\n fullMessage.substring(position - 2, position) == ': ') {\n // Add a linebreak so that the filename at the start of the\n // assertion message is always on its own line.\n String body = fullMessage.substring(0, position - 2);\n final int splitPoint = body.indexOf(' Failed assertion:');\n if (splitPoint >= 0) {\n body = '${body.substring(0, splitPoint)}\\n${body.substring(splitPoint + 1)}';\n }\n longMessage = '${message.trimRight()}\\n$body';\n }\n }\n }\n longMessage ??= fullMessage;\n } else if (exception is String) {\n longMessage = exception as String;\n } else if (exception is Error || exception is Exception) {\n longMessage = exception.toString();\n } else {\n longMessage = ' ${exception.toString()}';\n }\n longMessage = longMessage.trimRight();\n if (longMessage.isEmpty)\n longMessage = ' ';\n return longMessage;\n }\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"還有就是堆棧的上報,異常上報的地方都會有stack信息,對於Flutter框架封裝的","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"FlutterErrorDetails","attrs":{}}],"attrs":{}},{"type":"text","text":",提取","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"stack","attrs":{}}],"attrs":{}},{"type":"text","text":"即可.處理完異常信息,我們需要給異常信息添加一些額外的運行通用信息,來幫助解決異常.","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"1.3.2. Flutter異常附加信息","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了幫助Flutter異常的高效解決,我們在異常的上報中添加了一些附加信息,包括異常發生時的設備信息、頁面信息、內存信息、路徑埋點唯一檢索信息.其中,頁面信息的獲取方式可以在我們的另一篇文章中找到(附地址).這些信息可以幫助我們查看異常的走勢和修復狀況,如下圖:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/ff/ff98d1e09ff7a530e721f2936d8f2b37.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上報的一些系統現狀和運行信息,可以輔助開發同學定位問題:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/19/19e814e5c4929531f7a42dd619a4a693.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule","attrs":{}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"二、 後臺Flutter異常預處理","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當監控後臺收到移動端上報的異常日誌,首先要做的就是將收到的異常日誌進行預處理,其中最主要的兩個模塊就是異常的分級分類和異常堆棧的符號化解析.","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.1. Flutter異常的分級分類","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過上面,我們知道Flutter異常並不會導致崩潰,那麼Flutter異常一定會影響用戶麼?這裏要從Flutter異常和crash崩潰不同的地方說起.通常,crash發生時,一定代表我們的用戶受到了影響,但Flutter異常卻不一定.","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在所有的Flutter異常中,有一部分異常用戶並無感知.它可能是初期開發同學的代碼不夠規範導致無效調用引起,也可能是build的多次刷新報錯;還有一部分網絡異常導致的偶現錯誤,比如圖片錯誤,這種問題端上同學也不能處理(也有其他的監控服務處理了,比如網絡報警服務).在這種情況下,如果我們把所有的錯誤一股腦放到開發同學面前,不分輕重緩急,他們是沒法高效的分優先級去處理.開發同學的精力畢竟有限,我們應該集中精力去處理那些能處理以及對用戶真實發生影響的問題.","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所以針對Flutter異常,我們將其分爲3大類:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/83/83020ff9ad467c66c2ed200d021f008e.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"也是在經歷了第一階段開發同學對Flutter異常的處理不夠積極的情況,我們優化了監控平臺的能力,對Flutter的異常進行分級分類的處理.","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一是區分上報信息,也就是上文提到的上報頁面渲染失敗異常,包括白屏(紅屏)問題,二是後端服務對錯誤類型進一步分類處理.","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先是渲染失敗導致的紅屏和白屏是我們的一級問題,對於CastError、RangeError、PlatformException、NoSuchMethodError、MissingPluginException等多種錯誤類型,我們認爲其是影響業務的,也將其列爲一級問題.其它的,比如圖片異常或者網絡異常,我們將其放到二級問題.其中比較特殊的,比如NoSuchMethodError,我們對其中的部分異常進行正則過濾,也放到二級錯誤中.","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其中一級錯誤和二級錯誤分別展示到不同的地方以及提供後續不同的報警等處理方式,保證快速聚焦的核心問題上.","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.2. Flutter的符號化解析","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在Flutter1.17以上的版本中,官方支持了對Flutter產物去除符號表的功能,考慮到集成Flutter產物的app的安全性和包大小問題,我們在打包系統中集成了這個功能,通過官方支持的打包命令就可以在打包期間分離符號表文件.","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}}],"text":"—split-debug-info 可以分離出 debug info符號表信息","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是這也導致Flutter異常上報的堆棧是去符號化的,難以閱讀理解,如下所示:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"shell"},"content":[{"type":"text","text":"\"Warning: This VM has been configured to produce stack traces that violate the Dart standard.\n*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***\npid: 16540, tid: 6125465600, name beikeshareengine_0.1.ui\nisolate_dso_base: 10b860000, vm_dso_base: 10b860000\nisolate_instructions: 10b86a000, vm_instructions: 10b866000\n #00 abs 000000010bc7d08b _kDartIsolateSnapshotInstructions+0x41308b\n #01 abs 000000010bb0f037 _kDartIsolateSnapshotInstructions+0x2a5037\n #02 abs 000000010bcc72e7 _kDartIsolateSnapshotInstructions+0x45d2e7\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在這種情況下,我們需要一個符號化解析系統處理堆棧,將其轉化爲可理解的堆棧信息.當然剝離符號表信息不僅僅影響了Flutter異常的堆棧,對native crash中的相關Flutter堆棧行也會有影響,所以下文會對這兩塊分別闡述.","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"符號表解析首先要做的就是對編譯打包過程中符號表文件的處理.","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"2.2.1. Flutter符號表文件處理","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上文說到,在編譯過程中通過命令將符號文件生成並剝離出來並保存到指定目錄,文件類似這樣","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"app.ios-arm64.symbols","attrs":{}}],"attrs":{}},{"type":"text","text":".首先我們會先將文件上傳到artifactory倉庫中,並在打包過程中分析出app產物的其它相關信息,比如版本、hash等等,之後將這些信息發送監控平臺進行分析處理,以供之後的符號化解析使用.","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"完整架構圖如下:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/74/745e3bd70952c2eaad3414cf80a206c7.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有了符號表文件之後,剩下的就是對堆棧進行解析處理,首先是Flutter異常的符號化解析.","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"2.2.2. Flutter異常符號化解析","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先需要了解","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"app.ios-arm64.symbols","attrs":{}}],"attrs":{}},{"type":"text","text":"這個從Flutter產物中剔除出來的符號文件,它存儲了Dart VM AOT 編譯器將源代碼映射爲信息編碼的所有信息,是採用了DWARF格式的高度壓縮文件.這裏拿ios舉例,Android同理,通過file命令可知,其是一個ELF文件:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}},{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}}],"text":"➜ file app.ios-arm64.symbols","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}},{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}}],"text":"➜ app.ios-arm64.symbols : ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, BuildID[md5/uuid]=XXXXX, with debug_info, not stripped","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"ELF (Executable and Linkable Format)是一種爲可執行文件,目標文件,共享鏈接庫和內核轉儲(core dumps)準備的標準文件格式.通過下面命令可以生成兩個符號相關文件:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}},{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}}],"text":"➜ dwarfdump app.ios-arm64.symbols --debug-info > info.txt","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}},{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}}],"text":"➜ dwarfdump app.ios-arm64.symbols --debug-line > line.txt","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其中info文件中存儲的是源碼信息,line文件中存儲的是行號相關信息.info文件中我們拿其中一個函數信息舉例:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"0x0010f67f: TAG_subprogram [3] *\n AT_abstract_origin( {0x0003acac}\"MaterialLocalizationDa.datePickerHelpText\" )\n AT_low_pc( 0x00000000001a0118 )\n AT_high_pc( 0x00000000001a0134 )\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其中:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","marks":[{"type":"italic","attrs":{}},{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}}],"text":"TAG_subprogram","attrs":{}}],"attrs":{}},{"type":"text","marks":[{"type":"italic","attrs":{}},{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}}],"text":"是指代函數的意思,","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","marks":[{"type":"italic","attrs":{}},{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}}],"text":"AT_abstract_origin","attrs":{}}],"attrs":{}},{"type":"text","marks":[{"type":"italic","attrs":{}},{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}}],"text":"是其源碼信息,","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","marks":[{"type":"italic","attrs":{}},{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}}],"text":"AT_low_pc","attrs":{}}],"attrs":{}},{"type":"text","marks":[{"type":"italic","attrs":{}},{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}}],"text":"和","attrs":{}},{"type":"codeinline","content":[{"type":"text","marks":[{"type":"italic","attrs":{}},{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}}],"text":"AT_high_pc","attrs":{}}],"attrs":{}},{"type":"text","marks":[{"type":"italic","attrs":{}},{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}}],"text":"是這個函數相對於符號表文件高與低的偏移量,下文簡稱爲","attrs":{}},{"type":"codeinline","content":[{"type":"text","marks":[{"type":"italic","attrs":{}},{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}}],"text":"pc_offset","attrs":{}}],"attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在下面這樣一行堆棧中,","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"shell"},"content":[{"type":"text","text":" #02 abs 000000010bcc72e7 _kDartIsolateSnapshotInstructions+0x45d2e7\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"_kDartIsolateSnapshotInstructions","attrs":{}}],"attrs":{}},{"type":"text","text":"代表的是這行堆棧的錯誤信息是在","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"isolate_instructions","attrs":{}}],"attrs":{}},{"type":"text","text":"指令段中,後面跟的偏移量就是相對","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"isolate_instructions","attrs":{}}],"attrs":{}},{"type":"text","text":"起始的偏移量,下文簡稱爲","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"isolate_offset","attrs":{}}],"attrs":{}},{"type":"text","text":",它在一個符號表中是固定的.","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們只要通過","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"isolate_offset","attrs":{}}],"attrs":{}},{"type":"text","text":"找到","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"pc_offset","attrs":{}}],"attrs":{}},{"type":"text","text":",繼而就能找到源碼信息.他們的關係也很明顯,通過","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"nm","attrs":{}}],"attrs":{}},{"type":"text","text":"命令找到","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"isolate_instructions","attrs":{}}],"attrs":{}},{"type":"text","text":"相對符號表文件的偏移量","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"isolate_start","attrs":{}}],"attrs":{}},{"type":"text","text":",然後通過相加的方式得到","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"pc_offset","attrs":{}}],"attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}},{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}}],"text":"➜ nm app.ios-arm64.symbols | grep_kDartIsolateSnapshotInstructions","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}},{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}}],"text":"➜ _kDartIsolateSnapshotInstructions b 0x6000","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其中0x6000就是","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"isolate_start","attrs":{}}],"attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","marks":[{"type":"italic","attrs":{}},{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}}],"text":"isolate_start + isolate_offset = pc_offset","attrs":{}}],"attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最終我們只要在info文件中找到","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"pc_offset","attrs":{}}],"attrs":{}},{"type":"text","text":"在哪個源碼信息的","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"AT_low_pc","attrs":{}}],"attrs":{}},{"type":"text","text":"和","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"AT_high_pc","attrs":{}}],"attrs":{}},{"type":"text","text":"之間,就能找到源碼信息.","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"同樣的,拿到這些信息後在line文件中我們通過偏移地址的映射關係也能找到對應的行號信息,這裏我們就不做闡述.","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼這一套解析邏輯我們如何實現呢,官方既然提供了剔出符號化的邏輯,當然也會有符號化解析的邏輯.通過對flutter_tools源碼的閱讀可知,官方同樣提供了一個","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"SymbolizeCommand","attrs":{}}],"attrs":{}},{"type":"text","text":"的命令用於符號化解析,其通過獲取符號文件與堆棧輸入,最終通過","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"native_stack_traces","attrs":{}}],"attrs":{}},{"type":"text","text":"庫中的","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"DwarfStackTraceDecoder","attrs":{}}],"attrs":{}},{"type":"text","text":"解析處理,如下代碼所示:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"@override\n Future runCommand() async {\n Stream> input;\n IOSink output;\n\n // 分析參數獲取符號文件地址與堆棧\n if (argResults.wasParsed('output')) {\n final File outputFile = _fileSystem.file(stringArg('output'));\n if (!outputFile.parent.existsSync()) {\n outputFile.parent.createSync(recursive: true);\n }\n output = outputFile.openWrite();\n } else {\n final StreamController> outputController = StreamController>();\n outputController\n .stream\n .transform(utf8.decoder)\n .listen(_stdio.stdoutWrite);\n output = IOSink(outputController);\n }\n\n if (argResults.wasParsed('input')) {\n input = _fileSystem.file(stringArg('input')).openRead();\n } else {\n input = _stdio.stdin;\n }\n\n final Uint8List symbols = _fileSystem.file(stringArg('debug-info')).readAsBytesSync();\n // 解析處理\n await _dwarfSymbolizationService.decode(\n input: input,\n output: output,\n symbols: symbols,\n );\n\n return FlutterCommandResult.success();\n }\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其中對","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"DwarfStackTraceDecoder","attrs":{}}],"attrs":{}},{"type":"text","text":"邏輯的閱讀也能驗證上文邏輯.其中計算","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"pc_offset","attrs":{}}],"attrs":{}},{"type":"text","text":"偏移量的方式,官方還提供了其它幾種計算方式:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"PCOffset _retrievePCOffset(StackTraceHeader header, RegExpMatch match) {\n if (match == null) return null;\n final restString = match.namedGroup('rest');\n // 第一種,通過isolate_offset/vm_offset計算,也就是上文提到的\n if (restString.isNotEmpty) {\n final offset = tryParseSymbolOffset(restString);\n if (offset != null) return offset;\n }\n // 第二種,通過isolate_instructions和運行絕對地址計算\n if (header != null) {\n final addressString = match.namedGroup('absolute');\n final address = int.tryParse(addressString, radix: 16);\n return header.offsetOf(address);\n }\n // 第三種通過虛擬就地址計算,一般用不到\n final virtualString = match.namedGroup('virtual');\n if (virtualString != null) {\n final address = int.tryParse(virtualString, radix: 16);\n return PCOffset(address, InstructionsSection.none);\n }\n return null;\n}\n\n//對應上文提到的三種方式,分別和isolate_start/vm_start相加計算出pc_offset\nint virtualAddressOf(PCOffset pcOffset) {\n switch (pcOffset.section) {\n case InstructionsSection.none:\n // This address is already virtualized, so we don't need to change it.\n return pcOffset.offset;\n case InstructionsSection.vm:\n return pcOffset.offset + vmStartAddress;\n case InstructionsSection.isolate:\n return pcOffset.offset + isolateStartAddress;\n default:\n throw \"Unexpected value for instructions section\";\n }\n }\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其中第二種利用堆棧header信息中的","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"isolate_instructions: 10b86a000, vm_instructions: 10b866000","attrs":{}}],"attrs":{}},{"type":"text","text":"這兩個運行時偏移地址和","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"abs 000000010bc7d08b","attrs":{}}],"attrs":{}},{"type":"text","text":"相減也能得出","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"isolate_offset","attrs":{}}],"attrs":{}},{"type":"text","text":",之後通過第一種的邏輯最終得到","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"pc_offset","attrs":{}}],"attrs":{}},{"type":"text","text":".","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"除此之外,官方提供的","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"SymbolizeCommand","attrs":{}}],"attrs":{}},{"type":"text","text":"中的","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"runCommand","attrs":{}}],"attrs":{}},{"type":"text","text":"僅支持的文件堆棧輸入輸入,並且後端服務不可能直接依賴整個dart的執行環境,所以我們將","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"runCommand","attrs":{}}],"attrs":{}},{"type":"text","text":"中的邏輯拆分,並擴展可支持堆棧類型,如下代碼:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":" if (options.wasParsed('input')) {\n input = _fileSystem.file(stringArg('input')).openRead();\n } else if (options.wasParsed('input-string')) {\n //支持string輸入的堆棧\n String formatString = options['input-string'];\n input = Stream.value(value.codeUnits);\n } else {\n input = _stdio.stdin;\n }\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最終通過以下命令打包成一個linux\\macos可執行腳本","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"flutter symbolize","attrs":{}}],"attrs":{}},{"type":"text","text":",提供給後端服務用於解析堆棧信息.","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"shell"},"content":[{"type":"text","text":"➜ dart compile exe bin/symbolize.dart -o outputs/linux_x64_dart_2.13.3/symbolize\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"除了Flutter異常的符號化解析,去除符號表也會影響到Flutter引起的crash中的堆棧解析,下面我們介紹解析過程.","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"2.2.3. Flutter crash符號化解析","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因爲涉及到crash堆棧,Android和iOS的堆棧與解析方式就有些差別了,下文我們分別描述iOS和Android中的Flutter堆棧的解析處理.","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"iOS","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Flutter打包後的iOS產物是Framework,其中有App.Framework和Flutter.Framework.其中App.Framework裏是Flutter側dart的相關代碼,也是需要利用上文提到的符號化文件進行處理,而Flutter.Framework的符號化解析則利用iOS的crash解析方式處理,這裏我們就不做敘述.","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於iOS crash,其中App.Framework產物中引發的崩潰會包含類似下面的堆棧:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"shell"},"content":[{"type":"text","text":" 動態庫名稱 函數運行時地址 App.framework運行時基地址 相對App.framework偏移量\n5 App 0x0000000104609950 0x104488000 + 1579344\n36 App 0x00000001044911e4 0x104488000 + 37348\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們需要做的就是將其轉化爲","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"flutter symbolize","attrs":{}}],"attrs":{}},{"type":"text","text":"腳本能夠識別的堆棧,也就是上文提到的這種:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"shell"},"content":[{"type":"text","text":"堆棧編號 函數運行時絕對地址 dart Isolate代碼段 isolate_offset\n#00 abs 000000010455b93f _kDartIsolateSnapshotInstructions+0xc793f\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"也就是說我們要通過相對App.Framework的偏移量得到","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"isolate_offset","attrs":{}}],"attrs":{}},{"type":"text","text":",按照上文一樣的思路去處理.首先需要計算App.Framework中isolate和vm指令段相對App.Framework的偏移地址,通過這兩個地址和相對App.framework的偏移量相減就能得到相對isolate和vm的偏移地址,也就是","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"isolate_offset","attrs":{}}],"attrs":{}},{"type":"text","text":"和","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"vm_offset","attrs":{}}],"attrs":{}},{"type":"text","text":".那麼如何得到isolate和vm指令段相對App.Framework的偏移地址呢,通過","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"nm","attrs":{}}],"attrs":{}},{"type":"text","text":"命令也能拿到","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}},{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}}],"text":"➜ nm App.Framework | grep _kDartIsolateSnapshotInstructions","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}},{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}}],"text":"➜ 0000000000008000 T _kDartIsolateSnapshotInstructions","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}},{"type":"color","attrs":{"color":"#40A9FF","name":"user"}}],"text":"➜ nm App.Framework | gre","attrs":{}},{"type":"text","marks":[{"type":"italic","attrs":{}},{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}}],"text":"p _kDartVmSnapshotInstructions","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}},{"type":"color","attrs":{"color":"#40A9FF","name":"user"}}],"text":"➜ 000000000000","attrs":{}},{"type":"text","marks":[{"type":"italic","attrs":{}},{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}}],"text":"4000 T _kDartVmSnapshotInstructions","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其中0000000000008000就是isolate指令段相對App.Framework的偏移量.","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因爲這個命令的執行邏輯是對App.Framework進行分析,所以它實際上也是在上文提到的持續集成打包時的符號化處理過程中,通過上面的命令分析得到,然後保存到異常監控平臺.","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"具體解析實現步驟如下圖:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}}],"text":"1、相對App.framework偏移量減去持續集成nm命令得到的偏移量,得出isolate_offset或者vm_offset;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}}],"text":"2、然後利用上一步的結構拼接成","attrs":{}},{"type":"codeinline","content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}}],"text":"flutter symbolize","attrs":{}}],"attrs":{}},{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}}],"text":"能夠識別的堆棧.","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}}],"text":"3、對每行堆棧重複執行1、2步,然後使用","attrs":{}},{"type":"codeinline","content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}}],"text":"flutter symbolize","attrs":{}}],"attrs":{}},{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}}],"text":"腳本解析出來.","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"android","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Android和iOS的解析同理,我們也只要處理其中包含dart代碼的libapp.so相關的堆棧.對於Android crash,其中包含的libapp.so相關堆棧如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"shell"},"content":[{"type":"text","text":"#03 pc 00004828 /data/app/XXX/lib/arm/libapp.so (offset 0x200) (_kDartVmSnapshotInstructions+ 10280)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"它的處理方式就很簡單,因爲","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"isolate_offset","attrs":{}}],"attrs":{}},{"type":"text","text":"和","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"vm_offset","attrs":{}}],"attrs":{}},{"type":"text","text":"直接有了,所以我們只要拼接成轉化爲","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"flutter symbolize","attrs":{}}],"attrs":{}},{"type":"text","text":"腳本能夠識別的堆棧轉化爲","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"flutter symbolize","attrs":{}}],"attrs":{}},{"type":"text","text":"腳本能夠識別的堆棧,就可以處理.","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"到這裏,我們已經將Flutter堆棧解析成可理解的堆棧信息了,下一步就是利用Flutter異常上報的信息,對Flutter異常進行解析處理.","attrs":{}}]},{"type":"horizontalrule","attrs":{}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"三、 後臺Flutter異常解析","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這一部分主要的內容是對異常的聚合和分配,以及統計計算.","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3.1.聚合","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"聚合異常是監控平臺一個非常重要的能力,能夠幫我們統計某個異常的實時影響情況,根據閾值預警、提前作出反應.","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"比如說一個異常短時間發生次數超過閾值,那麼我們通過報警的方式通知給負責人,然後作出停止灰度,替換線上包或者熱修等決策.","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"聚合採用分行解析堆棧信息的方式,找到錯誤發生最接近業務(非Flutter框架代碼)或者最能體現錯誤的那一行棧幀.","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過以下正則能夠分析出每行的類名、函數名、包名、文件名","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}},{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}}],"text":"\"^#(\\d+) +(.+) \\((.+?):?(\\d+)?:?(\\d+)?\\)$\"","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"得出下面這個數據結構對象","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"public class StackFrame extends Symbol {\n public String structureName;//包名\n public String className;//類名\n public String methodName;//函數名\n public Component component;//組件\n public String file = \"\";//文件\n public int line = -1;//行號\n public String content;//棧幀原始信息 or pageName\n public boolean asynchronous = false;//是否異步幀\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"之後便可以根據","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"structureName","attrs":{}}],"attrs":{}},{"type":"text","text":"和App構建集成平臺的信息進行匹配,如果匹配成功,就把這行棧幀作爲聚合的信息.","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/10/109bab73e8501454c2ff3f7979d31295.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"注意: 聚合信息中不要包含行號信息,因爲可能發生異常被修改但並未修復的情況,去掉行號信息可以在這種情況下,讓錯誤還是聚合成一種.","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"3.1.1.特殊處理","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過業務棧幀來聚合處理異常在一些情況下可能不生效.通過對大量Flutter異常堆棧的分析,我們發現,因爲future異步調用的問題,有許多的堆棧中並沒有業務棧幀,並且會把異常聚合到無效棧幀.","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"比如下面這種PlatformException錯誤信息,如果按照業務棧幀優先的邏輯,對於這種沒有業務棧幀的堆棧,就會把第1行作爲聚合信息,這樣這會導致大量的系統相關的","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"MethodChannel","attrs":{}}],"attrs":{}},{"type":"text","text":"錯誤都聚合成一種錯誤,對我們的問題解決以及閾值報警都有很大的干擾.","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"shell"},"content":[{"type":"text","text":"#0 MethodChannel.invokeMethod (package:flutter/src/services/platform_channel.dart:319)\n#1 \n#2 PlatformViewsService.initUiKitView (package:flutter/src/services/platform_views.dart:168)\n#3 _UiKitViewState._createNewUiKitView (package:flutter/src/widgets/platform_view.dart:621)\n#4 _UiKitViewState._initializeOnce (package:flutter/src/widgets/platform_view.dart:571)\n#5 _UiKitViewState.didChangeDependencies (package:flutter/src/widgets/platform_view.dart:581)\n#6 StatefulElement._firstBuild (package:flutter/src/widgets/framework.dart:4376)\n...\n#167 _rootRun (dart:async/zone.dart:1126)\n#168 _CustomZone.run (dart:async/zone.dart:1023)\n#169 _CustomZone.runGuarded (dart:async/zone.dart:925)\n#170 _invoke (dart:ui/hooks.dart:259)\n#171 _drawFrame (dart:ui/hooks.dart:217)\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所以除了業務棧幀優先聚合的邏輯,我們對異步棧幀也做了特殊處理 : 異步棧幀(第2行)的調用者高於被調用者.通過這種處理,這類錯誤會聚合到第三行上,代碼中是","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"PlatformViewsService.initUiKitView","attrs":{}}],"attrs":{}},{"type":"text","text":"這個錯誤中去.","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"除此之外,我們也提供了棧幀白名單的策略,也及白名單中的棧幀信息不屬於錯誤信息.也可以使得異常不會聚合到某個無效棧幀中,進一步減少無效堆棧聚合的問題.","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3.2.分配","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當然,一個高效的監控平臺,少不了自動分配異常的能力,這可以幫助負責人快速收到報警和響應錯誤.上文已經描述了異常發生時聚合的那幾幀關鍵信息,對於分配也是如此.主要採用兩個策略:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}}],"text":"1、包含業務堆棧的異常,通過構建集成平臺的組件維護信息,直接指派到負責人;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}}],"text":"2、對於沒有業務棧幀的異常,根據異常的種類來分配,比如是白屏問題,就根據上文提到的異常附加信息中的頁面信息,來進行指派.","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最終效果如下所示:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/b8/b86307be1c627d06a158dffb3832a8d3.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3.3.統計計算","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於crash監控中,崩潰率計算一般會採用兩個口徑:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}}],"text":"會話崩潰率 : 用戶每打開一次app計做一次會話,用 崩潰次數/會話次數 得到","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}}],"text":"設備崩潰率 : 每個用戶崩潰只計做一次崩潰,用崩潰次數/用戶數 得到","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但這這兩種都不適合Flutter,因爲Flutter異常時,app並沒有崩潰,那麼按照上面提到的兩種計算口徑都不能真實的反應App穩定性.","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"比如打開一個頁面,可能發生多次異常,但並未崩潰,那麼按照會話崩潰率會得到 n/1這種不合理的異常率,尤其是混合開發中Flutter還不是app的全部功能的實現,通過會話和設備崩潰率統計還會有更多的偏差,因爲用戶打開App可能沒有使用Flutter功能.","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所以我們採用了一種新的統計口徑:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}}],"text":"頁面異常率, 用戶每打開一次頁面計做一次pv, 用 異常數/pv數得到","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過這種計算方式,我們不僅能夠得到app中Flutter本身整體的頁面異常率,還能在後續給單業務頁面計算頁面的穩定性.","attrs":{}}]},{"type":"horizontalrule","attrs":{}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"四、 結語","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以上,就是貝殼在Flutter大規模應用時監控異常穩定性的一些實踐和沉澱,希望對你有所幫助.對於Flutter異常的處理,未來還有一些需要做的,比如更細緻的分類、根據異常種類進行自定義閾值報警、以及異步堆棧回溯、單頁面分析等等,有機會的話會給大家帶來更多相關的文章.","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"感興趣的話記得點個喜歡","attrs":{}},{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}}],"text":"♥♥♥","attrs":{}},{"type":"text","text":"和關注","attrs":{}},{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}}],"text":"✩✩✩","attrs":{}},{"type":"text","text":",後續會第一時間收到其它文章.","attrs":{}}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章