iOS面试合集+答案(五)

2,598 阅读1小时+

image.png

这个栏目将持续更新--请iOS的小伙伴关注!

(答案不唯一,仅供参考,文章最后有福利)

八十一:反射是什么?可以举出几个应用场景么?

系统Foundation框架为我们提供了一些方法反射的API,我们可以通过这些API执行将字符串转为SEL等操作。由于OC语言的动态性,这些操作都是发生在运行时的。

1. // SEL和字符串转换
2. FOUNDATION_EXPORT NSString *NSStringFromSelector(SEL aSelector);
3. FOUNDATION_EXPORT SEL NSSelectorFromString(NSString *aSelectorName);
4. // Class和字符串转换
5. FOUNDATION_EXPORT NSString *NSStringFromClass(Class aClass);
6. FOUNDATION_EXPORT Class __nullable NSClassFromString(NSString *aClassName);
7. // Protocol和字符串转换
8. FOUNDATION_EXPORT NSString *NSStringFromProtocol(Protocol *proto) NS_AVAILABLE(10_5, 2_0);
9. FOUNDATION_EXPORT Protocol * __nullable NSProtocolFromString(NSString *namestr) NS_AVAILABLE(10_5, 2_0);
10.

通过这些方法,我们可以在运行时选择创建那个实例,并动态选择调用哪个方法。这些操作甚至可以由服务器传回来的参数来控制,我们可以将服务器传回来的类名和方法名,实例为我们的对象。

1. // 假设从服务器获取JSON串,通过这个JSON串获取需要创建的类为ViewController,并且调用这个类的getDataList方法。
2. Class class = NSClassFromString(@"ViewController");
3. ViewController *vc = [[class alloc] init];
4. SEL selector = NSSelectorFromString(@"getDataList");
5. [vc performSelector:selector];
6. 

反射机制使用技巧

假设有一天公司产品要实现一个需求:根据后台推送过来的数据,进行动态页面跳转,跳转到页面后根据返回到数据执行对应的操作。

遇到这样奇葩的需求,我们当然可以问产品都有哪些情况执行哪些方法,然后写一大堆if else判断或switch判断。
但是这种方法实现起来太low了,而且不够灵活,假设后续版本需求变了,还要往其他已有页面中跳转,这不就傻眼了吗....
这种情况反射机制就派上用场了,我们可以用反射机制动态的创建类并执行方法。当然也可以通过runtime来实现这个功能,但是我们当前需求反射机制已经足够满足需求了,如果遇到更加复杂的需求可以考虑用runtime来实现。
这时候就需要和后台配合了,我们首先需要和后台商量好返回的数据结构,以及数据格式、类型等,返回后我们按照和后台约定的格式,根据后台返回的信息,直接进行反射和调用即可。

假设和后台约定格式如下:

1. @{
2.      // 类名
3.      @"className" : @"UserListViewController", 
4.      // 数据参数
5.      @"propertys" : @{ @"name": @"liuxiaozhuang", 
6.                        @"age": @3 },
7.      // 调用方法名
8.      @"method" : @"refreshUserInformation"
9.  };
10. 

定义一个UserListViewController类,这个类用于测试,在实际使用中可能会有多个这样的控制器类。

1. #import <UIKit/UIKit.h>
2. // 由于使用的KVC赋值,如果不想把这两个属性暴露出来,把这两个属性写在.m文件也可以
3. @interface UserListViewController : UIViewController
4. @property (nonatomic,strong) NSString *name;/*!< 用户名 */
5. @property (nonatomic,strong) NSNumber *age;/*!< 用户年龄 */
6. /** 使用反射机制反射为SEL后,调用的方法 */
7. - (void)refreshUserInformation;
8. @end
9. 

下面通过反射机制简单实现了控制器跳转的方法,在实际使用中再根据业务需求进行修改即可。因为这篇文章主要是讲反射机制,所以没有使用runtime代码。

简单封装的页面跳转方法,只是做演示,代码都是没问题的,使用时可以根据业务需求进行修改。

1.  - (void)remoteNotificationDictionary:(NSDictionary *)dict {
2.      // 根据字典字段反射出我们想要的类,并初始化控制器
3.      Class class = NSClassFromString(dict[@"className"]);
4.      UIViewController *vc = [[class alloc] init];
5.      // 获取参数列表,使用枚举的方式,对控制器属性进行KVC赋值
6.      NSDictionary *parameter = dict[@"propertys"];
7.      [parameter enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
8.          // 在属性赋值时,做容错处理,防止因为后台数据导致的异常
9.          if ([vc respondsToSelector:NSSelectorFromString(key)]) {
10.             [vc setValue:obj forKey:key];
11.         }
12.     }];
13.     [self.navigationController pushViewController:vc animated:YES];
14.     // 从字典中获取方法名,并调用对应的方法
15.     SEL selector = NSSelectorFromString(dict[@"method"]);
16.     [vc performSelector:selector];
17. }
18. 

八十二:关联对象有什么应用,系统如何管理关联对象?其被释放的时候需要手动将其指针置空么?

我们在 iOS 开发中经常需要使用分类(Category),为已经存在的类添加属性的需求,但是使用 @property 并不能在分类中正确创建实例变量和存取方法。这时候就会用到关联对象。

分类中的 @property
1. @interface DKObject : NSObject
2. 
3. @property (nonatomic, strong) NSString *property;
4. 
5. @end
6. 

在使用上述代码时会做三件事:

  • 生成带下划线的实例变量 _property
  • 生成 getter 方法 - property
  • 生成 setter 方法 - setProperty:
1. @implementation DKObject {
2.     NSString *_property;
3. }
4. 
5. - (NSString *)property {
6.     return _property;
7. }
8.
9. - (void)setProperty:(NSString *)property {
10.    _property = property;
11.}
12.
13.@end
14.

这些代码都是编译器为我们生成的,虽然你看不到它,但是它确实在这里,我们既然可以在类中使用 @property 生成一个属性,那么为什么在分类中不可以呢?

我们来做一个小实验:创建一个 DKObject 的分类 Category,并添加一个属性 categoryProperty

1. @interface DKObject (Category)
2. 
3. @property (nonatomic, strong) NSString *categoryProperty;
4. 
5. @end
6. 

看起来还是很不错的,不过 Build 一下这个 Demo,会发现有这么一个警告:

image.png 在这里的警告告诉我们 categoryProperty 属性的存取方法需要自己手动去实现,或者使用 @dynamic 在运行时实现这些方法。

换句话说,分类中的 @property 并没有为我们生成实例变量以及存取方法,而需要我们手动实现。

使用关联对象

Q:我们为什么要使用关联对象?

A:因为在分类中 @property 并不会自动生成实例变量以及存取方法,所以一般使用关联对象为已经存在的类添加『属性』。

以下是与关联对象有关的 API,并在分类中实现一个伪属性:

1. #import "DKObject+Category.h"
2. #import <objc/runtime.h>
3. 
4. @implementation DKObject (Category)
5. 
6. - (NSString *)categoryProperty {
7.     return objc_getAssociatedObject(self, _cmd);
8. }
9. 
10.- (void)setCategoryProperty:(NSString *)categoryProperty {
11.    objc_setAssociatedObject(self, @selector(categoryProperty), categoryProperty, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
12.}
13.
14.@end
15.

这里的 _cmd 代指当前方法的选择子,也就是 @selector(categoryProperty)

我们使用了两个方法 objc_getAssociatedObject 以及 objc_setAssociatedObject 来模拟『属性』的存取方法,而使用关联对象模拟实例变量。

在这里有必要解释两个问题:

  • 为什么向方法中传入 @selector(categoryProperty)?
  • OBJC_ASSOCIATION_RETAIN_NONATOMIC 是干什么的?

关于第一个问题,我们需要看一下这两个方法的原型:

1. id objc_getAssociatedObject(id object, const void *key);
2. void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
3. 

@selector(categoryProperty) 也就是参数中的key,其实可以使用静态指针 static void *类型的参数来代替,不过在这里,笔者强烈推荐使用 @selector(categoryProperty) 作为 key 传入。因为这种方法省略了声明参数的代码,并且能很好地保证 key 的唯一性

OBJC_ASSOCIATION_RETAIN_NONATOMIC 又是什么呢?如果我们使用 Command 加左键查看它的定义:

1. typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
2.     OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
3.     OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. 
4.                                             *   The association is not made atomically. */
5.     OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied. 
6.                                             *   The association is not made atomically. */
7.     OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
8.                                             *   The association is made atomically. */
9.     OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
10.                                            *   The association is made atomically. */
11.};
12.

从这里的注释我们能看到很多东西,也就是说不同的 objc_AssociationPolicy 对应了不通的属性修饰符:

10.png 而我们在代码中实现的属性 categoryProperty 就相当于使用了 nonatomic 和 strong 修饰符。

在obj dealloc时候会调用object_dispose,检查有无关联对象,有的话_object_remove_assocations删除

八十三:Autoreleasepool所使用的数据结构是什么?AutoreleasePoolPage结构体了解么?

1. 每创建一个池子,会在首部创建一个 哨兵 对象,作为标记
2. 
3. 最外层池子的顶端会有一个next指针。当链表容量满了,就会在链表的顶端,并指向下一张表。
4. 

Autorelease对象什么时候释放?

这个问题拿来做面试题,问过很多人,没有几个能答对的。很多答案都是“当前作用域大括号结束时释放”,显然木有正确理解Autorelease机制。

在没有手加Autorelease Pool的情况下,Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop

例子:

1. __weak id reference = nil;
2. - (void)viewDidLoad {
3.     [super viewDidLoad];    NSString *str = [NSString stringWithFormat:@"sunnyxx"];    // str是一个autorelease对象,设置一个weak的引用来观察它
4.     reference = str;
5. }
6. - (void)viewWillAppear:(BOOL)animated {
7.     [super viewWillAppear:animated];    
8.     NSLog(@"%@", reference); 
9.     // Console: sunnyxx
10.}
11.- (void)viewDidAppear:(BOOL)animated {
12.    [super viewDidAppear:animated];    
13.    NSLog(@"%@", reference); 
14.    // Console: (null)
15.}
16.

当然,我们也可以手动干预Autorelease对象的释放时机:

1. - (void)viewDidLoad
2. {
3.     [super viewDidLoad];
4.     @autoreleasepool {        NSString *str = [NSString stringWithFormat:@"sunnyxx"];
5.     }    NSLog(@"%@", str); 
6. // Console: (null)
7. }
8. 
Autorelease原理

AutoreleasePoolPage

ARC下,我们使用@autoreleasepool{}来使用一个AutoreleasePool,随后编译器将其改写成下面的样子:

1. void *context = objc_autoreleasePoolPush();
2. // {}中的代码objc_autoreleasePoolPop(context);
3. 

而这两个函数都是对AutoreleasePoolPage的简单封装,所以自动释放机制的核心就在于这个类。

AutoreleasePoolPage是一个C++实现的类

image.png

  • AutoreleasePool并没有单独的结构,而是由若干个AutoreleasePoolPage以双向链表的形式组合而成(分别对应结构中的parent指针和child指针)。
  • AutoreleasePool是按线程一一对应的(结构中的thread指针指向当前线程)。
  • AutoreleasePoolPage每个对象会开辟4096字节内存(也就是虚拟内存一页的大小),除了上面的实例变量所占空间,剩下的空间全部用来储存autorelease对象的地址
  • 上面的id *next指针作为游标指向栈顶最新add进来的autorelease对象的下一个位置
  • 一个AutoreleasePoolPage的空间被占满时,会新建一个AutoreleasePoolPage对象,连接链表,后来的autorelease对象在新的page加入。

所以,若当前线程中只有一个AutoreleasePoolPage对象,并记录了很多autorelease对象地址时内存如下图:

image.png

图中的情况,这一页再加入一个autorelease对象就要满了(也就是next指针马上指向栈顶),这时就要执行上面说的操作,建立下一页page对象,与这一页链表连接完成后,新page的next指针被初始化在栈底(begin的位置),然后继续向栈顶添加新对象。

所以,向一个对象发送- autorelease消息,就是将这个对象加入到当前AutoreleasePoolPage的栈顶next指针指向的位置

释放时刻

每当进行一次objc_autoreleasePoolPush调用时,runtime向当前的AutoreleasePoolPage中add进一个哨兵对象,值为0(也就是个nil),那么这一个page就变成了下面的样子:

image.png

objc_autoreleasePoolPush的返回值正是这个哨兵对象的地址,被objc_autoreleasePoolPop(哨兵对象)作为入参,于是:

1.根据传入的哨兵对象地址找到哨兵对象所处的page

2.在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次- release消息,并向回移动next指针到正确位置

3.补充2:从最新加入的对象一直向前清理,可以向前跨越若干个page,直到哨兵所在的page(在一个page中,是从高地址向低地址清理)

刚才的objc_autoreleasePoolPop执行后,最终变成了下面的样子:

image.png

嵌套的AutoreleasePool

知道了上面的原理,嵌套的AutoreleasePool就非常简单了,pop的时候总会释放到上次push的位置为止,多层的pool就是多个哨兵对象而已,就像剥洋葱一样,每次一层,互不影响。

八十四:KVC的赋值和取值过程是怎样的?KVO原理是什么?

1、KVC赋值

1. // 1.1 创建人
2. PTLPerson *p = [[PTLPerson alloc] init];
3. self.person = p;
4. // 1.2 创建狗
5. PTLDog *dog = [[PTLDog alloc] init];
6. // 1.3 将狗赋值给人
7. [p setValue:dog forKeyPath:@"dog"];
8. // 1.4 通过KVC给dog的weight属性赋值 
9. 赋值时会自动找到人拥有的dogweight属性
10.[p setValue:@10.0 forKeyPath:@"dog.weight"];
11.NSLog(@"books = %@", [p valueForKeyPath:@"dog.weight"]);
12.[dog print];

2、 KVC取值

1. NSMutableArray *tempM = [NSMutableArray array];
2. // 2.1 kvc取出出数组books中price的值
3. for (PTLBook *book in [p valueForKeyPath:@"books"]) {
4.     [tempM addObject:[book valueForKeyPath:@"price"]];
5. }
6. NSLog(@"%@", tempM);
7. // 2.2 kvc取出数组中price的最大值
8. NSLog(@"Max = %@", [[p valueForKeyPath:@"books"] valueForKeyPath:@"@max.price"]);
NSLog(@"Max = %@", [[p valueForKeyPath:@"books"] valueForKeyPath:@"@max.price"]);

3、 KVO原理

  • KVO 是 Objective-C 对观察者设计模式的一种实现,另外一种是:通知机制(notification)
    KVO提供一种机制,指定一个被观察对象(例如A类),当对象某个属性(例如A中的字符串name)发生更改时,对象会获得通知,并作出相应处理
    在MVC设计架构下的项目,KVO机制很适合实现mode模型和controller之间的通讯。
    例如:代码中,在模型类A创建属性数据,在控制器中创建观察者,一旦属性数据发生改变就收到观察者收到通知,通过KVO再在控制器使用回调方法处理实现视图B的更新;(本文中的应用就是这样的例子.)
  • KVO在Apple中的API文档如下:
    Automatic key-value observing is implemented using a technique called isa-swizzling… 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 …
    KVO 的实现依赖于 Objective-C 强大的 Runtime【 ,从以上Apple 的文档可以看出苹果对于KVO机制的实现是一笔带过,而具体的细节没有过多的描述,但是我们可以通过Runtime的所提供的方法去探索关于KVO机制的底层实现原理.

  • 当观察某对象 A 时,KVO 机制动态创建一个对象A当前类的子类,并为这个新的子类重写了被观察属性 keyPath 的 setter 方法。setter 方法随后负责通知观察对象属性的改变状况。

  • Apple 使用了 isa 混写(isa-swizzling)来实现 KVO 。当观察对象A时,KVO机制动态创建一个新的名为:NSKVONotifying_A 的新类,该类继承自对象A的本类,且 KVO 为 NSKVONotifying_A 重写观察属性的 setter 方法,setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象属性值的更改情况。

  • NSKVONotifying_A类剖析:在这个过程,被观察对象的 isa 指针从指向原来的A类,被KVO机制修改为指向系统新创建的子类 NSKVONotifying_A类,来实现当前类属性值改变的监听;
    所以当我们从应用层面上看来,完全没有意识到有新的类出现,这是系统“隐瞒”了对KVO的底层实现过程,让我们误以为还是原来的类。但是此时如果我们创建一个新的名为“NSKVONotifying_A”的类(),就会发现系统运行到注册KVO的那段代码时程序就崩溃,因为系统在注册监听的时候动态创建了名为NSKVONotifying_A的中间类,并指向这个中间类了。
    因而在该对象上对 setter 的调用就会调用已重写的 setter,从而激活键值通知机制。

  • 子类setter方法剖析:KVO的键值观察通知依赖于 NSObject 的两个方法:willChangeValueForKey:和 didChangevlueForKey:,在存取数值的前后分别调用2个方法:
    被观察属性发生改变之前,willChangeValueForKey:被调用,通知系统该 keyPath 的属性值即将变更;当改变发生后, didChangeValueForKey: 被调用,通知系统该 keyPath 的属性值已经变更;
    之后observeValueForKey:ofObject:change:context: 也会被调用。且重写观察属性的setter 方法这种继承方式的注入是在运行时而不是编译时实现的。
    KVO为子类的观察者属性重写调用存取方法的工作原理在代码中相当于:
1. -(void)setName:(NSString *)newName 
2. { 
3. [self willChangeValueForKey:@"name"]; //KVO在调用存取方法之前总调用 
4. [super setValue:newName forKey:@"name"]; //调用父类的存取方法 
5. [self didChangeValueForKey:@"name"]; //KVO在调用存取方法之后总调用 
6. } 

八十五:iOS中UITableViewCell的重用机制原理?

  • 重用实现分析

查看UITableView头文件,会找到NSMutableArray* visiableCells,和NSMutableDictnery* reusableTableCells两个结构。visiableCells内保存当前显示的cells,reusableTableCells保存可重 用的cells

TableView显示之初,reusableTableCells为空,那么tableView dequeueReusableCellWithIdentifier:CellIdentifier返回nil。开始的cell都是通过 [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]来创建,而且cellForRowAtIndexPath只是调用最大显示cell数的 次数

比如:有100条数据,iPhone一屏最多显示10个cell。程序最开始显示TableView的情况是:

1、 用[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]创建10次cell,并给cell指定同样的重用标识(当然,可以为不同显示类型的 cell指定不同的标识)。并且10个cell全部都加入到visiableCells数组,reusableTableCells为空。

2、 向下拖动tableView,当cell1完全移出屏幕,并且cell11(它也是alloc出来的,原因同上)完全显示出来的时候。cell11加入到 visiableCells,cell1移出visiableCells,cell1加入到reusableTableCells。

3、 接着向下拖动tableView,因为reusableTableCells中已经有值,所以,当需要显示新的 cell,cellForRowAtIndexPath再次被调用的时候,tableView dequeueReusableCellWithIdentifier:CellIdentifier,返回cell1。cell1加入到 visiableCells,cell1移出reusableTableCells;cell2移出visiableCells,cell2加入到 reusableTableCells。之后再需要显示的Cell就可以正常重用了。

所以整个过程并不难理解,但需要注意正是因为这样的原因:配置Cell的时候一定要注意,对取出的重用的cell做重新赋值,不要遗留老数据

  • 一些情况

使用过程中,我注意到,并不是只有拖动超出屏幕的时候才会更新reusableTableCells表,还有:

1、 reloadData,这种情况比较特殊。一般是部分数据发生变化,需要重新刷新cell显示的内容时调用。在 cellForRowAtIndexPath调用中,所有cell都是重用的。我估计reloadData调用后,把visiableCells中所有 cell移入reusableTableCells,visiableCells清空。cellForRowAtIndexPath调用后,再把 reuse的cell从reusableTableCells取出来,放入到visiableCells。

2、 reloadRowsAtIndex,刷新指定的IndexPath。如果调用时reusableTableCells为空,那么 cellForRowAtIndexPath调用后,是新创建cell,新的cell加入到visiableCells。老的cell移出 visiableCells,加入到reusableTableCells。于是,之后的刷新就有cell做reuse了。

八十六:RunLoop剖析

一、RunLoop概念

RunLoop是通过内部维护的事件循环(Event Loop)来对事件/消息进行管理的一个对象。

1、没有消息处理时,休眠已避免资源占用,由用户态切换到内核态(CPU-内核态和用户态)
2、有消息需要处理时,立刻被唤醒,由内核态切换到用户态

为什么main函数不会退出?

1. int main(int argc, char * argv[]) {
2.     @autoreleasepool {
3.         return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
4.     }
5. }

UIApplicationMain内部默认开启了主线程的RunLoop,并执行了一段无限循环的代码(不是简单的for循环或while循环)

1. //无限循环代码模式(伪代码)
2. int main(int argc, char * argv[]) {        
3.     BOOL running = YES;
4.     do {
5.         // 执行各种任务,处理各种事件
6.         // ......
7.     } while (running);
8. 
9.     return 0;
10.}
11.

UIApplicationMain函数一直没有返回,而是不断地接收处理消息以及等待休眠,所以运行程序之后会保持持续运行状态。

二、RunLoop的数据结构

NSRunLoop(Foundation)CFRunLoop(CoreFoundation)的封装,提供了面向对象的API
RunLoop 相关的主要涉及五个类:

CFRunLoop:RunLoop对象
CFRunLoopMode:运行模式
CFRunLoopSource:输入源/事件源
CFRunLoopTimer:定时源
CFRunLoopObserver:观察者

1、CFRunLoop

pthread(线程对象,说明RunLoop和线程是一一对应的)、currentMode(当前所处的运行模式)、modes(多个运行模式的集合)、commonModes(模式名称字符串集合)、commonModelItems(Observer,Timer,Source集合)构成

2、CFRunLoopMode

由name、source0、source1、observers、timers构成

3、CFRunLoopSource

分为source0和source1两种

  • source0:
    即非基于port的,也就是用户触发的事件。需要手动唤醒线程,将当前线程从内核态切换到用户态
  • source1:
    基于port的,包含一个 mach_port 和一个回调,可监听系统端口和通过内核和其他线程发送的消息,能主动唤醒RunLoop,接收分发系统事件。
    具备唤醒线程的能力

4、CFRunLoopTimer

基于时间的触发器,基本上说的就是NSTimer。在预设的时间点唤醒RunLoop执行回调。因为它是基于RunLoop的,因此它不是实时的(就是NSTimer 是不准确的。 因为RunLoop只负责分发源的消息。如果线程当前正在处理繁重的任务,就有可能导致Timer本次延时,或者少执行一次)。

5、CFRunLoopObserver

监听以下时间点:CFRunLoopActivity

  • kCFRunLoopEntry
    RunLoop准备启动
  • kCFRunLoopBeforeTimers
    RunLoop将要处理一些Timer相关事件
  • kCFRunLoopBeforeSources
    RunLoop将要处理一些Source事件
  • kCFRunLoopBeforeWaiting
    RunLoop将要进行休眠状态,即将由用户态切换到内核态
  • kCFRunLoopAfterWaiting
    RunLoop被唤醒,即从内核态切换到用户态后
  • kCFRunLoopExit
    RunLoop退出
  • kCFRunLoopAllActivities
    监听所有状态

6、各数据结构之间的联系

线程和RunLoop一一对应, RunLoop和Mode是一对多的,Mode和source、timer、observer也是一对多的

image.png

三、RunLoop的Mode

关于Mode首先要知道一个RunLoop 对象中可能包含多个Mode,且每次调用 RunLoop 的主函数时,只能指定其中一个 Mode(CurrentMode)。切换 Mode,需要重新指定一个 Mode 。主要是为了分隔开不同的 Source、Timer、Observer,让它们之间互不影响。

image.png 当RunLoop运行在Mode1上时,是无法接受处理Mode2或Mode3上的Source、Timer、Observer事件的

总共是有五种CFRunLoopMode:

  • kCFRunLoopDefaultMode:默认模式,主线程是在这个运行模式下运行
  • UITrackingRunLoopMode:跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响)
  • UIInitializationRunLoopMode:在刚启动App时第进入的第一个 Mode,启动完成后就不再使用
  • GSEventReceiveRunLoopMode:接受系统内部事件,通常用不到
  • kCFRunLoopCommonModes:伪模式,不是一种真正的运行模式,是同步Source/Timer/Observer到多个Mode中的一种解决方案

四、RunLoop的实现机制

image.png

image

这张图在网上流传比较广。
对于RunLoop而言最核心的事情就是保证线程在没有消息的时候休眠,在有消息时唤醒,以提高程序性能。RunLoop这个机制是依靠系统内核来完成的(苹果操作系统核心组件Darwin中的Mach)。

image.png image

RunLoop通过mach_msg()函数接收、发送消息。它的本质是调用函数mach_msg_trap(),相当于是一个系统调用,会触发内核状态切换。在用户态调用 mach_msg_trap()时会切换到内核态;内核态中内核实现的mach_msg()函数会完成实际的工作。
即基于port的source1,监听端口,端口有消息就会触发回调;而source0,要手动标记为待处理和手动唤醒RunLoop

Mach消息发送机制
大致逻辑为:
1、通知观察者 RunLoop 即将启动。
2、通知观察者即将要处理Timer事件。
3、通知观察者即将要处理source0事件。
4、处理source0事件。
5、如果基于端口的源(Source1)准备好并处于等待状态,进入步骤9。
6、通知观察者线程即将进入休眠状态。
7、将线程置于休眠状态,由用户态切换到内核态,直到下面的任一事件发生才唤醒线程。

  • 一个基于 port 的Source1 的事件(图里应该是source0)。
  • 一个 Timer 到时间了。
  • RunLoop 自身的超时时间到了。
  • 被其他调用者手动唤醒。

8、通知观察者线程将被唤醒。
9、处理唤醒时收到的事件。

  • 如果用户定义的定时器启动,处理定时器事件并重启RunLoop。进入步骤2。
  • 如果输入源启动,传递相应的消息。
  • 如果RunLoop被显示唤醒而且时间还没超时,重启RunLoop。进入步骤2

10、通知观察者RunLoop结束。

五、RunLoop与NSTimer

一个比较常见的问题:滑动tableView时,定时器还会生效吗?
默认情况下RunLoop运行在kCFRunLoopDefaultMode下,而当滑动tableView时,RunLoop切换到UITrackingRunLoopMode,而Timer是在kCFRunLoopDefaultMode下的,就无法接受处理Timer的事件。
怎么去解决这个问题呢?把Timer添加到UITrackingRunLoopMode上并不能解决问题,因为这样在默认情况下就无法接受定时器事件了。
所以我们需要把Timer同时添加到UITrackingRunLoopModekCFRunLoopDefaultMode上。
那么如何把timer同时添加到多个mode上呢?就要用到NSRunLoopCommonModes

1. [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

Timer就被添加到多个mode上,这样即使RunLoop由kCFRunLoopDefaultMode切换到UITrackingRunLoopMode下,也不会影响接收Timer事件

六、RunLoop和线程

  • 线程和RunLoop是一一对应的,其映射关系是保存在一个全局的 Dictionary 里
  • 自己创建的线程默认是没有开启RunLoop的

1、怎么创建一个常驻线程?

1、为当前线程开启一个RunLoop(第一次调用 [NSRunLoop currentRunLoop]方法时实际是会先去创建一个RunLoop)
1、向当前RunLoop中添加一个Port/Source等维持RunLoop的事件循环(如果RunLoop的mode中一个item都没有,RunLoop会退出)
2、启动该RunLoop

1.    @autoreleasepool {
2.         NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
3.         [[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
4.         [runLoop run];
5.     }

2、输出下边代码的执行顺序

1.  NSLog(@"1");
2. dispatch_async(dispatch_get_global_queue(0, 0), ^{
3.     NSLog(@"2");
4.     [self performSelector:@selector(test) withObject:nil afterDelay:10];
5.     NSLog(@"3");
6. });
7. NSLog(@"4");
8. - (void)test
9. {
10.    NSLog(@"5");
11.}

答案是1423,test方法并不会执行。
原因是如果是带afterDelay的延时函数,会在内部创建一个 NSTimer,然后添加到当前线程的RunLoop中。也就是如果当前线程没有开启RunLoop,该方法会失效。
那么我们改成:

1. dispatch_async(dispatch_get_global_queue(0, 0), ^{
2.         NSLog(@"2");
3.         [[NSRunLoop currentRunLoop] run];
4.         [self performSelector:@selector(test) withObject:nil afterDelay:10];
5.         NSLog(@"3");
6.     });

然而test方法依然不执行。
原因是如果RunLoop的mode中一个item都没有,RunLoop会退出。即在调用RunLoop的run方法后,由于其mode中没有添加任何item去维持RunLoop的时间循环,RunLoop随即还是会退出。
所以我们自己启动RunLoop,一定要在添加item后

1. dispatch_async(dispatch_get_global_queue(0, 0), ^{
2.         NSLog(@"2");
3.         [self performSelector:@selector(test) withObject:nil afterDelay:10];
4.         [[NSRunLoop currentRunLoop] run];
5.         NSLog(@"3");
6.     });

3、怎样保证子线程数据回来更新UI的时候不打断用户的滑动操作?

当我们在子请求数据的同时滑动浏览当前页面,如果数据请求成功要切回主线程更新UI,那么就会影响当前正在滑动的体验。
我们就可以将更新UI事件放在主线程的NSDefaultRunLoopMode上执行即可,这样就会等用户不再滑动页面,主线程RunLoop由UITrackingRunLoopMode切换到NSDefaultRunLoopMode时再去更新UI

1. [self performSelectorOnMainThread:@selector(reloadData) withObject:nil waitUntilDone:NO modes:@[NSDefaultRunLoopMode]];

八十七:内存管理、自动释放池与循环引用

一、内存布局

image.png

  • 栈(stack):方法调用
  • 堆(heap):通过alloc等分配的对象
  • 未初始化数据(bss):未初始化的全局变量等
  • 已初始化数据(data):已初始化的全局变量等
  • 代码段(text):程序代码

二、内存管理方案

  • taggedPointer :存储小对象如NSNumber。深入理解Tagged Pointer
  • NONPOINTER_ISA(非指针型的isa):在64位架构下,isa指针是占64比特位的,实际上只有30多位就已经够用了,为了提高利用率,剩余的比特位存储了内存管理的相关数据内容。
  • 散列表:复杂的数据结构,包括了引用计数表和弱引用表
    通过SideTables()结构来实现的,SideTables()结构下,有很多SideTable的数据结构。
    而sideTable当中包含了自旋锁,引用计数表,弱引用表。
    SideTables()实际上是一个哈希表,通过对象的地址来计算该对象的引用计数在哪个sideTable中。

自旋锁:

  • 自旋锁是“忙等”的锁。
  • 适用于轻量访问。

引用计数表和弱引用表实际是一个哈希表,来提高查找效率。

三、MRC(手动引用计数)和ARC(自动引用计数)

1、MRC:alloc,retain,release,retainCount,autorelease,dealloc
2、ARC:
  • ARC是LLVM和Runtime协作的结果
  • ARC禁止手动调用retain,release,retainCount,autorelease关键字
  • ARC新增weak,strong关键字
3、引用计数管理:
  • alloc: 经过一系列函数调用,最终调用了calloc函数,这里并没有设置引用计数为1
  • retain: 经过两次哈希查找,找到其对应引用计数值,然后将引用计数加1(实际是加偏移量)
  • release:和retain相反,经过两次哈希查找,找到其对应引用计数值,然后将引用计数减1
  • dealloc:

image.png

4、弱引用管理:
  • 添加weak变量:通过哈希算法位置查找添加。如果查找对应位置中已经有了当前对象所对应的弱引用数组,就把新的弱引用变量添加到数组当中;如果没有,就创建一个弱引用数组,并将该弱引用变量添加到该数组中。
  • 当一个被weak修饰的对象被释放后,weak对象怎么处理的?
    清除weak变量,同时设置指向为nil。当对象被dealloc释放后,在dealloc的内部实现中,会调用弱引用清除的相关函数,会根据当前对象指针查找弱引用表,找到当前对象所对应的弱引用数组,将数组中的所有弱引用指针都置为nil。
5、自动释放池:

在当次runloop将要结束的时候调用objc_autoreleasePoolPop,并push进来一个新的AutoreleasePool

AutoreleasePoolPage是以栈为结点通过双向链表的形式组合而成,是和线程一一对应的。
内部属性有parent,child对应前后两个结点,thread对应线程 ,next指针指向栈中下一个可填充的位置。

  • AutoreleasePool实现原理?

编译器会将 @autoreleasepool {} 改写为:

1. void * ctx = objc_autoreleasePoolPush;
2.     {}
3. objc_autoreleasePoolPop(ctx);
  • objc_autoreleasePoolPush:
    把当前next位置置为nil,即哨兵对象,然后next指针指向下一个可入栈位置,
    AutoreleasePool的多层嵌套,即每次objc_autoreleasePoolPush,实际上是不断地向栈中插入哨兵对象。
  • objc_autoreleasePoolPop:
    根据传入的哨兵对象找到对应位置。
    给上次push操作之后添加的对象依次发送release消息。
    回退next指针到正确的位置。

四、循环引用

循环引用的实质:多个对象相互之间有强引用,不能释放让系统回收。
如何解决循环引用?

1、避免产生循环引用,通常是将 strong 引用改为 weak 引用。
比如在修饰属性时用weak
在block内调用对象方法时,使用其弱引用,这里可以使用两个宏

1. #define WS(weakSelf)            __weak __typeof(&*self)weakSelf = self; // 弱引用
2. #define ST(strongSelf)          __strong __typeof(&*self)strongSelf = weakSelf; //使用这个要先声明weakSelf

还可以使用__block来修饰变量
在MRC下,__block不会增加其引用计数,避免了循环引用
在ARC下,__block修饰对象会被强引用,无法避免循环引用,需要手动解除。

2、在合适时机去手动断开循环引用。
通常我们使用第一种。

循环引用场景:
  • 自循环引用
    对象强持有的属性同时持有该对象
  • 相互循环引用

image.png

  • 多循环引用

image.png

1、代理(delegate)循环引用属于相互循环引用

delegate 是iOS中开发中比较常遇到的循环引用,一般在声明delegate的时候都要使用弱引用 weak,或者assign,当然怎么选择使用assign还是weak,MRC的话只能用assign,在ARC的情况下最好使用weak,因为weak修饰的变量在释放后自动指向nil,防止野指针存在

2、NSTimer循环引用属于相互循环使用

在控制器内,创建NSTimer作为其属性,由于定时器创建后也会强引用该控制器对象,那么该对象和定时器就相互循环引用了。
如何解决呢?
这里我们可以使用手动断开循环引用:
如果是不重复定时器,在回调方法里将定时器invalidate并置为nil即可。
如果是重复定时器,在合适的位置将其invalidate并置为nil即可

3、block循环引用

一个简单的例子:

1. @property (copy, nonatomic) dispatch_block_t myBlock;
2. @property (copy, nonatomic) NSString *blockString;
3. 
4. - (void)testBlock {
5.     self.myBlock = ^() {
6.         NSLog(@"%@",self.blockString);
7.     };
8. }

由于block会对block中的对象进行持有操作,就相当于持有了其中的对象,而如果此时block中的对象又持有了该block,则会造成循环引用。
解决方案就是使用__weak修饰self即可

1. __weak typeof(self) weakSelf = self;
2. 
3. self.myBlock = ^() {
4.         NSLog(@"%@",weakSelf.blockString);
5.  };
  • 并不是所有block都会造成循环引用。 只有被强引用了的block才会产生循环引用
    而比如dispatch_async(dispatch_get_main_queue(), ^{}),[UIView animateWithDuration:1 animations:^{}]这些系统方法等
    或者block并不是其属性而是临时变量,即栈block
1. [self testWithBlock:^{
2.     NSLog(@"%@",self);
3. }];
4. 
5. - (void)testWithBlock:(dispatch_block_t)block {
6.     block();
7. }

还有一种场景,在block执行开始时self对象还未被释放,而执行过程中,self被释放了,由于是用weak修饰的,那么weakSelf也被释放了,此时在block里访问weakSelf时,就可能会发生错误(向nil对象发消息并不会崩溃,但也没任何效果)。
对于这种场景,应该在block中对 对象使用__strong修饰,使得在block期间对 对象持有,block执行结束后,解除其持有。

__weak typeof(self) weakSelf = self;

self.myBlock = ^() {
        __strong __typeof(self) strongSelf = weakSelf;
        [strongSelf test];
 };

八十八:剖析Block

一、什么是Block?

  • Block是将函数及其执行上下文封装起来的对象。

比如:

1. NSInteger num = 3;
2.     NSInteger(^block)(NSInteger) = ^NSInteger(NSInteger n){
3.         return n*num;
4.     };
5. 
6.     block(2);
7. 

通过clang -rewrite-objc WYTest.m命令编译该.m文件,发现该block被编译成这个形式:

1.     NSInteger num = 3;

2.     NSInteger(*block)(NSInteger) = ((NSInteger (*)(NSInteger))&__WYTest__blockTest_block_impl_0((void *)__WYTest__blockTest_block_func_0, &__WYTest__blockTest_block_desc_0_DATA, num));

3.     ((NSInteger (*)(__block_impl *, NSInteger))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 2);

其中WYTest是文件名,blockTest是方法名,这些可以忽略。
其中__WYTest__blockTest_block_impl_0结构体为

1. struct __WYTest__blockTest_block_impl_0 {
2.   struct __block_impl impl;
3.   struct __WYTest__blockTest_block_desc_0* Desc;
4.   NSInteger num;
5.   __WYTest__blockTest_block_impl_0(void *fp, struct __WYTest__blockTest_block_desc_0 *desc, NSInteger _num, int flags=0) : num(_num) {
6.     impl.isa = &_NSConcreteStackBlock;
7.     impl.Flags = flags;
8.     impl.FuncPtr = fp;
9.     Desc = desc;
10.  }
11.};
12.

__block_impl结构体为

1. struct __block_impl {
2.   void *isa;//isa指针,所以说Block是对象
3.   int Flags;
4.   int Reserved;
5.   void *FuncPtr;//函数指针
6. };
7. 

block内部有isa指针,所以说其本质也是OC对象
block内部则为:

1. static NSInteger __WYTest__blockTest_block_func_0(struct __WYTest__blockTest_block_impl_0 *__cself, NSInteger n) {
2. NSInteger num = __cself->num; // bound by copy
3. 
4.         return n*num;
5.     }
6. 

所以说 Block是将函数及其执行上下文封装起来的对象
既然block内部封装了函数,那么它同样也有参数和返回值。

二、Block变量截获

1、局部变量截获 是值截获。 比如:

1.     NSInteger num = 3;
2. 
3.     NSInteger(^block)(NSInteger) = ^NSInteger(NSInteger n){
4. 
5.         return n*num;
6.     };
7. 
8.     num = 1;
9. 
10.    NSLog(@"%zd",block(2));
11.

输出为2,意味着num = 1这里的修改num值是有效的,即是指针截获。
同样,在block里去修改变量m,也是有效的。

3、全局变量,静态全局变量截获:不截获,直接取值。

我们同样用clang编译看下结果。

1. static NSInteger num3 = 300;
2. 
3. NSInteger num4 = 3000;
4. 
5. - (void)blockTest
6. {
7.     NSInteger num = 30;
8. 
9.     static NSInteger num2 = 3;
10.
11.    __block NSInteger num5 = 30000;
12.
13.    void(^block)(void) = ^{
14.
15.        NSLog(@"%zd",num);//局部变量
16.
17.        NSLog(@"%zd",num2);//静态变量
18.
19.        NSLog(@"%zd",num3);//全局变量
20.
21.        NSLog(@"%zd",num4);//全局静态变量
22.
23.        NSLog(@"%zd",num5);//__block修饰变量
24.    };
25.
26.    block();
27.}
28.

编译后

1. struct __WYTest__blockTest_block_impl_0 {
2.   struct __block_impl impl;
3.   struct __WYTest__blockTest_block_desc_0* Desc;
4.   NSInteger num;//局部变量
5.   NSInteger *num2;//静态变量
6.   __Block_byref_num5_0 *num5; // by ref//__block修饰变量
7.   __WYTest__blockTest_block_impl_0(void *fp, struct __WYTest__blockTest_block_desc_0 *desc, NSInteger _num, NSInteger *_num2, __Block_byref_num5_0 *_num5, int flags=0) : num(_num), num2(_num2), num5(_num5->__forwarding) {
8.     impl.isa = &_NSConcreteStackBlock;
9.     impl.Flags = flags;
10.    impl.FuncPtr = fp;
11.    Desc = desc;
12.  }
13.};
14.

( impl.isa = &_NSConcreteStackBlock;这里注意到这一句,即说明该block是栈block)
可以看到局部变量被编译成值形式,而静态变量被编成指针形式,全局变量并未截获。而__block修饰的变量也是以指针形式截获的,并且生成了一个新的结构体对象

1. struct __Block_byref_num5_0 {
2.   void *__isa;
3. __Block_byref_num5_0 *__forwarding;
4.  int __flags;
5.  int __size;
6.  NSInteger num5;
7. };
8.

该对象有个属性:num5,即我们用__block修饰的变量。
这里__forwarding是指向自身的(栈block)。
一般情况下,如果我们要对block截获的局部变量进行赋值操作需添加__block
修饰符,而对全局变量,静态变量是不需要添加__block修饰符的。
另外,block里访问self或成员变量都会去截获self。

三、Block的几种形式

  • 分为全局Block(_NSConcreteGlobalBlock)、栈Block(_NSConcreteStackBlock)、堆Block(_NSConcreteMallocBlock)三种形式

    其中栈Block存储在栈(stack)区,堆Block存储在堆(heap)区,全局Block存储在已初始化数据(.data)区

1、不使用外部变量的block是全局block

比如:

1.     NSLog(@"%@",[^{
2.         NSLog(@"globalBlock");
3.     } class]);
4. 

输出:

1. __NSGlobalBlock__
2. 

2、使用外部变量并且未进行copy操作的block是栈block

比如:

1.   NSInteger num = 10;
2.     NSLog(@"%@",[^{
3.         NSLog(@"stackBlock:%zd",num);
4.     } class]);
5. 

输出:

1. __NSStackBlock__
2. 

日常开发常用于这种情况:

1. [self testWithBlock:^{
2.     NSLog(@"%@",self);
3. }];
4. 
5. - (void)testWithBlock:(dispatch_block_t)block {
6.     block();
7. 
8.     NSLog(@"%@",[block class]);
9. }
10.

3、对栈block进行copy操作,就是堆block,而对全局block进行copy,仍是全局block

  • 比如堆1中的全局进行copy操作,即赋值:
1. void (^globalBlock)(void) = ^{
2.         NSLog(@"globalBlock");
3.     };
4. 
5.  NSLog(@"%@",[globalBlock class]);
6. 

输出:

1. __NSGlobalBlock__
2. 

仍是全局block

  • 而对2中的栈block进行赋值操作:
1. NSInteger num = 10;
2. 
3. void (^mallocBlock)(void) = ^{
4. 
5.         NSLog(@"stackBlock:%zd",num);
6.     };
7. 
8. NSLog(@"%@",[mallocBlock class]);
9. 

输出:

1. __NSMallocBlock__
2. 

对栈blockcopy之后,并不代表着栈block就消失了,左边的mallock是堆block,右边被copy的仍是栈block
比如:

1. [self testWithBlock:^{
2. 
3.     NSLog(@"%@",self);
4. }];
5. 
6. - (void)testWithBlock:(dispatch_block_t)block
7. {
8.     block();
9. 
10.    dispatch_block_t tempBlock = block;
11.
12.    NSLog(@"%@,%@",[block class],[tempBlock class]);
13.}
14.

输出:

1. __NSStackBlock__,__NSMallocBlock__
2. 
  • 即如果对栈Block进行copy,将会copy到堆区,对堆Block进行copy,将会增加引用计数,对全局Block进行copy,因为是已经初始化的,所以什么也不做。

另外,__block变量在copy时,由于__forwarding的存在,栈上的__forwarding指针会指向堆上的__forwarding变量,而堆上的__forwarding指针指向其自身,所以,如果对__block的修改,实际上是在修改堆上的__block变量。

即__forwarding指针存在的意义就是,无论在任何内存位置, 都可以顺利地访问同一个__block变量。

  • 另外由于block捕获的__block修饰的变量会去持有变量,那么如果用__block修饰self,且self持有block,并且block内部使用到__block修饰的self时,就会造成多循环引用,即self持有block,block 持有__block变量,而__block变量持有self,造成内存泄漏。
    比如:
1.   __block typeof(self) weakSelf = self;
2. 
3.     _testBlock = ^{
4. 
5.         NSLog(@"%@",weakSelf);
6.     };
7. 
8.     _testBlock();
9. 

如果要解决这种循环引用,可以主动断开__block变量对self的持有,即在block内部使用完weakself后,将其置为nil,但这种方式有个问题,如果block一直不被调用,那么循环引用将一直存在。
所以,我们最好还是用__weak来修饰self

八十九:SDWebImage原理

SDWebImage

一个为UIImageView提供一个分类来支持远程服务器图片加载的库。

功能简介:

1.       1、一个添加了web图片加载和缓存管理的UIImageView分类
2.       2、一个异步图片下载器
3.       3、一个异步的内存加磁盘综合存储图片并且自动处理过期图片
4.       4、支持动态gif图
5.       5、支持webP格式的图片
6.       6、后台图片解压处理
7.       7、确保同样的图片url不会下载多次
8.       8、确保伪造的图片url不会重复尝试下载
9.       9、确保主线程不会阻塞

工作流程

1.   1、入口 setImageWithURL:placeholderImage:options: 会先把 placeholderImage 显示,然后 SDWebImageManager 根据 URL 开始处理图片。
2.   
3.   2、进入 SDWebImageManager-downloadWithURL:delegate:options:userInfo:,交给 SDImageCache 从缓存查找图片是否已经下载 queryDiskCacheForKey:delegate:userInfo:.
4.   
5.   3、先从内存图片缓存查找是否有图片,如果内存中已经有图片缓存,SDImageCacheDelegate 回调 imageCache:didFindImage:forKey:userInfo: 到 SDWebImageManager。
6.   
7.   4、SDWebImageManagerDelegate 回调 webImageManager:didFinishWithImage: 到 UIImageView+WebCache 等前端展示图片。
8.   
9.   5、如果内存缓存中没有,生成 NSInvocationOperation 添加到队列开始从硬盘查找图片是否已经缓存。
10.  
11.  6、根据 URLKey 在硬盘缓存目录下尝试读取图片文件。这一步是在 NSOperation 进行的操作,所以回主线程进行结果回调 notifyDelegate:。
12.  
13.  7、如果上一操作从硬盘读取到了图片,将图片添加到内存缓存中(如果空闲内存过小,会先清空内存缓存)。SDImageCacheDelegate 回调 imageCache:didFindImage:forKey:userInfo:。进而回调展示图片。
14.  
15.  8、如果从硬盘缓存目录读取不到图片,说明所有缓存都不存在该图片,需要下载图片,回调 imageCache:didNotFindImageForKey:userInfo:。
16.  
17.  9、共享或重新生成一个下载器 SDWebImageDownloader 开始下载图片。
18.  
19.  10、图片下载由 NSURLConnection 来做,实现相关 delegate 来判断图片下载中、下载完成和下载失败。
20.  
21.  11、connection:didReceiveData: 中利用 ImageIO 做了按图片下载进度加载效果。connectionDidFinishLoading: 数据下载完成后交给 SDWebImageDecoder 做图片解码处理。
22.  
23.  12、图片解码处理在一个 NSOperationQueue 完成,不会拖慢主线程 UI。如果有需要对下载的图片进行二次处理,最好也在这里完成,效率会好很多。
24.  
25.  13、在主线程 notifyDelegateOnMainThreadWithInfo: 宣告解码完成,imageDecoder:didFinishDecodingImage:userInfo: 回调给 SDWebImageDownloader。imageDownloader:didFinishWithImage: 回调给 SDWebImageManager 告知图片下载完成。
26.  
27.  14、通知所有的 downloadDelegates 下载完成,回调给需要的地方展示图片。将图片保存到 SDImageCache 中,内存缓存和硬盘缓存同时保存。写文件到硬盘也在以单独 NSInvocationOperation 完成,避免拖慢主线程。
28.  
29.  15、SDImageCache 在初始化的时候会注册一些消息通知,在内存警告或退到后台的时候清理内存图片缓存,应用结束的时候清理过期图片。
30.  
31.  16、SDWI 也提供了 UIButton+WebCache 和 MKAnnotationView+WebCache,方便使用。
32.  
33.  17、SDWebImagePrefetcher 可以预先下载图片,方便后续使用。

源码分析

主要用到的对象

一、图片下载

1、 SDWebImageDownloader

  • 1.单例,图片下载器,负责图片异步下载,并对图片加载做了优化处理
  • 2.图片的下载操作放在一个NSOperationQueue并发操作队列中,队列默认最大并发数是6
  • 3.每个图片对应一些回调(下载进度,完成回调等),回调信息会存在downloader的URLCallbacks(一个字典,key是url地址,value是图片下载回调数组)中,URLCallbacks可能被多个线程访问,所以downloader把下载任务放在一个barrierQueue中,并设置屏障保证同一时间只有一个线程访问URLCallbacks。,在创建回调URLCallbacks的block中创建了一个NSOperation并添加到NSOperationQueue中。
  • 4.每个图片下载都是一个operation类,创建后添加到一个队列中,SDWebimage定义了一个协议 SDWebImageOperation作为图片下载操作的基础协议,声明了一个cancel方法,用于取消操作。
1. @protocol SDWebImageOperation <NSObject>
2. -(void)cancel;
3. @end
  • 5.对于图片的下载,SDWebImageDownloaderOperation完全依赖于NSURLConnection类,继承和实现了NSURLConnectionDataDelegate协议的方法
1. connection:didReceiveResponse:
2. connection:didReceiveData:
3. connectionDidFinishLoading:
4. connection:didFailWithError:
5. connection:willCacheResponse:
6. connectionShouldUseCredentialStorage:
7. -connection:willSendRequestForAuthenticationChalleng
8. -connection:didReceiveData:方法,接受数据,创建一个CGImageSourceRef对象,在首次获取数据时(图片width,height),图片下载完成之前,使用CGImageSourceRef对象创建一个图片对象,经过缩放、解压操作生成一个UIImage对象供回调使用,同时还有下载进度处理。
9. 注:缩放:SDWebImageCompat中SDScaledImageForKey函数
10. 解压:SDWebImageDecoder文件中decodedImageWithImage
11.

2、SDWebImageDownloaderOption

  • 1.继承自NSOperation类,没有简单实现main方法,而是采用更加灵活的start方法,以便自己管理下载的状态
  • 2.start方法中创建了下载使用的NSURLConnections对象,开启了图片的下载,并抛出一个下载开始的通知,
  • 3.小结:下载的核心是利用NSURLSession加载数据,每个图片的下载都有一个operation操作来完成,并将这些操作放到一个操作队列中,这样可以实现图片的并发下载。

3、SDWebImageDecoder(异步对图片进行解码)

二、缓存

减少网络流量,下载完图片后存储到本地,下载再获取同一张图片时,直接从本地获取,提升用户体验,能快速从本地获取呈现给用户。
SDWebImage提供了对图片进行了缓存,主要由SDImageCache完成。该类负责处理内存缓存以及一个可选的磁盘缓存,其中磁盘缓存的写操作是异步的,不会对UI造成影响。

1、内存缓存及磁盘缓存

  • 1.内存缓存的处理由NSCache对象实现,NSCache类似一个集合的容器,它存储key-value对,类似于nsdictionary类,我们通常使用缓存来临时存储短时间使用但创建昂贵的对象,重用这些对象可以优化新能,同时这些对象对于程序来说不是紧要的,如果内存紧张就会自动释放。
  • 2.磁盘缓存的处理使用NSFileManager对象实现,图片存储的位置位于cache文件夹,另外SDImageCache还定义了一个串行队列来异步存储图片。
  • 3.SDImageCache提供了大量方法来缓存、获取、移除及清空图片。对于图片的索引,我们通过一个key来索引,在内存中,我们将其作为NSCache的key值,而在磁盘中,我们用这个key值作为图片的文件名,对于一个远程下载的图片其url实作为这个key的最佳选择。

2、存储图片
先在内存中放置一份缓存,如果需要缓存到磁盘,将磁盘缓存操作作为一个task放到串行队列中处理,会先检查图片格式是jpeg还是png,将其转换为响应的图片数据,最后吧数据写入磁盘中(文件名是对key值做MD5后的串)

3、查询图片
内存和磁盘查询图片API:

1. - (UIImage *)imageFromMemoryCacheForKey:(NSString *)key;
2. - (UIImage *)imageFromDiskCacheForKey:(NSString *)key;
3. 

查看本地是否存在key指定的图片,使用一下API:

1. - (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock;

4、移除图片
移除图片API:

1. - (void)removeImageForKey:(NSString *)key;
2. - (void)removeImageForKey:(NSString *)key withCompletion:(SDWebImageNoParamsBlock)completion;
3. - (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk;
4. - (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion;
5. 

5、清理图片(磁盘)

清空磁盘图片可以选择完全清空和部分清空,完全清空就是吧缓存文件夹删除。

1. - (void)clearDisk;
2. - (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion;

部分清理 会根据设置的一些参数移除部分文件,主要有两个指标:文件的缓存有效期(maxCacheAge:默认是1周)和最大缓存空间大小(maxCacheSize:如果所有文件大小大于最大值,会按照文件最后修改时间的逆序,以每次一半的递归来移除哪些过早的文件,知道缓存文件总大小小于最大值),具体代码参考- (void)cleanDiskWithCompletionBlock;

6、小结
SDImageCache处理提供以上API,还提供了获取缓存大小,缓存中图片数量等API,
常用的接口和属性:

1. @interface SDWebImageManager : NSObject
2. 
3. @property (weak, nonatomic) id <SDWebImageManagerDelegate> delegate;
4. 
5. @property (strong, nonatomic, readonly) SDImageCache *imageCache;
6. @property (strong, nonatomic, readonly) SDWebImageDownloader *imageDownloader;
7. 
8. ...
9. 
10.@end

SDWebImageManager声明了一个delegate属性,其实是一个id对象,代理声明了两个方法

1. // 控制当图片在缓存中没有找到时,应该下载哪个图片
2. - (BOOL)imageManager:(SDWebImageManager *)imageManager shouldDownloadImageForURL:(NSURL *)imageURL;
3. 
4. // 允许在图片已经被下载完成且被缓存到磁盘或内存前立即转换
5. - (UIImage *)imageManager:(SDWebImageManager *)imageManager transformDownloadedImage:(UIImage *)image withURL:(NSURL *)imageURL;

这两个方法会在SDWebImageManager的-downloadImageWithURL:options:progress:completed:方法中调用,而这个方法是SDWebImageManager类的核心所在(具体看源码)

SDWebImageManager的几个API:

1. (1)- (void)cancelAll   : 取消runningOperations中所有的操作,并全部删除
2. 
3. (2)- (BOOL)isRunning  :检查是否有操作在运行,这里的操作指的是下载和缓存组成的组合操作
4. 
5. (3) - downloadImageWithURL:options:progress:completed:   核心方法
6. 
7. (4)- (BOOL)diskImageExistsForURL:(NSURL *)url  :指定url的图片是否进行了磁盘缓存
8. 

四、视图扩展

在使用SDWebImage的时候,使用最多的是UIImageView+WebCache中的针对UIImageView的扩展,核心方法是sd_setImageWithURL:placeholderImage:options:progress:completed:, 其使用SDWebImageManager单例对象下载并缓存图片。

除了扩展UIImageView外,SDWebImage还扩展了UIView,UIButton,MKAnnotationView等视图类,具体可以参考源码,除了可以使用扩展的方法下载图片,同时也可以使用SDWebImageManager下载图片。

UIView+WebCacheOperation分类:
把当前view对应的图片操作对象存储起来(通过运行时设置属性),在基类中完成
存储的结构:一个loadOperationKey属性,value是一个字典(字典结构: key:UIImageViewAnimationImages或者UIImageViewImageLoad,value是 operation数组(动态图片)或者对象)

UIButton+WebCache分类
会根据不同的按钮状态,下载的图片根据不同的状态进行设置
imageURLStorageKey:{state:url}

五、技术点

  • 1.dispatch_barrier_sync函数,用于对操作设置顺序,确保在执行完任务后再确保后续操作。常用于确保线程安全性操作
  • 2.NSMutableURLRequest:用于创建一个网络请求对象,可以根据需要来配置请求报头等信息
  • 3.NSOperation及NSOperationQueue:操作队列是OC中一种告诫的并发处理方法,基于GCD实现,相对于GCD来说,操作队列的优点是可以取消在任务处理队列中的任务,另外在管理操作间的依赖关系方面容易一些,对SDWebImage中我们看到如何使用依赖将下载顺序设置成后进先出的顺序
  • 4.NSURLSession:用于网络请求及相应处理
  • 5.开启后台任务
  • 6.NSCache类:一个类似于集合的容器,存储key-value对,这一点类似于nsdictionary类,我们通常用使用缓存来临时存储短时间使用但创建昂贵的对象。重用这些对象可以优化性能,因为它们的值不需要重新计算。另外一方面,这些对象对于程序来说不是紧要的,在内存紧张时会被丢弃
  • 7.清理缓存图片的策略:特别是最大缓存空间大小的设置。如果所有缓存文件的总大小超过这一大小,则会按照文件最后修改时间的逆序,以每次一半的递归来移除那些过早的文件,直到缓存的实际大小小于我们设置的最大使用空间。
  • 8.图片解压操作:这一操作可以查看SDWebImageDecoder.m中+decodedImageWithImage方法的实现。
  • 9.对GIF图片的处理
  • 10.对WebP图片的处理。

九十:如何高性能的给 UIImageView 加个圆角?

不好的解决方案:使用下面的方式会强制Core Animation提前渲染屏幕的离屏绘制, 而离屏绘制就会给性能带来负面影响,会有卡顿的现象出现。

1. self.view.layer.cornerRadius = 5.0f;
2. self.view.layer.masksToBounds = YES;

正确的解决方案:使用绘图技术

1.  - (UIImage *)circleImage {
2.      // NO代表透明
3.      UIGraphicsBeginImageContextWithOptions(self.size, NO, 0.0);
4.      // 获得上下文
5.      CGContextRef ctx = UIGraphicsGetCurrentContext();
6.      // 添加一个圆
7.      CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height);
8.      CGContextAddEllipseInRect(ctx, rect);
9.      // 裁剪
10.     CGContextClip(ctx);
11.     // 将图片画上去
12.     [self drawInRect:rect];
13.     UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
14.     // 关闭上下文
15.     UIGraphicsEndImageContext();
16.     return image;
17. }

还有一种方案:使用了贝塞尔曲线"切割"个这个图片, 给UIImageView 添加了的圆角,其实也是通过绘图技术来实现的。

1.  UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
2.  imageView.center = CGPointMake(200, 300);
3.  UIImage *anotherImage = [UIImage imageNamed:@"image"];
4.  UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, NO, 1.0);
5.  [[UIBezierPath bezierPathWithRoundedRect:imageView.bounds
6.                         cornerRadius:50] addClip];
7.  [anotherImage drawInRect:imageView.bounds];
8.  imageView.image = UIGraphicsGetImageFromCurrentImageContext();
9.  UIGraphicsEndImageContext();
10. [self.view addSubview:imageView];
11. 

九十一:了解CoreData

CoreData的介绍:

  • CoreData是面向对象的API,CoreData是iOS中非常重要的一项技术,几乎在所有编写的程序中,CoreData都作为数据存储的基础。
  • CoreData是苹果官方提供的一套框架,用来解决与对象声明周期管理、对象关系管理和持久化等方面相关的问题。
  • 大多数情况下,我们引用CoreData作为持久化数据的解决方案,并利用它作为持久化数据映射为内存对象。提供的是对象-关系映射功能,也就是说,CoreData可以将Objective-C对象转换成数据,保存到SQL中,然后将保存后的数据还原成OC对象。

CoreData的特征:

  • 通过CoreData管理应用程序的数据模型,可以极大程度减少需要编写的代码数量。
  • 将对象数据存储在SQLite数据库已获得性能优化。
  • 提供NSFetchResultsController类用于管理表视图的数据,即将Core Data的持久化存储在表视图中,并对这些数据进行管理:增删查改。
  • 管理undo/redo操纵;
  • 检查托管对象的属性值是否正确。

Core Data的6成员对象

  • NSManageObject:被管理的数据记录Managed Object Model是描述应用程序的数据模型,这个模型包含实体(Entity)、特性(Property)、读取请求(Fetch Request)等。
  • NSManageObjectContext:管理对象上下文,持久性存储模型对象,参与数据对象进行各种操作的全过程,并监测数据对象的变化,以提供对undo/redo的支持及更新绑定到数据的UI。
  • NSPersistentStoreCoordinator:连接数据库的Persistent Store Coordinator相当于数据文件管理器,处理底层的对数据文件的读取和写入,一般我们与这个没有交集。
  • NSManagedObjectModel:被管理的数据模型、数据结构。
  • NSFetchRequest:数据请求;
  • NSEntityDescription:表格实体结构,还需知道.xcdatamodel文件编译后为.momd或者.mom文件。

Core Data的功能

  • 对于KVC和KVO完整且自动化的支持,除了为属性整合KVO和KVC访问方法外,还整合了适当的集合访问方法来处理多值关系;
  • 自动验证属性(property)值;
  • 支持跟踪修改和撤销操作;
  • 关系维护,Core Data管理数据的关系传播,包括维护对象间的一致性;
  • 在内存上和界面上分组、过滤、组织数据;
  • 自动支持对象存储在外部数据仓库的功能;
  • 创建复杂请求:无需动手写SQL语句,在获取请求(fetch request)中关联NSPredicate。NSPreadicate支持基本功能、相关子查询和其他高级的SQL特性。它支持正确的Unicode编码、区域感知查询、排序和正则表达式;
  • 延迟操作:Core Data使用 懒加载(lazy loading)方式减少内存负载,还支持部分实体化延迟加载和复制对象的数据共享机制;
  • 合并策略:Core Data内置版本跟踪和乐观锁(optimistic locking)来支持多用户写入冲突的解决,其中,乐观锁就是对数据冲突进行检测,若冲突就返回冲突的信息;
  • 数据迁移:Core Data的Schema Migration工具可以简化应对数据库结构变化的任务,在某些情况允许你执行高效率的数据库原地迁移工作;
  • 可选择针对程序Controller层的集成,来支持UI的显示同步Core Data在IPhone OS之上,提供NSFetchedResultsController对象来做相关工作,在Mac OS X上我们用Cocoa提供的绑定(Binding)机制来完成的。

九十二:简述内存管理基本原则

  • 之前:OC内存管理遵循“谁创建,谁释放,谁引用,谁管理”的机制,当创建或引用一个对象的时候,需要向她发送alloc、copy、retain消息,当释放该对象时需要发送release消息,当对象引用计数为0时,系统将释放该对象,这是OC的手动管理机制(MRC)。

  • 目前:iOS 5.0之后引用自动管理机制——自动引用计数(ARC),管理机制与手动机制一样,只是不再需要调用retain、release、autorelease;它编译时的特性,当你使用ARC时,在适当位置插入release和autorelease;它引用strong和weak关键字,strong修饰的指针变量指向对象时,当指针指向新值或者指针不复存在,相关联的对象就会自动释放,而weak修饰的指针变量指向对象,当对象的拥有者指向新值或者不存在时weak修饰的指针会自动置为nil。

  • 如果使用alloc、copy(mutableCopy)或者retian一个对象时,你就有义务,向它发送一条release或者autorelease消息。其他方法创建的对象,不需要由你来管理内存。

  • 向一个对象发送一条autorelease消息,这个对象并不会立即销毁, 而是将这个对象放入了自动释放池,待池子释放时,它会向池中每一个对象发送 一条release消息,以此来释放对象.

  • 向一个对象发送release消息,并不意味着这个对象被销毁了,而是当这个对象的引用计数为0时,系统才会调用dealloc方法,释放该对象和对象本身它所拥有的实例。

其他注意事项

  • 如果一个对象有一个_strong类型的指针指向着,找个对象就不会被释放。如果一个指针指向超出了它的作用域,就会被指向nil。如果一个指针被指向nil,那么它原来指向的对象就被释放了。当一个视图控制器被释放时,它内部的全局指针会被指向nil。用法“:不管全局变量还是局部变量用_strong描述就行。

  • 局部变量:出了作用域,指针会被置为nil。

  • 方法内部创建对象,外部使用需要添加_autorelease;

  • 连线的时候,用_weak描述。

  • 代理使用unsafe_unretained就相当于assign

  • block中为了避免循环引用问题,使用_weak描述;

  • 声明属性时,不要以new开头。如果非要以new开头命名属性的名字,需要自己定制get方法名,如:

1.  @property(getter=theString) NSString * newString;
  • 如果要使用自动释放池,用@autoreleasepool{}

  • ARC只能管理Foundation框架的变量,如果程序中把Foundation中的变量强制换成COreFoundation中的变量需要交换管理权;

  • 在非ARC工程中采用ARC去编译某些类:-fobjc-arc

  • 在ARC下的工程采用非ARC去编译某些类:-fno-fobjc-arc

九十三:GCD死锁问题解读

一.题干:

1.      __block int x = 0;
2.      __block int y = 0;
3.      dispatch_async(dispatch_get_global_queue(0, 0), ^{
4.          NSLog(@"%d", x++);
5.          dispatch_sync(dispatch_get_main_queue(), ^{
6.              NSLog(@"%d", ++y);
7.          });
8.          NSLog(@"%d", y++);
9.          NSLog(@"%d", x + y);
10.         while (1) {}
11.     });
12. 

那么下面直接解答

1.首先调用dispatch_async(异步调度)把我们的任务抛到dispatch_get_global_queue(全局并行队列), 这会让我们第一个block中的线程跳到子线程, 下文称gq.
2.打印NSLog(@"%d", x++);, 由于++x后面, 分号结束才自增1, 所以打印出0.
3.调用dispatch_syncNSLog(@"%d", ++y);任务同步调度到主线程队列中, 主线程会去执行该任务, 不死锁的原因是dispatch_sync并没有在主线程中创建, 而是在dispatch_get_global_queue中创建并等待任务执行结束, 由于它是子线程, 所以并不会阻塞.
4.下面就都是简单的逻辑运算了, 直到碰上while (1) {}死循环.

下面是答案

1.  2019-03-07 15:21:14.471910+0800 [75481:2405428] 0
2.  2019-03-07 15:21:14.476663+0800 [75481:2405369] 1
3.  2019-03-07 15:21:14.476834+0800 [75481:2405428] 1
4.  2019-03-07 15:21:14.476986+0800 [75481:2405428] 3
5.  

可能有些人对上面的子线程和主线程理解不太深刻, 这里我打印出block中的线程关系给大家看一下.

1.  dispatch_async(dispatch_get_global_queue(0, 0), ^{
2.          NSLog(@"A --- %@", [NSThread currentThread]);
3.          dispatch_sync(dispatch_get_main_queue(), ^{
4.              NSLog(@"B --- %@", [NSThread currentThread]);
5.          });
6.          NSLog(@"C --- %@", [NSThread currentThread]);
7.          while (1) {}
8.      });
9.  
1.  A --- <NSThread: 0x600002f836c0>{number = 3, name = (null)}
2.  B --- <NSThread: 0x600002fd5400>{number = 1, name = main}
3.  C --- <NSThread: 0x600002f836c0>{number = 3, name = (null)}
4.  

我们可以看到打印结果, 在执行dispatch_async后, 线程跳到了number = 3的子线程, 在子线程中进行同步方法的处理, 并不会阻塞主线程且阻塞子线程不会造成死锁, 所以这个理论是完全可以说得过去的.

二.拓展

通过上面的问题又拓展一下关于多线程GCD的知识, 你如果没看懂上面的解答, 不妨先了解一下基础知识.

通常上网络上对死锁的解释是

主线程队列是串行队列, 队列中的任务是一个执行完成后才去执行另一个, 用同步方法将任务1提交到主线程队列就会阻塞住主线程, 而这个刚提交的任务1必须等待主线程中的任务执行完毕才可以执行, 但这时主线程已经被阻塞了, 就是说要等任务1执行完成后才能去执行原有的任务, 所以双方在互相等待而卡住, 最后一个任务也没机会执行到, 就造成了死锁.

按照这种逻辑, 我也自动生成了一套自己的理解, 如果有不对的地方也请指出:

我认为主线程是永远都在执行任务的, 什么任务不重要, 这时候我们有一个任务要dispatch_sync同步抛到主线程队列中执行, 当我们抛进去的那一瞬间, 这个任务就捕获了主线程的控制权(或可以说成不阻塞), 但是主线程只能眼睁睁看着它却不能执行它, 因为主线程在等待之前的任务执行完毕, 这样双方都在等待主线程处理自己, 所以会造成死锁, 而我们的题目刚开始就跳到了子线程, 在子线程中等待不会捕获主线程控制权(或可以说成不阻塞主线程), 所以不会造成死锁.

假设声明:

这里还是要声明一点就是 捕获了主线程的控制权 是我自己假想出来的东西, 不真实, 没有任何依据, 只是凭空想象出这么一个东西来一定程度上的帮助理解, 如果思想不对也请指出.

下面拓展一下队列的知识:

队列的概念: 队列就跟我们排队一样, 从类型上一共分为两种串行队列和并行队列.

串行队列: 10个人在售票窗口买票, 中途不能插队, 任务是按照顺序进行的, 第一个执行完了才继续执行第二个一直到第10个.

并行队列: 10个人在10个自动售货机前买票, 同时进行, 至于谁先买完, 我们无从得知...

然后说一下GCD, 平时常用的调度方法包括下面两种dispatch_syncdispatch_async下面我们就依次介绍一下.

我们首先来看一下iOS中的线程

image.png image

主线程: 应用只有一个, 编号为1且名称为main, 在主线程中同步处理耗时任务会阻塞.

子线程: 应用中可能有多个, 编号不确定可能为1也可能不为1, 处理耗时操作不会阻塞.(因为发现过为1且名字不为main的线程, 所以这里还要大家来证实)

dispatch_sync: 翻译为同步调度, 在指定队列中同步扔进去一个任务(block), 该任务可能由主线程或子线程处理.

dispatch_async: 翻译为异步调度, 在指定队列中扔进去一个任务(block), 该任务可能由主线程或子线程处理.

所以在这里我结合队列总结一下, 在我们的程序中一共存在4种队列(我知道的), 分别是:

1.主线程队列

1.  dispatch_get_main_queue()
2.  
3.  主线程队列是一个典型的串行队列, 里面最多只能容许一个线程来执行, 也就是主线程, 向里面插入任务, 无论是同步或异步, 该任务均由`主线程`执行, 但在主线程运行的队列中同步调度会死锁.
4.   

2.全局并行队列

1.  dispatch_get_global_queue(0, 0)
2.  
3.  全局并行队列是一个典型的并行队列.
4.  

3.串行队列

1.  dispatch_queue_create("com.objcat.serial", DISPATCH_QUEUE_SERIAL);
2.  
3.  串行队列是自己创建出的队列, 主线程队列一样, 任务也是一个一个来执行的.
4.  

4.并行队列

1.  dispatch_queue_create("com.objcat.concurrent", DISPATCH_QUEUE_CONCURRENT);
2.  
3.  并行队列是自己创建出的队列, 跟全局并行队列一样, 不同线程上的任务是一起执行的, 哪个先执行完并不一定.
4.  

所以到这里你应该会明白一个道理, 是否死锁与线程的种类调度的类型有关, 当在主线程上同步调度任务的时候才会出现死锁.

网上还有这张图, 也给拿来了

image.png 我们可以看到任务3卡在了任务2之前并阻塞了线程, 而任务2在等任务3, 任务3在等任务2, 所以就造成了死锁.

三.反思

有可能你认为刚才讲的故事不是特别通顺, 没错, 我同样认为在很多地方仍解释不通, 假如当前主线程队列中不存在任务, 我向其中插入一个任务为什么就不能执行呢?

我尝试在死锁前面打断点来查看主线程队列中的任务

image.png

结果我发现任务是viewDidLoad, 证明确实有任务正在进行.

这样就可以解释通, 主线程一直在处理viewDidLoad的代码, 所以当我们强行插入一个任务的时候主线程队列就会因为这个任务的强行插入而转为互相等待状态因此会死锁.

九十四:谈一谈网络中的 session 和 cookie?

因为 Http 无状态的特性,如果需要判断是哪个用户,这时就需要 CookieSession

Cookie 存储在客户端,用来记录用户状态,区分用户。一般都是服务端把生成的 Cookie 通过响应返回给客户端,客户端保存。

Session 存储在网络端,需要依赖 Cookie 机制。服务端生成了 Session 后,返回给客户端,客户端 setCookie:sessionID,所以下次请求的时候,客户端把 Cookie 发送给服务端,服务端解析出 SessionID,在服务端根据 SessionID 判断当前的用户。

如何修改 Cookie?

  • 相同 Key 值得新的 Cookie 会覆盖旧的 Cookie.
  • 覆盖规则是 name path 和 domain 等需要与原有的一致

如何删除 Cookie?

  • 相同 Key 值得新的 Cookie 会覆盖旧的 Cookie.
  • 覆盖规则是 name path 和 domain 等需要与原有的一致
  • 设置 Cookieexpires 为过去的一个时间点,或者 maxAge = 0

如何保证 Cookie 的安全?

  • Cookie 进行加密处理
  • 只在 Https 上携带 Cookie
  • 设置 CookiehttpOnly,防止跨站脚本攻击。

九十五:UIWindow,UIView,CALayer的区别

1. UIWindow

1.  @interface UIWindow : UIView
2.  
3.  @property(nonatomic) UIWindowLevel windowLevel;                   // default = 0.0
4.  @property(nonatomic,readonly,getter=isKeyWindow) BOOL keyWindow;
5.  - (void)becomeKeyWindow;                               // override point for subclass. Do not call directly
6.  - (void)resignKeyWindow;                               // override point for subclass. Do not call directly
7.  
8.  - (void)makeKeyWindow;
9.  - (void)makeKeyAndVisible;                             // convenience. most apps call this to show the main window and also make it key. otherwise use view hidden property
10. 
11. @property(nullable, nonatomic,strong) UIViewController *rootViewController NS_AVAILABLE_IOS(4_0);  // default is nil
12. @end
13. 

继承自UIView,是一种特殊的 UIView通常在一个app中只会有一个keyUIWindow。

iOS程序启动完毕后,创建的第一个视图控件就是UIWindow,接着创建控制器的view,最后将控制器的view添加到UIWindow上,于是控制器的view就显示在屏幕上了

主要作用是提供一个区域用来显示UIView;将事件分发给UIView;与UIViewController一起处理屏幕的旋转事件。

2. UIView

1.  @interface UIView : UIResponder <NSCoding, UIAppearance, UIAppearanceContainer, UIDynamicItem, UITraitEnvironment, UICoordinateSpace, UIFocusItem, UIFocusItemContainer, CALayerDelegate>
2.  
3.  @property(nonatomic,readonly,strong) CALayer  *layer;
4.  @end
5.  
1.  @interface UIResponder : NSObject <UIResponderStandardEditActions>
2.  

继承自UIResponder,间接继承自NSObject,主要是用来构建用户界面的,并且可以响应事件。

对于UIView,侧重于对内容的显示管理;其实是相对于CALayer的高层封装。

3. CALayer

1.  @interface CALayer : NSObject <NSSecureCoding, CAMediaTiming>
2.  

直接继承自NSObject,所以不能响应事件

其实就是一个图层,UIView之所以能显示在屏幕上,主要是它内部有一个CALayer对象。在创建UIView时,它内部会自动创建一个图层,当UIView需要显示在屏幕上的时候,会调用drawRect:方法进行绘图,并且会将所有内容绘制到自己的图层上,绘图完毕后,系统会将图层拷贝到屏幕上,这样完成UIView的显示。

image.png

  • layer给view提供了基础设施,使得绘制内容和呈现更高效动画更容易、更低耗
  • layer不参与view的事件处理、不参与响应链

九十六:事件传递和响应机制

1. 事件的产生
  • 发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的事件队列中,为什么是队列而不是栈?因为队列的特点是FIFO,即先进先出,先产生的事件先处理才符合常理,所以把事件添加到队列。
  • UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发送事件给应用程序的主窗口(keyWindow)
  • 主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件,这也是整个事件处理过程的第一步。

找到合适的视图控件后,就会调用视图控件的touches方法来作具体的事件处理。

2. 事件的传递
  • 触摸事件的传递是从父控件传递到子控件
  • 也就是UIApplication->window->寻找处理事件最合适的view

注 意: 如果父控件不能接受触摸事件,那么子控件就不可能接收到触摸事件

3. 应用如何找到最合适的控件来处理事件?
  • 1.首先判断主窗口(keyWindow)自己是否能接受触摸事件
  • 2.判断触摸点是否在自己身上
  • 3.子控件数组中从后往前遍历子控件,重复前面的两个步骤(所谓从后往前遍历子控件,就是首先查找子控件数组中最后一个元素,然后执行1、2步骤)
  • 4.如果没有符合条件的子控件,那么就认为自己最合适处理这个事件,也就是自己是最合适的view。
3.1 寻找最合适的view底层剖析

两个重要的方法:

1.  - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
2.  - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
3.  
3.2 hitTest:withEvent 方法介绍

什么时候调用?

  • 只要事件一传递给一个控件,这个控件就会调用他自己的hitTest:withEvent:方法

作用

  • 寻找并返回最合适的view(能够响应事件的那个最合适的view)

注 意
1.不管这个控件能不能处理事件,也不管触摸点在不在这个控件上,事件都会先传递给这个控件,随后再调用hitTest:withEvent:方法
2.如果hitTest:withEvent:方法中返回nil,那么调用该方法的控件本身和其子控件都不是最合适的view,也就是在自己身上没有找到更合适的view。那么最合适的view就是该控件的父控件。

3.3 pointInside:withEvent 方法介绍

判断点在不在当前view上(方法调用者的坐标系上)如果返回YES,代表点在方法调用者的坐标系上;返回NO代表点不在方法调用者的坐标系上,那么方法调用者也就不能处理事件。

UIView不能接收触摸事件的三种情况:

  • 不允许交互:userInteractionEnabled = NO
  • 隐藏:如果把父控件隐藏,那么子控件也会隐藏,隐藏的控件不能接受事件
  • 透明度:如果设置一个控件的透明度<0.01,会直接影响子控件的透明度。alpha:0.0~0.01为透明。
4. 事件的响应
4.1 触摸事件处理的整体过程
  • 1 用户点击屏幕后产生的一个触摸事件,经过一系列的传递过程后,会找到最合适的视图控件来处理这个事件
  • 2 找到最合适的视图控件后,就会调用控件的touches方法来作具体的事件处理touchesBegan…touchesMoved…touchedEnded…
  • 3 这些touches方法的默认做法是将事件顺着响应者链条向上传递(也就是touch方法默认不处理事件,只传递事件),将事件交给上一个响应者进行处理
4.2 响应者链条

在iOS程序中无论是最后面的UIWindow还是最前面的某个按钮,它们的摆放是有前后关系的,一个控件可以放到另一个控件上面或下面,那么用户点击某个控件时是触发上面的控件还是下面的控件呢,这种先后关系构成一个链条就叫响应者链。也可以说,响应者链是由多个响应者对象连接起来的链条

在iOS中响应者链的关系可以用下图表示:

image.png

响应者对象 能处理事件的对象,也就是继承自UIResponder的对象

作用 能很清楚的看见每个响应者之间的联系,并且可以让一个事件多个对象处理。

如何判断上一个响应者

  • 1 如果当前这个view是控制器的view,那么控制器就是上一个响应者
  • 2 如果当前这个view不是控制器的view,那么父控件就是上一个响应者

响应者链的事件传递过程

  • 1 如果当前view是控制器的view,那么控制器就是上一个响应者,事件就传递给控制器;如果当前view不是控制器的view,那么父视图就是当前view的上一个响应者,事件就传递给它的父视图
  • 2 在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给window对象进行处理
  • 3 如果window对象也不处理,则其将事件或消息传递给UIApplication对象
  • 4 如果UIApplication也不能处理该事件或消息,则将其丢弃
5. 总结

事件处理的整个流程总结:

  • 1.触摸屏幕产生触摸事件后,触摸事件会被添加到由UIApplication管理的事件队列中(即,首先接收到事件的是UIApplication)。
  • 2.UIApplication会从事件队列中取出最前面的事件,把事件传递给应用程序的主窗口(keyWindow)。
  • 3.主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件。(至此,第一步已完成)
  • 4.最合适的view会调用自己的touches方法处理事件
  • 5.touches默认做法是把事件顺着响应者链条向上抛。

如何做到一个事件多个对象处理:
因为系统默认做法是把事件上抛给父控件,所以可以通过重写自己的touches方法和父控件的touches方法来达到一个事件多个对象处理的目的。

1.  - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ 
2.      // 1.自己先处理事件...
3.      NSLog(@"do somthing...");
4.      // 2.再调用系统的默认做法,再把事件交给上一个响应者处理
5.      [super touchesBegan:touches withEvent:event]; 
6.  }
7.  

事件的传递和响应的区别:
事件的传递是从上到下(父控件到子控件),事件的响应是从下到上(顺着响应者链条向上传递:子控件到父控件。

九十七:UIView block动画实现原理

在了解UIView block动画实现原理之前,需要先了解CALayer的可动画属性。

1. CALayer的可动画属性

CALayer拥有大量的属性,看CALayer的头文件内容,会发现很多的属性的注释中,最后会有一个词叫做Animatable,直译过来是可动画的。下面的截图只是CALayer众多可动画属性中的一部分(注意frame并不是可动画的属性)

1.  /* The bounds of the layer. Defaults to CGRectZero. Animatable. */
2.  
3.  @property CGRect bounds;
4.  
5.  /* The position in the superlayer that the anchor point of the layer's
6.   * bounds rect is aligned to. Defaults to the zero point. Animatable. */
7.  
8.  @property CGPoint position;
9.  
10. /* The Z component of the layer's position in its superlayer. Defaults
11.  * to zero. Animatable. */
12. 
13. @property CGFloat zPosition;
14. 
15. /* Defines the anchor point of the layer's bounds rect, as a point in
16.  * normalized layer coordinates - '(0, 0)' is the bottom left corner of
17.  * the bounds rect, '(1, 1)' is the top right corner. Defaults to
18.  * '(0.5, 0.5)', i.e. the center of the bounds rect. Animatable. */
19. 
20. @property CGPoint anchorPoint;
21.
22. /* The Z component of the layer's anchor point (i.e. reference point for
23.  * position and transform). Defaults to zero. Animatable. */
24. 
25. @property CGFloat anchorPointZ;
26. 
27. /* A transform applied to the layer relative to the anchor point of its
28.  * bounds rect. Defaults to the identity transform. Animatable. */
29. 
30. @property CATransform3D transform;
31. 

如果一个属性被标记为Animatable,那么它具有以下两个特点:

1、直接对它赋值可能产生隐式动画;
2、我们的CAAnimation的keyPath可以设置为这个属性的名字。

当我们直接对可动画属性赋值的时候,由于有隐式动画存在的可能,CALayer首先会判断此时有没有隐式动画被触发。它会让它的delegate(没错CALayer拥有一个属性叫做delegate)调用actionForLayer:forKey:来获取一个返回值,这个返回值在声明的时候是一个id对象,当然在运行时它可能是任何对象。这时CALayer拿到返回值,将进行判断:

  • 如果返回的对象是一个nil,则进行默认的隐式动画;
  • 如果返回的对象是一个[NSNull null] ,则CALayer不会做任何动画;
  • 如果是一个正确的实现了CAAction协议的对象,则CALayer用这个对象来生成一个CAAnimation,并加到自己身上进行动画。

理解完这些,我们再来分析UIView的block动画就容易理解了。

2. UIView的block动画
1.  Amazing things happen when they are in a block.
2.  

有趣的是,如果这个CALayer被一个UIView所持有,那么这个CALayer的delegate就是持有它的那个UIView。

大家应该可以思考出这样的问题:为什么同样的一行代码在block里面就有动画在block外面就没动画,就像下面这样:

1.  /** 产生动画 */
2.  - (void)createAnimation {
3.      UIView *redView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
4.      redView.backgroundColor = [UIColor redColor];
5.      [self.view addSubview:redView];
6.  
7.      // 这样写没有动画
8.      redView.center = CGPointMake(200, 300);
9.  
10.     [UIView animateWithDuration:1.25 animations:^{
11.         // 写在block里面就有动画
12.         redView.center = CGPointMake(200, 300);
13.     }];
14. }
15. 

image.png

既然UIView就是CALayer的delegate,那么actionForLayer:forKey:方法就是由UIView来实现的。所以UIView可以相当灵活的控制动画的产生。

当我们对UIView的一个属性赋值的时候,它只是简单的调用了它持有的那个CALayer的对应的属性的setter方法而已,根据上面的可动画属性的特点,CALayer会让它的delegate(也就是这个UIView)调用actionForLayer:forKey:方法。实际上结果大家都应该能想得到:在UIView的动画block外面,UIView的这个方法将返回NSNull,而在block里面,UIView将返回一个正确的CAAction对象(这里将不深究UIView是如何判断此时setter的调用是在动画block外面还是里面的)。

为了证明这个结论,我们将继续进行实验:

1.  /** UIView 动画产生原理 */
2.  - (void)uiviewAnimation {
3.      NSLog(@"%@",[self.view.layer.delegate actionForLayer:self.view.layer forKey:@"position"]);
4.  
5.      [UIView animateWithDuration:1.25 animations:^{
6.          NSLog(@"%@",[self.view.layer.delegate actionForLayer:self.view.layer forKey:@"position"]);
7.      }];
8.  }
9.  

我们分别在block外面和block里面打印actionForLayer:forKey:方法的返回值,看看它究竟是什么玩意。

1.  2019-06-11 19:45:23.973002+0800 YaZhaiInterviewQuestionDemo[93569:1953578] <null>
2.  2019-06-11 19:45:23.973622+0800 YaZhaiInterviewQuestionDemo[93569:1953578] <_UIViewAdditiveAnimationAction: 0x600001ff0d40>
3.  

打印发现,我们的结论是正确的:在block外面,这个方法将返回一个NSNull(是尖括号的null,nil打印出来是圆括号的null),而在block里面返回了一个叫做UIViewAdditiveAnimationAction类的对象,这个类是一个私有类,遵循了苹果一罐的命名规范: xxAction,一定就是一个实现了CAAction协议的对象了。

这也就说明了为什么我们对一个view的center赋值,如果这行代码在动画block里面,就会有动画,在block外面则没有动画。

九十八:MVVM和MVC的区别

MVVM和MVC的区别

1. MVC

image.png

MVC

MVC的弊端

  • 厚重的View Controller

    • M:模型model的对象通常非常的简单。根据Apple的文档,model应包括数据和操作数据的业务逻辑。而在实践中,model层往往非常薄,不管怎样,model层的业务逻辑不应被拖入到controller。
    • V:视图view通常是UIKit控件(component,这里根据习惯译为控件)或者编码定义的UIKit控件的集合。View的如何构建(PS:IB或者手写界面)何必让Controller知晓,同时View不应该直接引用model(PS:现实中,你懂的!),并且仅仅通过IBAction事件引用controller。业务逻辑很明显不归入view,视图本身没有任何业务。
    • C:控制器controller。Controller是app的“胶水代码”:协调模型和视图之间的所有交互。控制器负责管理他们所拥有的视图的视图层次结构,还要响应视图的loading、appearing、disappearing等等,同时往往也会充满我们不愿暴露的model的模型逻辑以及不愿暴露给视图的业务逻辑。网络数据的请求及后续处理,本地数据库操作,以及一些带有工具性质辅助方法都加大了Massive View Controller的产生。
  • 遗失(无处安放)的网络逻辑
    苹果使用的MVC的定义是这么说的:所有的对象都可以被归类为一个model,一个view,或是一个controller。

你可能试着把它放在Model对象里,但是也会很棘手,因为网络调用应该使用异步,这样如果一个网络请求比持有它的model生命周期更长,事情将变的复杂。显然View里面做网络请求那就更格格不入了,因此只剩下Controller了。若这样,这又加剧了Massive View Controller的问题。若不这样,何处才是网络逻辑的家呢?

  • 较差的可测试性

由于View Controller混合了视图处理逻辑和业务逻辑,分离这些成分的单元测试成了一个艰巨的任务。

2. MVVM

一种可以很好地解决Massive View Controller问题的办法就是将 Controller 中的展示逻辑抽取出来,放置到一个专门的地方,而这个地方就是 viewModel 。MVVM衍生于MVC,是对 MVC 的一种演进,它促进了 UI 代码与业务逻辑的分离。它正式规范了视图和控制器紧耦合的性质,并引入新的组件。他们之间的结构关系如下:

image.png MVVM

2.1 MVVM 的基本概念
  • MVVM 中,viewview controller正式联系在一起,我们把它们视为一个组件
  • viewview controller 都不能直接引用model,而是引用视图模型(viewModel
  • viewModel 是一个放置用户输入验证逻辑,视图显示逻辑,发起网络请求和其他代码的地方
  • 使用MVVM会轻微的增加代码量,但总体上减少了代码的复杂性
2.2 MVVM 的注意事项
  • view 引用viewModel ,但反过来不行(即不要在viewModel中引入#import UIKit.h,任何视图本身的引用都不应该放在viewModel中)(PS:基本要求,必须满足
  • viewModel 引用model,但反过来不行* MVVM 的使用建议
  • MVVM 可以兼容你当下使用的MVC架构。
  • MVVM 增加你的应用的可测试性。
  • MVVM 配合一个绑定机制效果最好(PS:ReactiveCocoa你值得拥有)。
  • viewController 尽量不涉及业务逻辑,让 viewModel 去做这些事情。
  • viewController 只是一个中间人,接收 view 的事件、调用 viewModel 的方法、响应 viewModel 的变化。
  • viewModel 绝对不能包含视图 view(UIKit.h),不然就跟 view 产生了耦合,不方便复用和测试。
  • viewModel之间可以有依赖。
  • viewModel避免过于臃肿,否则重蹈Controller的覆辙,变得难以维护。
2.3 MVVM 的优势
  • 低耦合:View 可以独立于Model变化和修改,一个 viewModel 可以绑定到不同的 View
  • 可重用性:可以把一些视图逻辑放在一个 viewModel里面,让很多 view 重用这段视图逻辑
  • 独立开发:开发人员可以专注于业务逻辑和数据的开发 viewModel,设计人员可以专注于页面设计
  • 可测试:通常界面是比较难于测试的,而 MVVM 模式可以针对 viewModel来进行测试
2.4 MVVM 的弊端
  • 数据绑定使得Bug 很难被调试。你看到界面异常了,有可能是你 View 的代码有 Bug,也可能是 Model 的代码有问题。数据绑定使得一个位置的 Bug 被快速传递到别的位置,要定位原始出问题的地方就变得不那么容易了。
  • 对于过大的项目,数据绑定和数据转化需要花费更多的内存(成本)。主要成本在于:
  • 数组内容的转化成本较高:数组里面每项都要转化成Item对象,如果Item对象中还有类似数组,就很头疼。
  • 转化之后的数据在大部分情况是不能直接被展示的,为了能够被展示,还需要第二次转化。
  • 只有在API返回的数据高度标准化时,这些对象原型(Item)的可复用程度才高,否则容易出现类型爆炸,提高维护成本。
  • 调试时通过对象原型查看数据内容不如直接通过NSDictionary/NSArray直观。
  • 同一API的数据被不同View展示时,难以控制数据转化的代码,它们有可能会散落在任何需要的地方。
3. 总结
  • MVC的设计模式也并非是病入膏肓,无药可救的架构,最起码目前MVC设计模式仍旧是iOS开发的主流框架,存在即合理。针对文章所述的弊端,我们依旧有许多可行的方法去避免和解决,从而打造一个轻量级的ViewController
  • MVVMMVC的升级版,完全兼容当前的MVC架构,MVVM虽然促进了UI 代码与业务逻辑的分离,一定程度上减轻了ViewController的臃肿度,但是ViewViewModel之间的数据绑定使得 MVVM变得复杂和难用了,如果我们不能更好的驾驭两者之间的数据绑定,同样会造成Controller 代码过于复杂,代码逻辑不易维护的问题。
  • 一个轻量级的ViewController是基于MVCMVVM模式进行代码职责的分离而打造的。MVC和MVVM有优点也有缺点,但缺点在他们所带来的好处面前时不值一提的。他们的低耦合性,封装性,可测试性,可维护性和多人协作便利大大提高了开法效率。
  • 同时,我们需要保持的是一个拥抱变化的心,以及理性分析的态度。在新技术的面前,不盲从,也不守旧,一切的决策都应该建立在认真分析的基础上,这样才能应对技术的变化。

九十九:NSCache,NSDictionary,NSArray的区别

1. NSArray

NSArray作为一个存储对象的有序集合,可能是被使用最多的集合类。

性能特征
在数组的开头和结尾插入/删除元素通常是一个O(1)操作,而随机的插入/删除通常是 O(N)的。

有用的方法
NSArray的大多数方法使用isEqual:来检查对象间的关系(例如containsObject:)。有一个特别的方法

1.  indexOfObjectIdenticalTo:

用来检查指针相等,如果你确保在同一个集合中搜索,那么这个方法可以很大的提升搜索速度。

更多相关资料参考

2. NSDictionary

一个字典存储任意的对象键值对。 由于历史原因,初始化方法使用相反的对象到值的方法,

1.  [NSDictionary dictionaryWithObjectsAndKeys:object, key, nil]
2.  

而新的快捷语法则从key开始

1.  @{key : value, ...}

NSDictionary中的键是被拷贝的并且需要是恒定的。如果在一个键在被用于在字典中放入一个值后被改变,那么这个值可能就会变得无法获取了。一个有趣的细节,在NSDictionary中键是被拷贝的,而在使用一个toll-free桥接的CFDictionary时却只被retain。CoreFoundation类没有通用对象的拷贝方法,因此这时拷贝是不可能的(*)。这只适用于使用CFDictionarySetValue()的时候。如果通过setObject:forKey使用toll-free桥接的CFDictionary,苹果增加了额外处理逻辑来使键被拷贝。反过来这个结论则不成立 — 转换为CFDictionary的NSDictionary对象,对其使用CFDictionarySetValue()方法会调用回setObject:forKey并拷贝键。

3. NSCache

NSCache是一个非常奇怪的集合。在iOS 4/Snow Leopard中加入,默认为可变并且线程安全的。这使它很适合缓存那些创建起来代价高昂的对象。它自动对内存警告做出反应并基于可设置的成本清理自己。与NSDictionary相比,键是被retain而不是被拷贝的。

NSCache的回收方法是不确定的,在文档中也没有说明。向里面放一些类似图片那样比被回收更快填满内存的大对象不是个好主意。(这是在PSPDFKit中很多跟内存有关的crash的原因,在使用自定义的基于LRU的链表的缓存代码之前,我们起初使用NSCache存储事先渲染的图片。)

NSCache可以设置撑自动回收实现了NSDiscardableContent协议的对象。实现该属性的一个比较流行的类是同时间加入的NSPurgeableData,但是在OS X 10.9之前,是非线程安全的(没有信息表明这是否也影响到iOS或者是否在iOS 7中被修复了)。

NSCache性能

那么NSCache如何承受NSMutableDictionary的考验?加入的线程安全必然会带来一些消耗。

4. iOS 构建缓存时选 NSCache 而非NSDictionary
  • 当系统资源将要耗尽时,NSCache可以自动删减缓存。如果采用普通的字典,那么就要自己编写挂钩,在系统通知时手动删减缓存,NSCache会先行删减 时间最久为被使用的对象
  • NSCache 并不会拷贝键,而是会保留它。此行为用NSDictionary也可以实现,但是需要编写比较复杂的代码。NSCache对象不拷贝键的原因在于,很多时候键都是不支持拷贝操作的对象来充当的。因此NSCache对象不会自动拷贝键,所以在键不支持拷贝操作的情况下,该类比字典用起来更方便
  • NScache是线程安全的,NSDictionary不是。在开发者自己不编写加锁代码的前提下,多个线程可以同时访问NSCache。对缓存来说,线程安全通常是很重要的,因为开发者可能在某个线程中读取数据,此时如果发现缓存里找不着指定的键,那么就要下载该键对应的数据了

商业转载请联系作者获得授权,非商业转载请注明出处。NSDictionary中的键是被拷贝的并且需要是恒定的。如果在一个键在被用于在字典中放入一个值后被改变,那么这个值可能就会变得无法获取了。一个有趣的细节,在NSDictionary中键是被拷贝的,而在使用一个toll-free桥接的CFDictionary时却只被retain。CoreFoundation类没有通用对象的拷贝方法,因此这时拷贝是不可能的(*)。这只适用于使用CFDictionarySetValue()的时候。如果通过setObject:forKey使用toll-free桥接的CFDictionary,苹果增加了额外处理逻辑来使键被拷贝。反过来这个结论则不成立 — 转换为CFDictionary的NSDictionary对象,对其使用CFDictionarySetValue()方法会调用回setObject:forKey并拷贝键。

3. NSCache

NSCache是一个非常奇怪的集合。在iOS 4/Snow Leopard中加入,默认为可变并且线程安全的。这使它很适合缓存那些创建起来代价高昂的对象。它自动对内存警告做出反应并基于可设置的成本清理自己。与NSDictionary相比,键是被retain而不是被拷贝的。

NSCache的回收方法是不确定的,在文档中也没有说明。向里面放一些类似图片那样比被回收更快填满内存的大对象不是个好主意。(这是在PSPDFKit中很多跟内存有关的crash的原因,在使用自定义的基于LRU的链表的缓存代码之前,我们起初使用NSCache存储事先渲染的图片。)

NSCache可以设置撑自动回收实现了NSDiscardableContent协议的对象。实现该属性的一个比较流行的类是同时间加入的NSPurgeableData,但是在OS X 10.9之前,是非线程安全的(没有信息表明这是否也影响到iOS或者是否在iOS 7中被修复了)。

NSCache性能

那么NSCache如何承受NSMutableDictionary的考验?加入的线程安全必然会带来一些消耗。

4. iOS 构建缓存时选 NSCache 而非NSDictionary
  • 当系统资源将要耗尽时,NSCache可以自动删减缓存。如果采用普通的字典,那么就要自己编写挂钩,在系统通知时手动删减缓存,NSCache会先行删减 时间最久为被使用的对象
  • NSCache 并不会拷贝键,而是会保留它。此行为用NSDictionary也可以实现,但是需要编写比较复杂的代码。NSCache对象不拷贝键的原因在于,很多时候键都是不支持拷贝操作的对象来充当的。因此NSCache对象不会自动拷贝键,所以在键不支持拷贝操作的情况下,该类比字典用起来更方便
  • NScache是线程安全的,NSDictionary不是。在开发者自己不编写加锁代码的前提下,多个线程可以同时访问NSCache。对缓存来说,线程安全通常是很重要的,因为开发者可能在某个线程中读取数据,此时如果发现缓存里找不着指定的键,那么就要下载该键对应的数据了

传送门:

iOS面试合集+答案(一) (juejin.cn)

iOS面试合集+答案(二) (juejin.cn)

iOS面试合集+答案(三) (juejin.cn)

iOS面试合集+答案(四) (juejin.cn)

iOS面试资料大全 (qq.com)

一份iOS开发者的学习路线参考 (qq.com)

小伙伴们,1-5已经更新完啦,喜欢的朋友可以点个赞加关注哦!