内容概要:
————————————
一、熟悉Objective-C
1. 了解Objective-C的起源
- Objective-C为C语言添加了面向对象的特性,是其超集,C语言的所有功能在编写Objective-C代码时依然适用
- Objective-C是由
Smalltalk
(20世纪70年代出现的一种面向对象的语言,消息型语言的鼻祖)演化而来,其语言使用“消息结构”
而不是“函数调用”- 使用消息结构的语言,其
运行时所应执行的代码由运行环境来决定
,而使用函数调用的语言是由编译器来决定 - 在函数调用中,如果调用的函数是多态,那么在运行时就要按照“虚方法表”来查出到底应该执行哪个函数实现。而采用消息结构的语言,无论是否多态,总是在运行时才会查找所要执行的方法
- 使用消息结构的语言,其
Objective-C的重要工作都由
“运行期组件”
而非编译器完成,使用Objective-C的面向对象特性所需的全部数据结构及函数都在运行期组件里面
。运行期组件中含有全部内存管理方法,运行期组件本质上就是一种与开发者所编代码相链接的“动态库”,其代码能把开发者所编写的所有程序粘合起来,这样的话,只需要更新运行期组件,即可提升应用程序性能,而那种许多工作都在“编译期”完成的语言,若想获得类似的性能提升,则要重新编译应用程序代码NSString *someString = @"The String"; // 这句话的含义是,它声明了一个名为 someString 的变量,其类型是 NSString * ,也就是说,此变量为指向 NSString 的指针
- 所有Objective-C 语言的对象都必须如上面的方式声明,因为对象所占内存总是
分配在“堆空间”
中,而不是在“栈”中
,不能在栈中分配Objective-C对象
NSString *someString = @"The String";
NSString *anotherString = someString;
// 说明:只有一个NSString实例,然而有两个变量指向此实例,两个变量都是NSString *型,
// 这说明当前“栈帧”里分配了两块内存,每块内存的大小都能容下一枚指针(在32位架构的计算机上是4字节,64位计算机上是8字节),
// 这两块内存里的值都一样,就是 NSString实例的内存地址
- 分配在堆中的内存必须直接管理,而分配在栈上用于保存变量的内存则会在其
栈帧弹出时自动清理
。 - Objective-C将堆内存管理抽象出来了。不需要用malloc及free来分配或释放对象所占内存。Objective-C运行环境把这部分工作抽象为一套内存管理架构,名叫“引用计数”。
- Objective-C代码中
不含*的变量
,它们可能会使用栈空间
,这些变量保存的不是Objective-C对象(比如CGRect)。与创建结构体相比,创建对象需要额外开销
,比如分配和释放内存等 - Objective-C使用
动态绑定的消息结构
,在运行时才会检查对象类型,接收一条消息之后,应执行何种代码,是由运行期环境而非编译器决定
2. 在类的头文件中尽量少引入其他头文件
- 如果不需要知道导入类的接口细节,在 .h 文件中尽量不要直接使用 #import ,而
使用 @class 导入所需类
(只是为了说明存在这个类),这就叫做向前声明
- 除非却有必要,否则不要引入头文件。一般来说,应在某个类的头文件中使用向前声明来提及别的类,并在实现文件中引入那些类的头文件,这样可以尽量降低类之间的耦合。向前声明只在却有需要的时候在引入,可以
减少类的使用者所需引入的头文件数量,减少编译时间,同时也解决了两个类互相引用的问题
- 有时无法使用向前声明,比如要声明某个类遵循一个协议。尽量把该类遵循的协议这条声明放在class-continuation(即.m中空的分类,@interface className()<协议> )分类中,如果不行的话就把该协议单独放在一个头文件中,然后将其引入。这样做不仅可以
缩减编译时间,而且还能降低彼此依赖程度
3. 多用字面量语法,少用与之等价的方法
// 字符串
NSString *str = "xxxxx";
// 数值
NSNumber *intNumber = @1;
NSNumber *boolNumber = @YES;
// 数组
NSArray *arr = @[@"f", @"u", @"n", @"k", @"y"];
// 字典
NSDictionary *dic = @{@"name" : @"funky",
@"age" : @18 };
- 应该使用字面量语法来创建字符串、数值、数组、字典,与创建此类对象的常规方法相比,更加
简明扼要
- 应该通过取下标操作来访问数组下标或字典中的键所对应的元素
- 用字面量语法来创建数组、字典时,若值中有nil,则会抛出异常,因此,
要确保值里不含nil
- 局限性:使用字面量语法创建出来的字符串、数组、字典对象都是不可变的。若想使用可变版本的对象,则需要复制一份:
NSMutableArray *mutable =[@[@”1”,@”2”] mutableCopy];
4. 多用类型常量,少用#define预处理指令
不需要对外公开常量,仅在自己的编译单元使用
- 例如动画的执行时间,在实现文件(.m)中不要使用预处理指令定义常量 #define ANIMATION_DURATION 0.3 使用 static const NSTimeInterval kAnimationDuration = 0.3,这样定义出来的常量含类型信息;
- 如果试图修改
const
修饰符所声明的常量,那么编译器就会报错; static
修饰符意味着该变量仅在定义该变量的编译单元(实现文件 .m)中可见
,如果声明此变量的时候没有使用static修饰,则编译器会为它创建一个“外部符号”
,若是另一个编译单元中也声明了同名变量,那么编译器会报错;- 关于命名:若常量局限于某“实现文件”之内,则在前面加字母 k,若常量在类之外可见,则通常以类名做为前缀。
需要对外公开常量
- 例如登陆成功后发送通知 在 .h 中 extern NSString *const HSLoginManagerDidLoginNotification; ,在 .m 中 NSString *const HSLoginManagerDidLoginNotification = @”HSLoginManagerDidLoginNotification”;
- 上面的例子中 HSLoginManagerDidLoginNotification 是一个常量,而这个常量是一个指针,指向 NSString 对象;
extern
关键字是要告诉编译器,在全局符号表
中会有一个叫 HSLoginManagerDidLoginNotification 的符号,也就是说编译器无需查看其定义,即允许代码使用此常量,因为它知道当链接成为二进制文件以后,肯定能找到这个常量。 - 为了避免名称冲突,最好用与之相关的类名做名称的前缀,系统框架中一般也这么做,例如 UIApplicationDidEnterBackgroundNotification。
- 例如定义需要对外公开的动画时间,在 .h 中 extern const NSTimeInterval HSAnimatedViewAnimationDuration;,在 .m 中 const NSTimeInterval HSAnimatedViewAnimationDuration = 0.3;
5. 用枚举表示状态,选项,状态码
- 应该用枚举来表示状态机的状态、传递给方法的选项以及状态码等值,给这些值起个易懂的名字
- 最好使用
NSENUM 和 NSOPTIONS
宏来定义枚举类型,并指明其底层数据类型。这样做可以确保枚举是用开发者所选的底层数据类型实现出来的,而不会采用编译器所选的类型 - 在处理枚举类型的
switch语句中不要实现default分支
,这样有新加入的枚举的话,编译器就会提示开发者:switch 语句并未处理所有枚举 如果用枚举来表示选项,且多个选项又可同时使用,那么就将各选项值定义为2的幂,以便
通过按位或操作将其组合起来
,以按位或操作来组合的枚举都使用 NS_OPTIONS 定义
, 而不需要组合使用的枚举,则使用 NS_ENUM 来定义
1
2
3
4
5
6
7
8
9typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
UIViewAutoresizingNone = 0,
UIViewAutoresizingFlexibleLeftMargin = 1 << 0,
UIViewAutoresizingFlexibleWidth = 1 << 1,
UIViewAutoresizingFlexibleRightMargin = 1 << 2,
UIViewAutoresizingFlexibleTopMargin = 1 << 3,
UIViewAutoresizingFlexibleHeight = 1 << 4,
UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};注:‘<<’为左移操作符,‘1 << 2’表示1左移2位,即为二进制100
———————————————-
二、对象,消息,运行期
“对象”
就是“基本构造单元”,开发者可以通过对象来存储并传递数据
- 在对象之间传递数据并执行任务的过程就叫做
“消息传递”
- 当应用程序运行起来以后,为其
提供相关支持的代码
叫做“Objective-C运行期环境”
,它提供了一些使得对象之间能够传递消息的重要函数,并且包含创建类实例所用的全部逻辑
6. 理解“属性”
“属性”(property)
是用来封装对象中的数据
1 | @inteface person:NSObject { |
- 这种写法的问题是:
- 对象布局在
编译期(compile time)
就已经固定了。只要碰到访问_firstName变量的代码,编译器就把其替换为“偏移量”(offset)
,这个偏移量是“硬编码”(hardcode)
,表示该变量距离存放对象的内存区域的起始地址有多远
。如果又加了一个实例变量,在firstName前加一个变量dateOfBirth,那么原来指向_firstName的地址偏移量就指向了_dateOfBirth了,就会读取到错误的值。所以,如果代码使用了编译期计算出来的偏移量,那么在修改类定义之后必须重新编译
,否则就会出错。例如:某个代码库中的代码使用了一份旧的类定义。如果和其相链接的代码使用了新的类定义,那么运行时就会出现不兼容现象。
- 对象布局在
- Objective-C的做法是:
- 把
实例变量当做一种存储偏移量所用的“特殊变量”
(special variable),交由“类对象”(class object)保管
。偏移量会在运行期查找
,如果类定义变了,那么存储的偏移量也就变了,这样的话,无论何时访问实例变量,总能使用正确的偏移量。甚至可以在运行期向类中新增实例变量,这就是稳固的“应用程序二进制接口”
(Application Binary Interface,ABI)
- 把
- 属性的优势是:
- 可以使用
点语法
,另外如果使用了属性的话,那么编译器就会自动编写访问这些属性所需的方法,此过程叫做自动合成
(autosynthesis)。需要强调的是,这个过程由编译器在编译期执行
,所以编辑器里看不到这些“合成方法”(synthesized method)的源代码。 - 除了生成方法代码之外,编译器还要
自动向类中添加适当类型的实例变量
,并且在属性名前面加下划线,以此作为实例变量的名字。也可以在类的实现代码里通过@synthesize
语法来指定实例变量的名字:@synthesize firstName = _myFirstName
(不建议使用,推荐使用默认方案)。 - 如果你不想让编译器自动合成存取方法,则可以自己实现。如果你只实现了其中一个存取方法,那么另一个还是会由编译器来合成。还有一种办法能阻止编译器自动合成存取方法,就是使用
@dynamic
关键字,它会告诉编译器:不要自动创建实现属性所用的实例变量,也不要为其创建存取方法
。而且,在编译访问属性的代码时,即使编译器发现没有定义存取方法,也不会报错,他相信这些方法能在运行期找到。
- 可以使用
属性的四类特质是:
(1)原子性:在默认情况下,由编译器合成的方法会通过锁定机制确保其原子性,
默认具有“atomic”的特质
,如果属性使用nonatomic特质,则不使用同步锁。- atomic与nonatomic的区别:具备atomic特质,获取方法会通过锁定机制来确保其操作的原子性, 也就是说,如果两个线程读写同一属性,那么无论何时,总能看到有效的属性值。若是不加锁的话(使用nonatomic),那么当其中一个线程正在改写某属性值时,另外一个线程也许会把尚未修改好的属性值读取出来,这样的话,读取到的值会是错误的。在iOS的开发中,通常使用nonatomic,因为在iOS开发中
使用同步锁的开销较大
,会带来性能问题,一般情况下,并不要求属性必须是原子的,因为这并不能保证“线程安全”
,若要实现“线程安全”的操作,还需采用更深层的锁定机制才行。一个线程在连续多次读取某属性值的过程中有别的线程在同时改写该值,那么即便将属性声明为atomic,也还是会读到不同的属性值,因此在iOS开发中,一般都会使用nonatomic特质。通常在开发Mac OS X程序时,使用atomic属性不会有性能瓶颈问题。
- atomic与nonatomic的区别:具备atomic特质,获取方法会通过锁定机制来确保其操作的原子性, 也就是说,如果两个线程读写同一属性,那么无论何时,总能看到有效的属性值。若是不加锁的话(使用nonatomic),那么当其中一个线程正在改写某属性值时,另外一个线程也许会把尚未修改好的属性值读取出来,这样的话,读取到的值会是错误的。在iOS的开发中,通常使用nonatomic,因为在iOS开发中
(2)读写权限:具备“readwrite(读写)”特质的的属性拥有“获取方法”和“设置方法”,具备“readonly(只读)”特质的属性仅拥有“获取方法”
(3)内存管理语义:属性用于封装数据,而数据要有“具体的所有权语义”,下面的一组特质
只会影响“设置方法”
- assign:“设置方法”只会执行对
“纯量类型”
(scalar type,例如CGFloat或NSInterger)的简单赋值操作 - strong:此特质表明了属性定义了一种
“拥有关系”
。为这种属性设置新值时,会先保留新值,并释放旧值,然后再将新值设置上去
。 - weak:此特质表明了属性定义了一种
“非拥有关系”
。为这种属性设置新值时,设置方法既不保留新值,也不释放旧值
。此特质类似assign,然而在属性所指的对象被销毁时,属性值也会清空。 - copy:此特质所表达的所属关系与strong类似,然而设置方法并
不保留新值,而是将其“copy”
。当属性类型为NSString *时,经常用此特质来保护其封装性,保护数据不会在对象不知情的情况下被修改。 - unsafe_unretained:此特质的语义和assign相同,但是它
适用于“对象类型”
,该特质表达一种“非拥有关系”,当目标对象被销毁时,属性值不会自动清空
(“不安全”,unsafe),这一点与weak不同。
- assign:“设置方法”只会执行对
(4)方法名:
getter=
指定”获取方法”的方法名,如果某个属性是Boolean型,而你想为其获取方法加上”is”,那就可以用这个方法来指定 1
@property (nonatomic, getter = isOn) BOOL on;
setter=
指定”设置方法”的方法名.这种做法几乎用不到
- 要点:
- 可以用@property语法来定义对象中所封装的数据
- 通过“特质”来指定存储数据所需的正确语义
- 在设置属性所对应的实例变量时,一定要遵从该属性值所声明的语义
- 开发iOS程序时应该使用nonatomic属性,因为atomic会严重影响性能
7. 在对象内部尽量直接访问实例变量
通过属性:
1
2
3
4
5
6
7
8- (NSString *)fullName {
return [NSString stringWithFormat:@“%@ %@“,self.firstName,self.lastName];
}
- (void)setFullName:(NSString *)fullName {
NSArray *components = [fullName componentsSeparatedByString:@“ “];
self.firstName = components[0];
self.lastName = components[1];
}直接访问实例变量:
1
2
3
4
5
6
7
8
9- (NSString *)fullName {
return [NSString stringWithFormat:@“%@ %@“,_firstName,_lastName];
}
- (void)setFullName:(NSString *)fullName {
NSArray *components = [fullName componentsSeparatedByString:@“ “];
_firstName = components[0];
_lastName = components[1];
}直接使用实例变量和使用设置方法的区别
- 不经过Objective-C的“方法派发”,所以直接
访问实例变量的速度比较快
,编译器所生成的代码会直接访问保存对象实例变量的那块内存 - 直接访问实例变量时,
不会调用“设置方法”,会绕过为相关属性所定义的“内存管理语义”
。比如说,如果在ARC下直接访问一个声明为copy的属性,那么并不会拷贝该属性,只会保留新值并释放旧值 - 如果直接访问实例变量,
不会触发“键值观测”(KVO)通知
- 通过属性来访问有助于
排查与之相关的错误
,因为可以给“获取方法”或“设置方法”中新增“断点”,监控改属性的调用者及其访问时机
- 不经过Objective-C的“方法派发”,所以直接
- 要点
- 在对象内部读取数据时,应该直接通过实例变量来读,而写入数据时,则应该通过属性来写(在写入实例变量时,通过其“设置方法”来做,而在读取实例变量时,则直接访问)
- 在
初始化方法以及dealloc方法
中,总是应该直接通过实例变量来读写数据
- 有时会使用
惰性初始化技术(懒加载)
配置某份数据,这种情况下,需要通过属性来读取数据
8. 理解“对象等同性”这一概念
- 若想检测对象的等同性,要提供“isEqual:”与hash方法。对于对象等同性,不要使用 == 来判断两个对象是否相等,该操作比较的是两个指针本身,应该使用“isEqual”方法,而NSObject对象的isEqual方法默认也只是比较指针,有特殊比较需求,需要自己重写isEqual方法
- 相同的对象必须具有相同的哈希码,但是两个哈希码相同的对象却未必相同。
- 不要盲目地逐个检测每条属性,而是应该依据具体需求来定制检测方案。
编写hash方法时,应该使用计算速度快而且哈希碰撞几率低的算法。
直接取属性的hash比使用字符串拼接效率高,类似下面代码:
1
2
3
4
5
6- (NSUInteger)hash {
NSUInteger firstNameHash = [_firstName hash];
NSUInteger lastNameHash = [_lastName hash];
NSUInteger ageHash = _age;
return firstNameHash ^ lastNameHash ^ ageHash;
}
9. 以“类簇模式”隐藏实现细节
- “类簇”(class cluster)是一种很有用的模式,可以
隐藏“抽象基类”背后实现细节
。
比如UIButton类的类方法:1
+(UIButton*)buttonWithType:(UIButtonType)type;
该方法返回的对象,其类型取决于传入的按钮类型。UIButton类的使用者无须关心创建出来的按钮具体属于哪个子类,也不用考虑按钮的绘制方式等实现细节。
“工厂模式(Factory pattern)”
是创建类簇的方法之一
例如:假设有一个处理雇员的类,每个雇员都有“名字”和“薪水”这两个属性,管理者可以命令其执行日常工作。但是,各种雇员的工作内容却不同。经历在带领雇员做项目时,无须关心每个人如何完成其工作,仅需指示其开工即可。
首先要定义抽象基类:
typedef NS_ENUM(NSUInteger, ECOEmployeeType){
ECOEmployeeTypeDeveloper,
ECOEmployeeTypeDesigner,
ECOEmployeeTypeFinance,
}
@interface EOCEmployee: NSObject
@property (copy) NSString *name;
@property NSUInter salary;
//Helper for creating Employee objects
+ (EOCEmployee*)employereWithType:(ECOEmployeeType)type;
//Make Employees do their respective day’s work
- (void)doADaysWork;
@end
.
@implementation EOCEmployee
+ (EOCEmployee*)employereWithType:(ECOEmployeeType)type{
switch(type){
case ECOEmployeeTypeDeveloper:
return [ECOEmployeeTypeDeveloper new];
break;
case ECOEmployeeTypeDesigner:
return [ECOEmployeeTypeDesigner new];
break;
case ECOEmployeeTypeFinance:
return [ECOEmployeeTypeFinance new];
break;
}
}
- (void)doADaysWork{
//Subclasses implement this
}
- 要点:
- 类簇模式可以把实现细节隐藏在一套简单的公共接口后面
- 系统框架中经常使用类簇
- 从类簇的公共抽象基类中继承子类时要当心,若有开发文档,则应首先阅读
10. 在既有类中使用关联对象存放自定义数据
- “关联对象”(Associated Object):可以给某对象关联许多其他对象,这些对象通过“键”来区分。存储对象值的时候,可以指明“存储策略”(storage policy),用以维护相应的“内存管理语义”。存储策略由名为objc_AssociationPolicy的枚举所定义,下表列出了该枚举值,同时还列出了与之等效的@property属性:假如关联对象成为了属性,那么它就会具备对应的语义。
下列方法可以管理关联对象:
// 此方法以给定的键和策略为某对象设置关联对象值。 void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy) // 此方法根据给定的键从某对象中获取相应的关联对象值。 id objc_getAssociatedObject(id object, void *key) // 此方法移除指定对象的全部关联对象。 void objc_removeAssociatedObjects(id object)
我们可以把某对象想象成NSDictionary
,把关联到该对象的值理解为字典中的条目。然而两者之间有个重要差别:设置关联对象时用的键是个“不透明的指针”
。如果在两个键上调用“isEqual:”方法的返回值为YES,那么NSDictionary就认为二者相等;然而在设置关联对象值时,若想令两个键匹配到同一个值,则二者必须是完全相同的指针
才行。鉴于此,在设置关联对象时,通常使用静态全局变量做键
。
关联对象举例:
#import <objc/runtime.h> static void *EOCMyAlertViewKey = "EOCMyAlertViewKey"; -(void)askUserQuestion { UIAlertView *alert = [[UIAlertView alloc]initWithTitle:@"Question" message:@"What do you want to do?" delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Continue", nil]; void (^block)(NSInteger) = ^(NSInteger buttonIndex){ if(buttonIndex==0){ [self doCancel]; }else{ [self doContinue]; } }; objc_setAssociatedObject(alert, EOCMyAlertViewKey, block, OBJC_ASSOCIATION_COPY); [alert show]; } #pragma mark---UIAlertViewDelegate -(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex { void (^block)(NSInteger) = objc_getAssociatedObject(alertView, EOCMyAlertViewKey); block(buttonIndex); }
创建完警告视图之后,设定一个与之关联的“块”(block),等到执行delegate方法时再将其读出来。创建警告视图与处理操作结果的代码都放在一起了,这样比原来更易读懂,因为我们无需在两部分代码之间来回游走,即可明白警告视图的用处。但是,采用该方案时需注意:块可能要捕获某些变量,这也许会造成循环引用。
- 要点:
- 可以通过“关联对象”机制来把两个对象连起来。
- 定义关联对象时可制定内存管理语义,用以模仿定义属性时所采用的“拥有关系”与“非拥有关系”。
- 只有在其他做法不可行时才应该选用关联对象,因为这种做法通常会引入难于查找的bug
11. 理解objc_msgSend的作用
传递消息
(pass a message)是指在对象上调用方法。消息有“名称”(name)或“选择子”(selector),可以接受参数,可能有返回值。- 由于Objective-C是C的超集,所以最好先理解C语言的函数调用方式。C语言使用
“静态绑定”(static binding)
,也就是说,在编译期就能决定运行时所应调用的函数
。
以下列代码为例:
#include <stdio.h>
void printHello(){
printf("Hello,world!\n");
}
void printGoodbye(){
printf("Goodbye,world!\n");
}
void doTheThing(int type){
if(type==0){
printHello();
}else{
printGoodbye();
}
}
如果不考虑“内联”(inline),那么编译器在编译代码的时候就已经知道程序中有printHello与printGoodbye这两个函数了,于是会直接生成调用这些函数的指令。而函数地址实际上是硬编码在指令之中的。若是将上面那段代码写成下面这样:
void doTheThing(int type){
void (*fnc)();
if(type==0){
fnc = printHello;
}else{
fnc = printGoodbye;
}
fnc();
}
这时就得使用
“动态绑定”
(dynamic binding)了,因为所要调用的函数直到运行期才能确定
。编译器在这种情况下生产的指令与刚才那个例子不同,在第一个例子中,if与else语句中都有函数调用指令。而在第二个例子中,只有一个函数调用指令,不过待调用的函数地址无法硬编码在指令之中,而是要在运行期读取出来
。在Objective-C中,如果向某对象传递消息,那就会使用
动态绑定机制
来决定需要调用的方法。在底层,所有方法都是普通的C语言函数,然而对象收到消息之后,究竟该调用哪个方法则完全于运行期决定
,甚至可以在程序运行时改变,这些特性使得Objective-C成为一门真正的动态语言。
给对象发送消息可以这样来写:
id returnValue = [someObject messageName:parameter];
在本例中,someObject叫做
“接收者”(receiver)
,messageName叫做“选择子”(selector)
。选择子与函数合起来成为“消息”(message)
。编译器看到此消息后,将其转换为一条标准的C语言函数调用,所调用的函数是消息传递机制中的核心函数,叫做objc_msgSend
,其“原型”如下:1
2
3void objc_msgSend(id self, SEL cmd, …)
// 第一个参数的代表接收者,第二个参数代码选择子(SEL是选择子的类型),后续参数是消息中那些参数,其顺序不变。
// 选择子指的就是方法的名字。“选择子”与“方法”这两个词经常交替使用编译器会把刚才那个例子中的消息转换为如下函数:
1
id returnValue = objc_msgSend(someObjct, @selector(messageName:), parameter);
消息的调用过程
- objc_msgSend函数会依据接收者与选择子的类型来调用适当的方法。为了完成此操作,该方法需要在接收者所属的类中搜寻其
“方法列表”
(list of methods),如果能找到与选择子名称相符的方法,就跳至其代码实现。若是找不到,那就沿着继承体系继续向上查找,等找到合适的方法之后再跳转。如果最终还是找不到相符的方法,那就执行“消息转发”
(message forwading)操作。 objc_msgSend会将匹配结果缓存在“快速映射表”(fast map)里面
,每个类都有这样一块缓存,若是稍后还向该类发送与选择子相同的消息,那么执行起来就很快了。当然,这种“快速执行路径”(fast path)还是不如“静态绑定的函数调用操作”(statically bond fuction call)那样迅速,不过只要把选择子缓存起来了,那就不会慢很多。
- objc_msgSend函数会依据接收者与选择子的类型来调用适当的方法。为了完成此操作,该方法需要在接收者所属的类中搜寻其
有些特殊情况需要Objective-C运行环境中的另一些函数来处理:
objc_msgSend_stret
如果待发送的消息要返回结构体,那么可交由此函数处理。只有当CPU的寄存器能够容纳得下消息返回类型时,这个函数才能处理此消息。若是返回值无法容纳与CPU寄存器中(比如说返回的结构体太大了),那么就由另一个函数执行派发。此时,那个函数会通过分配在栈上的某个变量来处理消息所返回的结构体。objc_msgSend_fpret
如果消息返回的是浮点数,那么可交由此函数处理。在某些架构的CPU中调用函数时,需要对“浮点数寄存器”(floating-point register)做特殊处理,也就是说,通常所用的objc_msgSend在这种情况下并不合适。这个函数是为了处理x86等架构CPU中某些令人稍觉惊讶的奇怪状况。objc_msgSendSuper
如果要给超类发消息,例如[super message:parameter],那么就交由此函数处理。也有另外两个与objc_msgSend_stret和objc_msgSend_fpret等级的函数,用于处理发给super的相应消息。objc_msgSend等函数一旦找到应该调用的方法实现之后,就会“跳转过去”。之所以能这样做,是因为Objective-C对象的每个方法都可以视为简单的C函数,其原型如下:
<return_type> Class_selector(id self, SEL _cmd, ….)
- 每个类里都有一张表格,其中的指针都会指向这种函数,而选择子的名称则是查表时所用的“键”。objc_msgSend等函数正是通过这张表格来寻找应该执行的方法并跳转至其实现的。请注意,原型的样子和objc_msgSend函数很像。这不是巧合,而是为了利用
“尾调用优化”
(tail-call optmization)技术,令“跳至方法实现”这一操作变得更简单些。
- 如果某函数的最后一项操作是调用另外一个函数,那么就可以运用“尾调用优化”技术。编译器会生成跳转至另一个函数所需的指令码,而且不会向调用堆栈中推入新的“栈帧”(frame stack)。
只有当某函数的最后一个操作仅仅是调用其他函数而不是将其返回值另作他用时,才能执行“尾调用优化”
。这项优化对objc_msgSend非常关键,如果不这么做的话,那么每次调用Objective-C方法之前,都需要为调用objc_msgSend函数准备“栈帧”,大家在“栈踪迹”(stack trace)中就可以看到这种“栈帧”。此外,若是不优化,还会过早地发生“栈溢出”(stack overflow)现象。
- 要点:
- 消息由接收者、选择子以及参数构成。给某对象“发送消息”(invoke a message)也就相当于在该对象上“调用方法”(call a method)。
- 发给某对象的全部消息都要由“动态消息派发系统”(dynamic message dispatch system)来处理,该系统会查出对于的方法,并执行其代码
12. 理解消息转发机制
- 在编译期向类发送了其无法解读的消息并不会报错,因为在
运行期可以继续向类中添加方法
,所以编译器在编译时还无法确知类中到底会不会有某个方法实现。当对象接收到无法解读的消息
后,就会启动“消息转发”
(message forwarding)机制,程序员可经由此过程告诉对象应该如何处理未知消息。
- 消息转发分为两大阶段:
- 第一阶段先征询接收者,所属的类,看其是否能动态添加方法,以处理当前这个“未知的选择子”(unknown selector),这叫做
“动态方法解析”
(dynamic method resolution)。 - 第二个阶段涉及“完整的消息转发机制”(full forwarding mechanism)。如果运行期系统已经把第一阶段执行完了,那么接收者自己就无法再以动态新增方法的手段来响应包含该选择子的消息了。此时,运行期系统会请求接收者以其他手段来处理与消息相关的方法调用。这又细分为两小步。首先,请求接收者看看有没有其他对象能处理这条消息。若有,则运行期系统会把消息转给那个对象,于是消息转发过程结束,一切正常。若没有
“备援的接收者”
(replacement receiver),则启动完整的消息转发机制
,运行期系统会把与消息有关的全部细节都封装到NSInvocation对象中,再给接收者最后一次机会,令其设法解决当前还未处理的这条消息。
- 第一阶段先征询接收者,所属的类,看其是否能动态添加方法,以处理当前这个“未知的选择子”(unknown selector),这叫做
动态方法解析
对象在收到无法解读的消息后,首先将调用其所属类的下列类方法:
1
+(BOOL)resolveInstanceMethod:(SEL)sel;
该方法的参数就是那个未知的选择子,其返回值为Boolean类型,表示这个类是否能新增一个实例方法用以处理此选择子。在继续往下执行转发机制前,本类有机会新增一个处理此选择子的方法。假如尚未实现的方法不是实例方法而是类方法,那么运行系统就会调用另外一个方法:
1
+(BOOL)resolveClassMethod:(SEL)sel;
使用这种办法的
前提是
:相关方法的实现代码已经写好,只等着运行的时候动态插在类里面就可以了。此方案常用来实现@dynamic属性。比如,要访问CoreData框架中的NSManagedObject对象的属性时就可以这么做,因为实现这些属性的存取方法在编译期就能确定。
下面代码演示了如何用“resolveInstanceMethod: ”来实现@dynamic属性:#import <objc/runtime.h> id autoDictionaryGetter(id self, SEL _cmd); void autoDictionarySetter(id self, SEL _cmd, id value); +(BOOL)resolveInstanceMethod:(SEL)sel { NSString *selectorString = NSStringFromSelector(sel); if(/* sel is from a @dynamic property */){ if([selectorString hasPrefix:@"set"]){ class_addMethod(self, sel, (IMP)autoDictionarySetter, "v@:@"); }else{ class_addMethod(self, sel, (IMP)autoDictionaryGetter, "@@:"); } return YES; } return [super resolveInstanceMethod:sel]; }
首先将选择子化为字符串,然后检查其是否表示设置方法。若前缀为set,则表示设置方法,否则就是获取方法。不管哪种情况,都会把处理该选择子的方法加到类里面,所添加的方法是纯C函数实现的。C函数可能会用代码来操作相关的数据结构,类之中的属性数据就存放在那些数据结构里面。以CoreData为例,这些存取方法也许要和后端数据库通信,以便获取或更新相应的值。
备援接收者
当前接收者还有第二次机会能处理未知的选择子,在这一步中,运行期系统会问它:能不能把这条消息转给其他接收者来处理。与该步骤对应的处理方法如下:
1
-(id)forwardingTargetForSelector:(SEL)aSelector
方法参数代表未知的选择子,若当前接收者能
找到备援对象,则将其返回,若找不到,就返回nil
。通过此方案,我可以用“组合”(composition)来模拟出“多重继承”(multiple inheritance)的某些特性。在一个对象内部,可能还有一系列其他对象,该对象可经由此方法将能够处理某选择子的相关内部对象返回,这样的话,在外界看来,好像是该对象亲自处理了这条消息似的。
注意:我们无法操作经由这一步所转发的消息。若是想在发送给备援接收者之前先修改消息内容,那就得通过完整的消息转发机制来做了。
完整的消息转发
如果转发算法已经来到这一步的话,那么唯一能做的就是启用完整的消息转发机制了。首先
创建NSInvocation对象
,把与尚未处理的那条消息有关的全部细节都封装于其中
。此对象包含选择子、目标(target)及参数。在触发NSInvocation对象时,“消息派发系统”(message-dispatch system)将亲自出马,把消息指派给目标对象。
此步骤会调用一列方法来转发消息:1
-(void)forwardInvocation:(NSInvocation *)anInvocation
这个方法可以实现得很简单:只需改变调用目标,使消息在新目标上得以调用即可。然而这样实现出来的方法与“备援接收者”方案所实现的方法等效,所以很少有人采用这么简单的实现方式。比较有用的实现方式为:在触发消息前,先以某种方式改变消息内容,比如追加另外一个参数,或者改换选择子,等等。
实现此方法时,若发现某调用操作不应由本类处理,则需调用超类的同名方法。这样的话,继承体系中的每个类都有机会处理此调用请求,直至NSObject。如果最后调用了NSObject类的方法,那么该方法还会继而调用
“doseNotRecognizeSelector:”
以抛出异常,此异常表明选择子最终未能得到处理。
消息转发全流程
下面这张流程图描述了消息转发转发机制处理消息的各个步骤:
接收者在每一步中均有机会处理消息。步骤越往后,处理消息的代价就越大。最好能在第一步就处理完,这样的话,运行期系统就可以将此方法缓存起来了
。如果这个类的实例稍后还收到同名选择子,那么根本无须启动消息转发流程。若想在第三步里把消息转给备援的接收者,那还不如把转发操作提前到第二步。因为第三步只是修改了调用目标,这项改动放在第二步执行会更为简单,不然的话,还得创建并处理完整的NSInvocation。
以完整的例子演示动态方法解析:
为了说明消息转发机制的意义,下面示范如何以动态方法解析来实现@dynamic属性。假设要编写一个类似于“字典”的对象,它里面可以容纳其他对象,只不过开发者要直接通过属性来存取其中的数据。这个类的设计思路是:由开发者来添加属性定义,并将其声明为@dynamic,而类则会自动处理相关属性值的存放与获取操作。
该类的接口可以写成:#import <Foundation/Foundation.h> @interface EOCAutoDictionary : NSObject @property(nonatomic,strong)NSString *string; @property(nonatomic,strong)NSNumber *number; @property(nonatomic,strong)NSDate *date; @property(nonatomic,strong)id opaqueObject; @end
在类的内部,每个属性的值还是会存放在字典里,所以我们先在类中编写如下代码,并将属性声明为@dynamic,这样的话,编译器就不会为其自动生成实例变量及存取方法了:
#import "EOCAutoDictionary.h" #import <objc/runtime.h> id autoDictionaryGetter(id self, SEL _cmd); void autoDictionarySetter(id self, SEL _cmd, id value); @interface EOCAutoDictionary() @property(nonatomic,strong)NSMutableDictionary *backingStore; @end @implementation EOCAutoDictionary @dynamic string,number,date,opaqueObject; -(instancetype)init{ if(self = [super init]){ _backingStore = [NSMutableDictionary new]; } return self; } +(BOOL)resolveInstanceMethod:(SEL)sel { NSString *selectorString = NSStringFromSelector(sel); if([selectorString hasPrefix:@"set"]){ class_addMethod(self, sel, (IMP)autoDictionarySetter,"v@:@"); }else{ class_addMethod(self, sel, (IMP)autoDictionaryGetter,"@@:"); } return YES; }
- 当开发者首次在EOCAutoDictionary实例上访问某个属性时,运行期系统还找不到对应的选择子,因为所需的选择子既没有直接实现,也没有合成出来。现在假设要写入opaqueObject属性,那么系统就会以“setOpaqueObject:”为选择子调用上面这个方法。同理,在读取该属性时,系统也会调用上述方法,只不过传入的选择子是opaqueObject。
- resolveInstanceMethod方法会判断选择子的前缀是否为set,以此分辨其是set选择子还是get选择子。在这两种情况下,都要向类中新增一个处理该选择子所用的方法,这两个方法分别以autoDictionarySetter及autoDictionaryGetter函数指针的形式出现。此时就用到了class_addMethod方法,它可以向类中动态地添加方法,用以处理给定的选择子。第三个参数为函数指针,指向待添加的方法。而最后一个参数则表示待添加方法的“类型编码”(type encoding)。在本例中,编码开头的字符表示方法的返回值类型,后续字符则表示其所接受的各个参数。(参见Objective-C type Encodings)
getter函数可以用下列代码实现:
id autoDictionaryGetter(id self, SEL _cmd)
{
//Get the backing store from the object
EOCAutoDictionary *typedSelf = (EOCAutoDictionary*)self;
NSMutableDictionary *backingStore = typedSelf.backingStore;
//The key is simply the selector name
NSString *key = NSStringFromSelector(_cmd);
//Return the value
return [backingStore objectForKey:key];
}
而setter函数则可以这么写:
void autoDictionarySetter(id self, SEL _cmd, id value)
{
//Get the backing store from the object
EOCAutoDictionary *typedSelf = (EOCAutoDictionary*)self;
NSMutableDictionary *backingStore = typedSelf.backingStore;
/*The selector will be for example,"setOpaqueObject:".
*We need to remove the "set",“:” and lowercase the first
*letter of the remainder.*/
NSString *selectorString = NSStringFromSelector(_cmd);
NSMutableString *key = [selectorString mutableCopy];
//Remove the ":"at the end
[key deleteCharactersInRange:NSMakeRange(key.length-1, 1)];
//Remove the "set" prefix
[key deleteCharactersInRange:NSMakeRange(0, 3)];
//Lowercase the first character
NSString *lowercaseFirstChar = [[key substringToIndex:1]lowercaseString];
[key replaceCharactersInRange:NSMakeRange(0, 1) withString:lowercaseFirstChar];
if(value){
[backingStore setObject:value forKey:key];
}else{
[backingStore removeObjectForKey:key];
}
}
EOCAutoDictionary的用法很简单:1
2EOCAutoDictionary *dict = [EOCAutoDictionary new];
dict.date = [NSDate dateWithTimeIntervalSince1970:4753728000];
其他属性的访问方式与date类似,要想添加新属性,只需要@property来定义,并将其声明为@dynamic即可。在iOS的CoreAnimation框架中,CALayer类就用了本例相似的实现方式,这使得CALayer成为“兼容于键值编码的”(key-value-coding-compliant)容器类,这就等于说,能够向里面随意添加属性,然后以键值对的形式来访问。于是,开发者就可以向其中新增自定义的属性了,这些属性值的存储工作由基类直接负责,我们只需在CALayer的子类中定义新属性即可。
- 要点:
- 若对象无法响应某个选择子,则进入消息转发流程。
- 通过运行期的动态方法解析功能,我们可以在需要用到某个方法时再将其加入类中。
- 对象可以把无法解读的某些选择子转交给其他对象来处理。
- 经过上述两步之后,如果还是没办法处理选择子,那就启动完整的消息转发机制。
13. 用“方法调配技术”调试“黑盒方法”
方法调配(method swizzling):给定的选择子名称相对应的方法可以在运行期进行改变,不需要源代码,也不需要通过继承子类来覆写方法就能改变这个类本身的功能,这样一来,新功能将在本来的所有实例中生效,而不是仅限于覆写了相关方法的那些子类实现
IMP:类的方法列表会把选择子的名称映射到相关的方法实现之上,使得“动态消息派发系统”能够据此找到应该调用的方法。这些方法均以函数指针的形式来表示,这种指针叫做IMP,其原型:
id (*IMP)(id, SEL, …)
NSString类可以响应lowercaseString、uppercaseString、capitalizedString等选择子。这张映射表中的每个选择子都映射到了不同的IMP之上:
Objective-C运行期系统提供的几个方法都能够用来操作这张表。开发者可以向其中新增选择子,也可以改变某选择子所对应的方法实现,还可以交换两个选择子所映射到的指针。经过几次操作之后,类的方法表就会变成:
在新的映射表中,多了一个名为newSelector的选择子,capitalizedString的实现也变了,而lowercaseString与uppercaseString的实现则互换了。上述修改均无须编写子类,只要
修改了“方法表”的布局
,就会反映到程序中所有的NSString实例之上。交换方法实现,可用下列函数:
1
2// 此函数的两个参数表示待交换的两个方法实现
void method_exchangeImplementations(Method m1, Method m2)方法实现则可通过下列函数获得:
1
2// 此函数根据给定的选择从类中取出与之相关的方法。
Method class_getInstanceMethod(Class aClass, SEL aSelector)执行下列代码,即可交换前面提到的lowercaseString与uppercaseString方法实现:
1
2
3
4Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSString class], @selector(uppercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);
// 在NSString实例上调用lowercaseString,那么执行的将是uppercaseString的原有实现,反之亦然在实际应用中,直接交换两个方法实现的,意义并不大。因为lowercaseString与uppercaseString这两个方法已经各自实现的很好了,没必要再交换了。可以通过这一手段来
为既有的方法实现增添新功能
。比方说,想要在调用lowercaseString时记录某些信息,这时就可以通过交换方法实现来达成此目标:1
2
3@interface NSString (EOCMyAdditions)
-(NSString *)eoc_myLowercaseString;
@end
新方法的实现代码可以这样写:1
2
3
4
5-(NSString *)eoc_myLowercaseString {
NSString *lowercase = [self eoc_myLowercaseString];
NSLog(@"%@ => %@",self,lowercase);
return lowercase;
}
这段代码看上去好像会陷入递归调用的死循环,不过大家要记住,此方法是准备和lowercaseString方法互换的。所以,在运行期,eoc_myLowercaseString选择子实际上对应于原有的lowercaseString方法实现。最后通过下列代码来交换这两个方法实现:1
2
3Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSString class], @selector(eoc_myLowercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);
执行完上述代码之后,只要在NSString实例上调用lowercaseString方法,就会输出一行记录消息:1
2
3
4NSString *string = @"ThIs iS tHe StRiNg";
NSString *lowercaseString = [string lowercaseString];
NSLog(@"lowercaseString = %@",lowercaseString);
//ThIs iS tHe StRiNg => this is the string
通过此方案,开发者可以为那些“完全不知道其具体实现的”(completely opaque,“完全不透明的”)黑盒方法增加日志记录功能
,这非常有助于程序调试。然而,此做法只在调试程序时有用。很少人在调试程序之外的场合用上述“方法调配技术”来永久改动某个类的功能。不能仅仅因为Objective-C语言有这个特性就一定要用它。若是滥用,反而会令代码变得不易读懂且难于维护
。
- 要点:
- 在运行期,可以向类中新增或替换选择子所对应的方法实现。
- 使用另一份实现来替换原有的方法实现,这道工序叫做“方法调配”,开发者常用此技术向原有实现中添加新功能。
- 一般来说,只有调试程序的时候才需要在运行期修改方法实现,这种做法不宜滥用。
14. 理解“类对象”的用意
- 对象类型并非在编译期就绑定好了,而是要在运行期查找,有个特殊的类型叫做 id,它能指代任意的Objective-C对象类型
“在运行期检视对象类型”
这一操作也叫做“类型信息查询”
(introspection,“内省”),这个强大而有用的特性内置于Foudation框架的NSObject协议里,凡是由公共根类(common root class,即NSObject与NSProxy)继承而来的对象都要遵从此协议
。在程序中不要直接比较对象所属的类,明智的做法是调用“类型信息查询方法”。每个Objective-C对象实例都是指向某块内存数据的指针。所以在声明变量时,类型后面要跟一个“*”字符:
1
2
3NSString *pointerVariable = @“Some string”;
// pointerVariable可以理解成存放内存地址的变量,而NSString本身的数据就存放于那个地址中
// 所有Objective-C对象都是如此,若是想把对象所需的内存分配在栈上,编译器则会报错对于通用的对象类型id,由于其本身已经是指针了,所以我们能够这样写:
1
2
3id genericTypedString = @“Some string”;
// 这种定义方式与用NSString*来定义相比,其语法意义相同
// 唯一区别在于如果声明时指定了具体类型,那么在该类实例上调用其所没有的方法时,编译器会探知此情况,并发出警告信息。描述Objective-C对象所用的数据结构定义在运行期程序库的头文件里,id类型本身也定义在这里:
1
2
3typedef struct objc_object{
Class isa;
} *id;
由此可见,每个对象结构体的首个成员是Class类型的变量
。该变量定义了对象所属的类,通常称为“is a”指针
。例如,刚才例子中所用的对象“是一个”(is a)NSString,所以其“is a”指针就指向NSString。
Class对象也定义在运行期程序库的头文件中:
1
2
3
4
5
6
7
8
9
10
11
12
13typedef struct objc_class *Class;
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
Class super_class;
const char *name;
long version;
long info;
long instance_size;
struct objc_ivar_list *ivars;
struct objc_method_list **methodLists;
struct objc_cache *cache;
struct objc_protocol_list *protocols;
};此结构体存放类的
“元数据”
(metadata),例如类的实例实现了几个方法,具备多个实例变量等信息。此结构体的首个变量也是isa指针,这说明Class本身亦为Objective-C对象。结构体里还有个变量叫做super_class,它定义了本类的超类
。类对象所属的类型(也就是isa指针所指向的类型)是另外一个类,叫做“元类”
(metaclass),用来表述类对象本身所具备的元数据。“类方法”就定义于此处,因为这些方法可以理解成类对象的实例方法。每个类仅有一个“类对象”,而每个“类对象”仅有一个与之相关的“元类”。假设有个名为SomeClass的子类从NSObject中继承而来,则其继承体系如图:
super_class
指针确立了继承关系,而isa指针
描述了实例所属的类。通过这张布局关系图即可执行“类型信息查询”。我们可以查出对象是否能够响应某个选择子,是否遵从某项协议,并且能看出此对象位于“类继承体系”(class hierarchy)的哪一部分。
在类继承体系中查询类型信息
可以用类型信息查询方法来检视类继承体系。
“isMemberOfClass:”
能够判断出对象是否为某个特定类的实例,而“isKindOfClass:”
则能够判断出对象是否为某类或其派生类的实例。1
2
3
4
5NSMutableDictionary *dict = [NSMutableDictionary new];
[dict isMemberOfClass:[NSDictionary class]];//NO
[dict isMemberOfClass:[NSMutableDictionary class]];//YES
[dict isKindOfClass:[NSDictionary class]];//YES
[dict isKindOfClass:[NSArray class]];//NO
像这样的类型信息查询方法使用isa指针获取对象所属的类,然后通过super_class指针在继承体系中游走。由于对象是动态的,所以此特性显得极为重要。
- 由于Objective-C使用“动态类型系统”(dynamic typing),所以用于查询对象所属类的类型信息查询功能非常有用。从collection中获取对象时,通常会查询类型信息,这些对象不是“强类型的”(strongly typed),把它们从collection中取出来时,其类型通常是id。如果想知道具体类型,那就可以使用类型信息查询方法。
例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16-(NSString *)commaSeparatedStringFromObjects:(NSArray *)array{
NSMutableString *string = [NSMutableString new];
for(id object in array){
if([object isKindOfClass:[NSString class]]){
[string appendFormat:@"%@,",object];
}else if ([object isKindOfClass:[NSNumber class]]){
[string appendFormat:@"%d,",[object intValue]];
}else if ([object isKindOfClass:[NSData class]]){
NSString *base64Encoded = /*base 64 encodeed data*/;
[string appendFormat:@"%@",base64Encoded];
}else{
//Type not supported
}
}
return string;
}
也可以用比较类对象是否等同的办法来做。若是若此,那就要使用==操作符,而不要使用比较Objective-C对象时常用的“isEqual:”方法。原因在于,类对象是“单例”(singleton),在应用程序范围内,每个类的Class仅有一个实例。也就是说,另外一种可以精确判断出对象是否为某类实例的办法是:1
2
3
4id object = /*…*/;
if([object class]==[EOCSomeClass class]){
//‘object’ is an instance of EOCSomeClass
}
即便能这样做,我们也应该尽量使用类型信息查询方法,而不应该直接比较两个类对象是否等同,因为前者可以正确处理那些使用了消息传递机制的对象。比方说,某个对象可能会把其收到的所有选择子都转发给另外一个对象。这样的对象叫做“代理”(proxy),此种对象均以NSProxy为根类。
通常情况下,如果在此种代理对象上调用class方法,那么返回的是代理对象本身(此类是NSProxy的子类),而非接受的代理的对象所属的类。然而,若是改用“isKindOfClass:”这样的类型信息查询方法,那么代理对象就会把这条消息转给“接受代理的对象”(proxied object)。也就是说,这条消息的返回值与直接在接受代理的对象上面查询其类型所得的结果相同。因此,这样查出来的类对象与通过class方法所返回的那个类对象不同,class方法所返回的类表示发起代理的对象,而非接受代理的对象。
- 要点
- 每个实例都有一个指向Class对象的指针,用以表明其类型,而这些Class对象则构成了类的继承体系。
- 如果对象类型无法在编译器确定,那么就应该使用类型信息查询方法来探知。
- 尽量使用类型信息查询方法来确定对象类型,而不要直接比较类对象,因为某些对象可能实现了消息转发功能。