假如我们对面向对象的思维已经C语言都很熟悉的话,对于我们学习Objective-C将会非常有用。假如我们对C语言还不熟悉的话,那我们需要学习一下C语言。

AD:2014WOT全球软件技术峰会北京站 课程视频发布

2010年11月编程语言排行榜和2月编程语言排行榜讲的都是Objective-C。Objective-C是Mac软件开发领域最主要的开发语言,假如我们对面向对象的思维已经C语言都很熟悉的话,对于我们学习Objective-C将会非常有用。假如我们对C语言还不熟悉的话,那我们需要学习一下C语言。

方法调用(Calling Methods)

为了能够尽快上手,我们先来看一些简单的例子。Objective-C语法里面基本的方法调用是这样的:

     
     
     
     
  1. [object method];  
  2.  
  3. [object methodWithInput:input];  
  4.  

对象的方法可以返回值:

     
     
     
     
  1. output = [object methodWithOutput];  
  2.  
  3. output = [object methodWithInputAndOutput:input];  
  4.  

我们也可以在类里面调用如何创建对象的方法。下面的这个例子里面,我们调用了NSString类的string方法:

     
     
     
     
  1. id myObject = [NSString string];  
  2.  

id的类型意味着myObject这个变量可以指向任意类型的变量。当我们编译这个应用程序的时候,并不知道他实现的真实的类和方法。

在这个例子里面,很明显这个对象的类型应该是NSString,所以我们可以改一下他的类型:

     
     
     
     
  1. NSString* myString = [NSString string];  
  2.  

现在myString就是一个NSString类型的变量。这个时候假如我们试图使用一个NSString没有实现的方法时,编译器就会警告我们。

一定要注意在对象类型的右边有一个星号。所有的Objective-C对象变量都是指针类型的。id类型已经预先被定义成一个指针类型了。所以我们不需要再加星号。

嵌套消息调用(Nested Messages)

在许多编程语言里面嵌套消息,或者嵌套函数看起来就像这样:

     
     
     
     
  1. function1 ( function2() );  

function2的返回值被传递给function1当输入参数。在Objective-C里面,嵌套消息调用就像这样:

     
     
     
     
  1. [NSString stringWithFormat:[prefs format]];  

我们应该尽量避免在一行代码里面嵌套调用超过两个。因为这样的话,代码的可读性就不太好。

多参输入的方法(Multi-Input Methods)

多个输入参数的方法。在Objective-C里面,一个方法名可以被分割成几段。在头文件里面,就应该这样子来定义一个多输入参数的方法:

     
     
     
     
  1. -(BOOL)writeToFile:(NSString *)path atomically:(BOOL)useAuxiliaryFile;  
  2.  

我们这样来调用它:

     
     
     
     
  1. BOOL result = [myData writeToFile:@"/tmp/log.txt" atomically:NO];  
  2.  

参数不一定要给它命名。在运行期系统里面这个方法真实的名字叫writeToFile:atomically:。

     
     
     
     
  1. Accessors(Getter & Setter)  
  2.  

在Objective-C里面所有的实例对象默认都是私有的。所有在大多数情况下我们需要用accessors去读取或者设置变量的值。有两个语法都支持这样的操作,这个时传统的老的语法:

     
     
     
     
  1. [photo setCaption:@"Day at the Beach"];  
  2. output = [photo caption];  
  3.  

第二行的代码其实并非直接去读对象实例的变量。事实上它调用的是名叫caption的方法。在Objective-C里大多数情况下我们不需要给getters加get的前缀。

无论什么时候我们见到方括号,其实我们都是向一个对象或者一个类发送了一个消息。

Dot Syntax

在Objective-C 2.0里面,新增加了一个"."操作的语法。在Mac OS X 10.5里面就使用了Objective-C 2.0语法:

     
     
     
     
  1. photo.caption = @"Day at the Beach";  
  2. output = photo.caption;  
  3.  

我们两种方式都可以使用。但是在一个工程里面最好保持风格一致,只使用某一种。"."操作只能够被使用在setters和getters里面,而不能用在一般意思的方法上。

创建对象

主要有两种方式来创建一个对象。第一种办法像这面这样:

     
     
     
     
  1. NSString* myString = [NSString string];  
  2.  

这是一种非常习惯性的风格。在这种方式情况下,我们创建的是系统自动释放(autoreleased)类型的对象。关于自动释放类型autoreleased,我们以后会深入讨论一下。然而在许多情况下,我们需要手动的去创建对象:

     
     
     
     
  1. NSString* myString = [[NSString alloc] init];  
  2.  

这是一个嵌套的方法调用。第一个调用的NSString自己的alloc方法。这是一个相对比较底层的调用,因为他创建了内容,以及实例化了一个对象。

第二块代码调用了新创建对象的init方法。这个init方法实现了比较常用的基本设置,比如创建实例对象的参数。对于一般开发人员而言,实现这个客户的类的具体的细节并不清楚。

在一些情况下,我们可以用不通的初始化方式去赋初值:

     
     
     
     
  1. NSNumber* value = [[NSNumber alloc] initWithFloat:1.0];  
  2.  

基本的内存管理

假如我们正在为Mac OS X开发一个应用程序,我们可以选择是否启用垃圾回收机制。这就意味着我们不需要去考虑内存管理,除了一个特别复杂的情形我们需要处理一下。

然而,我们有的时候我们的开发环境没有垃圾回收机制,比如iPhone开发的时候就没有垃圾回收机制。在这种情况下,我们就需要了解一些基本的内存管理方面的概念。

假如我们手动的通过alloc创建了一个对象,我们需要用完这个对象后release它。我们不需要手动的去release一个autoreleased类型的对象,假如真的这样去做的话,我们的应用程序将会crash。

这里有两个例子:

     
     
     
     
  1. // string1 will be released automatically  
  2.  
  3. NSString* string1 = [NSString string];  
  4.  
  5. // must release this when done  
  6.  
  7. NSString* string2 = [[NSString alloc] init];  
  8.  
  9. [string2 release];  
  10.  

就这个教程而言,我们可以人为autoreleased对象会在当前函数方法调用完成后被释放。

当然了,还有很多关于内存管理的只是我们需要学习,但是这需要我们了解更多的基本概念以后才能去涉及。

设计一个类的Interface

就Objective-C语言而言,创建一个类非常简单。它非常典型的分成了两个部分。

类的接口通常保存在ClassName.h文件里,它定义了实例的参数,以及一些公开的方法。

类的实现在ClassName.m文件里。它包含了真正运行的代码和那些方法。它还经常定义一些私有的方法。这些私有的方法对于子类是不可见的。

这里有一个接口文件的大概。类名Photo,所以文件名叫Photo.h:

     
     
     
     
  1. #import  
  2.  
  3. @interface Photo : NSObject {  
  4.  
  5. NSString* caption;  
  6.  
  7. NSString* photographer;  
  8.  
  9. }  
  10.  
  11. @end  
  12.  

首先,我们把Cocoa.h import进来。Cocoa的应用程序的所有的基本的类大多都是这样做的。#import宏指令会自动的避免把同一个文件包含多次。

@interface符号表明这是Photo类的声明。冒号指定了父类。上面这个例子父类就是NSObject。

在大括弧里面,有两个变量:caption和photographer。两个都是NSString类型的。当然了,他们也可以是任何别的类型包括id类型的。

最后@end结束整个声明。

增加方法

让我们为成员变量加一些getters:

     
     
     
     
  1. #import  
  2.  
  3. @interface Photo : NSObject {  
  4.  
  5. NSString* caption;  
  6.  
  7. NSString* photographer;  
  8.  
  9. }  
  10.  
  11. - caption;  
  12.  
  13. - photographer;  
  14.  
  15. @end  
  16.  

别忘记,Objective-C方法不需要加get前缀。一个单独小横杆表明它是一个实例的方法。假如是一个加号的话,那就说明它是一个类的方法。

编译器默认的方法的返回类型为id。还有所有的方法的参数的默认类型也都是id类型的。所以上面的代码从技术上讲是对的。但是很少这么用。我们还是给它加上返回类型吧:

     
     
     
     
  1. #import  
  2.  
  3. @interface Photo : NSObject {  
  4.  
  5. NSString* caption;  
  6.  
  7. NSString* photographer;  
  8.  
  9. }  
  10.  
  11. - (NSString*) caption;  
  12.  
  13. - (NSString*) photographer;  
  14.  
  15. @end  
  16.  

下面我们再加上setters:

     
     
     
     
  1. #import  
  2.  
  3. @interface Photo : NSObject {  
  4.  
  5. NSString* caption;  
  6.  
  7. NSString* photographer;  
  8.  
  9. }  
  10.  
  11. - (NSString*) caption;  
  12.  
  13. - (NSString*) photographer;  
  14.  
  15. - (void) setCaption: (NSString*)input;  
  16.  
  17. - (void) setPhotographer: (NSString*)input;  
  18.  
  19. @end  
  20.  

Setters不需要返回任何值,所以我们把它的类型指定为void.

类的实现

我们通过实现getters来创建一个类的实现:

     
     
     
     
  1. #import "Photo.h"  
  2.  
  3. @implementation Photo  
  4.  
  5. - (NSString*) caption {  
  6.  
  7. return caption;  
  8.  
  9. }  
  10.  
  11. - (NSString*) photographer {  
  12.  
  13. return photographer;  
  14.  
  15. }  
  16.  
  17. @end  
  18.  

这部分的代码由@implementation再来加上类名开始,以@end结束。就跟类的接口定义一样,所有的方法跟接口定义里的一样。所有的对象都必要既要定义也要实现。

假如我们以前也写过代码的话,Objective-C里面的getters看上去跟别的差不多。所以我们下面就来介绍setters,它需要一点说明。

     
     
     
     
  1. - (void) setCaption: (NSString*)input  
  2.  
  3. {  
  4.  
  5. [caption autorelease];  
  6.  
  7. caption = [input retain];  
  8.  
  9. }  
  10.  
  11. - (void) setPhotographer: (NSString*)input  
  12.  
  13. {  
  14.  
  15. [photographer autorelease];  
  16.  
  17. photographer = [input retain];  
  18.  
  19. }  
  20.  

每个setter处理两个变量。第一个是当前存在对象的应用。第二个是新的输入对象。在支持垃圾回收的开发环境里,我们只要直接赋新值就可以了:

     
     
     
     
  1. - (void) setCaption: (NSString*)input {  
  2.  
  3. caption = input;  
  4.  
  5. }  
  6.  

但是假如我们不可以用垃圾回收机制的话,我们就需要先retain旧的对象,然后retain新的对象。

有两种方法可以释放一个引用对象:release 和 autorelease。标准的release会直接删除引用。autorelease方法会在将来的某个时候去release它。在它声明周期结束前,它会毫无疑问的存在。在本例中,上面setPhotographer中的photographer对象,将会在函数结束的时候被释放。

在setter里面用autorelease是安全的,因为新对象跟老的对象有可能是同一个对象有可能指向的是同一个对象。对于一个我们即将retain的对象,我们不应该立即release它。

这个也许现在看起来会困惑,但是随着我们的学习,会越来越能理解它。现在我们不需要立刻完全理解它。

初始化

我们可以创建一个初始化方法去给类的实例的成员变量赋初值:

     
     
     
     
  1. - (id) init  
  2.  
  3. {  
  4.  
  5. if ( self = [super init] )  
  6.  
  7. {  
  8.  
  9. [self setCaption:@"Default Caption"];  
  10.  
  11. [self setPhotographer:@"Default Photographer"];  
  12.  
  13. }  
  14.  
  15. return self;  
  16.  
  17. }  
  18.  

上面的代码感觉没啥好解释的,虽然第二行代码好像看上去没啥用。这个是一个单等于号,就是把[super init]的值赋给了self。

它基本上是在调用父类去实现它的初始化。这个if代码段是设置默认值之前验证初始化是否成功。

释放资源Dealloc

这个dealloc方法是在当一个对象希望被从内容里面删除的时候调用。这个我们释放在子类里面引用成员变量的最好的时机:

     
     
     
     
  1. - (void) dealloc  
  2.  
  3. {  
  4.  
  5. [caption release];  
  6.  
  7. [photographer release];  
  8.  
  9. [super dealloc];  
  10.  
  11. }  
  12.  

开始两行我们发送了release通知给了两个成员变量。我们不要在这里用autorelease。用标准的release更快一点。

最后一行的[super dealloc];非常重要。我们必须要发送消息去让父类清除它自己。假如不这么做的话,这个对象其实没有被清除干净,存在内存泄露。

dealloc在垃圾回收机制下不会被调用到。取而代之的是,我们需要实现finalize方法。

更多关于内存管理

Objective-C的内存管理系统基于引用记数。所有我们需要关心的就是跟踪我们引用,以及在运行期内是否真的释放了内存。

用最简单的术语来解释,当我们alloc一个对象的时候,应该在某个时候retain了它。每次我们调用了alloc或者retain之后,我们都必须要调用release。

这就是引用记数理论。但是在实践的时候,只有两种情况我们需要创建一个对象:

1. 成为一个类的成员变量

2. 只临时的在一个函数里面被使用

在更多的时候,一个成员变量的setter应该仅仅autorelease旧的对象,然后retain新的对象。我们只需要在dealloc的时候调用release就可以了。

所以真正需要做的就是管理函数内部的local的引用。唯一的原则就是:假如我们alloc或者copy了一个对象,那么我们在函数结束的时候需要release或者autorelease它。假如我们是通过别的方式创建的,就不管。

这里是管理成员对象的例子:

     
     
     
     
  1. - (void) setTotalAmount: (NSNumber*)input  
  2.  
  3. {  
  4.  
  5. [totalAmount autorelease];  
  6.  
  7. totalAmount = [input retain];  
  8.  
  9. }  
  10.  
  11. - (void) dealloc  
  12.  
  13. {  
  14.  
  15. [totalAmount release];  
  16.  
  17. [super dealloc];  
  18.  
  19. }  
  20.  

这里是本地引用的例子。我们只需要release我们用alloc创建的对象:

     
     
     
     
  1. NSNumber* value1 = [[NSNumber alloc] initWithFloat:8.75];  
  2.  
  3. NSNumber* value2 = [NSNumber numberWithFloat:14.78];  
  4.  
  5. // only release value1, not value2  
  6.  
  7. [value1 release];  
  8.  

这里是用本地引用对象去设一个成员变量的例子:

     
     
     
     
  1. NSNumber* value1 = [[NSNumber alloc] initWithFloat:8.75];  
  2.  
  3. [self setTotal:value1];  
  4.  
  5. NSNumber* value2 = [NSNumber numberWithFloat:14.78];  
  6.  
  7. [self setTotal:value2];  
  8.  
  9. [value1 release];  
  10.  

注意到如何管理本地引用其实都是一样的。不管你是否把它设给了一个成员变量。我们无须考虑setters的内部实现。

如果我们很好的理解了这些的话,我们基本上理解了80%的Objective-C内存管理方面的内容了。

属性Properties

前面我们写caption和author的accessors的时候,你可以已经注意到了代码非常简明,应该可以被抽象提取出来。

属性在Objective-C里是一个新的功能。他可以让我们自动的生成accessors,另外还有一些别的优点。我们可以把上面Photo的类转成用属性来实现:

上面那个类原先的实现是这样:

     
     
     
     
  1. #import  
  2.  
  3. @interface Photo : NSObject {  
  4.  
  5. NSString* caption;  
  6.  
  7. NSString* photographer;  
  8.  
  9. }  
  10.  
  11. - (NSString*) caption;  
  12.  
  13. - (NSString*) photographer;  
  14.  
  15. - (void) setCaption: (NSString*)input;  
  16.  
  17. - (void) setPhotographer: (NSString*)input;  
  18.  
  19. @end  
  20.  

假如用属性来实现就是这样:

     
     
     
     
  1. #import  
  2.  
  3. @interface Photo : NSObject {  
  4.  
  5. NSString* caption;  
  6.  
  7. NSString* photographer;  
  8.  
  9. }  
  10.  
  11. @property (retain) NSString* caption;  
  12.  
  13. @property (retain) NSString* photographer;  
  14.  
  15. @end  
  16.  

@property是Objective-C来声明属性的编译指令。括号里面的"retain"指明了setter需要retain输入的对象。这行其他的部分指定了属性的类型以及名字。

下面让我们来看看这个类的实现:

     
     
     
     
  1. #import "Photo.h"  
  2.  
  3. @implementation Photo  
  4.  
  5. @synthesize caption;  
  6.  
  7. @synthesize photographer;  
  8.  
  9. - (void) dealloc  
  10.  
  11. {  
  12.  
  13. [caption release];  
  14.  
  15. [photographer release];  
  16.  
  17. [super dealloc];  
  18.  
  19. }  
  20.  
  21. @end  
  22.  

@synthesize指令自动的生成了我们的setters和getters。所以我们只需要实现类的dealloc方法。

Accessors只有当他们原先没有的时候,才会被生成。所以可以放心大胆的去用@synthesize来指定属性。而且可以随意实现你自己的getter和setter。编译器会自己去找哪个方法没有。

属性声明还有别的选项,但是限于篇幅层次,我们下次再介绍。

Logging

在Objective-C里,往console写日记非常简单。事实上NSLog()跟C语言的printf()两个函数几乎完全相同,除了NSLog是用额外的“%@”去获得对象。

     
     
     
     
  1. NSLog ( @"The current date and time is: %@", [NSDate date] );  
  2.  

我们可以log一个对象到console里去。NSLog函数调用要输出对象的description方法,然后打印返回的NSString。我们可以在自己的类里重写description方法,这样我们就可以得到一个自定义的字符串。

调用nil对象的方法(Calling Methods on Nil)

在Objective-C里,nil对象被设计来跟NULL空指针关联的。他们的区别就是nil是一个对象,而NULL只是一个值。而且我们对于nil调用方法,不会产生crash或者抛出异常。

这个技术被framework通过多种不同的方式使用。最主要的就是我们现在在调用方法之前根本无须去检查这个对象是否是nil。假如我们调了nil对象的一个有返回值的方法,那么我们会得到一个nil返回值。

我们可以通过nil对象让我们的dealloc函数实现看上去更好一些:

     
     
     
     
  1. - (void) dealloc  
  2. {  
  3. self.caption = nil;  
  4. self.photographer = nil;  
  5. [super dealloc];  
  6. }  
  7.  

之所以可以这么做是因为我们给把nil对象设给了一个成员变量,setter就会retain nil对象(当然了这个时候nil对象啥事情也不会做)然后release旧的对象。这个方式来释放对象其实更好,因为这样做的话,成员变量连指向随机数据的机会都没有,而通过别的方式,出现指向随机数据的情形机会不可避免。

注意到我们调用的self.VAR这样的语法,这表示我们正在用setter,而且不会引起任何内存问题。假如我们直接去设值的话,就会有内存溢出:

     
     
     
     
  1. // incorrect. causes a memory leak.  
  2. // use self.caption to go through setter  
  3. caption = nil;  
  4.  

Categories

Categories是Objective-C里面最常用到的功能之一。 基本上category可以让我们给已经存在的类增加方法,而不需要增加一个子类。而且不需要知道它内部具体的实现。

如果我们想增加某个framework自带的类的方法,这非常有效。如果我们想在我们程序工程的NSString能够增加一个方法,我们就可以使用category。甚至都不需要自己实现一个NSString的子类。

比如,我们想在NSString里面增加一个方法来判断它是否是一个URL,那我们就可以这么做:

     
     
     
     
  1. #import  
  2.  
  3. @interface NSString (Utilities)  
  4.  
  5. - (BOOL) isURL;  
  6.  
  7. @end  
  8.  

这跟类的定义非常类似。区别就是category没有父类,而且在括号里面要有category的名字。名字可以随便取,但是习惯叫法会让人比较明白category里面有些什么功能的方法。

这里是具体的实现。但是要注意,这本身并不是一个判断URL很好的实现。我们主要是为了整体的了解category的概念。

     
     
     
     
  1. #import "NSString-Utilities.h"  
  2.  
  3. @implementation NSString (Utilities)  
  4.  
  5. - (BOOL) isURL  
  6.  
  7. {  
  8.  
  9. if ( [self hasPrefix:@"http://"] )  
  10.  
  11. return YES;  
  12.  
  13. else  
  14.  
  15. return NO;  
  16.  
  17. }  
  18.  
  19. @end  
  20.  

现在我们可以在任何的NSString类对象里都可以调用这个方法了。下面的代码在console里面打印的"string1 is a URL":

     
     
     
     
  1. NSString* string1 = @"http://www.CocoaDev.cn/";  
  2.  
  3. NSString* string2 = @"Pixar";  
  4.  
  5. if ( [string1 isURL] )  
  6.  
  7. NSLog (@"string1 is a URL");  
  8.  
  9. if ( [string2 isURL] )  
  10.  
  11. NSLog (@"string2 is a URL");  
  12.  

跟子类不一样,category不能增加成员变量。我们还可以用category来重写类原先的存在的方法,但是这需要非常非常小心。

记住,当我们通过category来修改一个类的时候,它对应用程序里的这个类所有对象都起作用。

后记

上面Objective-C的比较基础的大概的讲了一下。Objective-C还是比较好上手的。没有特别的语法需要去学习。而且一些概念在Objective-C里面被反复运用。