iOS實(shí)錄12:NSMutableArray使用中忽視的問題

[這是第12篇]

導(dǎo)語: NSMutableArray提供的API能解決絕大部分的需求吮龄,但是在實(shí)際iOS開發(fā)中汰蓉,在某些場(chǎng)景下泛源,需要考慮線程安全 或 弱對(duì)象引用 或 刪除元素這三個(gè)問題究珊。

一婴氮、線程安全的NSMutableArray####

NSMutableArray本身是線程不安全的只洒。簡(jiǎn)單來說许帐,線程安全就是多個(gè)線程訪問同一段代碼,程序不會(huì)異常毕谴、不Crash成畦。而編寫線程安全的代碼主要依靠線程同步距芬。

1、不使用atomic修飾屬性

原因有二循帐,如下:

1 ) atomic 的內(nèi)存管理語義是原子性的框仔,僅保證了屬性的setter和getter方法是原子性的,是線程安全的拄养,但是屬性的其他方法离斩,如數(shù)組添加/移除元素等并不是原子操作,所以不能保證屬性是線程安全的瘪匿。

2 ) atomic雖然保證了getter跛梗、setter方法線程安全,但是付出的代價(jià)很大棋弥,執(zhí)行效率要比nonatomic慢很多倍(有說法是慢10-20倍)核偿。

總之:使用nonatomic修飾NSMutableArray對(duì)象就可以了,而使用鎖顽染、dispatch_queue來保證NSMutableArray對(duì)象的線程安全漾岳。

2、打造線程安全的NSMutableArray

《Effective Objective-C 2.0..》書中第41條:多用派發(fā)隊(duì)列家乘,少用同步鎖中指出:使用“串行同步隊(duì)列”(serial synchronization queue)蝗羊,將讀取操作及寫入操作都安排在同一個(gè)隊(duì)列里,即可保證數(shù)據(jù)同步仁锯。而通過并發(fā)隊(duì)列耀找,結(jié)合GCD的柵欄塊(barrier)來不僅實(shí)現(xiàn)數(shù)據(jù)同步線程安全,還比串行同步隊(duì)列方式更高效业崖。

GCD的柵欄塊作用示意圖.png

說明:柵欄塊單獨(dú)執(zhí)行野芒,不能與其他塊并行。知道當(dāng)前所有并發(fā)塊都執(zhí)行完畢双炕,才會(huì)單獨(dú)執(zhí)行這個(gè)柵欄塊狞悲。線程安全的NSMutableArray實(shí)現(xiàn)如下:

//QSThreadSafeMutableArray.h
@interface QSThreadSafeMutableArray : NSMutableArray

@end

//QSThreadSafeMutableArray.m
#import "QSThreadSafeMutableArray.h"
@interface QSThreadSafeMutableArray()

@property (nonatomic, strong) dispatch_queue_t syncQueue;
@property (nonatomic, strong) NSMutableArray* array;

@end

@implementation QSThreadSafeMutableArray

#pragma mark - init 方法
- (instancetype)initCommon{

    self = [super init];
    if (self) {
        //%p 以16進(jìn)制的形式輸出內(nèi)存地址,附加前綴0x
        NSString* uuid = [NSString stringWithFormat:@"com.jzp.array_%p", self];
        //注意:_syncQueue是并行隊(duì)列
        _syncQueue = dispatch_queue_create([uuid UTF8String], DISPATCH_QUEUE_CONCURRENT);
    }
    return self;
}

- (instancetype)init{

    self = [self initCommon];
    if (self) {
        _array = [NSMutableArray array];
    }
    return self;
}

//其他init方法略

#pragma mark - 數(shù)據(jù)操作方法 (凡涉及更改數(shù)組中元素的操作妇斤,使用異步派發(fā)+柵欄塊摇锋;讀取數(shù)據(jù)使用 同步派發(fā)+并行隊(duì)列)
- (NSUInteger)count{

    __block NSUInteger count;
    dispatch_sync(_syncQueue, ^{
        count = _array.count;
    });
    return count;
}

- (id)objectAtIndex:(NSUInteger)index{

    __block id obj;
    dispatch_sync(_syncQueue, ^{
        if (index < [_array count]) {
            obj = _array[index];
        }
    });
    return obj;
}

- (NSEnumerator *)objectEnumerator{

    __block NSEnumerator *enu;
    dispatch_sync(_syncQueue, ^{
        enu = [_array objectEnumerator];
    });
    return enu;
}

- (void)insertObject:(id)anObject atIndex:(NSUInteger)index{

    dispatch_barrier_async(_syncQueue, ^{
        if (anObject && index < [_array count]) {
            [_array insertObject:anObject atIndex:index];
        }
    });
}

- (void)addObject:(id)anObject{

    dispatch_barrier_async(_syncQueue, ^{
        if(anObject){
           [_array addObject:anObject];
        }
    });
}

- (void)removeObjectAtIndex:(NSUInteger)index{

    dispatch_barrier_async(_syncQueue, ^{
    
        if (index < [_array count]) {
            [_array removeObjectAtIndex:index];
        }
    });
}

- (void)removeLastObject{

    dispatch_barrier_async(_syncQueue, ^{
        [_array removeLastObject];
    });
}

- (void)replaceObjectAtIndex:(NSUInteger)index withObject:(id)anObject{

    dispatch_barrier_async(_syncQueue, ^{
        if (anObject && index < [_array count]) {
            [_array replaceObjectAtIndex:index withObject:anObject];
        }
    });
}

- (NSUInteger)indexOfObject:(id)anObject{

    __block NSUInteger index = NSNotFound;
    dispatch_sync(_syncQueue, ^{
        for (int i = 0; i < [_array count]; i ++) {
            if ([_array objectAtIndex:i] == anObject) {
                index = i;
                break;
            }
        }
    });
    return index;
}

- (void)dealloc{

    if (_syncQueue) {
        _syncQueue = NULL;
    }
}

@end

說明1:使用dispatch queue來實(shí)現(xiàn)線程同步;將同步與異步派發(fā)結(jié)合起來站超,可以實(shí)現(xiàn)與普通加鎖機(jī)制一樣的同步行為荸恕,又不會(huì)阻塞執(zhí)行異步派發(fā)的線程;使用同步隊(duì)列及柵欄塊死相,可以令同步行為更加高效融求。

說明2:NSMutableDictionary本身也是線程不全的,實(shí)現(xiàn)線程安全的NSMutableDictionary原理同線程安全的NSMutableArray算撮。(代碼見
QSUseCollectionDemo)

2生宛、線程安全的NSMutableArray使用
//線程安全的NSMutableArray
QSThreadSafeMutableArray *safeArray = [[QSThreadSafeMutableArray alloc]init];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

for (NSInteger i = 0; i < 10; i++) {
    
    dispatch_async(queue, ^{
        NSString *str = [NSString stringWithFormat:@"數(shù)組%d",(int)i+1];
        [safeArray addObject:str];
    });
}

sleep(1);
NSLog(@"打印數(shù)組");
[safeArray enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
    
    NSLog(@"%@",obj);
}];

說明1:先要初始化QSThreadSafeMutableArray對(duì)象县昂,初始化工作不是線程安全的。

說明2:多個(gè)線程幾乎同時(shí)添加數(shù)據(jù)元素陷舅,使用QSThreadSafeMutableArray倒彰,沒有發(fā)生遺漏數(shù)據(jù),也沒有因?yàn)橘Y源競(jìng)爭(zhēng)導(dǎo)致的奔潰蔑赘。而NSMutableArray對(duì)象在同樣情況下會(huì)出問題(遺漏數(shù)據(jù) 或 crash)狸驳。

二、NSMutableArray弱引用對(duì)象####

在iOS中缩赛,容器類是強(qiáng)引用其存儲(chǔ)的元素的,將對(duì)象添加到容器時(shí)撰糠,該對(duì)象的引用計(jì)數(shù)+1酥馍,這很好保證了訪問容器類中元素時(shí),元素是始終存在容器類中阅酪。這種強(qiáng)引用同時(shí)也埋下了造成循環(huán)引用的可能旨袒。實(shí)現(xiàn)容器類中弱引用對(duì)象,是個(gè)考慮的問題术辐。容器類中僅以NSMutableArray為例砚尽,實(shí)現(xiàn)弱引用對(duì)象、

1辉词、NSMutableArray分類實(shí)現(xiàn)
//NSMutableArray+WeakReferences.h
@interface NSMutableArray (WeakReferences)

+ (id)mutableArrayUsingWeakReferences;

+ (id)mutableArrayUsingWeakReferencesWithCapacity:(NSUInteger)capacity;

@end


//NSMutableArray+WeakReferences.m
#import "NSMutableArray+WeakReferences.h"
@implementation NSMutableArray (WeakReferences)

+ (id)mutableArrayUsingWeakReferences {

    return [self mutableArrayUsingWeakReferencesWithCapacity:0];
}

+ (id)mutableArrayUsingWeakReferencesWithCapacity:(NSUInteger)capacity {

    CFArrayCallBacks callbacks = {0, NULL, NULL, CFCopyDescription, CFEqual};
    // Cast of C pointer type 'CFMutableArrayRef' (aka 'struct __CFArray *') to Objective-C pointer type 'id' requires a bridged cast
    return (id)CFBridgingRelease(CFArrayCreateMutable(0, capacity, &callbacks));
    // return (id)(CFArrayCreateMutable(0, capacity, &callbacks));
}

@end

** 說明1 **: 參考自Non-retaining array for delegates

說明2:在NSDictionary/NSMutableDictionary中必孤,也是強(qiáng)引用values。想弱引用values瑞躺,只需要使用NSMapTable(iOS 6推出)敷搪,它不僅和字典有相似的數(shù)據(jù)結(jié)構(gòu),還可以指定key是強(qiáng)引用幢哨,value是弱引用赡勘。

2、其他實(shí)現(xiàn)

思路:將需要添加到容器中的對(duì)象捞镰,包裝在另一個(gè)存儲(chǔ)對(duì)它的弱引用的對(duì)象中闸与。

//QSWeakObjectWrapper.h
@interface QSWeakObjectWrapper : NSObject

@property (nonatomic, weak, readonly) id weakObject;

- (id)initWithWeakObject:(id)weakObject;

@end

//QSWeakObjectWrapper.m
#import "QSWeakObjectWrapper.h"
@implementation QSWeakObjectWrapper

- (id)initWithWeakObject:(id)weakObject{

    if (self = [super init]) {
        _weakObject = weakObject;
    }

    return self;
}

@end

** 說明 **: 我們實(shí)現(xiàn)了弱引用元素,即不希望數(shù)組保留對(duì)象岸售,這是為了解決數(shù)組中循環(huán)引用的問題践樱;但平時(shí)還是默認(rèn)使用強(qiáng)引用數(shù)組元素,因?yàn)槿跻脭?shù)組元素冰评,數(shù)組中元素在釋放映胁,數(shù)組會(huì)出問題。

三甲雅、刪除NSMutableArray中的元素####

1解孙、removeObjectAtIndex VS removeObject
  • removeObjectAtIndex:刪除指定NSMutableArray中指定index的對(duì)象( index不能越界)坑填。

  • removeObject:刪除NSMutableArray中所有isEqual:待刪對(duì)象的對(duì)象

說明1:removeObjectAtIndex:最多只能刪除一個(gè)對(duì)象,而removeObject:可以刪除多個(gè)對(duì)象(只要符合isEqual:的都刪除掉)弛姜。

說明2:在NSMutableArray遍歷中使用removeObject:刪除該NSMutableArray內(nèi)部對(duì)象脐瑰,此舉可能引發(fā)誤刪

2、刪除元素錯(cuò)誤做法

下面羅列幾種比較常見錯(cuò)誤的做法

1)for in 循環(huán)中刪除數(shù)組內(nèi)部對(duì)象廷臼。

NSMutableArray *arr = [[NSMutableArray alloc]initWithObjects:@"1",@"2",@"3",@"3",@"4",@"3",@"5",@"3",@"6",nil];    
for (NSString *str in arr) {
    if ([str isEqualToString:@"3"]) {
        
        NSInteger index = [arr indexOfObject:@"3"];
        [arr removeObjectAtIndex:index];
    }
}

說明:在for in 循環(huán)中刪除數(shù)組內(nèi)部對(duì)象可能會(huì)引起崩潰苍在。只有一種情況例外,在for in 循環(huán)中荠商,如果刪除的是數(shù)組中最后一個(gè)元素的話寂恬,程序就不會(huì)崩潰,這是因?yàn)楫?dāng)for in 循環(huán)遍歷到最后一個(gè)元素時(shí)莱没,已經(jīng)遍歷結(jié)束了初肉。奔潰時(shí)候報(bào)錯(cuò)如下:

  *** Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <__NSArrayM: 0x610000045040>
   was mutated while being enumerated.'

2)for循環(huán)遍歷中從前往后刪除

NSMutableArray *arr = [[NSMutableArray alloc]initWithObjects:@"1",@"2",@"3",@"3",@"4",@"3",@"5",@"3",@"6",nil];    
for (NSInteger i = 0; i < [arr count]; i++) {

    NSString *str = [arr objectAtIndex:i];
    if ([str isEqualToString:@"3"]) {
        [arr removeObjectAtIndex:i];
    }
}

說明:如果是刪除相同元素,相同元素相鄰饰躲,會(huì)被漏刪牙咏。有些童鞋在for循環(huán)遍歷使用removeObject,可以做到不漏刪嘹裂,這是因?yàn)閞emoveObject本身特點(diǎn)就是刪除數(shù)組中所有isEqual:待刪對(duì)象的對(duì)象妄壶。因之,掩蓋了問題寄狼,那么做也是不對(duì)丁寄。

3、刪除元素正確做法

1)直接使用removeObject(如果是刪除相同元素)

因?yàn)槠浔旧硖攸c(diǎn)就是刪除數(shù)組中所有isEqual:待刪對(duì)象的對(duì)象例嘱,解決刪除相同元素這種問題很適合狡逢,不需要在遍歷時(shí)候使用。

2)在for循環(huán)遍歷從后往前刪除

NSMutableArray *arr = [[NSMutableArray alloc]initWithObjects:@"1",@"2",@"3",@"3",@"4",@"3",@"5",@"3",@"6",nil];
for (NSInteger i = [arr count] - 1; i >= 0; i--) {

    NSString *str = [arr objectAtIndex:i];
    if ([str isEqualToString:@"3"]) {
        [arr removeObjectAtIndex:i];
    }
}

四拼卵、其他

  • Class Clusters(類簇)是抽象工廠模式在iOS下的一種實(shí)現(xiàn)奢浑,Class Clusters僅對(duì)外暴露出簡(jiǎn)單的接口,而隱藏了內(nèi)部多個(gè)私有的類和方法的實(shí)現(xiàn)腋腮。NSMutableArray雀彼、NSMutableDictionary就是Class Clusters(類簇)中代表。

  • NSMutableArray/NSArray中存儲(chǔ)的元素是允許重復(fù)的即寡,其提供的常用接口的性能有差異徊哑。 indexOfObject: 、containsObject:聪富、removeObject: 都會(huì)遍歷數(shù)組中的元素莺丑,這意味著著調(diào)用這些接口,時(shí)間復(fù)雜度至少是O(n); 而objectAtIndex:removeLastObject梢莽、firstObject萧豆、lastObjectaddObject:這些接口的時(shí)間復(fù)雜度是O(1)昏名。

  • NSArray提供的indexOfObject:inSortedRange:options:usingComparator: 使用的是二分查找涮雷,時(shí)間復(fù)雜度是 O(log n)。

     //比indexOfObject:的效率高
     NSArray *arr = [NSArray arrayWithObjects:@"5",@"1",@"2",@"4",@"3",nil];
     NSLog(@"2 index = %ld",[arr indexOfObject:@"2" inSortedRange:NSMakeRange(0, [arr count]) options:NSBinarySearchingFirstEqual   usingComparator:^NSComparisonResult(id  _Nonnull obj1, id  _Nonnull obj2) {
          if ([obj1 integerValue] > [obj2 integerValue]) {
              return NSOrderedDescending; //
          }else if ([obj1 integerValue] < [obj2 integerValue]) {
              return NSOrderedAscending;
          }
          return NSOrderedSame;
      }]);
    
  • NSMutableArray/NSArray的排序轻局。

     NSArray *arr = [NSArray arrayWithObjects:@"5",@"1",@"2",@"4",@"3",nil];
      //遞減排序
     NSArray *sortArray = [arr sortedArrayUsingComparator:^NSComparisonResult(id  _Nonnull obj1, id  _Nonnull obj2) {
      if ([obj1 integerValue] > [obj2 integerValue]) {
          return NSOrderedAscending; //
      }else if ([obj1 integerValue] < [obj2 integerValue]) {
          return NSOrderedDescending;
      }
      return NSOrderedSame;
    }];
    
    NSLog(@"sortArray = %@",sortArray); //{"5","4","3","2","1"}
    

End

  • 源碼參考QSUseCollectionDemo

  • 我是南華coder洪鸭,一名北漂的初級(jí)iOS程序猿。iOS實(shí)(踐)錄系列是我的一點(diǎn)開發(fā)心得仑扑,希望能夠拋磚引玉览爵。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市夫壁,隨后出現(xiàn)的幾起案子拾枣,更是在濱河造成了極大的恐慌,老刑警劉巖盒让,帶你破解...
    沈念sama閱讀 221,820評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異司蔬,居然都是意外死亡邑茄,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門俊啼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來肺缕,“玉大人,你說我怎么就攤上這事授帕⊥荆” “怎么了?”我有些...
    開封第一講書人閱讀 168,324評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵跛十,是天一觀的道長(zhǎng)彤路。 經(jīng)常有香客問我,道長(zhǎng)芥映,這世上最難降的妖魔是什么洲尊? 我笑而不...
    開封第一講書人閱讀 59,714評(píng)論 1 297
  • 正文 為了忘掉前任,我火速辦了婚禮奈偏,結(jié)果婚禮上坞嘀,老公的妹妹穿的比我還像新娘。我一直安慰自己惊来,他們只是感情好丽涩,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,724評(píng)論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著裁蚁,像睡著了一般矢渊。 火紅的嫁衣襯著肌膚如雪继准。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,328評(píng)論 1 310
  • 那天昆淡,我揣著相機(jī)與錄音锰瘸,去河邊找鬼。 笑死昂灵,一個(gè)胖子當(dāng)著我的面吹牛避凝,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播眨补,決...
    沈念sama閱讀 40,897評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼管削,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了撑螺?” 一聲冷哼從身側(cè)響起含思,我...
    開封第一講書人閱讀 39,804評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎甘晤,沒想到半個(gè)月后含潘,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,345評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡线婚,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,431評(píng)論 3 340
  • 正文 我和宋清朗相戀三年遏弱,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片塞弊。...
    茶點(diǎn)故事閱讀 40,561評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡漱逸,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出游沿,到底是詐尸還是另有隱情饰抒,我是刑警寧澤,帶...
    沈念sama閱讀 36,238評(píng)論 5 350
  • 正文 年R本政府宣布诀黍,位于F島的核電站袋坑,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏蔗草。R本人自食惡果不足惜咒彤,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,928評(píng)論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望咒精。 院中可真熱鬧镶柱,春花似錦、人聲如沸模叙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,417評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至故觅,卻和暖如春厂庇,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背输吏。 一陣腳步聲響...
    開封第一講書人閱讀 33,528評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工权旷, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人贯溅。 一個(gè)月前我還...
    沈念sama閱讀 48,983評(píng)論 3 376
  • 正文 我出身青樓拄氯,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親它浅。 傳聞我的和親對(duì)象是個(gè)殘疾皇子译柏,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,573評(píng)論 2 359

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