从Swift看Objective-C的数组使用

状态维护是个怎么说都不够的话题,毕竟状态的处理是我们整个App最核心的部分,也是最容易出bug的地方。之前写过一篇以函数式编程的角度看状态维护的文章,这次从Swift语言层面的改进,看看Objective C下该如何合理的处理数组的维护。

Objective C数组的内存布局

要了解NSArray,NSSet,NSDictionary这些集合类的使用方法,我们需要先弄明白其对应的内存布局(Memory Layout),以一个NSMutableArray的property为例:


1

2

3

4

5

//declare

@property (nonatomic, strong) NSMutableArray*                 arr;

//init

self.arr = @[@1, @2, @3].mutableCopy;

arr初始化之后,以64位系统为例,其实际的内存布局分为三块:

第一块是指针NSMutableArray* arr所处的位置,为8个字节。第二块是数组实际的内存区域所处的位置,为连续3个指针地址,各占8个字节一共24个字节。第三块才是@1,@2,@3这些NSNumber对象真正的内存空间。当我们调用不同的API对arr进行操作的时候,要分清楚实际是在操作哪部分内存。

比如:


1

self.arr = @[@4];

是在对第一块内存区域进行赋值。


1

self.arr[0] = @4;

是在对第二块内存区域进行赋值。


1

[self.arr[0] integerValue];

是在访问第三块内存区域。

之前写过一篇多线程安全的文章,我们知道即使在多线程的场景下,对第一块内存区域进行读写都是安全的,而第二块和第三块内存区域都是不安全的。

NSMutableArray为什么危险?

在Objective C的世界里,带Mutable的都是危险分子。我们看下面代码:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

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

NSMutableArray* localArr = self.arr;执行之后,我们的内存模型是这样的:

这行代码实际上只是新生成了8个字节的第一类内存空间给localArr,localArr实际上还是和arr共享第二块和第三块内存区域,当在thread 2执行[localArr removeAllObjects];清理第二块内存区域的时候,如果主线程正在同时访问第二块内存区域_arr[1],就会导致crash了。这类问题的根本原因,还是在对于同一块内存区域的同时读写。

Swift的改变

Swift对于上述的数组赋值操作,从语言层面做了根本性的改变。

Swift当中所有针对集合类的操作,都符合一种叫copy on write(COW)的机制,比如下面的代码:


1

2

3

4

5

6

7

8

9

10

var arr = [1, 2, 3]

var localArr = arr

print("arr: \(arr)")

print("localArr: \(localArr)")

arr += [4];

print("arr: \(arr)")

print("localArr: \(localArr)")

当执行到var localArr = arr的时候,arr和localArr的内存布局还是和Objective C一致,arr和localArr都共享第二第三块内存区域,但是一旦出现写操作(write),比如arr += [4];的时候,Swift就会针对原先arr的第二块内存区域,生成一份新的拷贝(copy),也就是所谓的copy on write,执行cow之后,arr和localArr就指向不同的第二块内存区域了,如下图所示:

一旦出现针对arr写操作,系统就会将内存区域2拷贝至一块新的内存区域4,并将arr的指针指向新开辟的区域4,之后再发生数组的改变,arr和localArr就指向不同的区域,即使在多线程的环境下同时发生读写,也不会导致访问同一内存区域的crash了。

上面的代码,最后打印的结果中,arr和localArr中所包含的元素也不一致了,毕竟他们已经指向各自的第二类内存区域了。

这也是为什么说Swift是一种更加安全的语言,通过语言层面的修改,帮助开发者避免一些难以调试的bug,而这一切都是对开发者透明的,免费的,开发者并不需要做特意的适配。还是一个简单的=操作,只不过背后发生的事情不一样了。

Objective C的领悟

Objective C还没有退出历史舞台,依然在很多项目中发挥着余热。明白了Swift背后所做的事情,Objective C可以学以致用,只不过要多写点代码。

Objective C既然没有COW,我们可以自己copy。

比如需要对数组进行遍历操作的时候,在遍历之前先Copy:


1

2

3

4

NSMutableArray* iterateArr = [self.arr copy];

for (int i = 0; i < iterateArr.count; i ++) {

    NSLog(@"element: %@", iterateArr[i]);

}

比如当我们需要修改数组中的元素的时候,在开始修改之前先Copy:


1

2

3

4

5

6

7

self.arr = @[@1, @2, @3].mutableCopy;

   

NSMutableArray* modifyArr = [self.arr mutableCopy];

[modifyArr removeAllObjects];

[modifyArr addObjectsFromArray:@[@4, @5, @6]];

self.arr = modifyArr;

比如当我们需要返回一个可变数组的时候,返回一个数组的Copy:


1

2

3

4

5

6

7

- (NSMutableArray*)createSamples

{    

    [_samples addObject:@1];

    [_samples addObject:@2];

    

    return [_samples mutableCopy];

}

只要是针对共享数组的操作,时刻记得copy一份新的内存区域,就可以实现手动COW的效果,这样Objective C也能在维护状态的时候,是多线程安全的。

Copy更健康

除了NSArray之外,还有其他集合类NSSet,NSDictionary等,NSString本质上也是个集合,对于这些状态的处理,copy可以让他们更加安全。

宗旨是避免共享状态,这不仅仅是出于多线程场景的考虑,即使是在UI线程中维护状态,在一个较长的时间跨度内状态也可能出现意料之外的变化,而copy能隔绝这种变化带来的副作用。

当然copy也不是没有代价的,最明显的代价是内存方面的额外开销,一个含有100个元素的array,如果copy一份的话,在64位系统下,会多出800个字节的空间。这也是为什么Swift只有在write的时候才copy,如果只是读操作,就不会产生copy额外的内存开销。但综合来看,这点内存开销和我们程序的稳定性比起来,几乎可以忽略不计。在维护状态的时候多使用copy,让我们的函数符合Functional Programming当中的纯函数标准,会让我们的代码更加稳定。

总结

学习Swift的时候,如果细心观察,可以发现其他很多地方,也有Swift避免共享同一块内存区域的语法特性。要能真正理解这些语言背后的机制,说到底还是在于我们对于memory layout的理解。

时间: 2024-08-01 23:38:59

从Swift看Objective-C的数组使用的相关文章

Swift入门(五)——数组(Array)

集合 集合的定义 Swift中提供了两种数据结构用于存放数据的集合,分别是数组(Array)和字典(Dictionary).他们的主要区别在于数组中的元素由下标确定,而字典中的数据的值由数据的键(Key)决定.以下我们认为集合就是数组或字典. 集合的可变性 我们可以定义一个集合常量或者集合变量.一旦定义为常量,就意味着集合的长度.内容和顺序都不能再修改了.比如,定义为常量的数组,不能再向其中添加新的元素. 数组的创建 由于swift中变量的创建遵循" var 变量名:变量类型 "的语法

Swift学习笔记四:数组和字典

最近一个月都在专心做unity3d的斗地主游戏,从早到晚,最后总算是搞出来了,其中的心酸只有自己知道.最近才有功夫闲下来,还是学习学习之前的老本行--asp.net,现在用.net做项目流行MVC,而不是之前的三层,既然技术在更新,只能不断学习,以适应新的技术潮流! 创建MVC工程 1.打开Visual studio2012,新建MVC4工程 2.选择工程属性,创建MVC工程 3.生成工程的目录 App_Start:启动文件的配置信息,包括很重要的RouteConfig路由注册信息 Conten

初识Swift(二)-数组与数据字典

最近学院发疯,要期末了,却要补一个期中考试,一直在忙,没有继续学下去.郁闷了好几天,今天终于得到一个实习的机会,只能加紧ios的学习,不过,我先学习一下Swift,等到书到了,就开始好好弄一下ios.言归正传,继续Swift数组和数据字典的学习. 不过,到这里,真的发现,Swift有js的身影,比传统语言简单了好多. 数组 定义与初始化 方式一 import Foundation var a1:String[]=[] a1+="aa1" a1+="aa2" a1+=

Swift编程语言翻译与学习——数组

Swift 语言提供经典的数组和字典两种集合类型来存储集合数据.数组用来按顺序存储相同类型的数据.字典虽然无序存储相同类型数据值但是需要由独有的标识符引用和寻址(就是键值对). Swift 语言里的数组和字典中存储的数据值类型必须明确. 这意味着我们不能把不正确的数据类型插入其中. 同时这也说明我们完全可以对获取出的值类型非常自信. Swift 对显式类型集合的使用确保了我们的代码对工作所需要的类型非常清楚,也让我们在开发中可以早早地找到任何的类型不匹配错误. 注意: Swift 的数组结构在被

Swift 中 String 与 CChar 数组的转换

在现阶段Swift的编码中,我们还是有很多场景需要调用一些C函数.在Swift与C的混编中,经常遇到的一个问题就是需要在两者中互相转换字符串.在C语言中,字符串通常是用一个char数组来表示,在Swift中,是用CChar数组来表示.从CChar的定义可以看到,其实际上是一个Int8类型,如下所示: 1 2 3 4 5 /// The C 'char' type. /// /// This will be the same as either `CSignedChar` (in the comm

Swift学习第一天之数组

Swift学习第一天: 1:数组的使用 数组的定义: let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 2:数组的遍历 for num in numbers { print(num) } 3:也是通过下标来制定内容 let num1 = numbers[0] let num2 = numbers[1] 4.定义可变不可变 `let` 定义不可变数组 `var` 定义可变数组 5.向可变数组里面追加内容 array1.append("wangwu"

IOS-Swift、Objective-C、C++混合编程

1.Objective-C调用C++代码 后缀为m文件的是Objective-C的执行文件,而后缀为mm文件的是Objective-C++文件. 直接在Objective-C中是无法调用C++代码的,所以如果需要在Objective-C调用C++语言就需要直接将后缀m文件改为mm,然后就可以调用C++代码了. Objective-C兼容C,Objective-C++兼容C.C++. 接下来是在OC工程中创建C++文件,并调用C++的代码: 然后在OC文件中直接用C++的语法调用C++,所以前提是

Swift中的元组,数组,字典

元组(Tuples)与数组和字典的差别较大,元组是把多个值组成一个复合值: let http 404 Error = (404,"Not Found") //http 404 的类型是(Int,String),值是(404,"Not Found") 可以在定义元组的时候给单个元素命名,这时候元组的数组有一些相似: let http200Status = (statusCode: 200, description: "OK") 数组(Array)和

Swift - 将字符串拆分成数组(把一个字符串分割成字符串数组)

在Swift中,如果需要把一个字符串根据特定的分隔符拆分(split)成字符串数组,通常有如下两种方法: 1,使用componentsSeparatedByString()方法 1 2 3 4 5 let str = "北京.上海.深圳.香港" print("原始字符串:\(str)") let splitedArray = str.componentsSeparatedByString(".") print("拆分后的数组:\(spl