第八章 类NSObject和运行时系统
根类的作用:
作为一门动态编程语言,Objective-C有很多动态的特性,因此Objective-C不仅需要编译环境,同时还需要一个运行时系统来执行编译好的代码。运行时系统扮演的角色类似于Objective-C的操作系统,它负责完成对象生成,释放时的内存管理,为发来的消息查找对应的处理方法等。
通常情况下,程序中无法直接使用运行时系统提供的功能。根类方法中提供了运行时系统的基本功能。根类相当于运行时系统的一个接口。
根类通过哪些方式提供了哪些功能对系统有很大的影响。因此根类不同的系统之间是无开发出通用的程序的。
类和实例:
NSObject只有一个实例变量,就是Class类型的变量isa。isa用于标识实例对象属于哪个类对象。因为isa决定着实例变量和类的关系,非常重要,所以子类不可修改isa的值。另外,也不能通过直接访问isa来查询实例变量到底属于哪个类,而要通过实例方法class来完成查询。
下面对类和实例变量的相关方法进行说明:
- (Class)class //返回消息接受者所属类的类对象
+ (Class)class //返回类对象
//虽然可以使用类名作为消息的接受者来调用类方法,但当类对象是其他消息的参数,或者将类对象赋值给变量的时候,需要通过这个类方法来获取类对象
- (id)self //返回消息接收者自身。是一个无任何实际动作但很有用的方法
- (BOOL)isMemberOfClass:(Class)aClass //判断消息接受者是不是参数aClass类的对象
- (BOOL)isKindOfClass:(Class)aClass //判断消息接受者是否是参数aClass类或者aClass类的子类的实例。这个函数和isMemberOfClass:的区别在于当消息的接收者是aClass的子类的实例时也会返回YES。
- (Class)superclass //返回消息接收者所在类饿父类的类对象
+ (Class)superclass //返回消息接收类的父类的类对象
+ (Class)class //返回类对象
//虽然可以使用类名作为消息的接受者来调用类方法,但当类对象是其他消息的参数,或者将类对象赋值给变量的时候,需要通过这个类方法来获取类对象
- (id)self //返回消息接收者自身。是一个无任何实际动作但很有用的方法
- (BOOL)isMemberOfClass:(Class)aClass //判断消息接受者是不是参数aClass类的对象
- (BOOL)isKindOfClass:(Class)aClass //判断消息接受者是否是参数aClass类或者aClass类的子类的实例。这个函数和isMemberOfClass:的区别在于当消息的接收者是aClass的子类的实例时也会返回YES。
- (Class)superclass //返回消息接收者所在类饿父类的类对象
+ (Class)superclass //返回消息接收类的父类的类对象
实例对象的生成和释放:
+ (id)alloc //生成消息接收类的实例对象。通常和init或init开头的方法连用,生成实例对象的同时需要对其进行初始化。子类不允许重写alloc。
- (void)dealloc //释放实例对象。dealloc被作为release的结果调用。除了在子类中重写dealloc的情况之外,程序中不允许直接调用dealloc。
- (oneway void)release //将消息的引用计数减1。引用计数变为0时,dealloc方法被调用,消息接收者被释放
- (id)retain //为消息接收者的引用计数加1,同时返回消息接收者。
- (id)autorelease //把消息的接收者加入到自动释放池中,同时返回消息接收者。
- (NSUInteger)retainCount //返回消息接收者的引用计数,可在调试时使用这个方法。NSUInteger是无符号整数类型。
- (void)finalize //垃圾收集器在释放接收者对象之前会执行finalize方法
- (void)dealloc //释放实例对象。dealloc被作为release的结果调用。除了在子类中重写dealloc的情况之外,程序中不允许直接调用dealloc。
- (oneway void)release //将消息的引用计数减1。引用计数变为0时,dealloc方法被调用,消息接收者被释放
- (id)retain //为消息接收者的引用计数加1,同时返回消息接收者。
- (id)autorelease //把消息的接收者加入到自动释放池中,同时返回消息接收者。
- (NSUInteger)retainCount //返回消息接收者的引用计数,可在调试时使用这个方法。NSUInteger是无符号整数类型。
- (void)finalize //垃圾收集器在释放接收者对象之前会执行finalize方法
上面从dealloc到reatainCount都是手动引用计数管理内存时使用的方法,使用ARC时不可用,finalize仅供垃圾回收有效时使用。
初始化:
- (id) init //init可对alloc生成的实例对象进行初始化。子类中可以重写init或者定义新的以init开头的初始化函数。
+ (void)initialize //被用于类的初始化,也就是对类中共同使用的变量进行初始化设定等。这个方法会在类收到第一个消息之前被自动执行,不允许手动调用。
+ (id)new //new是alloc和init的组合。new方法返回的实例对象的所有者就是调用new方法的对象。但是把alloc和init组合定义为new并没有什么优点。根据类的实现不同,new方法并不会每次都返回一个全新的实例对象。有时new方法会返回对象池中预先生成的对象,也有可能每次都返回同一个对象。
+ (void)initialize //被用于类的初始化,也就是对类中共同使用的变量进行初始化设定等。这个方法会在类收到第一个消息之前被自动执行,不允许手动调用。
+ (id)new //new是alloc和init的组合。new方法返回的实例对象的所有者就是调用new方法的对象。但是把alloc和init组合定义为new并没有什么优点。根据类的实现不同,new方法并不会每次都返回一个全新的实例对象。有时new方法会返回对象池中预先生成的对象,也有可能每次都返回同一个对象。
对象的比较:
- (BOOL)isEqual:(id)anObject //消息接收者如果和参数anObject相等就返回YES。
- (NSUInteger)hash //在把对象放入容器等的时候,返回系统内部用的散列值。
- (NSUInteger)hash //在把对象放入容器等的时候,返回系统内部用的散列值。
对象内容的描述:
+ (NSString *)description //返回一个NSString类型的字符串,表示消息接收者所属类的内容。通常都是这个类的类名。
- (NSString *)description //返回一个NSString类型的字符串,表示消息接收者的实例对象的内容。通常是类名加id值。子类中也可以重新定义description的返回值。例如,NSString的实例会返回字符串的内容,NSArray的实例会对数组中的每一个元素调用description,然后将调用结果用句号进行分割,并一起返回。
- (NSString *)description //返回一个NSString类型的字符串,表示消息接收者的实例对象的内容。通常是类名加id值。子类中也可以重新定义description的返回值。例如,NSString的实例会返回字符串的内容,NSArray的实例会对数组中的每一个元素调用description,然后将调用结果用句号进行分割,并一起返回。
消息发送机制:
选择器和SEL类型:
程序中的方法名(选择器)在编译后会被一个内部标识符所替代,这个内部标识符所对应的数据类型就是SEL类型。
OC为了能够在程序中操作编译后的选择器,定义了@selector()指令,通过使用@selector()指令,就可以直接引用编译后的选择器。选择器对应的SEL类型的值和处理器相关。如果SEL类型的变量无效的话,可设其为NULL,或者也可以使用(SEL)0这种常见的表达方法。
用SEL类型的变量来发送消息的方法如下:
- (id)performSelector:(SEL)aSelector //向消息接收者发送aSelector代表的消息,返回这个消息执行的结果。
- (id)performSelector:(SEL)aSelector
withObject:(id)anObject //向消息接收者发送aSelector代表的消息,消息的参数为anObject,返回这个消息执行的结果。
- (id)performSelector:(SEL)aSelector
withObject:(id)anObject //向消息接收者发送aSelector代表的消息,消息的参数为anObject,返回这个消息执行的结果。
下面两个消息表达式进行的处理是相同的:
[target description];
[target performSelector:@selector(description)];
[target performSelector:@selector(description)];
下面这个例子展示了如何根据条件动态决定执行哪个方法:
SEL method = [cond1] ? @selector(activate:) : @selector(hide:);
id obj = [cond2] ? myDocument : defaultDocument;
[target performSelector:method withObject:obj];
id obj = [cond2] ? myDocument : defaultDocument;
[target performSelector:method withObject:obj];
这种调用方式很像C语言中的函数指针,利用函数指针也可以实现和上面同样的功能。
函数指针是函数在内存中的地址,指针对应的函数是在编译时决定的,不能够执行指定之外的函数。SEL类型就相当于方法名,根据消息接收者的不同,来动态执行不同的方法。
消息搜索:
对象受到一个消息后会执行哪个方法是被动态决定的。
所有的实例变量都存在一个Class类型的isa变量,它就是类对象。当收到消息后,运行时系统会从类内开始检查是否有和这个消息选择器相同的方法,找到就执行。没找到就去父类中找,一直到根类,如果还没找到,就会提示执行时错误。
如果每次收到消息都需要查找相应的方法,会造成很大的开销。因此,运行时系统内部会缓存一个散列表。当下次在收到同样的消息时,直接利用上次缓存的信息即可,不需要从头搜索。
NSObject定义了一个可以动态查询一个对象是否能够响应某个选择器的方法:
- (BOOL)respondsToSelector:(SEL)aSelector //查询消息接收者中是否有能够响应aSelector的方法,包括从父类继承来的方法。
- (BOOL)instancesRespondToSelector:(SEL)aSelector //查询消息接收者所属的类中是否有能够响应aSelector的方法,包括从父类继承来的方法。
- (BOOL)instancesRespondToSelector:(SEL)aSelector //查询消息接收者所属的类中是否有能够响应aSelector的方法,包括从父类继承来的方法。
以函数的形式来调用方法:
类中定义的方法通常是以函数的形式实现的,但通常在编程的时候并不会直接操作方法所对应的函数。但如果想让程序尽可能的快一点,或者需要按照C语言的惯例传递函数指针的时候,可以直接调用方法对应的函数,以节省发送消息的开销。另外,执行动态加载方法的定义等时,也可以将方法作为函数调用。但是,如果以函数的形式来调用方法的话,将无法利用面向对象的动态绑定等功能。虽然消息发送同函数调用相比确实慢一点,但却有面向对象的动态绑定,多态等优点。同这些优点相比,速度上略微的损失是不值得一提的。
通过下面的方法,可以获得某个对象持有的方法的函数指针,这些方法都被定义在NSObject中:
- (IMP)methodForSelector:(SEL)aSelector //搜索和指定选择器相对应的方法,并返回指向该方法实现的函数指针。实例对象和类对象都可以使用这个方法。对实例对象使用时,会返回实例方法对应的函数,对类对象使用时,会返回类对象对应的函数。
+ (IMP)instanceMethodForSelector:(SEL)aSelector //搜索和指定选择器相对应的实例方法,并返回指向该实例方法实现的函数指针。
+ (IMP)instanceMethodForSelector:(SEL)aSelector //搜索和指定选择器相对应的实例方法,并返回指向该实例方法实现的函数指针。
IMP是“implementation”的缩写,它是一个函数指针,指向了方法实现代码的入口。IMP的定义为:
typedef id (*IMP)(id, SEL, ...);
这个被指向的函数包括id(self指针),调用的SEL(方法名),以及其他一些参数。例:
- (id)setBox:(id)obj1 title:(id)obj2;
foo是这个方法所属类的一个实例变量。获取指向setBox的函数指针,并通过该指针进行函数调用过程如下:
IMP funcp;
funcp = [foo methodForSelector:@selector(setBox:title:)];
xyz = (*funcp)(foo, @selector(setBox:title:), param1, param2);
funcp = [foo methodForSelector:@selector(setBox:title:)];
xyz = (*funcp)(foo, @selector(setBox:title:), param1, param2);
对self进行赋值:
self是方法的一个隐含参数,它代表的是收到消息的对象自身。
- (id)initWitMax:(int)a /*推荐使用这种写法*/
{
if ((self = [super init]) != nil) {
max = a;
}
return self;
}
在OPENSTEP时代,OC初始化方法一般采用下面这种写法。但这里需要注意的是没有用父类初始化方法的返回值对self进行赋值。子类的初始化方法和父类的初始化方法都是对同一个对象进行操作的,所以不需要显示地对self进行赋值操作。需要注意的是这种写法也有可能出错,除了初始化失败外,父类的初始化方法也有可能并没有返回self而是返回了其他对象。如类簇构成的类在初始化方法中就没有返回self。所以第一种方法是更为安全的做法。
- (id)initWithMax:(int)a /*旧的写法*/
{
[super init];
max = a;
return self;
}
{
if ((self = [super init]) != nil) {
max = a;
}
return self;
}
在OPENSTEP时代,OC初始化方法一般采用下面这种写法。但这里需要注意的是没有用父类初始化方法的返回值对self进行赋值。子类的初始化方法和父类的初始化方法都是对同一个对象进行操作的,所以不需要显示地对self进行赋值操作。需要注意的是这种写法也有可能出错,除了初始化失败外,父类的初始化方法也有可能并没有返回self而是返回了其他对象。如类簇构成的类在初始化方法中就没有返回self。所以第一种方法是更为安全的做法。
- (id)initWithMax:(int)a /*旧的写法*/
{
[super init];
max = a;
return self;
}
另外,在用ARC的时候,如果初始化方法的返回值没有被用到,编译就会发生错误。
发送消息的速度:
消息送信所需要的时间是函数调用所需时间的2倍。另外,直接发送消息比用performSelector:发送消息速度更快。
类对象和根类:
类对象的类被叫做元类。实例对象所属的类是class。类对象所属的类是元类。
OC中很多的概念都来自Smalltalk,元类的概念就是其中之一。但现在的OC中已经不存在元类的概念了,程序中也不能操作元类。用于表示对象的id类型和表示类的Class类型实际上都是指向结构的指针。
类对象和实例对象都存在一个成员变量isa,它是一个objc_class类型的指针。类对象中保存的是实例方法,元类对象中保存的是类方法,通过这样的定义能够统一实现实例方法和类方法的调用机制。
任何一个类对象都是都是继承了根类的元类对象的一个实例。也就是说,类对象可以执行根类对象的实例方法。
总结:
- 所有类的实例对象都可以执行根类的实例方法
- 如果在派生类在重新定义类实例方法,新定义的方法会被执行
- 所有类的类对象都可以执行根类的类方法
- 如果在派生类中重新定义了类方法,新定义的方法会被执行 。
- 所有类的类对象都可以执行根类的实例方法
- 即使在派生类中重新定义了实例方法,根类中的方法也会被执行。
- 如果在派生类中将实例方法作为类方法重新定义了的话,新定义的方法会被执行。
Target-action paradigm(目标-动作模式):
通过使用SEL类型的变量,能够在运行时动态决定执行哪个方法。实际上,Application框架就利用了这种机制实现了GUI控件对象间的通信。
来看看下面这个例子:
@interface myCell : NSObject
{
SEL action;
id target;
...
}
- (void)setAction:(SEL)aSelector;
- (void)setTarget:(id)anObject;
- (void)performClick:(id)sender;
...
@end
@implementation myCell
- (void)setAction:(SEL)aSelector
{
action = aSelector;
}
- (void)setTarget:(id)anObject
{
target = anObject;
}
- (void)performClick:(id)sender
{
(void)[target performClick:action withObject:sender];
}
...
@end
{
SEL action;
id target;
...
}
- (void)setAction:(SEL)aSelector;
- (void)setTarget:(id)anObject;
- (void)performClick:(id)sender;
...
@end
@implementation myCell
- (void)setAction:(SEL)aSelector
{
action = aSelector;
}
- (void)setTarget:(id)anObject
{
target = anObject;
}
- (void)performClick:(id)sender
{
(void)[target performClick:action withObject:sender];
}
...
@end
Application框架的目标-动作模式在发送消息时使用了下面这种形式定义的方法,即只有一个id类型的参数,没有返回值。这种形式的方法叫做动作方法。
- (void) xxxx:(id)sender;
当用户操作了某个GUI控件,action指定的消息就会被发送到事先设定好的target,消息定义如上所示,消息的参数通常都是GUI的控件id。这样一来,消息的接收者target就会知道到底哪个控件发送了什么样的消息。
setTarget:和setAction:来指定目标和动作,使用ARC时推荐使用弱引用。
UIKit是iPhone和iPad中用来建立和管理应用程序界面的框架。Application框架中的动作方法只有一种格式,UIKit框架中的动作方法则有以下三种格式:
- - (void)xxxx;
- - (void)xxxx:(id)sender;
- - (void)xxxx:(id)sender forEvent:(UIEvent *)event;//表示与操作相关的事件信息
Xcode中的动作方法和Outlet的写法:
综合开发环境Xcode及其用于设计和测试GUI的工具Interface Builder中,为了连接对象,在类的接口部分中声明了一些宏,通过这些宏变量能够将代码连接到nib。
动作方法的声明如下,IBAction是一个宏,被定义为void:
- (IBAction) xxxx:(id)sender;
outlet的含义:
outlet可以被理解为一个插座,可以通过outlet从控件取出信息,或将新的信息赋给控件。outlet通常是一个类的实例变量,例如:一个指向NSButton类型的控件的实例变量声明如下:
IBOutlet NSButton *theButton;
这里的IBOutlet也是一个宏,被定义为空,另外还有下面这样一种定义方式:
IBOutletCollection(NSButton) NSArray *buttons;
这样声明之后,就可以将Xcode上面的多个控件都连接到这个Outlet上。IBOutletCollection(NSButton)也是一个宏定义,编译之后会被替换为空。在多个控件需要统一处理的时候,这种定义方法会很方便。
属性声明也可以定义为Outlet,如下所示:
@property(weak) IBOutlet NSButton *okButton;
Xcode可以把这个属性声明看作是Outlet,并用其来连接控件。另外也可以为这个属性声明增加访问方法。
使用ARC进行开发的情况下,要注意避免形成对象之间的引用循环。所以,除了主要的对象之间的连接使用强引用之外,其余的对象之间进行连接时都推荐使用弱引用。属性声明时,建议加上assign或者weak选项。
Objective-C和Cocoa环境:
cocoa环境和Mac OS X:
我们经常提到的cocoa环境通常是指AppKit和Foundation这两个核心框架,但有时候也包含Core Foundation或Core Date等框架。
Cocoa是为了提供构建应用程序所必需的功能而设计的,所以才会利用下层的功能。
Cocoa Touch 和iOS:
iPhone和iPad的操作系统iOS使用Cocoa Touch作为GUI环境。下图展示了iOS的架构以及和Cocoa Touch的关系。
有Foundation和UIKit框架组合而成的GUI环境称为Cocoa Touch:
框架:
将开发软件和执行软件所必需的图形库,头文件和设定用的各种信息全部汇总在一起就构成了框架。其中包括了应用程序执行时所必须的动态链接库。
框架提供了程序运行的基本功能和GUI基础。在这些基本功能之上,通过添加独有的处理,就可以实现要实现的功能。
在Mac OSX中,将开发和执行软件所必需的图形库、头文件和设定用的各种信息全部汇总在一起就构成了框架。值得一提的是,其中包括了应用程序执行时所必须的动态链接库。
框架是应用程序的骨架的意思。框架提供了程序运行的基本功能和GUI基础。在这些基本功能
之上,通过添加独有的处理,就可以实现要实现的功能。
Mac OSX中最重要的框架是Foundation框架、Application框架(也称为Appkit框架或Application Kt框架)、Core Foundation框架和System框架。Foundation框架提供了包括NSObject在内的Objective-C的基本类库。Core Foundation框架是一组C语言接口,它们为iOS应用程序提供基本的数据管理和服务功能(详情请参照附录B)。Application框架包含了与Cocoa的GUI的基础——窗口环境相关的类。另外,System框架包含了与Cocoa最底层的Mach核心和Unix相关的类库。因为系统框架通常都和程序执行相关,所以编译程序的时候不需要指定-framewok选项。
还有一个Cocoa框架,实际上它是由Foundation框架、Application框架和CoreData框架组成的。像这样,将多个框架嵌套打包的技术称为umbrella framework。不过该技术仅仅是苹果公司提供功能的一种方法,不建议普通开发者提供自己的umbrella framework。
如上图,iOS中的Cocoa Touch由Foundation框架和UIKit框架构成。iOS 的 Foundation框架和Mac OS X的Foundation框架有很大一部分是共用的,例如字符串或数组等的基本类。UIKit是负责用户接口的框架,其中定义了基本的GUI控件和用于处理触摸屏幕之类的事件的类。另外,iOS可以通过利用Core Foundation框架中的数据结构提供面向其他框架或设备的功能。
框架的构成和头文件:
框架的每个目录通常包含以下要素:
- 类库----框架同名文件
- CodeResources----记录了文件散列值的XML文件
- Headers----包含了头文件的目录
- Resources----包含了不同国家语言用的文件等各类资源等目录
- Versions----包含了框架的各种版本的目录
#import<框架名/头文件名>
全新的运行时系统
64位模型和整数类型:
数据类型发生了变化的话,主要受影响的是调用API时的参数和返回值的类型。
ILP32和LP64中的int类型都是32位,这种情况下,就算数据类型变更到了64位,因为当前的程序使用的整数类型还是32位,所以也不能利用到64位的优点。为了解决这个问题,Cocoa环境引入了NSInter类型,NSInteger在32位的数据模型下被定义为int,在64位数据模型下被定义位long。除此之外,Cocoa中还定义了无符号的NSUInteger类型。
健壮的实例变量:
如果框架中的类发生了变化,应用程序就一定得重新编译才能够在早期的运行时系统中继续执行。这称为脆弱的二进制接口。
现代运行时系统针对这个问题进行了改进,使实例变量发生变化的情况下,应用程序不重新编译也能够继续执行。这称为健壮的实例变量。但实例变量的改变仅限于变量顺序的变化或增加了新的实例变量这种程度,删除了实例变量或改变了实例变量的类型时还是需要重新编译。