[toc]
第 6 条 对象、消息、运行期
消息传递
在对象之间传递数据并执行任务的过程就叫做消息传递
属性
- 属性是Objective-C的一项特性,用于封装对象中的数据。
- 也可以把属性当作一种简称,其意思是说:编译器会自动写出一套存取方法,用以访问给定类型中具有给定名称的变量。
作用域
public private protect
Objective-C 为什么很少定义变量的作用域
- 定义变量的作用域会导致对象布局在编译器就已经固定了(查看22页)
- 如果代码使用了编译器计算出来的偏移量,那么在修改类定义之后必须重新编译,否则就会出错。
dynamic
使用@dynamic关键字,他会告诉编译器:不要自动创建实现属性所用的实例变量,也不要为其创建存取方法。而且,在编译访问属性的代码时,即使编译器发现没有定义存取方法,也不会报错,他相信这些方法能在运行期找到。
property
- 生成属性
- 如果使用了属性的话,那么编译器就会自动编写访问这些属性所需的方法,此过程称为“自动合成”。需要强调的是,这个过程有编译器在编译期执行,所以编辑器看不到这些“合成方法“的源代码。除了生成方法之外,编译器还要自动向类中添加适当类型的实例变量,并且在属性名前加下划线,以此作为实例变量的名字。
synthesize
- 使用@synthesize语法来指定实例变量的名字。
- 可以解决setter getter方法不能同时存在的问题。
属性的特质:
原子性
- 只作用域setter 和 getter方法
读写权限
- 具备readwrite特质的属性拥有“获取方法”与“设置方法”。若属性由@synthesize实现,则编译器会自动生成这两个方法
- 具备readonly特质的属性仅拥有获取方法,只有该属性由@synthesize实现时,编译器才会为其生成获取方法。
内存管理的语义 (需要详细查看)
- assign:针对纯量类型
- strong:此特质表明该属性定义了一种“拥有关系”,为这种属性设置新值时,设置方法会先保留新值,并释放旧值,然后在将新值设置上去。
- weak:此特质表明了一种”非拥有关系“,为这种属性设置新值时,设置方法既不保留新值也不释放旧值,此特质同assign蕾丝,然而在属性所指的对象遭到摧毁时,属性值也会清空。
- copy:此特质所表达的所属关系与strong相似。然而设置方法并不保留新值,而是将其“拷贝”。
方法名:
- getter == name 指定“获取方法”的方法名
- setter = name 指定设置的方法名
为什么iOS开发中设置属性不用nonatomic
在iOS中使用同步锁的开销较大,这会带来性能问题。一般情况下并不要求属性必须是“原子的”,因为这并不能保证“线程安全”,若要实现“线程安全”的操作,还需采用更为深层的锁定机制才行。
要点
- 可以用@property语法来定义对象中锁封装的数据
- 通过“特质“来指定存储数据所需的正确语义
- 在设置属性所对应的实例变量时,一定要遵从该属性所声明的语义
- 开发iOS程序时应该使用nonatomic属性,因为atomic属性会严重影响性能
第 7 条 在对象内部尽量直接访问实例变量
直接访问_firstname的好处
- 由于不经过Objective-C的方法派发(第11条)步骤,所以直接访问实例变量的速度当然比较快。在这种情况下,编译器所生成的代码会直接访问保存对象实例变量的那块内存。如果调用属性,则会走类的方法列表
- 直接访问实例变量时,不会调用其“设置方法”,这就饶过了为相关属性所定义的“内存管理语义”
直接访问_firstname的坏处
- 如果直接访问实例变量,那么不会触发“键值观测” (kvo)通知
- 通过属性来访问,有助于排查与之相关的错误
第 8 条 理解“对象等同性”这一概念
isEqualString 比 isEqual快
因为后者要执行额外的步骤,因为他不知道受测对象的类型。
使用isEqual来判断对象等同性:
- 直接判断两个指针是否相同,相同直接返回yes
- 比较对象所属的类,不同则直接返回no
- 检测每个属性值是否相同
- 实现hash方法
注意:有时候我们可能认为:一个EOCPerson实例可以与其子类(比如EOCSmithPerson)实例相等。在继承体系中判断等同性时,经常遭遇此类问题。所以实现 isEqual方法时需要考虑到这种情况
特定类所具有的等同性判定方法
详见 33页
等同性判定的执行深度
NSArry的检测方式为先看两个数组所含对象个数是否相等,若相等,则在每个对应位置的脸哥哥对象身上调用其“isEqual"方法。如果对应位置上的对象均相等,那么这两个数组就相等,这叫做“深度等同性判定”。
容器中可变类的等同性
把某个对象放入collection之后,就不应再改变其哈希码了。collection会把各个对象按照其哈希码分装到不同的“箱子数组”中。如果某对象在放入“箱子”之后哈希码又变了,那么现在所处的箱子对他来说是错误的
要点
- 若想检测对象的等同性,请提供“isEqual:”与hash方法
- 相同的对象必须具有相同的哈希码,但是两个哈希码相同的对象却未必相同
- 不要盲目的逐个检测每条属性,而是应该依照具体需求来制定监测方案
- 编写hash时,应该使用计算速度快而且哈希码碰撞几率低的算法
第 9 条 以 “类族模式” 隐藏实现细节
- 类族可以灵活应对多个类,经他们的实现细节隐藏在抽象积累后面,以保持接口整洁
- 工厂模式是创建类族的办法之一
- 系统框架中许多类族。大部分collection类都是类族。例如NSArry与其可变版本NAMutableArray。所以检测两个一个对象是不是数组的时候使用 [maybeAbArray class] == [NSArray class]是错误的。应该使用[maybeAnArray isKindOfClass:[NSArray class]]
- NSArray 本身只不过是包在其他隐藏对象外面的壳,他仅仅定义了多有数组都需要具备的一些接口
要点
- 类族模式可以把实现细节隐藏在一套简单的公共接口后面
- 系统框架中经常使用类族
- 从类族的公共抽象基类中继承子类时要当心,如有开发文档,则应首先阅读
第 10 条 在既有类中使用关联对象存放自定义数据
- (UIButton *)btn {
if (_btn == nil) {
_btn = [UIButton buttonWithType:UIButtonTypeCustom];
_btn.backgroundColor = [UIColor redColor];
_btn.frame = CGRectMake(100, 100, (self.view.frame.size.width - 100) / 2, 50);
[_btn addTarget:self action:@selector(clickBtn:) forControlEvents:UIControlEventTouchUpInside];
_btn.tag = 101;
}
void (^block)(NSInteger) = ^(NSInteger btnIndex){
NSLog(@"%ld",(long)btnIndex);
};
objc_setAssociatedObject(_btn, XSYBtnKey, block, OBJC_ASSOCIATION_COPY_NONATOMIC);
return _btn;
}
- (void)clickBtn:(UIButton *)btn {
void (^ block)(NSInteger) = objc_getAssociatedObject(btn, XSYBtnKey);
block(btn.tag);
}
第 11 条 理解objc_msgSend的作用
消息传递
在对象上调用方法时Objective-C中经常使用的功能。用Objective-C的术语来说,这叫做“消息传递”。消息有“名称 或 “选择子”,可以接受参数,而且可能还有返回值
for example
id returnValue = [someObject messageName:parameter];
someObject 叫做“接受者”。messageName叫做“选择子”。选择子与参数合起来称为“消息”。
编译器看到此消息后,将其转换成一条标准的c语言函数调用,所调用的函数乃是消息传递机制中的核心函数,叫做objc_magSend,其“原型”如下:
void objc_msgSend(id self, SEL cmd, ..)
查找类中的方法列表
objc_msgSend函数会依据接受者与选择子的类型来调用适当的方法。 为了完成完成此操作,该方法需要在接受者所属的类中搜索其“方法列表”,如果能找到与选择子名称相符的方法,就跳至其实现代码。如是找不到就沿着继承体系继续向上查找,等找到合适的方法之后再跳转。如果最终还是找不到相符的方法,那就执行“消息转发”操作。这么说来,香调用一个方法似乎需要很多的步骤。所幸的是objc_msgSend会将匹配结果缓存再“快速映射表”里面,每个类都有这样一块缓存,若是稍后还向该类发送与选择子相同的消息,那么执行起来就很快了。
选择子就是查表时所用的“键”
要点
- 消息由接受者、选择子及参数构成。给某对象发送消息,也就相当于在该对象上“调用方法”
- 发送给某对象的全部消息都要由“动态消息派发系统”(dynamic message dispatch system)来处理,该系统会查出对应的方法,并执行其代码
第 12 条 理解消息转发机制
当提示"unrecognized selector sent to instance" 时要执行消息转发的步骤
- 消息转发分为两个阶段:第一阶段先征询接受者所属的类,看其是否能动态添加方法,以处理当前这个“未知选择子”,这叫做“动态方法解析”(dynamic method resolution)
- 第二阶段涉及完整的消息转发机制(图 2-2)
动态方法解析
image.png- 第一步:类中的方法类表中含有此选择子
- 如果没有则执行以下消息转发流程
- 第二步:进行动态方法解析,调用 +(BOOL)resolveInstanceMethod:(SEL)selector 动态添加方法。使用这种方法的前提是:相关方法的实现代码已经写好,只等着运行的时候动态插在类里面就可以了。比如实现的那个 XSYAutoDictionary
- 第三步:备援接受者,这一步无法操作转发的消息 。在这一步中,运行期系统会问它:能不能把这条消息转给其他接受者来处理。与该步骤对应的处理方法如下:-(id)forwardingTargetForSelector:(SEL)selector.若能找到备援对象,则将其返回,若找不到就返回nil,通过此方案,我们可以用“组合”来模拟出“多重继承”的某些特性。在一个对象内部可能还有一系列其他对象,该对象可经由此方法将能够处理某选择子的相关内部对象返回,这样的话,在外界看来好像是该对象亲自处理了这些消息似的。比如 nstimer的弱引用
- 第四步 完成整的消息转发 调用 -(void)forwardInvocation:(NSInvocation*)invocation 。首先创建NSInvocation对象,把与尚未处理的那条消息有关的全部细节都封于其中,此对象包括选择子、目标及参数。在出发NSInvocation对象是,“消息派发系统”将亲自出马,吧消息指派给目标对象。实现此方法时若发现某调用操作不应有本类处理,则需调用超类的同名方法。这样的话继承体系中的每个类都有机会处理此调用请求,直至NSObject。
- 如果最后调用了NSObject类的方法,那么该方法还会继而调用“doesNotRecognizeSelector:"以抛出异常,此异常表明选择子最终未能得到处理
要点
- 若对象无法响应某个选择子,则进入消息转发流程
- 通过运行期的动态方法解析功能,我们可以在需要用到某方法时再将其加入类中
- 对象可以把其无法解读的某些选择子转交给其他对象来处理
- 经由上述两步之后,如果还是没办法处理选择子,那就启动完整的消息转发流程
第 13 条 用方法调配技术调试黑盒方法
与给定的选择子相对应的方法实现也是可以改变的原因
类的方法列表会把选择子的名称映射到相应的方法实现上,使得“动态消息派发系统”能够据此找到应该调用的方法。这些方法均以函数指针的形式来表示,这种指针叫做 IMP,原型 id (*IMP)(id, SEL, ...)
要点
- 在运行期,可以向类中新增或替换选择子对应的方法实现
- 使用另一份实现来替换原有的方法实现,这道工序叫做“方法调配”,开发者常用此技术项原有实现中添加新功能
第 14 条 理解类对象的用意
消息接受者究竟是何物?
运行期系统是如何赵浩到某个对象的类型的呢?
“在运行期检视对象类型”这一操作也叫做“类型信息查询(内省)”这个强大而有用的特性内置于Fundation框架的NSObject协议里。
编译器无法确定某类型对象到底能解读多少种选择子,因为运行期还可向其中动态新增
在类继承体系中查询类型信息
“isMemberOfClass"能够判断出对象是否为某个特定类的实例,而“isKindOfClass"则能够判断出对象是否为某个派生类的实例
判断类对象是否相等
id object = /.../
if ([object class] == [EOCSomeClass class]) {
}
原因在于:类对象是“单利”,在应用程序范围内,每个类的Class仅有一个实例。
要点
- 每个实例都有一个指向Class对象的指针,用以表明其类型,而这些Class对象则构成了继承体系
- 如果对象类型无法在编译器确定,那么就应该使用类型信息查询方法来探知
- 尽量使用类型信息查询方法来确定对象类型,而不要直接比较类对象,因为某些类对象可能实现了消息转发功能。
网友评论