对象、消息、运行期
对象 --- 在Objective-C等面向对象语言编程时,“对象”(object)就是“基本构造单元”,开发者可以通过对象来存储并传递数据。
消息 --- 在对象之间传递数据并执行任务的过程就叫做“消息传递”(Messaging)
运行期 --- 当应用程序运行起来以后,为其提供相关支持的代码叫做“Objective-C运行期环境”(Objectice-C runtime),它提供了一些使得对象之间能够传递消息的重要函数,并且包含创建类实例所用的全部逻辑。
六、理解“属性”这一概念
属性(property)是Objective-C的一项特性,用户封装对象中的数据,iOS开发中最常用最方便的变量声明方式,允许我们用点语法来访问对象的实例变量。实质上类似如下
属性 = 成员变量 + set方法 + get方法。
属性特质
@property(nonatomic,readwrite,copy,setter=<name>) NSString *firstName;
-
原子性
如果属性具备noatomic特质,则不使用同步锁,iOS由于性能原因,都是使用noatomic的,MacOS则多用atomic,使用同步锁。 -
读写权限
拥有readwrite特质的属性拥有获取方法getter和设置方法setter。
拥有readonly特质的属性仅拥有获取方法。可以在.h头文件中对外公开为只读属性,然后再.m的class-continuation分类中将其重新定义为读写属性。 -
内存管理语义
属性用于封装数据,而数据则要有“具体的所有权语义。内存管理语义这个特质仅会影响设置方法。-
assign 设置方法只会执行针对纯量类型(非OC对象)的简单赋值操作,如CGFloat,NSInteger
-
strong 定义了一种拥有关系。为这种属性设置新值时,设置方法会先保留新值,并释放旧值,然后再将新值设置上去。对象引用计数+1,功能等价于MRC里面的retain
-
weak 非拥有关系,为这种属性设置新值时,既不保留新值,也不释放旧值。然而在属性值所指对象遭到摧毁时,属性值也会被清空为nil。weak是为打破循环引用而生的
-
unsafe_unretained 类似weak,但是区别于在当目标对象遭到摧毁时,属性值不会自动清空,这是不安全的。不建议使用!
-
copy 拥有关系,实质上是设置为传入对象的copy对象的指针。设置完后,与传入的对象无关联。
@property (nonatomic, copy) NSString *stringCopy -(void)setStringCopy:(NSString *)stringCopy{ [_stringCopy release]; _stringCopy = [stringCopy copy]; }
当属性类型为NSString *时,经常用此特质来保护其封装性
-
-
方法名
@property(nonatomic,getter=isOn)Bool on; //getter=<name> 指定获取方法的方法名 //setter=<name>,这种用法一般不常用,没必要
特别注意:
如果想在其他方法里设置属性值,那么同样要遵守属性定义中所宣称的语义。如下代码:
@interface EOCPerson:NSManagedObject
@property(copy,readonly) NSString *firstName;
@property(copy,readonly) NSString *lastName;
-(id)initWithFirstName:(NSString *)firstName lastName:(NSString*)lastName;
@end
//.m文件中
-(id)initWithFirstName:(NSString *)firstName lastName:(NSString*)lastName{
if((self = [super init])){
_firstName = [firstName copy];
_lastName = [lastName copy];
}
}
在实现这个自定义初始化方法时,一定要遵循属性定义中宣称的”copy“语义,因为属性定义就相当于类和待设置的属性值之间达成的契约。
七、在对象内部尽量直接访问实例变量
- "A:在对象内部读取数据时,应该直接通过实例变量来读。B:写入数据时,则应该通过属性来写。" 这种方案的优点是:既能提高读取操作的速度,又能控制对属性的写入操作,使得相关属性的内存管理语义得以贯彻。
- 针对1中A读取内部数据的例外情况是,在使用懒加载初始化技术配置某个数据的话,应该通过属性来读取数据
- 针对1中B,写入数据的例外情况是,初始化方法和dealloc方法中,总是应该直接通过实例变量来读写数据
八、理解"对象等同性"这一概念
根据“等同性”(equality)来比较对象是一个非常有用的功能。按照==操作符比较出来的结果未必是我们想要的,应为该操作比较的是两个指针本身,而不是其所指的对象。 我们应该使用NSObject协议中声明的"isEqual":方法来判断两个对象的等同性。
-(BOOL)isEqual:(id)object{
if(self == object)
return YES;
if([self class] != [object class])
return NO;
EOCPerson *otherPerson = (EOCPerson *)object;
if(![_firstName isEqualToString:otherPerson.firstName])
return NO;
if(![_lastName isEqualToString:otherPerson.lastName])
return NO;
if(_age != otherPerson.age)
return NO;
return YES;
}
-(NSUInterger)hash {
NSUInteger firstNameHash = [_firstName hash];
NSUInteger lastNameHash = [_lastName hash];
NSUInteger ageHash = _age;
return firstNameHash ^ lastNameHash ^ ageHash;
}
- 若想检测对象的等同性,请提供“isEqual:”与hash方法
- 相同的对象必须具有相同的哈希码,但是两个哈希码相同的对象却未必相同
- 不要盲目地逐个检测每条属性,而是应该依照具体需求来定制检测方案。
- 编写hash方法时,应该使用计算速度快而哈希码碰撞几率低的算法
九、以“类族模式”隐藏实现细节
创建如NSArray类族的子类时,需要遵守几条规则:
- 子类应该继承自类族中的抽象基类
- 子类应该定义自己的数据存储方式
- 子类应当覆写超类文档中指明需要覆写的方法
要点:
- 类族模式可以把实现细节隐藏在一套简单的公共接口后面
- 系统框架中经常使用类族
- 从类族的公共抽象基类中继承子类时要当心,若有开发文档,则应首先阅读
十、在既有类中使用关联对象存放自定义数据
“关联对象”(Associated Object)是一个黑科技,在某些类无法继承出子类来存放额外信息时,可以用关联对象的方法,来增加键值对,达到给既定类存放额外信息的目的。
关联类型 | 等效的@property属性 |
---|---|
OBJC_ASSOCIATION_ASSIGN | assign |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | nonatomic,retain |
OBJC_ASSOCIATION_COPY_NONATOMIC | nonatomic,copy |
OBJC_ASSOCIATION_RETAIN | retain |
OBJC_ASSOCIATION_COPY | copy |
有以下方法管理关联对象:
void objc_setAssociatedObject(id object,void *key,id value,objc_AssociationPolicy)
//该方法以给定的键和策略为某对象设置关联对象值
id objc_getAssociatedObject(id object, void *key)
//此方法根据给定的键从某对象中获取相应的关联对象值
void objc_removeAssociatedObjects(id object)
//此方法移除指定对象的全部关联对象
特别注意: 设置关联对象时用的键key是个不透明的指针。设置关联对象值时,通常使用静态全局变量做键。
staic void *EOCMyALertViewKey = "EOCMyAlertViewKey";
//关联对象,绑定block
objc_setAssociatedObject(alertView,EOCMyAlertViewKey,block,OBJC_ASSOCIATION_COPY);
//取值,取出block
void(^block)(NSInteger) = objc_getAssociatedObject(alertView,EOCMyAlertViewKey);
- 可以通过“关联对象”机制来把两个对象连起来
- 定义关联对象时可以指定内存管理语义,用以模仿定义属性时所采用的“拥有关系”与“非拥有关系”
- 只有在其他做法不可行时才应选用 关联对象,因为这种做法通常会引入难以查找的bug,如循环引用
十一、理解objc_msgSend的作用
id returnValue = [someObject messageName:parameter];
someObject叫做"接收者"(receiver),messageName叫做“选择子”(selector),选择子与参数合起来称为“消息”(message)。 编译器看到此消息后,将其转换为一条标准的c语言函数调用,所调用的函数乃是消息传递机制中的核心函数,叫做objc_msgSend,其原型如下
void objc_msgSend(id self,SEL cmd, ...)
编译器会把上面例子中的消息转换为如下函数:
id returnValue = objc_msgSend(someObject,@selector(messageName:),parameter);
objc_msgSend函数会依据接收者与选择子的类型来调用适当的方法,为了完成此操作,该方法需要在接收者所属的类中搜寻其“方法列表”(list of methods),如果能找到与选择子名称相符的方法,就跳至其实现代码。若是找不到,那就沿着继承体系继续向上查找,等找到合适的方法再跳转,如果最终还是找不到相符的方法,那就执行消息转发(message forwarding)操作。
objc_msgSend会将匹配结果缓存在“快速映射表”(fast map)里面,每个类都有这样一块缓存,若是稍后还向该类发送与选择子相同的消息,那么执行就会很快了。
Objective-C运行环境中的另一些函数
- objc_msgSend_stret 如果待发送的消息要返回结构体,就交由该函数处理
- objc_msgSend_fpret 如果消息返回的是浮点数,则交由此函数处理
- objc_msgSendSuper 如果要给超类发消息,例如[super message:parameter],那么就交由次函数处理
如果某函数的最后一项操作是调用另外一个函数,那么就可以运用“尾调用技术”,编译器会生成调转至另一函数所需的指令码,而且不会向调用堆栈中推入新的栈幁。
要点:
- 消息由接收者、选择子以及参数构成。给某对象“发送消息",也就相当于在该对象上"调用方法"
- 发给某对象的全部消息都要由"动态消息派发系统"来处理,该系统会查出对应的方法,并执行其代码
十二、理解消息转发机制
当对象接收无法解读的消息后,就会启动"消息转发"(message forwarding)机制,程序员可经由此过程告诉对象应该如何处理未知消息。
消息转发分为两大阶段:
- 第一阶段先征询接收者,所属的类,看其是否能 动态添加方法,以处理当前这个"未知的选择子",这叫做"动态方法解析"(dynamic method resolution)
- 第二阶段涉及"完整的消息转发机制"(full forwarding mechanism),细分为两步:首先,请接收者看看有没有其他对象能处理这条消息,若有,则运行期系统会把消息转个那个对象,消息转发过程结束。其次,若没有备援的接收者(replacement receiver),则启动完整的消息转发机制,运行期系统会把与消息有关的全部细节都封装到NSInvocation对象中,再给接收者最后一次机会,令其设法解决当前未处理的这条消息
动态方法解析
对象在收到无法解读的消息后,首先调用其所属类的下列方法:
+(BOOL)resolveInstanceMethod:(SEL)selector
该方法的参数就是那个未知的选择子,令其返回值为Boolean类型,<u>表示这个类是否能新增一个实例方法用于处理此选择子</u>。在继续往下执行转发机制之前,本类有机会新增一个处理此选择子的方法。假如未实现的方法不是实例方法而是类方法,那么运行期系统就会调用另一个方法,叫做"resolveClassMethod:" 。
<u>使用动态方法解析的前提是:相关方法的实现代码已经写好,只等着运行的时候动态插在类里面就可以了</u>。 此方案常用来实现@dynamic属性,比方说,要访问CoreData框架中NSManagedObjects对象的属性时就可以这么做,因为实现这些属性所需的存取方法在编译期就能确定。
id autoDictionaryGetter(id self,SEL _cmd);
void autoDictionarySetter(id self,SEL _cmd,id value);
+(BOOL)resolveInstanceMethod:(SEL)selector{
NSString *selectorStirng = NSStringFormSelector(selector);
if(/* selector is from a @dynamic property */){
if([selectorString hasPrefix:@"set"]){
class_addMethod(self,selector,(IMP)autoDictionarySetter,@"v@:@");
}else{
class_addMethod(self,selector,(IMP)autoDictionaryGetter,@"@@:"
}
return YES;
}
return [super resolveInstanceMethod:selector];
}
备援接收者
当前接收者还有第二次机会能处理未知的选择子,在这一步中,运行期系统会问它:能不能把这条消息转给其他接收者来处理。
-(id)forwardingTargetForSelector:(SEL)selector
方法参数代表未知的选择子,若当前接收者能找到备援对象,则将其返回,否则就返回nil。
值得注意的是,我们无法操作经由这一步所转发的消息,若是想在发送给备援接收者之前修改消息内容,那就得通过完整的消息转发机制来做了。
完整的消息转发
如果转发算法已经来到这一步的话,那么唯一能做的就是启用完整的消息转发机制了。
首先创建NSInvocation对象,把尚未处理的那条消息有关的全部细节都封于其中。此对象包含选择子、目标(target)及参数。在触发NSInvocation对象时,"消息派发系统"(message-dispatch system)将亲自出马,把消息指派给目标对象。
-(void)forwardInvocation:(NSInvocation *)invocation
消息转发全流程
要点
- 若对象无法响应某个选择子,则进入消息转发流程
- 通过运行期的动态方法解析功能,我们可以在需要用到某个方法时再将其加入类中
- 对象可以把其无法解读的某些选择子转交给其他对象来处理
- 经过上述两步之后,如果还是没有办法处理选择子,那就启动完整的消息转发机制
十三、用"方法调配技术"
类的方法列表会把选择子的名称映射到相关的方法实现之上,使得"动态消息派发系统"能够据此找到应该调用的方法.这些方法均以函数指针的形式来表示,这种指针叫做IMP。其原型如下:
id (*IMP)(id,SEL,...)
可以通过以下c函数互换两个方法实现
void method_exchangeImplementations(Method m1,Method m2)
//此函数的两个参数表示待交换的两个方法实现,而方法实现则可通过下列函数获得
Method class_getInstanceMethod(Class class,SEL aSelector)
//此函数根据给定的选择子从类中取出与之相关的方法
可以用自定义的类中方法交换某个目标类中方法,如下:
@implement NSString (EOCMyAdditions)
-(NSString *)eoc_myLowercaseString{
NSString *lowercase = [self eoc_myLowercaseString];
NSLog(@"%@ => %@",self,lowercase);
return lowercase;
}
@end
Method originalMethod = class_getInstanceMethod([NSString class],@selector(lowercaseString)];
Method swappedMethod = class_getInstanceMethod([NSString class],@selector(eoc_myLowercaseString));
method_exchangeImplementations(originalMethod,swappedMethod);
通过此方案,开发者可以为那些"完全不知道其具体实现的"黑盒方法增加日志记录功能,这非常有助于程序调试,然后此做法只在调试的时候有用。若是滥用,会令代码变得不易读懂且难于维护。
要点
- 在运行期,可以向类中新增或替换选择子所对应的方法实现
- 使用另一份实现来代替原有的方法实现,这道工序叫做"方法调配",开发者常用此技术向原有实现中添加新功能
- 一般来说,只有调试程序的时候才需要在运行期修改方法实现,这种方法不宜滥用
十四、理解"类对象"的用意
类型信息查询: "在运行期检视对象类型"这一操作叫做类型信息查询(introspection,内省),这个强大而有用的特性内置于Foundation框架的NSObject协议里,凡是由公共根类(common root class,即NSObject与NSProxy)继承而来的对象都要遵从此协议。
Class对象也定义在运行期程序库的头文件中:
typedef struct objc_class *Class;
struct objc_class{
Class isa;
Class superClass;
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;
}
其中super_class 指针确立了继承关系,而isa指针描述了实例所属的类。
"isMemberOfClass:"能够判断出对象是否为某个特定类的实例;而"isKindOfClass:"则能够判断出是否为某类或其派生类的实例
要点:
- 每个实例都有一个指向Class对象的指针isa,用以表明其类型,而这些Class对象则构成了类的继承体系
- 如果对象类型无法在编译期确定,那么就应该使用类型信息查询方法来探知
- 尽量使用类型信息查询方法来确定对象类型,而不要直接比较类对象,因为某些对象可能实现了消息转发功能