iOS Recording 12: Issues Neglected in NSMutableArray Use

Keywords: iOS

[This is Article 12]

Introduction: The API provided by NSMutableArray addresses most of the requirements, but in actual iOS development, threading security or weak object reference or deletion of elements are three issues to consider in some scenarios.

I. Thread-safe NSMutableArray

NSMutableArray itself is thread insecure.Simply put, thread security means that multiple threads access the same piece of code without exception or Crash.Writing thread-safe code relies primarily on thread synchronization.

1. Do not use atomic modifiers

There are two reasons for this:

1) The memory management semantics of atomic are atomic, only the setter and getter methods of attributes are atomic and thread-safe, but other methods of attributes, such as adding/removing elements from arrays, are not atomic and therefore cannot be guaranteed to be thread-safe.

2) Atomic guarantees thread security for getter and setter methods, but it costs a lot and executes many times slower than nonatomic (it is said to be 10-20 times slower).

In summary: use nonatomic to modify the NSMutableArray object, and use lock, dispatch_queue to keep the NSMutableArray object thread safe.

2. Create thread-safe NSMutableArray

In Effective Objective-C 2.0., Article 41: Using more dispatch queues than synchronization locks indicates that using serial synchronization queue, data synchronization can be guaranteed by placing both read and write operations in the same queue.Through concurrent queues, combined with the barrier of GCD, not only can the data synchronization thread be secured, but also more efficient than serial synchronization queues.


Graph of GCD's fence block action. png

Description: A fence block is executed independently and cannot be parallel to other blocks.This fence block will only be executed individually until all current concurrent blocks have been executed.Thread-safe NSMutableArray implements the following:

//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 method
- (instancetype)initCommon{

    self = [super init];
    if (self) {
        //%p outputs the memory address in hexadecimal form with the prefix 0x
        NSString* uuid = [NSString stringWithFormat:@"com.taobao.weex.array_%p", self];
        //Note: _syncQueue is a parallel queue
        _syncQueue = dispatch_queue_create([uuid UTF8String], DISPATCH_QUEUE_CONCURRENT);
    }
    return self;
}

- (instancetype)init{

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

//Other init methods omitted

#pragma mark - Data manipulation method (asynchronous dispatch + fence block for operations involving changing elements in an array; synchronous dispatch + parallel queue for reading data)
- (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

Description 1: Use dispatch queue to synchronize threads; combine synchronization with asynchronous dispatch to achieve the same synchronous behavior as regular locking mechanisms without blocking threads that perform asynchronous dispatch; use synchronous queues and fence blocks to make synchronous behavior more efficient.

Note 2: NSMutableDictionary itself is thread-incomplete. NSMutableDictionary, which implements thread security, works in the same way as NSMutableArray, which is thread-safe.(See code)
QSUseCollectionDemo)

2. Thread-safe NSMutableArray use
//Thread-safe 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:@"array%d",(int)i+1];
        [safeArray addObject:str];
    });
}

sleep(1);
NSLog(@"Print Array");
[safeArray enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {

    NSLog(@"%@",obj);
}];

Description 1: The QSThreadSafeMutableArray object must be initialized first. Initialization is not thread safe.

Description 2: Multiple threads add data elements almost at the same time. Using QSThreadSafeMutableArray, no data misses occur, and no runs due to resource competition occur.NSMutableArray objects, on the other hand, have problems (missing data or crash) in the same situation.

2. NSMutableArray Weak Reference Objects

In iOS, a container class is strongly referencing the elements it stores. When an object is added to a container, the reference count for that object is + 1, which ensures that elements always exist in the container class when accessing elements in the container class.This strong reference also buries the possibility of creating circular references.Implementing weak reference objects in container classes is a consideration.Only NSMutableArray is used as an example in the container class to implement weak reference objects,

1. Implementation of NSMutableArray Classification
//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

Description 1: Reference from Non-retaining array for delegates

Note 2: In NSDictionary/NSMutableDictionary, values are also strongly referenced.To weakly refer to values, you only need to use NSMapTable (introduced by iOS 6), which not only has a similar data structure to a dictionary, but also specifies that key is a strong reference and value is a weak reference.

2. Other implementations

Idea: An object that needs to be added to a container is wrapped in another object that stores a weak reference to it.

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

Description: We have implemented a weak reference element, that is, we do not want the array to retain objects, which is to solve the problem of circular references in arrays; however, we still default to using a strong reference array element because the elements in the array are freed and the array will have problems.

3. Delete elements from NSMutableArray

1,removeObjectAtIndex VS removeObject
  • removeObjectAtIndex: Deletes the object specified index in the specified NSMutableArray (index cannot be out of bounds).

  • removeObject: Delete all isEqual s in NSMutableArray: Objects to be deleted

Description 1: removeObjectAtIndex: Only one object can be deleted at most, while removeObject: can delete multiple objects (as long as those conforming to isEqual: are deleted).

Note 2: Use removeObject in NSMutableArray traversal: Delete the NSMutableArray internal object, which may cause a mistake

2. Error in deleting elements

Here are a few of the more common errors

1) Delete the internal objects of the array in the for-in loop.

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

Description: Deleting objects inside an array in a for-in loop may cause a crash.There is only one exception. In a for-in loop, if the last element in the array is deleted, the program will not crash because when the for-in loop traverses to the last element, the traversal is complete.The following errors were reported during the rush:

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

2) Delete after going in for loop traversal

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

Description: If the same element is deleted, it will be omitted if the same element is adjacent.Some children's shoes use removeObject to iterate through the for loop, which does not miss deletion because removeObject itself is characterized by deleting all isEqual objects in the array: objects to be deleted.Because of this, the problem is covered up, so it is also wrong to do so.

3. Correct practices for deleting elements

1) Use removeObject directly (if deleting the same element)

Because it is characterized by the deletion of all isEqual objects in the array, it is appropriate to solve the problem of deleting the same elements, which does not need to be used during traversal.

2) Delete from back to front in for loop traversal

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

IV. Off-Text

  • Class Clusters is an implementation of abstract factory mode under iOS. Class Clusters only exposes simple interfaces to the outside world, but hides implementations of multiple private classes and methods inside.NSMutableArray, NSMutableDictionary are the representatives of Class Clusters.

  • Source Reference QSUseCollectionDemo

Posted by Haktar on Tue, 18 Jun 2019 09:16:40 -0700