一、对 C 的扩展
1.#import 语句
1)告知编译器查找指定头文件;
2)保证头文件仅被包含一次。
3)#import" ": 编译器会首先查找项目本地目录是否存在相应的头文件,若不存在,则在库目录中查找系统头文件中是否有匹配项。#import< >: 只会在库目录中的系统头文件中查找是否有匹配项。
4)@import fileName: 编译器会使用Modules的引用方式。
Modules相当于将框架进行了封装。在实际编译之时加入一个用于存放已编译过的Modules列表(可以省去相应文件的编译时间)。如果在编译的文件中引用某个Module,则首先在前述列表内查找,若找到,说明已经被加载过则直接使用已有的,若未找到,则把引用的头文件编译后加入到此表中。这样被引用到的Module只会被编译一次,并且在开发时,不需要的文件不会被意外引用,从而同时解决了编译时间和引用泛滥两方面的问题。
2.框架
将头文件、库、图片、声音等内容聚集在一个独立单元中的集合体。
Foundation 框架处理的是用户界面之下的那些层的特性,例如数据结构和通信机制。
3.NSLog()和@“字符串”
NSLog() 类似于 printf()。
使用前缀可以避免名称冲突。NS前缀表示此函数来自于Cocoa。
@“字符串”表示将引号内的字符串作为 NSString 元素来处理。
使用 NSLog() 输出任意对象的值时,使用 %@ 格式:
NSLog(@"%@",@"Hello World\n");
在类中提供 description() 方法就可以自定义 NSLog() 输出对象的方式。
NSLog 与 printf 的区别:
- 在 debug 模式下,NSLog 的输出会被写入 system.log 文件,而 printf 不具备日志属性。
- NSLog 会自动添加换行符,printf需要手动添加换行符。
- NSLog 会自动加上时间和进程信息,printf 仅将输入的内容输出,不会添加额外信息。
- NSLog 使用 NSString* 格式,而 printf 使用 const char* 格式。
- NSLog支持使用 %@ 打印对象类型,而 printf 不支持。
格式指定符:
4.BOOL 类型
通过 #define 指令将 YES 定义为1,将 NO定义为0。
在 32bit 系统中:
BOOL 为 signed char 的别名,为 8 位二进制数。因此若将长度大于 1 字节的整型值赋予 BOOL 变量,则编译器会截取低八位数值赋予变量。
因此,BOOL类型不仅仅只具有 YES/NO 两个值,非零值并不等于 YES,所以不要将 BOOL 变量直接与YES/NO 进行比较。
在 64bit 系统中:
BOOL 为 bool.
NSInteger & NSUInteger:
二、面向对象编程OOP
面向对象编程以数据为中心,函数为数据服务。
过程式编程建立在函数之上,数据为函数服务。
1.间接(indirection)
将代码段A封装为代码B,若代码C需要使用代码段A,可直接调用代码B。以后可以在不更改代码C的情况下更新代码A。
2.id
id 是一种泛型,表示一个可以指向任何类型的对象的指针,可用来引用任何类型的对象。例如下述的 shapes 用于指代几种描述不同形状的类,而 id 可直接用来作为 shapes 的类型。
void drawShapes (id shapes[ ], int count);
3.发送消息 / 调用方法
[shape draw]:通知名称为 shape 的对象执行 draw 操作。
方括号中第一项是对象,其余部分是需要对象执行的操作。
也可以将类当作对象来发送消息。
消息(message):对象可以执行的操作,用于通知对象去做什么。对象接收到消息后,将查询自身所属的类,以便找到相应的方法(method)来执行。
方法调度(method dispatcher): 用于推测执行什么方法以响应某个特定消息。
4.接口(interface, API)
类为对象提供的特性描述。接口展示了类的结构。
接口不提供接口的实现代码的细节信息。
@interface Circle: 表明这是新类 Circle 的接口。
若未声明成员变量,则可以省略{ }。
每个参数前面的字符串(包括冒号),均为方法名称的一部分。
@interface Circle : NSObject{
int radius;
}
- (void)draw:(int)radius; //方法声明
// 方法的名称为 setNum:atIndex:
- (void)setNum:(int*)number atIndex:(int)index;
@end //Circle
' - ' 用于区分函数原型与方法声明,前者没有先行短线。
最好在 @end 后添加注释来说明类的名称。
5.实现(implementation)
@implementation Circle
- (void) draw: (int) radius {
self->radius = radius;
} //draw
@end //Circle
可以在 @implementation 中定义在 @interface 中未声明过的方法。
在 Objective-C 中不存在真正的私有方法,也无法将某个方法标识为私有方法,从而禁止其他代码调用它。
6.实例化对象
创建一个新的对象,需要向相应的类发送 new 消息。
id shape=[Circle new];
int r=5;
[shape draw:r];
所有的 Objective-C 对象都使用动态分配的内存。
三、继承和复合
继承的类之间的关系是 "is a",即如果“ X 是一个 Y ”,就可以使用继承。
复合的类之间的关系是“has a”,即如果“ X 有 n 个 Y ”,就可以使用复合。
1.继承
Objective-C 不支持多继承,但是可以通过 category、protocol 等特性来达到多继承的效果。
超类==父类 子类==孩子类
对于子类对象而言,其对应的 self 指针指向继承链中第一个类(即最高层的父类)的第一个成员变量。
[super draw:r];
请求编译器向当前类的超类发送消息,若超类中未定义相应的消息,则编译器会沿着继承链继续向上查找。
2.复合
composition: 在对象中可以再引用其他对象。引用其他对象时,可以利用其他对象提供的特性。复合是通过包含其他对象的对象指针实现的。
在Objective-C 中,所有对象间的交互都是通过指针实现的。
真正包含在 Car 中的是其他对象的对象指针,而非实际的对象。
@interface Car: NSObject {
Engine* engine;
Tire* tires[4];
}
-(void) print;
@end //Car
3.存取方法
若要对其他对象的属性进行操作,应尽量使用相应的存取方法来间接的访问,不能直接改变属性值。
accessor method: 用于读取或改变对象的某个属性的方法。
setter method: 用于改变对象属性。命名方法:set+对象属性名称(首字母大写)。
getter method: 用于读取对象属性。命名方法:对象属性名称(首字母小写)。不要将 get 作为 getter 方法名的前缀。 在 Cocoa 中,如果使用 get 作为方法名前缀,就意味着该方法会将传入的参数作为指针来返回值。例如 getBytes: 方法,其参数就是用于存储字节的内存缓冲区的地址。
四、源文件的组织与Xcode的使用
1.源文件的组织
依赖关系(dependency)是两个实体之间的一种关系。若文件A依赖于文件B,则如果文件B发生了变化,那么就需要重新编译文件A以适应这种变化。
类 Car 与类 Tire、类 Engine 之间构成复合关系,后两者分别有自身对应的头文件,在 Car.h 中:
#import <Cocoa/Cocoa.h>
//或者选择导入 Tire.h、Engine.h
@class Tire;
@class Engine;
@interface Car: NSObject
-(Tire*) trieAtIndex: (int) index;
-(Engine*) engine;
@end //Car
@class 创建了一个前向引用,表明:这是一个类,在此处只会通过指针来引用其对象。类的具体实现,在其他文件中。
若类A与类B循环依赖:
则不能使用#import语句来让这两个类互相引用,否则会出现编译错误。
可在A.h中使用@class B,在B.h中使用@class A,那么类A、B就可以相互引用了。
若类A继承自类B,则在 A.h 中便不能使用 @class 语句。
因为在内存中为子类添加成员变量时,它们会被放在超类成员变量的后面。所以编译器需要预先知道所有关于超类的信息才能成功地为子类编译 @interface 部分。
2.Xcode的使用
可以使用导航器面板底部的搜索框来过滤列表文件。
选中文本之后,Control + I 可以用于调整代码格式。
Control + [ 与 Control + ] 可以将选定的代码左移、右移。
代码自动完成列表(即智能提示列表)可通过 esc 键关闭或开启。
将光标放在类名上,之后选择 Edit->Refactor->Rename 选项,便可以替换所有项目文件(可自行选择是否替换某个文件)中的类名。
Shift + Command + O : 用于搜寻项目内容。
在源代码中插入 #pragma mark whatever(可以是任何文字),之后在右侧 Code Review 窄栏,便可以看到提示文字。
按住 option 键并双击某个符号,便可以打开相应的 API 文档。
五、Foundation Kit
Foundation 框架是以 CoreFoundation 框架为基础来创建的。
1.范围与几何数据类型
使用结构体而不是使用对象的原因:
因为 Objective-C 对象是动态分配的,而动态分配开销很大,会降低系统性能。
NSRange:
typedef struct _NSRange {
unsigned int location;
unsigned int length;
}NSRange;
location 可以使用 NSNotFound 来表示没有范围,例如变量未初始化。
创建方式:
1)直接赋值
NSRange range;
range.location = 15;
range.length = 5;
2)使用聚合结构赋值机制
NSRange range = {15,5};
3)快捷创建函数
NSRange range = NSMakeRange(15,5);
好处:可以在任何能够使用函数的地方使用快捷函数,如作为函数实参。
快捷创建函数:CGPointMake()、CGSizeMake()、CGRectMake().
struct CGPoint {
float x;
float y;
};
struct CGSize {
float width;
float height;
};
struct CGRect {
GPoint origin;
CGSize size;
};
2.字符串(NSString)
1)创建字符串
+(id) stringWithFormat: (NSString*) format, ... ;
//可变参数
NSString* height = [NSString stringWithFormat: @"The fan is %d cm",20];
2)类对象、类方法、实例方法
class object:包含了指向超类、类名和类方法列表的指针,还包含了一个 long 类型的数据,以字节为单位指定了新创建的实例对象的大小。
class method:在声明方法时,以 + 作为前缀。此方法属于类对象,不属于类的实例对象,用于创建新的实例或访问全局数据。
factory method:用来创建新对象的类方法,例如 stringWithFormat。
实例方法:在声明方法时,以减号 - 作为前缀。在指定的对象实例中起作用。例如 length 。
类方法与实例方法均可继承。
3)字符串大小、字符串比较
//能够处理各种语言的字符串,如中文、英文等国际字符串。
- (NSUInteger) length;
//检查两个字符串的内容是否相同。
- (BOOL) isEqualToString: (NSString*) aString;
NSString* str1=@"str1";
NSString* str2=@"str2";
if( [str1 isEqualToString : str2] ){
NSLog(@"They are the same.\n");
}
//按照字典序比较接受对象和传递过来的字符串。此函数区分大小写。
- (NSComparisonResult) compare: (NSString*) aString;
//options参数是掩位码,可以使用位或运算符( | )来添加选项标记,常用的选项包括不区分大小写、区分大小写、(比较字符串的长度,而非字符串值)等。
-(NSComparisonResult) compare: (NSString*) aString options: (NSStringCompareOptions) mask;
4)在字符串中查找另外一个字符串
- (BOOL) hasPrefix: (NSString*) aString;
- (BOOL) hasSuffix: (NSString*) aString;
在字符串中搜索指定的字符串,若未找到匹配项,则返回值的 location 元素等于 NSNotFound。
- (NSRange) rangeOfString: (NSString*) aString;
5)可变性
NSString 是不可变的,即一旦被创建,便不能改变其值。
NSString 的子类 NSMutableString 是可变的。
容量仅作为建议,可超过其大小。
+ (id) stringWithCapacity: (NSUInteger) capacity;
在字符串的尾部附加新的字符串。
- (void) appendString: (NSString*) aString;
- (void) appendFormat: (NSString*) format, ... ;
删除字符串中的字符。常与 rangeOfString 结合使用。
- (void) deleteCharactersInRange: (NSRange) aRange;
3.集合
不要创建 NSString、NSArray、NSDictionary 的子类,因为这些类实际上是以类簇(class clusters)的方式实现的,即一群隐藏在通用接口之下的与实现相关的类。可通过复合或者使用类别来实现相关需求。
1)NSArray
两个限制:
1.只能存储 Objective-C 的对象,不能存储 C 语言的基础类型,例如 int、float、enum、struct等,也不能存储 NSArray 中的随机指针。原因如下:因为NSArray和NSDictionary中只能存放对象,而基本类型不是对象。那为什么NSArray和NSDictionary中只能存放对象呢?因为 Objective-C 的对象都是动态分配的,位于堆上。而基本数据类型一般是位于栈上。所以NSArray和NSDictionary中只能存放对象(暂时的想法)。
2.不能存储 nil (对象的零值或 NULL 值)。原因如下:1)数组列表尾部的 nil 代表数组列表结束;2)nextObject 返回 nil 时,循环结束,见下一小节“枚举”。
在 NSArray 列表尾部添加 nil 代表列表结束。数组的最后一个元素必须是 nil。不可变数组,既不能添加元素,也不能删除元素。
NSArray* array1 = [NSArray arrayWithObjects: @"one", @"two", nil];
NSArray* array2 = @[@"one", @"two"]; //字面量语法,结尾处不能添加 nil
//获取数组包含的对象的个数。
- (NSUInteger) count;
//访问数组元素。
NSLog(@"the element: %@, %@", [array1 objectAtIndex: 0], array2[1]);
NSString* str=@"one:two:three:four";
//以指定符号为分隔符切分字符串,而后转换为数组
NSArray* arr=[str componentsSeparatedByString:@":"];
//以指定符号为分隔符合并数组元素,而后转换为字符串
str=[arr componentsJoinedByString:@","];
可变数组:NSMutableArray。不能使用字面量语法创建可变数组。
//容量仅作为建议,可超过其大小。
+ (id) arrayWithCapacity: (NSUInteger) numItems;
//在数组尾部添加对象。
- (void) addObject: (id) anObject;
//删除特定索引处的对象。索引从0开始。删除对象之后,被删除对象后面的元素依次前移。
- (void) removeObjectAtIndex: (NSUInteger) index;
//在指定的索引位置必须存在一个可被替换的对象,可使用 [NSNull,null] 作为占位符。新对象会被保留,被替换对象将被自动释放。
- (void)replaceObjectAtIndex:(NSUInteger)index withObject:(ObjectType)anObject;
遍历数组的方式:
通过索引、使用 NSEnumerator、使用快速枚举、使用代码块枚举对象。
2)枚举
nextObject 返回 nil 时,循环结束。因此若在数组中存储 nil 值,则无法判断 nil 是存储在数组中的数值还是代表循环结束的标志。
对可变数组进行枚举时,不能改变数组的尺寸,否则枚举器会出现不可预期的问题。
NSMutableArray* array = [NSMutableArray arrayWithObjects: @"one", @"two", nil];
NSEnumerator* enumerator = [array objectEnumerator]; //数组请求枚举器
id arrEnum = nil;
while( arrEnum = [enumerator nextObject] ){ //向枚举器请求其下一个对象
NSLog(@"the element: %@", arrEnum);
}
快速枚举:
NSMutableArray* array = [NSMutableArray arrayWithObjects: @"one", @"two", nil];
for( NSString* str in array ){
NSLog(@"the element: %@", str);
}
通过代码块枚举对象:
NSMutableArray* array = [NSMutableArray arrayWithObjects: @"one", @"two", nil];
[array enumerateObjectsUsingBlock:^(NSString* str, NSUInteger index, BOOL* stop){
NSLog(@"the element: %@", str);
}];
3)字典
NSDictionary 是不可变的对象,key 通常是 NSString 字符串,value 可以是任意类型的 Objective-C 对象。
字典与数组有着同样的两个限制。此外字面量语法中也不能在结尾添加 nil 值。
NSString* str1=@"one";
NSString* str2=@"two";
//value,key
NSDictionary* strs=[NSDictionary dictionaryWithObjectsAndKeys: str1,@"A", str2,@"B", nil];
//key,value
NSDictionary* strss=@{@"a": str1, @"b": str2};
//通过 key 访问 value
NSString* str=[strs objectForKey:@"A"];
NSLog(@"%@,%@",strs[@"A"],strss[@"A"]);
//若不存在相应的 key ,则会返回 nil。
NSMutableDictionary 是可变的字典。不能使用字面量语法初始化可变字典。
//创建字典并指定容量,容量仅作为建议。
+ (id) dictionaryWithCapacity: (NSUInteger) numItems;
//为字典添加元素。
NSString* str1=@"one";
NSString* str2=@"two";
NSMutableDictionary* strsss=[NSMutableDictionary dictionary];
[strsss setObject:str1 forKey:@"1"]; //value,key
[strsss setObject:str2 forKey:@"2"]; //若key已存在,则新值会替换掉原值。
//删除关键字。
- (void) removeObjectForKey: (id) aKey;
4.其他数值
1)NSNumber
用于封装基本数据类型。
//创建对象:将基本数据类型封装为对象的过程称为装箱(boxing)。
//此处仅是示例,其他基本数据类型也有对应方法
+ (NSNumber*) numberWithInt: (int) value;
//从对象中提取基本数据:从对象中提取基本数据类型的过程叫做开箱(unboxing)。
- (int) intValue;
//通过字面量语法创建对象:
NSNumber* number;
number=@'a'; //字符型
number=@12ul; //unsigned long
number=@12ll; //long long
number=@12.34f; //float
number=@12.34; //双浮点型
number=@YES; //BOOL,等价于 [NSNumber numberWithBool:YES]
2)NSValue
NSNumber 是 NSValue 的子类,NSValue 可以用于封装任意值。
CGRect rect = CGRectMake (0, 0, 5, 5);
//第一个参数是想要封装的变量的地址,第二个参数是用来说明此数据实体的类型和大小的字符串,@encode编译器指令可以自动生成相应的字符串
NSValue* value = [NSValue valueWithBytes:&rect objCType:@encode(CGRect)];
NSMutableArray* array = [NSMutableArray new];
[array addObject: value];
//以下是提取数据实体的三种方法
NSValue* value1=array[0];
NSValue* value2=[array objectAtIndex:0];
[value getValue:&rect];
CGRect rect1 = CGRectMake (0, 0, 9, 9);
NSValue* value3=[NSValue valueWithBytes:&rect1 objCType:@encode(CGRect)];
[value3 getValue:&rect]; //将 value3 对应的数据存储在 &rect 对应的地址
NSLog(@"%@,%@,%@,%@,%@",value1,value2,value3,value,NSStringFromCGRect(rect));
3)NSNull
//NSNull 类仅有一个方法:
+ (NSNull*) null;
//通过方法调用 [NSNull null] 来表示空值,但空值不表示没有值。
NSString* str1=@"one";
NSString* str2=@"two";
NSDictionary* strs=@{@"a": str1, @"b": str2, @"C": [NSNull null]};
NSDictionary* strs=[NSDictionary dictionaryWithObjectsAndKeys: str1,@"A", str2,@"B", [NSNull null],@"C", nil];
六、内存管理
内存泄漏( leak memory ):若只分配而不释放内存,则程序的内存占用量会不断增加,最终导致系统内存被耗尽从而使得程序崩溃。
不要使用刚被释放的内存,否则可能会误用陈旧的、未来得及清理的数据。
以下两种方法都是有效的:1)将内存清理的代码集中组织到某个方法;2)在创建时便设置对象为自动释放。
1.引用计数(reference counting) / 保留计数(retain counting)
引用计数器/保留计数器:每个对象都有一个与之相关联的整数。若某段代码需要访问对象,则该对象的引用计数器值加1。当代码结束访问时,计数器值减1。当计数器值为0时,对象将被销毁,占用的内存会被系统回收。
当使用 alloc、new方法或 copy 消息创建一个新对象时,对象的引用计数器值被设置为1。给对象发送 retain 消息,将增加引用计数器的值;发送 release 消息,将减少引用计数器的值。
当对象因引用计数器值为0而即将被销毁时,Objective-C 会自动向对象发送一条 dealloc 消息。可重写对象的 dealloc 方法。但是不要直接调用 dealloc 方法,此方法在需要销毁对象时由编译器自动调用。在子类的 dealloc 方法中,一定要调用 [super dealloc] 以释放超类所占用的资源,并且确保其是最后一条语句。
- (id) retain;
retain 方法返回 id 类型的值,则可以完成如下便捷操作:
[ [array retain] setNumber: 5, atIndex: 0];
- (oneway void) release;
- (NSUInteger) retainCount; //获取引用计数器的值
2.对象所有权
如果一个对象内有指向其他对象的实例变量,则称该对象拥有这些对象。
如果一个函数创建了一个对象,则称该函数拥有这个对象。
在访问方法中,先保留对象,然后再释放对象。
3.自动释放池(autorelease pool)
自动释放池以栈的形式实现:当创建了一个新的自动释放池时,其会被添加到栈顶。
1)autorelease 方法
NSObject 类提供了 autorelease 方法:
- (id) autorelease;
该方法预先设定了一条会在未来某个时间发送的 release 消息,其返回值是接收该消息的对象。
当给一个对象发送 autorelease 消息时,实际上是将该对象添加到了自动释放池中。当自动释放池被销毁时,会向该池中的所有对象发送 release 消息。
2)创建自动释放池
当向对象发送 autorelease 消息时,该对象会被添加到相应的自动释放池中,此时对象的引用计数器的值不发生改变。创建时被设置为自动释放的对象也会被添加到自动释放池中。自动释放池会有一个引用指向被添加的对象。当自动释放池被销毁时,将向对象发送一条 release 消息。
通过 @autorelease 关键字创建自动释放池。当使用 @autorelease{} 时,所有在花括号内的代码会使用新池。
通过 NSAutoreleasePool 对象创建自动释放池。创建和释放 NSAutoreleasePool 对象之间的代码会使用新池。
使用关键字方法,效率更高。
使用 AppKit 时,Cocoa 会定期自动创建和销毁自动释放池。
4.Cocoa 的内存管理规则
1)当使用 alloc、new方法或 copy 消息创建一个新对象时,对象的引用计数器值被设置为1。当不再使用该对象时,应向其发送一条 release 或 autorelease 消息。这样,该对象将在其使用寿命结束时被销毁。
2)当通过除 alloc、new、copy 方法之外的方法创建对象时,可以假设该对象的引用计数器的值为1,并且已被设置为自动释放,则不需要执行操作来确保该对象得到清理。但若想在一段时间内拥有该对象,则需要保留它并确保在操作完成时释放它。
3)若保留了某个对象,则必须确保最终释放或自动释放该对象。必须保证 retain 和 release 方法的使用次数相等。
事件循环:
在使用图形应用程序的用户做出决定(如点击鼠标等)之前,程序会处于休眠状态。当事件发生时,程序将被唤醒并执行必要的操作以响应该事件。在处理完成后,程序会返回休眠状态继续等待下一事件的发生。
为了降低程序的内存占用,Cocoa 会在程序开始处理事件之前创建一个自动释放池,并在处理事件之后销毁自动释放池。
5.垃圾回收与自动引用计数(Automatic Reference Counting, ARC)
垃圾回收器在运行时工作,通过返回的代码决定哪些对象仍在使用,哪些对象可以回收。因此要防止指针指向某个不再使用的对象,否则该指针与该对象不会被清理。
垃圾回收器的工作时间无法准确确定,因此会对移动设备的可用性产生不利影响。因为移动设备的资源相较于电脑,会更少。所以当用户使用设备时,若突然进行内存清理,则设备可能会卡顿。
ARC 在编译时工作,编译器会自动插入 retain 和 release 语句。
垃圾回收机制和 ARC 机制不能一起使用。
ARC 只对可保留的对象指针有效:代码块指针、Objective-C 对象指针、通过 _attribute_((NSObject))
类型定义的指针。
其他类型的指针,如 char*,都不支持 ARC 特性。
要使用 ARC,必须满足以下条件:
1)对象的最上层集合知道如何去管理其子对象。
2)必须能够对某个对象的引用计数器的值进行加1或减1的操作。也就是说所有 NSObject 类的子类都能进行内存管理。
3)在传递对象时,程序必须能够在调用者和接受者之间传递对象所有权。
6.强引用与弱引用
当用指针指向某个对象时,如果对其内存做了管理,则拥有了对这个对象的 strong reference。否则,拥有的则是 weak reference。
弱引用有助于处理保留循环(retain cycle,类似于循环引用)。
归零弱引用(zeroing weak reference):让对象自己去清空弱引用的对象。在指向的对象被释放后,这些弱引用会被设置为 nil。就可以像平常的指向 nil 值的指针一样被处理。
使用归零弱引用的方法:关键字和特性不能一起使用,二者相互排斥。
1)__weak NSString* myString; 2)@property(weak) NSString* myString;
在不支持弱引用的旧系统上,可通过 __unsafe_unretained 关键字或 unsafe_unretained 特性来告知 ARC ,此引用为弱引用。
使用强引用的方法:__strong 关键字、strong 特性。
使用 ARC 时的属性命名规则:
1)属性名称不能以 new 开头;2)属性不能只有一个 readonly 而没有内存管理特性。
桥接转换(bridged cast):使用不同的数据类型达到同一目的。通常使用 __bridge、__bridge_retained、__bridge_transfer
。
7.异常
Cocoa 要求所有的异常必须是 NSException 类型的异常。可以创建 NSException 类的子类来作为自己的异常。
虽然可以通过其他对象来抛出异常,但 Cocoa 不会处理它们。
1)与异常有关的关键字
@try:指明可能会抛出异常的代码块。
@catch:指明用来处理已抛出异常的代码块。该代码块接收一个参数,通常是 NSException 类型。
@finally:指明一段代码,无论是否有异常抛出,该段代码都会被执行。
@throw:抛出异常。
2)捕捉不同类型的异常
要在最后使用一个通用的处理代码。可以使用 goto、return 语句退出异常处理代码。
@try{
// do something
NSException* e= ...;
@throw e;
}@catch (NSException* e{
@throw; //rethrow e, 在 @catch 代码块中,可以重复抛出异常而无需指定异常对象
}@catch(id value){
}@finally{
//与当前 @catch 异常处理代码相关的 @finally 代码块会在 @throw 引发下一个异常处理调用(包括在 @catch 代码块中重新抛出异常)之前执行代码。
//因为无论如何,@finally 代码块都会被执行,所以可以在其中执行清理工作,例如内存释放。
}
使用 @try 建立异常不会产生太多的资源消耗,但是捕捉异常会消耗大量资源并影响程序运行速度。
3)抛出异常
程序会创建一个 NSException 实例来抛出异常,并会使用以下两种方式之一:
NSException* theException = [NSException exceptionWithName: ...];
1)使用"@throw exceptionName;"语句抛出异常:@throw theException;
2)向某个 NSException 对象发送 raise 消息:[theException raise];
不要同时使用两种方法。 raise 消息只对 NSException 对象有用,而 @throw 可以用在其他对象上。
七、对象初始化
1.创建对象
[className new],[ [className alloc] init],二者等价。
2.分配对象
[className alloc];
向某个类发送 alloc 消息,就能为类分配一块足够大的内存,以存放该类的全部成员变量。同时会将该内存区域全部初始化为默认值。BOOL 类型被初始化为 NO,数字类型被初始化为0,指针被初始化为 nil。
3.初始化对象
如果不需要设置状态,或 alloc 方法的内存清零的默认行为已经足够,则不需要显式创建 init 方法。
[ [className alloc] init];
不能写成 T* t=[T alloc]; [t init]; (这是两条语句,两步操作)
因为初始化方法(init)返回的对象可能与分配(alloc)的对象不同。
若初始化对象时出现问题,则 init 方法会返回 nil。
两个对象不同的原因:
NSString、NSArray实际上是以类簇的方式实现的。由于 init 方法可以接收参数,因此此方法能够依据其接收的参数,决定返回类簇中最适合的对象。即 init 方法可能会决定创建类簇中另一个类的对象(该对象更符合接收的参数),然后返回该对象而非原先的对象。
// [super init] : 让超类完成自身的初始化工作
(id) init {
if( self = [super init] ) {
// do something
}
return self;
}
由于 init 方法可能会返回完全不同的对象,所以当这种情况发生时,便需要更新 self。而且当 init 方法出现问题时,会返回 nil ,从而使得 self 被赋值为 nil,进而使得 if 语句不会被执行。 b
若在 init 方法中会创建复杂但实际可能用不上的对象,不如选择使用惰性求值,即在 init 方法中只为对象预留位置,等到需要时再创建对象。
初始化函数的一般规则是,若当前对象需要某些信息进行初始化,那么应该将这些信息作为 init 方法的一部分。这样便构成了便利初始化函数(convenience initializer) ,即用于完成某些额外工作的初始化方法。
指定初始化函数(designated initializer) :类中的某个初始化方法被指定为指定初始化函数,其他所有的初始化方法都使用指定初始化函数执行初始化操作。在子类的指定初始化函数中,要调用超类的指定初始化函数以完成超类的初始化。一般选择接收参数最多的初始化方法作为指定初始化函数。
由于超类初始化函数可能返回一个完全不同的对象,所以在子类初始化函数中,一定要将超类初始化函数的返回值赋给 self,并将更新后的 self 值作为子类 init 方法的返回值。
八、属性(Property)
1.@property、@synthesize、@dynamic
属性只支持自动生成 -setBlah、-blah 方法,不支持需要接收额外参数的 setter、getter 方法。
@ 符号标志着 “这是 Objective-C 的语法“。
@property 预编译指令的作用:在 @interface 中不再需要显式声明相应成员变量的setter、getter方法,编译器会自动声明。
@dynamic 预编译指令的作用:告诉编译器,属性的setter与getter方法由用户自己实现,不自动生成。并且让编译器不要创建相应的成员变量。
@synthesize 预编译指令的作用:编译器将在 @implementation 中添加属性的 setter、getter 方法的预编译实现代码。这些代码是看不到的,但是这些方法确实存在并可以被调用。
Objective-C 为property
属性声明添加了自动合成,也就是说系统自动添加了@synthesize
。因此现在不需要手动添加@synthesize 预编译指令。此时编译器会自动添加 @synthesize propertyName=_propertyName;
即此时会自动添加一个名为 _propertyName 的成员变量。
需要手动添加@synthesize的情况:
1)为实例变量起别名
@interface Circle : NSObject{
int innerRadius;
}
@property int radius;
@end //Circle
@implementation Circle
//将属性 radius 绑定到 innerRadius上,否则按照默认方式,编译器会自动创建新的成员变量_radius,并将属性 radius 绑定到 _radius 上。
@synthesize radius = innerRadius;
@end //Circle
int main(int argc, char * argv[]) {
Circle* circle=[Circle new];
circle.radius=6;
NSLog(@"%d",circle.radius);
return 0;
}
2)如果属性是只读属性,但是重写了getter
方法,则系统不会自动生成成员变量,需要手动添加@synthesize
。
如果属性可读可写,但是同时重写了setter/getter
方法,则系统不会自动生成成员变量,需要手动添加@synthesize
。这种情况下,如果只重写了setter/getter
其中一个,系统仍然会执行自动合成。
3)实现了带有property属性的protocol。
假设有一个子类,并且想要从子类直接通过属性来访问变量。则此时成员变量必须放在 .h 头文件中。
若变量只属于当前类,则可以将其放在 .m 文件中,此时 @interface 代码中不能包含相应的声明语句。
在声明相应的变量的属性之后,可以使用点表达式来访问对象的属性。
[tire setRainHandling: 20+i]; => tire.rainHandling = 20+i;
若点表达式出现在等号的左侧,则调用相应属性的 setter 方法;若点表达式出现在等号的右侧,则调用相应属性的 getter 方法。
2.属性的特性
atomic: 默认属性,给getter和setter加了个互斥锁。即若有线程在访问setter,其他线程只能等待访问完成后才能访问,能够保证同一时间只有一个线程执行写入操作,同一时间可以有多个线程执行读取操作。因为加锁,所以影响了效率。
nonatomic: 非原子性,线程不安全,但是效率较高。
retain: MRC 下使用,ARC 下基本使用 strong。用于修饰强引用,将指针原来指向的旧对象释放掉,然后指向新对象,同时将新对象的引用计数加 1;setter 方法的实现是 release 旧值,retain 新值,用于 OC 对象类型。
strong: 表明需要引用(持有)这个对象(reference to the object),负责保持这个对象的生命周期。
weak: 也会引用(reference/pointer)指向对象,但是不会增加引用计数。如果对象A被销毁,则所有指向对象A的弱引用都会自动设置为nil。常用weak解决循环引用问题。
assign: 作用和weak类似,唯一区别是:如果对象A被销毁,则所有指向这个对象A的assign属性并不会被自动设置为nil。这时候这些属性就变成野指针,再访问这些属性,程序可能会崩溃。因此,assign常用于修饰基本数据类型。
copy: 与 strong 类似。但在setter方法中,不保留新值,而是将其拷贝,并对传入的对象进行引用计数加1的操作。通常情况下,不可变对象属性修饰符使用copy,可变对象属性修饰符使用 strong。用 copy 修饰可以防止属性受外界影响,例如若将NSMutableArray
赋值给NSArray
,则修改前者会导致后者的值跟着变化。
readwrite: 属性可读写,此为属性的默认特性。
readonly: 只读属性,此处意味着编译器不自动合成 setter 方法,因此不能直接对其赋值。如果需要对外暴露某个属性,但是又不想该属性被修改。可以在 .h 文件中将该属性设置为 readonly,在 .m 文件中声明一个同名属性,将其设置为 readwrite。并且在 @implementation 中加上 @synthesize XXX,以合成该属性的存取方法。
指定编译器自动生成的 setter、getter 方法的名称:
@property (getter=isHidden) BOOL hidden;
九、类别(category)
类别是为现有类添加新方法的方式。可以为任何类添加新的方法,但是该类必须要有声明。
通常将类别代码放在独立的文件中,该文件通常以 "类名+类别名" 的方式命名。
类别,又称为分类。可以用于拆分主类,降低主类文件代码量。也便于分工协同工作。
分类中的方法实现会覆盖主类的方法实现,即使不引入分类的头文件,调用方法时也会调用分类的方法实现。
1. @interface 部分
@interface NSString (NumberConvenience)
- (NSNumber*) lengthAsNumber;
@end
在保证类别名称唯一的前提下,可以向一个类添加任意数量的类别。
可以在类别中添加方法,也可以添加属性,但必须是 @dynamic 类型的属性。并且不能添加成员变量。添加的属性不会自动合成成员变量。
任意 NSString 对象都能响应 lengthAsNumber 消息,这表明类别具有强大的兼容性。通过使用类别,不需要创建 NSString 类的子类便可获得一种新的行为。
2. @implementation 部分
@implementation NSString (NumberConvenience)
- (NSNumber*) lengthAsNumber{
//do something
}
@end
3.优势与劣势
类别有两个局限性:1)无法向类中添加新的实例变量;2)若类别中的方法与现有方法重名,则类别的方法具有更高优先级。
优势:1)将类的实现代码分散到多个不同文件或框架中;2)创建对私有方法的前向引用;3)向对象添加非正式协议。
4.类扩展(class extension)
在类扩展中添加的属性和方法都是私有的。此处的“私有”是指:最好不要在类的外部使用,此属性与方法仅供类内部使用。因此类扩展最好仅在类的实现文件中使用。
只要知道"私有"属性或方法的名称,外部仍然能够使用,所以 Objective-C 中并不存在真正私有的属性和方法。
类扩展是一种特殊的类别,具有如下特点:
1)不需要命名;2)可以在自己的类中使用;3)可以添加实例变量;4)可以将只读权限改为可读写权限,此时编译器生成的 setter 方法只能在类中访问,对外不公开,公共接口中只有 getter 方法;5)不限制创建的数量。
//类的声明:
@interface Things : NSObject
@property (assign) NSInteger thing1;
@property (readonly, assign) NSInteger thing2;
@end
//类的扩展:
@interface Things () {
NSInteger thing4;
}
@property (readwrite, assign) NSInteger thing2;
@property (assign) NSInteger thing3;
@end
5.创建私有方法的前向引用
如果程序调用了对象的某个方法,但是编译器却没有在当前文件中找到该方法的声明或定义,则编译器会报错。
此时可以通过在类别中声明被调用方法来解决该问题。必须将类别放在调用者之前,才能实现前向引用,不然编译器仍然报错。
例如:
@interface Circle(private)
- (void) draw: (int) radius;
@end
6.委托与非正式协议
delegate: 一个类的对象请求另一个类的对象执行某些工作。委托就是某个对象指定其委托对象处理某些特定任务的设计模式。
委托方法里所请求的操作由委托对象执行,委托方法的调用者是另一个对象。
编写委托对象A并将其提供给对象B,通过在委托对象A所属的类中实现特定的委托方法,可以控制对象B的某些行为:当对象B想要执行某些行为时,它会向委托对象A发送一条消息,即调用委托方法。委托对象A可以通过响应委托方法,来决定是否执行对象B请求的操作。
让对象B使用对象A作为委托对象:
[B setDelegate: A];
run循环:
它在等待某些事情发生之前一直处于阻塞状态,即不执行任何代码。实际上 run 方法将一直保持运行而不会返回,因此位于其后的所有代码都不会被执行。
委托强调类别的另一种应用:
将委托方法声明为 NSObject 的类别。只要对象所属的类继承自 NSobject 类并且实现了委托方法,则相应的对象就可以成为委托对象。
非正式协议:
创建一个 NSObject 的类别称为“创建一个非正式协议”。非正式协议表示“这里有一些你可能希望实现的方法,你可以使用它们更好地完成工作”。使用非正式协议时,可以仅实现想要获得响应的方法。
7.响应选择器
对象B如何知道其委托对象A是否能够处理发送给它的消息呢?
对象B首先会检查对象,询问其是否能响应该选择器。若能响应,则对象B会给对象A发送消息。
选择器可以被传递,可以作为方法的参数使用,甚至可以作为实例变量被存储。
使用 @selector 编译指令来指定选择器(selector):
Car 类的 setEngine 方法的选择器:@selector(setEngine:)
NSObject 类提供了 respondsToSelector 方法,该方法询问对象以确定其是否能够响应某个特定的消息:
Car* car=[ [Car alloc] init];
if( [ car respondsToSelector: @selector(setEngine:) ] ){
//do something
}
Car 类中有 setEngine 方法,所以 Car 类的对象确实能够响应 setEngine 消息。
十、协议(protocol)
1.声明协议与采用协议
@protocol NSCopying <MyParentProtocol>
- (id) copyWithZone: (NSZone*) zone;
@end
在协议中不能引入新的实例变量。
协议名称必须唯一,并且可以继承父协议。所有采用了此协议的类都必须实现协议规定的方法。
可以按任意顺序列出要遵循的协议。必须实现协议规定的所有方法。
@interface Car : NSObject <NSCopying,NSCoding>
@end
Car 类的子类不再需要显式地遵循 NSCopying、NSCoding 协议。因为当Car类的子类继承于Car类时,便已经获得了Car类的所有内容,包括对相应协议的遵循。
[self class] :返回 self 对象所属的类。
由于 allocWithZone 是类方法,因此需要将消息发送给一个类,而非一个对象。
[ [ [self class] allocWithZone:zone] init];
[ [ ClassName allocWithZone:zone] init];
在类中最好使用上述第一条语句进行调用。因为无法保证当前类永远不可能有子类。当子类调用第二条语句时,程序仅创建父类的对象。而且若子类增加了一些额外的实例变量,所创建的父类对象将无法容纳额外变量,从而导致内存溢出。
可以为实例变量和方法参数的类型指定协议名称,若实例变量或传入参数的类型不遵循指定协议,则编译器会报错。
- (void) setObjectValue: (id<NSCopying>) object;
2.协议修饰符
@protocol BaseballPlayer
-(void) drawHugeSalary;
@optional
-(void) slideHome;
@required
-(void) swingBat;
@end;
@required 为默认修饰符。
非正式协议具有类似的功能,仍要添加这个特性的原因:可以用来在类声明或方法声明中明确表达我们的意图。非正式协议仅会列出必须实现的方法,而不会列出未实现的方法。
十一、代码块
代码块对象(即代码块,也称作闭包)是对函数的扩展。除了函数中的代码,代码块还包含变量绑定。
代码块包含自动型、托管型绑定。 automatic binding 使用的是栈内存,而 managed binding 使用的是堆内存。
1.特性
代码块借鉴了函数指针,具有如下特征:
1)返回类型可以手动声明也可以由编译器推导;
2)具有指定类型的参数列表;
3)拥有名称。
2.声明与实现
int (^square_block)(int number);
int (^square_block)(int number) =
^(int number) {return (number * number); };
NSLog(@"%d", square_block(5)); //调用代码块时不需要幂符号
//语法:
<returntype> (^blockName)(list of arguments) = ^(arguments){ body; };
//编译器可以通过代码块的内容推导出返回类型,因此可以省略返回类型。若代码块没有参数,则也可以省略参数。
void (^blockName)() = ^{ body; };
3.使用代码块
1)常规语法是将代码块声明为变量,因此可以像使用函数一样使用代码块。
NSLog(@"%d", square_block(5));
2)不创建代码块变量,而是在代码中内联代码块的内容,如将代码块作为参数传递。
NSArray* array= [NSArray arrayWithObjects:@"Amir",@"Mishal",@"Irrum",@"Adam", nil];
NSLog(@"Unsorted Array: %@", array);
NSArray* sortedArray = [array sortedArrayUsingComparator:^(NSString* object1, NSString* object2) {
return [object1 compare:object2];
}];
NSLog(@"Sorted array: %@", sortedArray);
3)通过使用 typedef 关键字。用法类似于函数指针。
typedef int (^multiply)(int a, int b);
multiply mp = ^(int a, int b){
return a*b;
};
NSLog(@"%d",mp(2,3));
4)捕获参数。
代码块被声明时会捕获创建点时的状态。代码块可以使用声明代码块时已存在的标准类型的变量。
捕获局部变量:
代码块在定义时,会复制局部变量的值,将其作为常量使用。之后局部变量的值更改与否,不会影响捕获到的变量。即值传递。
typedef double (^multiply)(void);
double a = 10,b = 20;
__block double c = 5; //添加 __block 前缀,局部变量才可以在代码块中被更改
multiply mp = ^(void){
c = a * b;
return a * b;
};
NSLog(@"%f",mp()); //200
a=7;
NSLog(@"%f",mp()); //200
长度可变的数组、包含长度可变的数组的结构体均不能被添加 ____block 前缀。
捕获静态变量与全局变量:
若将上述的 double 类型变量更换为全局变量或静态变量,则外部变量的值的更新,会影响代码块内部捕获的值。
即两次输出结果会变为:200、140.
参数变量:
无论实参是局部变量,还是静态变量,亦或是全局变量,外部变量的值的更新,都会影响参数变量的值。
typedef double (^multiply)(double c, double d);
double a=10,b=20;
multiply mp = ^(double c, double d){return c*d;};
NSLog(@"%f",mp(a,b)); //200
a=7;
NSLog(@"%f",mp(a,b)); //140
4.block 对应的循环引用
如果对象 A 强持有对象 B,B 也强持有 A。当初始化完成后,A、B 引用计数均为1,相互赋值后两者的引用计数都变成2。当 A、B 的生命周期结束后,系统分别给 A、B 发送 release 消息。之后A 的引用计数变成1,B 的引用计数变成1。两者引用计数为 1,从而无法被释放,造成内存泄漏。
block 中的循环引用: 通常是由于 self 强制有 block,而 block 又强持有 self 造成的循环引用。如果能通过weak断开引用环,那么问题就解决了。
// 循环引用
self.blockProperty = ^(NSString *string) {
self.anotherProperty = string;
// 使用成员变量也会造成循环引用
// do something
};
// 非循环引用
self.anotherBlockProperty = ^(NSString *string) {
// do something
// 未引用 self
};
解除循环引用:
十二、UIKit
Mac应用程序使用的是 AppKit 框架,而 iOS 应用程序使用的是 UIKit 框架。
UIViewController 是用于管理视图的类,可管理一些基本操作,例如调整视图大小、旋转视图等。如果想自定义对视图的操作,需要创建 UIViewController 类的子类。
Model-View-Controller(模型-视图-控制器),用于拆分以下三类代码,帮助确保代码的最大可重用性。
模型:保存程序数据的类。
视图:用户可以看到并能与之交互的元素。
控制器:绑定模型与视图的代码。
在控制器类中使用 outlet 来引用 storyboard 中的对象,相当于指向用户界面中的对象的指针。用 IBOutlet 关键字声明。
通常使用类扩展来放置视图控制器的输出接口,因为此视图控制器之外的代码不需要使用它们。
在 storyboard 中对对象进行设置,以触发控制器类中的某些特殊方法(action method / action)。操作方法可接受 0 个或 1 个参数,该参数通常被命名为 sender,指向触发该方法的对象。
视图控制器会在 nib 文件加载和对象初始化完成后调用 viewDidLoad 方法。
一般情况下,一个视图离开后,另一个视图会随即显示在屏幕上。由于前一个视图已经不再显示,因此也就不需要再保留该视图。可使用 viewDidUnload 方法在视图使用完毕后清理内存,以释放占用的内存。
十三、消息传递机制
编译器在编译时会根据方法的名称、参数来生成一个用于区分该方法的唯一 ID,该 ID 是 SEL 类型的。
只要方法名称与参数相同,则其 ID 就是相同的。因此若子类重写了父类的方法,则两个方法的 ID 仍是相同的。此时在父类中调用该方法,实际调用的是被子类重写之后的方法。
[object method];
该语句并不会立即执行 method 方法的代码。实际上,该语句是在运行时给对象实例 object 发送了一条名为 method 的消息。此消息可能会被 object 对象处理,也可能被转发给其他对象,也可能不予处理。多条不同的消息可能会对应同一个方法的实现。
1.类与对象
类是由 Class 类型表示的,其本质上是一个指向 object_class 结构体的指针。
typedef struct object_class *Class
struct object_class{
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE; // 父类
const char *name OBJC2_UNAVAILABLE; // 类名
long version OBJC2_UNAVAILABLE; // 类的版本信息,默认为0
long info OBJC2_UNAVAILABLE; // 类信息,供运行期使用的一些位标识
long instance_size OBJC2_UNAVAILABLE; // 该类的实例变量大小
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; // 该类的成员变量链表
struct objc_method_list *methodLists OBJC2_UNAVAILABLE; // 方法定义的链表
struct objc_cache *cache OBJC2_UNAVAILABLE; // 方法缓存
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 协议链表
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
对象是由 id 类型表示的,其本质上是一个指向 objc_object 结构体的指针。
isa 是一个指向其所属类的指针。
typedef struct objc_object *id;
struct objc_object{
Class isa OBJC_ISA_AVAILABILITY;
};
当调用方法时,会给相应对象实例发送消息,runtime 机制会先根据对象所对应的 isa 指针找到其所属的类,而后在类的方法列表以及父类的方法列表中去寻找与消息相对应的方法,找到后即开始运行该方法。
2.元类(meta class)
所有的类,其本身也是一个对象,归属于 meta class。因此 meta class 是当前类所归属的类,主要用于存储当前类的类方法。
因此,类方法的调用与实例方法的调用机制相同。类本身的 isa 指针指向 meta class。
防止这种结构无限延伸,所有 meta class 的 isa 指针都指向最上层基类的 meta class。
3.方法
方法实际上是一个指向 objc-method 结构体的指针。
SEL与IMP 之间的关系类似于哈希表中的 key与value,通过 SEL,可以找到对应的 IMP,从而找到对应方法的代码。
typedef struct objc_method *Method
struct objc_method{
SEL method_name OBJC2_UNAVAILABLE; // 方法名
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE; // 方法实现
}
SEL:
又称为选择器,是一个指向 objc_selector 结构体的指针。
typedef struct objc_selector *SEL;
Objective-C 在编译时,会根据方法的名字、方法参数(不确定是否包含)生成一个唯一的整型标识( Int
类型的地址),即 SEL
。
因为一个类的方法列表中不能存在两个相同的 SEL
,所以 Objective-C 不支持重载。
可以实现参数个数不同的函数重载,但是如果参数个数相同,则无论二者的参数类型是否相同,重载方法均不能通过编译。
但是不同类之间可以存在相同的 SEL
,因为不同类的实例对象执行相同的 selector
时,会在各自的方法列表中根据 SEL
去寻找自己对应的 IMP
。
通过下面三种方法可以获取 SEL
:1)sel_registerName
函数;2)Objective-C 编译器提供的 @selector()
方法;
3)NSSeletorFromString()
方法。
IMP:
本质上是一个函数指针,指向代码的内存地址。
typedef id (*IMP)(id, SEL,...);
id
:指向 self
的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针);
SEL
:方法选择器;
...
:方法的参数列表。
HOOK:
改变程序执行流程,即 Method Swizzle。
// 方法交换
OBJC_EXPORT void
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2);
// 替换方法
OBJC_EXPORT IMP _Nullable
class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types);
// setIMP
OBJC_EXPORT IMP _Nonnull
method_setImplementation(Method _Nonnull m, IMP _Nonnull imp);
// getIMP
OBJC_EXPORT IMP _Nullable
class_getMethodImplementation(Class _Nullable cls, SEL _Nonnull name);
FishHook:
通过符号表来交换 C 函数的内部实现。
4.方法调用(即消息传递)
在 Objective-C 中,消息直到运行时才绑定到方法实现上。编译器会将消息表达式 [receiver message];
转化为消息函数的调用,即 objc_msgSend
。这个函数将消息接收者、方法名、方法参数作为主要参数。
objc_msgSend(receiver, selector, arg1, arg2,...)
objc_msgSend
通过以下几个步骤实现了动态绑定机制:
1)首先,获取 selector
指向的方法实现。由于相同的方法可能在不同的类中有着不同的实现,因此根据 receiver
所属的类进行判断。
2)其次,传递 receiver
对象、方法指定的参数给方法实现。
3)最后,返回调用的方法的返回值。
消息传递的关键在于上述的 objc_class
结构体。当创建一个新对象时,先为其分配内存,并初始化其成员变量。其中 isa
指针也会被初始化,以使得对象可以访问类及类的继承链。
消息传递流程如下:
1)当消息传递给一个对象时,首先在运行时系统缓存 objc_cache
中查找。如果找到,则执行。否则,继续执行后续步骤。
2)objc_msgSend
通过对象的 isa
指针获取到类的结构体,然后在方法分发表 methodLists
中查找方法的 selector
。如果未找到,将沿着类的 isa
找到其父类,并在父类的分发表 methodLists
中继续查找。
3)以此类推,一直沿着类的继承链追溯至 NSObject
类。一旦找到 selector
,则传入相应的参数来执行方法的具体实现,并将该方法加入缓存 objc_cache
。如果最后仍然没有找到 selector
,则会进入消息转发流程。
5.消息转发(message forwarding)
当一个对象能接收一个消息时,会走正常的消息传递流程。
当一个对象无法接收某一消息时,默认情况下,如果以 [object message];
的形式调用方法,若 object
无法响应 message
消息,则编译器会报错。如果是以 performSeletor:
的形式调用方法,则需要等到运行时才能确定 object
是否能接收 message
消息。如果不能接收该消息,则程序崩溃。
对于后者,当不确定一个对象是否能接收某个消息时,可以调用 respondsToSelector:
来进行判断。
if ([self respondsToSelector:@selector(method)]) {
[self performSelector:@selector(method)];
}
事实上,当一个对象无法接收某一消息时,会启动消息转发机制。通过消息转发机制,可以告诉对象如何处理未知的消息。
消息转发机制大致可分为三个步骤:
1)动态方法解析(Dynamic Method Resolution);
2)备用接收者;
3)完整消息转发。
十四、KVC、KVO
1.KVC(KeyValueCoding)
1) 用处:
- 通过属性名称来访问属性,不需通过Setter、Getter方法访问;
- 修改控件的内部属性;
- 在运行时动态的访问和修改对象的属性;
- 访问和修改私有变量。
2)用法
若person对象有个属性是address,address有个属性是town,则通过person访问town属性的方式如下:
// 通过key来访问
id address = [person valueForKey:@"address"];
id town = [address valueForKey:@"town"];
// 通过keypath来访问
id town = [person valueForKeyPath:@"address.town"];
// 通过KVC赋值
// key值不能为nil,且必须存在,否则会抛出异常
Test *test = [[Test alloc] init];
[test setValue:@"xiaoming" forKey:@"name"];
[test setValue:@15 forKeyPath:@"name.age"];
取值:
- 首先按 getKey、Key、isKey、_Key 的顺序查找方法,找到的话会直接调用。若取得的值是对象,则直接返回。若是BOOL或者Int等NSNumber支持的数据类型, 则会将其包装成一个NSNumber对象再返回。否则会将其转换为 NSValue 对象再返回。
- 若未找到上述方法,KVC 会查找 countOf,objectInAtIndex 或 AtIndexes 格式的方法。如果 countOf 方法和另外两个方法中的一个被找到,那么就会返回一个可以响应NSArray所有方法的代理集合对象(类型是NSKeyValueArray,是NSArray的子类)。
- 若未找到上述方法,那么会同时查找countOf,enumeratorOf,memberOf格式的方法。如果这三个方法都找到,那么就返回一个可以响应NSSet的方法的代理集合对象。
- 如果还没有找到,则检查类方法
+ (BOOL)accessInstanceVariablesDirectly
的返回值,如果返回YES(默认行为),则会按_key,_iskey,key,iskey的顺序搜索成员变量名,若找到,则按照第一步的方式返回结果。 - 若未找到相应的成员变量或前述类方法返回NO,那么会直接调用
valueForUndefinedKey:
方法,默认行为是抛出异常。
赋值:
- 首先查找 setKey 方法,若找到,则直接赋值;
- 若未找到,则检查类方法
+ (BOOL)accessInstanceVariablesDirectly
的返回值,如果返回YES(默认行为),则会按_key,_iskey,key,iskey的顺序搜索成员变量名,若找到,则直接赋值。 - 若未找到相应的成员变量或前述类方法返回NO,那么会直接调用
valueForUndefinedKey:
方法,默认行为是抛出异常。
2.KVO(KeyValueObserving)
对目标对象的某属性添加观察,当该属性发生变化时,通过触发观察者对象实现的 KVO 接口方法,来自动通知观察者。KVO 基于 KVC 而实现。直接修改成员变量的值不会触发 KVO,因为没有触发 setter 方法。
如果想监听当前类自身属性的变化,则只需要改写 Setter 方法,在 Setter 方法中添加响应逻辑。
KVO 是通过 isa-swizzling 实现的。编译器自动为被观察对象创造一个派生类,并将被观察对象的 isa 指向这个派生类。如果用户注册了对此目标对象的某一个属性的观察,那么派生类会重写相应的方法,并在其中添加进行通知的代码。Objective-C 在发送消息的时候,会通过 isa 指针找到当前对象所属的类对象。而类对象中保存着当前对象的实例方法,因此在向此对象发送消息时,实际上是发送到了派生类对象的方法。由于编译器在派生类中,对方法进行了重写,并添加了通知代码,因此会向注册的观察者发送通知。
重写 setter:
在 setter 中会添加以下两个方法的调用:
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
然后在 didChangeValueForKey:
中,去调用:
// 观察者必须实现该方法
- (void)observeValueForKeyPath:(nullable NSString *)keyPath
ofObject:(nullable id)object
change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change
context:(nullable void *)context;
所有继承了NSObject的类型,都能使用KVO.
将观察者对象与被观察者对象注册与解除注册:
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
手动实现 KVO:
@interface Target : NSObject
{
int age;
}
// for manual KVO - age
- (int) age;
- (void) setAge:(int)theAge;
@end
@implementation Target
- (id) init
{
self = [super init];
if (nil != self)
{
age = 10;
}
return self;
}
// for manual KVO - age
- (int) age
{
return age;
}
- (void) setAge:(int)theAge
{
[self willChangeValueForKey:@"age"];
age = theAge;
[self didChangeValueForKey:@"age"];
}
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"age"]) {
return NO; // 代表对该属性不再自动发送通知
}
// 对于其他的 key,要转交给 super 来处理
return [super automaticallyNotifiesObserversForKey:key];
}
@end
在类 ClassA 中,使得属性 progressLabel 能够对属性 progress 的变化作出反应:
@interface ClassA
@property (nonatomic, assign) CGFloat progress;
@property (nonatomic, strong) UILabel *progressLabel;
@end
@implementation ClassA
// some other methods
- (void)setProgress:(CGFloat)progress {
_progress = progress;
_progressLabel.text = [NSString stringWithFormat:@"progress: %f", progress];
}
@end
十五、语法
1.assert
ASSERT ()是一个调试程序时经常使用的宏,如果表达式为FALSE (0), 程序将报告错误,并终止执行。如果表达式不为0,则继续执行后面的语句。
ASSERT 只有在 Debug 版本中才有效,如果编译为 Release 版本则被忽略。
2.三目运算符
NSString *string = @"string";
//下述两条语句等价
string ? string : @"";
string ?: @"";
3.isKindOfClass
可用于验证 id 等类型对象的真正类型。
Returns a Boolean value that indicates whether the receiver is an instance of given class or an instance of any class that inherits from that class.
参考文章
《Objective-C基础教程(第2版)》
网络博客(链接较多,此处不一一列举)