首页 > 代码库 > <<Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法>>笔记-对象、消息、运行期

<<Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法>>笔记-对象、消息、运行期

6、理解属性这一概念

  可以用@property 语法来定义对象中所封装的数据。

  通过“特质”来指定存储数据所需的正确语义。

  在设置属性所对应的实例变量时,一定要遵从该属性所声明的语义。

  开发iOS程序时应该使用 nonatomic 属性,因为 atomic 属性会严重影响性能。

7、在对象内部尽量直接访问实例变量

  对象内部访问实例变量的两种方法:通过属性访问、直接访问。

  这两种写法有一个区别:

    由于不进过 Object-C 的“方法派送”步骤,所以直接访问实例变量的速度当然比较快。这种情况下,编译器所生成的代码会直接访问保存对象实例变量的那块内存。

    直接访问实例变量时,不会调用其“设置方法”,这就绕过了为相关属性所定义的“内存管理语义”。比方说,如果在 ARC 下直接访问一个声明为 copy 的属性,那么并不会拷贝该属性,只会保留新值并释放旧值。

    如果直接访问实例变量,那么不会触发“键值观测”(Key-Value Observing,KVO)通知。这样做是否会产生问题,还取决于具体的对象行为。

    通过属性来访问有助于排查与之相关的错误,因为可以给“获取方法”和/或“设置方法”中新增“断点”,监控该属性的调用者及其访问实际。

  有一种合理的折中方案,那就是:在写入实例变量时,通过“设置方法”来做,而在读取实例变量时,则直接访问之。此办法既能提高读取操作的速度,又能控制对属性的写入操作。之所以要通过“设置方法”来写入实例变量,其首要原因在于,这样做能够确保相关属性的“内存管理定义”得以贯彻。

  但是,选用这种做法时,需注意几个问题。

    一个需要注意的问题是,在初始化方法中应该如何设置属性值。这种情况下总是应该直接访问实例变量,因为子类可能会“覆写”设置方法。但是,某些情况下却又必须在初始化方法中调用设置方法:如果待初始化的实例变量声明在超类中,而我们又无法在子类中直接访问此实例变量的话,那么久需要调用“设置方法”了。

    另外一个需要注意的问题是“惰性初始化”。这种情况下,必须通过“获取方法”来访问属性,否则,实例变量就永远不会初始化。

  在对象内部读取数据时,应该直接通过实例变量来读,而写入数据时,则应通过属性来写。

  在初始化方法及 dealloc 方法中,总是应该直接通过实例变量来读写数据。

  有时会使用惰性初始化技术配置某份数据,这种情况下,需要通过属性来读取数据。

8、理解“对象等同性”这一概念

  若想检测对象的等同性,请提供“isEqual:”与 hash 方法。

  相同的对象必须具有相同的哈希码,但是两个哈希码相同的对象却未必相同。

  不要盲目地逐个检测每条属性,而是应该依照具体需求来制定检测方案。

  编写 hash 方法时,应该使用计算速度快而且碰撞几率低的算法。

9、以“类族模式”隐藏实现细节

  子类应该继承自类族中的抽象基类。

  子类应该定义自己的数据存储方式。

  子类应当覆写超类文档中指明需要覆写的方法。

  类族模式可以把实现细节隐藏在一套简单的公共接口后面。

  系统框架中经常使用类族。

  从类族的公共抽象基类中继承子类时要当心,若有开发文档,则应首先阅读。

 10、在既有类中使用关联对象存放自定义数据

  下列方法可以管理关联对象:

    void objc_setAssociatedObject( id object, void * key, id value, objc_AssociationPolicy policy );

    此方法以给定的键和策略为某对象设置关联对象值。

    存储对象值的时候,可以指明“存储策略”,用以维护相关的“内存管理语义”。存储策略由名为 obj_AssociationPolicy 的枚举所定义:

      OBJC_ASSOCIATION_ASSIGN 对应 assign

      OBJC_ASSOCIATION_RETAIN_NONATOMIC 对应 nonatomic,retain

      OBJC_ASSOCIATION_COPY_NONATOMIC 对应 nonatomic,copy

      BOJC_ASSOCIATION_RETAIN 对应 retain

      BOJC_ASSOCIATION_COPY 对应 copy

    id objc_getAssociatedObject( id object, void * key );

    此方法根据给定的键从某对象中获取相应的关联对象值。

    void objc_removeAssociatedObjects( id object );

    此方法移除指定对象的全部关联对象。

  可以通过“关联对象”机制把两个对象连起来。

  定义关联对象时可指定内存管理语义,用以模仿定义属性时所采用的“拥有关系”与“非拥有关系”。

  只有在其他做法不可行时才应选用关联对象,因为这种做法通常会引入难于查找的 bug 。

11、理解 objc_msgSend 的作用

  在 Object-C 中,如果向某对象传递消息,那就会使用动态绑定机制来决定需要调用的方法。在底层,所有方法都是普通的 C 语言函数,然而对象收到消息之后,究竟该调用哪个方法则完全于运行期决定,甚至可以再程序运行时改变,这些特性使得 Objective-C 成为一门真正的动态语言。

  给对象发送消息可以这样来写:

  id returnValue = http://www.mamicode.com/[someObject messageName:parameter];

  在本例中,someObject 叫做“接受者”,messageName 叫做“选择子”。选择子于参数合起来称为“消息”。编译器看到此条消息后,将其转换为一条标准的 C 语言函数调用,所调用的函数是消息传递机制中的核心函数:

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

  编译器会把刚才那个例子中的消息转换为如下函数:

  id returnValue = http://www.mamicode.com/objc_msgSend( someObject,

                  @selector( messageName: ),

                  parameter );

  objc_msgSend 函数会依据接受者与选择子的类型来调用适当的方法。该方法需要在接受者所属的类中搜索其“方法列表”,如果能找到与选择子名称相符的方法,就跳至其实现代码。若找不到,那就沿着继承体系向上查找,等找到合适的方法之后再跳转。如果最终还是找不到相符的方法,那就执行“消息转发”操作。

  objc_msgSend 会将匹配结果缓存在“快速映射表”里,每个类都有这么一块缓存,若是稍后还向该类发送与选择子相同的消息,那么执行起来就很快了,当然啦,这种“快速执行路径”还是不如“静态绑定的函数调用操作”那样迅速。

12、理解消息转发机制

  当对象接收到无法解读的消息后,就会启动“消息转发”机制,程序员可经由此过程告诉对象应该如何处理未知消息。

  消息转发分为两大阶段。第一阶段先征询接受者,所属的类,看其是否能动态添加方法,以处理当前这个“未知的选择子”,这叫做“动态方法解析”。第二阶段涉及“完整的消息转发机制”。如果运行期系统已经把第一阶段执行完了,那么接受者自己就无法再以动态新增方法的手段来相应包含该选择子的消息了。

  动态方法解析

  对象在收到无法解读的消息后,首先将调用其所属类的下列类方法:

  +(BOOL)resolveInstanceMethod:(SEL)sel;

  +(BOOL)sesolveClassMethod:(SEL)sel;

  该方法的参数就是那个未知的选择子,其返回值为Boolean 类型,表示这个类是否能新增一个实例方法用以处理此选择子。在继续往下执行转发机制前,本类有机会新增一个处理此选择子的方法。

  使用这种办法的前提是:相关方法的实现代码已经写好,只等待运行的时候动态插在类里面。

  下面是添加方法的函数:

  class_addMethod(Class cls, SEL name, IMP imp,const char *types);

  Class cos:我们需要一个class,比如我的[Person class]。

  SEL name:这个很有意思,这个名字自己可以随意想,就是添加的方法在本类里面叫做的名字,但是方法的格式一定要和你需要添加的方法的格式一样,比如有无参数。

  IMP imp:添加方法的指针,IMP就是Implementation的缩写,它是指向一个方法实现的指针,每一个方法都有一个对应的IMP。这里需要的是IMP,所以你不能直接写方法,需要用到一个方法:

    class_getMethodImplementation(Class cls, SEL name);

  const char *types:这个东西其实也很好理解:

    比如:”v@:”意思就是这已是一个void类型的方法,没有参数传入。

    再比如 “i@:”就是说这是一个int类型的方法,没有参数传入。

    再再比如”i@:@”就是说这是一个int类型的方法,又一个参数传入。

  备援接收者

  当前接收者还有第二次机会能处理未知的选择子,在这一步中,运行期系统会问他:能不能把这条消息转发给其他接收者来处理。与该步骤对应的处理方法如下:

  -(id)forwardingTargetForSelector:(SEL)selector;

  方法参数代表未知的选择子,若当前接收者能找到备援对象,则将其返回,若找不到,就返回 nil 。通过此方案,我们可以用“组合”来模拟出“多重继承”的某些特性。在一个对象内部,可能还有一系列其他对象,该对象可经由此方法将能够处理某选择子的相关内部对象返回,这样的话,在外界看来,好像是该对象亲自处理了这些消息似的;

  请注意,我们无法操作经由这一步所转发的消息。

  完整的消息转发

  首先创建 NSInvocation 对象,把与尚未处理的那条消息有关的全部细节都封于其中。

  此步骤会调用下列方法来转发消息:

  - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{

    NSString * selectorString = NSStringFromSelector(aSelector);

    if ([selectorString isEqualToString:@"playMusic"]) {

      return [NSMethodSignature signatureWithObjCTypes:"v@"];

    }

     return nil;

  }

  -(void)forwardInvocation:(NSInvocation *)anInvocation{

      NSLog(@"%@",anInvocation);

      [anInvocation invokeWithTarget:_obj];

  }

  这个方法可以实现的很简单:只需改变调用目标,使消息在新目标上得以调用即可。然而这样实现出来的方法与“备援接收者”方案实现的方法等效。比较有用的实现方式为:在触发消息前,先以某种方式改变消息内容,比如追加另外一个参数,或是改换选择子等。

  接收者在每一步中均有机会处理消息。步骤越往后,处理消息的代价就越大。

  若对象无法响应某个选择子,则进入消息转发流程。

  通过运行期的动态方法解析功能,我们可以再需要用到某个方法时再将其加入类中。

  对象可以把其无法解读的某些选择子转交给其他对象来处理。

  经过上述两部之后,如果还是没办法处理选择子,那就启动完整的消息转发机制。

13、用“方法调配技术”调试“黑盒方法”

  若能善用此特性,则可发挥出巨大优势,因为我们既不需要源代码,也不需要通过继承子类来覆写方法就能改变这个类本身的功能。这样一来,新功能将在本类的所有实例中生效,而不是仅限于覆写了相关方法的那些子类实例。此方案经常称为“方法调配”。

  想交换方法实现,可用下列函数:

  void method_exchangeImplementations(Method m1, Method m2)

  此函数的两个参数表示待交换的两个方法实现,而方法实现则可通过下列函数获得:

  Method class_getInstanceMethod(Class aClass, SEL aSelector)

  此函数根据给定的选择从类中取出与之相关的方法。

  通过此方案,开发者可以为那些“完全不知道其具体实现“的黑盒方法增加功能,然而很少有人在调试程序之外的场合用上述“方法调配技术”来永久改动某个类的功能。不能仅仅因为 Objective-C 语音里有这个特性就一定要用它。若是滥用,反而会令代码变得不易读懂且难于维护。

  在运行期,可以向类中新增或替换选择子所对应的方法实现。

  使用另一份实现来替换原有的方法实现,这道工序叫做“方法调配”,开发者常用此技术向原有实现中添加新的功能。

  一般来说,只有调试程序的时候才需要在运行期修改方法实现,这种做法不宜滥用。

14、理解“类对象”的用意

  每个 Objective-C 对象实例都是指向某块内存数据的指针。所以在声明变量时,类型后面要跟一个“*”字符:

  NSString *pointerVariable = @"Some string";

  对于通用的对象类型 id,由于其本身已经是指针了,所以我们能够这样写:

  id genericTypedString = @"Some string";

  上面这种定义方式与用 NSString * 来定义相比,其语义相同。唯一区别在于,如果声明时指定了具体类型,那么在该类实例上调用其没有的方法时,编译器会探知此情况,并发出警告信息。

  描述 Objective-C 对象所用的数据结构定义在运行期程序库的头文件里。

  typedef struct objc_class *Class;

  struct objc_class {

    Class isa;

    Class super_class;

    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;

  };

  此结构体存放类的“元数据”,例如类的实例实现了几个方法,具备多少个实例变量等信息。此结构体的首个变量是 Class 类的变量,该变量定义了对象所属的类,通常称为 “isa”指针。结构体里还有个变量叫做 super_class,它定义了本类的超类。

  super_class 指针确立了继承关系,而 isa 指针描述了实例所属的类。我们可以查出对象是否能相应某个选择子,是否遵从某项协议,并且能看出此对象位于“继承体系”的哪一部分。

  在类继承体系中查询类型信息

  可以用类型信息查询方法来检视类继承体系。“isMemberOfClass:”能够判断出对象是否为某个特定类的实例,而“isKindOfClass:”则能够判断出对象是否为某类或其派生类的实例。

  也可以用比较类对象是否等同,若是如此,那就要用 == 操作符,而不要使用比较 Objective-C 对象时常有的“isEqual:”方法。原因在于,类对象是“单例”,每个类的Class仅有一个实例。也就是说,另外一种可以精确判读出对象是否为某类实例的办法是:[object class] == [EOCSomeClassclass]。

  每个实例都有一个指向 Class 对象的指针,用以表明其类型,而这些 Class 对象则构成了类的继承体系。

  如果对象类型无法在编译器确定,那么久应该使用类型信息查询方法来探知。

  尽量使用类型信息查询方法来确定对象类型,而不要直接比较类对象,因为某些对象可能实现了消息转发功能。

<<Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法>>笔记-对象、消息、运行期