iOS實錄12:NSMutableArray使用中忽視的問題

[這是第12篇]

導(dǎo)語: NSMutableArray提供的API能解決絕大部分的需求,但是在實際iOS開發(fā)中,在某些場景下,需要考慮線程安全 或 弱對象引用 或 刪除元素這三個問題。

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

NSMutableArray本身是線程不安全的。簡單來說,線程安全就是多個線程訪問同一段代碼,程序不會異常、不Crash。而編寫線程安全的代碼主要依靠線程同步。

1、不使用atomic修飾屬性

原因有二,如下:

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

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

總之:使用nonatomic修飾NSMutableArray對象就可以了,而使用鎖、dispatch_queue來保證NSMutableArray對象的線程安全。

2、打造線程安全的NSMutableArray

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

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

說明:柵欄塊單獨執(zhí)行,不能與其他塊并行。知道當(dāng)前所有并發(fā)塊都執(zhí)行完畢,才會單獨執(zhí)行這個柵欄塊。線程安全的NSMutableArray實現(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進制的形式輸出內(nèi)存地址,附加前綴0x
        NSString* uuid = [NSString stringWithFormat:@"com.jzp.array_%p", self];
        //注意:_syncQueue是并行隊列
        _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ā)+并行隊列)
- (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來實現(xiàn)線程同步;將同步與異步派發(fā)結(jié)合起來,可以實現(xiàn)與普通加鎖機制一樣的同步行為,又不會阻塞執(zhí)行異步派發(fā)的線程;使用同步隊列及柵欄塊,可以令同步行為更加高效。

說明2:NSMutableDictionary本身也是線程不全的,實現(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對象,初始化工作不是線程安全的。

說明2:多個線程幾乎同時添加數(shù)據(jù)元素,使用QSThreadSafeMutableArray,沒有發(fā)生遺漏數(shù)據(jù),也沒有因為資源競爭導(dǎo)致的奔潰。而NSMutableArray對象在同樣情況下會出問題(遺漏數(shù)據(jù) 或 crash)。

二、NSMutableArray弱引用對象####

在iOS中,容器類是強引用其存儲的元素的,將對象添加到容器時,該對象的引用計數(shù)+1,這很好保證了訪問容器類中元素時,元素是始終存在容器類中。這種強引用同時也埋下了造成循環(huán)引用的可能。實現(xiàn)容器類中弱引用對象,是個考慮的問題。容器類中僅以NSMutableArray為例,實現(xiàn)弱引用對象、

1、NSMutableArray分類實現(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中,也是強引用values。想弱引用values,只需要使用NSMapTable(iOS 6推出),它不僅和字典有相似的數(shù)據(jù)結(jié)構(gòu),還可以指定key是強引用,value是弱引用。

2、其他實現(xiàn)

思路:將需要添加到容器中的對象,包裝在另一個存儲對它的弱引用的對象中。

//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

** 說明 **: 我們實現(xiàn)了弱引用元素,即不希望數(shù)組保留對象,這是為了解決數(shù)組中循環(huán)引用的問題;但平時還是默認使用強引用數(shù)組元素,因為弱引用數(shù)組元素,數(shù)組中元素在釋放,數(shù)組會出問題。

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

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

  • removeObject:刪除NSMutableArray中所有isEqual:待刪對象的對象

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

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

2、刪除元素錯誤做法

下面羅列幾種比較常見錯誤的做法

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

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)部對象可能會引起崩潰。只有一種情況例外,在for in 循環(huán)中,如果刪除的是數(shù)組中最后一個元素的話,程序就不會崩潰,這是因為當(dāng)for in 循環(huán)遍歷到最后一個元素時,已經(jīng)遍歷結(jié)束了。奔潰時候報錯如下:

  *** 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];
    }
}

說明:如果是刪除相同元素,相同元素相鄰,會被漏刪。有些童鞋在for循環(huán)遍歷使用removeObject,可以做到不漏刪,這是因為removeObject本身特點就是刪除數(shù)組中所有isEqual:待刪對象的對象。因之,掩蓋了問題,那么做也是不對。

3、刪除元素正確做法

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

因為其本身特點就是刪除數(shù)組中所有isEqual:待刪對象的對象,解決刪除相同元素這種問題很適合,不需要在遍歷時候使用。

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下的一種實現(xiàn),Class Clusters僅對外暴露出簡單的接口,而隱藏了內(nèi)部多個私有的類和方法的實現(xiàn)。NSMutableArray、NSMutableDictionary就是Class Clusters(類簇)中代表。

  • NSMutableArray/NSArray中存儲的元素是允許重復(fù)的,其提供的常用接口的性能有差異。 indexOfObject: 、containsObject:、removeObject: 都會遍歷數(shù)組中的元素,這意味著著調(diào)用這些接口,時間復(fù)雜度至少是O(n); 而objectAtIndex:removeLastObject、firstObject、lastObject、addObject:這些接口的時間復(fù)雜度是O(1)

  • NSArray提供的indexOfObject:inSortedRange:options:usingComparator: 使用的是二分查找,時間復(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,一名北漂的初級iOS程序猿。iOS實(踐)錄系列是我的一點開發(fā)心得,希望能夠拋磚引玉。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,606評論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,582評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,540評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,028評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,801評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,223評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,294評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,442評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,976評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,800評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,996評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,543評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,233評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,926評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,702評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,991評論 2 374

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