第二章 对象、消息、运行期 52条 笔记
用Objective-C 等面向对象语言编程时,对象object 就是基本构造单元,开发者可以通过对象来存储并传递数据。
在对象之间传递数据任务并执行任务的过程就叫做消息传递 messaging .
当应用程序运行起来以后,为其提供相关支持的代码叫做objective-c运行期环境 objective -c runtime,它提供了一些使得对象之间能够传递消息的重要函数,并且bao包含创建类实例所用的全部逻辑。
第6条:理解属性的概念
Objective-C 对象通常会把其所需要的数据保存为各种实例变量。实例变量一般通过存取方法 access method 来访问。其中获取方法getter 和设置方法
把实例变量当做一种存储偏移量所有的特殊变量,交由类对象 报关。偏移量会在运行期查找,如果类的定义变了,那么存储的偏移量也就变了,这样的话,无论何时访问实例变量,总能找到正确的偏移量。甚至可以在运行期间向类中新增实例变量,这就是稳固的应用程序二进制接口 application binary interface ABI。
尽量不要直接访问实例变量,而应该通过存取方法来做。
可以把属性当做一种简称,其意思是说: 编译器会自动写出一套存取方法,用来访问给定类型中具有给定名称的变量。
编译器还要自动向类中添加适当类型的实例变量,并且在属性名前面加下划线,以此作为实例变量的名字。
属性 内存管理语义
assign 设置方法 只会执行针对纯量类型 scalar type 的简单赋值操作
stong 定义了一种拥有关系 ,为这种属性设置新值时,设置方法会先保留新值,并释放旧值,然后再将新值设置上去。
weak 非拥有关系nonowining relationship .设置方法即不保留新值,也不保留旧值。此特质同assign 类似,然而在属性所指的对象遭到摧毁时,属性值也会清空 。
copy 此特质所表达的所属关系与strong类似。然而设置方法并不保留新值,而是将其拷贝。
第7条: 在对象内部尽量直接访问实例变量
在对象之外访问实例变量时,总是应该通过属性来做,然而在对象内部访问实例变量时又该如何呢?
笔者强烈建议在读取实例变量的时候采用直接访问的形式,而在设置实例变量的时候通过属性来做。
另外一个要注意的地方是lazy initialization .在这种清空下,必须通过获取方法来访问属性,否则,实例变量就永远不会初始化。
第8条: 理解对象等同性这一概念
按照 ==操作符比较出来的结果是两个指针的比较,而不是其所指的对象。应该使用NSObject 协议中声明的isEqual方法来判断 两个对象的等同性。
第9条: 以类簇模式隐藏实现细节
第10条:在既有类中使用关联对象存放自定义数据
有时需要在对象中存放相关信息。这时通常从对象所属的类中集成一个子类,然后改用这个子类对象。可以给某对象关联许多其他对象,这些对象通过键 来区分。存储对象值的时候,可以指明存储策略 stoarge policy ,用来维护相应的内存管理语义。
存储策略由名为objc_AssociationPolicy的枚举所定义。
第11条:理解objc_msgSend的作用
在对象上调用方法是OC 中经常使用的功能。用OC 的术语来说,这叫做pass a message .消息有 name 或selector ,可以接受参数,也可以有返回值。
C语言使用动态绑定 static binding .在编译期就能决定运行时所应调用的函数。
如果不考虑内联inline,那么编译器在编译代码的时候就已经知道程序中有printHello与printGoodbye两个函数,于是会直接生成调用这些函数的指令。而函数地址实际上是硬编码在指令中的。
这时就得使用动态绑定 dynamic binding了。因为所要调用的函数直到运行期才能确定。
在OC中,如果向某对象传递消息,那就会使用动态绑定机制来决定需要调用的方法。在底层,所有方法都是普通的C语言函数,然而对象收到消息之后,究竟该调用哪个方法则完全于运行期决定,甚至可以在程序运行时改变,这些特性使得OC 成为一门真正的动态语言。
给对象发送消息可以这样 写:
id returnValue=[someObject messageName:parameter];
someObject叫做接受者 receiver ,messgeName叫做选择子 Selector 。选择子和参数合起来叫做消息。
编译器看到此消息后,将其转换成一条标准的C 语言函数调用,所调用的函数是消息传递机制中的核心函数,叫做objc_msgSend,其中原型prototype如下:
void objc_msgSend(id self ,SEL cmd,…).
这是个参数个数可变的函数。variadic function,能接受两个或两个以上的参数。第一个参数代表接受者,第二个参数代表选择子 SEL ,后续参数就是消息中的那些参数,其顺序不变。选择子就是是方法的名字。
id returnValue=objc_msgSend(someObject,@selector(messageName:),parameter);
objc_msgSend函数会依据接受者与选择子的类型来调用适当的方法。为了完成此操作,该方法需要在接受者所属的类中搜索其方法列表 list of methods ,如果能找到与选择子名称相符的方法,就跳至其实现代码。若是找不到,那就沿着继承体系继续向上查找,等找到合适的方法之后再跳转。如果最终还是找不到相符的方法,那就执行消息转发 message forwarding 操作。
前面将的内容只描述了部分消息的调用过程,其他 边界情况 edge case ,则需要交由OC 运行环境中的另一些函数来处理:
objc_msgSend_stret.如果待发送的消息要返回结构体,那么可交由此函数处理。只有当CPU的寄存器能够容纳的下消息返回类型,这个函数才能处理此消息。若是返回值无法容纳于CPU寄存器中,那么就由另一个函数进行派发。
objc_msgSend_fpret.如果消息返回的是浮点数,那么可交由此函数处理。在某些结构的CPU中调用函数时,需要对浮点数寄存器floating-point register 做特殊处理,也就是说通常使用的objc_msgSend在这种情况下并不合适。
objc_msgSendSuper。如果要费超类发消息,例如[super mssage:parameter],那么久交由此函数处理。
objc_msgSend等函数一旦找到应该调用的函数逇方法实现,就会跳转过去。之所以能这样做,是因为OC 对象的每个方法都可以视为简单的C函数,
<return_type> Class_selector(id self ,SEL _cmd,…)
每个类里都有一张表格,其中的指针都会指向这种函数,而选择子的名称则是查表时所用的键。
第12条:理解消息转发机制
对象在收到无法解读的消息之后会发生什么情况》
当对象接收到无法理解的消息之后,就会启动消息转发机制 message forwarding 机制,程序员可经由此过程告诉对象应该处理未知消息。
消息转发 分为两大阶段。第一阶段 先征询接受者,所属的类,看其是否动态添加方法,以处理当前这个未知的选择子unknown selector ,这叫做动态方法解析 dynamic method resoluton .第二阶段涉及 完整的消息转发机制 full forwarding mechanism .如果运行期系统已经把第一阶段执行完了,那么接受者自己就无法再以动态新增方法的手段来响应包含该选择子的消息了。
此时,运行期系统会请求接受者以其他手段来处理与消息相关的方法调用。这又细分为两小步。首先,请接受者看看有没有其他对象能处理这条消息。若有,则运行期系统会把消息转给那个对象,于是消息转发过程结束,一切正常。若没有,替代的接受者replacement receiver ,则启动完整的消息转发机制,运行期系统会把与消息有关的全部细节都封装到NSInvocation对象中,再给接受者最后一次机会,令其设法解决当前还未处理的这条消息。
动态方法解析
对象收到无法解读的消息后,首先将调用其所属类的下列方法:
假如 尚未实现的方法不是实例方法而是类方法,那么运行期系统就会调用另外一个方法 ,该方法与resolveInstanceMethod类似,叫做resolveClassMethod。
replacement receiver 替代接受者
当接受者还有第二次机会处理未知的选择子,在这一步,运行期系统会问它:能不能把这条消息转给其他接受者来处理。
-(id)forwardingTargetForSelector:(SEL)selector
方法参数代表未知的选择子,若当前接受者能找到替代对象,则将其返回,若找不到,就返回nil。
完整的消息转发
如果转发算法已经来到这一步,那么唯一能做的就是启用完整的消息转发机制。首先创建NSInvocation对象,把与未处理的那条消息有关的全部细节都封于其中。此对象包含选择子,目标及参数。
第13条: 用方法调配计数调试黑盒方法
与给定的选择子名称相对应的方法是不是也可以在运行期改变呢?
是可以的。
此方案成为 方法调配 method swizzling
NSString 类可以响应lowercaseString、uppercaseString、capitalizedString等选择子。
Objective-C 运行期系统提供的几个方法 都能够用来操作这张表。开发者可以向其中新增选择子,也可以改变某选择子所对应的方法实现,还可以交换两个选择子所映射到的指针。
想交换方法实现,可用下列函数:
void method_exchangeImplementations(Method m1,Method m2)
此函数的两个参数表示待交换的方法的实现,而方法实现则可通过下列函数获得:
Method class_getInstanceMethod(Class aClass,SEL aSelector)
此函数根据给定的选择子从类中取出与之相关的方法。
执行下列代码就可以交换前面提到的lowercaseString与uppercaseString方法实现:
从现在开始,如果在NSString实例上调用lowercaseString,那么将执行的将是uppercaseString的原有实现,反之亦然。
我们新编写一个方法,在此方法中实现所需要的附加功能,并调用原有实现。
此方法是准备和lowercaseString方法互换的。所以,在运行期,eoc_myLowercaseString选择子实际上对应于原有的lowercaseString方法实现。最后通过下列代码来交换这两个方法的实现:
执行完上述代码之后,只要在NSString实例上调用lowercaseString方法,就会输出一行记录消息:
第14条: 理解类对象的用意
对象类型并非在编译器就绑定了,而是要在运行期查找。而且还有个特殊的类型叫做id,它能指代任意的OC对象类型。一般情况下,应该指明消息接受者的具体类型,这样的话,如果向其发送了无法解读的消息,那么编译器就会产生警告。而类型为id的对象则不然,编译器假定它能响应所有消息。
typedef struct objc_object{
Class isa ;
} *id
每个对象结构体的首个成员是Class类的变量。该变量定义了对象所属的类,通常称为 is a 指针。
此结构体存放类的元数据 metadata,例如类的实例变量实现了几个方法,具备多少个实例变量等信息。此结构体的首个变量也是isa 指针,这说明Class 本事亦为OC 对象。类对象所属的类型 isa 指针所指的类型 是另外一个类,叫做元类metaclass ,用来表述类对象本身所具备的元数据 。