文章目錄
App崩潰問題
app經常會遇見崩潰問題,比如下
- 數據越界
- 多線程操作同一指針,當指針爲空時崩潰
- 野指針問題
KVO
問題NSNotification
線程問題
以及不可捕獲的崩潰問題:
- 後臺任務超時
- App超過系統限制的內存大小被殺死
- 主線程卡頓被殺死
可捕獲的崩潰信息收集
對於可以捕獲的崩潰問題,我們可以通過崩潰日誌分析,例如PLCrashReporter 和bugly
這些三方崩潰分析工具。
PLCrashReporter實現
在崩潰日誌中,Exception信息頭部經常有
Exception Type: EXC_CRASH (SIGABRT)
SIGABRT
就代表着一種崩潰信息,類似還有SIGSEGV
、SIGBUS
PLCrashReporter
通過註冊這些崩潰signalHandler回調
就可以獲得崩潰信息。
在 PLCrashReporter
源碼中
/**
* Register a new signal @a callback for @a signo.
*
* @param signo The signal for which a signal handler should be registered. Note that multiple callbacks may be registered
* for a single signal, with chaining handled appropriately by the receiver. If multiple callbacks are registered, they may
* <em>optionally</em> forward the signal to the next callback (and the original signal handler, if any was registered) via PLCrashSignalHandlerForward.
* @param callback Callback to be issued upon receipt of a signal. The callback will execute on the crashed thread.
* @param context Context to be passed to the callback. May be NULL.
* @param outError A pointer to an NSError object variable. If an error occurs, this pointer will contain an error object indicating why
* the signal handlers could not be registered. If no error occurs, this parameter will be left unmodified. You may specify
* NULL for this parameter, and no error information will be provided.
*
* @warning Once registered, a callback may not be deregistered. This restriction may be removed in a future release.
* @warning Callers must ensure that the PLCrashSignalHandler instance is not released and deallocated while callbacks remain active; in
* a future release, this may result in the callbacks also being deregistered.
*/
- (BOOL) registerHandlerForSignal: (int) signo
callback: (PLCrashSignalHandlerCallbackFunc) callback
context: (void *) context
error: (NSError **) outError
{
/* Register the actual signal handler, if necessary */
if (![self registerHandlerWithSignal: signo error: outError])
return NO;
/* Add the new callback to the shared state list. */
plcrash_signal_user_callback reg = {
.callback = callback,
.context = context
};
shared_handler_context.callbacks.nasync_prepend(reg);
return YES;
}
可以查看 registerHandlerWithSignal
來查看實現。
PLCrashReporter
將這些崩潰堆棧信息存儲後,App就崩潰了,等待下次重啓時,就可以取到這些日誌,進行上傳分析。
系統接口
系統函數的優點在於性能好,但是隻能獲取簡單的信息,無法通過dSYM文件來了解是哪行代碼出的問題。
void InstallSignalHandler(void)
{
signal(SIGHUP, SignalExceptionHandler);
signal(SIGINT, SignalExceptionHandler);
signal(SIGQUIT, SignalExceptionHandler);
signal(SIGABRT, SignalExceptionHandler);
signal(SIGILL, SignalExceptionHandler);
signal(SIGSEGV, SignalExceptionHandler);
signal(SIGFPE, SignalExceptionHandler);
signal(SIGBUS, SignalExceptionHandler);
signal(SIGPIPE, SignalExceptionHandler);
}
void SignalExceptionHandler(int signal)
{
NSMutableString *mstr = [[NSMutableString alloc] init];
[mstr appendString:@"Stack:\n"];
void* callstack[128];
int i, frames = backtrace(callstack, 128);
char** strs = backtrace_symbols(callstack, frames);
for (i = 0; i <frames; ++i) {
[mstr appendFormat:@"%s\n", strs[i]];
}
//這裏寫入mstr到你的日誌文件
}
不可捕獲的崩潰
iOS後臺模式
- 後臺模式,例如
音樂播放
,VOIP
,地圖
類app Background Fetch
設置一個間隔來每隔一段時間請求網絡數據。由於用戶可以在設置中關閉這種模式,導致它的使用場景很少Slience Push
是推送的一種,會在後臺喚醒30
秒,優先級很低。Push Kit
後臺喚醒app後會保活30
秒,主要用於VOIP應用提升體驗。Background Task
是最常用也是使用最多的模式,會在進入後臺時請求3分鐘進行額外的操作。
對於 Background Task
你可以使用UIApplication
的beginBackgroundTaskWithName:expirationHandler:
或者beginBackgroundTaskWithExpirationHandler:
方法來申請一些額外的時間。
任務最多執行3
分鐘,超過時間,你的app將會被殺死,造成崩潰。這也是app退到後臺容易產生崩潰的原因。
對於這種崩潰,我們一般是在申請的3
分鐘的Task
中,開啓一個定時器,當快到3分鐘時檢測app還在運行就意味着App即將被系統殺死,這個時候進行記錄日誌上傳來達到監控的效果。
Runloop 卡頓
對於Runloop
的卡頓分析,我們可以通過通過添加runloop
的CFRunLoopAddObserver
添加觀察者來觀察主線程Runloop
的狀態,通過回調返回,然後我們開啓子線程一個loop
循環來檢測單位允許時間內(例如 3秒)
是否收到了該狀態,如果超過了該值,則說明這個狀態轉變的時間過長了
。
注意:這個3秒時間,我們可以通過看門狗每個階段允許的事件來判斷,我們可通過下面的Watch Dog各個階段允許的超過時間來設置,不要大於看門狗殺死app的時間
CFRunLoopObserverRef
是觀察者,每個 Observer 都包含了一個回調(函數指針),當 RunLoop 的狀態發生變化時,觀察者就能通過回調接受到這個變化。可以觀測的時間點有以下幾個:
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即將進入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 剛從休眠中喚醒
kCFRunLoopExit = (1UL << 7), // 即將退出Loop
kCFRunLoopAllActivities = 0x0FFFFFFFU // 所有的狀態監聽
};
我們可以通過添加runloop
的CFRunLoopAddObserver
添加觀察者來觀察kCFRunLoopBeforeSources
和kCFRunLoopAfterWaiting
來檢測卡頓。
爲什麼選擇這兩個時間段呢?系統事件(例如觸摸)大多以source1觸發,所以在
kCFRunLoopBeforeSources
之後執行,有人說那我們監聽kCFRunLoopBeforeSources
和kCFRunLoopBeforeWaiting
的時間不就好啦?但是runloop沒有事件時,是會停止在kCFRunLoopBeforeWaiting
的,所以我們就監聽kCFRunLoopAfterWaiting
,當兩個狀態回調時,將一個flag置爲NO(這裏是檢測3次來實現的),然後每隔幾秒查看這個flag,這樣監聽是否卡頓。
dispatchSemaphore = dispatch_semaphore_create(0); //Dispatch Semaphore保證同步
//創建一個觀察者
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
//將觀察者添加到主線程runloop的common模式下的觀察中
CFRunLoopAddObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);
//回調
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
SMLagMonitor *lagMonitor = (__bridge SMLagMonitor*)info;
lagMonitor->runLoopActivity = activity;
dispatch_semaphore_t semaphore = lagMonitor->dispatchSemaphore;
dispatch_semaphore_signal(semaphore);
}
再進行創建子線程
//創建子線程監控
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//子線程開啓一個持續的loop用來進行監控
while (YES) {
long semaphoreWait = dispatch_semaphore_wait(dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, STUCKMONITORRATE * NSEC_PER_MSEC));
if (semaphoreWait != 0) {
if (!runLoopObserver) {
timeoutCount = 0;
dispatchSemaphore = 0;
runLoopActivity = 0;
return;
}
//兩個runloop的狀態,BeforeSources和AfterWaiting這兩個狀態區間時間能夠檢測到是否卡頓
if (runLoopActivity == kCFRunLoopBeforeSources || runLoopActivity == kCFRunLoopAfterWaiting) {
//出現三次出結果
if (++timeoutCount < 3) {
continue;
}
// TODO:寫入堆棧信息到日誌
} //end activity
}// end semaphore wait
timeoutCount = 0;
}// end while
});
戴明老師的 GCDFetchFeed 中就包含了這個處理
關於Runloop
卡頓檢測的詳細說明,我會在另一篇博文中說明。
Watch Dog
內存打爆
和主線程卡頓時間超過閾值
都會被Watch Dog
殺死
Watchdog
在app各個階段所允許的超時時間
launch(啓動) 20s
resume(恢復) 10s
suspend(掛起) 10s
quit(退出) 6s
background(後臺) 3min(iOS7之前,申請10min,之後改爲每次申請3min,可連續申請直到10min)
看門狗Watch Dog
的崩潰異常代碼通常是“0x8badf00d
”,即“ate bad food
”。
一些被系統殺掉的情況,我們可以通過異常編碼來分析。你可以在維基百科上,查看完整的異常編碼。這裏列出了 44 種異常編碼,但常見的就是如下三種:
0x8badf00d
,表示 App 在一定時間內無響應而被 watchdog 殺掉的情況。
0xdeadfa11
,表示 App 被用戶強制退出。
0xc00010ff
,表示 App 因爲運行造成設備溫度太高而被殺掉。
內存達到單個App上限被殺死
App
達到iOS系統
對單個app
設置的內存佔用上限後,被系統殺死,我們稱之爲 out of memory(OOM)
我們如何獲取app的內存上限大小呢?
JetsamEvent 分析內存大小
對於內存問題殺死的問題,我們可以分析JetsamEvent
日誌。
我們可以通過手機的設置->隱私->分析->分析數據中
找到JetsamEvent
開頭的相關日誌信息。
先找到per-process-limit
內容
{
"uuid" : "092ff2cc-0290-3310-a375-cc69c192b94d",
"states" : [
"daemon",
"idle"
],
"killDelta" : 6781,
"lifetimeMax" : 8241,
"age" : 4581135570382,
"purgeable" : 485,
"fds" : 50,
"genCount" : 0,
"coalition" : 832,
"rpages" : 7736,
"reason" : "per-process-limit",
"pid" : 8326,
"idleDelta" : 174877955757,
"name" : "photoanalysisd",
"cpuTime" : 111.697557
},
"rpages" : 7736
表示內存頁數爲 7736
,再從文件中找到pageSize
字段
"largestZoneSize" : 15192064,
"pageSize" : 4096,
"uncompressed" : 77779,
"zoneMapSize" : 70713344,
則可以計算當前app內存限額 7736*4096/1024/1024 = 30MB
-
iOS是如何發現
JetsamEvent
?
iOS會開啓優先級最高級的線程vm_pressure_monitor
來監聽系統內存情況,通過一個堆棧來維護所有app的進程。此外iOS
還會維護一個內存快照表,來保存每個進程的內存頁消耗情況。當
iOS
覺得內存有壓力,就會發出通知,告訴那些內存有壓力的app去釋放內存,didReceiveMemoryWarning
就是這裏產生,在這裏釋放內存能夠可能避免你的app被系統殺死。 -
iOS殺死線程的優先級
iOS內核
有個數組,專門維護線程的優先級。
內核使用的線程的優先級最高,操作系統其次,App
最後。
App在前臺的優先級高於後臺,且佔用線程多的App優先級將會降低
App在被iOS系統殺死前,系統大概有6s
時間來判斷優先級,Jetsam
日誌也是這個時間生成的.
XNU獲取內存限值
XNU獲取需要Root
權限,通過memorystatus_priority_entry
結構體獲取
// 獲取進程的 pid、優先級、狀態、內存閾值等信息
typedef struct memorystatus_priority_entry {
pid_t pid;
int32_t priority;
uint64_t user_data;
int32_t limit;
uint32_t state;
} memorystatus_priority_entry_t;
參考文章 : https://www.jianshu.com/p/2a283df2e839
task_info接口
根據XNU
源碼,我們可以知道apple獲取app使用信息的函數。
系統提供了一個函數獲取當前任務的信息,我們可以通過phys_footprint
獲取app的使用內存
#import <mach/mach.h>
uint64_t memoryFootprint() {
task_vm_info_data_t vmInfo;
mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
kern_return_t result = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t) &vmInfo, &count);
if (result != KERN_SUCCESS)
return 0;
return vmInfo.phys_footprint;
}
內存分配監聽
內存分配函數malloc
和calloc
等默認使用的是nano_zone
,nano_zone
是256B
以下內存的分配,大於256B
的時候會使用 scalable_zone
來分配。
使用 scalable_zone
分配內存的函數都會調用malloc_logger
函數,因爲系統需要有一個地方統計並管理內存的分配情況。
我們可以通過 fishhook
進行hook
內存分配的 mallloc_logger
,就可以掌握內存分配的大小。
參考文章 :
https://www.jianshu.com/p/302ed945e9cf
https://www.jianshu.com/p/2a283df2e839