第二章:对象、消息、运行期
6 理解属性这一概念 总结:
OC解决硬编码偏移量问题的做法:一种方案是把实例变量当做一种存储偏移量所用的特殊变量,交由类对象保管,偏移量会在运行期查找,叫做稳固的“应用程序二进制接口”ABI;二种方案是使用存取方法访问实例变量。属性的访问方法由编译器在编译期执行,并且编译器还会自动向类中添加实例变量。
eg:如果从core data的框架中的NSManagedObject类里继承一个子类,就需要在运行期动态创建存取方法。因为子类的某些属性不是实例变量,其数据来自后端的数据库中。
@interface EOCPerson:NSManagedObject
@property NSString *firstName;
@property NSString *lastName;
@end
@implementation EOCPerson
@dynamic firstName,lastName;//编译器在此时不会自动合成存取方法或实例变量。
@end
属性特质可以分为四类,原子性(nonatomic是不使用同步锁的意思),读/写权限,内存管理语义(只要实现属性所用的对象是可变的,就应该在设置新属性值时拷贝一份) 方法名@property(nonatomic,getter=isOn) BOOL on;
实现自定义的初始化方法时,一定要遵循属性定义中宣称的“copy”语义,_first = [firstName copy];使用于别的内存管理语义。
注意:不应该在init方法中调用存取方法;应该尽可能使用不可变对象。
7 在对象内部尽量直接访问实例变量 总结:
建议在对象内部读取实例变量的时候采用直接访问形式,而在设置实例变量的时候通过属性来做。
两种做法区别:直接访问实例变量速度快,编译器所生成的代码会直接访问保存对象实例变量的那块内存。但会绕过为相关属性定义的内存管理语义。不会触发键值观测。通过属性访问有助于排查相关错误。
通过设置方法写入实例变量时需要注意,在初始化方法中及dealloc方法中应该直接访问实例变量,对于初始化方法是因为子类可能会复写设置方法。在基类的默认初始化方法中,可能会将姓氏设为空字符串。此时若是通过设置方法来做,那么调用的将会是子类的设置方法,从而抛出异常。
- (void)setLastName:(NSString *)lastName
{
if(![lastName isEqualToString:@"smith"]){
[NSException raise:NSInvalidArgumentException format:@"Last name must be Smith"];
}
self.lastName = lastName;
}
另外:当要惰性初始化的时候,必须通过获取方法来访问属性,否则,实例变量就永远不会初始化。
8 理解“对象等同性”这一概念 总结:
判断等同性的两个关键方法:- (BOOL)isEqual:(id)object; - (NSUInteger)hash;编写hash方法时,应该用当前的对象做做实验,以便在减少碰撞频度与降低运算复杂程度之间取舍。有时候判断等同性可以根据特有的标识符,就像主键一样。
放入容器中的对象不应再改变,例如把某个对象放入set之后又修改其内容,那么后面的行为将很难预料。相同的对象必须具有相同的哈希码,但是两个哈希码相同的对象未必相同。不要盲目地逐个检测每条属性,而是应该依照具体需求来制定检测方案。编写hash方法时,应该使用计算速度快而且哈希码碰撞几率低的算法。
9 以类族模式隐藏实现细节 总结:
类族模式可以把实现细节隐藏在一套简单的公共接口后面。工厂模式是创建类族的办法之一。
Cocoa系统框架中大部分collection类都是类族。例如:用NSArray的alloc方法获取实例时,该方法首先会分配一个属于某类的实例,此实例充当“占位数组”。该数组稍后会转为另一个类的实例,而那个类则是NSArray的实体子类。
当向类族中新增实体子类时,对于Employee这个例子来说,若是没有工厂方法的源代码,就无法向其中新增雇员类别了。然而对于NSArray这样的类族来说,还是有办法增加子类的,但是从类族的公共抽象基类中继承子类时要当心,若有开发文档,应先阅读。
10 在既有类中使用关联对象存放自定义数据 总结:
有时需要在对象中存放相关信息,但是该类的实例可能是由某种机制所创建的,而我们无法令这种机制创建出自己缩写的子类实例。就要用关联对象(associated object)来解决这个问题。
objc_setAssociatedObject(object,void *key,id value,objc_AssociationPolicy),
objc_getAssociatedObject(object,key),
objc_removeAssociatedObjects(object)。
类似于NSDictionary,设置关联对象时用的键是不透明指针(opaque pointer),如果在两个键上调用isEqual方法的返回值是YES,那么NSDictionary就认为这相等。然后在设置关联对象时,若想令两个键匹配到同一个值,则二者必须是完全相同的指针才行。所以设置关联对象值时,通常使用静态全局变量做键static void *EOCMyAlertKey = "EOCMyAlertKey";。只有在其他做法不可行时才应选用关联对象,因为这种做法通常会引入难于查找的bug。
额外:关于类别(category)和类扩展(extern)
(1)category:
可以在不知道类的具体实现细节的情况下,为类添加方法。理论上不可添加变量,但是可以通过@dynamic方法,用关联引用添加。
类别中方法的优先级高于类中的。类别中不能调用super方法,这也是类别的一个局限。注意类别的方法可能会覆盖同一个类的其他类别的同名方法,也可能被覆盖,会引起编译器报错。所以命名最好加前缀来区别。
(2)extension:方法必须在.m文件中实现,否则会报错。
11 理解objc_msgSend的作用 总结:
边界情况(特殊情况),objc_msgSend_stret返回结构体,objc_msgSend_fpret返回浮点数,objc_msgSendSuper。
尾调用优化技术:如果某函数的最后一项操作是调用另外一个函数,而不会将其返回值另作他用时,才能执行“尾调用优化”。只保留内层函数的调用记录,如果所有函数都是尾调用,那么完全可以做到每次执行时,调用记录只有一项,这将大大节省内存。
消息由接收者,选择子及参数构成。给某对象发送消息,也就相当于在该对象上调用方法。发给某对象的全部消息都要有动态消息派发系统来处理,该系统会查出对应的方法,并执行其代码。
12 理解消息转发机制 总结:
消息转发分为两大阶段,一动态方法解析:先征询接收者,所述的类,看能否添加动态方法;二完整的消息转发机制:先看有没有其他对象能处理这条消息。若没有则启动完整的消息转发机制,运行期系统会把与消息有关的全部细节都封装到NSInvocation对象中。
+ (BOOL)resolveInstanceMethod:(SEL)selector;(尚未实现的是实例方法调用这个方法)
+(BOOL)resolveClassMethod:(SEL)selector;(尚未实现的是类方法)
例如:
用+ (BOOL)resolveInstanceMethod:(SEL)selector实现@dynamic属性return YES;或者是return [super resovleInstanceMethod:selector];动态添加方法
添加方法使用BOOL class_addMethod(cls,sel name,imp,const char *types)
其中:cls被添加方法的类,name方法名(可自己随便取名),imp实现方法的函数,types定义该函数返回值类型和参数类型的字符串,
- (int)saySomething:(NSString *)str;等同于int saySomething(id self, SEL _cmd,NSString str)
types就是i@:@其中 i代表返回值,@代表self,:代表_cmd,@代表str
具体可见官方文档 :https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html
- (id)forwardingTargetForSelector:(SEL)selector;可以模拟出多重继承的某些特性,但是无法操作由这一步所转发的消息。
- (void)forwardInvocation:(NSInvocation*)invocation;一是改变调用目标,但是与上一步效果等效。二是出发消息前,先以某种方式改变消息内容,追加另外一个参数,或是改换选择子。
步骤越往后,处理消息的代价就越大。
CALayer是兼容与键值编码的容器类,就是说,能够向里面随意添加属性,然后以键值对的形式来访问。属性值的存储工作由基类直接负责,我们只需在CALayer的子类中定义新属性即可。
若对象无法响应某个选择子,则进入消息转发流程。通过运行期的动态方法解析功能,我们可以在需要用到某个方法时再将其加入类中。对象可以把其无法解读的某些选择子转交给其他对象来处理。经过上述两步之后,如果还是没有办法处理选择子,那就启动完整的消息转发机制。
13 用方法调配技术调试黑盒方法 总结:
每个类都有一个选择子映射表,我们可以在程序运行过程中更改这个映射表。
总结:交换方法用void method_exchangeImplementations(Method m1,Method m2);方法实现用Method class_getInstanceMethod(class aClass,SEL aSelector)。通过此方案,可以为那些完全不知道其具体实现的黑盒方法增加日志记录功能,有助于程序调试。但若滥用,会使代码变得不易读懂且难于维护。
14 理解类对象的用意 总结:
在程序中不要直接比较对象所属的类,明智的做法是调用类型信息查询方法,isKindOfClass和isMemberOfClass,也可用比较类对象是否等同的办法来做。若是如此,就要使用==操作符,不要用isEqual,因为类对象是单利,在应用程序范围内,每个类的Class仅有一个实例。每个类仅有一个类对象,所以可以使用==,而每个类对象仅有一个与之相关的元类。尽量使用类型信息查询方法来确定对象类型,而不要直接比较对象,因为某些对象可能实现了消息转发功能,如果用直接比较的方法class返回的对象和查出来的类对象是不同的,class方法所返回的类表示发起代理的对象,而非接受代理的对象。