[這是第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ù)同步線程安全,還比串行同步隊列方式更高效。
說明:柵欄塊單獨執(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
我是南華coder,一名北漂的初級iOS程序猿。iOS實(踐)錄系列是我的一點開發(fā)心得,希望能夠拋磚引玉。