[iOS开发]——对象、消息、运行期
第6条:理解“属性”这一概念
什么是属性
“属性”(property)是OC的一项特性,用于封装对象中的数据。OC对象通常会把其所需要的数据保存为各种实例变量。
@property语法
我们在写代码的时候可以发现OC与其他面向对象的语言不同的地方在于OC很少去定义实例变量的作用域, 因为这样的话对象布局在编译期(compile time)就已经固定了。所以在访问到这些对象时,编译器就会把其替换为“偏移量”(offset),这个偏移量是“硬编码”(hardcode),表示该变量距离存放对象的内存区域的起始地址有多远。但是如果我们又加了一个实例变量会发生什么样的事情呢?
我们可以看到在加了一个实例变量后,以前的实例变量都发生了变化,如果代码使用的是编译期计算出来的偏移量,那么修改类定义之后必须重新编译,否则就会出错。道理很简单,就是地址变了,如果还按照以前的地址,肯定就会出错了。OC的做法是,把实例变量当作一种存储偏移量所用的“特殊变量”(special variable),交由“类对象”(class object)保管。偏移量会在运行期查找, 如果类的定义变了,那么存储的偏移量也就变了,这样的话,无论何时访问实例变量,总能使用正确的偏移量。
另外一种方法就是尽量不要直接访问实例变量,而应该通过存取方法来做。其中,“获取方法”(getter)用于读取变量值,而“设置方法”(setter)用于写入变量值。但是要访问属性,用“点语法”也可以办到,而且会比存取方法更方便好用。
使用属性,编译器会自动编写这些属性所需的方法,此过程叫做“自动合成”(autosynthesis)。
如果我们不想令编译器自动合成存取方法,那我们应该怎么做呢?
那就是使用@dynamic关键字了,它会告诉编译器:不要自动创建实现属性所用的实例变量,也不要为其创建存取方法。然后我们就需要在运行期动态创建存取方法了。
属性特质
使用属性时还有一个要注意的问题就是属性的各种特质也会影响编译器所生成的存取方法。
原子性
- atomic:默认值,原子性访问,单线程访问,表示如果有多个线程同时调用setter的话,不会出现某一个线程执行setter全部语句之前,另一个线程开始执行setter情况,安全性高于nonatomic,性能低于nonatomic,但atomic 并不代表线程安全,只是说对同一对象的set和get的操作是顺序执行的。值得我们注意的一点是atomic是默认值,假如属性不具备nonatomic特质,那么它就默认具备atomic。
- nonatomic:非原子性访问,可以多线程并发访问,oc中大多数都申明nonatomic属性。
读写权限
- readwrite:默认值,可读写。具备readwrite特质的属性拥有“获取方法”(getter)和“设置方法”(setter)。若该属性由@synthesize实现,则编译器会自动生成这两个方法。
- readonly:不能修改,只能读取,不生成setter方法。可以在“class—continuation分类”中重新定义为读写属性。
内存管理语义
属性用于封装数据,而数据则要有“具体的所有权定义”(concrete owenership semantic)。编译器在合成存取方法时,要根据此特质来决定生成的代码。如果自己编写存取方法,那么就必须同有关属性所具备的特质相符。
- assign:默认值,直接赋值, 主要是对基本数据类型使用:NSInteger,CGFloat 和C语言的 int double float char …
- retain:先release旧的对象,新对象的计数加1,并返回地址给引用者,主要对NSObject与其子类中使用。
- strong:强引用,ARC模式下与retain同作用。此特质表明该属性定义了一种“拥有关系”。为这种属性设置新值时,设置方法会先保留新值,并释放旧值,然后再将新值设置上去。
- weak:弱引用,ARC模式下与assign同作用。此特质表明该属性定义了一种“非拥有关系”。为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。然后在属性所指对象遭到摧毁时,属性值也会清空(nil out)。
- unsafe_unretained:此特质的语义和assign相同,但是它适用于“对象类型”(object type),该特质表达一种“非拥有关系”(“不保留”,unretained),当目标对象遭到摧毁时,属性值不会自动清空(“不安全”,unsafe),这一点与weak不同。
- copy:创建一个新对象,将旧对象的值赋值给新对象,release旧对象。 copy与retain的区别为:retain是指针拷贝,copy是内容拷贝,其主要对字符串NSString使用。
方法名
可通过如下特质来指定存取方法的方法名:
总结
- 可以用@property语法来定义对象中封装的数据。
- 通过“特质”来指定存储数据所需的正确语义。
- 在设置属性所对应的实例变量时,一定要遵从该属性所声明的语义。
- 开发iOS程序时应该使用nonatomic属性,因为atomic属性会严重影响性能。
第7条:在对象内部尽量直接访问实例变量
通过属性访问与直接访问
在读取实例变量的时候采用直接访问的形式,而在设置实例变量的时候通过属性来做。
两者的区别
- 由于不经过OC的方法派发步骤,所以直接访问实例变量的速度比较快。
- 直接访问实例变量时,不会调用其“设置方法”,会绕过为相关属性所定义的“内存管理语义”。
- 直接访问实例变量,不会触发KVO。
- 通过属性来访问(点语法)相当于在setter/getter处加了“断点”,可以排查错误。
要点
- 在对象内部读取数据时,应该直接通过实例变量来读,而写入数据时,则应通过属性来写。
- 在初始化方法及dealloc方法中,总是应该直接通过实例变量来读写数据。
- 使用懒加载时需要通过属性来读取数据。
第8条:理解“对象等同性”这一概念
几种比较的方法
- “==”操作符,比较的是两个指针本身,而不是其所指的对象。
- “isEqual”:一般两个类型不同的对象总是不相等的。如果“isEqual:”方法判定两个对象相等,那么其hash方法也必须返回同一个值。但是,如果两个对象的hash方法返回同一个值,那么“isEqual:”方法未必会认为两者相等。
- “isEqualToString”这是一个独特的判断方法,它的传递对象必须是NSString,否则结果未定义(undefined)。
- hash方法,根据等同性约定:若两个对象相等,则其哈希码也相等,但是两个哈希码相同的对象却也未必相等。在编写hash方法时,我们应该使用计算速度快效率高而且哈希码碰撞几率低的算法,这样我们消耗的内存会比较小。
特定类所具有的等同性判断
除了NSString之外,NSArray与NSDictionary类也具有特殊的等同性判定方法,分别是:“isEqualToArray:”和“isEqualToDictionary:”。但是这种方法仅限于对象是数组或字典,否则就会抛出异常。
如果需要经常判断等同性,那么可能会自己来创建等同性判定方法,因为无需检验参数类型,所以能大大提升检测效率,还有一个原因就是可以增加代码的可读性,让代码更美观。
在编写判定方法时,也应一并覆写“isEqual:”方法。后者的常见实现方法为:如果受测的参数与接受该信息的对象都属于同一个类,那么就调用自己编写的方法,否则就交由超类判断。
容器中可变类的等同性
在容器中放入可变类对象的时候,把某个对象放入collection之后,就不应该再改变其哈希码了。因为collection会把各个对象按照其哈希码分装到不同的“箱子数组”中,假如已经放入了一个箱子数组里,结果它的哈希码又变了,那么其所在的箱子对它来说就是错的。
要点
- 若想检测对象的等同性,请提供“isEqual:”与hash方法。
- 相同的对象必须具有相同的哈希码,但是两个哈希码相同的对象却未必相同。
- 不要盲目地逐个检测每条属性,而是应该依照具体需求来指定检测方案。
- 编写hash方法时,应该使用计算速度快而且哈希码碰撞几率低的算法。
第9条:以“类族模式”隐藏实现细节
关于“类族模式”这个名词大家听起来比较陌生,但是在很多人去写代码的时候,应该已经用过了。“类族”是一种很有用的模式,它可以隐藏“抽象基类”背后的实现细节。OC的系统框架中普遍使用此模式。类族模式的优点是可以灵活应对多个类,将它们的实现细节隐藏在抽象基类后面,以保持接口简洁。 用户无需自己创建子类实例,只需调用基类方法来创建即可。
创建类族
首先我们在创建类族之前需要去理解什么是类族?举个例子,假如你是一家公司的领导人,你要去管理你的员工,你的员工有连个属性分别是“姓名”“薪水”,你要去命令他执行每日的任务。作为管理者,你不需要管他完成任务的过程是怎么样的,你只需要知道他完成了还是没完成。
首先我们需要去定义这个类:
//定义员工类型
typedef NS_ENUM(NSUInteger, EOCEmployeeType) {
EOCEmployeeTypeDeveloper,
EOCEmployeeTypeDesiner,
EOCEmployeeTypeFinance
};
@interface EOCEmployee : NSObject
//定义属性
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSUInteger salary;
//定义方法
+ (EOCEmployee *)employeeWithType:(EOCEmployeeType)type;
- (void)doADaysWork;
@end
然后我们实现定义的方法:
@implementation EOCEmployee
+ (EOCEmployee *)employeeWithType:(EOCEmployeeType)type {
switch (type) {
case EOCEmployeeTypeDeveloper:
return [EOCEmployeeTypeDeveloper new];
break;
case EOCEmployeeTypeDesiner:
return [EOCEmployeeTypeDesiner new];
break;
case EOCEmployeeTypeFinance:
return [EOCEmployeeTypeFinance new];
break;
}
}
- (void)doADaysWork {
// Subclasses implement this.
}
@end
每个“实体子类”(concrete subclass)都是基类继承而来:
@interface EOCEmployeeTypeDeveloper : EOCEmployee
@end
@@implementation EOCEmployeeTypeDeveloper
- (void)doADaysWork {
[self writeCode];
}
@end
在本例中,基类实现了一个“类方法”,该方法根据雇员的类别分配好对应的雇员类实例。这种“工厂模式”(Factory pattern)是创建类族的方法之一。
另外我们还需要注意的一点就是:如果对象所属的类位于某个类族中,那么在查询其类型信息时,你可能觉得自己创建类某个类的实例,然而实际上创建的却是其子类的实例。
Cocoa里的类族
NSArray和NSMutableArray,实际上有两个抽象基类,一个用于不可变数组,另一个用于可变数组。尽管具备公共接口的类有两个,但仍然可以合起来算作一个类。
若想判断某对象是否位于类族中,不要直接检测两个“类对象”是否相等,应该:
id maybeAnArray = /*...*/
if ([maybeAnArray isKindOfClass:[Array class]]) {
NSLog ("!");
}
这样实际上是在判断这个类(maybeAnArray)是否是这个类(Array)或者这个类的子类(Array)的实例。
对于Cocoa中NSArray这样的类族来说,要想新增子类,需要遵守几条规矩:
- 子类应该继承自类族中的抽象基类。
若要编写NSArray类族的子类,则需令其继承自不可变数组的基类活或可变数组的基类。 - 子类应该定义自己的数据存储方式。
开发者编写NSArray子类时,经常在这个问题上受阻。子类必须用一个实例变量来存放数组中的对象。NSArray本身只不过是包在其他隐藏对象外面的壳,它仅仅定义了所有数组都需具备的一些接口,并不能自己保存那些对象。对于这个自定义的数组子类来说,可以用NSArray来保存其实例。 - 子类应当覆写父类文档中指明需要覆写的方法。
在每个抽象基类中,都有一些子类必须覆写的方法。比如说,想要编写NSArray的子类,就需要实现“count” 及 “objectAtIndex:”方法。像lastObject这种方法则无须实现,因为父类可以根据前面两个方法实现出这个方法。
要点
- 类族模式可以把实现细节隐藏在一套简单的公共接口后面
- 系统框架中经常使用类族
- 从类族的公共抽象基类中继承子类要当心,若有开发文档,则应首先阅读。
第10条:在既有类中使用关联对象存放自定义数据
有时候需要在对象中存放相关信息。这是我们通常会从对象所属的类中继承一个子类,然后改用这个子类的对象。然而并非所有情况下都能这么做,有时候类的实例可能是由某种机制所创建的,而开发者无法令这种机制创建出自己所写的子类实例。那怎么办呢?那就是关联对象(Associated Object)。
对象关联类型
管理关联对象的方法
//此方法以给定的健和策略为某对象设置关联对象值
void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy)
//此方法根据给定的键从某对象中获取相应的关联对象值
id objc_getAssociatedObject(id object,void *key)
//此方法移除制定对象的全部关联对象
void objc_removeAssociatedObjects(id object)
要点
- 可以通过“关联对象”机制来把两个对象连起来。
- 定义关联对象时可指定内存管理语义,用以模仿定义属性时所采用的“拥有关系”与“非拥有关系”。
- 只有在其他做法不可行时才应该选用关联对象,因为这种做法通常会引入难于查找的bug。
第11条:理解objc_msgSend的作用
在OC中,有一个名词叫“消息传递”(pass a message)。消息有“名称”(name)或“选择子”(selector),可以接受参数,而且可能还有返回值。
在OC中,如果向某对象传递信息,那就会使用动态绑定机制来决定需要的方法。为什么OC是真正的动态语言呢?因为对象收到信息之后,究竟该调用哪个方法则完全于运行期决定,甚至可以在程序运行时改变。
给对象发送信息:
id returnValue = [someObject messageName:parameter];
在本例中,someObject 叫做“接收者”(receiver),messageName 叫做“选择子”(selector)。选择子与参数合起来称为“消息”(message)。给某对象发送消息,也就相当于在该对象上“调用方法”。编译器看到此消息后,将其转换为一条标准的 C 语言函数调用,所调用的函数乃是消息传递机制中的核心函数,叫做 objc_msgSend,其“原型”(prototype)如下:
1 void objc_msgSend(id self, SEL cmd, ...)
这是个“参数个数可变的函数”(variadic function)(可变参数函数),能接受两个或两个以上的参数。第一个参数代表接收者,第二个参数代表选择子(SEL 是选择子的类型),后续参数就是消息中的那些参数,其顺序不变。选择子指的就是方法的名字。“选择子”与“方法”这两个词经常交替使用。编译器会把刚才那个例子中的消息转换为如下函数:
id returnValue = objc_msgSend(someObject, @selectro(messageName:), parameter);
objc_msgSend 函数会依据接收者与选择子的类型来调用适当的方法。为了完成此操作,该方法需要在接收者所属的类中搜寻其“方法列表”(list of methods),如果能找到与选择子名称相符的方法,就跳至其实现代码。若是找不到,那就沿着继承体系继续向上查找,等找到合适的方法之后再跳转。如果最终还是找不到相符的方法,那就执行“消息转发”(message forwarding)操作。
这么说来,想调用一个方法似乎需要很多步骤。所幸 objc_msgSend 会将匹配结果缓存在“快速映射表”(fast map)里面,每个类都有这样一块缓存,若是稍后还向该类发送与选择子相同的消息,那么执行起来就很快了。 当然了,这种“快速执行路径”(fast path)还是不如“静态绑定的函数调用操作”(statically bound function call)那样迅速,不过只要把选择子缓存起来了,也就不会慢很多,实际上,消息派发(message dispatch)并非应用程序的瓶颈所在。假如真是个瓶颈的话,那你可以只编写纯 C 函数,在调用时根据需要,把 Objective-C 对象的状态传进去。
前面讲的这部分内容只描述了部分消息的调用过程,其他 “边界情况”(edge case)(可理解为特殊情况)则需要交由 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 和 objc_msgSend_fpret 等效的函数,用于处理发给 super 的相应消息。
刚才提到,objc_msgSend等函数一旦找到应该调用的方法实现之后,就会“跳转过去”。 之所以这样做,是因为OC对象的每个方法都可以视为简单的C函数,其原型如下:
<return_type> Class_selector(id self, SEL _cmd, ...)
真正的函数名和上面写的可能不太一样,笔者用“类”(class)和“选择子”(selector)来命名是想解释其工作原理。每个类里都有一张表格,其中的指针都会指向这种函数,而选择子的名称则是查表时所用的“键”。objc_msgSend 等函数正是通过这张表格来寻找应该执行的方法并跳至其实现的。请注意,原型的样子和 objc_msgSend 函数很像。这不是巧合,而是为了利用“尾调用优化”(tail-call optimization)(又称“尾递归优化”),技术,令 “跳至方法实现”这一操作变得更简单些。
如果某函数的最后一项操作是调用另外一个函数,那么就可以运用 “尾调用优化”技术。 编译器会生成调转至另一函数所需的指令码,而且不会向调用栈中推入新的 “栈帧”(frame stack)。只有当某函数的最后一个操作仅仅是调用其他函数而不会将其返回值另做他用时,才能执行“尾调用优化”。这项优化对 objc_msgSend 非常关键,如果不这么做的话,那么每次调用 Objective-C 方法之前,都需要为调用 objc_msgSend 函数准备 “栈帧”,大家在 “栈踪迹”(stack trace)中可以看到这种 “栈帧”。此外,若是不优化,还会过早的发生“栈溢出”(stack overflow)现象。
在调试的时候,你就可以在栈“回溯”(backtrace)信息中发现总是出现 objc_msgSend。
要点
- 消息由接收者、选择子及参数构成。给某对象“发送消息”(invoke a message)也就相当于在该对象上“调用方法”(call a method)。
- 发给某对象的全部消息都要由“动态消息派发系统”(dynamic message dispatch system)来处理,该系统会查出对应的方法,并执行其代码。
第12条:理解消息转发机制
当对象接收到无法解读的消息后,就会启动“消息转发”(message forwarding)机制,程序员可经由此过程中告诉对象应该如何处理未知消息。
如果在控制台中看到下面这种提示消息,那就说明你曾向某个对象发送过一条其无法解读的消息,从而启动了消息转发机制,并将此消息转发给了NSObject的默认实现:
上面这段异常信息是由NSObject的“doesNotRecognizeSelector:”方法所抛出的。因为NSNumber类里奔来就没有名为lowercaseString的方法。
消息转发分为两大阶段。第一阶段先征询接收者,所属的类,看其是否能动态添加方法,已处理当前这个“未知的选择子”(unknown selector),这叫做“动态方法解析”(dynamic method resolution)。第二阶段涉及“完整的消息转发机制”(full forwarding mechanism)。如果运行期系统已经把第一阶段执行完了,那么接收者自己就无法再以动态新增方法的手段来响应包含该选择子的消息了。此时,运行期系统会请求接收者以其他手段来处理与消息相关的方法调用。这又细分为两小步。首先,请接收者看看有没有其他对象能处理这条信息。若有,则运行期系统会把消息转给那个对象,于是消息转发过程结束,一切正常。若没有“备源的接收者”(replacement receiver),则启动完整的消息转发机制,运行期系统会把与消息有关的全部细节都封装到NSInvocation对象中,再给接收者最后一次机会,令其设法解决当前还未处理的这条信息。
动态方法解析
对象在收到无法解读的消息后,首先调用其所属的下列方法:
+ (BOOL)resolveInstanceMethod:(SEL)selector
该方法的参数就是那个未知的选择子,其返回值为BOOLean类型,表示这个类是否能新增一个实例方法用以处理此选择子。在继续往下执行转发机制之前,本类有机会新增一个处理此选择子的方法。假如尚未实现的方法不是实例方法而是类方法,那么运行期系统就会调用另一个方法,该方法与“resolveInstanceMethod:”类型,叫做“resolveClassMethod:”。
备用接收者
当前接收者还有第二次机会能处理未知的选择子,在这一步中,运行期系统 会问它:能不能把这条消息转给其他接受者来处理。与该步骤对应的处理方法如下:
- (id)forwardingTargetForSelector:(SEL)selector
方法参数代表未知的选择子,若当前接收者能找到备援对象,则将其返回,若找不到,就返回nil。通过此方案,我们可以用“组合”(composition)来模拟出“多重继承”(multiple inheritance)的某些特征。在一个对象内部,可能还有一系列其他对象,该对象可经由此方法将能够处理某选择子的相关内部对象返回,这样的话,在外界看来,好像是该对象亲自处理了这些信息似的。
完整的消息转发
首先创建NSInvocation对象,把与尚未处理的那条消息有关的全部细节都封于其中。此对象包含选择子、目标(target)及参数。在触发NSInvocation对象时,“消息派发系统”(message-dispatch system)将亲自出马,把消息指派给目标对象。
此步骤会调用下列方法来转发消息:
- (void)forwardInvocation:(NSInvocation *)invocation;
这个方法可以实现得很简单,只需要改变调用目标,使消息在新目标上得以调用即可。比较有用的实现方式为:在触发消息前,先以某种方式改变消息内容,比如追加另外一个参数,或是改换选择子,等等。
实现此方法时,若发现某调用操作不应由本类来处理,则需调用超类的同名方法。这样的话,继承体系中的每个类都有机会处理此调用请求,直至NSObject。如果最后调用NSObject类的方法,那么该方法还会继而调用“doesNotRecognizeSelector:”以抛出异常,此异常表明选择子最终未能得到处理。
消息转发全流程
要点
- 若对象 无法响应某个选择子,则进入消息转发流程;
- 通过运行期的动态方法解析功能,我们可以在需要用到某个方法时再将其加入类中
- 对象可以把其无法解读的某些选择子转交给其他对象来处理
- 经过上述两步之后,如果还是没有处理选择子,那就启动完整的消息转发机制;
第13条:用“方法调配技术”调试“黑盒方法”
方法调配(method swizzling)
类的方法列表会把选择子的名称映射到相关的方法实现之上,使得“动态消息派发系统”能够据此找到应该调用的方法。这些方法均以函数指针的形式来表示,这种指针叫IMP,其原型如下:
id (*IMP)(id, SEL, ...)
NSString 类可以响应 lowercaseSting、uppercaseString、capitalizedString 等选择子,这张映射表中的每个选择子都映射到了不同的 IMP 之上:
Objective-C 运行期系统提供的几个方法都够用来操作这张表。开发者可以向其中新增选择子,也可以改变选择子所对应的方法实现,还可以交换两个选择子所映射到的指针。经过几次操作之后,类的方法表就会变成这样:
在新的映射表中,多了一个名为newSelector的选择子,capitalizedString的实现也变了,而lowercaseString与uppercaseString的实现则交换了。上述修改均无须编写子类,只要修改了“方法表”的布局,就会映射到程序中所有的NSString实例之上。
想交换方法实现,可用下列函数:
void method_exchangeImplementations(Method m1, Method, m2)
此函数的两个参数表示待交换的两个方法实现,而方法实现则可通过下列函数获得:
Method class_getInstanceMethod(Class aClass, SEL aSelector)
此函数根据给定的选择从类中取出与之相关的方法。执行下列代码,即可交换前面提到的 lowercaseString 与 uppercaseString 方法实现:
Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSString class], @selector(uppercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);
我们可以通过一个手段来为既有的方法实现增添新功能。我们新编写一个方法,在此方法中实现所需的附加功能,并调用原有实现。
新方法可以添加至NSString的一个“分类”(category)中:
@implementation NSString (EOCMyAdditions)
- (NSString *)eoc_myLowercaseString
{
NSString *lowercase = [self eoc_myLowercaseString];
NSLog(@"%@ => %@", self, lowercase);
return lowercase;
}
@end
这段代码看起来像是死循环,不过大家要记住,此方法是准备和lowercaseString方法互换的。所以,在运行期,eoc_myLowercaseString选择子实际上对应于原有的lowercaseString方法实现。最后通过下列代码来交换这两个方法实现:
Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSString class], @selector(eoc_myLowercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);
要点
- 在运行期,可以向类中新增或替换选择子所对应的方法实现。
- 使用另一份实现来替换原有的方法实现,这道工序叫做“方法调配”,开发者常用此技术向原有实现中添加新功能。
- 一般来说,只有调试程序的时候才需要在运行期修改方法实现,这种做法不宜滥用。
第14条:理解“类对象”的用意
每个OC对象实例都是指向某块内存数据的指针。所以在声明后面要跟一个“*”字符:
NSString *pointerVariable = @"Some string";
描述OC对象所用的数据结构定义在运行期程序库的头文件里,id类型本身也定义在这里:
typedef struct objc_object {
Class isa;
} *id;
由此可见,每个对象结构体的首个成员是Class类的变量。该变量定义了对象所属的类,通常称为“is a”指针。例如,刚才的例子中所用的对象“是一个”(is a)NSString,所以其“is a”指针就指向NSString。Class对象也定义在运行期程序库的头文件中:
typedef struct objc_class *Class;
struct objc_class {
Class isa;
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中继承而来,则其继承体系:
在类继承体系中查询类型信息
可以用类型信息查询方法来检视类继承体系。
“isMemberOfClass:”能够判断出对象是否为某个特定类的实例。
“isKindOfClass:”则能够判断出对象是否为某类或其派生类的实例。
由于Objective-C使用“动态类型系统”(dynamic typing),所以用于查询对象所属类的类型信息查询功能非常有用。从collection中获取对象时,通常会查询类型信息,这些对象不是“强类型的”(strongly typed),把他们从collection中取出来时,其类型通常是id。如果想知道具体类型,那就可以使用类型信息查询方法:
也可以使用比较类对象是否等同的方法来做,但是我们这里只能使用“==”,不能用“isEqual:”方法,因为类对象是“单例”(singleton),在应用程序范围内,每个类的Class仅有一个实例,也就是说,另外一种可以精确判断出对象是否为某个类的实例的办法是:
要点
- 每个实例都有一个指向Class对象的指针,用以表明其类型,而这些Class对象则构成了类的继承体系。
- 如果对象类型无法在编译期确定,那么就应该使用类型信息查询方法探知。
- 尽量使用类型信息查询方法来确定对象类型,而不要直接比较类对象,因为某些对象可能实现了消息转发功能。