9.1前言
内存管理在objective-C 2.0中是非常重要的,内存管理做得好不好,最直观的就是反应在你的程序crash上面。想要你的程序健壮稳定,那么请务必搞明白内存管理。
9.2 objective-C 2.0是怎样一个内存机制?
在objective-C 2.0语言中,内存管理主要是基于一个 release count的值来进行判断系统是否要回收该内存。当某对象的 retain count等于0时,系统则会回收这段内存。所以当我们想要释放掉某项目时,我们只需要使其retain count等于0即可。当我们想要hold住该段内存以便后面继续使用(具体怎么用将会在后面提到)那么你只需要保证在你要使用它之前 它的retain count>0即可。为何苹果要这样做?等下告诉你。
首先还是说明 retain count是如何去操作的。假设我们有一个class A:NSObject,然后我们声明一个变量 A *a,不用在意它是临时变量还是类属性变量,这时候它的retain count等于0;当我们给这个变量创建内存空间的时候 我们会使用NSObject类的 alloc 函数:a=[A alloc];这时候 a的retain count则会+1,能使 retain count +1的函数不止有alloc,还有 [a retain],和[a copy],关于这两个函数会在什么时候使用以及他们的区别,将在后面讨论。要想使retain count -1 你需要调用函数 [a release];那么写一串代码来直观的表示retain count的变化:
/* * 略* */
-( void) retainCountDemo
{
A *a; // retain count of a is 0
[a retain]; // retain count of a is 2
[a release]; // retain count of a is 1
A* a1 = [a copy]; // retain count of a1 is 1,and a's is 1 too
[a release]; // retain count of a is 0, and it will be dealloced
[a1 release]; // retain count of a1 is 0, and it will be dealloced
return;
}
9.3 copy 和 retain的区别?
copy和retain的区别在于它们的字面意思,不同之处在于一个是直接引用(比如说对象的引用),然后retain count+1,另外一个是复制使用(比如说字符串的使用),将复制到的对象a1的retain count+1.
9.4 他们有什么用?
objective-C 2.0是一门面向对象的语言,通过使用 retain count来就可以让开发者在各自的模块中创建使用以后就“释放”,而不用担心影响到其他模块如果同时会使用到该变量而造成的crash。最典型也是最常用的就是我们的NSURLConnection类里面的request,这种异步请求,你并不知道什么时候该释放,因为你没法判断什么时候请求能够返回,所以通过使用retain count 你只关心你创建了一个 NSURLConnction,在设置完接收对象,发送完请求以后你就可以release掉它了,而无需关心内存泄露之类的问题。因为在发送请求的时候,NSURLConnction会将自己retain一次,这时候的retain count变为2,你在自己的函数中调用release的时候retaincount则变为1,而当该请求返回的时候,完成回调函数,会自动调用release一次,这时候NSURLConnction的retain count变为0而释放掉。
通过上面的例子来总结,我们可以发现,其实这是一种延迟释放的机制(好吧,这个词是我自己想到的)。依照这个我们可以解决很多对象共用而不知道该谁去管理内存的问题,苹果的做法就是:管好你自己,你自己alloc+retain多少次,那么请你release多少次。所以,内存管理最基本的就是管好你自己!
9.5 内存管理有没有更简单点的方法呢?
有!但是不建议使用。
方法1:autorelease函数,通过函数名你应该可以知道这个是帮你自动release的。但是需要注意的是这个函数只会自动帮你release1次,你如果在中间使用了retain之类的,所以还请手动release。同时这个有一个致命的缺点,你想用autorelease在class A中创建对象然后传递给class B使用的话这是非常危险的,autorelease是基于系统自带的自动释放池来进行内存管理,系统会每隔一段时间去检测施放池中的对象,并且释放不在使用的对象。当你传递给B的时候,还没来得及使用,被自动释放掉了,那么你的程序又会crash。所以 autorelease通常都是在局部对象中使用。
方法2:IOS 5 ARC(Automatic Reference Counting),苹果终于发现了做IOS开发有太多无证程序猿了,经常因为内存释放不好而导致野指针,内存泄露,各种问题的存在。所以苹果在IOS5的SDK中加了这么一个东西,自动引用计数器。好了,这下各位可以放心的使用,而不用麻烦的去数retain release了。如果你能保证你的客户都会乖乖升级到IOS5的话…… 显然,暂时这东西还不太靠谱……
9.6 稍微高级点的应用:
于是我就只想到一个大量生成的临时对象和自动释放池这两个名词,具体如何使用,请去查阅苹果API,keywords:NSAutoreleasePool。
9.7 后记:
上面所写都是我自己对Objective-C的理解和自己以前所犯错误而总结出来的经验,遗憾的是正好我高考语文考试不及格,所以写的文章也不太好懂,还请见谅,如果有什么疑问或者意见,还请指出一起讨论学习。实例代码,非常简单,如果有朋友看了也不太明白,还请自己写demo验证。对于委托(delegate),通知(notification),属性等这些东西本来是想也在这里说一说的,但是因为思维跳跃比较大,所以还是抽时间另外写出来。
Objective-C 2.0 with Cocoa Foundation- 8,类方法以及私有方法
8,类方法以及私有方法
本系列讲座有着很强的前后相关性,如果你是第一次阅读本篇文章,为了更好的理解本章内容,笔者建议你最好从本系列讲座的第1章开始阅读,请点击这里。
Objective-C里面区别于实例方法,和Java或者C++一样,也支持类方法。类方法(Class Method) 有时被称为工厂方法(Factory Method)或者方便方法(Convenience method)。工厂方法的称谓明显和一般意义上的工厂方法不同,从本质上来说,类方法可以独立于对象而执行,所以在其他的语言里面类方法有的时候被称为静态方法。就像@interface曾经给我们带来的混乱一样,现在我们就不去追究和争论工厂方法的问题了,我们看到Objective-C的文章说工厂方法,就把它当作类方法好了。
在Objective-C里面,最受大家欢迎的类方法应该是alloc,我们需要使用alloc来为我们的对象分配内存。可以想象,如果没有alloc,我们将要如何来为我们的类分配内存!
和其他的语言类似,下面是类方法的一些规则,请大家务必记住。
1,类方法可以调用类方法。
2,类方法不可以调用实例方法,但是类方法可以通过创建对象来访问实例方法。
3,类方法不可以使用实例变量。类方法可以使用self,因为self不是实例变量。
4,类方法作为消息,可以被发送到类或者对象里面去(实际上,就是可以通过类或者对象调用类方法的意思)。
如果大家观察一下Cocoa的类库,会发现类方法被大量的应用于方便的对象创建和操作对象的,考虑到类方法的上述的特性,同学们在设计自己的类的时候,为了谋求这种方便,可以考虑使用类方法来创建或者操作对象。笔者认为,这个就是类方法的潜规则,在本章的范例程序里面,笔者将要遵守这个潜规则。
在上一章我们讲了一下实例变量的作用域,实例变量的作用域的方式和其他面向对象的语言没有什么不同。对于方法,非常遗憾的是,Objective-C并没有为我们提供诸如public,private和protected这样的限定,这就意味着在Objective-C里面,从理论上来说所有的方法都是公有的。但是,我们可以利用Objective-C的语言的特性,我们自己来实现方法的私有化。当然我们自己的私有化手段没有得到任何的编译器的支持,只是告诉使用者:“这是一个私有的方法,请不要使用这个方法”。所以,无论作为类的设计者和使用者都应该清楚在Objective-C里面的方法私有化的所有手段,这样就在类的设计者和使用者之间达成了一种默契,这种方式明显不是Objective-C语法所硬性规定的,所以也可以把这种手法成为一种潜规则。
本章所述的方法的私有化是一种有缺陷的手段,有一定的风险而且也没有完全实现私有化,在后面的章节里面笔者会陆续的给出其他的实现方法私有化的方法。
另外,Objective-C里面有一个其他不支持指针的语言没有的一个动态特性,那就是程序在执行的时候,可以动态的替换类的手段。动态的方法替换有很多种应用,本章实现了一个类似java里面的final函数。和final函数不同的是,如果子类重写了这个方法,编译器不会报错,但是执行的时候总是执行的你的超类的方法。
类方法,方法私有化和动态方法替换将是本章的主题。
8.1,本章程序的执行结果
在本章里面,我们将要继续使用我们在第4章已经构筑好的类Cattle和Bull。
笔者在这里暂时违反一下不修改已经生效的代码规则改写了一下Cattle和Bull类,在里面追加了一些类方法,用于创建Cattle系列的对象。
笔者也改写了Cattle的头文件用来实现方法的私有化。
面向对象的程序有一个很大的特色就是动态性,但是由于某种原因我们在设计超类的时候,也许会考虑把某个方法设定成为静态的,这样就有了诸如final的概念。在本章我们将要使用动态的方法替换来实现这个功能。我们将要构筑一个新类,名字叫做UnknownBull,我们使用动态方法替换导致即使UnknownBull重载了Cattle类的saySomething,但是向UnknownBull发送saySomething的时候,仍然执行的是Cattle的saySomething。本章程序的执行结果请参照下图:
图8-1,本章程序的执行结果。
本章程序可以点击这里下载。
8.2,实现步骤
第一步,按照我们在第2章所述的方法,新建一个项目,项目的名字叫做07-InitWithAndIvarScope。如果你是第一次看本篇文章,请到这里参看第二章的内容。
第二步,按照我们在第4章的4.2节的第二,三,四步所述的方法,把在第4章已经使用过的“Cattle.h”,“Cattle.m”,“Bull.h”还有“Bull.m”, 导入本章的项目里面。
第三步,打开“Cattle.h”和“Cattle.m”,分别修改成为下面的代码并且保存:
@interface Cattle : NSObject {
int legsCount;
}
- ( void )saySomething;
+ (id) cattleWithLegsCountVersionA:( int ) count;
+ (id) cattleWithLegsCountVersionB:( int ) count;
+ (id) cattleWithLegsCountVersionC:( int ) count;
+ (id) cattleWithLegsCountVersionD:( int ) count;
@end
#import < objc / objc - class .h >
@implementation Cattle
- ( void ) saySomething
{
NSLog( @" Hello, I am a cattle, I have %d legs. " , legsCount);
}
- ( void ) setLegsCount:( int ) count
{
legsCount = count;
}
+ (id) cattleWithLegsCountVersionA:( int ) count
{
id ret = [[Cattle alloc] init];
// NEVER DO LIKE BELOW
// legsCount = count;
[ret setLegsCount:count];
return [ret autorelease];
}
+ (id) cattleWithLegsCountVersionB:( int ) count
{
id ret = [[[Cattle alloc] init] autorelease];
[ret setLegsCount:count];
return ret;
}
+ (id) cattleWithLegsCountVersionC:( int ) count
{
id ret = [[self alloc] init];
[ret setLegsCount:count];
return [ret autorelease];
}
+ (id) cattleWithLegsCountVersionD:( int ) count
{
id ret = [[self alloc] init];
[ret setLegsCount:count];
if ([self class ] == [Cattle class ])
return [ret autorelease];
SEL sayName = @selector(saySomething);
Method unknownSubClassSaySomething = class_getInstanceMethod([self class ], sayName);
// Change the subclass method is RUDE!
Method cattleSaySomething = class_getInstanceMethod([Cattle class ], sayName);
// method_imp is deprecated since 10.5
unknownSubClassSaySomething -> method_imp = cattleSaySomething -> method_imp;
return [ret autorelease];
}
@end
第四步,打开“Bull.h”和“Bull.m”,分别修改成为下面的代码并且保存:
#import " Cattle.h "
@interface Bull : Cattle {
NSString * skinColor;
}
- ( void )saySomething;
- (NSString * ) getSkinColor;
- ( void ) setSkinColor:(NSString * ) color;
+ (id) bullWithLegsCount:( int ) count bullSkinColor:(NSString * ) theColor;
@end
@implementation Bull
- ( void ) saySomething
{
NSLog( @" Hello, I am a %@ bull, I have %d legs. " , [self getSkinColor],legsCount);
}
- (NSString * ) getSkinColor
{
return skinColor;
}
- ( void ) setSkinColor:(NSString * ) color
{
skinColor = color;
}
+ (id) bullWithLegsCount:( int ) count bullSkinColor:(NSString * ) theColor
{
id ret = [self cattleWithLegsCountVersionC:count];
[ret setSkinColor:theColor];
// DO NOT USE autorelease here!
return ret;
}
@end
第五步,创建一个新类,名字叫做“UnknownBull”,然后分别打开“UnknownBull.h”和“UnknownBull.m”,分别修改成为下面的代码并且保存:
#import " Bull.h "
@interface UnknownBull : Bull {
}
- ( void )saySomething;
@end
@implementation UnknownBull
- ( void )saySomething
{
NSLog( @" Hello, I am an unknown bull. " );
}
@end
第六步,打开“08-Class_Method_And_Private_Method.m” ,修改成为下面的样子并且保存
#import " Cattle.h "
#import " Bull.h "
#import " UnknownBull.h "
int main ( int argc, const char * argv[]) {
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
id cattle[ 5 ];
cattle[ 0 ] = [Cattle cattleWithLegsCountVersionA: 4 ];
cattle[ 1 ] = [Bull cattleWithLegsCountVersionB: 4 ];
cattle[ 2 ] = [Bull cattleWithLegsCountVersionC: 4 ];
cattle[ 3 ] = [Bull bullWithLegsCount: 4 bullSkinColor: @" red " ];
cattle[ 4 ] = [UnknownBull cattleWithLegsCountVersionD: 4 ];
for ( int i = 0 ; i < 5 ; i ++ )
{
[cattle[i] saySomething];
}
[pool drain];
return 0 ;
}
第七步,选择屏幕上方菜单里面的“Run”,然后选择“Console”,打开了Console对话框之后,选择对话框上部中央的“Build and Go”,如果不出什么意外的话,那么应该出现入图8-1所示的结果。如果出现了什么意外导致错误的话,那么请仔细检查一下你的代码。如果经过仔细检查发现 还是不能执行的话,可以到这里下载笔者为同学们准备的代码。 如果笔者的代码还是不能执行的话,请告知笔者。
8.2,方法的私有化
在讲述方法私有化之前,我们首先要提到一个Objective-C里面的一个概念,动态类型和静态类型。
所谓的动态类型,就是使用id来定义一个对象,比如说
所谓的静态类型,就是使用已知变量的的类型来定义对象,比如说
动态类型和静态类型各有好处,动态类型实现了多态性,使用静态类型的时候编译器会为你检查一下也许会出现危险的地方,比如说向一个静态类型的对象发送一个它没有定义的消息等等。
好的,我们现在打开“cattle.h”,大家可以发现,和以前的版本相比,我们的“cattle.h”少了一个方法的定义,那就是-(void) setLegsCount:(int) count;。笔者在本章的范例程序里面实现私有方法的手段比较简单,直接把-(void) setLegsCount:(int) count从“cattle.h”给删除掉了。
大家打开““cattle.m”,可以看到里面-(void) setLegsCount:(int) count是有实现部分的。实现部分和过去的版本没有任何区别的。
我们本章里面讲述的实现方法私有化的手段,就是从头文件当中不写方法的声明。这样做会导致如下几个现象
1,在类的实现文件.m里面,你可以向平常一样使用[self setLegsCount:4] 来发送消息,但是确省设定的编译器会很不礼貌的给你一个警告。
2,你可以向Cattle以及从Cattle继承的类的静态对象发送setLegsCount:4的消息,但是同样,确省设定的编译器会很不礼貌的给你一个警告。
3,你可以向Cattle以及从Cattle继承的类的动态对象发送setLegsCount:4的消息,编译器不会向你发送任何警告的。
说到这里,同学们也许会觉得这一节的方法私有化有一点奇怪,因为在上面的第二条里面,不能阻止对对象的私有方法进行调用。令我们更为恼火的是,居然在我们自己的类的实现文件里面需要调用的时候产生诸如第一条的警告!
让我们冷静一下。
我们说,在面向对象的程序里面,一般而言类的使用者只关心接口,不关心实现的。当我们类的实现部分的某个方法,在头文件里面没有定义的话,那么由于我们的类的使用者只是看头文件,所以他不应该是用我们定义的所谓的私有方法的。这一点,对于其他的语言来说也是一样的,其他的语言的私有方法和变量,如果我们把它们改为public,或者我们不修改头文件,使用指针也可以强行的访问到私有的变量和方法的,从这个角度上来说,私有化的方法和变量也只不过是一个摆设而已,没有人可以阻止我们去访问他们,探求埋藏在里面的奥秘。所谓的私有化只不过是一个潜规则而已,在正常的时候,我们大家都会遵守这个潜规则的。但是被逼无奈走投无路的时候我们也许会除了访问私有的东西无可选择。但是也不能过分,我们显然不可以把访问私有变量和函数当作一种乐趣。
说到这里,我想大家应该可以理解这种私有化方法的定义了。它只不过是一种信号,告诉类的使用者,“这是一个私有的函数,请不要使用它,否则后果自负” 。我们在看到别人的代码的时候看到了这种写法的时候,或者别人看到我们的代码的时候,大家都需要做到相互理解对方的隐藏私有部分的意图。还是还是这句话,在大多数时候,请不要破坏潜规则。
8.3, 类方法
我们现在转到本章最重要的主题,类方法。我们将要首先关注一下类方法的声明,现在请同学们打开"Cattle.h"文件,可以发现下面的代码:
2 + (id) cattleWithLegsCountVersionB:( int ) count;
3 + (id) cattleWithLegsCountVersionC:( int ) count;
4 + (id) cattleWithLegsCountVersionD:( int ) count;
类方法和实例方法在声明上的唯一的区别就是,以加号+为开始,其余的部分是完全一致的。 笔者在这里定义了4个不同版本的类方法,从功能上来说都是用来返回Cattle类或者其子类的对象的,其中cattleWithLegsCountVersionA到C是我们这一节讲解的重点。
让我们首先打开“Cattle.m” ,关注一下下面的代码:
2 {
3 id ret = [[Cattle alloc] init];
4 // NEVER DO LIKE BELOW
5 // legsCount = count;
6 [ret setLegsCount:count];
7 return [ret autorelease];
8 }
9 + (id) cattleWithLegsCountVersionB:( int ) count
10 {
11 id ret = [[[Cattle alloc] init] autorelease];
12 [ret setLegsCount:count];
13 return ret;
14 }
我们需要使用类方法创建对象,所以在第3行,我们使用了我们比较熟悉的对象的创建的方法创建了一个对象。大家注意一下第5行,由于类方法是和对象是脱离的所以我们是无法在类方法里面使用实例变量的。第6行,由于我们创建了对象ret,所以我们可以向ret发送setLegsCount:这个消息,我们通过这个消息,设定了Cattle的legsCount实例变量。在第7行,我们遇到了一个新的朋友,autorelease。我们在类方法里面创建了一个对象,当我们返回了这个对象之后,类方法也随之结束,类方法结束就意味着在我们写的类方法里面,我们失去了对这个对象的参照,也就永远无法在类方法里面控制这个对象了。在Objective-C里面有一个规则,就是谁创建的对象,那么谁就有负责管理这个对象的责任,类方法结束之后,除非和类的使用者商量好了让类的使用者释放内存,否则我们无法直接的控制这个过程。
记忆力好的同学应该可以回忆起来,笔者曾经在第二章提到过一种延迟释放内存的技术,这个就是autorelease。关于autorelease以及其他的内存管理方法,我们将在下一章放到一起讲解。到这里大家记住,使用类方法的潜规则是你要使用类方法操作对象,当你需要使用类方法创建一个对象的时候,那么请在类方法里面加上autorelease。
我们来看看cattleWithLegsCountVersionB的实现部分的代码,和cattleWithLegsCountVersionA唯一区别就是我们在创建的时候就直接的加上了autorelease。这样符合创建对象的时候“一口气”的把所有需要的方法都写到一起的习惯,采取什么方式取决于个人喜好。
我们再打开“08-Class_Method_And_Private_Method.m”,参看下面的代码
2 cattle[ 1 ] = [Bull cattleWithLegsCountVersionB: 4 ];
我们在回头看看本章程序的执行结果,心细的同学也许发现了一个很严重的问题,我们在第2行代码里面想要返回一个Bull的对象,但是输出的时候却变成了Cattle,原因就是我们在cattleWithLegsCountVersionB里面创建对象的时候,使用了id ret = [[[Cattle alloc] init] autorelease]。由于Bull里面没有重写cattleWithLegsCountVersionB,所以除非我们重写cattleWithLegsCountVersionB否则我们向Bull发送cattleWithLegsCountVersionB这个类方法的时候,只能得到一个Cattle的对象。我们可以要求我们的子类的设计者在他们的子类当中重写cattleWithLegsCountVersionB,但是这样明显非常笨拙,失去了动态的特性。我们当然有办法解决这个问题,现在请大家回到“Cattle.m”,参照下列代码:
2 {
3 id ret = [[self alloc] init];
4 [ret setLegsCount:count];
5 return [ret autorelease];
6 }
我们的解决方案就在第3行,我们不是用静态的Cattle,而是使用self。说到这里也许大家有些糊涂了,在其他的语言当中和self比较类似的是this指针,但是在Objective-C里面self和this有些不大一样,在类函数里面的self实际上就是这个类本身。大家可以打开debugger观察一下,self的地址就是Bull的Class的地址。所以程序执行到上面的代码的第3行的时候,实际上就等同于id ret = [[[Bull class] alloc] init];
我们可以在类方法里面使用self,我们可否通过使用self->legsCount来访问实例变量呢?答案是不可以,因为在这个时候对象没有被创建也就是说,没有为legsCount分配内存,所以无法访问legsCount。
由于Bull类在程序被调入内存的时候就已经初始化好了,Bull类里面的实例函数应该被放到了代码段,所以从理论上来说,我们可以通过使用[self setLegsCount:count]来调用实例方法的,但是不幸的是Objective-C没有允许我们这样做,我们在类方法中使用self来作为消息的接收者的时候,消息总是被翻译成为类方法,如果发送实例方法的消息的话,会在执行的时候找不到从而产生异常。这样做是有一定的道理的,因为一般而言,实例方法里面难免要使用实例变量,在类方法当中允许使用实例方法,实际上也就允许使用实例变量。
请同学们再次回到图8-1,可以发现通过使用神奇的self,我们动态的创建了Bull类的对象。但是等一下,我们的程序并不完美,因为Bull类的skinColor并没有得到初始化,所以导致了null的出现。我们在设计Cattle类也就是Bull的超类的时候,明显我们无法预测到Bull类的特征。消除这种问题,我们可以在得到了Bull对象之后使用setSkinColor:来设定颜色,当然我们也可以直接写一个Bull类的方法,来封装这个操作,请同学们打开“Bull.h”:
我们追加了一个类方法, bullWithLegsCount:bullSkinColor:用于创建Bull对象,请同学们打开“Bull.m”:
2 {
3 id ret = [self cattleWithLegsCountVersionC:count];
4 [ret setSkinColor:theColor];
5 // DO NOT USE autorelease here!
6 return ret;
7 }
上面这一段代码相信大家都可以看明白,笔者就不在这里赘述了。但是笔者需要强调一点,在这里我们不需要调用autorelease的,因为我们没有在这里创建任何对象。
经过了这个改造,通过在“08-Class_Method_And_Private_Method.m”里面我们使用
使得我们的代码终于正常了,请参照图8-1的第4行输出。
8.4,使用动态方法替换实现final功能
首先请同学们打开“Cattle.m”,参照下面的代码片断:
{
id ret = [[self alloc] init];
[ret setLegsCount:count];
if ([self class ] == [Cattle class ])
return [ret autorelease];
SEL sayName = @selector(saySomething);
Method unknownSubClassSaySomething = class_getInstanceMethod([self class ], sayName);
// Change the subclass method is RUDE!
Method cattleSaySomething = class_getInstanceMethod([Cattle class ], sayName);
// method_imp is deprecated since 10.5
unknownSubClassSaySomething -> method_imp = cattleSaySomething -> method_imp;
return [ret autorelease];
}
@end
在cattleWithLegsCountVersionD里面,我们将要通过使用动态的方法替换技术来实现final方法。
第3,4行代码,是用于创建Cattle或者从Cattle类继承的对象,并且设定实例变量legsCount。
第6,7行代码,是用来判断调用这个类方法的self是不是cattle,如果是cattle的话,那么就直接返回,因为我们要在这个方法里面把子 类的saySomething替换成为Cattle的saySomething,如果类是Cattle的话,那么很明显,我们不需要做什么事情的。
第9行代码是老朋友了,我们需要得到方法的SEL。
第10行和第12行,我们需要通过Objective-C的一个底层函数,class_getInstanceMethod来取得方法的数据结构 Method。让我们把鼠标移动到Method关键字上面,点击鼠标右键盘,选择“Jump to definition”,我们可以看到在文件“objc-class.h”里面的Method的定义。Method实际上是类方法在Class里面的数据结构,系统会使用Method的信息来构筑Class的信息。在Method类型的声明里面,我们看到了下面的代码
struct objc_method {
SEL method_name;
char * method_types;
IMP method_imp;
};
其中SEL和IMP我们已经很熟悉了,method_types是方法的类型信息,Objective-C使用一些预定义的宏来表示方法的类型,然后把这些信息放到method_types里面。
需要强调的是,苹果在10.5之后就降级了很多Objective-C 底层的函数,并且在64位的应用当中使得这些函数失效,笔者对剥夺了众多程序员的自由而感到遗憾。
第14行的代码,我们把子类的函数指针的地址替换成为Cattle类的saySomething,这样无论子类是否重写saySomething, 执行的时候由于runtime需要找到方法的入口地址,但是这个地址总是被我们替换为Cattle的saySomething,所以子类通过 cattleWithLegsCountVersionD取得对象之后,总是调用的Cattle的saySomething,也就实现了final。当 然,这种方法有些粗鲁,我们强行的不顾后果的替换了子类的重写。
替换的结果,就是虽然我们在“08-Class_Method_And_Private_Method.m”里面的cattle[4]l里面使用UnknownBull是图返回UnknownBull对象,我们也确实得到了UnknownBull对象,但是不同的是,我们在cattleWithLegsCountVersionD里面狸猫换太子,把UnknownBull的saySomething变成了Cattle的saySomething。
让我们回到图8-1,我们发现最后一行的输出为Cattle的saySomething。
关于final的实现方式,我们当然可以使用一个文明的方法来告知子类的使用者,我们不想让某个方法被重写。我们只需要定义一个宏
类的使用者看到这个FINAL之后,笔者相信在绝大多数时候,他会很配合你不会重写带FINAL定义的方法的。
8.5,本章总结
我们在本章里面讲述了方法私有化,类方法的定义和使用,动态方法替换等技术手段,也给大家强调和澄清了self的概念。
更重要的是,笔者向大家介绍了一些潜规则,希望大家可以遵守。
非常感谢大家这些天对我的鼓励以及支持!
6,NSObject的奥秘
本系列讲座有着很强的前后相关性,如果你是第一次阅读本篇文章,为了更好的理解本章内容,笔者建议你最好从本系列讲座的第1章开始阅读,请点击这里。
在上一章里面,笔者向大家介绍了在Objective-C里面的几个非常重要的概念, 简单的说就是SEL,Class和IMP。我们知道Objective-C是C语言的扩展,有了这3个概念还有我们以前讲过的继承和封装的概念,Objective-C发生了翻天覆地的变化,既兼容C语言的高效特性又实现了面向对象的功能。
Objective-C从本质上来说,还是C语言的。那么内部究竟是怎样实现SEL,Class和IMP,还有封装和继承的?为了解答这个问题,笔者决定在本章向大家概要的介绍一下Objective-C的最主要的一个类,NSObject。
不过说实在话,如果同学们觉得本章的内容比较晦涩难懂的话,不阅读本章的内容丝毫不会对写程序产生任何不良的影响,但是如果掌握了本章的内容的话,对加深对Objective-C的理解,对于今后笔者将要讲述的内容而言,将会是一个极大的促进。
6.1,本章程序的执行结果
在本章里面,我们将要继续使用我们在前面几章已经构筑好的类Cattle和Bull。由于在现在的Xcode版本里面,把一些重要的东西比如说Class的原型定义都放到了LIB文件里面,所以这些东西的具体的定义,对于我们来说是不可见的。
我们首先把第4章的代码打开,然后打开“Cattle.h” 文件,把鼠标移动到“NSObject”上面,单击鼠标右键,在弹出菜单里面选择“Jump to Definition”。然后会弹出一个小菜单,我们选择“interface NSObject” 。我们可以看到如下代码
interface NSObject
我们知道了,所谓的NSObject里面只有一个变量,就是Class类型的isa。isa的英文的意思就是is a pointer的意思。也就是说NSObject里面只有一个实例变量isa。好的,我们需要知道Class是一个什么东西,我们把鼠标移动到“Class”上面,单击鼠标右键,在弹出菜单里面选择“Jump to Definition”,我们看到了如下的代码:
typedef struct objc_object {
Class isa;
} *id;
...
我们在这里知道了,Class实际上是一个objc_class的指针类型,我们把鼠标移动到“objc_class”上面,单击鼠标右键,在弹出菜单里面选择“Jump to Definition”,发现我们还是在这个窗口里面,Xcode并没有把我们带到objc_class的定义去,所以我们无从知道objc_class内部究竟是一个什么样的东西。
笔者顺便提一下,大家也许注意到了id的定义,id实际上是objc_object结构的一个指针,里面只有一个元素那就是Class。那么根据上面我们看到的,所谓的id就是objc_class的指针的指针。让我们回忆一下下面的代码:
id cattle
这句话是在初始化和实例话cattle对象,这个过程,实际上可以理解为,runtime为我们初始化好了Class的指针,并且把这个指针返回给我们。我们初始化对象完成了之后,实际上我们得到的对象就是一个指向这个对象的Class指针。
让我们在回过头来说说这个神秘的Class,我们无法在Xcode里面看到Class也就是objc_class的定义。庆幸的是这部分的定义是GCC代码,是开源的。笔者下载了开源的代码之后,把开源的代码作了一些小小的调整,然后把Class的定义等等放到了我们的工程文件里面去,通过类型转化之后,我们终于可以看到Class,SEL,还有isa等等过去对我们来说比较“神秘”的东西的真正面目。
我们在前面几章里面在每一个章的第一节里面都要介绍一下本章程序执行结果的屏幕拷贝,本章也是一样,但是本章的执行结果非常简单。因为对于本章而言重点应该是放在对NSObject机制的理解上。
图6-1,本章程序运行结果
大家看到本章程序的运行结果的屏幕拷贝的时候,也许会觉得很无趣,因为单单从结果画面,我们没有发现任何令人感到很有兴趣的东西,相反,都是同学们已经很熟悉的一些老面孔。但是本章所要讲述的东西也许是同学们在其他语言里面从来没有遇到过的东西,这些东西将会令人感到新鲜和激动。
6.2,实现步骤
第一步,按照我们在第2章所述的方法,新建一个项目,项目的名字叫做06-NSObject。如果你是第一次看本篇文章,请到这里参看第二章的内容。
第二步,按照我们在第4章的4.2节的第二,三,四步所述的方法,把在第4章已经使用过的“Cattle.h”,“Cattle.m”,“Bull.h”还有“Bull.m” 导入本章的项目里面。如果你没有第4章的代码,请到这里下载。如果你没有阅读第4章的内容,请参看这里。
第三步,把鼠标移动到项目浏览器上面的“Source”上面,然后在弹出的菜单上面选择“Add”,然后在子菜单里面选择“New File”,然后在新建文件对话框的左侧最下面选择“Other”,然后在右侧窗口选择“Empty File”,选择“Next”,在“New File”对话框里面的“File Name”栏内输入“MyNSObject.h”。然后输入(或者是拷贝也可以,因为这是C的代码,如果你很熟悉C语言的话,可以拷贝一下节省时间)如下代码:
#include < stddef.h >typedef const struct
objc_selector
const char * sel_types;
} * MySEL;
typedef struct my_objc_object {
struct my_objc_class * class_pointer;
} * myId;
typedef myId ( *
typedef
typedef struct my_objc_class * MetaClass;
typedef struct my_objc_class * MyClass;
struct
MetaClass class_pointer;
unsigned
struct objc_protocol_list * protocols;
void * gc_object_type;
};
typedef struct objc_protocol {
struct my_objc_class * class_pointer;
char * protocol_name;
struct objc_protocol_list * protocol_list;
struct
} Protocol;
typedef
typedef
typedef union arglist {
char arg_regs[ sizeof ( char * )];
} *
typedef
typedef struct objc_ivar_list {
int
const char *
} ivar_list[
} IvarList,
typedef struct
MySEL method_name;
MyIMP method_imp;
} Method,
typedef struct objc_method_list {
struct objc_method_list * method_next;
int
Method method_list[
} MethodList,
struct objc_protocol_list {
struct objc_protocol_list *
size_t count;
Protocol
};
第四步,打开06-NSObject.m文件,输入如下代码并且保存
#import " Cattle.h "
#import " Bull.h "
#import " MyNSObject.h "
int main ( int argc, const char * argv[]) {
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
id cattle = [Cattle new ];
id redBull = [Bull new ];
SEL setLegsCount_SEL = @selector(setLegsCount:);
IMP cattle_setLegsCount_IMP = [cattle methodForSelector:setLegsCount_SEL];
IMP redBull_setLegsCount_IMP = [redBull methodForSelector:setLegsCount_SEL];
[cattle setLegsCount: 4 ];
[redBull setLegsCount: 4 ];
[redBull setSkinColor: @" red " ];
Class cattle_class = cattle -> isa;
MyClass my_cattle_class = cattle -> isa;
SEL say = @selector(saySomething);
IMP cattle_sayFunc = [cattle methodForSelector:say];
cattle_sayFunc(cattle, say);
Class redBull_class = redBull -> isa;
MyClass my_redBull_class = redBull -> isa;
IMP redBull_sayFunc = [redBull methodForSelector:say];
redBull_sayFunc(redBull, say);
[pool drain];
return 0 ;
}
第五步,在06-NSObject.m文件的窗口的“[pool drain];”代码的左侧单击一下窗口的边框,确认一下是否出现一个蓝色的小棒棒,如果有的话那么断点被选择好了。如图6-2所示
图6-2,选择执行断点
第六步,选择Xcode上面的菜单的“Run”,然后选择“Debuger” ,在Debuger窗口里面选择“Build and Go”。
好的,大家就停在这里,不要做其他的操作,我们把程序中断在程序几乎执行到最后的断点上,我们将要通过Debuger来看看Objective-C内部究竟发生了什么样的奇妙的魔法。
注意
6.3,超类方法的调用
我们现在打开“06-NSObject.m”文件,发现下面的代码:
IMP cattle_setLegsCount_IMP = [cattle methodForSelector:setLegsCount_SEL];
IMP redBull_setLegsCount_IMP = [redBull methodForSelector:setLegsCount_SEL];
这一段代码,对同学们来说不是什么新鲜的内容了,我们在第5章里面已经讲过,这个是SEL和IMP的概念。我们在这里取得了cattle对象和redBull对象的setLegsCount:的函数指针。
如果大家现在已经不在Debuger里面的话,那么请选择Xcode菜单里面的,“Run”然后选择“Debuger” 。
我们注意到在Debuger里面,cattle_setLegsCount_IMP的地址和redBull_setLegsCount_IMP是完全一样的,如图6-3所示:
图6-3,cattle_setLegsCount_IMP和redBull_setLegsCount_IMP的地址。
cattle_setLegsCount_IMP和redBull_setLegsCount_IMP的地址完全一样,说明他们使用的是相同的代码段。这种结果是怎样产生的呢?大家请打开“MyNSObject.h”,参照下列代码:
MetaClass class_pointer;
struct my_objc_class * super_class;
const char * name;
long version;
unsigned long info;
long instance_size;
struct objc_ivar_list * ivars;
struct objc_method_list * methods;
struct sarray * dtable;
struct my_objc_class * subclass_list;
struct my_objc_class * sibling_class;
struct objc_protocol_list * protocols;
void * gc_object_type;
};
笔者在这里把开源代码的名字的定义加上了“my_”前缀,仅仅是为了区分一下。“MyNSObject.h”里面的代码问题很多,笔者从来没有也不会在实际的代码里面使用这段代码,使用这些代码的主要目的是为了向大家讲解概念,请大家忽略掉代码里面的种种问题。
我们注意到这里的methods变量,里面包存的就是类的方法名字(SEL)定义,方法的指针地址(IMP)。当我们执行
的时候,runtime会通过dtable这个数组,快速的查找到我们需要的函数指针,查找函数的定义如下:
objc_msg_lookup(id receiver, SEL op)
{
if (receiver)
return sarray_get(receiver -> class_pointer -> dtable, (sidx)op);
else
return nil_method;
好的,现在我们的cattle_setLegsCount_IMP没有问题了,那么redBull_setLegsCount_IMP怎么办?在Bull类里面我们并没有定义实例方法setLegsCount:,所以在Bull的Class里面,runtime难道找不到setLegsCount:么?答案是,是的runtime直接找不到,因为我们在Bull类里面根本就没有定义setLegsCount:。
但是,从结果上来看很明显runtime聪明的找到了setLegsCount:的地址,runtime是怎样找到的?答案就在:
struct
在自己的类里面没有找到的话,runtime会去Bull类的超类cattle里面去寻找,庆幸的是它成功的在cattle类里面runtime找到了setLegsCount:的执行地址入口,所以我们得到了redBull_setLegsCount_IMP。 redBull_setLegsCount_IMP和cattle_setLegsCount_IMP都是在Cattle类里面定义的,所以他们的代码的地址也是完全一样的。
我们现在假设,如果runtime在cattle里面也找不到setLegsCount:呢?没有关系,cattle里面也有超类的,那就是NSObject。所以runtime会去NSObject里面寻找。当然,NSObject不会神奇到可以预测我们要定义setLegsCount:所以runtime是找不到的。
在这个时候,runtime 并没有放弃最后的努力,再没有找到对应的方法的时候,runtime会向对象发送一个forwardInvocation:的消息,并且把原始的消息以及消息的参数打成一个NSInvocation的一个对象里面,作为forwardInvocation:的唯一的参数。 forwardInvocation:本身是在NSObject里面定义的,如果你需要重载这个函数的话,那么任何试图向你的类发送一个没有定义的消息的话,你都可以在forwardInvocation:里面捕捉到,并且把消息送到某一个安全的地方,从而避免了系统报错。
笔者没有在本章代码中重写forwardInvocation:,但是在重写forwardInvocation:的时候一定要注意避免消息的循环发送。比如说,同学们在A类对象的forwardInvocation里面,把A类不能响应的消息以及消息的参数发给B类的对象;同时在B类的forwardInvocation里面把B类不能响应的消息发给A类的时候,容易形成死循环。当然一个人写代码的时候不容易出现这个问题,当你在一个工作小组里面做的时候,如果你重写forwardInvocation:的时候,需要和小组的其他人达成共识,从而避免循环调用。
6.4,重载方法的调用
让我们继续关注“06-NSObject.m”文件,请大家参考一下下面的代码:
2 MyClass my_cattle_class = cattle -> isa;
3 SEL say = @selector(saySomething);
4 IMP cattle_sayFunc = [cattle methodForSelector:say];
5 cattle_sayFunc(cattle, say);
6
7 Class redBull_class = redBull -> isa;
8 MyClass my_redBull_class = redBull -> isa;
9
10 IMP redBull_sayFunc = [redBull methodForSelector:say];
11 redBull_sayFunc(redBull, say);
本节的内容和6.3节的内容比较类似,关于代码部分笔者认为就不需要解释了,如果同学们有所不熟悉的话,可以参考一下第5章的内容。
在我们的Cattle类和Bull类里面,都有saySometing这个实例方法。我们知道只要方法的定义相同,那么它们的SEL是完全一样的。我们根据一个SEL say,在cattle和redBull对象里面找到了他们的函数指针。根据6.3节的讲述,我们知道当runtime接收到寻找方法的时候,会首先在这个类里面寻找,寻找到了之后寻找的过程也就结束了,同时把这个方法的IMP返回给我们。所以,在上面的代码里面的cattle_sayFunc和redBull_sayFunc应该是不一样的,如图6-4所示:
图6-4, cattle_sayFunc和redBull_sayFunc的地址
6.5,超类和子类中的Class
在类进行内存分配的时候,对于一个类而言,runtime需要找到这个类的超类,然后把超类的Class的指针的地址赋值给isa里面的super_class。所以,我们的cattle里面的Class应该和redBull里面的Class里面的super_class应该是完全相同的,请参照图6-5:
图6-5, cattle里面的Class和redBull里面的Class里面的super_class
6.6,实例变量的内存分配的位置
我们先来回忆一下对象是怎样被创建的。创建对象的时候,类的内容需要被调入到内存当中我们称之为内存分配(Allocation),然后需要把实体变量进行初始化(Initialization),当这些步骤都结束了之后,我们的类就被实例化了,我们把实例化完成的类叫做对象(Object)。
对于内存分配的过程,runtime需要知道分配多少内存还有各个实例变量的位置。我们回到“MyNSObject.h”,参照如下代码:
2 typedef struct objc_ivar_list {
3 int ivar_count;
4 struct objc_ivar {
5 const char * ivar_name;
6 const char * ivar_type;
7 int ivar_offset;
8 } ivar_list[ 1 ];
9 } IvarList, * IvarList_t;
我们仔细看看第5行的ivar_name,顾名思义这个是实例变量的名字,第6行的ivar_type是实例变量的类型,第7行的ivar_offset,这个就是位置的定义。runtime从类的isa里面取得了这些信息之后就知道了如何去分配内存。我们来看看图6-6:
图6-6,实例变量在内存中的位置
在cattle里面,我们看到了第一个实例变量是isa,第二个就是我们定义的legsCount。其中isa是超类的变量,legsCount是Cattle类的变量。我们可以看出来,总是把超类的变量放在前头,然后是子类的变量。
那么对于redBull而言是什么样子呢?我们来看看图6-7
图6-7,redBull里面的实例变量的位置
我们通过图6-7可以发现redBull的Class里面的skinColor的位置偏移是8,很明显,runtime为isa和legsCount预留了2个位置。
6.7本章总结
非常感谢大家!
在本章里面,笔者通过一个小小的“把戏”为同学们揭开了NSObject的神秘的面纱。本章的内容,虽然对理解Objective-C不是必需的,但是对以后的章节的内容的理解会有一个非常好的辅助作用,希望同学们花费一点点心思和时间阅读一下。
另外,笔者需要再次强调一下,由于笔者没有得到官方的正式的文档说明,所以不能保证本章的内容是完整而且准确的,本章内容仅供大家参考和娱乐,希望大家谅解。
5,Class类型,选择器Selector以及指针函数
本系列讲座有着很强的前后相关性,如果你是第一次阅读本篇文章,为了更好的理解本章内容,笔者建议你最好从本系列讲座的第1章开始阅读,请点击这里。
上一章笔者介绍了在Objective-C里面继承的概念。有了继承的知识我们可以重复的使用很多以前生效的代码,这样就大大的提高了代码开发的效率。在本章,笔者要向同学们介绍几个非常重要的概念,Class类型, 选择器Selector以及指针函数。
我们在实际上的编程过程中,也许会遇到这样的场景,那就是我们在写程序的时候不能确切的知道我们需要使用什么类,使用这个类的什么方法。在这个时候,我们需要在我们的程序里面动态的根据用户的输入来创建我们在写程序不知道的类的对象,并且调用这个对象的实例方法。Objective-C为我们提供了Class类型, 选择器Selector以及指针函数来实现这样的需求,从而大大的提高了我们程序的动态性能。
在Objective-C里面,一个类被正确的编译过后,在这个编译成功的类里面,存在一个变量用于保存这个类的信息。我们可以通过一个普通的字符串取得这个Class,也可以通过我们生成的对象取得这个Class。Class被成功取得之后,我们可以把这个Class当作一个已经定义好的类来使用它。
Selector和Class比较类似,不同的地方是Selector用于表示方法。 在Objective-C的程序进行编译的时候,会根据方法的名字(包括参数列表)确定一个唯一的身份证明(实际上就是一个整数),不用的类里面的相同名字相同声明的方法的身份证明是一样的。这样在程序执行的时候,runtime就不用费力的进行方法的名字比较来确定是执行哪一个方法了,只是通过一个整数的寻找就可以马上定位到相应的方法,然后找到相应的方法的入口地址,这样方法就可以被执行了。
笔者在前面的章节里面叙述过,在Objective-C里面消息也就是方法的执行比C语言的直接找到函数入口地址执行的方式,从效率上来讲是比较低下的。尽管Objective-C使用了Selector等招数来提高寻找效率,但是无论如何寻找的过程,都是要消耗一定的时间的。好在Objective-C是完全兼容C的,它也有指针函数的概念。当我们需要执行效率的时候,比如说在一个很大的循环当中需要执行某个功能的时候,我们可以放弃向对某一个对象发送消息的手段,用指针函数取而代之,这样就可以获得和C语言一样的执行效率了。
说到这里,可能有的同学已经有些茫然了。这些概念有些令人难以理解,但是它们确实是Objective-C的核心的功能。掌握了这些核心的功能之后,同学们可以很轻松的看懂苹果的SDK里面的很多东西含义,甚至可以自己动手写一些苹果没有为我们提供的功能。所以建议大家仔细研读本章的内容,如果有什么问题,可以发个帖子大家可以共同探讨。
从笔者的观点上来看,对于有Java或者C++或者其他面向对象的语言的经验的同学来说,前面的从第1到第4章的内容也许有些平淡无奇。从第5章开始,我们将要逐渐的深入到Objective-C的核心部分。笔者的最终目的,虽然是向大家介绍iPhone开发的入门,但是笔者认为了解了Objective-C的基本概念以及使用方法之后,熟悉iPhone的应用程序的开发将是一件水到渠成的轻松的事情。否则如果你直接就深入到iPhone的开发的话,在绝大多数时间你也许因为一个小小的问题就会困扰你几个小时甚至几天,解决这些问题的唯一方法就是熟悉Objective-C和Cocoa Foundation的特性。
好了,说了很多我们从下面就要开始,我们的手法和前面几章是一样的,我们首先要介绍一下本章程序的执行结果。
5.1,本章程序的执行结果
图5-1,第5章程序的执行结果
在本章里面,我们将要继续使用我们在前面几章已经构筑好的类Cattle和Bull。为了灵活的使用Cattle和Bull,我们将要构筑一个新的类,DoProxy。在DoProxy里面,我们将会引入几个我们的新朋友,他们分别是BOOL,SEL,IMP,CLASS。通过这些新的朋友我们可以动态的通过设定文件取得Cattle和Bull的类,还有方法以及方法指针。下面将要介绍如何构筑本章程序。同学们可以按照本章所述的步骤来构筑,也可以通过从这里下载。不过为了熟悉代码的写作,笔者强烈建议大家按照笔者所述的步骤来操作。
5.2,实现步骤
第一步,按照我们在第2章所述的方法,新建一个项目,项目的名字叫做05-Hello Selector。如果你是第一次看本篇文章,请到这里参看第二章的内容。
第二步,按照我们在第4章的4.2节的第二,三,四步所述的方法,把在第4章已经使用过的“Cattle.h”,“Cattle.m”,“Bull.h”还有“Bull.m” 导入本章的项目里面。如果你没有第4章的代码,请到这里下载。如果你没有阅读第4章的内容,请参看这里。
第三步,把鼠标移动到项目浏览器上面的“Source”上面,然后在弹出的菜单上面选择“Add”,然后在子菜单里面选择“New Files”,然后在新建文件对话框的左侧选择“Cocoa Touch Classes”,然后在右侧窗口选择“NSObject subclass”,选择“Next”,在“New File”对话框里面的“File Name”栏内输入“DoProxy.m”。在这里笔者没有给出图例,在这里新建文件的步骤和第3章的第二步到第四步相同,只是文件名字不一样。第一次看到本篇 文章的同学可以参照第3章。
第四步,打开“DoProxy.h”做出如下修改并且保存
#define SET_SKIN_COLOR @"setSkinColor:"
#define BULL_CLASS @"Bull"
#define CATTLE_CLASS @"Cattle"
@interface DoProxy : NSObject {
BOOL notFirstRun;
id cattle[ 3 ];
SEL say;
SEL skin;
void ( * setSkinColor_Func) (id, SEL, NSString * );
IMP say_Func;
Class bullClass;
}
- ( void ) doWithCattleId:(id) aCattle colorParam:(NSString * ) color;
- ( void ) setAllIVars;
- ( void ) SELFuncs;
- ( void ) functionPointers;
@end
第五步,打开“DoProxy.m”做出如下修改并且保存
#import " Cattle.h "
#import " Bull.h "
@implementation DoProxy
- ( void ) setAllIVars
{
cattle[ 0 ] = [Cattle new ];
bullClass = NSClassFromString(BULL_CLASS);
cattle[ 1 ] = [bullClass new ];
cattle[ 2 ] = [bullClass new ];
say = @selector(saySomething);
skin = NSSelectorFromString(SET_SKIN_COLOR);
}
- ( void ) SELFuncs
{
[self doWithCattleId:cattle[ 0 ] colorParam: @" brown " ];
[self doWithCattleId:cattle[ 1 ] colorParam: @" red " ];
[self doWithCattleId:cattle[ 2 ] colorParam: @" black " ];
[self doWithCattleId:self colorParam: @" haha " ];
}
- ( void ) functionPointers
{
setSkinColor_Func = ( void ( * )(id, SEL, NSString * )) [cattle[ 1 ] methodForSelector:skin];
// IMP setSkinColor_Func = [cattle[1] methodForSelector:skin];
say_Func = [cattle[ 1 ] methodForSelector:say];
setSkinColor_Func(cattle[ 1 ],skin, @" verbose " );
NSLog( @" Running as a function pointer will be more efficiency! " );
say_Func(cattle[ 1 ],say);
}
- ( void ) doWithCattleId:(id) aCattle colorParam:(NSString * ) color
{
if (notFirstRun == NO)
{
NSString * myName = NSStringFromSelector(_cmd);
NSLog( @" Running in the method of %@ " , myName);
notFirstRun = YES;
}
NSString * cattleParamClassName = [aCattle className];
if ([cattleParamClassName isEqualToString:BULL_CLASS] ||
[cattleParamClassName isEqualToString:CATTLE_CLASS])
{
[aCattle setLegsCount: 4 ];
if ([aCattle respondsToSelector:skin])
{
[aCattle performSelector:skin withObject:color];
}
else
{
NSLog( @" Hi, I am a %@, have not setSkinColor! " , cattleParamClassName);
}
[aCattle performSelector:say];
}
else
{
NSString * yourClassName = [aCattle className];
NSLog( @" Hi, you are a %@, but I like cattle or bull! " , yourClassName);
}
}
@end
第六步,打开“05-Hello Selector.m” 作出如下修改并且保存
#import " DoProxy.h "
int main ( int argc, const char * argv[]) {
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
DoProxy * doProxy = [DoProxy new ];
[doProxy setAllIVars];
[doProxy SELFuncs];
[doProxy functionPointers];
[pool drain];
return 0 ;
}
第七步,选择屏幕上方菜单里面的“Run”,然后选择“Console”,打开了Console对话框之后,选择对话框上部中央的“Build and Go”,如果不出什么意外的话,那么应该出现入图5-1所示的结果。如果出现了什么意外导致错误的话,那么请仔细检查一下你的代码。如果经过仔细检查发现 还是不能执行的话,可以到这里下载笔者为同学们准备的代码。 如果笔者的代码还是不能执行的话,请告知笔者。
5.3,BOOL类型
我们现在打开“DoProxy.h”文件。“DoProxy.h”文件的第3行到第5行是三个预定义的三个字符串的宏。我们将在程序当中使用这3个宏,为了实现代码的独立性,在实际的程序开发当中,我们也许考虑使用一个配置的文本文件或者一个XML来替代这些宏。但是现在由于笔者的主要目的是讲解Objective-C的概念,为了避免较多的代码给大家带来理解主题的困难,所以笔者没有使用配置文件或者XML来表述这些可以设定的常量。
“DoProxy.h”的第7行对同学们来说也是老朋友了,是通知编译器,我们需要声明一个DoProxy类,从NSObject继承。
我们在第8行遇到了我们的一个新的朋友,BOOL:
BOOL notFirstRun;
我们定义了一个notFirstRun的实例变量,这个变量是布尔类型的。我们的实例方法doWithCattleId需要被执行多次,我们在第一次执行doWithCattleId的时候需要向控制输出包含doWithCattleId的方法名字的字符串,关于这个字符串的内容,请参考图5-1。
好的,我们现在需要看看在Objective-C里面BOOL是怎样定义的,我们把鼠标移动到BOOL上面,然后单击鼠标右键选择弹出菜单的“Jump to Definition”,然后Xcode会打开objc.h文件,我们看到下面的代码:
// BOOL is explicitly signed so @encode(BOOL) == "c" rather than "C"
// even if -funsigned-char is used.
#define OBJC_BOOL_DEFINED
#define YES (BOOL)1
#define NO (BOOL)0
我们看到这段代码,我们可以这样理解,在Objective-C里面,BOOL其实是signed char,YES是1,NO是0。我们可以这样给BOOL赋值:
BOOL y = NO;
关于BOOL,实际上就是一个开关的变量,但是我们需要注意下面2点:
第一点,从本质上来说BOOL是一个8bit的一个char,所以我们在把其他比如说short或者int转换成为BOOL的时候一定要注意。如果short或者int的最低的8位bit都是0的话,尽管除了最低的8位以外都不是0,那么经过转换之后,就变成了0也就是NO。比如说我们有一个int的值是0X1000,经过BOOL转换之后就变成了NO。
第二点,Objective-C里面的所有的逻辑判断例如if语句等等和C语言保持兼容,如果数值不是0判断为真,如果数值是0那么就判断为假,并不是说定义了BOOL值之后就变成了只有1或者YES为真。所以下面的代码的判断都为真:
if ( 2 )
if ( - 1 )
5.4,SEL类型
让我们接着看“DoProxy.h”文件的下列代码:
2 SEL say;
3 SEL skin;
其中id cattle[3]定义了一个数组用于存储Cattle或者Bull对象。这一行代码估计大家都很熟悉,笔者就不赘述了。像这样的传统的数组并不能完全满足我们的需求,当我们需要做诸如追加,删除等操作的时候,会很不方便。在随后的章节里面笔者将要向大家介绍传统数组的替代解决方案NSArray。
上一段代码的第二行和第三行是本节所关注的,就是SEL类型。Objective-C在编译的时候,会根据方法的名字(包括参数序列),生成一个用 来区分这个方法的唯一的一个ID,这个ID就是SEL类型的。我们需要注意的是,只要方法的名字(包括参数序列)相同,那么它们的ID都是相同的。就是 说,不管是超类还是子类,不管是有没有超类和子类的关系,只要名字相同那么ID就是一样的。除了函数名字和ID,编译器当然还要把方法编译成为机器可以执 行的代码,这样,在一个编译好的类里面,就产生了如下图所示方法的表格示意图(本构造属于笔者推测,没有得到官方证实,所以图5-2为示意图仅供参考,我们可以暂时认为是这样的)。
图5-2,方法的表格示意图
请注意setSkinColor后面有一个冒号,因为它是带参数的。由于存在这样的一个表格,所以在程序执行的时候,我们可以方便的通过方法的名字,获取到方法的ID也就是我们所说的SEL,反之亦然。具体的使用方法如下:
2 SEL 变量名 = NSSelectorFromString(方法名字的字符串);
3 NSString * 变量名 = NSStringFromSelector(SEL参数);
其中第1行是直接在程序里面写上方法的名字,第2行是写上方法名字的字符串,第3行是通过SEL变量获得方法的名字。我们得到了SEL变量之后,可以通过下面的调用来给一个对象发送消息:
对象 performSelector:SEL变量 withObject:参数1 withObject:参数2]
这样的机制大大的增加了我们的程序的灵活性,我们可以通过给一个方法传递SEL参数,让这个方法动态的执行某一个方法;我们也可以通过配置文件指定需要执行的方法,程序读取配置文件之后把方法的字符串翻译成为SEL变量然后给相应的对象发送这个消息。
从效率的角度上来说,执行的时候不是通过方法名字而是方法ID也就是一个整数来查找方法,由于整数的查找和匹配比字符串要快得多,所以这样可以在某种程度上提高执行的效率。
5.5,函数指针
在讲解函数指针之前,我们先参看一下图5-2,函数指针的数值实际上就是图5-2里面的地址,有人把这个地址成为函数的入口地址。在图5-2里面我们可以通过方法名字取得方法的ID,同样我们也可以通过方法ID也就是SEL取得函数指针,从而在程序里面直接获得方法的执行地址。或者函数指针的方法有2种,第一种是传统的C语言方式,请参看“DoProxy.h” 的下列代码片断:
2 IMP say_Func;
其中第1行我们定义了一个C语言里面的函数指针,关于C语言里面的函数指针的定义以及使用方法,请参考C语言的书籍和参考资料。在第一行当中,值得我们注意的是这个函数指针的参数序列:
第一个参数是id类型的,就是消息的接受对象,在执行的时候这个id实际上就是self,因为我们将要向某个对象发送消息。
第二个参数是SEL,也是方法的ID。有的时候在消息发送的时候,我们需要使用用_cmd来获取方法自己的SEL,也就是说,方法的定义体里面,我们可以通过访问_cmd得到这个方法自己的SEL。
第三个参数是NSString*类型的,我们用它来传递skin color。在Objective-C的函数指针里面,只有第一个id和第二个SEL是必需的,后面的参数有还是没有,如果有那么有多少个要取决于方法的声明。
现在我们来介绍一下Objective-C里面取得函数指针的新的定义方法,IMP。
上面的代码的第一行比较复杂,令人难以理解,Objective-C为我们定义了一个新的数据类型就是在上面第二行代码里面出现的IMP。我们把鼠标移动到IMP上,单击右键之后就可以看到IMP的定义,IMP的定义如下:
typedef id
这个格式正好和我们在第一行代码里面的函数指针的定义是一样的。
我们取得了函数指针之后,也就意味着我们取得了执行的时候的这段方法的代码的入口,这样我们就可以像普通的C语言函数调用一样使用这个函数指针。当然我们可以把函数指针作为参数传递到其他的方法,或者实例变量里面,从而获得极大的动态性。我们获得了动态性,但是付出的代价就是编译器不知道我们要执行哪一个方法所以在编译的时候不会替我们找出错误,我们只有执行的时候才知道,我们写的函数指针是否是正确的。所以,在使用函数指针的时候要非常准确地把握能够出现的所有可能,并且做出预防。尤其是当你在写一个供他人调用的接口API的时候,这一点非常重要。
5.6,Class类型
到目前为止,我们已经知道了对应于方法的SEL数据类型,和SEL同样在Objective-C里面我们不仅仅可以使用对应于方法的SEL,对于类在Objective-C也为我们准备了类似的机制,Class类型。当一个类被正确的编译过后,在这个编译成功的类里面,存在一个变量用于保存这个类的信息。我们可以通过一个普通的字符串取得 这个Class,也可以通过我们生成的对象取得这个Class。Class被成功取得之后,我们可以把这个Class当作一个已经定义好的类来使用它。这样的机制允许我们在程序执行的过程当中,可以Class来得到对象的类,也可以在程序执行的阶段动态的生成一个在编译阶段无法确定的一个对象。
因为Class里面保存了一个类的所有信息,当然,我们也可以取得一个类的超类。关于Class类型,具体的使用格式如下:
2 Class 变量名 = [类或者对象 superclass];
3 Class 变量名 = NSClassFromString(方法名字的字符串);
4 NSString * 变量名 = NSStringFromClass(Class参数);
第一行代码,是通过向一个类或者对象发送class消息来获得这个类或者对象的Class变量。
第二行代码,是通过向一个类或者对象发送superclass消息来获得这个类或者对象的超类的Class变量。
第三行代码,是通过调用NSClassFromString函数,并且把一个字符串作为参数来取得Class变量。这个在我们使用配置文件决定执行的时候的类的时候,NSClassFromString给我们带来了极大的方便。
第四行代码,是NSClassFromString的反向函数NSStringFromClass,通过一个Class类型作为变量取得一个类的名字。
当我们在程序里面通过使用上面的第一,二或者第三行代码成功的取得一个Class类型的变量,比如说我们把这个变量名字命名为myClass,那么我们在以后的代码种可以把myClass当作一个我们已经定义好的类来使用,当然我们可以把这个变量作为参数传递到其他的方法当中让其他的方法动态的生成我们需要的对象。
5.7,DoProxy.h里面的方法定义
DoProxy.h里面还有一些实例方法,关于方法的定义的格式,同学们可以参照第三章。我们现在要对DoProxy.h里面定义的方法的做一下简要的说明。
2 - ( void ) setAllIVars;
3 - ( void ) SELFuncs;
4 - ( void ) functionPointers;
第一行的方法,是设定aCattle,也就是Cattle或者Bull对象的属性,然后调用saySomething方法,实现控制台的打印输出。
第二行的方法,是把我们定义的DoProxy类里面的一些变量进行赋值。
第三行的方法,是调用doWithCattleId方法。
第四行的方法,是调用了函数指针的方法。
好的,我们把DoProxy.h的内容介绍完了,让我们打开DoProxy.m。
5.8,DoProxy.m的代码说明
有了DoProxy.h的说明,同学们理解DoProxy.m将是一件非常轻松的事情,让我们坚持一下把这个轻松的事情搞定。由于篇幅所限,笔者在这里的讲解将会省略掉非本章的内容。
DoProxy.m代码如下:
2 #import " Cattle.h "
3 #import " Bull.h "
4
5 @implementation DoProxy
6 - ( void ) setAllIVars
7 {
8 cattle[ 0 ] = [Cattle new ];
9
10 bullClass = NSClassFromString(BULL_CLASS);
11 cattle[ 1 ] = [bullClass new ];
12 cattle[ 2 ] = [bullClass new ];
13
14 say = @selector(saySomething);
15 skin = NSSelectorFromString(SET_SKIN_COLOR);
16 }
17 - ( void ) SELFuncs
18 {
19 [self doWithCattleId:cattle[ 0 ] colorParam: @" brown " ];
20 [self doWithCattleId:cattle[ 1 ] colorParam: @" red " ];
21 [self doWithCattleId:cattle[ 2 ] colorParam: @" black " ];
22 [self doWithCattleId:self colorParam: @" haha " ];
23 }
24 - ( void ) functionPointers
25 {
26 setSkinColor_Func = ( void ( * )(id, SEL, NSString * )) [cattle[ 1 ] methodForSelector:skin];
27 // IMP setSkinColor_Func = [cattle[1] methodForSelector:skin];
28 say_Func = [cattle[ 1 ] methodForSelector:say];
29 setSkinColor_Func(cattle[ 1 ],skin, @" verbose " );
30 NSLog( @" Running as a function pointer will be more efficiency! " );
31 say_Func(cattle[ 1 ],say);
32 }
33 - ( void ) doWithCattleId:(id) aCattle colorParam:(NSString * ) color
34 {
35 if (notFirstRun == NO)
36 {
37 NSString * myName = NSStringFromSelector(_cmd);
38 NSLog( @" Running in the method of %@ " , myName);
39 notFirstRun = YES;
40 }
41
42 NSString * cattleParamClassName = [aCattle className];
43 if ([cattleParamClassName isEqualToString:BULL_CLASS] ||
44 [cattleParamClassName isEqualToString:CATTLE_CLASS])
45 {
46 [aCattle setLegsCount: 4 ];
47 if ([aCattle respondsToSelector:skin])
48 {
49 [aCattle performSelector:skin withObject:color];
50 }
51 else
52 {
53 NSLog( @" Hi, I am a %@, have not setSkinColor! " , cattleParamClassName);
54 }
55 [aCattle performSelector:say];
56 }
57 else
58 {
59 NSString * yourClassName = [aCattle className];
60 NSLog( @" Hi, you are a %@, but I like cattle or bull! " , yourClassName);
61 }
62 }
63 @end
第10行代码是通过一个预定义的宏BULL_CLASS取得Bull的Class变量。
第11和12行代码是使用bullClass来初始化我们的cattle实例变量数组的第2和第3个元素。
第14行是通过@selector函数来取得saySomething的SEL变量。
第15行是通过向NSSelectorFromString传递预定义的宏SET_SKIN_COLOR来取得setSkinColor的SEL变量。
第22行,笔者打算“戏弄”一下doWithCattleId,向传递了不合适的参数。
第26行,笔者取得了传统的C语言的函数指针,也是使用了第5.5节所述的第一种取得的方法。
第28行,笔者通过5.5节所述的第二种取得的方法得到了函数指针say_Func。
第29行和31行分别执行了分别在第26行和28行取得的函数指针。
第35行是一个BOOL型的实例变量notFirstRun 。当对象被初始化之后,确省的值是NO。第一次执行完毕之后,我们把这个变量设定成为YES,这样就保证了花括号里面的代码只被执行一次。
第37行我们通过_cmd取得了doWithCattleId这个方法名字用于输出。当然同学们在设计方法的提供给别人使用的时候,为了防止使用方法的人把这个方法本身传递进来造成死循环,需要使用_cmd这个系统隐藏的变量判断一下。笔者在这里没有做出判断,这样写从理论上来说存在一定的风险。
第42行,我们通过向对象发送className消息来取得这个对象的类的名字。
第43行和第44行,我们通过NSString的方法isEqualToString来判断取得的类的名字是否在我们事先想象的范围之内,我们只希望接受Bull或者Cattle类的对象。
第46行,本来我们想通过SEL的方式来进行这个牛股的设定,但是由于它的参数不是从NSObject继承下来的,所以我们无法使用。我们会有办法解决这个问题的,我们将在后面的章节里面介绍解决这个问题的方法。
第47行的代码,有一个非常重要NSObject的方法respondsToSelector,通过向对象发送这个消息,加上一个SEL,我们可以知道这个对象是否可以相应这个SEL消息。由于我们的Cattle无法相应setSkinColor消息,所以如果对象是Cattle类生成的话,if语句就是NO所以花括号里面的内容不会得到执行。
第59行,我们通过类的名字发现了一个假冒的Cattle,我们把这个假冒的家伙给揪出来,然后实现了屏幕打印。
5.9,本章总结
本章给同学们介绍了几个新的数据类型,以及使用方法,这些数据类型分别是BOOL,SEL,Class,IMP。
本章的内容很重要,希望同学们花一点时间仔细的理解一下。应该说,本章的内容有些令人难以理解,或者说知道了SEL,Class,IMP之后也许不知道如何使用,遇到什么情况的时候需要使用。不过在学习Objective-C的初级阶段,不知道这些也没有关系,但是SEL,Class,IMP的概念需要掌握,否则当你遇到别人写的质量比较高的代码或者苹果官方的技术文档的时候,你会觉得理解起来比较吃力。
本章内容也是理解下一章内容的基础,下一章我们将要讲述NSObject的奥秘。
谢谢大家!