使用method-swizzling讓程序更健壯

問題

熟悉iOS開發(fā)的都知道,如果我們往Array或Dictionary中插入nil,應用就會崩潰。如有下面客戶端代碼:

- (void)testDictionaryNullable {
    id nilObj  = nil;
    
    // 1. 此處會引起崩潰
    NSDictionary *dict = @{@"aa": nilObj, @"bb": @"bb"};
    NSLog(@"dict: %@", dict);
    
    NSMutableDictionary *mDict = [NSMutableDictionary new];
    mDict[@"aaa"] = nilObj; // 此處不會有問題
    [mDict setObject:@"bbb" forKey:@"bbb"];
    
    // 2. 此處會引起崩潰
    [mDict setObject:nilObj forKey:@"ccc"];

    NSLog(@"mDict: %@", mDict);
}

- (void)testArrayNullable {
    id nilObj  = nil;
    
    // 3. 此處會引起崩潰
    NSArray *array = @[@"aa", nilObj];
    NSLog(@"array: %@", array);
    
    NSMutableArray *mArray = [[NSMutableArray alloc] initWithCapacity:2];
    
    // 4. 此處會引起崩潰    
    mArray[0] = nilObj;
    
    // 5. 此處會引起崩潰        
    [mArray addObject:nilObj];
    [mArray addObject:@"bbb"];
    NSLog(@"mArray: %@", mArray);
}

注: mDict[@"aaa"] = nilObj; 系統(tǒng)默認可以進行空值檢查,不會有崩潰問題。所以以后還是推薦使用這種帶下標的方式來寫恐锣。

另外,解析json時舞痰,除了像mantle等json庫會把空值轉(zhuǎn)換為nil土榴,系統(tǒng)或其他第三方庫在解析json時,會把json中的null值轉(zhuǎn)換為NSNull响牛,而如果你沒有好好做檢查的話玷禽,把它當NSString或NSNumber來處理的話,就會出現(xiàn)各種unrecognized selector異常娃善,如:

- (void)testNSNull {
    NSString *nilObj = (NSString *)[NSNull null];
    
    NSLog(@"null:%d", [nilObj length]);
}

解決

當然论衍,我們可以小心地使用Array、Dictionary和NSNull聚磺,對它們進行充分的檢查坯台。但是,我們?yōu)槭裁床荒芟裣到y(tǒng)對NSMutableDictionary使用下標寫法時的處理一樣瘫寝,當插入或設置的值為nil時蜒蕾,我們不插入或設置,這樣可以保證程序不異常焕阿,也可以大量減少對返回數(shù)據(jù)的判斷咪啡。同時,讓對NSNull的任何調(diào)用暮屡,都不報錯撤摸,也可以減少對NSNull的檢查。

我們可以使用method swizzling達到上述效果。method swizzling可以查看參考資料1准夷。下面分別對Dictionary钥飞、Array、NSNull分別進行處理衫嵌。

對Dictionary進行method swizzling

我們首先對Dictionary進行處理读宙。先查看上面客戶端代碼中,第一處引起崩潰的地方楔绞,即構(gòu)造NSDictionary時结闸。我們查看console中的崩潰堆棧,如下:


我們可以在調(diào)用+[NSDictionary dictionaryWithObjects:forKyes:count] 時酒朵,對objects進行空值過濾桦锄,如果是空值的話,不插入字典耻讽。相關(guān)代碼如下:

@implementation NSDictionary (Safe)

+ (void)load {
    Method originalMethod = class_getClassMethod(self, @selector(dictionaryWithObjects:forKeys:count:));
    Method swizzledMethod = class_getClassMethod(self, @selector(na_dictionaryWithObjects:forKeys:count:));
    method_exchangeImplementations(originalMethod, swizzledMethod);
}

+ (instancetype)na_dictionaryWithObjects:(const id [])objects forKeys:(const id <NSCopying> [])keys count:(NSUInteger)cnt {
    id nObjects[cnt];
    id nKeys[cnt];
    int i=0, j=0;
    for (; i<cnt && j<cnt; i++) {
        if (objects[i] && keys[i]) {
            nObjects[j] = objects[i];
            nKeys[j] = keys[i];
            j++;
        }
    }
    
    return [self na_dictionaryWithObjects:nObjects forKeys:nKeys count:j];
}
@end

經(jīng)過這樣處理后察纯,客戶端代碼1處的代碼就不會崩潰了。

我們再來看针肥,客戶端代碼中的2處,對NSMutalbeDictionary調(diào)用setObject:forKey:香伴,同樣慰枕,我們查看崩潰堆棧,如下:

注: iOS中使用了大量的class cluster即纲,像NSMutaleDictionary 實例化出來的具帮,實際上是__NSDictionaryM 類,而NSDictionary 實例化出來的低斋,實際上是__NSPlaceholderDictionary蜂厅。class cluster的介紹也可以查看參考資料1。

我們可以用method swizzling修改-[__NSDictionaryM setObject:forKey:] 方法,讓它在設值時膊畴,先判斷是否value為空掘猿,為空則不設置。代碼如下:

@implementation NSMutableDictionary (Safe)

+ (void)load {
    Class dictCls = NSClassFromString(@"__NSDictionaryM");
    Method originalMethod = class_getInstanceMethod(dictCls, @selector(setObject:forKey:));
    Method swizzledMethod = class_getInstanceMethod(dictCls, @selector(na_setObject:forKey:));
    method_exchangeImplementations(originalMethod, swizzledMethod);
}

- (void)na_setObject:(id)anObject forKey:(id<NSCopying>)aKey {
    if (!anObject)
        return;
    [self na_setObject:anObject forKey:aKey];
}

@end

對Array進行method swizzling

使用method swizzling對Array的處理唇跨,與Dictionary的處理稠通,差不多。先打印崩潰堆棧买猖,然后把相關(guān)函數(shù)進行method swizzling改橘。代碼如下:

@implementation NSArray (Safe)

+ (void)load {
    Method originalMethod = class_getClassMethod(self, @selector(arrayWithObjects:count:));
    Method swizzledMethod = class_getClassMethod(self, @selector(na_arrayWithObjects:count:));
    method_exchangeImplementations(originalMethod, swizzledMethod);
}

+ (instancetype)na_arrayWithObjects:(const id [])objects count:(NSUInteger)cnt {
    id nObjects[cnt];
    int i=0, j=0;
    for (; i<cnt && j<cnt; i++) {
        if (objects[i]) {
            nObjects[j] = objects[i];
            j++;
        }
    }
    
    return [self na_arrayWithObjects:nObjects count:j];
}
@end

@implementation NSMutableArray (Safe)

+ (void)load {
    Class arrayCls = NSClassFromString(@"__NSArrayM");
    
    Method originalMethod1 = class_getInstanceMethod(arrayCls, @selector(insertObject:atIndex:));
    Method swizzledMethod1 = class_getInstanceMethod(arrayCls, @selector(na_insertObject:atIndex:));
    method_exchangeImplementations(originalMethod1, swizzledMethod1);
    
    Method originalMethod2 = class_getInstanceMethod(arrayCls, @selector(setObject:atIndex:));
    Method swizzledMethod2 = class_getInstanceMethod(arrayCls, @selector(na_setObject:atIndex:));
    method_exchangeImplementations(originalMethod2, swizzledMethod2);
}

- (void)na_insertObject:(id)anObject atIndex:(NSUInteger)index {
    if (!anObject)
        return;
    [self na_insertObject:anObject atIndex:index];
}

- (void)na_setObject:(id)anObject atIndex:(NSUInteger)index {
    if (!anObject)
        return;
    [self na_setObject:anObject atIndex:index];
}

@end

對NSNull進行處理

對NSNull的unrecognized selector,可以運用 runtime 的知識玉控,在forwarding時飞主,對無法識別的selector,不拋出異常。相關(guān)知識可以參考資料2碌识。代碼如下:

@implementation NSNull (Safe)

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    NSMethodSignature *signature = [super methodSignatureForSelector:sel];
    if (!signature) {
        signature = [NSMethodSignature signatureWithObjCTypes:@encode(void)];
    }
    return signature;
}

- (void)forwardInvocation:(NSInvocation *)invocation {
}

總結(jié)

本文使用method swizzling讽挟,對Dictionary、Array進行處理丸冕,使它們在處理空值時耽梅,也不會異常而導致程序崩潰。

對NSNull胖烛,重寫forwarding相關(guān)函數(shù)眼姐。使得當json解析成NSNull時,就算把它當成其他類處理佩番,也不會拋異常而導致程序崩潰众旗。

相關(guān)代碼,已經(jīng)上傳github

參考資料

1. Method Swizzling(小笨狼的博客)

2. Crash Reports-解開記憶體位置(KKBOX iOS/Mac OS X 基本開發(fā)教材)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末趟畏,一起剝皮案震驚了整個濱河市贡歧,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌赋秀,老刑警劉巖利朵,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異猎莲,居然都是意外死亡绍弟,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進店門著洼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來樟遣,“玉大人,你說我怎么就攤上這事身笤”” “怎么了?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵液荸,是天一觀的道長瞻佛。 經(jīng)常有香客問我,道長莹弊,這世上最難降的妖魔是什么涤久? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮忍弛,結(jié)果婚禮上响迂,老公的妹妹穿的比我還像新娘。我一直安慰自己细疚,他們只是感情好蔗彤,可當我...
    茶點故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般然遏。 火紅的嫁衣襯著肌膚如雪贫途。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天待侵,我揣著相機與錄音丢早,去河邊找鬼。 笑死秧倾,一個胖子當著我的面吹牛怨酝,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播那先,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼农猬,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了售淡?” 一聲冷哼從身側(cè)響起斤葱,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎揖闸,沒想到半個月后揍堕,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡楔壤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年鹤啡,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蹲嚣。...
    茶點故事閱讀 38,100評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖祟牲,靈堂內(nèi)的尸體忽然破棺而出隙畜,到底是詐尸還是另有隱情,我是刑警寧澤说贝,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布议惰,位于F島的核電站,受9級特大地震影響乡恕,放射性物質(zhì)發(fā)生泄漏言询。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一傲宜、第九天 我趴在偏房一處隱蔽的房頂上張望运杭。 院中可真熱鬧,春花似錦函卒、人聲如沸辆憔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽虱咧。三九已至熊榛,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間腕巡,已是汗流浹背玄坦。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留绘沉,地道東北人煎楣。 一個月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像梆砸,于是被迫代替她去往敵國和親转质。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,834評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉帖世,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,681評論 0 9
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理休蟹,服務發(fā)現(xiàn),斷路器日矫,智...
    卡卡羅2017閱讀 134,600評論 18 139
  • 親愛的哪轿,每次看到你盈魁, 看到的都是一個背影, 在前行的道路上窃诉, 我們不是應該肩并肩嗎杨耙? 然而, 你什么時候可以回頭看...
    張慶玲閱讀 623評論 2 2
  • 文/千花千樹 人生有喜车柠,亦有悲,是一個不斷成長和無奈老去的過程塑猖。在這個過程中竹祷,每個人都會經(jīng)歷3個重大時刻。其中羊苟,2...
    千花千樹閱讀 383評論 0 0
  • 從微信公號上看到一則故事:山上的寺院里有一頭驢塑陵,每天都在磨房里辛苦拉磨,天長日久践险,驢漸漸厭倦了這種平淡的生活猿妈。它每...
    星光下的月亮閱讀 865評論 3 4