AutoreleasePool、Block、Runloop整理笔记

@(iOS开发学习)[温故而知新,掘金博客]html

[TOC]ios

一、AutoreleasePool分析整理

为了分析AutoreleasePool,下面分四种场景进行分析

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
复制代码

场景一:对象没有被加入到AutoreleasePool中

#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

场景二:对象被加入到手动建立的AutoreleasePool中

#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结束以前就被释放了。 面试

场景三:对象被加入到系统的AutoreleasePool中

#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迭代中都加入了AutoreleasePoolRunloop开始后建立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结束后释放

AutoreleasePool定义

  • 自动释放池是由 AutoreleasePoolPage 以双向链表的方式实现的
  • 当对象调用 Autorelease 方法时,会将对象加入 AutoreleasePoolPage 的栈中
  • 调用 AutoreleasePoolPage::pop 方法会向栈中的对象发送 release 消息

在ARC环境下,以alloc/new/copy/mutableCopy开头的方法返回值取得的对象是本身生成而且持有的,其余状况是非本身持有的对象,此时对象的持有者就是AutoreleasePoolswift

当咱们使用@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对象、哨兵对象)的空闲位置。

objc_autoreleasePoolPush 执行过程

当调用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对象何时释放?

没有手动加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);			//调用 查看对象的引用计数
复制代码

NSThread、NSRunLoop 和 NSAutoreleasePool

根据苹果官方文档中对 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]);打印出的日志部分截图以下。

能够发现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 了。

何时用@autoreleasepool

根据 Apple的文档 ,使用场景以下:

  • 写基于命令行的的程序时,就是没有UI框架,如AppKit等Cocoa框架时。
  • 写循环,循环里面包含了大量临时建立的对象。(本文的例子)
  • 建立了新的线程。(非Cocoa程序建立线程时才须要)
  • 长时间在后台运行的任务。

Tagged Pointer

Tagged Pointer是一个可以提高性能节省内存的有趣的技术。在OS X 10.10中,NSString就采用了这项技术,如今让咱们来看看该技术的实现过程。

  • 一、Tagged Pointer专门用来存储小的对象,例如NSNumber、NSDate、NSString
  • 二、Tagged Pointer指针的值再也不是地址了,而是真正的值。再也不是一个对象,内存中的位置不在堆中,不须要malloc和free。避免在代码中直接访问对象的isa变量,而使用方法isKindOfClass和objc_getClass。
  • 三、在内存读取上有着3倍的效率,建立时比之前快了106倍。

面试会被问到的问题?

一、进入pool和出pool的时候,引用计数的变化? 二、为何采用双向链表,而不是单向链表? 三、你说到了哨兵,在链表中怎么使用哨兵能够简化编程?

参考博客

深刻理解RunLoop

深刻理解AutoreleasePool

黑幕背后的Autorelease

自动释放池的前世此生 ---- 深刻解析 autoreleasepool

iOS开发 自动释放池(Autorelease Pool)和RunLoop

Objective-C Autorelease Pool 的实现原理

深刻理解Tagged Pointer

【译】采用Tagged Pointer的字符串

Objective-C高级编程(一) 自动引用计数,看我就够了

2、Block学习整理

2.0、什么是Block

Block表面上是一个带自动变量(局部变量)的匿名函数。本质上是对闭包的对象实现,简单来讲Block就是一个结构体对象。在ARC下,大多数状况下Block从栈上复制到堆上的代码是由编译器实现的(Blcok做为返回值或者参数)

2.一、block编译转换结构

2.1.一、新建一个macOS项目,编写一个最简单的Block。

#import <Foundation/Foundation.h>

typedef void(^blockVoid)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        blockVoid block = ^() {
            NSLog(@"block");
        };
        block();
    }
    return 0;
}
复制代码

2.1.二、使用clang命令处理成cpp文件从而初步认识Block。

因为命令过长,咱们起一个别名genCpp_macalias genCpp_mac='clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk'

2.1.三、在终端进入.m所在的目录下面执行genCpp_mac main.m,当前目录下会生成同名.cpp文件

2.1.四、打开.cpp文件查看与Block相关的代码

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 };
复制代码

2.二、block实际结构Block_private.h

#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 可以访问它外部的局部变量,就是由于将这些变量(或变量的地址)复制到告终构体中。
};
复制代码

2.三、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 {
        ...
    }
}
复制代码

2.3.一、__NSStackBlock__与__NSMallocBlock__的区别:修饰类型是__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);
    };
}
复制代码

2.3.二、 __NSStackBlock__与__NSGlobalBlock__的区别:是否访问外部变量

/**
 __NSStackBlock__:访问外部变量
 __NSGlobalBlock__:没有访问外部变量
 */
-(void)blockStatckVsGloable {
    NSInteger i = 10;
    __weak void (^stackBlock)(void) = ^{
        NSLog(@"i = %ld", (long)i);
    };
    __weak void (^globalBlock2)(void) = ^{

    };
}
复制代码

2.四、访问修改变量对block结构的影响

2.4.一、全局变量

对于访问和修改全局变量,Block的结构不会发生变化。

2.4.二、全局静态变量

对于访问和修改全局静态变量,同全局变量,Block的结构不会发生变化。

2.4.三、局部变量

ARC下NSConcreteStackBlock若是引入了局部变量,会被NSConcreteMallocBlock 类型的 block 替代。

访问局部变量

修改局部变量(局部变量须要使用__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
    ...
}
复制代码

2.4.四、局部静态变量(修改不须要__Block修饰)

对于访问和修改局部静态变量,Block须要截获静态变量的指针,改变的时候直接经过指针改变值

2.4.五、self隐式循环引用

发生循环引用的时候,self强持有Block,从下面能够看出Block也是强持有self的。

2.六、block的辅助函数

详情请参考Block技巧与底层解析

2.六、访问or修改外部变量

截图来自谈Objective-C block的实现

参考博客

《Objective-C高级编程》Blocks

Block技巧与底层解析

Block源码解析和深刻理解

谈Objective-C block的实现

block没那么难(二):block和变量的内存管理

源码解析之从Block说开去

iOS 中的 block 是如何持有对象的

3、Runloop学习整理(持续更新纠正)

[TOC]

@(iOS开发学习)[温故而知新]

问题

一、什么是RunLoop

RunLoop实际上就是一个对象,这个对象管理了其须要处理的事件和消息,并提供了一个入口函数来执行上面 Event Loop 的逻辑。线程执行了这个函数后,就会一直处于这个函数内部 “接受消息->等待->处理” 的循环中,直到这个循环结束(好比传入 quit 的消息),函数返回。通常来说,一个线程一次只能执行一个任务,执行完成后线程就会退出。若是咱们须要RunLoop,让线程能随时处理事件但并不退出。RunLoop核心是一个有条件的循环。

Runloop组成

RunLoop的结构须要涉及到如下4个概念:Run Loop ModeInput SourceTimer SourceRun Loop Observer


一、一个 Runloop 包含若干个mode,每一个mode又包含若干个sourceInputSourceTimerSourceObservers) 二、Runloop 启动只能指定一个mode,若要切换mode只能从新启动Runloop指定另一个mode。这样作的目的是为了处理优先级不一样的SourceTimerObserver

Runloop的各类mode

NSUserDefaultRunloopMode
// 默认mode,一般主线程在这个mode下面运行
UITrackingRunloopMode
// 界面追踪mode,用于ScrollView追踪界面滑动,保证界面滑动时不受其余mode的影响
UIInitializationRunloopMode
// 刚启动App时进入的第一个mode,启动完成后就不在使用
GSEventReceiveRunloopMode
// 接受系统事件的内部mode,一般用不到。
NSRunloopCommonModes
// 并非一种真正的mode,系统把NSUserDefaultRunloopMode和UITrackingRunloopMode共同标记为NSRunloopCommonModes
复制代码
Runloop的各类mode的做用

指定事件在运行循环中的优先级。线程的运行须要不一样的模式,去响应各类不一样的事件,去处理不一样情境模式。(好比能够优化tableview的时候能够设置UITrackingRunLoopMode下不进行一些操做,好比设置图片等。)

一、InputSource:

官方文档分为三类,基于端口的自定义的、基于perform selector。可是也可经过函数调用栈对Source分为两类,source0和source1。source1是基于端口的,包含一个match_port和一个回调(函数指针),能主动唤醒Runloop的线程;source0是基于非端口的,只包含一个回调(函数指针),不能主动触发事件。也就是用户触发事件,包括自定义source和perfom selector

注意: 按钮点击事件从函数调用栈来看是Source0事件。其实是点击屏幕产生event事件,传递给Source1,而后Source1派发给Source0的。

二、TimerSource:

CFRunloopTimerRef,也就是NSTimerNSTimer建立时必须添加到Runloop中,不然没法执行,在添加到Runloop时,必须指定mode,决定NSTimer在哪一个mode下运行。

三、observer:

监听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生命周期

生命周期 主线程 子线程
建立 默认系统建立 苹果不容许直接建立 RunLoop,它只提供了两个自动获取的函数:CFRunLoopGetMain()CFRunLoopGetCurrent()。 在当前子线程调用[NSRunLoop currentRunLoop],若是有就获取,没有就建立
启动 默认启动 手动启动
获取 [NSRunLoop mainRunLoop]或者CFRunLoopGetMain() [NSRunLoop currentRunLoop]或者CFRunLoopGetCurrent()
销毁 app结束时 超时时间到了或者手动结束CFRunLoopStop()

CFRunLoopStop() 方法只会结束当前的 runMode:beforeDate: 调用,而不会结束后续的调用。

启动注意:

  • - 使用run启动后,Runloop会一直运行处理输入源的数据,在defaultMode模式下重复调用runMode:beforeDate:
  • - 使用runUntilDate:启动后与run启动的区别是,在设定时间到后会中止Runloop
  • - 使用runMode:beforeDate:启动,Runloop只运行一次,设定时间到达或者第一个input source被处理,Runloop会中止

运行:

Runloop会一直循环检测事件源CFRunloopSourceRef执行处理函数,首先会产生通知,CoreFundation向线程添加RunloopObserves来监听事件,并控制Runloop里面线程的执行和休眠,在有事情作得时候使NSRunloop控制的线程处理事情,空闲的时候让NSRunloop控制的线程休眠。先处理TimerSource,再处理Source0,而后Source1。

中止:

  • 一、线程结束
  • 二、由于没有任何事件源而退出( Runloop 只会检查 SourceTimer ,没有就关闭,不会检查Observer
  • 三、手动结束 Runloop

Runloop启动方式

启动方式 调用次数 描述
run 循环调用 无条件进入是最简单的作法,但也最不推荐。这会使线程进入死循环,从而不利于控制 runloop,结束 runloop 的惟一方式是 kill 它。它的本质就是无限调用 runMode:beforeDate: 方法。
runUntilDate 循环调用 若是咱们设置了超时时间,那么 runloop 会在处理完事件或超时后结束,此时咱们能够选择从新开启 runloop。也会重复调用 runMode:beforeDate:,区别在于它超时后就不会再调用。这种方式要优于前一种。
runMode:beforeDate: 单次调用 这是相对来讲最优秀的方式,相比于第二种启动方式,咱们能够指定 runloop 以哪一种模式运行。

经过run开启 runloop 会致使内存泄漏,也就是 thread 对象没法释放。

Runloop的做用:

  • 一、保持程序的持续运行(好比主运行循环)接收用户的输入
  • 二、处理App中的各类事件(好比触摸事件、定时器事件、Selector事件)
  • 三、任务调度(主调方产生不少事件,不用等到被调方执行完毕事件,采起执行其余操做)
  • 四、节省CPU资源,提升程序性能:该作事时作事,该休息时休息

RunLoop处理逻辑

摘自

二、Runloop和线程的关系

Runloop与线程是一一对应的,主线程的Runloop默认已经建立好了,子线程的须要本身手动建立(主线程是一一对应的,子线程能够没有,也能够最多有一个Runloop

三、Runloop与NSTimer的关系

  • 一、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 为了节省资源,并不会在很是准确的时间点回调这个 TimerTimer 有个属性叫作 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 处理的。

四、RunLoop应用

4.一、TableView中实现平滑滚动延迟加载图片

利用CFRunLoopMode的特性,能够将图片的加载放到NSDefaultRunLoopMode的mode里,这样在滚动UITrackingRunLoopMode这个mode时不会被加载而影响到。参考:RunLoopWorkDistribution

4.二、解决NSTime在ScrollView滚动时无效

若是利用scrollView类型的作自动广告滚动条 须要把定时器加入当前runloop的模式NSRunLoopCommonModes

4.三、检测UI卡顿

第一种方法经过子线程监测主线程的 runLoop,判断两个状态区域之间的耗时是否达到必定阈值。ANREye就是在子线程设置flag 标记为YES, 而后在主线程中将flag设置为NO。利用子线程时阙值时长,判断标志位是否成功设置成NO。NSRunLoop调用方法主要就是在kCFRunLoopBeforeSourceskCFRunLoopBeforeWaiting之间,还有kCFRunLoopAfterWaiting以后,也就是若是咱们发现这两个时间内耗时太长,那么就能够断定出此时主线程卡顿


第二种方式就是FPS监控,App 刷新率应该当努力保持在 60fps,经过CADisplayLink记录两次刷新时间间隔,就能够计算出当前的 FPS。 CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不同,其内部实际是操做了一个 Source)。若是在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 类似),形成界面卡顿的感受。在快速滑动TableView时,即便一帧的卡顿也会让用户有所察觉。

4.四、利用空闲时间缓存数据或者作其余的性能优化相关的任务

能够添加Observer监听RunLoop的状态。好比监听点击事件的处理(在全部点击事件以前作一些事情)

4.五、子线程常驻

某些操做,须要重复开辟子线程,重复开辟内存过于消耗性能,能够设定子线程常驻

若是子线程的NSRunLoop没有设置source or timer, 那么子线程的NSRunLoop会马上关闭

一、添加Source
// 无含义,设置子线程为常住线程,让子线程不关闭
[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
复制代码
二、添加Timer
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];
复制代码

4.六、UI界面更新,监听UICollectionView刷新reloadData完毕的时机

当在操做 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最终的更新操做放到主线程中,同时提供了一套相似UIViewCAlayer的相关属性,尽量保证开发者的开发习惯。监听主线程Runloop Observer的即将进入休眠和退出两种状态,收到回调是遍历队列中待处理的任务一一执行。

4.七、事件响应

苹果注册了一个 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进程。

4.八、手势识别

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

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

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

五、Runloop与PerformSelecter

performSelecter:after函数依赖Timer SourceRunloop的启动;Runloop依赖Source(不限于Timer Source),没有Sources就会退出

参考:关于 performSelector 的一些小探讨

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()
    }
}
复制代码
  • - 当调用 NSObjectperformSelecter:afterDelay: 后,实际上其内部会建立一个 Timer Source 并添加到当前线程的 RunLoop 中。因此若是当前线程没有 RunLoop,则这个方法会失效。
  • - 当调用 performSelector:onThread: 时,实际上其会建立一个 Timer Source 加到对应的线程去,一样的,若是对应线程没有 RunLoop 该方法也会失效。

是事件源的performSelector方法有:

// 主线程
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:
复制代码

不是事件源的performSelector方法有:

- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
复制代码

六、Runloop与AutoreleasePool

一个线程能够包含多个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

RunloopGCD并无直接的关系,当调用了DispatchQueue.main.async从子线程到主线程进行通讯刷新UI的时候,libDispatch会向主线程Runloop发送消息唤醒RunloopRunloop从消息中获取Block,而且在__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__回调里执行block操做。dispatch到非主线程的操做所有是由libDispatch驱动的。

参考博客

Run Loops 官方文档

RunLoop在iOS开发中的应用

格而知之5:我所理解的Run Loop

深刻理解RunLoop

深刻研究 Runloop 与线程保活

解密-神秘的 RunLoop

视频学习

runLoop学习笔记

iOS学习之深刻理解RunLoop

RunLoop与Timer以及经常使用Mode
NSTimer须要注意的地方

相关文章
相关标签/搜索