State maintenance is a topic that cannot be talked about enough. After all, state handling is the core part of our entire App, and it is also the most bug-prone place. I’ve written about state maintenance from a functional programming perspective. This time, I’ll take a look at how to properly handle array maintenance in Objective C from the Swift language level.

The memory layout of Objective C arrays

NSArray, NSSet, NSDictionary, NSArray, NSSet, NSMutableArray

//declare
@property (nonatomic, strong) NSMutableArray*                 arr;
//init
self.arr = @[@1, @2, @3].mutableCopy;
Copy the code

After arR initialization, taking a 64-bit system as an example, its actual memory layout is divided into three parts:



The first piece is the pointerNSMutableArray* arrThe position is 8 bytes. The second block is the location of the actual memory region of the array, which is the location of the three consecutive pointer addresses, each of which is 24 bytes. The third block is the real memory for the at sign 1, at sign 2, at sign 3 NSNumber objects. When we call different apis to operate on arR, we need to know which part of memory we are actually operating on.

Such as:

self.arr = @[@4];Copy the code

We’re assigning to the first memory region.

self.arr[0] = @4;Copy the code

I’m assigning to the second memory region.

[self.arr[0] integerValue];Copy the code

We’re accessing the third memory region.

In a previous article on multithreaded security, we learned that even in multithreaded scenarios, it is safe to read or write to the first memory region, but not to read or write to the second or third memory region.

Why is NSMutableArray dangerous?

In Objective C, Mutable is dangerous. Let’s look at the following code:

//main thread self.arr = @[@1, @2, @3].mutableCopy; for (int i = 0; i < _arr.count; i ++) { NSLog(@"element: %@", _arr[i]); } //thread 2 NSMutableArray* localArr = self.arr; //get result from server NSArray* results = @[@8, @9, @10]; //refresh local arr [localArr removeAllObjects]; [localArr addObjectsFromArray:results];Copy the code

NSMutableArray* localArr = self.arr; After execution, our memory model looks like this:



This line of code actually generates 8 bytes of first class memory space for localArr, which actually shares the second and third memory areas with ARR when executed on Thread 2[localArr removeAllObjects];If the main thread is accessing the second memory region while clearing the second memory region_arr[1]“Would cause a crash. The root cause of this problem is reading and writing simultaneously to the same memory area.

The change of the Swift,

Swift has made a fundamental language change to the above array assignment operation.

All operations on collection classes in Swift comply with a mechanism called copy on Write (COW), as in the following code:

var arr = [1, 2, 3]
var localArr = arr
print("arr: \(arr)")
print("localArr: \(localArr)")

arr += [4];
print("arr: \(arr)")
print("localArr: \(localArr)")Copy the code

When var localArr = arr is executed, the memory layout of ARR and localArr is the same as in Objective C, arR and localArr share the second and third memory regions, but once a write operation occurs, such as arr += [4]; After COW is executed, ARR and localArr will point to different second memory region, as shown in the figure below:



Once a write to arR occurs, the system copies the memory region 2 to a new memory region 4, and points the pointer to arR to the new region 4. Then the array changes, and ARR and localArr point to different regions. Even in a multithreaded environment, both the read and write occur at the same time. It will no longer cause crashes to access the same memory region.

In the code above, the final printed result, arR and localArr contain different elements, after all, they already point to the second type of memory region.

This is why Swift is a more secure language, with language changes that help developers avoid hard-to-debug bugs, all of which are transparent to developers, free of charge, and require no special adaptation. It’s still a simple = operation, but what’s going on behind the scenes is different.

Understanding Objective C

Objective C isn’t dead yet, and it’s still in the works for a lot of projects. Understanding what Swift is doing behind the scenes, Objective C can “learn from foreigners to beat them”, but with a little more code.

In Objective C since there’s no COW, we can copy it ourselves.

For example, Copy an array before iterating through it:

NSMutableArray* iterateArr = [self.arr copy];
for (int i = 0; i < iterateArr.count; i ++) {    
    NSLog(@"element: %@", iterateArr[i]);
}
Copy the code

For example, when we need to modify an element in an array, we Copy it before starting to modify it:

self.arr = @[@1, @2, @3].mutableCopy;
NSMutableArray* modifyArr = [self.arr copy];
[modifyArr removeAllObjects];
[modifyArr addObjectsFromArray:@[@4, @5, @6]];
self.arr = modifyArr;Copy the code

For example, when we need to return a mutable array, we return a Copy of the array:

- (NSMutableArray*)createSamples
{    
    [_samples addObject:@1];
    [_samples addObject:@2];    
    return [_samples mutableCopy];
}Copy the code

As long as the operation on a shared array, always remember to copy a new memory area, you can implement manual COW effect, so that Objective C can maintain state, multithreaded safety.

Copy more healthy

In addition to NSArray, there are other collection classes NSSet, NSDictionary, etc., NSString is also a collection by nature, and copy can make them more secure for handling those states.

The goal is to avoid sharing state, and not just for multithreaded scenarios. Even if state is maintained in UI threads, it can change unexpectedly over a long time span, and copy can isolate the side effects of that change.

Copy is not without its costs, of course. The most obvious cost is the memory overhead. A copy of an array of 100 elements can add 800 bytes of space on a 64-bit system. This is why Swift only copies when it writes; if it only reads, it incurs no additional memory overhead of copy. But all in all, this memory overhead is negligible compared to the stability of our program. Using copy more often when maintaining state, and making our functions conform to the pure function standard in Functional Programming, will make our code more stable.

conclusion

If you look closely when you learn Swift, you’ll see that there are many other places where Swift avoids sharing the same memory area syntactic features. To truly understand the mechanics behind these languages, it all comes down to our understanding of Memory layout.

Welcome to follow the public account: MrPeakTech