美文网首页DevSupporttomiOS 基础
iOS实录12:NSMutableArray使用中忽视的问题

iOS实录12:NSMutableArray使用中忽视的问题

作者: 南华coder | 来源:发表于2017-06-27 17:58 被阅读3243次

    [这是第12篇]

    导语: NSMutableArray提供的API能解决绝大部分的需求,但是在实际iOS开发中,在某些场景下,需要考虑线程安全 或 弱对象引用 或 删除元素这三个问题。

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

    NSMutableArray本身是线程不安全的。简单来说,线程安全就是多个线程访问同一段代码,程序不会异常、不Crash。而编写线程安全的代码主要依靠线程同步。

    1、不使用atomic修饰属性

    原因有二,如下:

    1 ) atomic 的内存管理语义是原子性的,仅保证了属性的setter和getter方法是原子性的,是线程安全的,但是属性的其他方法,如数组添加/移除元素等并不是原子操作,所以不能保证属性是线程安全的。

    2 ) atomic虽然保证了getter、setter方法线程安全,但是付出的代价很大,执行效率要比nonatomic慢很多倍(有说法是慢10-20倍)。

    总之:使用nonatomic修饰NSMutableArray对象就可以了,而使用锁、dispatch_queue来保证NSMutableArray对象的线程安全。

    2、打造线程安全的NSMutableArray

    《Effective Objective-C 2.0..》书中第41条:多用派发队列,少用同步锁中指出:使用“串行同步队列”(serial synchronization queue),将读取操作及写入操作都安排在同一个队列里,即可保证数据同步。而通过并发队列,结合GCD的栅栏块(barrier)来不仅实现数据同步线程安全,还比串行同步队列方式更高效。

    GCD的栅栏块作用示意图.png

    说明:栅栏块单独执行,不能与其他块并行。知道当前所有并发块都执行完毕,才会单独执行这个栅栏块。线程安全的NSMutableArray实现如下:

    //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进制的形式输出内存地址,附加前缀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 - 数据操作方法 (凡涉及更改数组中元素的操作,使用异步派发+栅栏块;读取数据使用 同步派发+并行队列)
    - (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来实现线程同步;将同步与异步派发结合起来,可以实现与普通加锁机制一样的同步行为,又不会阻塞执行异步派发的线程;使用同步队列及栅栏块,可以令同步行为更加高效。

    说明2:NSMutableDictionary本身也是线程不全的,实现线程安全的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:@"数组%d",(int)i+1];
            [safeArray addObject:str];
        });
    }
    
    sleep(1);
    NSLog(@"打印数组");
    [safeArray enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        
        NSLog(@"%@",obj);
    }];
    

    说明1:先要初始化QSThreadSafeMutableArray对象,初始化工作不是线程安全的。

    说明2:多个线程几乎同时添加数据元素,使用QSThreadSafeMutableArray,没有发生遗漏数据,也没有因为资源竞争导致的奔溃。而NSMutableArray对象在同样情况下会出问题(遗漏数据 或 crash)。

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

    在iOS中,容器类是强引用其存储的元素的,将对象添加到容器时,该对象的引用计数+1,这很好保证了访问容器类中元素时,元素是始终存在容器类中。这种强引用同时也埋下了造成循环引用的可能。实现容器类中弱引用对象,是个考虑的问题。容器类中仅以NSMutableArray为例,实现弱引用对象、

    1、NSMutableArray分类实现
    //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推出),它不仅和字典有相似的数据结构,还可以指定key是强引用,value是弱引用。

    2、其他实现

    思路:将需要添加到容器中的对象,包装在另一个存储对它的弱引用的对象中。

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

    ** 说明 **: 我们实现了弱引用元素,即不希望数组保留对象,这是为了解决数组中循环引用的问题;但平时还是默认使用强引用数组元素,因为弱引用数组元素,数组中元素在释放,数组会出问题。

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

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

    • removeObject:删除NSMutableArray中所有isEqual:待删对象的对象

    说明1:removeObjectAtIndex:最多只能删除一个对象,而removeObject:可以删除多个对象(只要符合isEqual:的都删除掉)。

    说明2:在NSMutableArray遍历中使用removeObject:删除该NSMutableArray内部对象,此举可能引发误删

    2、删除元素错误做法

    下面罗列几种比较常见错误的做法

    1)for in 循环中删除数组内部对象。

    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 循环中删除数组内部对象可能会引起崩溃。只有一种情况例外,在for in 循环中,如果删除的是数组中最后一个元素的话,程序就不会崩溃,这是因为当for in 循环遍历到最后一个元素时,已经遍历结束了。奔溃时候报错如下:

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

    2)for循环遍历中从前往后删除

    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循环遍历使用removeObject,可以做到不漏删,这是因为removeObject本身特点就是删除数组中所有isEqual:待删对象的对象。因之,掩盖了问题,那么做也是不对。

    3、删除元素正确做法

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

    因为其本身特点就是删除数组中所有isEqual:待删对象的对象,解决删除相同元素这种问题很适合,不需要在遍历时候使用。

    2)在for循环遍历从后往前删除

    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下的一种实现,Class Clusters仅对外暴露出简单的接口,而隐藏了内部多个私有的类和方法的实现。NSMutableArray、NSMutableDictionary就是Class Clusters(类簇)中代表。

    • NSMutableArray/NSArray中存储的元素是允许重复的,其提供的常用接口的性能有差异。 indexOfObject: 、containsObject:、removeObject: 都会遍历数组中的元素,这意味着着调用这些接口,时间复杂度至少是O(n); 而objectAtIndex:removeLastObjectfirstObjectlastObjectaddObject:这些接口的时间复杂度是O(1)

    • NSArray提供的indexOfObject:inSortedRange:options:usingComparator: 使用的是二分查找,时间复杂度是 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实(践)录系列是我的一点开发心得,希望能够抛砖引玉。

    相关文章

      网友评论

      • 我是韩叫兽:准备在项目中直接用了,感谢
      • 8fe8946fa366:有一个疑问?为什么读操作要用同步函数,那读操作不就是串行执行的吗,为什么读操作不可以并发执行。还有就是写操作是并发执行的话,不还是不安全的吗?
      • Obtenir:如果在barrier中同时执行add和remove,不是还有多线程问题吗?
      • 5ffe309d6c06:up主,为什么读操作不能使用异步派发呢,反正写操作也是需要等待队列中所有的读操作完成才会进行写入的啊,这样效率还会高点啊
      • iPhone贴膜小哥:很棒 用barrier有效的解决了线程安全问题
        woshishui1243:@南华coder 既然是异步派发,应该是使用异步调用dispatch_async吧,如果还用dispatch_sync达不到目的吧
        南华coder:@iPhone贴膜小哥 :smile:
      • 464f83d646fa:写的很是完善
        南华coder:@小脸书 谢谢
      • 花样棉花:真棒👍
        花样棉花: @南华coder 不客气!加油↖(^ω^)↗!你很棒,撸起袖子加油干!随时欢迎你来给我文章指出问题,共同进步!我需要你的帮助和支持!
        南华coder:@孤独总是那么可怕 谢赞

      本文标题:iOS实录12:NSMutableArray使用中忽视的问题

      本文链接:https://www.haomeiwen.com/subject/mxbfcxtx.html