Objective-C Tutorial

C Basics

Objective-C 是 C 的超集,但其语言核心构造依赖于 C

注释

// This is an inline comment

/* This is a block comment.
   It can span multiple lines. */

变量

声明变量可以使用 <type> <name> 语法,赋值用 =,类型转换可以在变量放置新的类型

// main.m
#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {  
    @autoreleasepool {
        double odometer = 9200.8;
        int odometerAsInteger = (int)odometer;

        NSLog(@"You've driven %.1f miles", odometer);        // 9200.8
        NSLog(@"You've driven %d miles", odometerAsInteger); // 9200
    }
    return 0;
}

常量

const 常量告诉编译器赋值后不能再改变了

double const pi = 3.14159;  
pi = 42001.0;               // Compiler error  

算术

NSLog(@"6 + 2 = %d",  6 + 2);    // 8  
NSLog(@"6 - 2 = %d",  6 - 2);    // 4  
NSLog(@"6 * 2 = %d",  6 * 2);    // 12  
NSLog(@"6 / 2 = %d",  6 / 2);    // 3  
NSLog(@"6 %% 2 = %d", 6 % 2);    // 0  

自增自减

int i = 0;  
NSLog(@"%d", i);    // 0  
i++;  
NSLog(@"%d", i);    // 1  
i++;  
NSLog(@"%d", i);    // 2  

条件语句

int modelYear = 1990;  
if (modelYear < 1967) {  
    NSLog(@"That car is an antique!!!");
} else if (modelYear <= 1991) {
    NSLog(@"That car is a classic!");
} else if (modelYear == 2013) {
    NSLog(@"That's a brand new car!");
} else {
    NSLog(@"There's nothing special about that car.");
}

C 也提供了 switch 语句,但只支持整数类型

// Switch statements (only work with integral types) 
switch (modelYear) {  
    case 1987:
        NSLog(@"Your car is from 1987.");
        break;
    case 1988:
        NSLog(@"Your car is from 1988.");
        break;
    case 1989:
    case 1990:
        NSLog(@"Your car is from 1989 or 1990.");
        break;
    default:
        NSLog(@"I have no idea when your car was made.");
        break;
}

循环

whilefor 通常用来循环迭代,而 breakcontinue 用来退出循环

int modelYear = 1990;  
// While loops
int i = 0;  
while (i<5) {  
    if (i == 3) {
        NSLog(@"Aborting the while-loop");
        break;
    }
    NSLog(@"Current year: %d", modelYear + i);
    i++;
}
// For loops
for (int i=0; i<5; i++) {  
    if (i == 3) {
        NSLog(@"Skipping a for-loop iteration");
        continue;
    }
    NSLog(@"Current year: %d", modelYear + i);
}

而 Objective-C 提供了 for-in 快速枚举

// For-in loops ("Fast-enumeration," specific to Objective-C)
NSArray *models = @[@"Ford", @"Honda", @"Nissan", @"Porsche"];  
for (id model in models) {  
    NSLog(@"%@", model);
}

Macros 宏指令

#define 指令将宏名称映射为一个表达式,当编译器解析代码前,预处理器将会扫描并替换所有的宏为真实的表达式

// main.m
#import <Foundation/Foundation.h>

#define PI 3.14159
#define RAD_TO_DEG(radians) (radians * (180.0 / PI))

int main(int argc, const char * argv[]) {  
    @autoreleasepool {
        double angle = PI / 2;              // 1.570795
        NSLog(@"%f", RAD_TO_DEG(angle));    // 90.0
    }
    return 0;
}

上面定义了两个宏,第一个是 π,第二个可以接收一个参数。

Typedef

typedef 关键字允许你创建新的数据类型或对已存在的类型重定义,下面将 unsigned char 定义为 ColorComponent

// main.m
#import <Foundation/Foundation.h>


typedef unsigned char ColorComponent;

int main(int argc, const char * argv[]) {  
    @autoreleasepool {
        ColorComponent red = 255;
        ColorComponent green = 160;
        ColorComponent blue = 0;
        NSLog(@"Your paint job is (R: %hhu, G: %hhu, B: %hhu)",
              red, green, blue);
    }
    return 0;
}

typedef 通常用在结构体和枚举类型中

结构体

结构体属于 C 的原始类型,他允许我们将一些变量聚集起来组成一个复杂的数据类型,但是不提供任何面向对象的语言特性(方法),下面用 typedef 来定义了一个 Color 类型的结构体:

// main.m
#import <Foundation/Foundation.h>

typedef struct {  
    unsigned char red;
    unsigned char green;
    unsigned char blue;
} Color;

int main(int argc, const char * argv[]) {  
    @autoreleasepool {
        Color carColor = {255, 160, 0};
        NSLog(@"Your paint job is (R: %hhu, G: %hhu, B: %hhu)",
              carColor.red, carColor.green, carColor.blue);
    }
    return 0;
}

初始化使用 {255, 160, 0} 语法(赋值必须按照顺序来),可以通过点语法来访问。

枚举

枚举是一堆常量的集合,有点类似于结构体,通常用 typedef 来定义一个枚举类型

// main.m
#import <Foundation/Foundation.h>

typedef enum {  
    FORD,
    HONDA,
    NISSAN,
    PORSCHE
} CarModel;

int main(int argc, const char * argv[]) {  
    @autoreleasepool {
        CarModel myCar = NISSAN;
        switch (myCar) {
            case FORD:
            case PORSCHE:
                NSLog(@"You like Western cars?");
                break;
            case HONDA:
            case NISSAN:
                NSLog(@"You like Japanese cars?");
                break;
            default:
                break;
        }
    }
    return 0;
}

原始数组

虽然 Objective-C 从更高的框架级别上提供了 NSArrayNSMutableArray,但在某些性能敏感的领域我们还是可以使用原始的 C 提供的数组,语法如下:

int years[4] = {1968, 1970, 1989, 1999};  
years[0] = 1967;  
for (int i=0; i<4; i++) {  
    NSLog(@"The year at index %d is: %d", i, years[i]);
}

指针

指针是指一段内存地址

  • & 返回一段内存地址,可用做创建指针
  • * 返回指针的内容
int year = 1967;          // Define a normal variable  
int *pointer;             // Declare a pointer that points to an int  
pointer = &year;          // Find the memory address of the variable  
NSLog(@"%d", *pointer);   // Dereference the address to get its value  
*pointer = 1990;          // Assign a new value to the memory address
NSLog(@"%d", year);       // Access the value via the variable  

上述行为可视化表示如下:

指针在一个连续的内存地址中移动时尤其有用,下面使用指针遍历数组中的元素:

char model[5] = {'H', 'o', 'n', 'd', 'a'};  
char *modelPointer = &model[0];  
for (int i=0; i<5; i++) {  
    NSLog(@"Value at memory address %p is %c",
          modelPointer, *modelPointer);
    modelPointer++;
}
NSLog(@"The first letter is %c", *(modelPointer - 5));  

使用 ++ 操作符移到下一个内存地址单元,然后用 NSLog() 打印出结果。打印完毕后再将指针移回第一个

The Null Pointer

null pointer 是一种特殊的指针,意味着不指向任何对象,C 语言中只有一种 null 指针,通过 NULL macro 引用。下面的例子展示了指针的对象可以被掏空:

int year = 1967;  
int *pointer = &year;  
NSLog(@"%d", *pointer);     // Do something with the value  
pointer = NULL;             // Then invalidate it  

Void Pointers

void pointer 是一种泛型类型可以指向任意类型,可看做是一段随机的内存地址,因此你需要给他更多的信息来解释其所指向的内容,最简单的方式是将其显式转换为 non-void pointer

int year = 1967;  
void *genericPointer = &year;  
int *intPointer = (int *)genericPointer;  
NSLog(@"%d", *intPointer);  

void pointers 的通用特性带来很大的便利性,比如 NSString 中定义的将 C 数组转换为 Objective-C 对象的方法:

- (id)initWithBytes:(const void *)bytes
             length:(NSUInteger)length
           encoding:(NSStringEncoding)encoding

bytes 参数指向了 C 数组中的第一个内存单元,length 参数定义了读取多少 bytes,encoding 决定了编码形式(single-byte, UTF-8, and UTF-16)

Objective-C 中的指针

很多情况下你只需要了解 OC 环境下的指针就足够了,一个 NSString 对象只需要存储一个指针,而不是一个变量:

NSString *model = @"Honda";  

提到空指针,C 与 OC 之间有点区别:C 使用 NULL,而 OC 使用 nil:

NSString *anObject;    // An Objective-C object  
anObject = NULL;       // This will work  
anObject = nil;        // But this is preferred  
int *aPointer;         // A plain old C pointer  
aPointer = nil;        // Don't do this  
aPointer = NULL;       // Do this instead  

Functions

变量、条件、循环、函数是组成所有现代语言必不可少的部分。你可以通过函数来组织与重用任意代码,这部分主要介绍 C 语言的函数,以及基本语法,声明和实现、作用域和一些函数库。

基本语法

一个 C 函数有四部分:返回值、函数名、参数、代码块。函数定义完成后,可以通过调用的方式来执行(传入必要参数)下面的函数在限定范围内产生一个随机数:

// main.m
#import <Foundation/Foundation.h>

int getRandomInteger(int minimum, int maximum) {  
    return arc4random_uniform((maximum - minimum) + 1) + minimum;
}

int main(int argc, const char * argv[]) {  
    @autoreleasepool {
        int randomNumber = getRandomInteger(-10, 10);
        NSLog(@"Selected a random number between -10 and 10: %d",
              randomNumber);
    }
    return 0;
}

内建的 arc4random_uniform() 函数返回一个从 0 到你提供参数范围内的一个随机数

函数可以使用指针对象作为参数或返回值,因此你可以无缝对接 OC(OC 中所有对象都是指针)

// main.m
#import <Foundation/Foundation.h>

NSString *getRandomMake(NSArray *makes) {  
    int maximum = (int)[makes count];
    int randomIndex = arc4random_uniform(maximum);
    return makes[randomIndex];
}

int main(int argc, const char * argv[]) {  
    @autoreleasepool {
        NSArray *makes = @[@"Honda", @"Ford", @"Nissan", @"Porsche"];
        NSLog(@"Selected a %@", getRandomMake(makes));
    }
    return 0;
}

getRandomMake() 函数接受一个 NSArray 对象作为参数,返回一个 NSString 对象

声明和实现

函数使用前必须被定义,为了解决定义在调用之后所产生的问题,C 允许你将函数声明从实现中分离出来:

// main.m
#import <Foundation/Foundation.h>

// Declaration
NSString *getRandomMake(NSArray *);

int main(int argc, const char * argv[]) {  
    @autoreleasepool {
        NSArray *makes = @[@"Honda", @"Ford", @"Nissan", @"Porsche"];
        NSLog(@"Selected a %@", getRandomMake(makes));
    }
    return 0;
}

// Implementation
NSString *getRandomMake(NSArray *makes) {  
    int maximum = (int)[makes count];
    int randomIndex = arc4random_uniform(maximum);
    return makes[randomIndex];
}

Static 关键字

static 可以用在修饰函数和变量上,但所产生的效果有所差异:

Static Functions

默认的,所有函数都能够被全局使用,意味着只要再一个文件中定义了一个函数,那么该函数在任何地方能被调用。而 static 关键字可以限制了函数只能在当前文件中使用。因此可以用来创建私有函数、避免命名冲突。

下面展示了如何创建 static function,如果你在一个新文件中创建了下面的代码,那么从 main.m 中是无法调用 getRandomInteger() 函数的。注意声明和定义都要加 static 关键字:

// Static function declaration
static int getRandomInteger(int, int);

// Static function implementation
static int getRandomInteger(int minimum, int maximum) {  
    return arc4random_uniform((maximum - minimum) + 1) + minimum;
}

Static Local Variables

声明在函数体内部的变量通常叫做局部变量,而且函数每次调用完毕后都会重置。然而如果使用 static 来修饰局部变量,函数会记住该变量的值,并不会被重置:

例如 currentCount 变量永远不会被重置:

// main.m
#import <Foundation/Foundation.h>

int countByTwo() {  
    static int currentCount = 0;
    currentCount += 2;
    return currentCount;
}

int main(int argc, const char * argv[]) {  
    @autoreleasepool {
        NSLog(@"%d", countByTwo());    // 2
        NSLog(@"%d", countByTwo());    // 4
        NSLog(@"%d", countByTwo());    // 6
    }
    return 0;
}

与用来修饰 function 不同,static 用来修饰变量并不会引起作用域的变化,也就是说函数内部的局部变量仍然只能够存活于函数体内。

Function Libraries

Objective-C 并不支持命名空间,因此为了避免和一些全局函数的命名冲突,大的框架通常会有一些自身的前缀(在类与函数上)这就是为什么常见 NSMakeRange()CGImageCreate(),而不是 makeRange()imageCreate()

创建自己的函数库时,应该将函数声明放在单独的头文件中与实现分开(就和 Objective-C classes 那样)这样以后使用只需要导入头文件即可,不用操心如何实现。CarUtilities 库的头文件可能类似于下面这种:

// CarUtilities.h
#import <Foundation/Foundation.h>

NSString *CUGetRandomMake(NSArray *makes);  
NSString *CUGetRandomModel(NSArray *models);  
NSString *CUGetRandomMakeAndModel(NSDictionary *makesAndModels);  

相应的实现文件实现了具体功能,上面说过可以使用 static 创建私有函数:

// CarUtilities.m
#import "CarUtilities.h"

// Private function declaration
static id getRandomItemFromArray(NSArray *anArray);

// Public function implementations
NSString *CUGetRandomMake(NSArray *makes) {  
    return getRandomItemFromArray(makes);
}
NSString *CUGetRandomModel(NSArray *models) {  
    return getRandomItemFromArray(models);
}
NSString *CUGetRandomMakeAndModel(NSDictionary *makesAndModels) {  
    NSArray *makes = [makesAndModels allKeys];
    NSString *randomMake = CUGetRandomMake(makes);
    NSArray *models = makesAndModels[randomMake];
    NSString *randomModel = CUGetRandomModel(models);
    return [randomMake stringByAppendingFormat:@" %@", randomModel];
}

// Private function implementation
static id getRandomItemFromArray(NSArray *anArray) {  
    int maximum = (int)[anArray count];
    int randomIndex = arc4random_uniform(maximum);
    return anArray[randomIndex];
}

现在我们可以从 main.m 中导入头文件,并调用库中声明的函数了,注意如果我们调用私有函数,会报错:

// main.m
#import <Foundation/Foundation.h>
#import "CarUtilities.h"

int main(int argc, const char * argv[]) {  
    @autoreleasepool {
        NSDictionary *makesAndModels = @{
            @"Ford": @[@"Explorer", @"F-150"],
            @"Honda": @[@"Accord", @"Civic", @"Pilot"],
            @"Nissan": @[@"370Z", @"Altima", @"Versa"],
            @"Porsche": @[@"911 Turbo", @"Boxster", @"Cayman S"]
        };
        NSString *randomCar = CUGetRandomMakeAndModel(makesAndModels);
        NSLog(@"Selected a %@", randomCar);
    }
    return 0;
}

Classes

Objective-C 也是一门面向对象的语言,首先在类中定义了一组可重用的属性和方法,然后你通过实例化对象来与其他的属性和类进行交互。

Objective-C 类似与 C++,将具体实现抽象为类接口。一个接口声明关于一个类的一组公共的属性和方法,然后相应的实现定义了如何让这些属性和接口工作起来。这与我们学习的将函数的功能分离有些类似:

在这个模型中,我们暴露了类接口的基本语法、实现、属性和方法,以及实例化对象的典型方式。

Creating Classes

创建类也很简单,可以使用 Xcode 的模板,选择 Objective-C class 下面的 the iOS > Cocoa Touch category。创建的类都继承自 NSObject。一旦创建好,在 Build Phases tab 下的 Compile Sources 部分可以看到我们创建的源文件,如果没有通过 Xcode 创建源文件,那么我们编译的时候要手动拖到这里:

Interfaces 接口

Car.h 包含一些样板代码,我们来修改成如下:

// Car.h
#import <Foundation/Foundation.h>

@interface Car : NSObject {
    // Protected instance variables (not recommended)
}

@property (copy) NSString *model;

- (void)drive;

@end

@interface 后紧跟 class 和 superclass(用 : 隔开),Protected variables 可以定义在花括号中,但我们通常将其放置在 .m 文件中而不是这里。

@property 声明了一个公开属性,copy 表示其内存管理行为。这个例子中表示分配一个拷贝给 model 而不是一个指针。

-(void)drive 声明了一个不带参数的实例方法,void 表示没有返回值

Implementations 实现

任何类的实现第一件事情是导入相应的接口。@implementation 类似于 @interface,除了你不需要包含 super class。私有实例变量可以存储在类名之后的花括号中:

// Car.m
#import "Car.h"

@implementation Car {
    // Private instance variables
    double _odometer;
}

@synthesize model = _model;    // Optional for Xcode 4.4+

- (void)drive {
    NSLog(@"Driving a %@. Vrooooom!", self.model);
}

@end

@synthesize 是一种属性生成便利存取方法的指令,默认 getter 方法名就是属性名,而 setter 方法是 set 前缀 + 属性名。其中的 _model 表示私有实例变量。

Xcode 4.4 以后,声明 @property 时会自动生成 @synthesize,因此可以省略。

drive 方法与声明一致,这里提供了他的实现。注意我们使用 self.model(属性)来代替 _model(实例变量)。实例变量通常用在 初始化方法(init methods)和销毁方法(dealloc method)中。

self 关键字代表自身实例对象的引用(类似于 C++ 和 Java 中的 this),除了调用属性,还可以执行方法 [self anotherMethod]

Instantiation and Usage 实例化和应用

任何文件需要访问一个类都必须要导入他的头文件(Car.h),而不应该直接去访问实现文件:

// main.m
#import <Foundation/Foundation.h>
#import "Car.h"

int main(int argc, const char * argv[]) {  
    @autoreleasepool {
        Car *toyota = [[Car alloc] init];

        [toyota setModel:@"Toyota Corolla"];
        NSLog(@"Created a %@", [toyota model]);

        toyota.model = @"Toyota Camry";
        NSLog(@"Changed the car to a %@", toyota.model);

        [toyota drive];

    }
    return 0;
}

在 #import 之后,我们就可以使用 alloc/init 来实例化对象了。实例化分两步,第一步为对象分配内存,第二步初始化。

值得注意的是所有的对象其实只是存储了指针,所以这里是 Car *toyota 而不是 Car toyota。调用方法时使用方括号

[toyota setModel:@"Toyota Corolla"] 

如果是其他语言,会使用:

toyota.setModel("Toyota Corolla");  

Class Methods and Variables 类方法和变量

上面介绍了实例等级的属性和方法,现在学习下 class-level 的属性和方法。在其他语言中也叫 “static” methods/properties,不要和我们之前学过的 static 关键字搞混淆了。

类方法声明和实例方法差不多,不过前面是 +

// Car.h
+ (void)setDefaultModel:(NSString *)aModel;

implementation 实现文件中也是以 + 开头:

// Car.m
#import "Car.h"

static NSString *_defaultModel;

@implementation Car {
...

+ (void)setDefaultModel:(NSString *)aModel {
    _defaultModel = [aModel copy];
}

@end

虽然在技术 Objective-C 上并不存在一个 class-level 变量,你可以用 static variable 在定义实现前来模拟:

// Car.m
#import "Car.h"

static NSString *_defaultModel;

@implementation Car {
...

+ (void)setDefaultModel:(NSString *)aModel {
    _defaultModel = [aModel copy];
}

@end

[aModel copy] 创建了一份拷贝然后赋值给 _defaultModel,这其实就是属性中(copy)做的事情。

类方法调用时和实例方法类似,但直接使用类名调用:

// main.m
[Car setDefaultModel:@"Nissan Versa"];

构造器方法 “Constructor” Methods

Objective-C 中没有构造方法,取而代之的是一个对象的初始化通过 alloc/init 组合来实现。稍后我们来讨论下 class-level 的初始化方法。

init 是默认的初始化方法,但是你也可以定义你自己的接受参数的初始化方法。自定义的初始化方法和普通方面没太大区别,只是注意要以 init 开头:

// Car.h
- (id)initWithModel:(NSString *)aModel;

要实现这个方法,你要遵循一定的初始化范式,如下:

// Car.m
- (id)initWithModel:(NSString *)aModel {
    self = [super init];
    if (self) {
        // Any custom setup work goes here
        _model = [aModel copy];
        _odometer = 0;
    }
    return self;
}

- (id)init {
    // Forward to the "designated" initialization method
    return [self initWithModel:_defaultModel];
}

初始化方法总应该返回一个自身对象的引用,如果初始化失败则返回 nil。因此我们需要在使用前检查 self,我们只需一个初始化方法做这些,其余的(init)转发到这个指定初始化方法(designated initializer)即可

注意初始化方法中我们使用了实例变量而不是属性

Class-Level Initialization 类层级上的初始化

initialize 方法是类级别上的初始化方法,他给你一次在使用前配置类的机会,例如可以在使用 Car 类前给 _defaultModel 变量分配一个有效值:

// Car.m
+ (void)initialize {
    if (self == [Car class]) {
        // Makes sure this isn't executed more than once
        _defaultModel = @"Nissan Versa";
    }
}

类方法 initialize 对于所有类来说只会被调用一次(在其使用前)。这包括 Car 的所有子类,也就是说 Car 会收到两次 initialize 调用,如果其子类没有重载的话。为了解决这个问题,比较好的习惯是使用 self == [Car class] 来判断当前类是否为 Car,如果 self 是子类就不用再执行重复的初始化了。

这里的 self 关键字代表 Class 而不是实例

Objective-C 并不会强迫标记重载方法,尽管 init 和 initialize 都是由父类 NSObject 定义的,当你在 Car.m 中重新定义时编译器不会报错

在下面的 main.m 中展示了我们自定义的初始化方法的使用,在类第一次使用前,[Car initialize] 会被自动调用,_defaultModel 会被设置为 @"Nissan Versa"(通过类初始化),随后我们通过自定义的初始化(initWithModel:)来修改:

// main.m
#import <Foundation/Foundation.h>
#import "Car.h"

int main(int argc, const char * argv[]) {  
    @autoreleasepool {

        // Instantiating objects
        Car *nissan = [[Car alloc] init];
        NSLog(@"Created a %@", [nissan model]);

        Car *chevy = [[Car alloc] initWithModel:@"Chevy Corvette"];
        NSLog(@"Created a %@, too.", chevy.model);

    }
    return 0;
}

Dynamic Typing

Classes 自身也可以看成是对象,这就可以查询他的属性(自省),甚至改变他们的行为(反射),这种动态类型功能非常强大,你不需要知道对象的类型也可以调用方法和设置属性

最简单的方式是通过类方法来得到一个类对象,例如 [Car class] 返回一个对象来表示 Car 类,你可以将其作为参数传递给方法 isMemberOfClass:isKindOfClass: 得到接收者实例的相关信息。一个通用的方法如下:

// main.m
#import <Foundation/Foundation.h>
#import "Car.h"

int main(int argc, const char * argv[]) {  
    @autoreleasepool {
        Car *delorean = [[Car alloc] initWithModel:@"DeLorean"];

        // Get the class of an object
        NSLog(@"%@ is an instance of the %@ class",
              [delorean model], [delorean class]);

        // Check an object against a class and all subclasses
        if ([delorean isKindOfClass:[NSObject class]]) {
            NSLog(@"%@ is an instance of NSObject or one "
                  "of its subclasses",
                  [delorean model]);
        } else {
            NSLog(@"%@ is not an instance of NSObject or "
                  "one of its subclasses",
                  [delorean model]);
        }

        // Check an object against a class, but not its subclasses
        if ([delorean isMemberOfClass:[NSObject class]]) {
            NSLog(@"%@ is a instance of NSObject",
                  [delorean model]);
        } else {
            NSLog(@"%@ is not an instance of NSObject",
                  [delorean model]);
        }

        // Convert between strings and classes
        if (NSClassFromString(@"Car") == [Car class]) {
            NSLog(@"I can convert between strings and classes!");
        }
    }
    return 0;
}

NSClassFromString() 函数是另外一种得到 Class 的函数(通过类名得到 Class),这是非常灵活的,可以让你在 runtime 时动态请求一个 Class 对象,但也比较低效。因为这个原因,还是尽量使用 class 方法来得到类对象。

前面提到 OC 不支持命名空间,因此类在命名时也要加前缀防止命名冲突。

Properties

一个对象的属性可以让其他对象来更改他的状态,但是一个精心设计的面向对象编程语言,是不允许直接访问一个对象的内部状态的,取而代之的是通过存取方法(getters and setters)通常用于对象基础操作的抽象。

Interacting with a property via accessor methods

@property 指令可以自动生成存取方法,你无需关注实现细节

这个模块可以让你修改 getter 和 setter 行为,一些属性(copy)决定了背后的内存处理细节。

The @property Directive 指令

先来查看下 Car 类的头文件和实现:

// Car.h
#import <Foundation/Foundation.h>

@interface Car : NSObject

@property BOOL running;

@end
// Car.m
#import "Car.h"

@implementation Car

@synthesize running = _running;    // Optional for Xcode 4.4+

@end

编译器为 running 属性自动生成了 gettersetter 方法,以及实例变量 _running

- (BOOL)running {
    return _running;
}
- (void)setRunning:(BOOL)newValue {
    _running = newValue;
}

使用 @property 声明后,你可以直接调用 getter 和 setter 方法,也可以在 Car.m 中提供自己的 getter/setters,这种情况是要强制写 @synthesize

Properties 还可以通过点语法来使用 getter 和 setter 方法

// main.m
#import <Foundation/Foundation.h>
#import "Car.h"

int main(int argc, const char * argv[]) {  
    @autoreleasepool {
        Car *honda = [[Car alloc] init];
        honda.running = YES;                // [honda setRunning:YES]
        NSLog(@"%d", honda.running);        // [honda running]
    }
    return 0;
}

The getter= and setter= Attributes

如果你不喜欢 @property 默认的命名规范,你可以将 getter/setter 方法改用 getter= 和 setter= 修饰。一种常见的示例是针对 Boolean 属性,针对 getter 方法加前缀 is

@property (getter=isRunning) BOOL running;

现在生成的访问器是 isRunningsetRunning,注意公开属性仍然调用 running,应该使用点语法:

Car *honda = [[Car alloc] init];  
honda.running = YES;                // [honda setRunning:YES]  
NSLog(@"%d", honda.running);        // [honda isRunning]  
NSLog(@"%d", [honda running]);      // Error: method no longer exists  

The readonly Attribute 只读属性

只读属性阻止了 setter 方法,使用 readonly 标识

#import <Foundation/Foundation.h>

@interface Car : NSObject

@property (getter=isRunning, readonly) BOOL running;

- (void)startEngine;
- (void)stopEngine;

@end
// Car.m
#import "Car.h"

@implementation Car

- (void)startEngine {
    _running = YES;
}
- (void)stopEngine {
    _running = NO;
}

@end

我们不能使用 self.running 来修改,因为 running 属性是只读的,所以只能通过实例变量的方式修改,测试一下:

Car *honda = [[Car alloc] init];  
[honda startEngine];
NSLog(@"Running: %d", honda.running);  
honda.running = NO;                      // Error: read-only property  

properties 可以让我们避免写一些无趣的 getter 和 setter 方法。

The nonatomic Attribute

Atomicity 描述了属性在线程环境中的行为。当你拥有多个线程在同一时刻读写。意味着正在进行的 getter/setter 可以个被另一个操作中断,这样的话容易导致数据损坏。

Atomic 属性锁住对象来保证读写操作真正完成。但你要记住,使用原子属性,并不意味着你的代码就是真正线程安全的。

@property 默认是 atomic,这会产生额外的开销,如果你不是在多线程环境下,可以将其改为 nonatomic

这里还有一个小问题要注意:如果使用了 atomic,并且自己实现了存取方法,那么 gettersetter 都要配对实现,不能只实现 getter 而不实现 setternoatomic 没有这种烦恼。

Memory Management

在所有的面向对象编程的语言中,对象存活于内存中,尤其针对手机是一种稀缺资源。我们的目标是高效地管理内存。Objective-C 采用对象所有权来管理内存。如果一个对象没有拥有者了,那么就可以销毁释放内存。

Automatic Reference Counting 自动引用计数自动地替我们来管理内存,但我们需要理解 @property 中的 strong, weakcopy

The strong Attribute

strong 创建了一种强引用关系,从 Xcode 4.3 以后,ARC 默认将 @property 设置为 strong,我查了下 stackoverflow,具体细节在此:

  • If a header file is imported only into ARC-enabled classes, and there is no default ownership declared, then the ownership of the property within this header file is strong.
  • If a header file is imported into at least one non-ARC class, and there is no default ownership declared, then the ownership of the property is assign!

The weak Attribute

强引用有时会导致循环引用,这时我们要靠 weak 来打破循环

The copy Attribute

copy 是比起 strong 的强引用转而创建一个副本,只有遵循 NSCopying protocol 协议的对象才可以使用这个属性。

该属性通常用于表示值的场景(相对于表示关系)比如,开发者通常针对 NSString 使用 copy 而不是 strong

// Car.h
@property (nonatomic, copy) NSString *model;

Other Attributes 其他的一些属性

下面一些属性用于 ARC 出现之前

The retain Attribute

retain 用于手动内存管理,相当于 ARC 的 strong

The unsafe_unretained Attribute

unsafe_unretained 类似于 ARC 的 weak,但他不会自动销毁,你唯一使用他的理由是与不支持 weak 的代码兼容。

The assign Attribute

assign 仅仅是简单的指针赋值操作,不做任何内存管理。默认用于原始的数据类型(Int 等),在 MRC 的时代也常常当做 weak 来使用。

Methods

方法表示一个对象该如何做一个操作

Naming Conventions 命名规范

Objective-C 的方法为了消除 API 的歧义,方法名都非常冗长,基本遵循三个规则:

  1. 不要缩减任何东西
  2. 显式标明方法中的参数名
  3. 显式说明方法中的返回值

牢记这三条规则,然后阅读下面的接口:

// Car.h
#import <Foundation/Foundation.h>

@interface Car : NSObject

// Accessors
- (BOOL)isRunning;
- (void)setRunning:(BOOL)running;
- (NSString *)model;
- (void)setModel:(NSString *)model;

// Calculated values
- (double)maximumSpeed;
- (double)maximumSpeedUsingLocale:(NSLocale *)locale;

// Action methods
- (void)startEngine;
- (void)driveForDistance:(double)theDistance;
- (void)driveFromOrigin:(id)theOrigin toDestination:(id)theDestination;
- (void)turnByAngle:(double)theAngle;
- (void)turnToAngle:(double)theAngle;

// Error handling methods
- (BOOL)loadPassenger:(id)aPassenger error:(NSError **)error;

// Constructor methods
- (id)initWithModel:(NSString *)aModel;
- (id)initWithModel:(NSString *)aModel mileage:(double)theMileage;

// Comparison methods
- (BOOL)isEqualToCar:(Car *)anotherCar;
- (Car *)fasterCar:(Car *)anotherCar;
- (Car *)slowerCar:(Car *)anotherCar;

// Factory methods
+ (Car *)car;
+ (Car *)carWithModel:(NSString *)aModel;
+ (Car *)carWithModel:(NSString *)aModel mileage:(double)theMileage;

// Singleton methods
+ (Car *)sharedCar;

@end

Calling Methods 调用方法

在前面类的章节已经讨论过方法调用。带参数的方法紧跟冒号

[porsche initWithModel:@"Porsche"];

多个参数

[porsche initWithModel:@"Porsche" mileage:42000.0];

对比:

// Python/Java/C++
porsche.drive("Home", "Airport");

// Objective-C
[porsche driveFromOrigin:@"Home" toDestination:@"Airport"];

嵌套调用:

// JavaScript
Car.alloc().init()

// Objective-C
[[Car alloc] init];

Protected and Private Methods 保护方法和私有方法

Objective-C 中没有 protectedprivate 这样的关键字

想要实现 private,可以在头文件中不声明方法,然后在实现文件中实现的方法就是类的私有方法。而 protected 方法,Objective-C 提供了 categories 来实现,稍后会学习

Selectors

Selectors是 Objective-C 内部针对方法名的一种表示,他让我们将方法看做是一种独立的实体,将一个要执行的动作从对象中剥离出来。

有两种方式可以得到 selector

  • @selector()
  • NSSelectorFromString() 将一个 string 转换为 selector

二者都返回一个特殊的数据类型 SEL,但是后者不那么高效

// main.m
#import <Foundation/Foundation.h>
#import "Car.h"

int main(int argc, const char * argv[]) {  
    @autoreleasepool {
        Car *porsche = [[Car alloc] init];
        porsche.model = @"Porsche 911 Carrera";

        SEL stepOne = NSSelectorFromString(@"startEngine");
        SEL stepTwo = @selector(driveForDistance:);
        SEL stepThree = @selector(turnByAngle:quickly:);

        // This is the same as:
        // [porsche startEngine];
        [porsche performSelector:stepOne];

        // This is the same as:
        // [porsche driveForDistance:[NSNumber numberWithDouble:5.7]];
        [porsche performSelector:stepTwo
                      withObject:[NSNumber numberWithDouble:5.7]];

        if ([porsche respondsToSelector:stepThree]) {
            // This is the same as:
            // [porsche turnByAngle:[NSNumber numberWithDouble:90.0]
            //              quickly:[NSNumber numberWithBool:YES]];
            [porsche performSelector:stepThree
                          withObject:[NSNumber numberWithDouble:90.0]
                          withObject:[NSNumber numberWithBool:YES]];
        }
        NSLog(@"Step one: %@", NSStringFromSelector(stepOne));
    }
    return 0;
}

Selectors 可以在任意对象上通过 performSelector: 来执行,withObject: 处理那些需要带一个或两个参数的方法(且需要这些参数必须是对象)

如果感觉以上限制太多,可以参考高级用法 NSInvocation

如果不确定是否能响应 SEL 方法,可以用 respondsToSelector: 来检查一下

Selectors 这种主要由方法名和描述参数的标签组成,当然有参数的别忘了冒号

以下就是具体实现,注意这里用 NSNumber 替代了 double,因为 performSelector:withObject: 不支持原始的 C 类型

// Car.h
#import <Foundation/Foundation.h>

@interface Car : NSObject

@property (copy) NSString *model;

- (void)startEngine;
- (void)driveForDistance:(NSNumber *)theDistance;
- (void)turnByAngle:(NSNumber *)theAngle
            quickly:(NSNumber *)useParkingBrake;

@end
// Car.m
#import "Car.h"

@implementation Car

@synthesize model = _model;

- (void)startEngine {
    NSLog(@"Starting the %@'s engine", _model);
}

- (void)driveForDistance:(NSNumber *)theDistance {
    NSLog(@"The %@ just drove %0.1f miles",
          _model, [theDistance doubleValue]);
}

- (void)turnByAngle:(NSNumber *)theAngle
            quickly:(NSNumber *)useParkingBrake {
    if ([useParkingBrake boolValue]) {
        NSLog(@"The %@ is drifting around the corner!", _model);
    } else {
        NSLog(@"The %@ is making a gentle %0.1f degree turn",
              _model, [theAngle doubleValue]);
    }
}

@end

Protocols

一个协议就是一组属性和方法并且能被任意类实现。这被一个普通的类型接口更加灵活,作为一种解耦方式,可以在完全无关的类中重用。

不相关的类部署 StreetLegal 协议

创建协议

一个简单的 StreetLegal 协议类如下所示:

// StreetLegal.h
#import <Foundation/Foundation.h>

@protocol StreetLegal <NSObject>

- (void)signalStop;
- (void)signalLeftTurn;
- (void)signalRightTurn;

@end

任何部署了该协议的对象都要实现所有的方法,注意 <NSObject> 表示将 NSObject protocol 收纳进了 StreetLegal 协议。因此任何对象遵循了 StreetLegal 协议,也要同时遵循 NSObject protocol,幸运的是 NSObject 这个根类本身已经遵循了该协议,所以只要继承自 NSObject 的对象都会遵循此协议。

部署协议

可以通过在 class/superclass 后面加 <> 括号来部署要遵循的协议,下面的 Bicycle 类就遵循 StreetLegal 协议

// Bicycle.h
#import <Foundation/Foundation.h>
#import "StreetLegal.h"

@interface Bicycle : NSObject <StreetLegal>

- (void)startPedaling;
- (void)removeFrontWheel;
- (void)lockToStructure:(id)theStructure;

@end

这样即使 Bicycle 从不同的父类继承而来,我们可以分别部署多个协议

Bicycle 的实现并没有什么特殊之处,只是确保了将 Bicycle.h 和 StreetLegal.h 中所有声明的方法全部实现:

// Bicycle.m
#import "Bicycle.h"

@implementation Bicycle

- (void)signalStop {
    NSLog(@"Bending left arm downwards");
}
- (void)signalLeftTurn {
    NSLog(@"Extending left arm outwards");
}
- (void)signalRightTurn {
    NSLog(@"Bending left arm upwards");
}
- (void)startPedaling {
    NSLog(@"Here we go!");
}
- (void)removeFrontWheel {
    NSLog(@"Front wheel is off."
          "Should probably replace that before pedaling...");
}
- (void)lockToStructure:(id)theStructure {
    NSLog(@"Locked to structure. Don't forget the combination!");
}

@end

现在我们可以调用协议方法了:

// main.m
#import <Foundation/Foundation.h>
#import "Bicycle.h"

int main(int argc, const char * argv[]) {  
    @autoreleasepool {
        Bicycle *bike = [[Bicycle alloc] init];
        [bike startPedaling];
        [bike signalLeftTurn];
        [bike signalStop];
        [bike lockToStructure:nil];
    }
    return 0;
}

Type Checking With Protocols

将协议名放置在尖括号中,紧跟类型之后,下面声明创建的 CarmysteryVehicle 部署了 StreetLegal 协议

// main.m
#import <Foundation/Foundation.h>
#import "Bicycle.h"
#import "Car.h"
#import "StreetLegal.h"

int main(int argc, const char * argv[]) {  
    @autoreleasepool {
        id <StreetLegal> mysteryVehicle = [[Car alloc] init];
        [mysteryVehicle signalLeftTurn];

        mysteryVehicle = [[Bicycle alloc] init];
        [mysteryVehicle signalLeftTurn];
    }
    return 0;
}

不论 Car 和 Bicycle 的父类是什么,他们都可以遵循相同的 StreetLegal 协议。我们可以使用 conformsToProtocol: 方法来判断对象是否遵循指定的协议:

if ([mysteryVehicle conformsToProtocol:@protocol(StreetLegal)]) {  
    [mysteryVehicle signalStop];
    [mysteryVehicle signalLeftTurn];
    [mysteryVehicle signalRightTurn];
}

Protocols In The Real World 真实世界中的协议

在 iOS 和 OS X 的应用开发中,任何应用的入口都是由 application delegate 对象来处理程序生命周期中许多事件,比起让 delegate 继承自某一特定的父类,UIKit 框架部署了一个协议:

@interface YourAppDelegate : UIResponder <UIApplicationDelegate>

只要响应 UIApplicationDelegate 中定义的方法,我们可以使用任意对象作为 application delegate

Categories

类别可以将单个类分散到多个文件中,这样可以缓解维护一个超大代码块所带来的复杂度,也更加模块化。不然想在一个超过一万行的代码中导航到我们需要的地方,想想也是醉了。

Using multiple files to implement a class

这部分我们使用 category 来扩展现有的类,而不用去打扰原有的类。然后我们将会学习如何模拟 protected 方法,Extensions 类似于 categories,让我们一探究竟

Setting Up

开始前先做一点基础工作,我们需要一个 base class:

// Car.h
#import <Foundation/Foundation.h>

@interface Car : NSObject

@property (copy) NSString *model;
@property (readonly) double odometer;

- (void)startEngine;
- (void)drive;
- (void)turnLeft;
- (void)turnRight;

@end

相应的实现也是针对不同方法输出些 log 罢了:

// Car.m
#import "Car.h"

@implementation Car

@synthesize model = _model;

- (void)startEngine {
    NSLog(@"Starting the %@'s engine", _model);
}
- (void)drive {
    NSLog(@"The %@ is now driving", _model);
}
- (void)turnLeft {
    NSLog(@"The %@ is turning left", _model);
}
- (void)turnRight {
    NSLog(@"The %@ is turning right", _model);
}

@end

现在我们想要添加一些有关汽车维修的方法,可以用将其放在专门的 category 中

Creating Categories

选择 Objective-C category template 创建 Car 的类别 Maintenance。完成后在 Car+Maintenance.h 文件中添加些代码

// Car+Maintenance.h
#import "Car.h"

@interface Car (Maintenance)

- (BOOL)needsOilChange;
- (void)changeOil;
- (void)rotateTires;
- (void)jumpBatteryUsingCar:(Car *)anotherCar;

@end

在运行时 runtime,这些方法将变成 Car 类的一部分,即使他们声明在不同的文件中。只要他们都定义在 Car.h 中你都可以访问到。

category implementation 和标准的 implementation 并没有太大区别,除了 category name 出现在圆括号之中:

// Car+Maintenance.m
#import "Car+Maintenance.h"

@implementation Car (Maintenance)

- (BOOL)needsOilChange {
    return YES;
}
- (void)changeOil {
    NSLog(@"Changing oil for the %@", [self model]);
}
- (void)rotateTires {
    NSLog(@"Rotating tires for the %@", [self model]);
}
- (void)jumpBatteryUsingCar:(Car *)anotherCar {
    NSLog(@"Jumped the %@ with a %@", [self model], [anotherCar model]);
}

@end

值得注意的一点是 category 可以用来覆盖基类中原有的一些方法,但是永远不要这么去做,category 是一种扁平化的结构,如果你有多个类别,每个类别都做了覆盖,那么 Objective-C 不会知道你到底要调用那个版本,所以这种情况还是使用子类化比较好吧。

Using Categories

想要使用 category 中定义的 API,首先还是需要导入的,在导入 Car+Maintenance.h 之后,我们就可以使用里面的所有方法了:

// main.m
#import <Foundation/Foundation.h>
#import "Car.h"
#import "Car+Maintenance.h"

int main(int argc, const char * argv[]) {  
    @autoreleasepool {
        Car *porsche = [[Car alloc] init];
        porsche.model = @"Porsche 911 Turbo";
        Car *ford = [[Car alloc] init];
        ford.model = @"Ford F-150";

        // "Standard" functionality from Car.h
        [porsche startEngine];
        [porsche drive];
        [porsche turnLeft];
        [porsche turnRight];

        // Additional methods from Car+Maintenance.h
        if ([porsche needsOilChange]) {
            [porsche changeOil];
        }
        [porsche rotateTires];
        [porsche jumpBatteryUsingCar:ford];
    }
    return 0;
}

“Protected” Methods

categories 是一种强大的组织工具,任意文件都可以根据自身需要有选择性地载入对应的 category,从而使用其中暴露的 API,而对其他未载入 category 的文件来说,这些 API 是隐形的。

// Car+Protected.h
#import "Car.h"

@interface Car (Protected)

- (void)prepareToDrive;

@end
// Car+Protected.m
#import "Car+Protected.h"

@implementation Car (Protected)

- (void)prepareToDrive {
    NSLog(@"Doing some internal work to get the %@ ready to drive",
          [self model]);
}

@end

上面定义了一个名为 Protectedcategory,我们只在 Car 和他的子类 Coupe 的实现文件 .m 中导入(注意不能在 .h 中导入,否则就成立公开的了

// Car.m
#import "Car.h"
#import "Car+Protected.h"

@implementation Car
...
- (void)drive {
    [self prepareToDrive];
    NSLog(@"The %@ is now driving", _model);
}
...

创建 Car 的子类 Coupe

// Coupe.h
#import "Car.h"

@interface Coupe : Car
// Extra methods defined by the Coupe subclass
@end
// Coupe.m
#import "Coupe.h"
#import "Car+Protected.h"

@implementation Coupe

- (void)startEngine {
    [super startEngine];
    // Call the protected method here instead of in `drive`
    [self prepareToDrive];
}

- (void)drive {
    NSLog(@"VROOOOOOM!");
}

@end

现在我们可以通过 [ford drive][porsche startEngine] 来调用 protected 方法 prepareToDrive,但是如果你直接调用,编译器会报错

// main.m
#import <Foundation/Foundation.h>
#import "Car.h"
#import "Coupe.h"

int main(int argc, const char * argv[]) {  
    @autoreleasepool {
        Car *ford = [[Car alloc] init];
        ford.model = @"Ford F-150";
        [ford startEngine];
        [ford drive]; // Calls the protected method

        Car *porsche = [[Coupe alloc] init];
        porsche.model = @"Porsche 911 Turbo";
        [porsche startEngine]; // Calls the protected method
        [porsche drive];

        // "Protected" methods have not been imported,
        // so this will *not* work
        // [porsche prepareToDrive];

        SEL protectedMethod = @selector(prepareToDrive);
        if ([porsche respondsToSelector:protectedMethod]) {
            // This *will* work
            [porsche performSelector:protectedMethod];
        }


    }
    return 0;
}

最后注意,如果我们使用 performSelector: 在运行时动态执行 prepareToDrive 是 OK 的,因为 Objective-C 中的所有方法都是公开的,并不能真正隐藏掉这些代码,Categories 仅仅是采用一种常规的方式来控制 API 的哪些部分对其他文件可见。

Extensions

Extensions 类似于 categories,让你添加一些方法到 Class 的主接口之外。但是相比于 categories,extension’s API 必须在 main implementation 中实现,而不能在 category 里去实现。

还记得我们之前是如何添加私有方法的吗?在 implementation 中添加方法,而不是头文件中。不过这只适合添加少量的私有方法,如果你有一个庞大的类就不太适合了。Extensions 让你可以正式声明你的私有方法。

比如要添加私有方法 engineIsWorking ,可以先在 Car.m 之上定义一个 extension,然后在 main @implementation 代码区去实现该方法:

// Car.m
#import "Car.h"

// The class extension
@interface Car ()
- (BOOL)engineIsWorking;
@end

// The main implementation
@implementation Car

@synthesize model = _model;

- (BOOL)engineIsWorking {
    // In the real world, this would probably return a useful value
    return YES;
}
- (void)startEngine {
    if ([self engineIsWorking]) {
        NSLog(@"Starting the %@'s engine", _model);
    }
}
...
@end

除了声明私有 API,extensions 还能用来重新声明之前公共接口的属性,通常用来将之前的 readonly 变成 readwrite

// Car.m
#import "Car.h"

@interface Car ()
@property (readwrite) double odometer;
- (BOOL)engineIsWorking;
@end
...

Blocks

Blocks 是 Objective-C 中的匿名函数,可以被用做对象间的数据传递。

Creating Blocks

Blocks 使用和函数一样的机理,你可以像声明一个函数那样声明一个 block 变量,然后定义并执行

// main.m
#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {  
    @autoreleasepool {
        // Declare the block variable
        double (^distanceFromRateAndTime)(double rate, double time);

        // Create and assign the block
        distanceFromRateAndTime = ^double(double rate, double time) {
            return rate * time;
        };
        // Call the block
        double dx = distanceFromRateAndTime(35, 1.5);

        NSLog(@"A car driving 35 mph will travel "
              @"%.2f miles in 1.5 hours.", dx);
    }
    return 0;
}

^ 用来标明 distanceFromRateAndTime 变量是一个 block,如同函数一样,需要包含参数、返回值,这样编译器可以确保类型安全。

block 本质上是匿名函数,^double(double rate, double time){return rate * time;}; 其实是一个没有名字的函数,在将其分配给了 distanceFromRateAndTime 变量之后,我们可以使用变量来调用此函数。

Parameterless Blocks

如果不带参数,那么可以将参数完全省略,返回类型也可以省略,最终简化为 ^{ ... }

double (^randomPercent)(void) = ^ {  
    return (double)arc4random() / 4294967295;
};
NSLog(@"Gas tank is %.1f%% full",  
      randomPercent() * 100);

内建的 arc4random() 可能返回的最大值是 4294967295,我们用来做除数,这样就得到了 0 ~ 1 之间的随机数。

Closures

block 本质上是匿名函数,可以在 block 内部访问外部的非局部变量(non-local variables),例如 getFullCarName 可以引用 block 前面定义的 make 变量

NSString *make = @"Honda";  
NSString *(^getFullCarName)(NSString *) = ^(NSString *model) {  
    return [make stringByAppendingFormat:@" %@", model];
};
NSLog(@"%@", getFullCarName(@"Accord"));    // Honda Accord  

非局部变量是作为常数变量拷贝并存储在 block 内部的,也就是说他们是只读的,如果尝试为内部的 make 变量赋一个新值,block 会抛出一个编译错误。

Accessing non-local variables as const copies

事实上 block 针对局部变量的拷贝其实是创建了一个快照(snapshot),Non-local variables 中的值在 block 中被冰冻了起来,block 总是使用该值,即使 Non-local variables 稍后会在 block 外发生变化,block 中的值仍然保持不变。观察下面的例子我们尝试改变 make 变量的值

NSString *make = @"Honda";  
NSString *(^getFullCarName)(NSString *) = ^(NSString *model) {  
    return [make stringByAppendingFormat:@" %@", model];
};
NSLog(@"%@", getFullCarName(@"Accord"));    // Honda Accord

// Try changing the non-local variable (it won't change the block)
make = @"Porsche";  
NSLog(@"%@", getFullCarName(@"911 Turbo")); // Honda 911 Turbo  

Mutable Non-Local Variables

将 non-local variables 在 block 中冰冻起来使用,是默认选项,他确保了安全。但是我们可以改变这一行为,使用 __block 来修改

__block NSString *make = @"Honda";  

他告诉 block 捕获变量的引用,直接创建 block 内部变量和外部 make 变量直接的引用,这样如果在外部为 make 赋新值,那么 block 内部的关联变量也会发生改变

Accessing non-local variables by reference

类似于 static local variables__block 被看做是在 block 间传递的一块内存。由此我们可以使用 block 来做 generators,例如下面的代码片段,i 在每次累加完成后都会记录下来

__block int i = 0;  
int (^count)(void) = ^ {  
    i += 1;
    return i;
};
NSLog(@"%d", count());    // 1  
NSLog(@"%d", count());    // 2  
NSLog(@"%d", count());    // 3  

Blocks as Method Parameters

虽然将 block 赋值给一个变量看上去不错,但在真实世界中,我们更多地是将其作为方法参数。

下面的方法 Car 的接口声明了一个记录汽车行使距离的方法,他的第二个速度参数就是一个 block(接受一个时间作为 block 的参数),block 的数据类型是 double (^)(double time)

// Car.h
#import <Foundation/Foundation.h>

@interface Car : NSObject

@property double odometer;

- (void)driveForDuration:(double)duration
       withVariableSpeed:(double (^)(double time))speedFunction
                   steps:(int)numSteps;

@end

在 Car 的实现中,我们可以使用 speedFunction 来执行 block

// Car.m
#import "Car.h"

@implementation Car

@synthesize odometer = _odometer;

- (void)driveForDuration:(double)duration
       withVariableSpeed:(double (^)(double time))speedFunction
                   steps:(int)numSteps {
    double dt = duration / numSteps;
    for (int i=1; i<=numSteps; i++) {
        _odometer += speedFunction(i*dt) * dt;
    }
}

@end

在 main 函数中可以看到,block 作为参数通常在调用时定义实现,这样也更加符合直觉

// main.m
#import <Foundation/Foundation.h>
#import "Car.h"

int main(int argc, const char * argv[]) {  
    @autoreleasepool {
        Car *theCar = [[Car alloc] init];

        // Drive for awhile with constant speed of 5.0 m/s
        [theCar driveForDuration:10.0
               withVariableSpeed:^(double time) {
                           return 5.0;
                       } steps:100];
        NSLog(@"The car has now driven %.2f meters", theCar.odometer);

        // Start accelerating at a rate of 1.0 m/s^2
        [theCar driveForDuration:10.0
               withVariableSpeed:^(double time) {
                           return time + 5.0;
                       } steps:100];
        NSLog(@"The car has now driven %.2f meters", theCar.odometer);
    }
    return 0;
}

Defining Block Types

我们可以用 typedef 来简化 block 的声明,如下今后我们直接使用 SpeedFunction 就好了

// Car.h
#import <Foundation/Foundation.h>

// Define a new type for the block
typedef double (^SpeedFunction)(double);

@interface Car : NSObject

@property double odometer;

- (void)driveForDuration:(double)duration
       withVariableSpeed:(SpeedFunction)speedFunction
                   steps:(int)numSteps;

@end

Exceptions & Errors

Exceptions 表示程序员级别的 bugs,比如数组越界,通常用来通知开发者意外发生,因此 exceptions 通常发生在生产代码中。

另一方面,errors 是用户级别的错误,类似于尝试加载的文件不存在。因为 error 是程序执行时产生的异常,因此你需要手动去检查这些条件并通知用户错误发生,多数情况下并不会导致程序崩溃。

Exceptions

异常通常由 NSException 类来表示,他设计采用一个通用的方式封装异常数据,你很少子类化或自定义一个 exception 对象。NSException 类有三个属性:

  • name:异常的标识(一个 NSString 对象)
  • reason:包含对异常的描述
  • userInfo:一个 NSDictionary 对象(key-value)包含一些关于异常额外的信息

异常通常用来在开发早期处理一些严重错误,如果需要尝试处理一些可能出现的问题,那么还是用 error 对象比较合适。

处理 Exceptions

异常可以使用 try-catch-finally 模式来处理,将可能会出问题的代码放进 @try 中,如果发生问题,相应的 @catch() 代码块将被执行来处理问题。最后无论发没发生错误,都执行 @finally 代码块。

以下例子中,如果 @try 中的数组元素不存在,我们会在 @catch() 中显示异常细节:

// main.m
#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {  
    @autoreleasepool {
        NSArray *inventory = @[@"Honda Civic",
                               @"Nissan Versa",
                               @"Ford F-150"];
        int selectedIndex = 3;
        @try {
            NSString *car = inventory[selectedIndex];
            NSLog(@"The selected car is: %@", car);
        } @catch(NSException *theException) {
            NSLog(@"An exception occurred: %@", theException.name);
            NSLog(@"Here are some details: %@", theException.reason);
        } @finally {
            NSLog(@"Executing finally block");
        }
    }
    return 0;
}

Objective-C 的 @try/@catch() 并不高效,不要用来替代传统的循环控制工具,尽可能地使用条件检测语句(if 语句)

因此上面的代码并不经常使用,我们一般使用如下代码来替代:

if (selectedIndex < [inventory count]) {  
    NSString *car = inventory[selectedIndex];
    NSLog(@"The selected car is: %@", car);
} else {
    // Handle the error
}

内建的 Exceptions

iOS 和 OS X 的标准库中都内建了许多异常,最常用的如下:

  • NSRangeException
  • NSInvalidArgumentException
  • NSInternalInconsistencyException
  • NSGenericException

注意:这些都是 Exception Name(NSString 对象),并不是 NSException 的子类,因此需要判定某一类型的 exception,需要这么写:

...
} @catch(NSException *theException) {
    if (theException.name == NSRangeException) {
        NSLog(@"Caught an NSRangeException");
    } else {
        NSLog(@"Ignored a %@ exception", theException);
        @throw;
    }
} 
...

自定义异常

你可以使用 @throw 来抛出一个携带自定义数据的异常对象(NSException),最简单的方式就是使用 exceptionWithName:reason:userInfo: 来创建一个 NSException 实例。下面的例子在全局函数中抛出了一个异常,然后在 main 函数中去捕获他:

// main.m
#import <Foundation/Foundation.h>

NSString *getRandomCarFromInventory(NSArray *inventory) {  
    int maximum = (int)[inventory count];
    if (maximum == 0) {
        NSException *e = [NSException
                          exceptionWithName:@"EmptyInventoryException"
                          reason:@"*** The inventory has no cars!"
                          userInfo:nil];
        @throw e;
    }
    int randomIndex = arc4random_uniform(maximum);
    return inventory[randomIndex];
}

int main(int argc, const char * argv[]) {  
    @autoreleasepool {
        @try {
            NSString *car = getRandomCarFromInventory(@[]);
            NSLog(@"The selected car is: %@", car);
        } @catch(NSException *theException) {
            if (theException.name == @"EmptyInventoryException") {
                NSLog(@"Caught an EmptyInventoryException");
            } else {
                NSLog(@"Ignored a %@ exception", theException);
                @throw;
            }
        }
    }
    return 0;
}

除非必要,没有必要真么做,一是异常表示编程错误,二是 @throw 操作开销比较大,还是用 if 来判断错误比好。

Errors

错误也表示一种错误,但不至于导致程序崩溃,他的应用范围也比异常广,通过对错误的正常检查是提高代码质量的表现。

NSError 类封装了失败操作的细节。主要特性类似于 NSException,主要属性如下:

  • domain
  • code
  • userInfo

userInfo 字典包含了比 NSException 字典更多的信息,下面是i一些预设的 key

  • NSLocalizedDescriptionKey
  • NSLocalizedFailureReasonErrorKey
  • NSUnderlyingErrorKey

处理错误

Errors 不需要语言提供形如 @try@catch() 的结构。许多方法被设置为接收一个间接引用的 NSError 对象。一个间接引用是指向指针的指针对象,通常指向一个全新的 NSError 实例。你可以使用 (NSError **)error 来接受该间接实例。

下面的例子展示了载入的文件不存在时的情形 stringWithContentsOfFile:encoding:error:

// main.m
#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {  
    @autoreleasepool {
        NSString *fileToLoad = @"/path/to/non-existent-file.txt";

        NSError *error;
        NSString *content = [NSString stringWithContentsOfFile:fileToLoad
                                                      encoding:NSUTF8StringEncoding
                                                         error:&error];

        if (content == nil) {
            // Method failed
            NSLog(@"Error loading file %@!", fileToLoad);
            NSLog(@"Domain: %@", error.domain);
            NSLog(@"Error Code: %ld", error.code);
            NSLog(@"Description: %@", [error localizedDescription]);
            NSLog(@"Reason: %@", [error localizedFailureReason]);
        } else {
            // Method succeeded
            NSLog(@"Content loaded!");
            NSLog(@"%@", content);
        }
    }
    return 0;
}

注意只有在该方法获取的内容 content 为 nil 时,才应该尝试访问 NSError 的引用,而且不要用 NSError 来判断是否成功或失败。

内建的错误

类似于 NSException, NSError 用来设计成为一个通用的对象来代表错误。框架通常都有内置的错误,比如:

NSMachErrorDomain  
NSPOSIXErrorDomain  
NSOSStatusErrorDomain  
NSCocoaErrorDomain  

一旦你决定了 error domain,你可以检查一个特定的错误代码

...
if (content == nil) {  
    if ([error.domain isEqualToString:@"NSCocoaErrorDomain"] &&
        error.code == NSFileReadNoSuchFileError) {
        NSLog(@"That file doesn't exist!");
        NSLog(@"Path: %@", [[error userInfo] objectForKey:NSFilePathErrorKey]);
    } else {
        NSLog(@"Some other kind of read occurred");
    }
} 
...

自定义的错误

在一个大工程中,最佳实践是在头文件中定义一些错误,例如一个 InventoryErrors.h 文件定义了一个包含各种错误代码的域

NSString *InventoryErrorDomain = @"com.RyPress.Inventory.ErrorDomain";

enum {  
    InventoryNotLoadedError,
    InventoryEmptyError,
    InventoryInternalError
};

技术上,自定义的错误域可以是任何你想要的,但是推荐的方式是 com.<Company>.<Framework-or-project>.ErrorDomain。正如 InventoryErrorDomain,该枚举对象定义了错误代码常量。

使用起来也很简单,在方法或函数的参数中加入 NSError **error

// main.m
#import <Foundation/Foundation.h>
#import "InventoryErrors.h"

NSString *getRandomCarFromInventory(NSArray *inventory, NSError **error) {  
    int maximum = (int)[inventory count];
    if (maximum == 0) {
        if (error != NULL) {
            NSDictionary *userInfo = @{NSLocalizedDescriptionKey: @"Could not"
            " select a car because there are no cars in the inventory."};

            *error = [NSError errorWithDomain:InventoryErrorDomain
                                         code:InventoryEmptyError
                                     userInfo:userInfo];
        }
        return nil;
    }
    int randomIndex = arc4random_uniform(maximum);
    return inventory[randomIndex];
}

int main(int argc, const char * argv[]) {  
    @autoreleasepool {
        NSArray *inventory = @[];
        NSError *error;
        NSString *car = getRandomCarFromInventory(inventory, &error);

        if (car == nil) {
            // Failed
            NSLog(@"Car could not be selected");
            NSLog(@"Domain: %@", error.domain);
            NSLog(@"Error Code: %ld", error.code);
            NSLog(@"Description: %@", [error localizedDescription]);

        } else {
            // Succeeded
            NSLog(@"Car selected!");
            NSLog(@"%@", car);
        }
    }
    return 0;
}

内存管理

对象所有权机制的实现是通过引用计数系统,其内部跟踪每个对象有多少拥有者。当你宣称一个对象的所有权时,就增加该对象的引用计数。当不拥有该对象时,则释放引用计数。引用计数为 0 时,对象被销毁。

过去,人们通过定义在 NSObject protocol 中的内存管理方法来手动控制管理对象的引用计数。Xcode 4.2 带来了 ARC

手动保持释放

在手动保持释放环境中,你的工作是声明并放弃对象的所有权。主要通过下面的方式:

Method Behavior
alloc Create an object and claim ownership of it.
retain Claim ownership of an existing object.
copy Copy an object and claim ownership of it.
release Relinquish ownership of an object and destroy it immediately.
autorelease Relinquish ownership of an object but defer its destruction.

你要平衡保持和释放

The alloc Method

alloc 方法除了为对象分配内存外,还会将其引用加 1

// main.m
#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {  
    @autoreleasepool {
        NSMutableArray *inventory = [[NSMutableArray alloc] init];
        [inventory addObject:@"Honda Civic"];
        NSLog(@"%@", inventory);
    }
    return 0;
}

上面的代码我们没有释放引用,因此会导致内存泄露,用 Product > Analyze 来分析的话

The release Method

release 方法释放对象的所有权,引用计数减 1

[inventory release];

释放过头了会导致悬空指针的出现

The retain Method

retain 方法为一个现存的对象宣称所有权。比如,我们可以使用 retain 来创建一个强引用

// CarStore.h
#import <Foundation/Foundation.h>

@interface CarStore : NSObject

- (NSMutableArray *)inventory;
- (void)setInventory:(NSMutableArray *)newInventory;

@end

我们为属性 inventory 声明了存取方法

// CarStore.m
#import "CarStore.h"

@implementation CarStore {
    NSMutableArray *_inventory;
}

- (NSMutableArray *)inventory {
    return _inventory;
}

- (void)setInventory:(NSMutableArray *)newInventory {
    _inventory = newInventory;
}

@end

回到 main.m 中,让我们将变量 inventory 分配给 CarStore 的 inventory 属性:

// main.m
#import <Foundation/Foundation.h>
#import "CarStore.h"

int main(int argc, const char * argv[]) {  
    @autoreleasepool {
        NSMutableArray *inventory = [[NSMutableArray alloc] init];
        [inventory addObject:@"Honda Civic"];

        CarStore *superstore = [[CarStore alloc] init];
        [superstore setInventory:inventory];
        [inventory release];

        // Do some other stuff...

        // Try to access the property later on (error!)
        NSLog(@"%@", [superstore inventory]);
    }
    return 0;
}

最后一行 [superstore inventory] 调用时,inventory 已经被提前释放掉了,为了避免这种情况

// CarStore.m
- (void)setInventory:(NSMutableArray *)newInventory {
    _inventory = [newInventory retain];
}

不过,当我们传递一个新值时,旧值会永远无法访问,意味着这块内存不会被释放,因此需要在赋新值时,释放旧值:

// CarStore.m
- (void)setInventory:(NSMutableArray *)newInventory {
    if (_inventory == newInventory) {
        return;
    }
    NSMutableArray *oldValue = _inventory;
    _inventory = [newInventory retain];
    [oldValue release];
}

上图是内存管理的过程,所有的分配和释放内存都保持着平衡。

The copy Method

另一种方式的 retain 是 copy,他创建了一个全新的实例对象,并增加了其引用计数,并不会影响原对象。

// CarStore.m
- (void)setInventory:(NSMutableArray *)newInventory {
    if (_inventory == newInventory) {
        return;
    }
    NSMutableArray *oldValue = _inventory;
    _inventory = [newInventory copy];
    [oldValue release];
}

The autorelease method

autorelease 推迟了释放对象的时机

// CarStore.h
+ (CarStore *)carStore;

// CarStore.m
+ (CarStore *)carStore {
    CarStore *newStore = [[CarStore alloc] init];
    return [newStore autorelease];
}

释放推迟到最近的 @autoreleasepool{} 结束后。所有内建的工厂方法,例如 NSString 的 stringWithFormat:stringWithContentsOfFile: 内部实现原理与 carStore 方法相同。在 ARC 之前这是通用的做法,不用担心忘记 release 了。

如果你将 superstore 的构造方法改成如下,你不必在 main() 结尾释放它

// main.m
CarStore *superstore = [CarStore carStore];  

事实上,你不允许释放 superstore 实例,因为你不再拥有它,而是 carStore 工厂方法拥有

The dealloc Method

该方法相对于 init,在对象销毁前做一些清理工作,该方法会自动被 runtime 执行,不应该直接调用

// CarStore.m
- (void)dealloc {
    [_inventory release];
    [super dealloc];
}

Automatic Reference Counting

ARC 环境下不再需要内存管理方法了,dealloc 方法仅在一些低等级的分配内存函数时使用,比如 malloc()