天天看點

[iOS研習記]——記MJExtension多線程Crash的解決曆程

難纏的Crash問題

   本篇部落格的起源是由于收集到線上使用者産生的一些難纏的Crash問題,通過堆棧資訊觀察,Crash的堆棧資訊主要有兩類:

一類如下:

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%才是修複此問題。

   首先,根據前面我們對問題的分析和推理,可以從mj_objectWithKeyValues和mj_setup方法進行切入,通過對MJExtension代碼的Review,可以發現這些方法中有一個宏使用的非常頻繁,後來也證明問題确實出在這個宏的定義上:

[iOS研習記]——記MJExtension多線程Crash的解決曆程

這幾個宏的定義如下:

#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);

// 信号量

#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信号量變量被定義成了局部靜态變量,局部靜态 變量有一個特點:其被建立後會被放入全局資料區,但是其受函數作用域的控制,即建立後不會銷毀,函數内永遠可用,但是對函數外來說是隐藏的。如果在不同的函數中使用了相同名稱的靜态局部變量,真正放入全局資料區的實際上是多個不同的變量。

我們可以通過檢視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檔案的布局資訊如下:

[iOS研習記]——記MJExtension多線程Crash的解決曆程

可以看到,實際存儲的靜态變量名都被加上了函數字首。

到此,我們基本将問題定位到了,當多線程對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,使這個信号量變量被聲明為了一個全局變量,如下:

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,避免類過早的被加載。