必备知识架构-数据结构-①nil、isEqual

[toc]

前言:你真的懂isEqual与hash?

参考文章:

eg:

1
2
1、NSMutableSet/NSSet中添加对象的时候,就会调用- (NSUInteger)hash方法。
2、NSMutableSet/NSSet中添加 obj2 对象的时候,如果NSMutableSet/NSSet 中之前就已经存在 obj1 对象,且 obj1 对象的 - (NSUInteger)hash 返回值和当前要添加的 obj2 的- (NSUInteger)hash 返回值相等, 则 obj2 会继续调用- (BOOL)isEqual:方法,其中此方法以 obj1 为参数;否则不等, 继续下一个元素判断。

通过重写isEqual,我们可以做到对象的判等可以完全由您决定, 即使两个完全不同的对象。

一、基础

1、nil, Nil,NULL 与 NSNull 的区别

  • nil 指向一个对象的指针为空,在objc.h 的定义如下: NSString *name = nil;
  • Nil 指向一个类的指针为空,定义如下: Class aClass = Nil;
  • NULL 指向C类型的指针为空, 例如: int*pInt = NULL;
  • NSNull 在Objective-C中是一个类,只是名字中有个Null,多用于集合(NSArray,NSDictionary)中值为空的对象

2、==、 isEqualToString、isEqual

2.1、背景:为什么要有isEqual方法?

答:判断两个对象是否相等(而不是判断两个对象的地址是否相等)。

对于基本类型, ==运算符比较的是值;
对于对象类型, ==运算符比较的是对象的地址(即是否为同一对象)

1
2
3
4
5
6
7
8
UIColor *color1 = [UIColor colorWithRed:0.5 green:0.5 blue:0.5 alpha:1.0];
UIColor *color2 = [UIColor colorWithRed:0.5 green:0.5 blue:0.5 alpha:1.0];
NSLog(@"color1 == color2 = %@", color1 == color2 ? @"YES" : @"NO");
NSLog(@"[color1 isEqual:color2] = %@", [color1 isEqual:color2] ? @"YES" : @"NO");

// 打印结果如下:
color1 == color2 = NO
[color1 isEqual:color2] = YES

总结:

1、==
判断指针指向的地址,即指针值,是否相等
①对于基本类型, ==运算符比较的是值;
②对于对象类型, ==运算符比较的是对象的地址(即是否为同一对象))

2、isEqualToString
直接判断字符串内容是否相等

3、isEqual
先判断两个对象的地址是否相同,再判断类型是否一致,然后再判断对象的具体内容是否一致
//情况①:地址相同则肯定相等;
//情况②:地址不同时候,如果类型一致且里面的内容相等,则也算相等

2.1、苹果官方重写isEqual 的demo

isEqual:先判断两个对象的地址是否相同(采用==),再判断类型是否一致(采用isKindOfClass),然后再判断对象的具体内容是否一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (BOOL)isEqual:(id)object {
if (self == object) {
return YES;
}

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

return [self isEqualToPerson:(Person *)object];
}

- (BOOL)isEqualToPerson:(Person *)person {
if (!person) {
return NO;
}

BOOL haveEqualNames = (!self.name && !person.name) ||
[self.name isEqualToString:person.name];
BOOL haveEqualBirthdays = (!self.birthday && !person.birthday) ||
[self.birthday isEqualToDate:person.birthday];

return haveEqualNames && haveEqualBirthdays;
}

附:通过重写isEqual,我们可以做到对象的判等可以完全由您决定, 即使两个完全不同的对象。

2.2、==、 isEqualToString、isEqual验证示例

equal

四、weak

以下内容来自: iOS weak关键字实现原理

  • 1、weak关键字修饰对象的【初始化及重新赋值】实现原理
  • 2、weak关键字是如何保证在weak对象执行语句时内存对象不被释放的呢?
  • 3、当weak对象指向的内存对象被释放后,weak对象自动置为nil。原理是怎样?

1、weak关键字修饰对象初始化及重新赋值实现原理

objc_initWeak:当初始化一个weak对象并将内存对象赋值给该weak对象时会调用该方法。

objc_storeWeak:当为一个weak对象赋新值时会调用该方法。

1
2
3
4
5
6
7
8
9
10
11
12
objc_initWeak(id *location, id newObj)
{
//对内存对象进行非nil判断
if (!newObj) {
*location = nil;
return nil;
}

//调用storeWeak方法,三个参数的意义在storeWeak方法中描述
return storeWeak<false/*old*/, true/*new*/, true/*crash*/>
(location, (objc_object*)newObj);
}
1
2
3
4
5
6
objc_storeWeak(id *location, id newObj)
{
//调用storeWeak方法,三个参数的意义在storeWeak方法中描述
return storeWeak<true/*old*/, true/*new*/, true/*crash*/>
(location, (objc_object *)newObj);
}

由官方文档可知,无论是初始化weak对象还是为weak对象赋值,最终都会调用到 storeWeak方法,不同点在于,二者传入三个模板值不同。

如果weak对象与旧内存对象的关联,则先调用weak_unregister_no_lock,将weak对象与旧内存对象解除指向关系。

然后在将weak对象与旧内存对象的关联解除后,就调用 weak_register_no_lock方法将weak对象与新内存对象进行关联

至此,weak关键字修饰对象的初始化和重新赋值流程就完成了。

2、weak关键字是如何保证在weak对象执行语句时内存对象不被释放的呢?

问:weak关键字是如何保证【在weak对象执行语句时】内存对象【不被释放】的呢?

答:其实很简单,就是对内存对象进行计数增加。

每次在使用weak对象时,都相当于调用一次 objc_loadWeak

在weak对象执行的语句中,weak对象所指向的内存对象计数会+1,这样就保证在语句中不会发生执行一半而释放内存对象的问题。

3、当weak对象指向的内存对象被释放后,weak对象自动置为nil。原理是怎样?

当内存对象释放时,会调用到一个叫weak_claer_no_lock的方法。在 weak_claer_no_lock方法中,会进行对weak对象的置空操作。

4、Weak-Strong搭配使用的误解

在使用Block时,我们可以使用weak关键字来避免外部变量被Block强引用而导致的循环引用,同时为了Block中的代码能够正常执行,许多开发者提出了Weak-Strong搭配使用的方式,类似如下:

1
2
3
4
5
6
7
8
{
__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
[strongSelf test1];
[strongSelf test2];
};
}

以上代码相对于单独使用weak来说还是有好处的,使用Weak-Strong搭配的方式的话,可以保证在执行 [strongSelf test1]和 [strongSelf test2]时,是向同一对象发送消息。

为什么这么说呢?当开始执行Block语句时,若self还存在,那么strongSelf可以保证在整个Block代码块中不会被释放,即使Block中调用无数次strongSelf,strongSelf也不会因为多线程而在半途被释放;若开始执行Block时,self已经被释放,那么之后所有的消息都会被发送至nil。所以Weak-Strong搭配可以保证Block中语句被处理为一个事务。

1
2
3
4
5
6
7
{
__weak typeof(self) weakSelf = self;
self.block = ^{
[weakSelf test1]; // 可能这时候self还没释放,有值。
[weakSelf test2]; // 而到这时候self刚好释放,为ni。导致Block中语句不是被发送给同一对象去处理。
};
}

所以说,Weak-Strong并不能保证Block中语句一定会被执行,它只能保证Block中语句作为一个事务被发送到同一对象处。只要理解了weak实现原理,我们就能明白何时单独使用weak也能完成代码功能,而何时必须使用Weak-Strong来保证代码事务能力。

5、weak关键字涉及到的数据结构

略。请查看 iOS weak关键字实现原理

1、SideTables

SideTables本质上是一个全局的 StripedMap。而StripedMap本质是一个数组。

SideTables中, Item类型为 SideTable

2、SideTable

SideTable中包含三个元素,分别是 1.自旋锁 2.记录对象引用计数的字典 3.记录对象弱引用信息的数据结构 weak_table_t

3、weak_table_t

weak_table_t本质上是一个数组,其中每个Item为 weak_entry_t

4、weak_entry_t

weak_entry_t就比较有意思了,它本质上是个字典。其中的key值为对象,而value对应为一个数组,数组中保存的值均为 weak_referrer_t类型的数据。

5、weak_referrer_t

weak_referrer_t本质上是 objc_object **,即Objective-C对象的地址。

所以,weak_entry_tvalue数组中,每一个Item均为一个地址,即weak对象的地址。

在项目中有某个功能需要用到多个delegate对象,这就需要把delegate放到容器中,但又只能弱引用weak保存delegate,否则会导致delegate对象不能被释放。所以,我们的需求就是希望能在容器中只保存弱引用的delegate对象。

在OC中Foundation框架中的常用容器类(NSSet,NSDictionary,NSArray)及其可变子类在加入元素时,均会对元素进行强引用。有的时候(比如持有多个Delegate对象时),希望有对应的弱引用容器使用。

支持弱引用的容器类,有如下:

[NSHashTable weakObjectsHashTable]

[NSPointerArray weakObjectsPointerArray]

[NSPointerArray pointerArrayWithOptions:]

详见:YYKit学习笔记之NSHashTable与NSMapTable

其他参考:

在iOS项目开发过程中,我们经常会使用到NSSetNSArrayNSDictionary三个类,它们为我们设计较友好的数据结构时提供了很方便的方法\

对于NSSet来说,object是强引用的,和NSDictionary中的value是一样的。而NSDictionary中的key则是copy的,因此当开发者想要使NSSet的objects或者NSDictionary的values为weak,或者NSDictionary使用没有实现协议的对象作为key时,比较麻烦(需要使用NSValue的方法valueWithNonretainedObject)。
还好苹果为我们提供了相对于NSSet和NSDictionary更通用的两个类NSHashTable和NSMapTable。

关联对象实现原理

iOS底层原理总结 - 关联对象实现原理

关联对象并不是存储在被关联对象本身内存中,而是存储在全局的统一的一个AssociationsManager中,如果设置关联对象为nil,就相当于是移除关联对象。

回过头来看objc_AssociationPolicy policy 参数: 属性以什么形式保存的策略。

1
2
3
4
5
6
7
8
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, // 指定一个弱引用相关联的对象
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, // 指定相关对象的强引用,非原子性
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, // 指定相关的对象被复制,非原子性
OBJC_ASSOCIATION_RETAIN = 01401, // 指定相关对象的强引用,原子性
OBJC_ASSOCIATION_COPY = 01403 // 指定相关的对象被复制,原子性
};
复制代码

我们会发现其中只有RETAIN和COPY而为什么没有weak呢?

NSPointerArray

1、NSPointerArray

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
- (instancetype)init {
if (self = [super init]) {
_delegates = [NSPointerArray weakObjectsPointerArray];
}
return self;
}

// 添加对象指针
- (void)addDelegate:(id)delegate {
[_delegates addPointer:(__bridge void*)delegate];
}
// 和 NSArray在概念上不一样的地方是,NSPointerArray需要添加的是对象的指针地址,尽管他俩都是在操作指针。所以在添加对象时,需要将其转换为指针类型,__bridge转换十分具有 CoreFoundation特色


// 移除对象指针
- (void)removeDelegate:(id)delegate {
NSUInteger index = [self indexOfDelegate:delegate];
if (index != NSNotFound) {
[_delegates removePointerAtIndex:index];
}
[_delegates compact];
}

- (NSUInteger)indexOfDelegate:(id)delegate {
for (NSUInteger i = 0; i < _delegates.count; i += 1) {
if ([_delegates pointerAtIndex:i] == (__bridge void*)delegate) {
return i;
}
}
return NSNotFound;
}