Objective-C对象之类对象和元类对象(一)
作为C语言的超集,面向对象成为Objective-C与C语言的最大区别,因此,对象是Objective-C中最重要的部分之一。目前面向对象的语言有很多,Objective-C中的对象又和其他语言中的对象有什么区别呢?下面来简单介绍Objective-C中对象的实现。
1、Objective-C中的类
谁都知道,所有的对象都是由其对应的类实例化而来,殊不知类本身也是一种对象,先不要对这句话感到惊讶。
首先我们来关注Objective-C中的类。在Objective-C中,我们用到的几乎所有类都是NSObject类的子类,NSObject类定义格式如下(忽略其方法声明):
@interface NSObject <NSObject> { Class isa; } |
这个Class为何物?在objc.h中我们发现其仅仅是一个结构(struct)指针的typedef定义:
typedef struct objc_class *Class; |
同样的,objc_class又是什么呢?在Objective-C2.0中,objc_class的定义如下:
struct objc_class { Class isa; } |
写到这里大家可能就晕了,怎么又有一个isa??这些isa到底是什么?之间有什么区别和联系?接下来解答这一连串的疑问。
其实在Objective-C中任何的类定义都是对象。即在程序启动的时候任何类定义都对应于一块内存。在编译的时候,编译器会给每一个类生成一个且只生成一个”描述其定义的对象”,也就是苹果公司说的类对象(class
object),他是一个单例(singleton), 而我们在C++等语言中所谓的对象,叫做实例对象(instance
object)。对于实例对象我们不难理解,但类对象(class object)是干什么吃的呢?我们知道Objective-C是门很动态的语言,因此程序里的所有实例对象(instace
object)都是在运行时由Objective-C的运行时库生成的,而这个类对象(class object)就是运行时库用来创建实例对象(instance
object)的依据。
再回到之前的问题,肿么这个实例对象(instance object)的isa指针指向的类对象(class
object)里面还有一个isa呢?这个类对象(class objec)的isa指向的依然是一个objc-class,它就是“元类对象”(metaclass
object),它和类对象(class object)的关系是这样的:
2、类对象(class object)
①类对象的实质
我们知道了:类对象是由编译器创建的,即在编译时所谓的类,就是指类对象(官方文档中是这样说的:
The class object is the compiled version of the class)。任何直接或间接继承了NSObject的类,它的实例对象(instance
objec)中都有一个isa指针,指向它的类对象(class object)。这个类对象(class object)中存储了关于这个实例对象(instace
object)所属的类的定义的一切:包括变量,方法,遵守的协议等等。因此,类对象能访问所有关于这个类的信息,利用这些信息可以产生一个新的实例,但是类对象不能访问任何实例对象的内容。
当你调用一个 “类方法” 例如 [NSObject alloc],你事实上是发送了一个消息给他的类对象。
②类对象和实例对象的区别
当然有区别了,尽管类对象保留了一个类实例的原型,但它并不是实例本身。它没有自己的实例变量,也不能执行那些类的实例的方法(只有实例对象才可以执行实例方法)。然而,类的定义能包含那些特意为类对象准备的方法–类方法(
而不是的实例方法)。类对象从父类那里继承类方法,就像实例从父类那里继承实例方法一样。
③类对象与类名
在源代码中,类对象由类名表示。
在下面的例子中,Retangle类 用从NSObject那里继承来的方法来返回类的版本号:
int versionNumber = [Rectangle version]; |
只有在消息表达式中作为接收者,类名才代表类对象。其他地方,你需要要求一个实例或者类返回class
id。 响应class消息:
id aClass = [anObject class]; id rectClass = [Rectangle class]; |
如同上面的例子显示的那样,类对象像其他对象一样,也是id类型。
总之,类对象是一个功能完整的对象,所以也能被动态识别(dynamically typed),接收消息,从其他类继承方法。特殊之处在于它们是由编译器创建的,缺少它们自己的数据结构(实例变量),只是在运行时产生实例的代理。
3、元类对象(metaclass object)
元类对象的实质
实际上,类对象是元类对象的一个实例!!元类描述了 一个类对象,就像类对象描述了普通对象一样。不同的是元类的方法列表是类方法的集合,由类对象的选择器来响应。当向一个类发送消息时,objc_msgSend会通过类对象的isa指针定位到元类,并检查元类的方法列表(包括父类)来决定调用哪个方法。元类代替了类对象描述了类方法,就像类对象代替了实例对象描述了实例化方法。
很显然,元类也是对象,也应该是其他类的实例,实际上元类是根元类(root
class’s metaclass)的实例,而根元类是其自身的实例,即根元类的isa指针指向自身。
类的super_class指向其父类,而元类的super_class则指向父类的元类。元类的super
class链与类的super class链平行,所以类方法的继承与实例方法的继承也是并行的。而根元类(root
class’s metaclass)的super_class指向根类(root class),这样,整个指针链就链接起来了!!
记住,当一个消息发送给任何一个对象, 方法的检查 从对象的 isa 指针开始,然后是父类。实例方法在类中定义,
类方法 在元类和根类中定义。(根类的元类就是根类自己)。在一些计算机语言的原理中,一个类和元类层次结构可以更自由的组成,更深元类链和从单一的元类继承的更多的实例化的类。Objective-C
的类方法 是使用元类的根本原因,在其他方面试图在隐藏元类。例如 [NSObject class] 完全相等于
[NSObject self],所以,在形式上他还是返回的 NSObject->isa 指向的元类。
Objective-C语言是一组实用的折中方案。
还有些不明白? 下面这个图标可能会有些帮助:
综上所述,类对象(class object)中包含了类的实例变量,实例方法的定义,而元类对象(metaclass
object)中包括了类的类方法(也就是C++中的静态方法)的定义。类对象和元类对象中当然还会包含一些其它的东西,苹果以后也可能添加其它的内容,但对于我们只需要记住:类对象存的是关于实例对象的信息(变量,实例方法等),而元类对象(metaclass
object)中存储的是关于类的信息(类的版本,名字,类方法等)。要注意的是,类对象(class object)和元类对象(metaclass
object)的定义都是objc_class结构,其不同仅仅是在用途上,比如其中的方法列表在类对象(instance
object)中保存的是实例方法(instance method),而在元类对象(metaclass object)中则保存的是类方法(class
method)。关于元类对象可以参考苹果官方文档" The Objective-‐C Programming
Language "
4、类对象和元类对象的相关方法
①object_getClass跟随实例的isa指针,返回此实例所属的类,对于实例对象(instance)返回的是类(class),对于类(class)则返回的是元类(metaclass),
②-class方法对于实例对象(instance)会返回类(class),但对于类(class)则不会返回元类(metaclass),而只会返回类本身,即[@"instance"
class]返回的是__NSCFConstantString,而[NSString class]返回的是NSString。
③class_isMetaClass可判断某类是否为元类.
④使用objc_allocateClassPair可在运行时创建新的类与元类对,使用class_addMethod和class_addIvar可向类中增加方法和实例变量,最后使用objc_registerClassPair注册后,就可以使用此类了。看到动态语言牛逼的地方了吗,可以在需要时更改已经定义好的类!Objective-C的类别方法估计底层就是这么实现的,只是不知道为什么类别不能增加实例变量,有高手请留言。
Objective-C对象之初始化和两段构造法(二)
Objective-C为我们提供了两种初始化对象的方法:Objective-C2.0以后可用的new方法和两段构造法。既然要比较这两种初始化方法,就从它们本身的异同出发吧。
一、两段构造法
这是Objective-C特有的对象创建方法,书写形式如下:
NSString*s=[[NSString alloc] init]; |
所谓的两段构造,就是指将alloc和init分开来写,这和大多数其它语言(如C、C++、Java、JavaScript)都不一样。先来看看alloc和init都干了什么吧:
1、alloc方法
当对象创建时,cocoa会从应用程序的虚拟地址空间上为该对象分配足够的内存。cocoa会遍历该对象所有的成员变量,通过成员变量的类型来计算所需占用的内存。
当我们通过alloc或allocWithZone方法创建对象时,cocoa会返回一个未”初使化“过的对象。在这个过程中,cocoa除了上面提到的申请了一块足够大的内存外,还做了以下3件事:
①将该新对象的引用计数(Retain Count)设置成1。
②将该新对象的isa成员变量指向它的类对象。isa成员变量指向分配内存的类对象(class
object),这是在NSObject类中定义的,所以保证Cocoa的所有对象都带有此成员变量。它与Objective-C的运行时是一体的,借助该变量可以实现Cocoa对象在运行时的自省(Introspection)功能。
③将该新对象的所有其它成员变量的值设置成零。(根据成员变量类型的不同,零有可能是指nil或0)
④返回指向该对象的一个指针。
2、init方法
大部分情况下,我们都不希望所有成员变量都是零,所以
①init方法会做真正的初使化工作,让对象的成员变量的值符合我们程序逻辑中的初始化状态。例如,NSMutableString可能就会额外再申请一块字符数组,用于动态修改字符串。
②返回真正可以使用的指向该对象的指针
init还有一个需要注意的问题,某些情况下,init会造成alloc的原本空间不够用,而进行第二次分配内存空间。所以下面的写法是错的:
NSString *s=[NSString alloc]; |
[s init];// 这儿init返回的地址可能会变。s原本的指针地址可能是无效的地址。
为此,苹果引入了一个编程规范,让大家写的时候将alloc 和init写在一行。所以上面的代码正确的写法是
NSString *s=[[NSString alloc] init]; |
二、new方法
可能是为了和其他语言保持一致,苹果后来也推出了new方法来初始化对象。作为类方法的new,只是简单地等价于
alloc + init,却不能指定init的参数,所以实际使用中很少见到。
三、使用两段构造法的原因
有人可能要问,Objective-C的对象创建方法和大多数其它语言(如C、C++、Java、JavaScript)都不一样,是什么原因促使Objective-C做了这种设计?
1、历史原因
这里面多多少少就有历史的因素了。Objective-C是一门非常老的语言。如果你查阅文档,你会发现它和C++出生在同一时代(两种语言的发行年份都是1983年),都是作为C语言的面向对象的接班人被推出。当然,最终C++胜出。由于历史久远,Objective-C也无法有太多优秀的语言做参考,所以,有很多历史遗留的设计。
2、设计原则
简单看来,根据设计模式的Single Responsibility的设计原则,苹果觉得alloc和init是做的2件不同的事情,把这两件事情分开放在2个函数中,对于程序员更加清楚明了。更详细查阅文档后,我觉得这是由于历史原因,让苹果觉得alloc方法过于复杂,在历史上,alloc不仅仅是分配内存,还可以详细的指定该内存所在的内存分区(用NSZone表示)。
同时由于分配和初始化阶段是分开的,初始化方法的实现只需处理新实例的变量,并完全忽略有关分配的问题,简化了初始化方法的过程。
四、NSZone简介
早期苹果是建议程序员使用 allocWithZone来管理内存分配的,每个NSZone表示一块内存分区,+allocWithZone:(NSZone
*)zone方法可以允许对象从指定分区分配内存。内存区是Cocoa的一个功能部件,它能使同时使用的对象或计算机的地址空间中相邻的对象保持在内存中,以此提高程序的性能。要解释对象在内存中的位置会如何影响性能,需要解释应用程序需要比物理内存更大的内存时会发生什么情况。
每个Cocoa应用程序都有很大的可寻址内存,当应用程序动态的分配内存时,即使计算机的所有物理内存都已经被占用,操作系统仍然会提供内存。要满足该分配要求,操作系统会使用页面调度(paging)或者交换(swapping)操作将一些物理内存中的内容复制到硬盘,之前正在使用的物理内存就可以被提供出来使用了,而之前的那些数据应经被写入硬盘。如果有需要先前复制到硬盘的那部分内存数据,操作系统会将另外一块物理内存复制到硬盘,并将先前的旧内存再度调回内存。即时内存在硬盘间调度,操作系统仍然能为每个应用程序映射地址空间到物理内存。操作系统的这一功能即是虚拟内存(virtual
memory)。
由于从物理内存额硬盘中相互调度是很消耗时间的,因此,使用虚拟内存会影响性能。过多的页面调度会降低系统性能,这称为抖动(thrashing)。如果一起使用的两个或多个对象在内存中的位置很远,抖动发生的可能性将会大大增加,因此对象实例的内存分配的位置也很重要。
分区用于确保分配给同时使用的对象的内存位于相邻位置。当需要某个对象时,另外相邻的对象也基本会用到,需要的所有对象同时调入内存的可能性就更大,当不需要时,又可以都同时调出内存,Cocoa中的NSZone类型是指定标识内存区的C结构的对象,+allocWithZone:(NSZone
*)zone方法允许NSZone变量从指定分区分配内存。已达到减少抖动的目的。可见当年苹果的设计师们的良苦用心!!!
只是,分区是一个十分底层的东西,而且,随着硬件设备的发展,物理内存的不断增大,以及操作系统内存分配函数复杂性的提高,使用分区的最初目的已经逐渐消失了。自从Mac
OS X 10.5上引入了垃圾回收机制后,苹果就不建议程序员使用allocWithZone了,事实上,cocoa框架也会忽略+allocWithZone:(NSZone
*)zone指定的分区。苹果在文档中也提到,+allocWithZone:(NSZone *)zone仅仅是一个历史遗留设计了。 |