前言(之前网络看到的,不知出处了。)
Objective-c语言在申请对象的时,需要使用两段构造(Two Stage Creation)的模式。一个对象的创建,需要先调用alloc方法或allocWithZone方法,再调用init方法或initWithSomething方法。如下是一个NSString对象的创建示例:
NSString * str = [[NSString alloc] initWithString:@"http://www.jianshu.com"];
由于该语言的对象创建方法和大多数其它语言(如C、C++、Java、JavaScript)都不一样,所以引起了我的好奇。是什么原因促使Objective-C做了这种设计,而又是什么原因促使大多数其它语言都采用"new"方法来一次性创建对象呢?
对象的创建
我们先来看看在对象的创建过程中,alloc和init到底做了哪些事情。
alloc方法
根据苹果的官方文档。当对象创建时,cocoa会从应用程序的虚拟地址空间上为该对象分配足够的内存。cocoa会遍历该对象所有的成员变量,通过成员变量的类型来计算所需占用的内存。
当我们通过alloc或allocWithZone方法创建对象时,cocoa会返回一个未”初使化“过的对象。在这个过程中,cocoa除了上面提到的申请了一块足够大的内存外,还做了以下3件事:
1. 将该新对象的引用计数(Retain Count)设置成1。
2. 将该新对象的isa成员变量指向它的类对象。
3. 将该新对象的所有其它成员变量的值设置成零。(根据成员变量类型,零有可能是指nil或Nil或0.0)
isa成员变量是在NSObject中定义的,所以保证Cocoa的所有对象都带有此成员变量。借助该变量可以实现Cocoa对象在运行时的自省(Introspection)功能。
init方法
大部分情况下,我们都不希望所有成员变量都是零,所以init方法会做真正的初使化工作,让对象的成员变量的值符合我们程序逻辑中的初始化状态。例如,NSMutableString可能就会额外再申请一块字符数组,用于动态修改字符串。
init还有一个需要注意的问题。某些情况下,init会造成alloc的原本空间不够用,而第二次分配内存空间。所以下面的写法是错的:
NSString * s = [NSString alloc];
[s init]; // 这儿init返回的地址可能会变。s原本的指针地址可能是无效的地址。
为此,苹果引入了一个编程规范,让大家写的时候将alloc 和init写在一行。所以上面的代码正确的写法是
NSString * s = [[NSString alloc] init];
为什么这么设计
说回来文章开始时提出来问题,为什么苹果要这么设计而其它语言不这么设计?
上面提到,alloc其实不只干了申请内存的事情,还做了:
1. 内存管理的事情,设置Retain Count。
2. 运行时自省的功能,设置isa变量。
3. 非逻辑性的初使化功能,设置所有成员变量为零。
简单看来,根据设计模式的Single Responsibility的设计原则,苹果觉得alloc和init是做的2件不同的事情,把这两件事情分开放在2个函数中,对于程序员更加清楚明 了。
更详细查阅文档后,我觉得这是由于历史原因,让苹果觉得alloc方法过于复杂,在历史上,alloc不仅仅是分配内存,还可以详细的指定该内存所在 的内存分区(用NSZone表示)。
这就是下面要提到的allocWithZone方法。
在《Cocoa Design Patterns》一书也提到,早期苹果是建议程序员使用 allocWithZone来管理内存分配的,每个NSZone表示一块内存分区,allowWithZone方法可以允许对象从指定分区分配内存。了解 到这段历史后,也不难理解苹果这么设计的原因了。因为在这种情况下,alloc要处理的情况复杂,和init放到一起不合适。
而对于大多数出生在90年代的语言来说(例如Java,JavaScript,C#),由于内存具体的分配方案都不需要程序员操心了,所以就不需要单独为内存分配实现一个alloc方法了。
补充资料:allocWithZone被废弃
自从Mac OS X 10.5上引入了垃圾回收机制后,苹果就不建议程序员使用allocWithZone了,事实上,cocoa框架也会忽略allocWithZone指定的分区。