@(iOS开发学习)[温故而知新,掘金博客]html
[TOC]ios
Person类用于打印对象的释放时机git
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
@property (nonatomic, strong) NSString* name;
@end
NS_ASSUME_NONNULL_END
@implementation Person
- (void)dealloc {
NSLog(@"func = %s, name = %@", __func__, self.name);
}
@end
复制代码
#import <UIKit/UIKit.h>
#import "Person.h"
NS_ASSUME_NONNULL_BEGIN
@interface AutoreleasePoolWithOutVC : UIViewController
@end
NS_ASSUME_NONNULL_END
@interface AutoreleasePoolWithOutVC ()
@property (nonatomic, strong) Person* zhangSanStrong;
@property (nonatomic, weak) Person* zhangSanWeak;
@end
@implementation AutoreleasePoolWithOutVC
- (void)viewDidLoad {
[super viewDidLoad];
Person *xiaoMing = [[Person alloc] init];
xiaoMing.name = @"xiaoMing";
_zhangSanStrong = [[Person alloc] init];
_zhangSanStrong.name = @"zhangSanStrong";
Person *zhangSanWeak = [[Person alloc] init];
zhangSanWeak.name = @"zhangSanWeak";
_zhangSanWeak = zhangSanWeak;
NSLog(@"func = %s, xiaoMing = %@", __func__, xiaoMing);
}
- (void)viewWillAppear:(BOOL)animated {
NSLog(@"func = %s", __func__);
}
- (void)viewDidAppear:(BOOL)animated {
NSLog(@"func = %s", __func__);
}
@end
复制代码
运行结果: 栈中建立的临时对象xiaoMing和weak属性修饰的对象 _zhangSanWeak ,在viewDidLoad
结束后就被释放了。 github
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface AutoreleasePoolManualWithVC : UIViewController
@end
NS_ASSUME_NONNULL_END
#import "AutoreleasePoolManualWithVC.h"
#import "Person.h"
@interface AutoreleasePoolManualWithVC ()
@property (nonatomic, strong) Person* zhangSanStrong;
@property (nonatomic, weak) Person* zhangSanWeak;
@end
@implementation AutoreleasePoolManualWithVC
- (void)viewDidLoad {
[super viewDidLoad];
@autoreleasepool {
Person *xiaoMing = [[Person alloc] init];
xiaoMing.name = @"xiaoMing";
_zhangSanStrong = [[Person alloc] init];
_zhangSanStrong.name = @"zhangSanStrong";
Person *zhangSanWeak = [[Person alloc] init];
zhangSanWeak.name = @"zhangSanWeak";
_zhangSanWeak = zhangSanWeak;
}
NSLog(@"func = %s", __func__);
}
- (void)viewWillAppear:(BOOL)animated {
NSLog(@"func = %s", __func__);
}
- (void)viewDidAppear:(BOOL)animated {
NSLog(@"func = %s", __func__);
}
@end
复制代码
运行结果: 栈中建立的临时对象xiaoMing和weak属性修饰的对象 _zhangSanWeak,在viewDidLoad
结束以前就被释放了。 面试
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface AutoreleasePoolSystermWithVC : UIViewController
@end
NS_ASSUME_NONNULL_END
#import "AutoreleasePoolSystermWithVC.h"
@interface AutoreleasePoolSystermWithVC ()
@property (nonatomic, strong) NSString* zhangSanStrong;
@property (nonatomic, weak) NSString* zhangSanWeak;
@end
@implementation AutoreleasePoolSystermWithVC
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"func = %s start", __func__);
_zhangSanStrong = [NSString stringWithFormat:@"zhangSanStrong"];
NSString* zhangSanWeak = [NSString stringWithFormat:@"zhangSanStrong"];
_zhangSanWeak = zhangSanWeak;
[self printInfo];
NSLog(@"func = %s end", __func__);
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
NSLog(@"func = %s start", __func__);
[self printInfo];
NSLog(@"func = %s end", __func__);
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NSLog(@"func = %s start", __func__);
[self printInfo];
NSLog(@"func = %s end", __func__);
}
- (void)printInfo {
NSLog(@"self.zhangSanStrong = %@", _zhangSanStrong);
NSLog(@"self.zhangSanWeak = %@", _zhangSanWeak);
}
@end
复制代码
运行结果: 系统在每一个Runloop
迭代中都加入了AutoreleasePool
,Runloop
开始后建立AutoreleasePool并Autorelease对象
加入到pool中,Runloop
结束后或者休眠的时候Autorelease对象
被释放掉。 objective-c
Tagged Pointer
)对象被加入到系统的AutoreleasePool中看了别人的博客后,决定手动验证一下,又不想彻底copy别人的代码,本身仿写初始化的时候又懒得写太多内容,索性写了@“1”,因此致使结果与博客不一致,所以更加怀疑人生。我想不止我一个要这种状况😄。编程
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface AutoreleasePoolSystermWithTaggedPointerVC : UIViewController
@end
NS_ASSUME_NONNULL_END
#import "AutoreleasePoolSystermWithTaggedPointerVC.h"
@interface AutoreleasePoolSystermWithTaggedPointerVC ()
@property (nonatomic, weak) NSString* tagged_yes_1; // 是Tagged Pointer
@property (nonatomic, weak) NSString* tagged_yes_2; // 是Tagged Pointer
@property (nonatomic, weak) NSString* tagged_yes_3; // 是Tagged Pointer
@property (nonatomic, weak) NSString* tagged_no_1; // 非Tagged Pointer
@property (nonatomic, weak) NSString* tagged_no_2; // 非Tagged Pointer
@property (nonatomic, weak) NSString* tagged_no_3; // 非Tagged Pointer
@end
@implementation AutoreleasePoolSystermWithTaggedPointerVC
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"func = %s start", __func__);
NSString* tagged_yes_1_str = [NSString stringWithFormat:@"1"];
_tagged_yes_1 = tagged_yes_1_str;
NSString* tagged_yes_2_str = [NSString stringWithFormat:@"123456789"];
_tagged_yes_2 = tagged_yes_2_str;
NSString* tagged_yes_3_str = [NSString stringWithFormat:@"abcdefghi"];
_tagged_yes_3 = tagged_yes_3_str;
NSString* tagged_no_1_str = [NSString stringWithFormat:@"0123456789"];
_tagged_no_1 = tagged_no_1_str;
NSString* tagged_no_2_str = [NSString stringWithFormat:@"abcdefghij"];
_tagged_no_2 = tagged_no_2_str;
NSString* tagged_no_3_str = [NSString stringWithFormat:@"汉字"];
_tagged_no_3 = tagged_no_3_str;
[self printInfo];
NSLog(@"func = %s end", __func__);
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
NSLog(@"func = %s start", __func__);
[self printInfo];
NSLog(@"func = %s end", __func__);
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NSLog(@"func = %s start", __func__);
[self printInfo];
NSLog(@"func = %s end", __func__);
}
- (void)printInfo {
NSLog(@"self.tagged_yes_1 = %@", _tagged_yes_1);
NSLog(@"self.tagged_yes_2 = %@", _tagged_yes_2);
NSLog(@"self.tagged_yes_3 = %@", _tagged_yes_3);
NSLog(@"self.tagged_no_1 = %@", _tagged_no_1);
NSLog(@"self.tagged_no_2 = %@", _tagged_no_2);
NSLog(@"self.tagged_no_3 = %@", _tagged_no_3);
}
@end
复制代码
运行结果:bootstrap
Tagged Pointer
类型的Autorelease对象
,系统不会释放Tagged Pointer
类型的Autorelease对象
,系统会在当前Runloop
结束后释放
在ARC环境下,以alloc
/new
/copy
/mutableCopy
开头的方法返回值取得的对象是本身生成而且持有的,其余状况是非本身持有的对象,此时对象的持有者就是AutoreleasePool
。swift
当咱们使用@autoreleasepool{}
时,编译器会将其转换成如下形式缓存
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
}
复制代码
__AtAutoreleasePool
定义以下:
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
.
.
// 将中间对象压入栈中(atautoreleasepoolobj也是一个对象,至关于哨兵,表明一个 autoreleasepool 的边界,
// 与当前的AutoreleasePool对应,pop的时候用来标记终点位置,被当前的AutoreleasePool第一个压入栈中,
// 出栈的时候最后一个被弹出)
.
.
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
复制代码
建立时调用了objc_autoreleasePoolPush()
方法,而释放时调用objc_autoreleasePoolPop()
方法,只是一层简单的封装。
AutoreleasePool
并无单独的结构,本质上是一个双向链表,结点是AutoreleasePoolPage
对象。每一个结点的大小是4KB
(4*1024=4096字节),除去实例变量的大小,剩余的空间用来存储Autorelease对象的地址
。
class AutoreleasePoolPage {
magic_t const magic; // 用于校验AutoreleasePage的完整性
id *next; // 指向栈顶最后push进来的Autorelease对象的下一个位置
pthread_t const thread; // 保存了当前页所在的线程,每个 autoreleasepool 只对应一个线程
AutoreleasePoolPage * const parent; // 双向链表中指向上一个节点,第一个结点的 parent 值为 nil
AutoreleasePoolPage *child; // 双向链表中指向下一个节点,最后一个结点的 child 值为 nil
uint32_t const depth; // 深度,从0开始,日后递增1
uint32_t hiwat; // high water mark
};
复制代码
next指针指向将要添加新对象(Autorelease对象、哨兵对象)的空闲位置。
![]()
当调用AutoreleasePoolPage::push()
方法时,首先向当前的page(hotPage)结点next指针指向的位置添加一个哨兵对象(POOL_SENTINEL
,值为nil)。若是后面嵌套着AutoreleasePool
则继续添加哨兵对象,不然将Autorelease对象压入哨兵对象的上面。向高地址移动next指针,直到 next == end()
时,表示当前page已满。当 next == begin()
时,表示 AutoreleasePoolPage 为空;当 next == end()
时,表示 AutoreleasePoolPage 已满。
hotPage
而且当前 page
不满。调用 page->add(obj)
方法将对象添加至 AutoreleasePoolPage
的栈中hotPage
而且当前 page
已满。调用 autoreleaseFullPage
初始化一个新的页,调用 page->add(obj)
方法将对象添加至 AutoreleasePoolPage
的栈中hotPage
。调用 autoreleaseNoPage
建立一个 hotPage
,调用 page->add(obj)
方法将对象添加至 AutoreleasePoolPage
的栈中。objc_autoreleasePoolPop
执行过程当调用AutoreleasePoolPage::pop()
的方法时,pop 函数的入参就是 push 函数的返回值,也就是 POOL_SENTINEL
的内存地址,即 pool token
。当执行 pop 操做时,根据传入的哨兵对象地址找到哨兵对象所处的page,将晚于(上面的)哨兵对象压入的Autorelease
对象进行release。即内存地址在 pool token 以后的全部 Autoreleased 对象
都会被 release 。直到 pool token 所在 page 的 next 指向 pool token
为止。并向回移动next指针到正确位置。
没有手动加AutoreleasePool
的状况下,Autorelease对象
是在当前的Runloop
迭代结束的时候释放的。手动添加的Autorelease对象
也是自动计数的,当引用计数为0的时候,被释放掉。
实际验证以前,首先了解几个私有API,查看自动释放池的状态,在ARC下查看对象的引用计数
//先声明私有的API
extern void _objc_autoreleasePoolPrint(void);
extern uintptr_t _objc_rootRetainCount(id obj);
_objc_autoreleasePoolPrint(); //调用 打印自动释放池里的对象
_objc_rootRetainCount(obj); //调用 查看对象的引用计数
复制代码
根据苹果官方文档中对 NSRunLoop 的描述,咱们能够知道每个线程,包括主线程,都会拥有一个专属的 NSRunLoop 对象,而且会在有须要的时候自动建立。一样的,根据苹果官方文档中对 NSAutoreleasePool 的描述,咱们可知,在主线程的 NSRunLoop 对象(在系统级别的其余线程中应该也是如此,好比经过 dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) 获取到的线程)的每一个 event loop 开始前,系统会自动建立一个 autoreleasepool ,并在 event loop 结束时 drain 。
添加打印Runloop的代码:NSLog(@"[NSRunLoop currentRunLoop] = %@", [NSRunLoop currentRunLoop]);
打印出的日志部分截图以下。
_wrapRunLoopWithAutoreleasePoolHandler()
。
第一个 Observer 监视的事件是 Entry
(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush()
建立自动释放池。其 order 是-2147483647,优先级最高,保证建立释放池发生在其余全部回调以前。
第二个 Observer 监视了两个事件: BeforeWaiting
(准备进入休眠) 时调用_objc_autoreleasePoolPop()
和 _objc_autoreleasePoolPush()
释放旧的池并建立新池;Exit
(即将退出Loop) 时调用 _objc_autoreleasePoolPop()
来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其余全部回调以后。
在主线程执行的代码,一般是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 建立好的 AutoreleasePool 环绕着,因此不会出现内存泄漏,开发者也没必要显示建立 Pool 了。
根据 Apple的文档 ,使用场景以下:
Tagged Pointer是一个可以提高性能、节省内存的有趣的技术。在OS X 10.10中,NSString就采用了这项技术,如今让咱们来看看该技术的实现过程。
一、进入pool和出pool的时候,引用计数的变化? 二、为何采用双向链表,而不是单向链表? 三、你说到了哨兵,在链表中怎么使用哨兵能够简化编程?
自动释放池的前世此生 ---- 深刻解析 autoreleasepool
iOS开发 自动释放池(Autorelease Pool)和RunLoop
Objective-C Autorelease Pool 的实现原理
Objective-C高级编程(一) 自动引用计数,看我就够了
Block
表面上是一个带自动变量(局部变量)的匿名函数。本质上是对闭包的对象实现,简单来讲Block
就是一个结构体对象。在ARC
下,大多数状况下Block
从栈上复制到堆上的代码是由编译器实现的(Blcok
做为返回值或者参数)
#import <Foundation/Foundation.h>
typedef void(^blockVoid)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
blockVoid block = ^() {
NSLog(@"block");
};
block();
}
return 0;
}
复制代码
因为命令过长,咱们起一个别名genCpp_mac。 alias genCpp_mac='clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk'
struct __block_impl {
void *isa; // 指向所属类的指针,也就是block的类型(包含isa指针的皆为对象)
int Flags; // 标志变量,在实现block的内部操做时会用到
int Reserved; // 保留变量
void *FuncPtr; // block执行时调用的函数指针
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
// 显式的构造函数
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_x0_1sgmhpyx6535p2pfkfsbfvww0000gn_T_main_94a22e_mi_0);
}
// 纪录了block结构体大小等信息
static struct __main_block_desc_0 {
size_t reserved; // 保留字段
size_t Block_size; // block大小(sizeof(struct __main_block_impl_0))结构体大小须要保存是由于,每一个 block 由于会 capture 一些变量,这些变量会加到 __main_block_impl_0 这个结构体中,使其体积变大
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
/*
把多余的转换去掉,看起来就比较清楚了:
第一部分:block的初始化
参数一:__main_block_func_0(是block语法转换的C语言函数指针)
参数二:__main_block_desc_0_DATA(做为静态全局变量初始化的 __main_block_desc_0 结构体实例指针)
struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
struct __main_block_impl_0 *blk = &tmp;
第二部分:
block的执行: blk()
去掉转化部分:
(*blk -> imp.FuncPtr)(blk);
这就是简单地使用函数指针调用函数。由Block语法转换的 __main_block_func_0 函数的指针被赋值成员变量FuncPtr中,另外 __main_block_func_0的函数的参数 __cself 指向Block的值,经过源码能够看出 Block 正式做为参数进行传递的。
*/
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
blockVoid block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
复制代码
#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 {
uintptr_t reserved;
uintptr_t size;
};
#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
// requires BLOCK_HAS_COPY_DISPOSE
void (*copy)(void *dst, const void *src);
void (*dispose)(const void *);
};
#define BLOCK_DESCRIPTOR_3 1
struct Block_descriptor_3 {
// requires BLOCK_HAS_SIGNATURE
const char *signature;
const char *layout; // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};
struct Block_layout {
void *isa; // 全部对象都有该指针,用于实现对象相关的功能。
volatile int32_t flags; // contains ref count(block copy 的会对该变量操做)
int32_t reserved; // 保留变量
void (*invoke)(void *, ...); // 函数指针,指向具体的 block 实现的函数调用地址。
struct Block_descriptor_1 *descriptor; // 表示该 block 的附加描述信息,主要是 size 大小,以及 copy 和 dispose 函数的指针。
// imported variables // capture 过来的变量,block 可以访问它外部的局部变量,就是由于将这些变量(或变量的地址)复制到告终构体中。
};
复制代码
Block种类 | 存储区 | 拷贝效果 | 生命周期 |
---|---|---|---|
_NSConcreteStatckBlock | 栈 | 从栈拷贝到堆,往flags中并入BLOCK_NEEDS_FREE这个标志代表block须要释放,在release以及再次拷贝时会用到);若是有辅助拷贝函数,就调用,将捕获的变量从栈拷贝到堆中 | 出了做用域 |
_NSConcreteMallocBlock | 堆 | 单纯的引用计数加一 | 引用计数为0,runloop结束后释放 |
_NSConcreteGlobalBlock | 全局数据区 | 什么都不作,直接返回Blcok | app整个生命周期 |
__NSGlobalBlock__
类型的Block,无论是__strong
仍是__weak
修饰(不加默认__strong
)。__strong
修饰的是__NSMallocBlock__
类型,被__weak
修饰的是__NSStackBlock__
类型。注意: 访问外部变量的Block,在编译完成后其实都是
__NSStackBlock__
类型的,只是在ARC中被__strong
修饰的会在运行时被自动拷贝一份,最终调用_Block_copy_internal
函数,将isa由_NSConcreteStatckBlock
指向_NSConcreteMallocBlock
。
static void *_Block_copy_internal(const void *arg, const int flags) {
struct Block_layout *aBlock;
...
aBlock = (struct Block_layout *)arg;
...
// Its a stack block. Make a copy.
if (!isGC) {
// 申请block的堆内存
struct Block_layout *result = malloc(aBlock->descriptor->size);
if (!result) return (void *)0;
// 拷贝栈中block到刚申请的堆内存中
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
// reset refcount
result->flags &= ~(BLOCK_REFCOUNT_MASK); // XXX not needed
result->flags |= BLOCK_NEEDS_FREE | 1;
// 改变isa指向_NSConcreteMallocBlock,即堆block类型
result->isa = _NSConcreteMallocBlock;
if (result->flags & BLOCK_HAS_COPY_DISPOSE) {
//printf("calling block copy helper %p(%p, %p)...\n", aBlock->descriptor->copy, result, aBlock);
(*aBlock->descriptor->copy)(result, aBlock); // do fixup
}
return result;
}
else {
...
}
}
复制代码
__strong
仍是__weak
/**
__NSMallocBlock__:__strong修饰
__NSStackBlock__:__weak修饰
*/
-(void)blockStatckVsGloable {
NSInteger i = 10;
__strong void (^mallocBlock)(void) = ^{
NSLog(@"i = %ld", (long)i);
};
__weak void (^stackBlock)(void) = ^{
NSLog(@"self.number = %@", self.number);
};
}
复制代码
/**
__NSStackBlock__:访问外部变量
__NSGlobalBlock__:没有访问外部变量
*/
-(void)blockStatckVsGloable {
NSInteger i = 10;
__weak void (^stackBlock)(void) = ^{
NSLog(@"i = %ld", (long)i);
};
__weak void (^globalBlock2)(void) = ^{
};
}
复制代码
对于访问和修改全局变量,Block的结构不会发生变化。
对于访问和修改全局静态变量,同全局变量,Block的结构不会发生变化。
ARC下NSConcreteStackBlock若是引入了局部变量,会被NSConcreteMallocBlock 类型的 block 替代。
__main_block_copy_0
用于在将Block拷贝到堆中的时候,将包装局部变量(
localVariable
)的对象(
__Block_byref_localVariable_0
)从栈中拷贝到堆中,因此即便局部变量在栈中被销毁,Block依然能对堆中的局部变量进行修改操做。结构体
__Block_byref_localVariable_0
中的
__forwarding
变量用来指向局部变量在堆中的拷贝。目的是为了保证操做的值始终是堆中的拷贝,而不是栈中的值。
static void _Block_byref_assign_copy(void *dest, const void *arg, const int flags) {
struct Block_byref **destp = (struct Block_byref **)dest;
struct Block_byref *src = (struct Block_byref *)arg;
...
// 堆中拷贝的forwarding指向它本身
copy->forwarding = copy; // patch heap copy to point to itself (skip write-barrier)
// 栈中的forwarding指向堆中的拷贝
src->forwarding = copy; // patch stack to point to heap copy
...
}
复制代码
对于访问和修改局部静态变量,Block须要截获静态变量的指针,改变的时候直接经过指针改变值
发生循环引用的时候,self强持有Block,从下面能够看出Block也是强持有self的。
详情请参考Block技巧与底层解析
[TOC]
@(iOS开发学习)[温故而知新]
RunLoop
实际上就是一个对象,这个对象管理了其须要处理的事件和消息,并提供了一个入口函数来执行上面 Event Loop
的逻辑。线程执行了这个函数后,就会一直处于这个函数内部 “接受消息->等待->处理
” 的循环中,直到这个循环结束(好比传入 quit
的消息),函数返回。通常来说,一个线程一次只能执行一个任务,执行完成后线程就会退出。若是咱们须要RunLoop
,让线程能随时处理事件但并不退出。RunLoop
核心是一个有条件的循环。
RunLoop的结构须要涉及到如下4个概念:
Run Loop Mode
、Input Source
、Timer Source
和Run Loop Observer
。
一、一个 Runloop 包含若干个
mode
,每一个mode
又包含若干个source
(InputSource
、TimerSource
、Observers
) 二、Runloop 启动只能指定一个mode
,若要切换mode
只能从新启动Runloop
指定另一个mode
。这样作的目的是为了处理优先级不一样的Source
、Timer
、Observer
。
NSUserDefaultRunloopMode
// 默认mode,一般主线程在这个mode下面运行
UITrackingRunloopMode
// 界面追踪mode,用于ScrollView追踪界面滑动,保证界面滑动时不受其余mode的影响
UIInitializationRunloopMode
// 刚启动App时进入的第一个mode,启动完成后就不在使用
GSEventReceiveRunloopMode
// 接受系统事件的内部mode,一般用不到。
NSRunloopCommonModes
// 并非一种真正的mode,系统把NSUserDefaultRunloopMode和UITrackingRunloopMode共同标记为NSRunloopCommonModes
复制代码
指定事件在运行循环中的优先级。线程的运行须要不一样的模式,去响应各类不一样的事件,去处理不一样情境模式。(好比能够优化tableview
的时候能够设置UITrackingRunLoopMode
下不进行一些操做,好比设置图片等。)
官方文档分为三类,
基于端口的
、自定义的
、基于perform selector
。可是也可经过函数调用栈对Source分为两类,source0和source1。source1是基于端口的,包含一个match_port和一个回调(函数指针),能主动唤醒Runloop的线程;source0是基于非端口的,只包含一个回调(函数指针),不能主动触发事件。也就是用户触发事件,包括自定义source和perfom selector
注意: 按钮点击事件从函数调用栈来看是Source0
事件。其实是点击屏幕产生event
事件,传递给Source1
,而后Source1
派发给Source0
的。
即
CFRunloopTimerRef
,也就是NSTimer
。NSTimer
建立时必须添加到Runloop
中,不然没法执行,在添加到Runloop
时,必须指定mode
,决定NSTimer
在哪一个mode
下运行。
监听
Runloop
的状态
Runloop
的各类状态/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Runloop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 即将从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将推出Runloop
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
复制代码
生命周期 | 主线程 | 子线程 |
---|---|---|
建立 | 默认系统建立 | 苹果不容许直接建立 RunLoop ,它只提供了两个自动获取的函数:CFRunLoopGetMain() 和 CFRunLoopGetCurrent() 。 在当前子线程调用[NSRunLoop currentRunLoop] ,若是有就获取,没有就建立 |
启动 | 默认启动 | 手动启动 |
获取 | [NSRunLoop mainRunLoop] 或者CFRunLoopGetMain() |
[NSRunLoop currentRunLoop] 或者CFRunLoopGetCurrent() |
销毁 | app结束时 | 超时时间到了或者手动结束CFRunLoopStop() |
CFRunLoopStop()
方法只会结束当前的runMode:beforeDate:
调用,而不会结束后续的调用。
Runloop会一直循环检测事件源CFRunloopSourceRef执行处理函数,首先会产生通知,CoreFundation向线程添加RunloopObserves来监听事件,并控制Runloop里面线程的执行和休眠,在有事情作得时候使NSRunloop控制的线程处理事情,空闲的时候让NSRunloop控制的线程休眠。先处理TimerSource,再处理Source0,而后Source1。
- 一、线程结束
- 二、由于没有任何事件源而退出( Runloop 只会检查 Source 和 Timer ,没有就关闭,不会检查Observer )
- 三、手动结束 Runloop
启动方式 | 调用次数 | 描述 |
---|---|---|
run | 循环调用 | 无条件进入是最简单的作法,但也最不推荐。这会使线程进入死循环,从而不利于控制 runloop,结束 runloop 的惟一方式是 kill 它。它的本质就是无限调用 runMode:beforeDate: 方法。 |
runUntilDate | 循环调用 | 若是咱们设置了超时时间,那么 runloop 会在处理完事件或超时后结束,此时咱们能够选择从新开启 runloop。也会重复调用 runMode:beforeDate:,区别在于它超时后就不会再调用。这种方式要优于前一种。 |
runMode:beforeDate: | 单次调用 | 这是相对来讲最优秀的方式,相比于第二种启动方式,咱们能够指定 runloop 以哪一种模式运行。 |
经过run开启 runloop 会致使内存泄漏,也就是 thread 对象没法释放。
Runloop
与线程是一一对应的,主线程的Runloop
默认已经建立好了,子线程的须要本身手动建立(主线程是一一对应的,子线程能够没有,也能够最多有一个Runloop
)
- 一、Timer Source会重复在预设的时间点(建立定时器时指定的时间间隔)向Runloop发送消息,执行任务回调函数。
- 二、主线程因为默认建立启动了Runloop因此定时器能够正常运行,可是子线程要想定时器能够正常运行,须要手动启动Runloop。
- 三、另外Timer添加到Runloop指定的默认mode是NSUserDefaultRunloopMode,当UIScrollView滚动的时候Runloop会自动切换到UITrackingRunloopMode,此时定时器是不能正常运行的,若是想正常运行,须要改变Timer添加到Runloop的mode为NSRunloopCommonMode
NSTimer
其实就是 CFRunLoopTimerRef
,他们之间是 toll-free bridged
的。一个 NSTimer
注册到 RunLoop
后,RunLoop
会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop
为了节省资源,并不会在很是准确的时间点回调这个 Timer
。Timer
有个属性叫作 Tolerance
(宽容度),标示了当时间点到后,允许有多少最大偏差。
若是某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就好比等公交,若是 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。
建立定时器的时候,会以NSUerDefaultRunloopMode
的mode自动加入到当前线程中。所以下面两种效果是等价的。
GCD定时器不受Runloop的mode的影响。GCD 自己与 RunLoop 是属于平级的关系。 他们谁也不包含谁,可是他们之间存在着协做的关系。当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其余线程仍然是由 libDispatch 处理的。
利用CFRunLoopMode的特性,能够将图片的加载放到NSDefaultRunLoopMode
的mode里,这样在滚动UITrackingRunLoopMode
这个mode时不会被加载而影响到。参考:RunLoopWorkDistribution
若是利用scrollView类型的作自动广告滚动条 须要把定时器加入当前runloop的模式NSRunLoopCommonModes
第一种方法经过子线程监测主线程的 runLoop,判断两个状态区域之间的耗时是否达到必定阈值。
ANREye
就是在子线程设置flag 标记为YES, 而后在主线程中将flag设置为NO。利用子线程时阙值时长,判断标志位是否成功设置成NO。NSRunLoop调用方法主要就是在kCFRunLoopBeforeSources
和kCFRunLoopBeforeWaiting
之间,还有kCFRunLoopAfterWaiting
以后,也就是若是咱们发现这两个时间内耗时太长,那么就能够断定出此时主线程卡顿
第二种方式就是FPS监控,App 刷新率应该当努力保持在 60fps,经过
CADisplayLink
记录两次刷新时间间隔,就能够计算出当前的 FPS。CADisplayLink
是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不同,其内部实际是操做了一个 Source)。若是在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 类似),形成界面卡顿的感受。在快速滑动TableView
时,即便一帧的卡顿也会让用户有所察觉。
能够添加Observer监听RunLoop的状态。好比监听点击事件的处理(在全部点击事件以前作一些事情)
某些操做,须要重复开辟子线程,重复开辟内存过于消耗性能,能够设定子线程常驻
若是子线程的NSRunLoop没有设置source or timer, 那么子线程的NSRunLoop会马上关闭
// 无含义,设置子线程为常住线程,让子线程不关闭
[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
复制代码
NSTimer *timer = [NSTimer timerWithTimeInterval:5.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
// 若是不改变mode,下面这行代码去掉后效果同样
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
[[NSRunLoop currentRunLoop] run];
复制代码
当在操做 UI 时,好比改变了
Frame
、更新了UIView/CALayer
的层次时,或者手动调用了UIView/CALayer
的 >setNeedsLayout
/setNeedsDisplay
方法后,这个UIView/CALayer
就被标记为待处理,并被提交到一个全局>的容器去。苹果注册了一个
Observer
监听BeforeWaiting
(即将进入休眠) 和 Exit (即将退出Loop
) 事件,回调去执行一个>很长的函数:_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
。这个函数里会遍历全部待处理的 >UIView/CAlayer
以执行实际的绘制和调整,并更新 UI 界面。若是主线程忙于大量的业务逻辑运算,此时去更新UI可能会卡顿。异步绘制框架
ASDK(Texture)
就是为了解决这个问题诞生的,根本原理就是将UI排版和绘制运算尽量的放到后台,将UI最终的更新操做放到主线程中,同时提供了一套相似UIView
和CAlayer
的相关属性,尽量保证开发者的开发习惯。监听主线程Runloop Observer
的即将进入休眠和退出两种状态,收到回调是遍历队列中待处理的任务一一执行。
苹果注册了一个 Source1
(基于 mach port
的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()
。
当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework
生成一个 IOHIDEvent
事件并由 SpringBoard
接收。 SpringBoard
只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event
,随后用 mach port
转发给须要的 App 进程。随后苹果注册的那个 Source1
就会触发__IOHIDEventSystemClientQueueCallback()
回调,并调用 _UIApplicationHandleEventQueue()
进行应用内部的分发。
_UIApplicationHandleEventQueue()
会把 IOHIDEvent
处理并包装成 UIEvent
进行处理或分发,其中包括识别 UIGesture
/处理屏幕旋转
/发送给 UIWindow
等。一般事件好比 UIButton
点击、touchesBegin
/Move
/End
/Cancel
事件都是在这个回调中完成的。
SpringBoard
是什么?
SpringBoard
实际上是一个标准的应用程序,这个应用程序用来管理IOS的主屏幕,除此以外像启动WindowSever(窗口服务器)
,bootstrapping(引导应用程序)
,以及在启动时候系统的一些初始化设置
都是由这个特定的应用程序负责的。它是咱们IOS程序中,事件的第一个接受者
。它只能接受少数的事件好比:按键(锁屏/静音等),触摸,加速,接近传感器等几种Event,随后使用macport
转发给须要的App进程。
当上面的 _UIApplicationHandleEventQueue()
识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin
/Move
/End
系列回调打断。随后系统将对应的 UIGestureRecognizer
标记为待处理。
苹果注册了一个 Observer
监测 BeforeWaiting
(Loop 即将进入休眠) 事件,这个 Observer 的回调函数是 _UIGestureRecognizerUpdateObserver()
,其内部会获取全部刚被标记为待处理的 GestureRecognizer
,并执行 GestureRecognizer
的回调。
当有 UIGestureRecognizer
的变化(建立/销毁/状态改变)时,这个回调都会进行相应处理。
performSelecter:after
函数依赖Timer Source
和Runloop
的启动;Runloop
依赖Source
(不限于Timer Source
),没有Sources
就会退出
import UIKit
class RunloopVc: UIViewController {
@objc func performSeletor() {
debugPrint("performSeletor \(Thread.current)")
}
func case0() {
// 结果:1 → 2 → 1.1 → performSeletor → 1.2
debugPrint("1")
DispatchQueue.global().async {
debugPrint("1.1 \(Thread.current)")
// 当前子线程 异步执行,1.1 → 1.2 → performSeletor
//self.performSelector(inBackground: #selector(self.performSeletor), with: nil)
// 当前子线程 同步执行,1.1 → performSeletor → 1.2
self.perform(#selector(self.performSeletor))
debugPrint("1.2 \(Thread.current)")
}
debugPrint("2")
}
func case1() {
// 结果:1 → 2 → performSeletor(子线程异步执行)
debugPrint("1")
self.performSelector(inBackground: #selector(performSeletor), with: nil)
debugPrint("2")
}
func case2() {
// 结果:1 → 2 → performSeletor(主线程异步执行)
debugPrint("1")
self.perform(#selector(performSeletor), afterDelay: 1)
debugPrint("2")
}
func case3() {
// 结果:1 → 2 → performSeletor(主线程异步执行)
debugPrint("1")
self.perform(#selector(performSeletor), with: nil, afterDelay: 1, inModes: [.default])
debugPrint("2")
}
func case4() {
// 结果:1 → 2 → performSeletor不会执行
debugPrint("1")
DispatchQueue.global(qos: .default).async {
debugPrint("1.1 \(Thread.current)")
self.perform(#selector(self.performSeletor), afterDelay: 1)
debugPrint("1.2 \(Thread.current)")
}
debugPrint("2")
}
func case5() {
// 结果:1 → 2 → 1.1 → 1.2 → 1.3
debugPrint("1")
DispatchQueue.global(qos: .default).async {
debugPrint("1.1 \(Thread.current)")
// Runloop中没有source是会自动退出
RunLoop.current.run()
debugPrint("1.2 \(Thread.current)")
self.perform(#selector(self.performSeletor), afterDelay: 10)
debugPrint("1.3 \(Thread.current)")
}
debugPrint("2")
}
func case6() {
// 结果:1 → 2 → 1.1 → 1.2 → performSeletor → 1.3
debugPrint("1")
DispatchQueue.global(qos: .default).async {
debugPrint("1.1 \(Thread.current)")
self.perform(#selector(self.performSeletor), afterDelay: 1)
debugPrint("1.2 \(Thread.current)")
// perform后runloop唤醒
RunLoop.current.run()
// 1.3 可以执行,是由于定时器执行完毕后已经无效,致使Runloop中没有source,因此线程执行完毕后退出
debugPrint("1.3 \(Thread.current)")
}
debugPrint("2")
}
func case7() {
// 结果:1 → 2 → 1.1 → performSeletor → 1.2 → 1.3
debugPrint("1")
DispatchQueue.global(qos: .default).async {
debugPrint("1.1 \(Thread.current)")
self.perform(#selector(self.performSeletor),
on: Thread.current,
with: nil,
waitUntilDone: true)
debugPrint("1.2 \(Thread.current)")
RunLoop.current.run()
// 1.3 不执行,是由于定时器source无效,Runloop结束了
debugPrint("1.3 \(Thread.current)")
}
debugPrint("2")
}
func case8() {
// 结果:1 → 2 → 1.1 → 1.2 → performSeletor → 1.3不执行
debugPrint("1")
DispatchQueue.global(qos: .default).async {
debugPrint("1.1 \(Thread.current)")
self.perform(#selector(self.performSeletor),
on: Thread.current,
with: nil,
waitUntilDone: false)
debugPrint("1.2 \(Thread.current)")
RunLoop.current.run()
// 1.3 不执行,是由于定时器source还存在,Runloop没有结束
debugPrint("1.3 \(Thread.current)")
}
debugPrint("2")
}
func case9() {
// 结果:1 → 2 → 1.1 → 1.2 → performSeletor → 1.3
debugPrint("1")
DispatchQueue.global(qos: .default).async {
debugPrint("1.1 \(Thread.current)")
self.perform(#selector(self.performSeletor),
on: Thread.current,
with: nil,
waitUntilDone: false)
debugPrint("1.2 \(Thread.current)")
// 演示 1s 后结束runloop
RunLoop.current.run(until: NSDate.init(timeIntervalSince1970: NSDate.init().timeIntervalSince1970 + 1) as Date)
debugPrint("1.3 \(Thread.current)")
}
debugPrint("2")
}
override func viewDidLoad() {
super.viewDidLoad()
case5()
}
}
复制代码
NSObject
的 performSelecter:afterDelay:
后,实际上其内部会建立一个 Timer Source
并添加到当前线程的 RunLoop
中。因此若是当前线程没有 RunLoop
,则这个方法会失效。performSelector:onThread:
时,实际上其会建立一个 Timer Source
加到对应的线程去,一样的,若是对应线程没有 RunLoop
该方法也会失效。// 主线程
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:
/// 指定线程
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
/// 针对当前线程
performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:
/// 取消,在当前线程,和上面两个方法对应
cancelPreviousPerformRequestsWithTarget:
cancelPreviousPerformRequestsWithTarget:selector:object:
复制代码
- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
复制代码
一个线程能够包含多个AutoReleasePool,可是一个AutoReleasePool只能对应一个惟一的线程 添加打印Runloop的代码:NSLog(@"[NSRunLoop currentRunLoop] = %@", [NSRunLoop currentRunLoop]);
打印出的日志部分截图以下:
能够发现App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调callout都是
_wrapRunLoopWithAutoreleasePoolHandler()
。
第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 建立自动释放池。其 order 是-2147483647,优先级最高,保证建立释放池发生在其余全部回调以前。
第二个 Observer 监视了两个事件:
BeforeWaiting
(准备进入休眠) 时调用_objc_autoreleasePoolPop()
和_objc_autoreleasePoolPush()
释放旧的池并建立新池;Exit(即将退出Loop) 时调用_objc_autoreleasePoolPop()
来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其余全部回调以后。
在主线程执行的代码,一般是写在诸如事件回调、Timer回调内的。这些回调会被
RunLoop
建立好的AutoreleasePool
环绕着,因此不会出现内存泄漏,开发者也没必要显示建立 Pool 了。
疑问:子线程默认不会开启 Runloop,那出现 Autorelease 对象如何处理?不手动处理会内存泄漏吗?
解释:
在子线程你建立了 Pool 的话,产生的
Autorelease 对象
就会交给 pool 去管理。 若是你没有建立 Pool ,可是产生了Autorelease 对象
,就会调用autoreleaseNoPage
方法。在这个方法中,会自动帮你建立一个 hotpage(hotPage 能够理解为当前正在使用的AutoreleasePoolPage
,若是你仍是不理解,能够先看看 Autoreleasepool 的源代码,再来看这个问题 ),并调用page->add(obj)
将对象添加到AutoreleasePoolPage
的栈中,也就是说你不进行手动的内存管理,也不会内存泄漏啦!StackOverFlow 的做者也说道,这个是 OS X 10.9+和 iOS 7+ 才加入的特性。而且苹果没有对应的官方文档阐述此事,可是你能够经过源码了解。这里张贴部分源代码:
static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
// No pool in place.
// hotPage 能够理解为当前正在使用的 AutoreleasePoolPage。
assert(!hotPage());
// POOL_SENTINEL 只是 nil 的别名
if (obj != POOL_SENTINEL && DebugMissingPools) {
// We are pushing an object with no pool in place,
// and no-pool debugging was requested by environment.
_objc_inform("MISSING POOLS: Object %p of class %s "
"autoreleased with no pool in place - "
"just leaking - break on "
"objc_autoreleaseNoPool() to debug",
(void*)obj, object_getClassName(obj));
objc_autoreleaseNoPool(obj);
return nil;
}
// Install the first page.
// 帮你建立一个 hotpage(hotPage 能够理解为当前正在使用的 AutoreleasePoolPage
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);
// Push an autorelease pool boundary if it wasn't already requested. // POOL_SENTINEL 只是 nil 的别名,哨兵对象 if (obj != POOL_SENTINEL) { page->add(POOL_SENTINEL); } // Push the requested object. // 把对象添加到 自动释放池 进行管理 return page->add(obj); } 复制代码
Runloop
和GCD
并无直接的关系,当调用了DispatchQueue.main.async
从子线程到主线程进行通讯刷新UI的时候,libDispatch
会向主线程Runloop
发送消息唤醒Runloop
,Runloop
从消息中获取Block
,而且在__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
回调里执行block
操做。dispatch
到非主线程的操做所有是由libDispatch
驱动的。