【HarmonyOS Next】鴻蒙應用崩潰處理思路詳解
一、崩潰問題發現后定位
1. 崩潰現象:
常見的崩潰問題表現為,應用操作后白屏閃退,或者應用顯示無響應卡死。
2.定位問題:
發現崩潰后,我們首先需要了解復現步驟,精確定位復現步驟。因為提供復現步驟得人,可能是用戶和測試,非開發人員,其中的步驟并非最短路徑。
3.排查問題點
根據復現步驟,我們需要查看日志表現,鴻蒙的DevEco IDE提供了日志看板,根據HiLog和FaultLog,我們可以初步區分崩潰問題的類型。
根據日志看板的FaultLog,可以查看崩潰輸出的
- JS Crash,一般是ArkTS原生邏輯的崩潰
- CppCrash,一般是NDK,C層的崩潰
JS Crash
點擊進入JS Crash中,可以查看到崩潰信息,以下幾種類型的錯誤:
可以根據JS Crash提示的錯誤行數,直接點擊跳轉到錯誤代碼處,根據提示檢查和修復問題。
Device info:xxx
Build info:xxx-xxx x.x.x.xxx(xxxx)
Fingerprint:a370fceb59011d96e41e97bda139b1851c911012ab8c386d1a2d63986d6d226d
Module name:com.xxx.xxX
Version:1.0.0
Versioncode:1000000
PreInstalled:No
Foreground:Yes
Pid:39185Uid:20020145
Reason:TypeError
Error name:TypeError
Error message:Cannot read property needRenderTranslate of undefined
Stacktrace:Cannot get SourceMap info, dump raw stack:at anonymous (entry/src/main/ets/pages/Index.ts:49:49)
this.translationUpY = (this,multiCardsNum >= 1)? sceneContainerSessionListlthis.multiCardsNum -、
1].needRenderTranslate.translateY :0
this.translationDownY = (this.multiCardsNum >= 2)? sceneContainerSessionList[this.multiCardsNum -
2].needRenderTranslate.translateY :0;
CppCrash
根據日志提示,檢查是否有以下錯誤情況:
AppFreeze
一般是由于耗時操作,導致堵塞主線程。此時用戶操作時,會觸發無響應。一般分為以下三種情況,超時時間一般在6s左右。
內存泄漏
這種問題排查起來是最麻煩的,所以保持良好的代碼編程規范,不需要的對象該釋放釋放,不用的句柄也需要釋放。
一般碰到內存泄漏,根據提供的復現步驟,很多情況下是非必現。(如果是必現,那最好了,修復該問題會很快。)我們需要操作復現步驟,使用鴻蒙DevEco IDE的ProFiler工具:
檢測應用操作時的內存使用情況。
二、問題解決,檢查類似錯誤情況一起修復
1.根據章節一問題定位清楚后,根據錯誤具體情況,思考修復方案
2.根據問題情況,檢查其他代碼是否有同類問題,一起修復后驗證。
3.根據官網提供的材料進行學習,編碼過程中進行規范和排查:
性能優化舉例:
業務場景是:點擊跳轉下一頁,直接加載Web頁面。這是大多數三方應用的實現方式,其實應該在后臺創建一個ArkWeb組件來預先啟動用于渲染的Web渲染進程。這樣跳轉到web頁面的時間就不會那么長:
// 創建NodeController
// common.ets
import { UIContext } from '@kit.ArkUI';
import { webview } from '@kit.ArkWeb';
import { NodeController, BuilderNode, Size, FrameNode } from '@kit.ArkUI';// @Builder中為動態組件的具體組件內容
// Data為入參封裝類
class Data {url: string = 'https://www.example.com';controller: WebviewController = new webview.WebviewController();
}
function webBuilder(data: Data) {Column() {Web({ src: data.url, controller: data.controller }).domStorageAccess(true).zoomAccess(true).fileAccess(true).mixedMode(MixedMode.All).width('100%').height('100%').onPageEnd((event) => {// 輸出Web頁面加載完成時間console.info(`load page end time: ${Date.now()}`);})}
}let wrap = wrapBuilder<Data[]>(webBuilder);// 用于控制和反饋對應的NodeContainer上的節點的行為,需要與NodeContainer一起使用
export class MyNodeController extends NodeController {private rootnode: BuilderNode<Data[]> | null = null;private root: FrameNode | null = null;private rootWebviewController: webview.WebviewController | null = null;// 必須要重寫的方法,用于構建節點數、返回節點掛載在對應NodeContainer中// 在對應NodeContainer創建的時候調用、或者通過rebuild方法調用刷新makeNode(uiContext: UIContext): FrameNode | null {console.info(' uicontext is undefined : ' + (uiContext === undefined));if (this.rootnode != null) {const parent = this.rootnode.getFrameNode()?.getParent();if (parent) {console.info(JSON.stringify(parent.getInspectorInfo()));parent.removeChild(this.rootnode.getFrameNode());this.root = null;}this.root = new FrameNode(uiContext);this.root.appendChild(this.rootnode.getFrameNode());// 返回FrameNode節點return this.root;}// 返回null控制動態組件脫離綁定節點return null;}// 當布局大小發生變化時進行回調aboutToResize(size: Size) {console.info('aboutToResize width : ' + size.width + ' height : ' + size.height);}// 當controller對應的NodeContainer在Appear的時候進行回調aboutToAppear() {console.info('aboutToAppear');}// 當controller對應的NodeContainer在Disappear的時候進行回調aboutToDisappear() {console.info('aboutToDisappear');}// 此函數為自定義函數,可作為初始化函數使用// 通過UIContext初始化BuilderNode,再通過BuilderNode中的build接口初始化@Builder中的內容initWeb(url: string, uiContext: UIContext, control: WebviewController) {if (this.rootnode != null) {return;}// 綁定預創建的WebviewControllerthis.rootWebviewController = control;// 創建節點,需要uiContextthis.rootnode = new BuilderNode(uiContext);// 創建動態Web組件this.rootnode.build(wrap, { url: url, controller: control });}// 此函數為自定義函數,可作為初始化函數使用loadUrl(url: string) {if (this.rootWebviewController !== null) {// 復用預創建組件,重新加載urlthis.rootWebviewController.loadUrl(url);}}
}// 創建Map保存所需要的NodeController
let NodeMap: Map<string, MyNodeController | undefined> = new Map();
// 創建Map保存所需要的WebViewController
let controllerMap: Map<string, WebviewController | undefined> = new Map();// 初始化需要UIContext 需在Ability獲取
export const createNWeb = (url: string, uiContext: UIContext) => {// 創建NodeControllerlet baseNode = new MyNodeController();let controller = new webview.WebviewController();// 初始化自定義web組件baseNode.initWeb(url, uiContext, controller);controllerMap.set(url, controller);NodeMap.set(url, baseNode);
};// 自定義獲取NodeController接口
export const getNWeb = (url: string): MyNodeController | undefined => {// 加載新的Url時,建議復用預創建的Web組件if (!NodeMap.get(url) && NodeMap.get('about://blank')) {// 獲取預創建的Web組件let webNode = NodeMap.get('about://blank') as MyNodeController;// 重新加載urlwebNode.loadUrl(url);return webNode;}return NodeMap.get(url);
};
三、思考如何避免問題再次發生,復盤
1.保持好的開發習慣創建踩坑文檔,避免自己第二次再發生該問題
2.根據錯誤情況,分析是否為自己代碼邏輯問題,邏輯bug不可避免,只能從開發經驗和code review中盡量避免
3.代碼容錯分支問題,只保證了主流程,未考慮代碼錯誤分支的覆蓋情況。這種情況,需要自己吸取教訓,避免再次發生。
4.使用檢測工具對代碼進行掃描,提前規避一些問題:
5.使用IDE提供的AppAnglyzer生成應用檢測報告。根據報告提示,進行修改:
6.使用鴻蒙提供的DevEco Testing工具,進行穩定性和功耗等測試: