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

[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。

經過分析上面的特色,能夠推理出:優化

  1. 問題必定出在mj_objectWithKeyValues方法或mj_setup相關方法中。
  2. 此問題必定是因爲業務的某種使用方式或場景的改變觸發的。
  3. 必定和多線程相關,推測和鎖可能相關。

問題的定位與復現

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

相關文章
相關標籤/搜索