《Effective Objective-C》概念篇
阅读原文时间:2023年07月11日阅读:2
1.运行时
  • OC 语言由 Smalltalk(20世纪70年代出现的一种面向对象的语言) 演化而来,后者是消息型语言的鼻祖。
  • OC 使用动态绑定的消息结构,在运行时检查对象类型。
  • 使用消息结构的语言,其运行时执行的代码由运行环境来决定。而使用函数调用的语言,则由编译器决定。
  • OC 对象所占内存总是分配在“堆空间”,绝不会分配在“栈”上。分配在堆中的内存必须直接管理,而分配在栈上用于保存变量的内存会在栈帧弹出时自动清理。
  • OC 运行期内存管理架构,名叫“引用计数”。
  • OC 中会遇到定义中不含 * 的变量,也是会使用“栈空间”。比如 CoreGraphics 框架的 CGRect,整个系统框架都在使用这种结构体,如果使用 OC 对象,性能会受影响。
2.属性
  • 用来封装对象中的数据。

  • 如果使用了属性的话,那么编译器就会主动编写访问这些属性所需的方法,此过程叫做“自动合成”。这个过程在编译期执行,点语法是编译时特性。

  • @synthesize 语法来指定实例变量的名字。

    @synthesize testString = _testString;

  • @dynamic 关键字会告诉编译器不要创建实现属性所用的实例变量,也不要为其创建存取方法。而且,在编译访问属性的代码时,即使编译器发现没有定义存取方法也不会报错,它相信这些方法能在运行时找到。

    @dynamic testString;
    _testString //Use of undeclared identifier '_testString'

  • 属性特质的设定也会影响编译器所生成的存取方法。属性特质包括:原子性、读写属性、内存管理语义(assign、strong、weak、unsafe_unretained、copy)、方法名(getter==…)。

  • 如果想在其他方法里设置属性值,同样要遵守属性定义中所宣称的语义,因为“属性定义”就相当于“类”和“待设置的属性值”之间所达成的契约

    比如在指定构造器中对成员变量的赋值

    @interface TestObject ()
    //虽然这个属性已经是只读性质,也要写上具体的语义,以此表明初始化方法在设置这些值时所用方法
    @property(copy,readonly) NSString *testString;
    @end

    @implementation TestObject

    • initWithString:(NSString *)string {

      self = [super init];

      if (self) {
      //用初始化方法设置好属性值之后,就不要再改变了,此时属性应设为“只读”
      _testString = [string copy];
      }
      return self;
      }
      @end

3.对象等同性
  • “==”操作符比较的是两个指针本身,而不是所指的对象。应该使用 NSObject 协议中声明的“isEqual”方法来判断两个对象的等同性。

    NSString *textA = @"textA";
    NSString *textAnother = [NSString stringWithFormat:@"textA"];
    NSLog(@"%d",textA == textAnother);// 0
    NSLog(@"%d",[textA isEqual:textAnother]);// 1
    NSLog(@"%d",[textA isEqualToString:textAnother]);// 1
  • 在自定义的对象中正确复写“isEqual”"hash"方法,来判定两个方法是否相等。

  • 如果 “isEqual”方法判定两个对象相等,那么其 hash 方法也必须返回同一个值。

    比如下面这个类

    @interface TestObject : NSObject
    @property NSString *testString;
    @end

    @implementation TestObject

    • (BOOL)isEqual:(id)object {
      if (self == object) return YES;
      if ([self class] != [object class]) return NO;

      TestObject *otherObject = (TestObject *)object;
      if (![self.testString isEqualToString:otherObject.testString]) {
      return NO;
      }
      return YES;
      }
      -(NSUInteger)hash {
      //在没有性能问题下,hash 方法可以直接返回一个数
      return 1227;
      }

    @end

在继承体系中判断等同性,还需判断是否是其子类

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

特定类型等同性判断
  • 自己创建等同性判断方法,无需检测参数类型,大大提升检测速度。就像“isEqualToString”一样。

    • (BOOL)isEqualToTestObject:(TestObject *)testobject {

      if (self == testobject) {
      return YES;
      }
      if (![self.testString isEqualToString:testobject.testString]) {
      return NO;
      }
      return YES;
      }

    • (BOOL)isEqual:(id)object {

      if ([self class] == [object class]) {
      return [self isEqualToTestObject:(TestObject *)object];
      }else {
      return [super isEqual:object];
      }
      }

  • 有时候无需将所有数据逐个比较,只根据其中部分数据即可判明二者是否相等。

比方说一个模型类的实例是根据数据库的数据创建而来,那么其中可能会含有一个唯一标识符(unique identifier),在数据库中用作主键。这时候,我们就可以根据标识符来判定等同性,尤其是此属性声明为 readonly 时更应该如此。只要标识符相等,就可以说明这两个对象是由相同数据源创建,据此断定,其他数据也相等。

当然,只有类的编写者才知道那个关键属性是什么。

要点:不要盲目的逐个检测每条属性,而是应该按照具体需求制定检测方案

4.理解 objc_msgSend
  • 在对象上调用方法是 OC 中经常使用的方法。专业术语叫做“传递消息”,消息有名称(或叫选择子),可以接受参数,或许还有返回值。

  • 在 OC 中,对象收到消息之后,究竟该调用哪个方法完全于运行期决定,甚至可以在运行时改变,这些特性使 OC 成为一门真正的动态语言。

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

id value = [obj messageName:parameter]

obj 叫做接收者,messageName 叫做 selector,selector 和参数合起来称为消息

编译器看到此消息后,将其转换为一条标准的 C 语言函数调用

void objc_msgSend(id _Nullable self, SEL _Nonnull op, …)

第一个参数代表接收者,第二个代表 selector(SEL是selector类型)

这是个“参数个数可变的函数”,”…“ 代表后续参数,就是消息中的参数

  • objc_msgSend 方法在接收者所属的类中搜寻其”方法列表“,如果能找到与 selector 名称相符的方法,就跳至其实现代码。若是找不到,就沿着继承体系继续向上查找,等找到合适的方法之后再跳转。如果最终还是找不到相符的方法,就执行”消息转发“(在之后解释)。

  • 看起来,想调用一个方法似乎需要很多步骤。所幸 objc_msgSend 会将匹配结果缓存在”快速映射表“中,每个类都有这样一个缓存,若是后来还向该类发送相同的消息,那么执行起来就会很快了。

  • 其他消息发送函数

    //Sends a message with a simple return value to the superclass of an instance of a class.
    objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, …)
    //Sends a message with a data-structure return value to an instance of a class.
    objc_msgSend_stret(id _Nullable self, SEL _Nonnull op, …)
    //Sends a message with a data-structure return value to the superclass of an instance of a class.
    objc_msgSendSuper_stret(struct objc_super * _Nonnull super, SEL _Nonnull op, …)

  • 刚才提到 objc_msgSend 找到合适的方法之后,就会”跳转过去“。之所以可以这样做是因为 OC 对象的每个方法都可以视为简单的 C 函数,原型如下:

    Class_selector(id self, SEL _cmd, …)

每个类都有一张表格,selector 的名称就是查表时所用的 key

  • 原型的样子和 objc_msgSend 很像,而且函数的最后一项操作是调用另一个函数而且不会将其返回值另作他用,就可以利用”尾调用优化“技术,令”跳至方法实现“变得简单。

尾调用技术:编译器会生成跳转至另一函数所需的指令码,而且不会向调用堆栈中推入新的”栈帧“

  • 要点:发给某对象的全部消息都要由“动态消息派发系统”来处理,该系统会查出对应的方法,并执行其代码
5.消息转发机制
  • 对象在收到无法解读的消息之后会发生什么情况?如果在控制台中看到上面这种提示信息,那就说明你给对象发送了一条其无法解读的消息,启动了消息转发机制

    • 因为在运行期可以继续向类中添加方法,所以编译器在编译期还无法确知类中是否有某个方法的具体实现。
    • 当对象接收到无法解读的消息,就会触发“消息转发机制”,程序员可以经由此过程告诉对象如何处理未知消息

消息转发分为两大阶段

  • 第一阶段:征询接收者,能否动态添加方法,处理当前这个“未知的 selector”

    //当未知的 selector 是实例方法时的调用

    • (BOOL)resolveInstanceMethod:(SEL)sel ;
      //当未知的 selector 是类方法时的调用
    • (BOOL)resolveClassMethod:(SEL)sel;

使用这种办法的前提是:相关方法的实现代码已经写好,只等着运行时加入类里面

  • 第二阶段:运行时系统会请求接收者以其他手段处理与消息相关的方法调用,可细分为两小步。

    //第一步:询问能不能把未知的消息转给其他接收者处理

    • (id)forwardingTargetForSelector:(SEL)aSelector ;

若当前接收者能找到备援接收者,则将其返回,若找不到,则返回 nil

-如果返回一个对象,则运行期系统把消息转给那个对象,于是消息转发结束

如果返回 nil,执行第二步