[iOS研習記]——記MJExtension多線程Crash的解決歷程
難纏的Crash問題
本篇博客的起源是因爲收集到線上用戶產生的一些難纏的Crash問題,經過堆棧信息觀察,Crash的堆棧信息主要有兩類:多線程
一類以下:async
1 MJExtensionDemo 0x000000010903a5e0 main + 0, 2 MJExtension 0x000000010923f00d +[NSObject(MJClass) mj_setupBlockReturnValue:key:] + 333, 3 MJExtension 0x000000010923ec86 +[NSObject(MJClass) mj_setupIgnoredPropertyNames:] + 70, 4 MJExtensionTests 0x00000001095ebe1b -[MJExtensionTests testNestedModelArray] + 1467, 5 CoreFoundation 0x00007fff204272fc __invoking___ + 140, 6 CoreFoundation 0x00007fff204247b6 -[NSInvocation invoke] + 303,
一類以下:函數
1 MJExtensionDemo 0x000000010729e5e0 main + 0, 2 MJExtension 0x00000001074a3255 +[NSObject(MJClass) mj_totalObjectsWithSelector:key:] + 453, 3 MJExtension 0x00000001074a2ccf +[NSObject(MJClass) mj_totalIgnoredPropertyNames] + 47, 4 MJExtension 0x00000001074a3dcb -[NSObject(MJKeyValue) mj_setKeyValues:context:] + 443, 5 MJExtension 0x00000001074a3bdf -[NSObject(MJKeyValue) mj_setKeyValues:] + 79, 6 MJExtension 0x00000001074a6536 +[NSObject(MJKeyValue) mj_objectWithKeyValues:context:] + 710, 7 MJExtension 0x00000001074a623f +[NSObject(MJKeyValue) mj_objectWithKeyValues:] + 79,
此時使用的MJExtension版本爲3.2.4,雖然堆棧信息比較清楚,然而其最後的調用都是在MJExtension內部,且發生此Crash的概率很是小(約爲萬分之幾),定位和解決此Crash並不容易。佈局
經過分析,發現此Crash有以下特色:測試
- 調用棧中最終定位到的函數都在MJExtension進行JSON轉對象或模型setup配置時。
- 只有在多線程使用MJExtension方法時會出現此Crash。
- 是App在某次版本更新後纔開始出現此類Crash。
經過分析上面的特色,能夠推理出:優化
- 問題必定出在mj_objectWithKeyValues方法或mj_setup相關方法中。
- 此問題必定是因爲業務的某種使用方式或場景的改變觸發的。
- 必定和多線程相關,推測和鎖可能相關。
問題的定位與復現
對於iOS端開發,定位和解決Crash畢竟兩個流程,首先是根據線索來分析和定位問題,獲得一個大概的猜測,以後按照本身的猜測去提供外部條件,來嘗試復現問題,若是問題可以成功復現並復原與線程問題類似的堆棧現場,則基本完成了90%的工做,剩下的10%纔是修復此問題。spa
首先,根據前面咱們對問題的分析和推理,能夠從mj_objectWithKeyValues和mj_setup方法進行切入,經過對MJExtension代碼的Review,能夠發現這些方法中有一個宏使用的很是頻繁,後來也證實問題確實出在這個宏的定義上:線程
這幾個宏的定義以下:3d
#ifndef MJ_LOCK #define MJ_LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER); #endif #ifndef MJ_UNLOCK #define MJ_UNLOCK(lock) dispatch_semaphore_signal(lock); #endif // 信號量 #define MJExtensionSemaphoreCreate \ static dispatch_semaphore_t signalSemaphore; \ static dispatch_once_t onceTokenSemaphore; \ dispatch_once(&onceTokenSemaphore, ^{ \ signalSemaphore = dispatch_semaphore_create(1); \ }); #define MJExtensionSemaphoreWait MJ_LOCK(signalSemaphore) #define MJExtensionSemaphoreSignal MJ_UNLOCK(signalSemaphore)
能夠看到,這個宏的最終使用方式是經過信號量來實現鎖邏輯。問題出在static和宏定義自己,宏定義是作簡單的替換,所以在實際使用時,dispatch_semaphore_t信號量變量被定義成了局部靜態變量,局部靜態 變量有一個特色:其被建立後會被放入全局數據區,可是其受函數做用域的控制,即建立後不會銷燬,函數內永遠可用,可是對函數外來講是隱藏的。若是在不一樣的函數中使用了相同名稱的靜態局部變量,真正放入全局數據區的其實是多個不一樣的變量。code
咱們能夠經過查看C文件編譯後的.o可執行文件來驗證局部靜態變量的這一特色:
測試代碼以下:
#include <stdio.h> int main(int argc, const char * argv[]) { static char *string = "hello"; return 0; } void func1() { static char *string = "world"; }查看.o文件的佈局信息以下:
能夠看到,實際存儲的靜態變量名都被加上了函數前綴。
到此,咱們基本將問題定位到了,當多線程對MJExtension中的多個不一樣的函數進行調用時,若是這些函數中都有此加鎖邏輯,實際上這個鎖邏輯並無生效,會產生多線程數據讀寫Crash。要復現這個場景就很是簡單了:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ for (int i = 0; i < 1000; i++) { MJStatusResult *result = [MJStatusResult mj_objectWithKeyValues:dict]; } }); for (int i = 0; i < 1000; i++) { [MJStatus mj_setupIgnoredPropertyNames:^NSArray *{ return @[@"name"]; }]; }
經過場景復現,基本能夠定位此問題緣由。
幾個疑問的解答
1. 產生此Crash的核心原理
多線程鎖失效致使的多線程讀寫異常。
2.爲什麼版本更新後會出現
須要從業務使用上來分析,以前的版本相似mj_setup相關方法的調用會放入類的+load方法中,這個在main函數調用以前,全部類的解析配置都已完成,基本不會出現多線程問題,新版本作了冷啓動的優化,將mj_setup相關方法放入了+(void)initialize方法中,使得多線程問題被觸發的機率大大增長了。
MJExtension後續版本
截止到本篇博客編寫時間,MJExtension最新版本3.2.5已經處理了這個鎖問題的Bug,其修復方式是將static修改成了extern,使這個信號量變量被聲明爲了一個全局變量,以下:
#ifndef MJ_LOCK #define MJ_LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER); #endif #ifndef MJ_UNLOCK #define MJ_UNLOCK(lock) dispatch_semaphore_signal(lock); #endif // 信號量 #define MJExtensionSemaphoreCreate \ extern dispatch_semaphore_t mje_signalSemaphore; \ extern dispatch_once_t mje_onceTokenSemaphore; \ dispatch_once(&mje_onceTokenSemaphore, ^{ \ mje_signalSemaphore = dispatch_semaphore_create(1); \ }); // .m文件中 dispatch_semaphore_t mje_signalSemaphore; dispatch_once_t mje_onceTokenSemaphore;
修改後的代碼保證了鎖的惟一性。
建議
使用MJExtension庫時,若是須要進行解析配置,優先使用複寫相關配置+方法來實現,例如:
// 不建議的使用方式 + (void)initialize { [self mj_setupObjectClassInArray:^NSDictionary *{ return @{ @"nicknames" : MJStatus.class }; }]; } // 建議的使用方式 + (NSDictionary *)mj_objectClassInArray { return @{ @"nicknames" : @"MJStatus" }; }
而且,在配置類型時,儘可能使用NSString而不要使用Class,避免類過早的被加載。