美文网首页
Effective Objective-C 2.0(读书笔记)二

Effective Objective-C 2.0(读书笔记)二

作者: 木子影 | 来源:发表于2017-03-16 14:54 被阅读14次

第二章 对象、消息、运行时

OC中,对象就是基本构造单元,开发者可以用对象来存储并传递数据。在对象之间传递数据并执行任务的过程就叫做“消息传递”。当应用程序运行起来以后,为其提供相关支持的代码叫做“Objective-C运行期环境”,它提供了一些使得对象之间能够传递消息的重要函数。

六:理解“属性”这一概念

@property 属性语法,会自动为实例变量创建存取方法。再实现方法中用如下类似代码 :

@interface LRG : NSObject
@property NSString *firstName;
@property NSString *lastName;
@end

@implementation LRG
@dynamic firstName,lastname
@end

属性特质

属性拥有的特质可以分为四类:

1、原子性

PS:原子性是值再并发编程中,如果某操作具备整体性,也就是说,系统其他部分无法观察到其中间步骤所生成的临时结果,而只能看到操作前与操作后的结果,那么该操作就是“原子的(atomic)”,或者说,该操作具备"原子性(atomiccity)"

再默认情况下,由编译器所合成的方法会通过锁定机制来保证其原子性,如果属性具备nonatomic特质,则不使用同步锁。

2、读/写权限

具备readwrite(读写)特质的属性会有 getter和setter,若该属性有@synthesize实现,则编译器会自动生成这两个方法。将属性声明为
@dynamic,这样编译器不会为其自动生成实例变量及存取方法了。
具备readonly(只读)特质的属性仅拥有获取方法,只有当该属性由@synthesize实现时,编译器才会为其合成获取方法。可以用此特质将该属性对外公开为只读属性,然后再“class-continuation分类”中将其重新定义为读写属性。

3、内存管理语义

属性用于封装数据,而数据则要有“具体的所有权语义”。

  • assign "设置方法"只会执行针对“纯量类型”的简单赋值操作。
  • strong 此特质表明该属性定义了一种“拥有关系”。为这种属性设置新值时,设置方法会先保留新值,并释放旧值,然后再讲新值设置上去。
  • weak此特质表明该属性定义了一种“非拥有关系”,为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。当属性所指的对象遭到销毁时,属性值也会清空。
  • unsafe_unretained 此特质语义和assign相同,但是它适用于对象类型,该特质表达一种“非拥有关系”,当目标对象遭到摧毁时,是性质不会自动清空,与weak有区别。
  • copy 此特质表达所属关系与strong有些类似。当设置方法并不保留新值,而是将其拷贝。当属性类型为NSStrign *时,经常用此特性来保证其封装性。只要实现属性所用对象是可变的,就应该再设置新属性值的时候拷贝一份。

4、方法名

@property(nonatomic,getter=isON)Bool on;

再iOS开发中将所有属性都声明为nonatomics是因为在iOS中使用同步锁的开销较大,这回带来性能问题。使用原子性并不能保证线程安全。例如一个线程再多次读取某属性值的同事另外一个线程同时改写此值,那么即便声明为atomic,也还是会读取到不同的属性值。

要点:
1、可以通过@property语法来定义对象中所封装的数据。
2、通过“特质”来指定存储数据所需的正确语义。
3、再设置属性所对应的实例变量时,一定要遵从该属性所声明的语义。

七:再对象内部尽量直接访问实例变量

直接访问即是用_+实例变量名。建议是再读取实例变量的时候采用直接访问的形式,而再设置实例变量的时候通过属性来做。

区别:

  • 由于不经过Objective_C的“方法派发”步骤,所以直接访问实例变量的速度当然比较快。再这种情况下,编译器所生成的代码会直接访问保存对象实例变量的那块内存。
  • 直接访问实例变量时,不会调用其“设置方法”,这就绕过了为相关属性所定义的内存管理语义。
  • 如果直接访问实例变量,那么不会触发“键值观测KVO”通知。
  • 通过属性来访问有助于排查与之相关的错误,因为可以给“获取方法”或“设置方法”中新增断点,来监控该属性的调用者以及访问时机。

惰性初始化即是“懒加载”。在这种情况下必须通过“获取方法”来访问属性,否则,实例变量就永远不会初始化。例如某个对象不常用切存储该对象所花费的成本较高,则适合用惰性加载:

-(EOCBrain *)brain{
        if(!_brain){
   {
                _brain=[Brain new];
   }
   return _brain;
}

要点:
1、再对象内部读取数据时,应该直接通过实例变量来读取,而写入数据时,则应通过属性来写。
2、在初始化以及dealloc方法中,总是应该直接通过实例变量来读写数据。
3、有时会使用惰性初始化技术配置某份数据,这种情况下,需要通过属性来读取数据。

八:对象等同性

要点:
1、若想检测对象的等同性,请提供"isEqual"与"hash"方法。
2、相同的对象必须具有相同的哈希吗,但是两个哈希吗相同的对象却未必相同。

九:以类族模式隐藏实现细节

类族是一种很有用的模式,可以隐藏”抽象基类“背后的实现细节。

创建类族

typedef NS_ENUM(NSUInteger,EOCEmployeeType){
         EOCEmployeeTypeDeveloper,
         EOCEmployeeTpyeDesigner,
         EOCEmployeeTpyeFinance,
};
@interface EOCEmployee : NSObject 
@property (copy) NSString *name;
@property NSUInteger salary;
+(EOCEmployee* )emploryeeWithType:(EOCEmployee)type{
        switch (type){
     case   EOCEmployeeTypeDeveloper:
               return [EOCEmployeeDeveloper new];
               break;
     case    EOCEmployeeTpyeDesigner:
              return [EOCEmployeeDesigner new];
              break;
     case      EOCEmployeeTpyeFinance:
             return [EOCEmployeeFinance new];
              break;
    }
}
-(void)duADaysWork{

}
每个子类实体都从基类集成而来,重新实现-(void)doADaysWord{ }

OC没办法指明某个基类是抽象类(abstract),于是开发者会在文档中写明。这种情况下,基类接口一般都没有明为init的成员方法,这暗示改类的实例也许不应该由用户直接创建。抽象基类没有实例,所以不可以用[employee isMemberOfClass[EOCEmployee class]];来判断。可以用isKindOfClass来判断。
增加新子类需要遵循几条规则:

  • 子类应该继承自类族中的抽象基类。
  • 子类应该定义自己的数据存存储方式。
  • 子类应该复写超类文档中指明需要复写的方法。

要点:
1、类族模式可以把实现细节隐藏在一套简单的公共接口后面。
2、系统框架中经常使用类族。
3、从类族的公共抽象基类中继承子类时要当心,若有开发文档,则应首先阅读。

十:再既有类中关联对象存放自定义数据

要点:
1、可以通过“关联对象“机制把两个对象连接起来
2、定义关联对象时可指定内存管理语义,用以模仿定义属性所采用的”拥有关系“与”非拥有关系“。
3、只有在其他做法不可行时才应用关联对象,因为这种做法通常会引入难以查找的BUG。

十一:理解objc_msgSend的作用

在对象上调用方法是Objective-C中经常使用的功能。用Objective-C中的术语是”消息传递“。消息有”名称“或者”(selector)“,可以接受参数可能还有返回值。
  再OC中,若是像某对象传递消息,那就会使用动态绑定机制来决定需要调用的方法。再底层,所有方法都是普通的C语言函数,然而对象收到消息后,究竟该调用哪个方法则完全在运行时决定,设置可以在程序运行时改变,这些特性使得OC成为一门真正的动态语言。

给对象发消息可以这样来写:
id returnValue = [someObject messageName:parameter];

someObject叫做”接受者“(receiver),messageName叫做”选择子“(selector)。选择子与参数合起来称为”消息“。编译器看到此消息后,将其转换为一条标砖的C语言函数调用,所调用的函数乃是消息传递机制中的核心函数,叫做objc_msgSend,其”原型“如下:

  void objc_msgSend(id self,SEL cmd,...)

这是个参数可变的函数,能接受两个或两个以上的参数。第一个参数代表接受者,第二个参数代表选择子(SEL是选择子的类型),后续参数就是消息中的那些参数,其顺序不变。选择子就是指方法的名字。“选择子”与“方法”两个词经常交替使用。编译器会把刚才那个例子中的消息转换为如下函数:

      id returnValue = objc_msgSend(someObject,@selector(messageName:),parameter);

objc_msgSend函数会一句接受者与选择子的类型来调用适当的方法。为了完成此操作,该方法需要再接受者所属的类中搜寻其“方法列表”(list of methods),如果能找到与选择子名称相符的方法,就跳至其实现代码。若是找不到,就执行消息转发(message forwarding)操作。
  objc_msgSend会将匹配结果缓存在 快速映射列表(fast map)里面,每个类都有一个这样的缓存,若是稍后还向该类发送与选择子相同的消息,那么执行起来就很快了。当然,快速执行路径还是不如静态绑定的函数调用操作那样迅速,当然缓存后速度也不会太差。
  边界情况处理函数:

  • objc_msgSend_stret。如果待发送消息要返回结构体,那么可交由此函数处理。只有当CPU的寄存器能够容纳的下消息返回类型时,这个函数才能处理此消息。若是返回值无法容纳于CPU寄存器中,那么就由另外一个函数执行派发。此时,那个函数会通过分配在栈上的某个变量来处理消息所返回的结构体。
  • objc_msgSend_fpret。 如果消息返回的是浮点数,那么可交由此函数处理。再某些架构的CPU中调用函数时,需要对浮点数寄存器做特殊的处理。在这种情况下objc_msgSend再这种情况下并不适用。这个函数是为了处理x86等架构CPU中令人稍觉惊讶的奇怪状况。
  • objc_msgSendSuper。如果要给超类发消息。那么就交由此函数处理。也另外有两个objc_msgSend_stret和objc_msgSend_fpret等效的函数,用于处理给超类发送消息。

objc_msgSend等函数一旦找到应该就调用的方法实现之后,就会跳转过去,之所以能这样做,是因为Objective-C对象的每个方法都可以视为简单的C函数,原型如下:
<return_type>Class_selector(id self,SEL _cmd)
  真正的函数名和相面写的可能不太一样,这样写是为了解释其工作原理。每个类都有一张表格,其中的指针都会指向这种函数,而选择子的名称则是查表时所用的。objc_msgSend等函数正式通过这张表格来寻找该执行的方法并跳至其实现的。原型的样子和objc_msgSend函数很像。这不是巧合,而是为了利用尾调用优化技术,另跳至方法实现这一操作变得更简单些。
  如果函数的最后一项操作是调用另一个函数,那么就可以运用尾调用优化技术。编译器会生成调转至另一函数所需的指令码,而且不会向调用堆栈中推入新的栈帧。只有当某函数的最后一个操作仅仅是调用其他函数而不会将其返回值另做他用时,才能执行尾调用优化。这项优化对objc_msgSend非常关键,如果不这么做的话,那么每次调用OC方法钱,都需要为调用objc_msgSend函数准备栈帧,再栈踪迹(stack trace)中可以看到这种栈帧。此外,若是不优化,还会过早的发生栈溢出(stack overflow)现象。

要点:

  1. 消息由接受者、选择子、及参数构成。给某对象发送消息(invoke a message)也就相当于在该对象上调用方法(call a method).
  2. 发送给某对象的全部消息都要由动态消息派发系统(dynamic message dispatch system)来处理,该系统会查出对应的方法,并执行代码。

十二:理解消息转发机制

编译期向类发送了无法解读的消息并不会报错,因为运行期可以继续向类添加方法,所以在编译器在编译时还无法确定类中到底会不会有某个方法实现。当对象接受到无法解读的消息时,就会启动消息转发(message forwarding)机制,程序员可经由此过程告诉对象应该如何处理位置消息。
  消息转发分为两大类:

  • 第一阶段:先征询接受者,所属的类,看其是否能动态的添加方法,以处理当前这个未知选择子(unknown selector),这叫做动态方法解析(dynamic method resolution)。
  • 第二阶段:涉及完整消息转发机制(full forwarding mechanism)。如果运行期系统已经把第一阶段执行完了,那么接受者自己就无法再以动态新增方法的手段来相应包含该选择子的消息了。此时,运行期系统会请求接受者以其他手段来处理与消息相关的方法调用。这又细分为两小步。首先:接受者看看有没有其他对象能处理这条消息。若有,则运行期系统会把消息转给那个对象,于是消息转发过程结束,一切如常。若没有备援的接受者,则启动完整的消息转发机制,运行期系统会把与消息有关的全部细节都封装到一个NSInvocation对象中,再给接受者最后一次机会,令其设法解决还未处理的这条消息。

动态方法解析

对象在收到无法解读的消息后,首先将调用其所属类的下列类方法:
+(BOOL)resolveInstanceMethod:(SEL)selector
  该方法的参数就是那个未知的选择子,其返回值为Boolean型,表示这个类是否能新赠一个实例方法用以处理此选择子。再继续执行转发机制之前,本类有机会新增一个处理此选择子的方法。假如尚未实现的方法不是实例方法而是类方法,那么运行期系统就会调用另外一个方法,与实例方法类似,叫做 resolveClassMethod
  使用这种方法的前提是:相关的方法的实现代码已经写好,只等着运行的时候动态插在类里面就可以了。

下面代码演示如何用 *resolveInstanceMethod:* 来实现@dynamic属性
id autoDictionaryGetter(id self,SEL _cmd);
void autoDictionarySetter(id self,SEL _cmd,id value);
+(BOOL)resolveInstanceMethod:(SEL)selector{
      NSString *selectorString=NSStringFromSelector(selector);
     if(/*selector is from a @dynamic property */){
           if([selectorString hasPrfix:@"set"]){
          class_addMethod(self,selector,(IMP)autoDictionarySetter,"v@:@");
       }
     }else{
        clasee_addMethod(self,selector,(IMP)autoDictionaryGetter,@"@@:");
        }
       return YES;
     }
     return [super resolveInstanceMethod:selector];
}

备援接受者

当前接受者还有第二次机会来处理位置的选择子,在这一步中,运行期系统会问它:能不能把这条消息转给其他接受者来处理。与该步骤对应的处理方法如下:
-(id)forwardingTargetForSelector:(SEL)selector
  若当前接受者能找到备援对象,则将其返回,若找不到,就返回nil。通过此方案,我们可以用 组合 来模拟出 多重继承 的某些特性。在一个对象内部,可能还有一系列其他对象,该对象可经由此方法将能够处理某些选择子的相关内部对象返回,这样的话,再外界看来,好像是该对象亲自处理了这些消息似得。

完整的消息转发

如果转发算法已经来到这一步的话,那么唯一能做的就是启用完整的消息转发机制。首先创建NSInvocation对象,把尚未处理的那条消息有关的全部细节都封于其中。此对象包含选择子、目标及参数。再出发NSInvocation对象时, 消息派发系统 将亲自出马,把消息指派给目标对象。调用下面方法
-(void)forwardInvocation:(NSInvocaiton*)invocation
  这个方法可以实现得很简单:只需要改变调用目标,使消息在新目标上得以调用即可。这样实现出来的方法与 备援接受者方案所实现的方法等效,所以很少有人采用这么简单的实现方式。比较有用的实现方式为:在触发消息前,先以某种方式改变消息内容,比如追加另外一个参数,或是改换选择子,等等。

消息转发全流程

这张流程图描述了消息转发即止处理消息的各个步骤:

消息转发.jpg

  接受者再每一步中均有机会处理消息。步骤越往后,处理消息的代价就越大。最好能在第一步就处理完成。这样的话,运行期系统就可以将此方法缓存起来了。如果这个类的实例稍后还会收到同名选择子,那么根本无需启动消息转发流程。

要点

  • 若对象无法响应某个选择子,则进入消息转发流程。
  • 通过运行期的动态方法解析功能,我们可以在需要用到某个方法时再将其加入类中。
  • 对象可以把其无法解读的某些选择子转交给其他对象来处理。
  • 经过上述两个步骤后,如果还是没办法处理选择子,那就启动完整的消息转发机制。

十三:用方法调配技术调试黑盒方法

OC对象接受到消息后,会调用何种方法,需要再运行期才能解析粗来。与给定的选择子名称对应的方法也可以在运行期改变。善用此特性我们既不需要源代码,也不需要通过继承子类来腹泻方法就能改变这个类本身的功能。这样一来新功能将在本类的所有实例中生效,而不是仅限于复写了相关方法的那些子类的实例。此方法常常称之为 方法调配(method swizzling)。
类的方法列表会把选择子的名称映射到相关的方法实现上,使得 动态消息派发系统 能够据此找到应该调用的方法。这些方法均以函数指针的形式来表示,这种指针叫IMP,其原型如下:
id (*IMP)(id,SEL,...)

例子:
void method_exchangeImplementations(Method m1,Method m2)//交换两个方法
Method class_getInstanceMethod(Classs class,SEL aSelector)//方法的实现的获取方法

通过此方案,开发者可以为那些 完全不知道具体实现的黑盒方法增加日志记录功能,这非常有助于程序测试。

要点:

  • 在运行期,可以向类中新增或替换选择子所对应的方法实现。
  • 使用另一份实现来替换原有的方法实现,这道工序叫做方法调配,开发者常用此技术向原有实现中添加新功能。
  • 一般来说,只有调试程序的时候才需要再运行期修改方法实现,这种方法不宜滥用。

十四:理解类对象的用意

再运行期检视对象类型,这一操作也叫做类型信息查询 这个强大而有用的特性内置于Foundation框架的NSObject协议里,凡是由公共根类(即NSObject/NSProxy)继承而来的对象都要遵从此协议。再程序中不要直接比较对象所属的类而是调用类型信息查询方法
  OC对象实例都是指向某块内存数据的指针。关系图如下:

3143F8B204E1C2E5669B14F50F730B96.jpg
  super_class指针确立继承关系,而isa指针描述了实例所属的类。通过这张布局关系图即可执行 类型信息查询
isMemberOfClass: 能够判断出对象是否为某个特定类的实例
isKindOfClass: 能判断出对象是否为某类或其派生类的实例。

要点:

  • 每个实例都有一个指向Class对象的指针,用以表明其类型,而这些Class对象则构成类的继承体系。
  • 如果对象类无法在编译期确定,那么就应该用类型信息查询方法来探知。
  • 尽量使用类型信息查询方法来确定对象类型,而不要直接比较类对象,因为某些对象可能实现了消息转发功能。

相关文章

网友评论

      本文标题:Effective Objective-C 2.0(读书笔记)二

      本文链接:https://www.haomeiwen.com/subject/kqmlnttx.html