了解 Objective-C
Objective_C 是一种面向对象的语言。但与jave、C++等语言不同,它使用了消息结构(messaging structure)而非函数调用(function calling)。Objective-C由Smalltalk演化而来,后者是消息语言的鼻祖。
消息与函数调用的区别看上去就像这样:
// Messaging (Objective-C)
Object* obj = [Object new];
[obj performWith:parameterl and:parameter2];
// Function calling (C++)
Object* obj = new Object;
obj->perform(parameter1, parameter2);
关键区别在于:使用消息结构的语言,其运行时所应执行的代码有运行环境来决定;而使用函数调用的语言,则由编译器决定。
运行期组件(runtime component)
Objective-C的重要工作都由运行期组件(runtime component)而非编译器来完成。使用Objective-C的面向对象特性所需的全部数据结构及函数都在运行期组件里面。
运行期组件本质上就是一种与开发者所编代码相链接的“动态库”(dynamic library),其代码能把开发者编写的所有程序粘合起来。这样只需更新运行期组件,即可提升应用程序性能。
Objective-C内存模型
若要理解内存模型,则需明白:Objectivec-C语言中的指针是用来指示对象的。想要声明一个变量,令其指代某个对象,可用以下语法:
NSString *someString = @"The string";
它声明了一个名为someString的变量,其类型是NSString*。即此变量是指向NSString的指针。所有Objective-C语言的对象都必须这样声明,因为对象所占内存总是分配在“堆空间”(heap space)中,而绝不会分配在“栈”(stack)上。
someString变量指向分配在堆里的某块内存,其中含有一个NSString对象。也就是说,如果再创建一个变量,令其指向同一地址,那么并不拷贝该对象,只是这两个对象会同时指向此对象:
NSString *someString = @"The string";
NSString *anotherString = someString;
此时由两个NSString*型变量指向一个NSString实例。如下图:
分配在堆中的内存必须直接管理,而分配在栈上用于保存变量的内存则会在其栈桢弹出时自动清理。
Objective-C运行期环境吧堆内存管理工作抽象为一套内存管理结构,名叫“引用计数”。
结构体
与创建对象相比,创建结构体可以减少许多开销,例如分配及释放堆内存等。如果只需保存int、float、double、char等“非对象类型”,那么通常使用结构体就可以了。
例如CGRect,其定义是:
struct CGRect {
CGPoint origin;
CGSize size;
};
typedef struct CGRect CGRect;
要点
- Objective-C为C语言添加了面向对象特性,是其超急。Objective-C使用动态绑定的消息结构,也就是说,在运行时才会检查对象类型。接收一条消息之后,究竟应执行何种代码,由运行期环境而非编译器来决定。
- 理解C语言的核心概念有助于写好Objective-C程序。尤其要掌握内存模型与指针。
在类的头文件中尽量少引入其他头文件
与C和C++一样,Objective-C也使用“头文件”(header file)与“实现文件”(implementation file)来区隔代码。用Objective-C语言编写“类”的标准方式为:以类名做文件名,分别创建两个文件,头文件后缀用.h,实现文件后缀用.m。创建好一个类之后,其代码看上去如下所示:
// EOCPerson.h
#import <Foundation/Foundation.h>
@interface EOCPerson : NSObject
@property (nonattomic, copy) NSString *firstName;
@property (nonattomic, copy) NSString *lastName;
@end
//EOCPerson.m
#import "EOCPerson.h"
@implementation EOCPerson
// Implementation of methods
@end
向前声明
如果又创建一个名为EOCEmployer的新类,然后为EOCPerson类添加这个属性。
// EOCPerson.h
#import <Foundation/Foundation.h>
@interface EOCPerson : NSObject
@property (nonattomic, copy) NSString *firstName;
@property (nonattomic, copy) NSString *lastName;
@property (nonatomic, strong) EOCEmployer *employer;
@end
此时比起在EOCPerson.h
下加入下面这行:
#import "EOCEmployer.h"
更推荐使用下面方法:
@class EOCEmployer;
这个方法叫做“向前声明”(forward declaring)该类。现在EOCPerson的头文件变成了这样:
// EOCPerson.h
#import <Foundation/Foundation.h>
@class EOCEmployer;
@interface EOCPerson : NSObject
@property (nonattomic, copy) NSString *firstName;
@property (nonattomic, copy) NSString *lastName;
@property (nonatomic, strong) EOCEmployer *employer;
@end
之所以使用向前声明,是因为在编译一个使用了EOCPerson类的文件时,不需要知道EOCEmployer类的全部细节,只需要知道有一个类名叫EOCEmploer就好。
EOCPerson类的实现文件则需引入EOCEmployer类的头文件,因为若要使用后者,则必须知道其所有接口细节。于是,实现文件就是:
//EOCPerson.m
#import "EOCPerson.h"
#import "EOCEmployer.h"
@implementation EOCPerson
// Implementation of methods
@end
尽量将引入头文件的时机延后,只在确有需要时才引入,这样就可以减少类的使用者所需引入头文件数量,减少编译时间。
相互引用
向前声明也解决了两个类相互引用的问题。
当编译器解析一个头文件时,编译器会发现它引入了另一个头文件,而那个头文件又回过头来引用第一个头文件,就会导致“循环引用”(chicken-and-egg situation)。使用#import而非#include指令虽然不会导致死循环,但这却意味着两个类里有一个无法被正确编译。
必须引入头文件
但是有时候必须要在头文件中引入其他头文件。如果你写的类继承自某个超类,则必须引入定义那个超类的头文件。同理,如果要声明你写的类尊从某个协议(protocol),那么该协议必须有完整定义,且不能使用向前声明。
例如,要从图形类中继承一个矩形类,且令其遵循绘制协议:
// EOCRectangle.h
#import "EOCShape.h"
#import "EOCDrawable.h"
@interface EOCRectangle : EOCShape<EOCDrawable>
@property (nonatomic, assign) float width;
@property (nonatomic, assign) float height;
@end
此时第二条#import是难免的。鉴于此,最好是把协议单独放在一个头文件中,以避免相互依赖问题。
然而有些协议,例如“委托协议”,就不用单独写一个头文件了。在那种情况下,协议之一与接收协议委托的类放在一起定义才有意义。此时,我们可以把在实现文件中声明此类实现了该委托协议,并把实现代码放在“class-continuation 分类”里。这样的话,只要在实现文件中引入包含委托协议的头文件即可,而不需要将其放在公共头文件里。
要点
- 厨房确有必要,否则不要引入头文件。一般来说,应在某个类的头文件中使用向前声明来提及别的类,并在实现文件中引入那些类的头文件。这样做可以尽量降低类之间的耦合(coupling)。
- 有时无法使用向前声明,比如要声明某个类遵循某一项协议。这种情况下,尽量把“该类遵循某协议”的声明移至“class-continuation分类”中。如果不行的话,就把协议单独放在一个头文件中,然后将其引入。
多用字面量语法,少用与之等价的方法
字符串字面量
不使用alloc及init方法来分配并初始化NSString对象,让语法更简洁。
NSString *someString = @"Effective Objective-C 2.0";
这种语法也可以来声明NSNumber、NSArray、NSDictionary类的实例。
字面数值
NSNumber *intNumber = @1;
NSNumber *floatNumber = @2.5f;
NSNumber *boolNumber = @YES;
NSNumber *charNumber = @'a';
字面量数组
字面量语法创建数组
NSArray *animals = @[@"cat", @"dog", @"mouse", @"badger"];
字面量语法操作数组
NSString *dog = animals[1];
字面量字典
“字典”是一种映射型数据结构,可向其中添加键值对。
字面量字典创建
NSDictionary *personData = @{@"firstName" : @"Matt", @"lastName" : @"Galloway", @"age" : @28};
字面量语法访问
NSString *lastName = personData[@"lastName"];
这样写省去了沉赘的语法,令此行代码简单易读。
局限性
字面量语法除了字符串以外,所创建出来的对象必须属于Foundation框架才行。然而一般来说,标准的实现已经很好了,使用这些已经足够了。
此外,使用字面量语法创建出来的字符串、数组、字典对象都是不可变的(immutable)。若想要可变版本的对象,,则需复制一份:
NSMutableArray *mutable = [@[@1, @2, @3, @4] mutableCopy];
这样做会多调用一个方法,而且还要再创建一个对象,不过使用字面量语法所带来的好处还是多与上述缺点的。
要点
- 应该使用字面量语法来创建字符串、数值、数组、字典。与创建此类对象的常规方法相比,这么做更加简明扼要。
- 应该通过取下标操作来访问数组下标或字典中的键所对应的元素。
- 用字面量语法创建数组或字典时,若值中有nil,则会抛出异常。因此,务必确保值里不含nil。