tips: 9.25更新了一下實(shí)現(xiàn)方式, 讓實(shí)現(xiàn)更簡(jiǎn)潔, 更科學(xué)
背景
NSDictionary和NSArray在日常開發(fā)中的重要性和頻率無需多言, 但是有時(shí)候在構(gòu)造的時(shí)候會(huì)要對(duì)nil做一系列的判斷, 最近作者在整理這類因?yàn)閚il而導(dǎo)致crash的問題時(shí), 不辭辛苦地給每個(gè)潛在為空的點(diǎn)都加上了判斷.
思考
為什么我們不在初始化的時(shí)候fix掉這個(gè)問題呢? 所以這次我打算在構(gòu)造的時(shí)候, 過濾掉nil. (關(guān)于這個(gè)做法是好是壞, 在最后有一些討論)
目標(biāo)
- 一般我初始化NSDictionary會(huì)用兩種方式
a.literal syntax即NSDictionary *dic = @{key1:value1, key2:value2}; 這是最常用的
b.NSDictionary *dic = [NSDictionary dictionaryWithObjectsAndKeys:value1, key1, value2, key2, nil]; 這個(gè)也相對(duì)常見 - 初始化NSArray一般也對(duì)應(yīng)是兩種, 不過后一種不會(huì)出現(xiàn)crash, 所以不羅列了:
c.liter syntax即NSArray *array = @[value1, value2, value3];
以上各初始化方法中, 如果:
a.中任意key或者value為nil, 會(huì)導(dǎo)致crash
b. key為空, 造成key value 不匹配, 會(huì)crash
c. 任意value為nil, 會(huì)crash
方法
1.為了解決這個(gè)問題, 首先想到的是runtime的class_replaceMethod以及method_exchangeImplementations, 可能是理解還不夠透徹, 嘗試了好幾回都沒有達(dá)到預(yù)期, 所以就改換策略了
2.再然后考慮過繼承, 但是很快被否決, 不能為了這么個(gè)問題, 就大張旗鼓地把全部NSDictionary改為XXDictionary, 得不償失, 純粹坑爹.
3.最后想到的是category, 仔細(xì)想想, 的確可以滿足我的需求, 可以重寫掉一些構(gòu)造方法
過程
-
首先要找到重寫哪個(gè)方法, dictionaryWithObjectsAndKeys:好說, 很直白 , 但是literal sytax要重寫哪個(gè)呢? 很簡(jiǎn)單, 故意把nil傳進(jìn)去, 然后開啟異常斷點(diǎn), 啟動(dòng)之后就會(huì)斷住了.如圖:
找到重寫方法
所以我們需要在category里面重寫
+(instancetype)dictionaryWithObjects:(const id _Nonnull __unsafe_unretained *)objects forKeys:(const id<NSCopying> _Nonnull __unsafe_unretained *)keys count:(NSUInteger)cnt
代碼實(shí)現(xiàn)如下:
+(instancetype)dictionaryWithObjects:(const id _Nonnull __unsafe_unretained *)objects forKeys:(const id<NSCopying> _Nonnull __unsafe_unretained *)keys count:(NSUInteger)cnt
{
NSUInteger keyCnt = 0, valueCnt = 0;
__unsafe_unretained id *objPtr = objects; // 指向objects初始位置
__unsafe_unretained id *keyPtr = keys; // 指向keys初始位置
// 遍歷找到為key nil的位置
for ( ; keyCnt < cnt; keyCnt ++, objPtr++) {
if (*objPtr == 0)
{
break;
}
}
// 遍歷找到為key nil的位置
for ( ; valueCnt < cnt; valueCnt ++, keyPtr++) {
if (*keyPtr == 0) // 遍歷找到為key nil的位置
{
break;
}
}
// 找到最小值
NSUInteger minCnt = MIN(keyCnt, valueCnt);
// 構(gòu)造合適的key,object array
NSArray *vs = [NSArray arrayWithObjects:objects count:minCnt];
NSArray *ks = [NSArray arrayWithObjects:keys count:minCnt];
// 用下面的方法構(gòu)造
return [self dictionaryWithObjects:vs forKeys:ks];
}
2.dictionaryWithObjectsAndKeys:
這里主要對(duì)keys和objects的數(shù)量進(jìn)行對(duì)比, 通過對(duì)va_list操作分離出object和key
代碼實(shí)現(xiàn):
+(instancetype)dictionaryWithObjectsAndKeys:(id)firstObject, ...
{
NSMutableArray *objects = [[NSMutableArray alloc] init];
NSMutableArray *keys = [[NSMutableArray alloc] init];
id eachObject;
va_list argumentList;
if (firstObject)
{
[objects addObject: firstObject];
va_start(argumentList, firstObject);
NSUInteger index = 1;
while ((eachObject = va_arg(argumentList, id)))
{
(index++ & 0x01) ? [keys addObject: eachObject] : [objects addObject: eachObject];
}
va_end(argumentList);
}
if (objects.count == keys.count)
{
// 直接寫空 跳到最后返回
}
else
{
(objects.count < keys.count)?[keys removeLastObject]:[objects removeLastObject];
}
return [self dictionaryWithObjects:objects forKeys:keys];
}
- NSArray的整個(gè)過程與Dictionary相差不多, 代碼上更簡(jiǎn)單一些:
找到需要改寫的方法為:
+(instancetype)arrayWithObjects:(const id _Nonnull __unsafe_unretained *)objects count:(NSUInteger)cnt
代碼實(shí)現(xiàn):
+(instancetype)arrayWithObjects:(const id _Nonnull __unsafe_unretained *)objects count:(NSUInteger)cnt
NSUInteger count = 0;
__unsafe_unretained id *objPtr = objects;
NSMutableArray *ma = [NSMutableArray new];
for ( ; count < cnt; count ++, objPtr++) {
if (*objPtr == 0)
{
break;
}
[ma addObject:*tmp];
}
return [[NSArray alloc] initWithArray:ma];
}
4.編譯警告
在__unsafe_unretained id *objPtr = objects;3處會(huì)有一個(gè)警告, 暫時(shí)沒有發(fā)現(xiàn)影響
結(jié)果
如下圖所示, 構(gòu)造NSDictionary無論是key 還是object有nil都不會(huì)crash, 當(dāng)然會(huì)丟失掉一個(gè)key或者object, 用literal syntax構(gòu)造array也會(huì)主動(dòng)丟棄掉nil
聲明一下, 這些代碼只作了初步的測(cè)試, 在我的測(cè)試代碼里面是可以跑通的, 同事因?yàn)槿痔鎿Q, 會(huì)有很多系統(tǒng)構(gòu)造也調(diào)用新的代碼, app沒有出現(xiàn)問題, 從一定程度上來說, 替換是成功的.
討論
crash的確是讓大家十分痛恨的一件事情, 無論是開發(fā)者還是用戶, 但是, 如果沒有直接crash, 可能會(huì)使得一些bug非常難以發(fā)現(xiàn), app也會(huì)有奇奇怪怪的表現(xiàn), 所以, 具體是想讓app更健壯 還是讓bug出現(xiàn)的更明顯, 這是一個(gè)問題.
一個(gè)小技巧:
在用NSMutableDictionary的時(shí)候, 會(huì)塞入一些鍵值對(duì), 當(dāng)value為nil時(shí)候, 用setObject:forKey:會(huì)crash, 但是用setValue:forKey:不會(huì), 但是會(huì)移除掉這個(gè)key(如有). 值得注意的是, key為nil的情況, 兩個(gè)方法都會(huì)crash.
更新
今天受http://www.cocoachina.com/ios/20150925/13459.html的啟發(fā), 在入?yún)⑸线M(jìn)行了一些修改, 可以把代碼變得更加簡(jiǎn)單, 通知可以直接nil, 而不會(huì)截?cái)嗪竺娴膮?shù), 通知也推薦看一下他用runtime的的實(shí)現(xiàn), 把一些訪問也做了保護(hù);
NSDictionary:
+(instancetype)dictionaryWithObjects:(const id[])objects forKeys:(const id[])keys count:(NSUInteger)cnt
{
NSMutableArray *validKeys = [NSMutableArray new];
NSMutableArray *validObjs = [NSMutableArray new];
for (NSUInteger i = 0; i < cnt; i ++) {
if (objects[i] && keys[i])
{
[validKeys addObject:keys[i]];
[validObjs addObject:objects[i]];
}
}
return [self dictionaryWithObjects:validObjs forKeys:validKeys];
}
NSArray:
+(instancetype)arrayWithObjects:(const id [])objects count:(NSUInteger)cnt
{
NSMutableArray *ma = [NSMutableArray new];
for (NSUInteger i = 0; i < cnt; i ++) {
if (objects[i])
{
[ma addObject:objects[i]];
}
}
return [[NSArray alloc] initWithArray:ma];
}