技術貼:手游頻繁崩潰"閃退"的原因是什么?如何解決

>>>  技術話題—商業文明的嶄新時代  >>> 簡體     傳統

作者:網易 Tjay(QA)

作為玩家,當游戲crash的時候是什么心情,如果這個游戲玩起來還不錯的話,那我可能還會打開第二次,如果這個游戲一般的話我可能直接怒刪了。當多次出現閃退crash的時候,這種糟糕的體驗很容易讓用戶流失,造成很大的損失。但是作為測試人員,面對如此棘手的事情,首先要做的是協助開發組解決問題。沒錯,第一件要做的事情就是去定位crash發生的代碼邏輯,到底是哪個文件的哪一段函數邏輯導致了這個crash問題。因此,我們需要去盡量重現crash場景,收集解析crash日志,以此定位到具體到游戲代碼邏輯中尋找導致crash的原因,改善項目的質量和體驗。本文闡述在App crash產生的原理,收集和解析過程,旨在經驗積累,與大家分享。


一.crash產生的原因


當iOS/Android設備上的App應用閃退時,操作系統會生成一個crash日志,保存在設備上。crash日志上有很多有用的信息,比如每個正在執行線程的完整堆棧跟蹤信息和內存映像,這樣就能夠通過解析這些信息進而定位crash發生時的代碼邏輯,從而找到App閃退的原因。通常來說,crash產生來源于兩種問題:違反iOS系統規則導致的crash和App代碼邏輯BUG導致的crash,下面分別對他們進行分析。


1.1違反iOS系統規則包括三種類型:


(1) 內存報警閃退


當iOS檢測到內存過低時,它的VM系統會發出低內存警告通知,嘗試回收一些內存;如果情況沒有得到足夠的改善,iOS會終止后臺應用以回收更多內存;最后,如果內存還是不足,那么正在運行的應用可能會被終止掉。在Debug模式下,可以主動將客戶端執行的動作邏輯寫入一個log文件中,這樣程序童鞋可以將內存預警的邏輯寫入該log文件,當發生如下截圖中的內存報警時,就是提醒當前客戶端性能內存吃緊,可以通過Instruments工具中的Allocations 和 Leaks模塊庫來發現內存分配問題和內存泄漏問題。


(2) 響應超時


當應用程序對一些特定的事件(比如啟動、掛起、恢復、結束)響應不及時,蘋果的Watchdog機制會把應用程序干掉,并生成一份相應的crash日志。這些事件與下列UIApplicationDelegate方法相對應,當遇到Watchdog日志時,可以檢查上圖中的幾個方法是否有比較重的阻塞UI的動作。


application:didFinishLaunchingWithOptions:


applicationWillResignActive:


applicationDidEnterBackground:


applicationWillEnterForeground:


applicationDidBecomeActive:


applicationWillTerminate:


(3) 用戶強制退出


一看到“用戶強制退出”,首先可能想到的雙擊Home鍵,然后關閉應用程序。不過這種場景一般是不會產生crash日志的,因為雙擊Home鍵后,所有的應用程序都處于后臺狀態,而iOS隨時都有可能關閉后臺進程,當應用阻塞界面并停止響應時這種場景才會產生crash日志。


這里指的“用戶強制退出”場景,是稍微比較復雜點的操作:先按住電源鍵,直到出現“滑動關機”的界面時,再按住Home鍵,這時候當前應用程序會被終止掉,并且產生一份相應事件的crash日志。


1.2應用邏輯的Bug


大多數閃退崩潰日志的產生都是因為應用中的Bug,這種Bug的錯誤種類有很多,比如


SEGV:(Segmentation Violation,段違例),無效內存地址,比如空指針,未初始化指針,棧溢出等;


SIGABRT:收到Abort信號,可能自身調用abort()或者收到外部發送過來的信號;


SIGBUS:總線錯誤。與SIGSEGV不同的是,SIGSEGV訪問的是無效地址(比如虛存映射不到物理內存),而SIGBUS訪問的是有效地址,但總線訪問異常(比如地址對齊問題);


SIGILL:嘗試執行非法的指令,可能不被識別或者沒有權限;


SIGFPE:Floating Point Error,數學計算相關問題(可能不限于浮點計算),比如除零操作;


SIGPIPE:管道另一端沒有進程接手數據;


常見的崩潰原因基本都是代碼邏輯問題或資源問題,比如數組越界,訪問野指針或者美術資源不存在,或美術資源大小寫錯誤等,這種問題的類型有很多,不再詳細介紹。


二.crash的收集


上文提到crash日志是操作系統層產生并保存在設備上的,那如果我的一臺設備在運行某App的時候crash了,可以通過什么方式拿到crash日志呢。如果是在windows上你可以通過itools或pp助手等輔助工具查看系統產生的歷史crash日志,然后再根據app來查看。如果是在Mac 系統上,只需要打開xcode->windows->organizer->devices,選擇device logs進行查看,如下圖,這些crash文件都可以導出來,然后再單獨對這個crash文件做處理分析。


以上這些是針對能夠拿到真機設備的情況下才能收集crash日志的。如果是針對玩家的話,當App在玩家的設備上crash的時候如何收集呢。先來看下市場上已有的商業軟件提供crash收集服務,他們這些軟件基本都提供了日志存儲,日志符號化解析和服務端可視化管理等服務:


Crashlytics (www.crashlytics.com)


Crittercism (www.crittercism.com)


Bugsense (www.bugsense.com)


TestFlight (www.testflightapp.com)


HockeyApp (www.hockeyapp.net)


Flurry(www.flurry.com)


具體這些商業軟件有哪些優缺點,有人做了如下統計:


除了上述所說的這些商業軟件外,還有一些開源的軟件也可以拿來收集crash日志,比如Razor,QuincyKit(git鏈接)等,這些軟件收集crash的原理其實大同小異,都是根據系統產生的crash日志進行了一次提取或封裝,然后將封裝后的crash文件上傳到對應的服務端進行解析處理。很多商業軟件都采用了Plcrashreporter這個開源工具來上傳和解析crash,比如HockeyApp,Flurry和crittercism等,下圖是筆者利用這一開源框架制作的一個收集crash的樣例。


通過這種方式就可以很好的支持開發人員收集crash日志的需求,進而定位和解決App產品存在的問題。如果有需要或者感興趣的可以深入的調研一下。


但是有個很重要的問題就是這種方式只能收集游戲引擎層(c++或object c代碼)的邏輯,如果是腳本邏輯問題產生的crash就無能無力了。而現在手游項目基本都是引擎(cocos2dx或Neox)+腳本(lua或javascript)的開發模式,幾乎所有的業務邏輯都在腳本層,游戲App時常發生的crash幾乎都是由腳本邏輯bug導致的,這該怎么處理呢?平時在開發階段,程序童鞋在Debug模式下開通了客戶端運行日志功能,當出現crash或者traceback等問題的時候直接去查看log文件的輸出即可知道原因了,但是在Release模式下一切log輸出均被屏蔽,邏輯運行的log消息輸出也就無法查看了。這種情況該又該如何處理呢?方法總比問題多,iOS/Android系統提供了異常發生時的處理API,只需要在程序啟動的地方加入對應的處理邏輯,當異常發生時就可以觸發對應的回調函數將必要的信息進行處理上傳,適時地反饋給開發組。比如,下圖是某項目組在iOS平臺收集crash的一個截圖:


其實,它具體的實現原理是這樣的:首先,在游戲應用程序啟動的地方需要開啟異常處理邏輯的handler:


最后需要當crash發生時,需要調用的回調函數處理具體如下:


這樣在當玩家在Release游戲版本中出現邏輯異常導致crash時,就會把對應的腳本層的異常(traceback或error等)以類似dump文件的形式發送到指定的服務端,方便運營維護人員進行快速定位分析。這些腳本層異常日志收集后的顯示效果如下:


以具體某一個異常日志文件為例,具體上傳的內容如下圖。這是一種直接可讀的文本,里面記錄著crash發生時代碼邏輯的traceback,通過閱讀代碼邏輯就可以直接定位到或推斷導致cras


以上就是收集crash的方法和原理,通過這種方式收集到crash日志后接下來就可以具體根據日志的內容進行解析來定位到底是什么原因導致的crash。


三.crash日志的解析


如果是腳本層邏輯導致的crash,如上所述,這種情況是可以直接根據收集的日志內容來定位導致crash的邏輯的。如果是引擎層發生了問題,該如何定位解析呢。先來看一個crash的栗子:

如上圖所示,


1)crash標識是應用進程產生crash時的一些標識信息,它描述了該crash的唯一標識(E838FEFB-ECF6-498C-8B35-D40F0F9FEAE4),所發生的硬件設備類型(iphone3,1代表iphone4),以及App進程相關的信息等;


2)基本信息描述的是crash發生的時間和系統版本;


3)異常類型描述的是crash發生時拋出的異常類型和錯誤碼;


4)線程回溯描述了crash發生時所有線程的回溯信息,每個線程在每一幀對應的函數調用信息(這里由于空間限制沒有全部列出);


5)二進制映像是指crash發生時已加載的二進制文件。以上就是一份crash日志包含的所有信息,接下來就需要根據這些信息去解析定位導致crash發生的代碼邏輯, 這就需要用到符號化解析的過程(洋名叫:symbolication)。


符號化解析過程有三種方法:


  1. xcode可視化查看,

  2. symbolicatecrash工具,

  3. atos工具;但是這三種方法都需要用到構建app時生成的.app文件和.app.dsym這兩個文件,第一種方式已經在第二章節提到過,不再贅述,下面介紹第二種和第三種解析的方式。



3.1 symbolicatecrash解析


symbolicatecrash是xcode自帶的一個命令行工具,在xcode5.0以前的位置是/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/PrivateFrameworks/DTDeviceKit.framework/Versions/A/Resources/,xcode5.0以后路徑就變成了/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/PrivateFrameworks/DTDeviceKitBase.framework/Versions/A/Resources/


比如以上述提到的TestFlight App為例,將TestFlight .crash?TestFlight .app和TestFlight .app.dsym三個文件放在同一個目錄下,然后運行 symbolicatecrash?TestFlight.crash TestFlight.app.dsym>?TestFlight .log,查看TestFlight.log文件的內容:


從圖中連線可以看出具體出現問題的邏輯代碼是在那個文件的哪一行,這樣就根據解析出來的指定函數來定位crash的發生原因。


3.2 atos方法


atos是一個BSD平臺的通用指令,通過它可以將數字地址轉換為對應的二進制映像或者進程的符號,通過該指令進行符號化解析的時候需要說明一點的是只有當.app文件、crash文件和.app.dsym文件三者的UUID都是一致的時候,該crash文件才能被正確解析,否則解析失敗。(注:uuid是app應用在移動設備上的唯一標識)可以通過以下方式來查看.app和.app.dsym文件的uuid,以上述提到的TestFlight應用為例:


而crash日志文件的uuid在二進制映像中的第一行:


由此可見armv7架構下三者保持一致,都是4a42d422a466338aa56e88840b74de3d,那接下來開始進行符號化解析。


從上文crash日志文件的線程回溯可以發現閃退時函數的回溯列表里格式不是完全一致,比如下圖中的方式1和方式2在第2列的表達方式上不太一樣,方式1是用的庫函數名,方式2則是一個基本地址。其實這兩種方式都可以用一種通用的解析方式來搞定:


首先計算加載地址(load address):


以方式1中的0x333d8049 UIApplicationMain + 1137 為例,這一幀對應的 load address=0x333d8049 -1137=0x333d7bd8


也就是UIApplicationMain的地址是0x333df12;方式2的0x00068b19 0x36000 + 207641,通過上述方式的計算就是load address=0x00068b19 -207641=0x36000 ,可以發現結果與第二列的值是相同的,也就是它的加載地址就是第二列的值0x36000? 然后用xcrun atos -arch armv7 -o TestFlight.app/TestFlight? 0x36000 0x00068b19 的指令來解析crash日志線程0和線程3中帶有TestFlight模塊的地址,結果發現TestFlight程序的代碼回溯過程:


可以看出base


address(基地址)是4000,函數的回溯過程是main.m文件的第16行的某個函數出現問題,然后該函數在邏輯調用中會調用到AFURLConnnectionOperation.m文件的第162行的某個函數,這個邏輯的調用與第一種方法解析的TestFlight.log文件作對比,crash的解析完全一致,由此就可以定位到crash的原因所在,接下來去解決crash文件也就水到渠成了。


四.小結


以上是根據自己的經驗和理解對iOS平臺下的crash問題(包括原理、收集和解析過程)進行的一次剖析,雖然蘋果的沙盒系統對iOS平臺的下的很多應用信息的提取有較多的限制,但是要相信方法總比問題多。對于crash問題的理解和收集過程可以很好地輔助項目組來提高項目的質量,同時對于更深入地理解iOS平臺知識和crash原理有很好的幫助。當然,本文更多的涉及iOS平臺下的crash問題,對于Android平臺的crash問題涉及較少。雖然細節的實現上可能有差異,但是內部的原理邏輯應該是相同或者相似的,后續筆者還將繼續關注關于Android平臺相關問題的調研學習。




GameLook 2015-08-23 08:50:02

[新一篇] 你還缺什么?老司機教你簡單做好游戲中“攻擊動作”的3個竅門

[舊一篇] 如何衡量地震災害程度
回頂部
寫評論


評論集


暫無評論。

稱謂:

内容:

驗證:


返回列表