Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Objective-C 语言 #39

Open
ShannonChenCHN opened this issue Apr 27, 2017 · 13 comments
Open

Objective-C 语言 #39

ShannonChenCHN opened this issue Apr 27, 2017 · 13 comments

Comments

@ShannonChenCHN
Copy link
Owner

ShannonChenCHN commented Apr 27, 2017


相关文档


延伸阅读

@ShannonChenCHN ShannonChenCHN changed the title 【专题】Objective-C 语言 Objective-C 语言 Jun 30, 2017
@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Oct 19, 2017

KVC 和 KVO

1. KVO(Key-Value Observing)

1.1 使用 KVO 时常见的两种崩溃场景:

(1) Instance was deallocated while key value observers were still registered with it

注:不过在最新的 iOS 版本中没有复现。

原因
A 监听了 B,如果 B 销毁时 A 还没有移除监听,就会导致崩溃。一般我们会在 A 的 dealloc 方法中移除监听,但是如果 B 的生命周期早于 A 结束,就会触发这种问题。
解决办法
核心在于保证在 B 被销毁前移除所有的监听者。当我们不知道 B 何时被销毁前,我们可以在 A 中通过实例变量保持对 B 的强引用,然后再在 A dealloc 时,移除监听。

@interface Observer : NSObject

@property (nonatomic, weak) ObserveredObject *object;

@end

@implementation Observer

- (void)dealloc {

	[self.object removeObserver:self forKeyPath:NSStringFromSelector(@selector(description))];

}


- (void)setObject:(ObserveredObject *)object {
  _object = object;

  [object addObserver:self forKeyPath:NSStringFromSelector(@selector(description)) options:NSKeyValueObservingOptionNew context:nil];
}

@end
////////////////////////////////////////////////////////////
@interface ObservedObject : NSObject

@end
@implementation ObservedObject

@end
////////////////////////////////////////////////////////////
int main() {
  Observer *observer = [[Observer alloc] init];
  {
    ObserveredObject *obj1 = [[ObserveredObject alloc] init];
    observer.object = obj1;
  }  // 会在这里发生崩溃
  
  return 0;
}

(2) Cannot remove an observer <Person 0x102fdd920> for the key path "page" from <Book 0x17022f3c0> because it is not registered as an observer.

原因:Person 类没有调用 -addObserver: forKeyPath: 注册为 Book 类的监听者,但是调用了 -removeObserver: forKeyPath: 方法。重复移除监听者就会导致这种问题。

1.2 KVO 的原理

@interface Sark : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation Sark

- (void)willChangeValueForKey:(NSString *)key {
}

@end
////////////////////////////////////////////////

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Sark *sark = [Sark new];
	  Sark *sarkB = [Sark new];
  
    //> breakpoint 1
    [sark addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
    //> breakpoint 2
    sark.name = @"萨萨萨";
    [sark removeObserver:self forKeyPath:@"name"];
    //> breakpoint 3
    NSLog(@"结束");
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { }

在三个断点处分别打出 Sark 的 class 和 isa(或者直接展开 debug area 中的变量):

// breakpoint 1
(lldb) po sark.class
Sark
(lldb) po object_getClass(sark)
Sark

// breakpoint 2
(lldb) po sark.class
Sark
(lldb) po object_getClass(sark)
NSKVONotifying_Sark

// breakpoint 3
(lldb) po sark.class
Sark
(lldb) po object_getClass(sark)
Sark

下面是苹果官方文档中对 KVO 实现细节的介绍:

Key-Value Observing Implementation Details
Automatic key-value observing is implemented using a technique called isa-swizzling.

The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.

When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.

You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.

下面我们通过符号断点 objc_allocateClassPair,然后再分析函数堆栈中汇编来看看 KVO 的大概实现过程,:

image

创建新类和修改 isa 的过程主要在 _NSKVONotifyingCreateInfoWithOriginalClass 函数中:

Foundation`_NSKVONotifyingCreateInfoWithOriginalClass:
    ...
    0x7fff207b40ea <+59>:  callq  0x7fff20949f14            ; symbol stub for: class_getName
    ...
    0x7fff207b410c <+93>:  leaq   0x1badbd(%rip), %rsi      ; _NSKVONotifyingCreateInfoWithOriginalClass.notifyingClassNamePrefix
    ...
    0x7fff207b4137 <+136>: callq  0x7fff2094a3ee            ; symbol stub for: objc_allocateClassPair
    ...
    0x7fff207b414b <+156>: callq  0x7fff2094a4b4            ; symbol stub for: objc_registerClassPair
    ...
    0x7fff207b415b <+172>: callq  0x7fff2094a526            ; symbol stub for: object_getIndexedIvars
    ...
    0x7fff207b41c1 <+274>: cmpq   $-0x1, 0x6640b667(%rip)   ; _NSKVONotifyingCreateInfoWithOriginalClass.onceToken + 7
    ...
    0x7fff207b41d2 <+291>: movq   0x663f1237(%rip), %rsi    ; "willChangeValueForKey:"
    0x7fff207b41d9 <+298>: callq  0x7fff20949f0e            ; symbol stub for: class_getMethodImplementation
    0x7fff207b41de <+303>: movb   $0x1, %cl
    0x7fff207b41e0 <+305>: cmpq   0x6640b651(%rip), %rax    ; _NSKVONotifyingCreateInfoWithOriginalClass.NSObjectWillChange
    ...
    0x7fff207b41ec <+317>: movq   0x663f1225(%rip), %rsi    ; "didChangeValueForKey:"
    0x7fff207b41f3 <+324>: callq  0x7fff20949f0e            ; symbol stub for: class_getMethodImplementation
    0x7fff207b41f8 <+329>: cmpq   0x6640b641(%rip), %rax    ; _NSKVONotifyingCreateInfoWithOriginalClass.NSObjectDidChange
    ...
    0x7fff207b4206 <+343>: movq   0x663f1223(%rip), %rsi    ; "_isKVOA"
    0x7fff207b420d <+350>: leaq   0x1ef(%rip), %rdx         ; NSKVOIsAutonotifying
    ...
    0x7fff207b4219 <+362>: callq  0x7fff207b4372            ; NSKVONotifyingSetMethodImplementation
    0x7fff207b421e <+367>: movq   0x663ee23b(%rip), %rsi    ; "dealloc"
    0x7fff207b4225 <+374>: leaq   0x1df(%rip), %rdx         ; NSKVODeallocate
    ...
    0x7fff207b4231 <+386>: callq  0x7fff207b4372            ; NSKVONotifyingSetMethodImplementation
    0x7fff207b4236 <+391>: movq   0x663f11fb(%rip), %rsi    ; "class"
    0x7fff207b423d <+398>: leaq   0x41e(%rip), %rdx         ; NSKVOClass
    ...
    0x7fff207b4249 <+410>: callq  0x7fff207b4372            ; NSKVONotifyingSetMethodImplementation

总结下来,KVO 的实现原理就是:

  • 当你第一次观察这个 sark 对象时,runtime 会动态创建一个继承自 Sark 的子类 NSKVONotifying_Sark。在这个新的 class 中,它重写了所有被观察的属性,然后将这个 sark 对象 的isa指针指向新创建的NSKVONotifying_Sark,所以这个 sark 对象就神奇地变成了一个新类的实例。(值得注意的是,上面例子中的 sarkB 对象的 isa 一直是Sark

  • 这个新类中会用一个函数(对于对象类型的属性就是 _NSSetObjectValueAndNotify())将被观察的 setter 方法“包”起来,它在调用真正的 setter 方法之前和之后分别调用了 willChangeValueForKeydidChangeValueForKey方法(只有在调用了 setter 方法或者使用 KVC 修改属性值才会触发 KVO,直接修改 ivar 值是不会触发 KVO 的)。

  • 有意思的是:苹果不希望这个机制暴露在外部。除了setter,这个动态生成的类同时也重写了其他几个必须实现的方法,其中一个就是-class方法,依旧返回原先的class(这一点跟苹果文档中所说的一致)。如果不仔细看的话,被 KVO 过的 object 看起来和原先的 object 没什么两样。

  • 另外,当我们重写被观察属性的 setter 方法时,不需要手动调用 willChangeValueForKeydidChangeValueForKey方法

如果你自己实现了一个 Sark 的子类 NSKVONotifying_Sark ,KVO 就会失效,console 上报错:

[general] KVO failed to allocate class pair for name NSKVONotifying_Sark, automatic key-value observing will not work for this class

参考

2. KVC (Key-Value Coding)

2.1 -setValue:forKey:的原理

当我们设置 setValue:forKey:时:

  • 首先会查找 setKey:_setKey: (按顺序查找)

  • 如果有直接通过 objc_msgSend 调用对应的 setter 方法

  • 如果没有,先查看 accessInstanceVariablesDirectly方法

+ (BOOL)accessInstanceVariablesDirectly{
      return YES;   ///> 可以直接访问成员变量
  //    return NO;  ///>  不可以直接访问成员变量,  
  ///> 直接访问会报NSUnkonwKeyException错误  
  }
  • 如果可以访问会按照 _key_isKeykeyiskey 的顺序查找成员变量

  • 找到直接复制

  • 未找到报错NSUnkonwKeyException错误

2.2 -valueForKey:的原理

  • kvc取值按照 getKeykeyiskey_key 顺序查找方法

  • 存在直接调用通过 objc_msgSend 调用对应的 getter 方法

  • 如果没找到先查看 accessInstanceVariablesDirectly 方法

+ (BOOL)accessInstanceVariablesDirectly{
      return YES;   ///> 可以直接访问成员变量
  //    return NO;  ///>  不可以直接访问成员变量,  
  ///> 直接访问会报NSUnkonwKeyException错误  
  }
  • 如果可以访问会按照 _key_isKeykeyiskey 的顺序查找成员变量

  • 找到直接复制

  • 未找到报错 NSUnkonwKeyException 错误

参考:

延伸阅读:

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Nov 14, 2017

Objective-C 语言

  • 动态绑定
  • 消息转发
  • 泛型

问题:当子类需要使用父类的一个私有属性(方法)时,需要把这个属性(方法)放到父类的header中,但暴露给子类的同时暴露给了外部调用者,如何解决?

建立一个私有header,使用类扩展定义父类需要暴露给子类的属性(方法),然后在各自的.m文件中引用。

参考

@ShannonChenCHN
Copy link
Owner Author

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Nov 19, 2017

@ShannonChenCHN
Copy link
Owner Author

指定初始化方法

#57

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Jan 22, 2018

Objective-C 属性

1. Objective-C 属性声明和合成

  • @synthesize@dynamic
  • 属性修饰符

2. Objective-C 属性的结构

  • ivar、getter、setter

3. Objective-C class properties

  • 什么是类属性
  • 如何定义类属性
    • getter 和 setter
  • 如何初始化类属性

参考:

Repository owner deleted a comment from hbfangqiao Feb 4, 2018
@ShannonChenCHN
Copy link
Owner Author

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Mar 13, 2018

Tag Pointer

1. 为什么要使用 Tag Pointer?

当从32位机器迁移到64位机器中后,对象的指针浪费了更多的内存。

(1)我们先看看原有的对象为什么会浪费内存:假设我们要存储一个NSNumber对象,其值是一个整数。正常情况下,如果这个整数只是一个NSInteger的普通变量,那么它所占用的内存是与CPU的位数有关,在32位CPU下占4个字节,在64位CPU下是占8个字节的。而指针类型的大小通常也是与CPU位数相关,一个指针所占用的内存在32位CPU下为4个字节,在64位CPU下也是8个字节。

所以一个普通的iOS程序,如果没有Tagged Pointer对象,从32位机器迁移到64位机器中后,虽然逻辑没有任何变化,但这种NSNumber、NSDate一类的对象所占用的内存会翻倍。

(2)效率上的问题:为了存储和访问一个NSNumber对象,我们需要在堆上为其分配内存,另外还要维护它的引用计数,管理它的生命期。这些都给程序增加了额外的逻辑,造成运行效率上的损失。

2. Tag Pointer 是什么?

由于NSNumber、NSDate一类的变量本身的值需要占用的内存大小常常不需要8个字节,拿整数来说,4个字节所能表示的有符号整数就可以达到20多亿(注:2^31=2147483648,另外1位作为符号位),对于绝大多数情况都是可以处理的。

我们可以将一个对象的指针拆成两部分,一部分直接保存数据,另一部分作为特殊标记,表示这是一个特别的指针,不指向任何一个地址。所以,引入了Tagged Pointer对象之后,64位CPU下NSNumber的内存图变成了以下这样:


(图片来源:infoQ)

Tagged Pointer有一个简单的应用,那就是NSNumber。它使用60位来存储数值。最低位置1。剩下3位为NSNumber的标志。在这个例子中,就可以存储任何所需内存小于60位的数值。

示例:

        NSNumber *number1 = @1;
        NSNumber *number2 = @2;
        NSNumber *number3 = @3;
        NSNumber *numberFFFF = @(0xFFFF);
        NSNumber *bigNumber = @(0xEFFFFFFFFFFFFFFF);

        NSLog(@"number1 pointer is %p", number1);
        NSLog(@"number2 pointer is %p", number2);
        NSLog(@"number3 pointer is %p", number3);
        NSLog(@"numberffff pointer is %p", numberFFFF);
        NSLog(@"bigNumber pointer is %p", bigNumber);

结果如下:

number1 pointer is 0xb000000000000012
number2 pointer is 0xb000000000000022
number3 pointer is 0xb000000000000032
numberFFFF pointer is 0xb0000000000ffff2
bigNumber pointer is 0x10921ecc0

对于前 4 个 NSNumber,除去最后的数字最末尾的2以及最开头的0xb,其它数字刚好表示了相应NSNumber的值。而 bigNumber 的地址更像是一个普通的指针地址。我们可以推断,当8字节可以承载用于表示的数值时,系统就会以Tagged Pointer的方式生成指针,如果8字节承载不了时,则又用以前的方式来生成普通的指针。

3. Tagged Pointer 是一个能够提升性能、节省内存的有趣的技术,其特点如下:

  • Tagged Pointer 专门用来存储小的对象,例如 NSNumber 和 NSDate(后来可以存储小字符串)
  • Tagged Pointer 指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。
  • 它的内存并不存储在堆中,也不需要 malloc 和 free,所以拥有极快的读取和创建速度。

4. Tagged Pointer 所带来的问题

因为 Tagged Pointer 并不是真正的对象,而是一个伪对象,所以你如果完全把它当成对象来使,可能会让它露马脚。所有对象都有 isa 指针(实际上最新的 runtime 源码实现已经改了),而 Tagged Pointer 其实是没有的,因为它不是真正的对象。 因为不是真正的对象,所以如果你直接通过 obj->isa 来访问 Tagged Pointer 的 isa 成员的话,在编译时将会有警告。

所以我们应该换成相应的方法或者函数调用,如 isKindOfClass:object_getClass,避免在代码中直接访问对象的 isa 变量。

参考:

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Mar 22, 2018

在 Objective-C 对象内部应该采用何种方式存取实例变量?

1. 设置变量值时最好通过 setter 方法来访问实例变量,获取变量值时直接访问实例变量

原因:

  • 内存管理
  • KVO

直接访问实例变量的方式比通过 getter 的方式效率要高,因为省去了消息发送的过程

2. dealloc 方法中应该直接访问实例变量

原因:
在 dealloc 被调用时,对象的一些资源已经被释放,如果通过调用 getter 方法的形式来调用,可能会导致一些问题。(待验证)

《Effective-C Objective-C 2.0》这本书上建议在 init 方法中也要直接访问实例变量,实际上书中给出的例子本身以及解释就有问题,既然子类重写了 setter 方法,那么在逻辑上,是否需要在 init 方法中调用 setter 方法,应该由开发者自己来决定,但是一般情况下,对于个别属性重写 setter 的情况还是要统一调用 setter方法;再次,如果是调用父类的属性,还是要调用 getter 方法的。

3. 懒加载的属性必须要通过 getter 方法来访问

参考:

  • 《Effective-C Objective-C 2.0》
  • 唐巧的博客

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Mar 28, 2018

isEqual:hash

1. NSObject 的子类如何实现 isEqual: 方法?

NSObject 的子类在实现它们自己的 isEqual: 方法时,应该完成下面的工作:

  • 实现一个新的 isEqualTo__ClassName__ 方法,进行实际意义上的值的比较。
  • 重载 isEqual: 方法进行类和对象的本体性检查,如果失败则回退到上面提到的值比较方法。
  • 重载 hash 方法

示例代码:

@implementation NSArray (Approximate)
- (BOOL)isEqualToArray:(NSArray *)array {
  if (!array || [self count] != [array count]) {
    return NO;
  }

  for (NSUInteger idx = 0; idx < [array count]; idx++) {
      if (![self[idx] isEqual:array[idx]]) {
          return NO;
      }
  }

  return YES;
}

- (BOOL)isEqual:(id)object {
  if (self == object) {
    return YES;
  }

  if (![object isKindOfClass:[NSArray class]]) {
    return NO;
  }

  return [self isEqualToArray:(NSArray *)object];
}
@end

2. 为什么要有hash方法?

从一个未排序的数组中查找一个元素的时间复杂度是 O(n),效率比较低,所以就出现了散列表(Hash Table)。

散列表是程序设计中基础的数据结构之一,它使得 NSSet 和 NSDictionary 能够非常快速地(O(1)) 进行元素查找。

通过把散列表和数组进行对比,有助于我们更好地理解散列表的性质:

  • 数组把元素存储在一系列连续的地址当中,例如一个容量为 n 的数组当中,包含了位置 0,1 一直到 n-1 这么多空白的槽位。要判断一个元素是不是在数组中存在,需要对数组中每个元素的位置都进行检查(除非数组当中的元素是已经排序过的,那是另一回事了)。
  • 散列表使用了一个不太一样的办法。相对于数组把元素按顺序存储(0, 1, ..., n-1),散列表在内存中分配 n 个位置,然后使用一个函数来计算出位置范围之内的某个具体位置。散列函数需要具有确定性。一个 好的 散列函数在不需要太多计算量的情况下,可以使得生成的位置分布接近于均匀分布。当两个不同的对象计算出相同的散列值时,我们称其为发生了 散列碰撞 。当出现碰撞时,散列表会从碰撞产生的位置开始向后寻找,把新的元素放在第一个可供放置的位置。随着散列表变得越来越致密,发生碰撞的可能性也会随之增加,导致查找可用位置花费的时间也会增加(这也是为什么我们希望散列函数的结果分布更接近于均匀分布)。

散列函数就是用来计算 hash 值的函数。

3. hash 方法什么时候被调用?

hash方法只在对象被添加至 NSSet 和设置为 NSDictionary 的 key 时会调用:

  • NSSet 添加新成员时, 需要根据 hash 值来快速查找成员, 以保证集合中是否已经存在该成员
  • NSDictionary 在查找 key 时, 也利用了 key 的 hash 值来提高查找的效率

4. hash 方法和相等性的关系

如果两个对象相等,它们的 hash 值也一定是相等的。但是反过来则不然,两个对象的散列值相等不一定意味着它们就是相等的。

为了优化判等的效率, 基于hash的NSSet和NSDictionary在判断成员是否相等时, 会这样做:

  • Step 1: 集成成员的hash值是否和目标hash值相等, 如果相同进入Step 2, 如果不等, 直接判断不相等。
  • Step 2: hash值相同(即Step 1)的情况下, 再进行对象判等, 作为判等的结果。

5. NSObject 的子类如何实现 hash 方法?

在实现一个 hash 函数的时候,一个很常见的误解来源于肯定后项,认为 hash 得到的值 必须 是唯一可区分的。这种误解往往会导致没必要的包含着从 Java 课本里抄袭过来的素数以及魔法咒语一样的操作的实现。实际上,对于关键属性的散列值进行一个简单的 XOR操作,就能够满足在 99% 的情况下的需求了。

其中需要技巧的一点是,找出哪个值对于对象来说是关键的。

例如,对于一个 NSDate 对象来说,从一个参考日期到它本身的时间间隔就已经足够了:

@implementation NSDate (Approximate)
- (NSUInteger)hash {
  return (NSUInteger)abs([self timeIntervalSinceReferenceDate]);
}

参考:

@ShannonChenCHN
Copy link
Owner Author

Objective-C 中的保留字

参考

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Apr 19, 2018

#import vs. @Class vs. @import

1. #import 和 @Class 之间的区别

1.1 #import 会包含这个类的所有信息,包括实体变量和方法,而 @Class 只是告诉编译器,其后面声明的名称是类的名称,至于这些类是如何定义的,暂时不用考虑。

1.2 在编译效率方面考虑,如果你有100个头文件都 #import 了同一个头文件,或者这些文件是依次引用的,如 A–>B, B–>C, C–>D 这样的引用关系。当最开始的那个头文件有变化的话,后面所有引用它的类都需要重新编译,如果你的类有很多的话,这将耗费大量的时间。而是用 @Class则不会。

1.3 如果有循环依赖关系,如:A–>B, B–>A这样的相互依赖关系,如果使用 #import 来相互包含,那么就会出现编译错误,如果使用 @Class 在两个类的头文件中相互声明,则不会有编译错误出现。

2. 分别在什么时候使用 #import 和 @Class

在头文件中,一般只需要知道被引用的类的名称就可以了。 不需要知道其内部的实体变量和方法,所以在头文件中一般使用 @Class 来声明这个名称是类的名称。 而在实现类里面,因为会用到这个引用类的内部的实体变量和方法,所以需要使用 #import 来包含这个被引用类的头文件。

3. @import#import 的区别

@import 的几个优点:

  • You can use modules with any of the system frameworks in iOS 7 and Mavericks.
  • Loads binary representation
  • More flexible than precompiled headers
  • Doesn’t need to parse the headers
  • being safer and more efficient than #import.
  • you don't need to add the framework in the project settings, it's done automatically.

4. #import "header.h"#import <header/header.h>" 的区别

#import ensures that a file is only ever included once so that you never have a problem with recursive includes.

#import "" first check the header in project folder then goes to system library, and the #import<> checks for system headers”.

4.1 "" are used for local files. That means files in the current directory.
< and > are used for system files found in the folders of your path. /usr/include is probably one of them.

4.2 It means the difference is in the order, in which the compiler searches different folders for files. The “fine.h” form gives precedence to the current folder (the one where the containing source file is). The <> form searches the system which include its own folder first.

4.3 So far my observation is that the quote marks "" searches for files in your project that you’ve got the implementation source to, and angle brackets <>when you’re referencing a library or framework.

总结起来就是,#import ""#import<> 都可以用来导入“本地”的文件,也就是当前 project 目录中包含的文件,也可以用来导入库文件,比如系统库所在的 /usr/include 目录中的文件。但是他们的区别在于编译器查找文件的顺序,#import ""优先查找“本地”的文件,而 #import<>优先查找系统目录。

5. 关于 @import 和 LLVM Module

https://clang.llvm.org/docs/Modules.html#problems-with-the-current-model
https://llvm.org/devmtg/2012-11/Gregor-Modules.pdf
https://onevcat.com/2013/06/new-in-xcode5-and-objc/#%E5%85%B3%E4%BA%8Eobjective-cmodules%E5%92%8Cautolinking

6. 参考

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Jun 26, 2018

isEqual: VS. isEqualToString:

1. performance

Apple 提供的 API Reference 中的说明是 :

When you know both objects are strings, this method is a faster way to check equality than isEqual:.

然而,实际上根据 Mark Dalrymple 的测试情况,各个方法的试验对比结果在不同机器上并不相同。而且即便是在百万万次的比较计算下,个方法之间的耗时并不大。所以,性能表现并不是决定选择哪个方法的关键因素。

2. type safety

理论上来说,isEqualToString:isEqual: 在类型诊断上做的更好。因为使用 isEqualToString: 方法时,如果 receiver 不是字符串的话,要么在编译时出现警告,要么就会在运行时发生奔溃。但是,如果 isEqualToString: 方法的参数没有显式声明类型的话,编译器不会警告,运行时只是结果会返回 NO,所以这也并不能保证类型准确,即便结果是对的。

因此,在这里,isEqualToString:isEqual: 仅仅是在语义上的区别,如果你是将一个字符串对象跟其他类型的对象进行比较的话,那么选用 isEqual: ,如果是想告诉阅读代码的人这是要比较两个字符串的话,那么就选用 isEqualToString:

3. compare:

compare: 也可以用来判断对等性,其跟前面两个方法的区别在于:

  • compare: 可以用来判断两个对象的“顺序关系”,这个方法的返回结果是一个 NSComparisonResult 类型的值
  • isEqualToString:isEqual: 进行的是逐字的字面比较,而 compare: 会考虑到字节级别的比较。比如 @"\u00f6"@"o\u0308" 在字面上不相同,但是字符编码结果相同(都是)。

4. nil

需要考虑两种情况:

  • 两个都为空时,视为相等
  • 两个都为空时,视为不相等

因为在 Objective-C 中给 nil 发消息,始终返回 0。所以给 nil 发送 isEqualToString:isEqual:,结果都是 NO,适合上面的第二种情况。如果要实现“两个都为空时,视为相等”,则需要手动检测了。

另外,给 nil 发送 compare: 消息,结果是 NSOrderedSame,所以需要注意下。

参考

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant