面试知识点整理

Part 0

看以下习题前可以先看看下面的别人整理好的面试题:

iOSInterviewQuestions【强烈推荐看】


Part 1

习题来自:iOS-InterviewQuestion-collection

0x01 内存管理

@autoreleasrPool 的释放时机?

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

参考:

幕背后的Autorelease

Autorelease对象的释放时机

arc 内存管理要点


自动引用计(ARC)数应该遵循的原则?

不能使用 retain/release/retainCount/autorelease

不能使用 NSAllocateObject/NSDeallocateObject

须遵守内存管理的方法命名规则

不要显示调用dealloc

使用 @autorelease 块代替NSAutoreleasePool

不能使用区域(NSZone)

对象型变量不能作为C语言结固体的成员

显示转换“id”和”void *”

参考:

OC内存管理教程之ARC(二)——自动引用计数规则


访问__weak修饰的变量,是否已经被注册在了 @autoreleasePool 中?为什么?

在访问 __weak 修饰的变量时,实际上必定会访问注册到 Autorelease Pool 的对象。如下来年两段代码是相同的效果:

1
2
3
4
5
6
id __weak obj1 = obj0;
NSLog(@"class=%@", [obj1 class]);
// 等同于:
id __weak obj1 = obj0;
id __autoreleasing tmp = obj1;
NSLog(@"class=%@", [tmp class]);

为什么会这样呢?因为 __weak 修饰符只持有对象的弱引用,而在访问对象的过程中,该对象有可能被废弃,如果把被访问的对象注册到 Autorelease Pool 中,就能保证 Autorelease Pool 被销毁前对象是存在的。

参考:

arc 内存管理要点


ARC 的 retainCount 怎么存储的?

the reference counts for most objects are stored in hash tables. Have a look at the _objc_rootRetain function in runtime/objc-arr.mm

参考:

Where is the retain count stored for NSObjects in Objective C


简要说一下 @autoreleasePool 的数据结构

1
2
3
4
5
6
7
8
9
AutoreleasePoolPage {
magic_t const magic;
id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
}

magic用来校验AutoreleasePoolPage结构是否完整;

next指向第一个可用的地址;

thread指向当前的线程;

parent指向父类

child指向子类

参考:

深入理解@autoreleasepool


__weak_Unsafe_Unretain 的区别

weak与unsafe_unretained的区别在于,weak会将被释放指针赋值为nil,而unsafe_unretained则会成为野指针。


为什么已经有了 ARC ,但还是需要 @AutoreleasePool 的存在?

ARC 并不是舍弃了 @autoreleasepool,而是在编译阶段帮你插入必要的 retain/release/autorelease 的代码调用。

所以,跟你想象的不一样,ARC 之下依然是延时释放的,依然是依赖于 NSAutoreleasePool,跟非 ARC 模式下手动调用那些函数本质上毫无差别,只是编译器来做会保证引用计数的正确性。


__weak 属性修饰的变量,如何实现在变量没有强引用后自动置为 nil

runtime 对注册的类, 会进行布局,对于 weak 对象会放入一个 hash 表中。 用 weak 指向的对象内存地址作为 key,当此对象的引用计数为0的时候会 dealloc,假如 weak 指向的对象内存地址是a,那么就会以a为键, 在这个 weak 表中搜索,找到所有以a为键的 weak 对象,从而设置为 nil。


说一下对 retain,copy,assign,weak,_Unsafe_Unretain 关键字的理解

assign表明 setter 仅仅是一个简单的赋值操作,通常用于基本的数值类型,例如CGFloat和NSInteger。

weak表明属性定义了一个非拥有者关系。当给属性设定一个新值的时候,这个值不会进行 retain,旧值也不会进行 release, 而是进行类似 assign 的操作。不过当属性指向的对象被销毁时,该属性会被置为nil。

strong表明属性定义一个拥有者关系。当给属性设定一个新值的时候,首先这个值进行 retain ,旧值进行 release ,然后进行赋值操作。

copy类似于 strong,不过在赋值时进行 copy 操作而不是 retain 操作。通常在需要保留某个不可变对象(NSString最常见),并且防止它被意外改变时使用。

unsafe_unretained的语义和 assign 类似,不过是用于对象类型的,表示一个非拥有(unretained)的,同时也不会在对象被销毁时置为nil的(unsafe)关系。

参考:

Objective-C 内存管理——你需要知道的一切

NSString属性什么时候用copy,什么时候用strong?


ARC 在编译时做了哪些工作

根据代码执行的上下文语境,在适当的位置插入 retainrelease


ARC 在运行时做了哪些工作

待查~


函数返回一个对象时,会对对象 autorelease 么?为什么?

参考 arc 内存管理要点 这篇。


说一下什么是 悬垂指针?什么是 野指针?

悬垂指针(dangling pointer)一般是说指向已经被释放的自由区内存(free store)的指针,野指针(wild pointer)则一般是未经初始化的指针。前者曾经有效过,后者从未有效过。

参考:

悬垂指针(Dangling pointer)和野指针(Wild pointer)


内存管理默认的关键字是什么?

strong


内存中的5大区分别是什么?

内存5大区:堆,栈,方法区,全局区,常量区
栈:不需要手动管理内存,会自动清理栈中的内存
堆: 需要手动管理内存
静态区:又称全局区
常量区: 储存常量的地方
方法区: 存放函数体的二进制代码


是否了解 深拷贝 和 浅拷贝 的概念,集合类深拷贝如何实现?

深拷贝是内存拷贝,指向的是不同的内存
浅拷贝是指针拷贝,指向的是同一块内存

集合类深拷贝是通过归档、解档实现。


BAD_ACCESS 在什么情况下出现?

  • 访问了野指针,比如对一个已经释放的对象执行了release、访问已经释放对象的成员变量或者发消息。
  • 死循环

0x02 Runtime

类对象的数据结构?

Objective-C类是由Class类型来表示的,它实际上是一个指向objc_class结构体的指针。它的定义如下:

1
typedef struct objc_class *Class;

查看objc/runtime.hobjc_class结构体的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE; // 父类
const char *name OBJC2_UNAVAILABLE; // 类名
long version OBJC2_UNAVAILABLE; // 类的版本信息,默认为0
long info OBJC2_UNAVAILABLE; // 类信息,供运行期使用的一些位标识
long instance_size OBJC2_UNAVAILABLE; // 该类的实例变量大小
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; // 该类的成员变量链表
struct objc_method_list **methodLists OBJC2_UNAVAILABLE; // 方法定义的链表
struct objc_cache *cache OBJC2_UNAVAILABLE; // 方法缓存
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 协议链表
#endif
} OBJC2_UNAVAILABLE;

在这个定义中,下面几个字段是我们感兴趣的

  1. isa:需要注意的是在Objective-C中,所有的类自身也是一个对象,这个对象的Class里面也有一个isa指针,它指向metaClass(元类),我们会在后面介绍它。
  2. super_class:指向该类的父类,如果该类已经是最顶层的根类(如NSObject或NSProxy),则super_class为NULL。
  3. cache:用于缓存最近使用的方法。一个接收者对象接收到一个消息时,它会根据isa指针去查找能够响应这个消息的对象。在实际使用中,这个对象只有一部分方法是常用的,很多方法其实很少用或者根本用不上。这种情况下,如果每次消息来时,我们都是methodLists中遍历一遍,性能势必很差。这时,cache就派上用场了。在我们每次调用过一个方法后,这个方法就会被缓存到cache列表中,下次调用的时候runtime就会优先去cache中查找,如果cache没有,才去methodLists中查找方法。这样,对于那些经常用到的方法的调用,但提高了调用的效率。
  4. version:我们可以使用这个字段来提供类的版本信息。这对于对象的序列化非常有用,它可是让我们识别出不同类定义版本中实例变量布局的改变。

参考:

Objective-C Runtime 运行时之一:类与对象


实例对象的数据结构?

实例对象都是一个id类型的对象,查看objc.h中对id的描述:

1
2
3
4
5
6
7
8
9
10
11
12
#if !OBJC_TYPES_DEFINED
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;
#endif

查看源文件,可以看出id其实就是一个指向objc_object结构体指针,它包含一个Class isa 成员。所以实例对象的数据结构实际上就是一个Class的数据结构。


元类对象的数据结构?

meta-class 是 Class 对象的类。每个 Class 都有个不同的自己的 meta-class(因此每个 Class 都可以有一个自己不同的方法列表)。也就是说每个类的 Class 不完全相同。

参考:

Objective-C 中的 Meta-class 是什么?


Category 的实现原理?以及如何实现添加一个属性?

参考:

从源码解读Category实现原理

深入理解Objective-C:Category


如何运用 Runtime 字典转模型?

参考:

利用运行时(runtime)字典转模型

简单的版本如上,其他还有一些关键字的不能作为key值得还要映射一下的。还有嵌套的等等其他的要考虑。


Category 有哪些用途?

1)无需创建继承类,实现对已有类扩展。并且可以被扩展的类的所有子类自动继承。

2)可以用来修复没有源码类的bug。

3)对于一个类多个开发人员维护的情况,可以根据不同用途创建不同分类。

注意点:

1)不能在分类中重写系统方法,因为会把系统的功能给覆盖掉,而且分类中不能调用super。但是,这种情况可以用来修复,没有源码的类中方法有Bug的情况。


CategoryExtension 有什么区别?

1)Category 的加载在运行时Extension 的加载在编译时

2) Extension 不能给没有源码的类添加方法。

3) Extension 是一个匿名的 Category


说一下 Method Swizzling? 说一下在实际开发中你在什么场景下使用过?

参考:

Method Swizzling

关于Method Swizzling踩过的坑,

参考这里

Method Swizzling


如何实现动态添加方法和属性?

1
2
3
4
5
6
7
8
9
10
11
//动态创建类
Class person = objc_allocateClassPair([NSObject class], "Person", 0);
//添加变量
class_addIvar(person, "name", sizeof(NSString *), 0, "@");
//添加函数 sayHi
class_addMethod(person, @selector(sayHi:), (IMP)sayHi, "v@:");
objc_registerClassPair(person);
//初始化一个person对象
id Tom = [[person alloc] init];
[Tom setValue:@"Tom" forKey:@"name"];
[Tom sayHi:@"Jeck"];

上述代码,我们通过objc_allocateClassPair()创建了一个继承NSObjectperson的子类,然后通过objc_registerClassPair()这个函数注册了person类,下面我们就可以使用这个类了,使用之前我们在给person类中添加一个name属性和sayHi:方法,分别通过class_addIvar()class_addMethod()来添加,接下来我们要实现我们添加的sayHi:方法,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
//这个函数必须写,要不然xcode会报错,实际运行的时候,这个函数是不会调用的
-(void)sayHi:(NSString *)name
{

}

//运行的时候,会调用这个方法
static void sayHi(id self, SEL _cmd, NSString *name)
{
Ivar n = class_getInstanceVariable([self class], "name");
id a = object_getIvar(self, n);
NSLog(@"hello %@,my name is %@",name,a);
}

static void sayHi(id self, SEL _cmd, NSString *name)这个就是我们运行时候调用的方法,其中self使我们使用person创建的对象Tom,_cmd是调用的方法名,name就是传过来的参数,如果有多个参数,可以写成static void sayHi(id self, SEL _cmd, NSString *name,...)省略号可以填写你愿意添加的参数。

这样我们就利用runtime动态的创建了一个person类,包括了name属性和sayHi:方法,运行如结果如下:

1
RunTimeDemo[46329:7242266] hello Jeck,my name is Tom

参考:

runtime常用方法


说一下对 isa 指针的理解, 对象的isa 指针指向哪里?(注意区分不同对象)

网上有2种说法:

1)isa 是指向元类的指针,不了解元类的可以看 Objective-C 中的 Meta-class 是什么?

2) 任何直接或间接继承了NSObject的类,它的实例对象(instacne objec)中都有一个isa指针,指向它的类对象(class object)。这个类对象(class object)中存储了关于这个实例对象(instace object)所属的类的定义的一切:包括变量方法遵守的协议等等。

参考:

Objective-C内存布局

Objective-C 中的 Meta-class 是什么?


Obj-C 中的类信息存放在哪里?

类方法存储在元类。

当前类的信息都在 data 里。看看源码就知道了。


一个 NSObject 对象占用多少内存空间?

通过 size_t class_getInstanceSize ( Class cls )接口可以获取NSObject 对象占用内存空间大小。

1
2
3
4
5
6
7
size_t z = class_getInstanceSize([NSObject class]);

//输出
(lldb) p z
(size_t) $0 = 8

单位是 byte

说一下对 class_rw_t 的理解?

rw代表可读可写。

ObjC 类中的属性、方法还有遵循的协议等信息都保存在 class_rw_t 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct class_rw_t {  
uint32_t flags;
uint32_t version;

const class_ro_t *ro;

method_array_t methods;
property_array_t properties;
protocol_array_t protocols;

Class firstSubclass;
Class nextSiblingClass;
};

其中还有一个指向常量的指针 ro,其中存储了当前类在编译期就已经确定的属性、方法以及遵循的协议

参考:

深入解析 Objective-C 中方法的结构


说一下对 class_ro_t 的理解?

储了当前类在编译期就已经确定的属性、方法以及遵循的协议。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct class_ro_t {  
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
uint32_t reserved;

const uint8_t * ivarLayout;

const char * name;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;

const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
};

参考:

深入解析 Objective-C 中方法的结构


说一下 Runtime 消息解析和转发

先看一下流程图:

在 Object-C 中调用方法最终都会翻译成调用方法实现的的函数指针,并传递给它一个对象指针一个选择器一组函数参数

objc_msgSend 的工作方式如下:

  1. 检查接受对象是否为nil 。如果是,调用nil处理程序。
  2. 在垃圾收集环境中(ios 不支持,写在这里是为了内容的完整性),检查有没有锻炼选择器(retain、release、autorelease、retainCount),如果有,返回 self。是的,这意味着在垃圾收集环境中 retainCount 会返回 self,不过你应该用不到。
  3. 检查类缓存中是不是已经有方法实现了,有的话,直接调用。
  4. 比较请求的选择器和类定义的选择器,如果找到了,调用方法实现。
  5. 比较请求的选择器和父类中定义的选择器,然后是父类的父类,以此类推。如果找到选择器,调用方法实现。
  6. 调用 resolveInstanceMethod: ( 或者 resolveClassMethod: )。如果它返回 YES ,那么重新开始。这一次对象会相应这个选择器,一般是因为它已经调用过 class_addMethod
  7. 调用 forwardingTargetForSelector:,如果返回非 nil,那就把消息发送到返回的对象上。这里不要返回 self,否则会形成死循环
  8. 调用 methodSignatureForSelector:,如果返回一个非 nil,创建一个 NSInvocation 并传给 forwardInvocation
  9. 调用 doesNotRecognizeSelector:,默认是抛出异常。

例子🌰

Xcode何时会报unrecognized selector的错误

1
2
3
4
5
6
- (void)viewDidLoad {
[super viewDidLoad];

AAPerson *person = [[Person alloc] init];
[person test];
}

当向AAPerson发送test这个消息时,runtime会根据对象的isa指针找到该对象实际所属的类,然后在该类的方法列表以及父类的方法列表里面找相应的方法运行,如果在最顶层的父类中依然找不到相应的方法实现时,程序在运行时就会报unrecognized selector sent to的错误并且崩溃,但是在此之前,objc的运行时给出了三次避免程序崩溃的机会。

1、Method resolution

objc运行时会调用+resolveInstanceMethod:或者+resolveClassMethod:,让我们有机会提供一个函数实现而不导致程序崩溃,如果在这里面添加了函数,系统就会重新启动一次消息发送的过程,否则就会进入到消息的快速转发流程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == NSSelectorFromString(@"test")) {
/**
class: 给哪个类添加方法
SEL: 添加哪个方法
IMP: 方法实现 => 函数 => 函数入口 => 函数名
type: 方法类型:void用v来表示,id参数用@来表示,SEL用:来表示
*/
class_addMethod(self, sel, (IMP)test, "v@:@");
return YES;
}else {
return [super resolveClassMethod:sel];
}
}

void test(id self, SEL _cmd, NSNumber *meter) {
NSLog(@"测试 - AAPerson");
}

2、Fast forwarding

如果目标对象实现了-forwardingTargetForSelector:的方法,runtime就会调用这个方法,给我们一个机会把这个消息转发给其他的对象,只要这个方法返回值不是nil和self,整个消息发送的过程就会被重启,这时发送的对象会变成我们返回的这个对象,否则就会移到下一步。

1
2
3
4
5
6
7
8
- (id)forwardingTargetForSelector:(SEL)aSelector {
AATarget *target = [[AATarget alloc] init];
if ([target respondsToSelector:aSelector]) {
return target; // 就会去调用AATarget里面的test方法
}else {
return [super forwardingTargetForSelector: aSelector];
}
}

3、Normal Fowarding

如果上面两种方法都没有被实现的话,就会来到第三步 —— 普通转发,这是runtime给我们最后一次避免崩溃的机会,首先它会-methodSignatureForSelector:来获得函数的参数和返回值类型,如果返回值为nil,则runtime会发出-doesNotRecognizeSelector: 的消息,程序崩溃。如果返回了一个函数签名,runtime会创建一个NSInvocation对象并发送-forwardInvocation:的消息给目标对象。

1
2
3
4
5
6
7
8
9
10
11
12
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
return signature;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL selector = [anInvocation selector];// anInvocation里面保存的是selector/target/参数
AATarget *target = [[WWTarget alloc] init];
if ([target respondsToSelector:selector]) {
[anInvocation invokeWithTarget:target];
}
}

如果上面的三步都没有实现的话,就会调用-doesNotRecognizeSelector:,程序崩溃。


如何运用 Runtime 字典转模型?

Runtime 遍历 ivar_list,结合 KVC 赋值。

Note:

对于特殊的(如id)或者自定义的属性和字典中的key不一样的时候会报错,这时候需要重写- (void)setValue:(id)value forUndefinedKey:(NSString *)key方法。

参考:

利用Runtime实现简单的字典转模型


如何运用 Runtime 进行模型的归解档?

Runtime 遍历 ivar_list

参考:

iOS - runtime快速归解档


在 Obj-C 中为什么叫发消息而不叫函数调用?

结合 objc_msgsend 讲一下,接收消息的过程。


说一下 Runtime 的方法缓存?存储的形式、数据结构以及查找的过程?

cache_t增量扩展的哈希表结构。哈希表内部存储的 bucket_t

  • 如果是有序方法列表,采用二分查找

  • 如果是无序方法列表,直接遍历查找


0x03 RunLoop

Runloop 和线程的关系

  • 一个线程对应一个 Runloop

  • 主线程的默认就有了 Runloop

  • 子线程的 Runloop 以懒加载的形式创建。

  • Runloop 存储在一个全局的可变字典里,线程是 keyRunloopvalue


讲一下 Runloop 的 Mode?(越详细越好)

苹果文档中提到的 Mode 有五个,分别是:

  • NSDefaultRunLoopMode
  • NSConnectionReplyMode
  • NSModalPanelRunLoopMode
  • NSEventTrackingRunLoopMode
  • NSRunLoopCommonModes

iOS 中公开暴露出来的只有 NSDefaultRunLoopModeNSRunLoopCommonModesNSRunLoopCommonModes 实际上是一个 Mode 的集合,默认包括 NSDefaultRunLoopModeNSEventTrackingRunLoopMode

参考:

Runloop · 笔试面试知识整理


讲一下 Observer ?(Mode中的重点)

1
2
3
4
5
6
7
8
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Loop
};

讲一下 Runloop 的内部实现逻辑?(运行过程)

参考:

深入理解RunLoop


你所知的哪些三方框架使用了 Runloop?(AFNetworking、Texture 等)


解释一下 事件响应 的过程

苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()

当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。这个过程的详细情况可以参考这里。SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的 App 进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。

_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。

详细参考(推荐阅读):

iOS 点击事件传递及响应


解释一下 手势识别 的过程?

当上面的 _UIApplicationHandleEventQueue()识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。

苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个 Observer 的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer 的回调。

当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。


解释一下 GCD 在 Runloop 中的使用?

1、RunLoop 的超时时间

这是 RunLoop的 API:

1
2
3
4
//mode默认为defaultMode、超时时间是100亿秒、false
void CFRunLoopRun(void)
// 可以设置mode、runloop 超时时间、是否处理完source立刻返回
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled)

RunLoop 的超时时间就是使用 GCD 中的 dispatch_source_t来实现的。

2、执行GCD MainQueue上的异步任务,即:dispatch_async(dispatch_get_main_queue(), block)产生的任务。

参考:

RunLoop总结:RunLoop 与GCD 、Autorelease Pool之间的关系


解释一下 NSTimer,以及 NSTimer 的循环引用

NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop 为了节省资源,并不会在非常准确的时间点回调这个TimerTimer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。

如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。

引起 NSTimer 循环引用的主要是捕获了self了,如下:

1
2
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 
target:self selector:@selector(timerAction:) userInfo:nil repeats:true];

就算是将上面的self改为weakSelf也没有用,最好的解决办法是用GCDdispatch_source解决。

如下:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#import "RNTimer.h"

@interface RNTimer ()
@property (nonatomic, readwrite, copy) dispatch_block_t block;
@property (nonatomic, readwrite, assign) dispatch_source_t source;
@end

@implementation RNTimer
@synthesize block = _block;
@synthesize source = _source;

+ (RNTimer *)repeatingTimerWithTimeInterval:(NSTimeInterval)seconds
block:(void (^)(void))block {
NSParameterAssert(seconds);
NSParameterAssert(block);

RNTimer *timer = [[self alloc] init];
timer.block = block;
timer.source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,
0, 0,
dispatch_get_main_queue());
uint64_t nsec = (uint64_t)(seconds * NSEC_PER_SEC);
dispatch_source_set_timer(timer.source,
dispatch_time(DISPATCH_TIME_NOW, nsec),
nsec, 0);
dispatch_source_set_event_handler(timer.source, block);
dispatch_resume(timer.source);
return timer;
}

- (void)invalidate {
if (self.source) {
dispatch_source_cancel(self.source);
dispatch_release(self.source);
self.source = nil;
}
self.block = nil;
}

- (void)dealloc {
[self invalidate];
}

- (void)fire {
self.block();
}

参考链接:

iOS | 小心NSTimer中的内存泄漏

RNTimer

拓展:

NSTimer 失效

从RunLoop源码探索NSTimer的实现原理


AFNetworking中如何运用Runloop?
AFURLConnectionOperation 这个类是基于 NSURLConnection 构建的,其希望能在后台线程接收 Delegate 回调。为此 AFNetworking 单独创建了一个线程,并在这个线程中启动了一个 RunLoop

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}

+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}

RunLoop 启动前内部必须要有至少一个 Timer/Observer/Source,所以 AFNetworking[runLoop run] 之前先创建了一个新的NSMachPort 添加进去了。通常情况下,调用者需要持有这个 NSMachPort (mach_port) 并在外部线程通过这个 port 发送消息到 loop 内;但此处添加 port 只是为了让 RunLoop 不至于退出,并没有用于实际的发送消息。

1
2
3
4
5
6
7
8
9
10
- (void)start {
[self.lock lock];
if ([self isCancelled]) {
[self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
} else if ([self isReady]) {
self.state = AFOperationExecutingState;
[self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
}
[self.lock unlock];
}

当需要这个后台线程执行任务时,AFNetworking 通过调用 [NSObject performSelector:onThread:..] 将这个任务扔到了后台线程的 RunLoop 中。


PerformSelector 的实现原理

当调用 NSObjectperformSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。

当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。

其他的performSelector系列方法是类似的

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
- (void)viewDidLoad {
[super viewDidLoad];

[self tryPerformSelectorOnMianThread];

__weak typeof(self) weakSelf = self;
//backGroundThread并不会被调用到,这是因为,在调用performSelector:onThread: withObject: waitUntilDone的时候,系统会给我们创建一个Timer的source,加到对应的RunLoop上去,然而这个时候我们没有RunLoop
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[weakSelf tryPerformSelectorOnbackGroundThread1];
});

//在tryPerformSelectorOnbackGroundThread1上我们添加个RunLoop,此时backGroundThread2就可以被调用到
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[weakSelf tryPerformSelectorOnbackGroundThread2];
});

//那么为什么主线程中的perfom selector却能够正常调用呢?通过上面的例子相信你已经猜到了,主线程的RunLoop是一直存在的,所以我们在主线程中执行的时候,无需再添加RunLoop。
//小结:当perform selector在后台线程中执行的时候,这个线程必须有一个开启的runLoop
}

- (void)tryPerformSelectorOnMianThread{
[self performSelector:@selector(mainThreadMethod) withObject:nil];
}

- (void)tryPerformSelectorOnbackGroundThread1{
//backGroundThread并不会被调用到,这是因为,在调用performSelector:onThread: withObject: waitUntilDone的时候,系统会给我们创建一个Timer的source,加到对应的RunLoop上去,然而这个时候我们没有RunLoop
[self performSelector:@selector(backGroundThread1) onThread:[NSThread currentThread] withObject:nil waitUntilDone:NO];
//默认是会在MainRunLoop中去执行,故他可以调用到backGroundThread
// [self performSelector:@selector(backGroundThread1) withObject:nil];
}

- (void)tryPerformSelectorOnbackGroundThread2{
//在tryPerformSelectorOnbackGroundThread1上我们添加个RunLoop,此时backGroundThread2就可以被调用到
[self performSelector:@selector(backGroundThread2) onThread:[NSThread currentThread] withObject:nil waitUntilDone:NO];
[[NSRunLoop currentRunLoop] run];
}

- (void)mainThreadMethod{
NSLog(@"execute %s",__func__);
}

- (void)backGroundThread1{
NSLog(@"currentThread:%@",[NSThread currentThread]);
NSLog(@"backGroundThread1 --> execute %s",__FUNCTION__);
}

- (void)backGroundThread2{
NSLog(@"currentThread:%@",[NSThread currentThread]);
NSLog(@"backGroundThread2 --> execute %s",__FUNCTION__);
}

执行结果:

1
2
3
2018-05-16 11:38:20.939516+0800 Test[6410:1215068] execute -[ViewController mainThreadMethod]
2018-05-16 11:38:20.940078+0800 Test[6410:1215134] currentThread:<NSThread: 0x600000470bc0>{number = 4, name = (null)}
2018-05-16 11:38:20.940309+0800 Test[6410:1215134] backGroundThread2 --> execute -[ViewController backGroundThread2]