欢迎访问移动开发之家(rcyd.net),关注移动开发教程。移动开发之家  移动开发问答|  每日更新
页面位置 : > > > 内容正文

Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法(Matt Galloway著)读书笔记(一),

来源: 开发者 投稿于  被查看 12662 次 评论:264

Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法(Matt Galloway著)读书笔记(一),


第一章:熟悉 Objective-C

第1条:了解 Objective-C 语言的起源

第2条:在类的头文件中尽量少引入其他头文件

背景:

使用 #import "ClassName.h" 可以引入其他文件的所有接口细节。

问题:
解决办法:
  • 将引入头文件的时机尽量延后,只在确有需要时才引入,这样可以减少类的使用者所需引入头文件的数量。

  • 第3条:多用字面量语法,少用与之等价的方法

    使用字面量语法(literal syntax)可以缩减源代码的长度,使其更为易读。

    第4条:多用类型常量,少用 #define 预处理指令

    问题:

    #define 定义的常量没有类型信息,编译器只会在编译前据此执行查找与替换操作。即使有人重新定义了常量值,编译器也不会产生警告信息,这将导致应用程序中的常量值不一致。

    解决办法:
    • 在实现文件中使用 static const 来定义“只在编译单元内可见的变量”。由于此类常量不在全局符号表中,所以无须为其名称加前缀。代码实现如下:
    // .h 文件
    @interface 类名: 父类名
    ...
    @end
    
    // .m 文件
    // 类内使用
    static const 类型 常量名 = 常量值;
    
    @implementation 类名
    ...
    @end
    
    
    • 在头文件中使用 extern 来声明的全局变量,并在相关实现文件中定义其值。这种常量要出现在全局符号表中,所以其名称应加以区分,通常用与之相关的类名做前缀。
    // .h 文件
    
    // 类外可用声明
    extern 类名 const 常量名;
    
    @interface 类名: 父类名
    ...
    @end
    
    // .m 文件
    // 类外可用声明
    类名 const 常量名 = 常量;
    
    @implementation 类名
    ...
    @end
    

    常量名称常用命名法是:

    第5条:用枚举表示状态、选项、状态码

    第二章:对象、消息、运行期

    第6条:理解 “属性” 这一概念

    第7条:在对象内部尽量直接访问实例变量

    第8条:理解 “对象等同性” 这一概念

    第9条:以 “类族模式” 隐藏实现细节

    类族模式:使用继承,实现多种职能的子类,父类通过设定不同的类型来创建某种子类,执行其相应的职能。 作用:将实现细节隐藏在一套简单的公共接口后面。 需要注意的是,创建的实例的真实类型是什么,需要我们知道

    新增 CocoaNSArray 这样的类族的子类,需要遵守以下几条规则:

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

    在对象中存放相关信息:

    关联对象方法:
    • 以给定的键和存储策略为某对象设置关联对象值

      objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy)
      

      参数说明:

      • object 关联的源对象。
      • key 关联的 key。通常使用静态全局变量做键。
      • value 关联 key 所对应的值。传 nil 可以清除现有的关联。
      • policy 关联的存储策略,也就是对应的内存管理语义,是一个枚举值。
        objc_AssociationPolicy 枚举值如下表: | 关联类型 | 等效的 @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 |
    • 根据给定的键从某对象中获取对应的关联对象值。

      objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
      
    • 移除指定对象的全部关联对象。

      objc_removeAssociatedObjects(id _Nonnull object)
      

    注:只有在其他方法都行不通时才考虑使用它。若是滥用,则很快就会令代码失控,使其难于调试。

    第11条:理解 objc_msgSend 的作用

    OC方法调用

    代码:

    id returnValue = [someObject messageName: paramter];
    

    代码说明:

    • someObject 接受者。
    • messageName 选择子。

    选择子和参数合起来称为“消息”

    底层C语言代码实现:

    id returnValue = objc_msgSend(someObject, @selector(messageName:), paramter);
    
    消息发送机制核心函数

    原型代码:

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

    这是个“参数个数可变函数”,能接受两个或者两个以上的参数。 参数说明:

    • self 接受者。
    • cmd 选择子(方法的名字)。
    • 后续参数为消息中的那些参数,顺序不变。

    具体实现:

    graph TD A[objc_msgSend] -->|获取接受者和选择子| B(接受者) B -->|查找与选择子名称相符的方法| C{方法列表} C -->|找到| D[方法实现代码] C -->|未找到| E{沿着继承体向上查找} E -->|找到| F[方法实现代码] E -->|未找到| G[消息转发]

    注:OC 方法调用需要很多步骤,较为耗时。objc_msgSend 会将匹配结果缓存在“快速映射表”,每个类都有这样一块缓存。虽然还是不如“静态绑定的函数调用操作”那么迅速,但是也不会慢很多。

    边界情况:

    • objc_msgSend_stret 待发送的消息要返回结构体,就交由此函数处理。
    • objc_msgSend_fpret 待发送的消息要返回浮点数,就交由此函数处理。
    • objc_msgSendSuper 要给超类发消息,就交由此函数处理。如:[super message:parameter]
    尾调用优化

    Objective-C 对象的每个方法都可以看做简单的 C 函数,其原型如下:

    <return_type> Class_selector(id self, SEL _cmd, ...)
    

    这个原型和 objc_msgSend 函数很像,是为了利用 “尾调用优化” 技术。令 “跳至方法实现” 这一操作跟简单些。

    使用范围:某函数的最后一项操作仅仅是调用另一个函数而不会将其返回值另作他用。 步骤:编译器会生成调转至另一函数所需的指令码,不会向调用堆栈中推人新的 “栈帧”。 不优化后果:

    第12条:理解消息转发机制

    消息转发: 第一阶段:动态方法解析: 征询接收者(所属的类),看其是否能动态添加方法,以处理当前这个 “未知的选择子”。 第二阶段: 1. 备援的接收者: 请接收者看看有没有其他对象(备援的接收者)能处理这条消息。 2. 完整的消息转发机制: 运行期系统会把与消息有关的全部细节都封装到 NSInvocation 对象中,再给接收者最后一次机会,令其设法解决当前还未处理的这条消息。

    动态方法解析

    是否能新增一个实例方法来处理选择子,调用方法如下:

    // 实例方法
    + (BOOL)resolveInstanceMethod:(SEL)selector
    // 类方法
    + (BOOL)resolveClassMethod:(SEL)selector
    

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

    动态添加方法函数如下:

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

    参数说明:

    • cls 添加方法的类
    • name 被添加方法的名字
    • imp 函数指针,指向待添加的方法(C语言实现)。
    • type 待添加方法的 “类型编码”
    备援接收者

    是否有其他对象处理这条消息,调用方法如下:

    - (id)forwardingTargetForSelector:(SEL)selector
    

    若找到备援对象,该方法返回备援对象,反之,返回 nil

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

    完整的消息转发机制

    创建 NSInvocation 对象,此对象包含 选择子目标参数

    消息派发调用方法如下:

    - (void)forwardInvocation:(NSInvocation *)invocation
    

    若发现某调用操作不应由本类处理,则向上寻找,直至 NSObject。如果最后调用了 NSObject 的方法,那么该方法还会继续调用 doesNotRecognizeSelector: 以抛出异常,表明选择子最终未能得到处理。

    总结:

    消息转发全流程如下图:

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

    消息转发代码:https://github.com/AlonerOwl/Runtime/tree/master/Runtime/MessageSend

    第13条:用 ”方法调配(method swizzling)技术“ 调试 ”黑盒方法“

    函数指针(IMP):id (*IMP)(id, SEL, ...)

    方法表:函数指针所组成的一个集合。

    操作类的方法表:

    • 新增选择子
      BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
      
      第12条已经说过了。
    • 改变某选择子所对应的方法实现
    • 交换两个选择子所映射到的指针,也就是交换两个方法实现。 方法交换函数:
      func method_exchangeImplementations(_ m1: Method, _ m2: Method)
      
      参数:两个待交换的方法实现 获取方法实现函数:
      Method class_getInstanceMethod(Class cls, SEL name)
      

    这个方法可以为那些 “完全不知道具体实现” 的黑盒方法增加日志记录功能,这非常有助于程序调试。 若是滥用,反而会令代码变的不易读懂且难于维护。

    method swizzling代码:https://github.com/AlonerOwl/Runtime/tree/master/Runtime/MethodSwizzling

    相关文章

      暂无相关文章

    用户评论