阅读 862

AutoreleasePool、Block、Runloop整理笔记

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

[TOC]

1、AutoreleasePool分析整理

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

Person类用于打印对象的释放时机

#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结束后就被释放了。

场景二:对象被加入到手动创建的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对象被释放掉。

场景四:(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
复制代码

运行结果:

  • Tagged Pointer类型的Autorelease对象,系统不会释放
  • Tagged Pointer类型的Autorelease对象,系统会在当前Runloop结束后释放

AutoreleasePool定义

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

在ARC环境下,以alloc/new/copy/mutableCopy开头的方法返回值取得的对象是自己生成并且持有的,其他情况是非自己持有的对象,此时对象的持有者就是AutoreleasePool

当我们使用@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 已满。

  • 1、有 hotPage 并且当前 page 不满。调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中
  • 2、有 hotPage 并且当前 page 已满。调用 autoreleaseFullPage 初始化一个新的页,调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中
  • 3、无 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就采用了这项技术,现在让我们来看看该技术的实现过程。

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

面试会被问到的问题?

1、进入pool和出pool的时候,引用计数的变化? 2、为什么采用双向链表,而不是单向链表? 3、你说到了哨兵,在链表中怎么使用哨兵可以简化编程?

参考博客

深入理解RunLoop

深入理解AutoreleasePool

黑幕背后的Autorelease

自动释放池的前世今生 ---- 深入解析 autoreleasepool

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

Objective-C Autorelease Pool 的实现原理

深入理解Tagged Pointer

【译】采用Tagged Pointer的字符串

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

二、Block学习整理

2.0、什么是Block

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

2.1、block编译转换结构

2.1.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.2、使用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.3、在终端进入.m所在的目录下面执行genCpp_mac main.m,当前目录下会生成同名.cpp文件

2.1.4、打开.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.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.3、block的种类

Block种类 存储区 拷贝效果 生命周期
_NSConcreteStatckBlock 从栈拷贝到堆,往flags中并入BLOCK_NEEDS_FREE这个标志表明block需要释放,在release以及再次拷贝时会用到);如果有辅助拷贝函数,就调用,将捕获的变量从栈拷贝到堆中 出了作用域
_NSConcreteMallocBlock 单纯的引用计数加一 引用计数为0,runloop结束后释放
_NSConcreteGlobalBlock 全局数据区 什么都不做,直接返回Blcok app整个生命周期

  • 1、只要不访问外部变量就是__NSGlobalBlock__类型的Block,不管是__strong还是__weak修饰(不加默认__strong)。
  • 2、如果访问外部变量,被__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.1、__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.2、 __NSStackBlock__与__NSGlobalBlock__的区别:是否访问外部变量

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

    };
}
复制代码

2.4、访问修改变量对block结构的影响

2.4.1、全局变量

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

2.4.2、全局静态变量

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

2.4.3、局部变量

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.4、局部静态变量(修改不需要__Block修饰)

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

2.4.5、self隐式循环引用

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

2.6、block的辅助函数

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

2.6、访问or修改外部变量

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

参考博客

《Objective-C高级编程》Blocks

Block技巧与底层解析

Block源码解析和深入理解

谈Objective-C block的实现

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

源码解析之从Block说开去

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

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

[TOC]

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

问题

1、什么是RunLoop

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

Runloop组成

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


1、一个 Runloop 包含若干个mode,每个mode又包含若干个sourceInputSourceTimerSourceObservers) 2、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下不进行一些操作,比如设置图片等。)

1、InputSource:

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

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

2、TimerSource:

CFRunloopTimerRef,也就是NSTimerNSTimer创建时必须添加到Runloop中,否则无法执行,在添加到Runloop时,必须指定mode,决定NSTimer在哪个mode下运行。

3、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。

停止:

  • 1、线程结束
  • 2、因为没有任何事件源而退出( Runloop 只会检查 SourceTimer ,没有就关闭,不会检查Observer
  • 3、手动结束 Runloop

Runloop启动方式

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

通过run开启 runloop 会导致内存泄漏,也就是 thread 对象无法释放。

Runloop的作用:

  • 1、保持程序的持续运行(比如主运行循环)接收用户的输入
  • 2、处理App中的各种事件(比如触摸事件、定时器事件、Selector事件)
  • 3、任务调度(主调方产生很多事件,不用等到被调方执行完毕事件,采取执行其他操作)
  • 4、节省CPU资源,提高程序性能:该做事时做事,该休息时休息

RunLoop处理逻辑

摘自

2、Runloop和线程的关系

Runloop与线程是一一对应的,主线程的Runloop默认已经创建好了,子线程的需要自己手动创建(主线程是一一对应的,子线程可以没有,也可以最多有一个Runloop

3、Runloop与NSTimer的关系

  • 1、Timer Source会重复在预设的时间点(创建定时器时指定的时间间隔)向Runloop发送消息,执行任务回调函数。
  • 2、主线程由于默认创建启动了Runloop所以定时器可以正常运行,但是子线程要想定时器可以正常运行,需要手动启动Runloop。
  • 3、另外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 处理的。

4、RunLoop应用

4.1、TableView中实现平滑滚动延迟加载图片

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

4.2、解决NSTime在ScrollView滚动时无效

如果利用scrollView类型的做自动广告滚动条 需要把定时器加入当前runloop的模式NSRunLoopCommonModes

4.3、检测UI卡顿

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


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

4.4、利用空闲时间缓存数据或者做其他的性能优化相关的任务

可以添加Observer监听RunLoop的状态。比如监听点击事件的处理(在所有点击事件之前做一些事情)

4.5、子线程常驻

某些操作,需要重复开辟子线程,重复开辟内存过于消耗性能,可以设定子线程常驻

如果子线程的NSRunLoop没有设置source or timer, 那么子线程的NSRunLoop会立刻关闭

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

苹果注册了一个 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.8、手势识别

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

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

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

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

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

7、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需要注意的地方

关注下面的标签,发现更多相似文章
评论