读《Objective‐C高级编程:iOS与OS X多线程和内存管理》的记录总结 - shenchunxing/better-ios-developer GitHub Wiki

image 先看一下手动内存管理:

手动内存管理

我个人觉得,学习一项新的技术之前,需要先了解一下它的核心思想。理解了核心思想之后,对技术点的把握就会更快一些:

内存管理的思想

  • 思想一:自己生成的对象,自己持有。
  • 思想二:非自己生成的对象,自己也能持有。
  • 思想三:不再需要自己持有的对象时释放对象。
  • 思想四:非自己持有的对象无法释放。

从上面的思想来看,我们对对象的操作可以分为三种:生成,持有,释放,再加上废弃,一共有四种。它们所对应的Objective-C的方法和引用计数的变化是:

对象操作 Objecctive-C方法 引用计数的变化
生成并持有对象 alloc/new/copy/mutableCopy等方法 +1
持有对象 retain方法 +1
释放对象 release方法 -1
废弃对象 dealloc方法

用书中的图来直观感受一下这四种操作: image

下面开始逐一解释上面的四条思想:

思想一:自己生成的对象,自己持有

在生成对象时,使用以下面名称开头的方法生成对象以后,就会持有该对象:

  • alloc
  • new
  • copy
  • mutableCopy

举个🌰:

id obj = [[NSObject alloc] init];//持有新生成的对象

这行代码过后,指向生成并持有[[NSObject alloc] init]的指针被赋给了obj,也就是说obj这个指针强引用[[NSObject alloc] init]这个对象。

同样适用于new方法:

id obj = [NSObject new];//持有新生成的对象

注意:
这种将持有对象的指针赋给指针变量的情况不只局限于上面这四种方法名称,还包括以他们开头的所有方法名称:

  • allocThisObject
  • newThatObject
  • copyThisObject
  • mutableCopyThatObject

举个🌰:

id obj1 = [obj0 allocObject];//符合上述命名规则,生成并持有对象

注意:尽管在allocObject方法的大括号结束后,obj的生命周期已经结束,但由于该对象已经被返回,因此它的所有权已经转移给了调用者obj1。调用者可以选择保留该对象(增加其引用计数)以延长其生命周期,或者在不需要使用该对象时释放它。

它的内部实现:

- (id)allocObject
{
    id obj = [[NSObject alloc] init];//持有新生成的对象
    return obj;
}

反过来,如果不符合上述的命名规则,那么就不会持有生成的对象,
看一个不符合上述命名规则的返回对象的createObject方法的内部实现🌰:

- (id)createObject
{
    id obj = [[NSObject alloc] init];//持有新生成的对象
    [obj autorelease];//取得对象,但自己不持有
    return obj;
}

经由这个方法返回以后,无法持有这个返回的对象。因为这里使用了autorelease。autorelease提供了这样一个功能:在对象超出其指定的生存范围时能够自动并正确地释放(详细会在后面介绍) image

也就是说,生成一个调用方不持有的对象是可以通过autorelease来实现的(例如NSMutableArray的array类方法)。

我的个人理解是:通过autorelease方法,使对象的持有权转移给了自动释放池。所以实现了:调用方拿到了对象,但这个对象还不被调用方所持有。

由这个不符合命名规则的例子来引出思想二:

思想二:非自己生成的对象,自己也能持有

我们现在知道,仅仅通过上面那个不符合命名规则的返回对象实例的方法是无法持有对象的。但是我们可以通过某个操作来持有这个返回的对象:这个方法就是通过retain方法来让指针变量持有这个新生成的对象:

id obj = [NSMutableArray array];//非自己生成并持有的对象
[obj retain];//持有新生成的对象

注意,这里[NSMutableArray array]返回的非自己持有的对象正是通过上文介绍过的autorelease方法实现的。所以如果想持有这个对象,需要执行retain方法才可以。

思想三:不再需要自己持有的对象时释放对象

对象的持有者有义务在不再需要这个对象的时候主动将这个对象释放。注意,是有义务,而不是有权利,注意两个词的不同。

来看一下释放对象的例子:

id obj = [[NSObject alloc] init];//持有新生成的对象
[obj doSomething];//使用该对象做一些事情
[obj release];//事情做完了,释放该对象

同样适用于非自己生成并持有的对象(参考思想二):

id obj = [NSMutableArray array];//非自己生成并持有的对象
[obj retain];//持有新生成的对象
[obj soSomething];//使用该对象做一些事情
[obj release];//事情做完了,释放该对象

可能遇到的面试题:调用对象的release方法会销毁对象吗?
答案是不会:调用对象的release方法只是将对象的引用计数器-1,当对象的引用计数器为0的时候会调用了对象的dealloc 方法才能进行释放对象的内存。

思想四:无法释放非自己持有的对象

在释放对象的时候,我们只能释放已经持有的对象,非自己持有的对象是不能被自己释放的。这很符合常识:就好比你自己才能从你自己的银行卡里取钱,取别人的卡里的钱是不对的(除非他的钱归你管。。。只是随便举个例子)。

两种不允许的情况:

1. 释放一个已经废弃了的对象

id obj = [[NSObject alloc] init];//持有新生成的对象
[obj doSomething];//使用该对象
[obj release];//释放该对象,不再持有了
[obj release];//释放已经废弃了的对象,崩溃

2. 释放自己不持有的对象

id obj = [NSMutableArray array];//非自己生成并持有的对象
[obj release];//释放了非自己持有的对象

思考:哪些情况会使对象失去拥有者呢?

  1. 将指向某对象的指针变量指向另一个对象。
  2. 将指向某对象的指针变量设置为nil。
  3. 当程序释放对象的某个拥有者时。
  4. 从collection类中删除对象时。

现在知道了引用计数式内存管理的四个思想,我们再来看一下四个操作引用计数的方法:

alloc/retain/release/dealloc的实现

某种意义上,GNUstep 和 Foundation 框架的实现是相似的。所以这本书的作者通过GNUstep的源码来推测了苹果Cocoa框架的实现。

下面开始针对每一个方法,同时用GNUstep和苹果的实现方式(追踪程序的执行和作者的猜测)来对比一下各自的实现。

GNUstep实现:

alloc方法

//GNUstep/modules/core/base/Source/NSObject.m alloc:

+ (id) alloc
{
    return [self allocWithZone: NSDefaultMallocZone()];
}
 
+ (id) allocWithZone: (NSZone*)z
{
    return NSAllocateObject(self, 0, z);
}

这里NSAllocateObject方法分配了对象,看一下它的内部实现:

//GNUstep/modules/core/base/Source/NSObject.m NSAllocateObject:

//obj_layout在对象内存中处于头部位置(也就是为了记录对象的引用计数),他后面的才是对象本身的大小
struct obj_layout {
    NSUInteger retained;
};
 
NSAllocateObject(Class aClass, NSUInteger extraBytes, NSZone *zone)
{
    int size = 计算容纳对象所需内存大小,这个大小包括对象本身的大小以及引用计数结构体obj_layout。
    //返回一个指向新分配内存的指针,此时还不是指向obj对象类型
    id new = NSZoneMalloc(zone, 1, size);
    //使用 memset 函数将新分配的内存区域全部置为零,以初始化对象的内存。
    memset (new, 0, size);

    //1.首先将 new 强制转换为 obj 类型的指针,这是为了让编译器知道该指针是指向一个结构体类型的内存
    //2.使用下标运算符 [1],将指针向后偏移一个单位。这里[1]就是跳过obj_layout,直接指向对象本身的内存地址
    //3.使用取地址运算符 &,获取偏移后的内存区域的起始地址,得到一个指向实际对象的指针。
    //4.将该指针强制转换为 id 类型,即 (id)&((obj)new)[1],将其赋值给 new 变量。这样,new 变量就成为了一个指向新分配内存的对象的指针。
    new = (id)&((obj)new)[1];
}
  1. NSAllocateObject函数通过NSZoneMalloc函数来分配存放对象所需要的内存空间。
  2. obj_layout是用来保存引用计数,并将其写入对象内存头部。

对象的引用计数可以通过retainCount方法来取得:

GNUstep/modules/core/base/Source/NSObject.m retainCount:

- (NSUInteger) retainCount
{   
    //注意,这里的+1是因为对象在调用 retainCount 方法时会增加一个临时的引用计数,以确保即使在获取计数期间对象被释放,仍然能够正确返回当前的引用计数值(不是很懂,是否可以理解为存储的大小本身是 -1的,加1后可以得到正确的引用计数)
    return NSExtraRefCount(self) + 1;
}
 
inline NSUInteger
NSExtraRefCount(id anObject)
{   
    //[-1]表示访问obj_layout的最后一个元素,也就是retained
    return ((obj_layout)anObject)[-1].retained;
}

我们可以看到,给NSExtraRefCount传入anObject以后,通过访问对象内存头部的.retained变量,来获取引用计数。

retain方法

//GNUstep/modules/core/base/Source/NSObject.m retain:

- (id)retain
{
    NSIncrementExtraRefCount(self);
    return self;
}
 
inline void NSIncrementExtraRefCount(id anObject)
{
    //retained变量超出最大值,抛出异常
    if (((obj)anObject)[-1].retained == UINT_MAX - 1){
        [NSException raise: NSInternalInconsistencyException
        format: @"NSIncrementExtraRefCount() asked to increment too far”];
    }
    
    ((obj_layout)anObject)[-1].retained++;//retained变量+1
}

release方法

//GNUstep/modules/core/base/Source/NSObject.m release

- (void)release
{
    //如果当前的引用计数 = 0,调用dealloc函数
    if (NSDecrementExtraRefCountWasZero(self))
    {
        [self dealloc];
    }
}
 
BOOL NSDecrementExtraRefCountWasZero(id anObject)
{
    //如果当前的retained值 = 0.则返回yes
    if (((obj)anObject)[-1].retained == 0){
        return YES;
    }
    
    //如果大于0,则-1,并返回NO
    ((obj)anObject)[-1].retained--;
    return NO;
}

dealloc方法

//GNUstep/modules/core/base/Source/NSObject.m dealloc

- (void) dealloc
{
    NSDeallocateObject (self);
}
 
inline void NSDeallocateObject(id anObject)
{
    obj_layout o = &((obj_layout)anObject)[-1];
    free(o);//释放
}

总结一下上面的几个方法:

  • Objective-C对象中保存着引用计数这一整数值。
  • 调用alloc或者retain方法后,引用计数+1。
  • 调用release后,引用计数-1。
  • 引用计数为0时,调用dealloc方法废弃对象。

下面看一下苹果的实现:

苹果的实现

alloc方法

通过在NSObject类的alloc类方法上设置断点,我们可以看到执行所调用的函数:

  • +alloc
  • +allocWithZone:
  • class_createInstance//生成实例
  • calloc//分配内存块

retainCount:

  • __CFdoExternRefOperation
  • CFBasicHashGetCountOfKey

retain方法

  • __CFdoExternRefOperation
  • CFBasicHashAddValue

release方法

  • __CFdoExternRefOperation
  • CFBasicHashRemoveValue

我们可以看到他们都调用了一个共同的 __CFdoExternRefOperation 方法。

看一下它的实现:

int __CFDoExternRefOperation(uintptr_t op, id obj) {

    CFBasicHashRef table = 取得对象的散列表(obj);
    int count;
 
    switch (op) {
    case OPERATION_retainCount:
        count = CFBasicHashGetCountOfKey(table, obj);
        return count;
        break;

    case OPERATION_retain:
        count = CFBasicHashAddValue(table, obj);
        return obj;
    
    case OPERATION_release:
        count = CFBasicHashRemoveValue(table, obj);
        return 0 == count;
    }
}

可以看出,__CFDoExternRefOperation通过switch语句 针对不同的操作来进行具体的方法调用,如果 op 是 OPERATION_retain,就去掉用具体实现 retain 的方法,以此类推。

可以猜想上层的retainCount,retain,release方法的实现:

- (NSUInteger)retainCount
{
    return (NSUInteger)____CFDoExternRefOperation(OPERATION_retainCount,self);
}

- (id)retain
{
    return (id)____CFDoExternRefOperation(OPERATION_retain,self);
}

//这里返回值应该是id,原书这里应该是错了
- (id)release
{
    return (id)____CFDoExternRefOperation(OPERATION_release,self);
}

我们观察一下switch里面每个语句里的执行函数名称,似乎和散列表(Hash)有关,这说明苹果对引用计数的管理应该是通过散列表来执行的。 image

在这张表里,key为内存块地址,而对应的值为引用计数。也就是说,它保存了这样的信息:一些被引用的内存块各自对应的引用计数。

那么使用散列表来管理内存有什么好处呢?

因为计数表保存内存块地址,我们就可以通过这张表来:

  • 确认损坏内存块的位置。
  • 在检测内存泄漏时,可以查看各对象的持有者是否存在。

autorelease

autorelease 介绍

当对象超出其作用域时,对象实例的release方法就会被调用,autorelease的具体使用方法如下:

  1. 生成并持有NSAutoreleasePool对象。
  2. 调用已分配对象的autorelease方法。
  3. 废弃NSAutoreleasePool对象。 image

所有调用过autorelease方法的对象,在废弃NSAutoreleasePool对象时,都将调用release方法(引用计数-1):

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain];//相当于obj调用release方法

NSRunLoop在每次循环过程中,NSAutoreleasePool对象都会被生成或废弃。
也就是说,如果有大量的autorelease变量,在NSAutoreleasePool对象废弃之前(一旦监听到RunLoop即将进入睡眠等待状态,就释放NSAutoreleasePool),都不会被销毁,容易导致内存激增的问题:

for (int i = 0; i < imageArray.count; i++)
{
    UIImage *image = imageArray[i];
    [image doSomething];
}

image

因此,我们有必要在适当的时候再嵌套一个自动释放池来管理临时生成的autorelease变量:

for (int i = 0; i < imageArray.count; i++)
{
    //临时pool
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    UIImage *image = imageArray[i];
    [image doSomething];
    [pool drain];
}

image

可能会出的面试题:什么时候会创建自动释放池?
答:运行循环检测到事件并启动后,就会创建自动释放池,而且子线程的 runloop 默认是不工作的,无法主动创建,必须手动创建。
举个🌰:
自定义的 NSOperation 类中的 main 方法里就必须添加自动释放池。否则在出了作用域以后,自动释放对象会因为没有自动释放池去处理自己而造成内存泄露。

autorelease实现

和上文一样,我们还是通过GNUstep和苹果的实现来分别看一下。

GNUstep 实现

//GNUstep/modules/core/base/Source/NSObject.m autorelease

- (id)autorelease
{
    [NSAutoreleasePool addObject:self];
}

如果调用NSObject类的autorelease方法,则该对象就会被追加到正在使用的NSAutoreleasePool对象中的数组里(作者假想了一个简化的源代码):

//GNUstep/modules/core/base/Source/NSAutoreleasePool.m addObject

+ (void)addObject:(id)anObj
{
    NSAutoreleasePool *pool = 取得正在使用的NSAutoreleasePool对象
    if (pool != nil){
        [pool addObject:anObj];
    }else{
        NSLog(@"NSAutoreleasePool对象不存在");
    }
}

- (void)addObject:(id)anObj
{
    [pool.array addObject:anObj];
}

也就是说,autorelease实例方法的本质就是调用NSAutoreleasePool对象的addObject类方法,然后这个对象就被追加到正在使用的NSAutoreleasePool对象中的数组里。

再来看一下NSAutoreleasePool的drain方法:

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

- (void)dealloc
{
    [self emptyPool];
    [array release];
}

- (void)emptyPool
{
    for(id obj in array){
        [obj release];
    }
}

我们可以看到,在emptyPool方法里,确实是对数组里每一个对象进行了release操作。

苹果的实现

我们可以通过objc4/NSObject.mm来确认苹果中autorelease的实现:

objc4/NSObject.mm AutoreleasePoolPage
 
class AutoreleasePoolPage
{
    static inline void *push()
    {
        //生成或者持有 NSAutoreleasePool 类对象
    }

    static inline void pop(void *token)
    {
        //废弃 NSAutoreleasePool 类对象
        releaseAll();
    }
    
    static inline id autorelease(id obj)
    {
        //相当于 NSAutoreleasePool 类的 addObject 类方法
        AutoreleasePoolPage *page = 取得正在使用的 AutoreleasePoolPage 实例;
       autoreleaesPoolPage->add(obj)
    }

    id *add(id obj)
    {   
        //将对象追加到内部数组中
    }
    
    void releaseAll()
    {
        //调用内部数组中对象的 release 方法
    }
};

//压栈
void *objc_autoreleasePoolPush(void)
{
    if (UseGC) return nil;
    return AutoreleasePoolPage::push();
}
 
//出栈
void objc_autoreleasePoolPop(void *ctxt)
{
    if (UseGC) return;
    AutoreleasePoolPage::pop(ctxt);
}

来看一下外部的调用:

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// 等同于 objc_autoreleasePoolPush
 
id obj = [[NSObject alloc] init];
[obj autorelease];
// 等同于 objc_autorelease(obj)
 
[NSAutoreleasePool showPools];
// 查看 NSAutoreleasePool 状况
 
[pool drain];
// 等同于 objc_autoreleasePoolPop(pool)

看函数名就可以知道,对autorelease分别执行push、pop操作。销毁对象时执行release操作。

可能出现的面试题:苹果是如何实现autoreleasepool的?
autoreleasepool以栈的数据结构实现,主要通过下列三个函数完成.
• objc_autoreleasepoolPush(压入)
• objc_autoreleasepoolPop(弹出)
• objc_autorelease(释放内部)

ARC内存管理

内存管理的思想

上面学习了非ARC机制下的手动管理内存思想,针对引用计数的操作和自动释放池的相关内容。现在学习一下在ARC机制下的相关知识。

ARC和非ARC机制下的内存管理思想是一致的:

  • 自己生成的对象,自己持有。
  • 非自己生成的对象,自己也能持有。
  • 不再需要自己持有的对象时释放对象。
  • 非自己持有的对象无法释放。

在ARC机制下,编译器就可以自动进行内存管理,减少了开发的工作量。但我们有时仍需要四种所有权修饰符来配合ARC来进行内存管理

四种所有权修饰符

但是,在ARC机制下我们有的时候需要追加所有权声明(以下内容摘自官方文档):

  • __strong:is the default. An object remains “alive” as long as there is a strong pointer to it.
  • __weak:specifies a reference that does not keep the referenced object alive. A weak reference is set to nil when there are no strong references to the object.
  • __unsafe_unretained:specifies a reference that does not keep the referenced object alive and is not set to nil when there are no strong references to the object. If the object it references is deallocated, the pointer is left dangling.
  • __autoreleasing:is used to denote arguments that are passed by reference (id *) and are autoreleased on return.

下面分别讲解一下这几个修饰符:

__strong修饰符

__strong修饰符 是id类型和对象类型默认的所有权修饰符:

__strong使用方法:

id obj = [NSObject alloc] init];

等同于:

id __strong obj = [NSObject alloc] init];

看一下内存管理的过程:

{
    id __strong obj = [NSObject alloc] init];//obj持有对象
}
//obj超出其作用域,强引用失效

__strong修饰符表示对对象的强引用。持有强引用的变量在超出其作用域时被废弃。

在__strong修饰符修饰的变量之间相互赋值的情况:

id __strong obj0 = [[NSObject alloc] init];//obj0 持有对象A
id __strong obj1 = [[NSObject alloc] init];//obj1 持有对象B
id __strong obj2 = nil;//ojb2不持有任何对象
obj0 = obj1;//obj0强引用对象B;而对象A不再被ojb0引用,被废弃
obj2 = obj0;//obj2强引用对象B(现在obj0,ojb1,obj2都强引用对象B)
obj1 = nil;//obj1不再强引用对象B
obj0 = nil;//obj0不再强引用对象B
obj2 = nil;//obj2不再强引用对象B,不再有任何强引用引用对象B,对象B被废弃

而且,__strong可以使一个变量初始化为nil:id __strong obj0;
同样适用于:id __weak obj1; id __autoreleasing obj2;

做个总结:被__strong修饰后,相当于强引用某个对象。对象一旦有一个强引用引用自己,引用计数就会+1,就不会被系统废弃。而这个对象如果不再被强引用的话,就会被系统废弃。

__strong内部实现:

生成并持有对象:

{
    id __strong obj = [NSObject alloc] init];//obj持有对象
}

编译器的模拟代码:

id obj = objc_mesgSend(NSObject, @selector(alloc));
objc_msgSend(obj,@selector(init));
objc_release(obj);//超出作用域,释放对象

再看一下使用命名规则以外的构造方法:

{
    id __strong obj = [NSMutableArray array];
}

编译器的模拟代码:

id obj = objc_msgSend(NSMutableArray, @selector(array));
objc_retainAutoreleasedReturnValue(obj);
objc_release(obj);

objc_retainAutoreleasedReturnValue的作用:持有对象,将对象注册到autoreleasepool并返回。

同样也有objc_autoreleaseReturnValue,来看一下它的使用:

+ (id)array
{
   return [[NSMutableArray alloc] init];
}

编译器的模拟代码:

+ (id)array
{
   id obj = objc_msgSend(NSMutableArray, @selector(alloc));
   objc_msgSend(obj,, @selector(init));
   return objc_autoreleaseReturnValue(obj);
}

objc_autoreleaseReturnValue:返回注册到autoreleasepool的对象。

__weak修饰符

__weak使用方法:

__weak修饰符大多解决的是循环引用的问题:如果两个对象都互相强引用对方,同时都失去了外部对自己的引用,那么就会形成“孤岛”,这个孤岛将永远无法被释放,举个🌰:

@interface Test:NSObject
{
    id __strong obj_;
}

- (void)setObject:(id __strong)obj;
@end

@implementation Test
- (id)init
{
    self = [super init];
    return self;
}

- (void)setObject:(id __strong)obj
{
    obj_ = obj;
}
@end


{
    id test0 = [[Test alloc] init];//test0强引用对象A
    id test1 = [[Test alloc] init];//test1强引用对象B
    [test0 setObject:test1];//test0强引用对象B
    [test1 setObject:test0];//test1强引用对象A
}

因为生成对象(第一,第二行)和set方法(第三,第四行)都是强引用,所以会造成两个对象互相强引用对方的情况:

image

所以,我们需要打破其中一种强引用:

@interface Test:NSObject
{
    id __weak obj_;//由__strong变成了__weak
}

- (void)setObject:(id __strong)obj;
@end

这样一来,二者就只是弱引用对方了: image

__weak内部实现

{
    id __weak obj1 = obj;
}

编译器的模拟代码:

 //声明变量obj1
id obj1;
//使用 objc_initWeak 函数将 obj 对象设置为 obj1 的弱引用。它会在内部创建一个弱引用指针,并将其绑定到 obj 对象上
objc_initWeak(&obj1,obj);
//取出附有__weak修饰符变量所引用的对象并retain,这个tmp实际上就是obj指向的对象。
//objc_loadWeakRetained会在加载弱引用时将对象的引用计数加一,以确保对象在使用期间不被提前释放。
id tmp = objc_loadWeakRetained(&obj1);
//将对象注册到autoreleasepool中,autoreleasepool释放的时候会执行release,上面的retain和这里的release后,obj1并没有让对象的引用计数发生变化
objc_autorelease(tmp);
//释放附有__weak的变量。会解除 obj1 与原始对象之间的绑定关系,并将 obj1 设置为 nil。
objc_destroyWeak(&obj1);

这确认了__weak的一个功能:使用附有__weak修饰符的变量,即是使用注册到autoreleasepool中的对象。

这里需要着重讲解一下objc_initWeak方法和objc_destroyWeak方法:

  • objc_initWeak:初始化附有__weak的变量,具体通过执行objc_strongWeak(&obj1, obj)方法,将obj对象以&obj1作为key放入一个weak表(Hash)中。
  • objc_destroyWeak:释放附有__weak的变量。具体通过执行objc_storeWeak(&obj1,0)方法,在weak表中查询&obj1这个键,将这个键从weak表中删除。

注意:因为同一个对象可以赋值给多个附有__weak的变量中,所以对于同一个键值,可以注册多个变量的地址。

当一个对象不再被任何人持有,则需要释放它,过程为:

  • objc_dealloc
  • dealloc
  • _objc_rootDealloc
  • objc_dispose
  • objc_destructInstance
  • objc_clear_deallocating
    • 从weak表中获取废弃对象的地址
    • 将包含在记录中的所有附有__weak修饰符变量的地址赋值为nil
    • 从weak表中删除该记录
    • 从引用计数表中删除废弃对象的地址

weak 表和引用计数表是由运行时系统在维护的。

__autoreleasing修饰符

__autoreleasing使用方法

ARC下,可以用@autoreleasepool来替代NSAutoreleasePool类对象,用__autoreleasing修饰符修饰变量来替代ARC无效时调用对象的autorelease方法(对象被注册到autoreleasepool)。 image

说到__autoreleasing修饰符,就不得不提__weak:

id  __weak obj1 = obj0;
NSLog(@"class = %@",[obj1 class]);

等同于:

id __weak obj1 = obj0;
id __autoreleasing tmp = obj1;
NSLog(@"class = %@",[tmp class]);//实际访问的是注册到自动个释放池的对象

注意一下两段等效的代码里,NSLog语句里面访问的对象是不一样的,它说明:在访问__weak修饰符的变量(obj1)时必须访问注册到autoreleasepool的对象(tmp)。为什么呢?

因为__weak修饰符只持有对象的弱引用,也就是说在将来访问这个对象的时候,无法保证它是否还没有被废弃。因此,如果把这个对象注册到autoreleasepool中,那么在@autoreleasepool块结束之前都能确保该对象存在。

__autoreleasing内部实现

将对象赋值给附有__autoreleasing修饰符的变量等同于ARC无效时调用对象的autorelease方法。

@autoreleasepool{
    id __autoreleasing obj = [[NSObject alloc] init];
}

编译器的模拟代码:

id pool = objc_autoreleasePoolPush();//pool入栈
id obj = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj, @selector(init));
//将先前分配的对象添加到最近的自动释放池中。通过调用objc_autorelease函数,对象的所有权会被转移给自动释放池,而不是当前作用域
objc_autorelease(obj);
objc_autoreleasePoolPop(pool);//pool出栈

在这里我们可以看到pool入栈,执行autorelease,出栈的三个方法。

ARC下的规则

我们知道了在ARC机制下编译器会帮助我们管理内存,但是在编译期,我们还是要遵守一些规则,作者为我们列出了以下的规则:

  1. 不能使用retain/release/retainCount/autorelease
  2. 不能使用NSAllocateObject/NSDeallocateObject
  3. 必须遵守内存管理的方法名规则
  4. 不要显式调用dealloc
  5. 使用@autorelease块代替NSAutoreleasePool
  6. 不能使用区域(NSZone)
  7. 对象型变量不能作为C语言结构体的成员
  8. 显式转换id和void*

1. 不能使用retain/release/retainCount/autorelease

在ARC机制下使用retain/release/retainCount/autorelease方法,会导致编译器报错。

2. 不能使用NSAllocateObject/NSDeallocateObject

在ARC机制下使用NSAllocateObject/NSDeallocateObject方法,会导致编译器报错。

3. 必须遵守内存管理的方法名规则

对象的生成/持有的方法必须遵循以下命名规则:

  • alloc
  • new
  • copy
  • mutableCopy
  • init

前四种方法已经介绍完。而关于init方法的要求则更为严格:

  • 必须是实例方法
  • 必须返回对象
  • 返回对象的类型必须是id类型或方法声明类的对象类型

4. 不要显式调用dealloc

对象被废弃时,无论ARC是否有效,系统都会调用对象的dealloc方法。

我们只能在dealloc方法里写一些对象被废弃时需要进行的操作(例如移除已经注册的观察者对象)但是不能手动调用dealloc方法。

注意在ARC无效的时候,还需要调用[super dealloc]:

- (void)dealloc
{
    //该对象的处理
    [super dealloc];
}

5. 使用@autorelease块代替NSAutoreleasePool

ARC下须使用使用@autorelease块代替NSAutoreleasePool。

6. 不能使用区域(NSZone)

NSZone已经在目前的运行时系统(OBC2被设定的环境)被忽略了。

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

C语言的结构体如果存在Objective-C对象型变量,便会引起错误,因为C语言在规约上没有方法来管理结构体成员的生存周期 。

8. 显式转换id和void*

非ARC下,这两个类型是可以直接赋值的

id obj = [NSObject alloc] init];
void *p = obj;
id o = p;

但是在ARC下就会引起编译错误。为了避免错误,我们需要通过__bridege来转换。

id obj = [[NSObject alloc] init];
void *p = (__bridge void*)obj;//显式转换
id o = (__bridge id)p;//显式转换

属性

来看一下属性的声明与所有权修饰符的关系

属性关键字 所有权 修饰符
assign __unsafe_unretained
copy __strong
retain __strong
strong __strong
__unsafe_unretained __unsafe_unretained
weak __weak

说一下__unsafe_unretained: __unsafe_unretained表示存取方法会直接为实例变量赋值。

这里的“unsafe”是相对于weak而言的。我们知道weak指向的对象被销毁时,指针会自动设置为nil。而__unsafe_unretained却不会,而是成为空指针。需要注意的是:当处理非对象属性的时候就不会出现空指针的问题。

Block篇

需要先知道的

Objective-C 转 C++的方法

因为需要看Block操作的C++源码,所以需要知道转换的方法,自己转过来看一看:

  1. 在OC源文件block.m写好代码。
  2. 打开终端,cd到block.m所在文件夹。
  3. 输入clang -rewrite-objc block.m,就会在当前文件夹内自动生成对应的block.cpp文件。

关于几种变量的特点

c语言的函数中可能使用的变量:

  • 函数的参数
  • 自动变量(局部变量)
  • 静态变量(静态局部变量)
  • 静态全局变量
  • 全局变量

而且,由于存储区域特殊,这其中有三种变量是可以在任何时候以任何状态调用的:

  • 静态变量
  • 静态全局变量
  • 全局变量

而其他两种,则是有各自相应的作用域,超过作用域后,会被销毁。

好了,知道了这两点,理解下面的内容就容易一些了。

Block的实质

先说结论:Block实质是Objective-C对闭包的对象实现,简单说来,Block就是对象。

下面分别从表层到底层来分析一下:

表层分析Block的实质:它是一个类型

Block是一种类型,一旦使用了Block就相当于生成了可赋值给Block类型变量的值。举个例子:

int (^blk)(int) = ^(int count){
        return count + 1;
};
  • 等号左侧的代码表示了这个Block的类型:它接受一个int参数,返回一个int值。
  • 等号右侧的代码是这个Block的值:它是等号左侧定义的block类型的一种实现。

如果我们在项目中经常使用某种相同类型的block,我们可以用typedef来抽象出这种类型的Block:

typedef int(^AddOneBlock)(int count);

AddOneBlock block = ^(int count){
        return count + 1;//具体实现代码
};

这样一来,block的赋值和传递就变得相对方便一些了, 因为block的类型已经抽象了出来。

深层分析Block的实质:它是Objective-C对象

Block其实就是Objective-C对象,因为它的结构体中含有isa指针。

下面将Objective-C的代码转化为C++的代码来看一下block的实现。

OC代码:

int main()
{
    void (^blk)(void) = ^{
        printf("Block\n");
    };
    return 0;
}

C++代码:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

//block结构体
struct __main_block_impl_0 {
    
  struct __block_impl impl;
    
  struct __main_block_desc_0* Desc;
  
  //Block构造函数
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;//isa指针
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
    
};

//将来被调用的block内部的代码:block值被转换为C的函数代码
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

        printf("Block\n");
}

static struct __main_block_desc_0 {
    
  size_t reserved;
  size_t Block_size;
    
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

//main 函数
int main()
{
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    return 0;
}

首先我们看一下从原来的block值(OC代码块)转化而来的C++代码:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

    printf("Block\n");
}

这里,*__cself 是指向Block的值的指针,也就相当于是Block的值它自己(相当于C++里的this,OC里的self)。

而且很容易看出来,__cself 是指向__main_block_impl_0结构体实现的指针。
结合上句话,也就是说Block结构体就是__main_block_impl_0结构体。Block的值就是通过__main_block_impl_0构造出来的。

下面来看一下这个结构体的声明:

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;
  }
};

可以看出,__main_block_impl_0结构体有三个部分:

第一个是成员变量impl,它是实际的函数指针,它指向__main_block_func_0。来看一下它的结构体的声明:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;  //今后版本升级所需的区域
  void *FuncPtr; //函数指针
};

第二个是成员变量是指向__main_block_desc_0结构体的Desc指针,是用于描述当前这个block的附加信息的,包括结构体的大小等等信息

static struct __main_block_desc_0 {
    
  size_t reserved;  //今后升级版本所需区域
  size_t Block_size;//block的大小
    
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

第三个部分是__main_block_impl_0结构体的构造函数,__main_block_impl_0 就是该 block 的实现

__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;
  }

在这个结构体的构造函数里,isa指针保持这所属类的结构体的实例的指针。__main_block_imlp_0结构体就相当于Objective-C类对象的结构体,这里的_NSConcreteStackBlock相当于Block的结构体实例,也就是说block其实就是Objective-C对于闭包的对象实现

Block截获自动变量和对象

Block截获自动变量(局部变量)

使用Block的时候,不仅可以使用其内部的参数,还可以使用Block外部的局部变量。而一旦在Block内部使用了其外部变量,这些变量就会被Block保存。

有趣的是,即使在Block外部修改这些变量,存在于Block内部的这些变量也不会被修改。来看一下代码:

int a = 10;
int b = 20;
    
PrintTwoIntBlock block = ^(){
    printf("%d, %d\n",a,b);
};
    
block();//10 20
    
a += 10;
b += 30;
    
printf("%d, %d\n",a,b);//20 50
    
block();//10 20

我们可以看到,在外部修改a,b的值以后,再次调用block时,里面的打印仍然和之前是一样的。给人的感觉是,外部到局部变量和被Block内部截获的变量并不是同一份。

那如果在内部修改a,b的值会怎么样呢?

int a = 10;
int b = 20;
    
PrintTwoIntBlock block = ^(){
    //编译不通过
    a = 30;
    b = 10;
};
    
block();

如果不进行额外操作,局部变量一旦被Block保存,在Block内部就不能被修改了。

但是需要注意的是,这里的修改是指整个变量的赋值操作,变更该对象的操作是允许的,比如在不加上__block修饰符的情况下,给在block内部的可变数组添加对象的操作是可以的。

NSMutableArray *array = [[NSMutableArray alloc] init];
    
NSLog(@"%@",array); //@[]
    
PrintTwoIntBlock block = ^(){
    [array addObject:@1];
};
    
block();
    
NSLog(@"%@",array);//@[1]

OK,现在我们知道了三点:

  1. Block可以截获局部变量。
  2. 修改Block外部的局部变量,Block内部被截获的局部变量不受影响。
  3. 修改Block内部到局部变量,编译不通过。

为了解释2,3点,我们通过C++的代码来看一下Block在截获变量的时候都发生了什么:
C代码:

int main()
{
    int dmy = 256;
    int val = 10;
    
    const char *fmt = "var = %d\n";
    
    void (^blk)(void) = ^{
        printf(fmt,val);
    };
    
    val = 2;
    fmt = "These values were changed. var = %d\n";
    
    blk();
    
    return 0;
}

C++代码:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  
  const char *fmt;  //被添加
  int val;          //被添加
  
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags=0) : fmt(_fmt), val(_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  const char *fmt = __cself->fmt; // bound by copy
  int val = __cself->val; // bound by copy

        printf(fmt,val);
    }

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

int main()
{
    int dmy = 256;
    int val = 10;

    const char *fmt = "var = %d\n";

    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val));

    val = 2;
    fmt = "These values were changed. var = %d\n";

    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

    return 0;
}

单独抽取__main_block_impl_0来看一下:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  const char *fmt; //截获的自动变量
  int val;         //截获的自动变量
  
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags=0) : fmt(_fmt), val(_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
  1. 我们可以看到,在block内部语法表达式中使用的自动变量(fmt,val)被作为成员变量追加到了__main_block_impl_0结构体中(注意:block没有使用的自动变量不会被追加,如dmy变量)。
  2. 在初始化block结构体实例时(请看__main_block_impl_0的构造函数),还需要截获的自动变量fmt和val来初始化__main_block_impl_0结构体实例,因为增加了被截获的自动变量,block的体积会变大。

再来看一下函数体的代码:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

  const char *fmt = __cself->fmt; // bound by copy
  int val = __cself->val; // bound by copy
  printf(fmt,val);
}

从这里看就更明显了:fmt,var都是从__cself里面获取的,更说明了二者是属于block的。而且从注释来看(注释是由clang自动生成的),这两个变量是值传递,而不是指针传递,也就是说Block仅仅截获自动变量的值,所以这就解释了即使改变了外部的自动变量的值,也不会影响Block内部的值

那为什么在默认情况下改变Block内部到变量会导致编译不通过呢?
我的思考是:既然我们无法在Block中改变外部变量的值,所以也就没有必要在Block内部改变变量的值了,因为Block内部和外部的变量实际上是两种不同的存在:前者是Block内部结构体的一个成员变量,后者是在栈区里的临时变量。

现在我们知道:被截获的自动变量的值是无法直接修改的,但是有两个方法可以解决这个问题:

  1. 改变存储于特殊存储区域的变量。
  2. 通过__block修饰符来改变。

1. 改变存储于特殊存储区域的变量

  • 全局变量,可以直接访问。
  • 静态全局变量,可以直接访问。
  • 静态变量,直接指针引用。

我们还是用OC和C++代码的对比看一下具体的实现:

OC代码:

int global_val = 1;//全局变量
static int static_global_val = 2;//全局静态变量

int main()
{
    static int static_val = 3;//静态变量
    
    void (^blk)(void) = ^{
        global_val *=1;
        static_global_val *=2;
        static_val *=3;
    };
    return 0;
}

C++代码:

int global_val = 1;
static int static_global_val = 2;


struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *static_val;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_val, int flags=0) : static_val(_static_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};


static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

  int *static_val = __cself->static_val; // bound by copy

  global_val *=1;
  static_global_val *=2;
  (*static_val) *=3;
}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

int main()
{
    static int static_val = 3;

    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_val));
    return 0;
}

我们可以看到,

  • 全局变量和全局静态变量没有被截获到block里面,它们的访问是不经过block的(与__cself无关):

    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

    int *static_val = __cself->static_val; // bound by copy

    global_val *=1; static_global_val *=2; (*static_val) *=3; }

  • 访问静态变量(static_val)时,将静态变量的指针传递给__main_block_impl_0结构体的构造函数并保存:

    struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int *static_val;//是指针,不是值

    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_val, int flags=0) : static_val(_static_val) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } };

那么有什么方法可以在Block内部给变量赋值呢?-- 通过__block关键字。在讲解__block关键字之前,讲解一下Block截获对象:

Block截获对象

我们看一下在block里截获了array对象的代码,array超过了其作用域存在:

blk_t blk;
{
    id array = [NSMutableArray new];
    blk = [^(id object){
        [array addObject:object];
        NSLog(@"array count = %ld",[array count]);
            
    } copy];
}
    
blk([NSObject new]);
blk([NSObject new]);
blk([NSObject new]);

输出:

block_demo[28963:1629127] array count = 1
block_demo[28963:1629127] array count = 2
block_demo[28963:1629127] array count = 3

看一下C++代码:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  id array;//截获的对象
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, id _array, int flags=0) : array(_array) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

值得注意的是,在OC中,C结构体里不能含有被__strong修饰的变量,因为编译器不知道应该何时初始化和废弃C结构体。但是OC的运行时库能够准确把握Block从栈复制到堆,以及堆上的block被废弃的时机,在实现上是通过__main_block_copy_0函数和__main_block_dispose_0函数进行的:

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign((void*)&dst->array, (void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

其中,_Block_object_assign相当于retain操作,将对象赋值在对象类型的结构体成员变量中。
_Block_object_dispose相当于release操作。

这两个函数调用的时机是在什么时候呢?

函数

被调用时机

__main_block_copy_0

从栈复制到堆时

__main_block_dispose_0

堆上的Block被废弃时

什么时候栈上的Block会被复制到堆呢?

  • 调用block的copy函数时
  • Block作为函数返回值返回时
  • 将Block赋值给附有__strong修饰符id类型的类或者Block类型成员变量时
  • 方法中含有usingBlock的Cocoa框架方法或者GCD的API中传递Block时

什么时候Block被废弃呢?

堆上的Block被释放后,谁都不再持有Block时调用dispose函数。

__weak关键字:

{
        id array = [NSMutableArray new];
        id __weak array2 = array;
        blk = ^(id object){
            [array2 addObject:object];
            NSLog(@"array count = %ld",[array2 count]);
        };
    }
    
    blk([NSObject new]);
    blk([NSObject new]);
    blk([NSObject new]);

输出:

 block_demo[32084:1704240] array count = 0
 block_demo[32084:1704240] array count = 0
 block_demo[32084:1704240] array count = 0

因为array在变量作用域结束时被释放,nil被赋值给了array2中。

__block的实现原理

__block修饰局部变量

先通过OC代码来看一下给局部变量添加__block关键字后的效果:

__block int a = 10;
int b = 20;
    
PrintTwoIntBlock block = ^(){
    a -= 10;
    printf("%d, %d\n",a,b);
};
    
block();//0 20
    
a += 20;
b += 30;
    
printf("%d, %d\n",a,b);//20 50
    
block();/10 20

我们可以看到,__block变量在block内部就可以被修改了。

加上__block之后的变量称之为__block变量,

先简单说一下__block的作用:
__block说明符用于指定将变量值设置到哪个存储区域中,也就是说,当自动变量加上__block说明符之后,会改变这个自动变量的存储区域。

接下来我们还是用clang工具看一下C++的代码:

OC代码

int main()
{
    __block int val = 10;
    
    void (^blk)(void) = ^{
        val = 1;
    };
    return 0;
}

C++代码

struct __Block_byref_val_0 {
  void *__isa;
__Block_byref_val_0 *__forwarding;
 int __flags;
 int __size;
 int val;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_val_0 *val; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_val_0 *val = __cself->val; // bound by ref

        (val->__forwarding->val) = 1;
    }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
int main()
{
    __attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};

    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));
    return 0;
}

在__main_block_impl_0里面发生了什么呢?

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_val_0 *val; // by ref
  
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

>__main_block_impl_0里面增加了一个成员变量,它是一个结构体指针,指向了 __Block_byref_val_0结构体的一个实例。那么这个结构体是什么呢?

这个结构体是变量val在被__block修饰后生成的!!
该结构体声明如下:
```objc
struct __Block_byref_val_0 {
  void *__isa;
__Block_byref_val_0 *__forwarding;
 int __flags;
 int __size;
 int val;
};

我们可以看到,这个结构体最后的成员变量就相当于原来自动变量。
这里有两个成员变量需要特别注意:

  1. val:保存了最初的val变量,也就是说原来单纯的int类型的val变量被__block修饰后生成了一个结构体。这个结构体其中一个成员变量持有原来的val变量。
  2. __forwarding:通过__forwarding,可以实现无论__block变量配置在栈上还是堆上都能正确地访问__block变量,也就是说__forwarding是指向自身的。

用一张图来直观看一下: image

怎么实现的?

  1. 最初,__block变量在栈上时,它的成员变量__forwarding指向栈上的__block变量结构体实例。
  2. 在__block被复制到堆上时,会将__forwarding的值替换为堆上的目标__block变量用结构体实例的地址。而在堆上的目标__block变量自己的__forwarding的值就指向它自己。

我们可以看到,这里面增加了指向__Block_byref_val_0结构体实例的指针。这里//by ref这个由clang生成的注释,说明它是通过指针来引用__Block_byref_val_0结构体实例val的。

因此__Block_byref_val_0结构体并不在__main_block_impl_0结构体中,目的是为了使得多个Block中使用__block变量。

举个例子:

int main()
{
    __block int val = 10;
    
    void (^blk0)(void) = ^{
        val = 12;
    };
    
    void (^blk1)(void) = ^{
        val = 13;
    };
    return 0;
}


int main()
{
    __attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};

    void (*blk0)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));

    void (*blk1)(void) = ((void (*)())&__main_block_impl_1((void *)__main_block_func_1, &__main_block_desc_1_DATA, (__Block_byref_val_0 *)&val, 570425344));
    return 0;
}

我们可以看到,在main函数里,两个block都引用了__Block_byref_val_0结构体的实例val。

那么__block修饰对象的时候是怎么样的呢?

__block修饰对象

__block可以指定任何类型的自动变量。下面来指定id类型的对象:

看一下__block变量的结构体:

struct __Block_byref_obj_0 {
  void *__isa;
__Block_byref_obj_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 id obj;
};

被__strong修饰的id类型或对象类型自动变量的copy和dispose方法:

static void __Block_byref_id_object_copy_131(void *dst, void *src) {
 _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}


static void __Block_byref_id_object_dispose_131(void *src) {
 _Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}

同样,当Block持有被__strong修饰的id类型或对象类型自动变量时:

  • 如果__block对象变量从栈复制到堆时,使用_Block_object_assign函数,

  • 当堆上的__block对象变量被废弃时,使用_Block_object_dispose函数。

    struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; __Block_byref_obj_0 *obj; // by ref __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_obj_0 *_obj, int flags=0) : obj(_obj->__forwarding) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } };

可以看到,obj被添加到了__main_block_impl_0结构体中,它是__Block_byref_obj_0类型。

三种Block

细心的同学会发现,在上面Block的构造函数__main_block_impl_0中的isa指针指向的是&_NSConcreteStackBlock,它表示当前的Block位于栈区中。实际上,一共有三种类型的Block:

Block的类

存储域

拷贝效果

_NSConcreteStackBlock

从栈拷贝到堆

_NSConcreteGlobalBlock

程序的数据区域

什么也不做

_NSConcreteMallocBlock

引用计数增加

全局Block:_NSConcreteGlobalBlock

因为全局Block的结构体实例设置在程序的数据存储区,所以可以在程序的任意位置通过指针来访问,它的产生条件:

  • 记述全局变量的地方有block语法时。
  • block不截获的自动变量时。

以上两个条件只要满足一个就可以产生全局Block,下面分别用C++来展示一下第一种条件下的全局Block:

c代码:

void (^blk)(void) = ^{printf("Global Block\n");};

int main()
{
    blk();
}

C++代码:

struct __blk_block_impl_0 {
  struct __block_impl impl;
  struct __blk_block_desc_0* Desc;
  __blk_block_impl_0(void *fp, struct __blk_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteGlobalBlock;//全局
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __blk_block_func_0(struct __blk_block_impl_0 *__cself) {
printf("Global Block\n");}

static struct __blk_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __blk_block_desc_0_DATA = { 0, sizeof(struct __blk_block_impl_0)};

static __blk_block_impl_0 __global_blk_block_impl_0((void *)__blk_block_func_0, &__blk_block_desc_0_DATA);
void (*blk)(void) = ((void (*)())&__global_blk_block_impl_0);

int main()
{
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
}

我们可以看到Block结构体构造函数里面isa指针被赋予的是&_NSConcreteGlobalBlock,说明它是一个全局Block。

栈Block:_NSConcreteStackBlock

在生成Block以后,如果这个Block不是全局Block,那么它就是为_NSConcreteStackBlock对象,但是如果其所属的变量作用域名结束,该block就被废弃。在栈上的__block变量也是如此。

但是,如果Block变量和__block变量复制到了堆上以后,则不再会受到变量作用域结束的影响了,因为它变成了堆Block:

堆Block:_NSConcreteMallocBlock

将栈block复制到堆以后,block结构体的isa成员变量变成了_NSConcreteMallocBlock。

其他两个类型的Block在被复制后会发生什么呢?

Block类型

存储位置

copy操作的影响

_NSConcreteGlobalBlock

程序的数据区域

什么也不做

_NSConcreteStackBlock

从栈拷贝到堆

_NSConcreteMallocBlock

引用计数增加

而大多数情况下,编译器会进行判断,自动将block从栈上复制到堆:

  • block作为函数值返回的时候
  • 部分情况下向方法或函数中传递block的时候
    • Cocoa框架的方法而且方法名中含有usingBlock等时。
    • Grand Central Dispatch 的API。

除了这两种情况,基本都需要我们手动复制block。

那么__block变量在Block执行copy操作后会发生什么呢?

  1. 任何一个block被复制到堆上时,__block变量也会一并从栈复制到堆上,并被该Block持有。
  2. 如果接着有其他Block被复制到堆上的话,被复制的Block会持有__block变量,并增加__block的引用计数,反过来如果Block被废弃,它所持有的__block也就被释放(不再有block引用它)。

Block循环引用

如果在Block内部使用__strong修饰符的对象类型的自动变量,那么当Block从栈复制到堆的时候,该对象就会被Block所持有。

所以如果这个对象还同时持有Block的话,就容易发生循环引用。

typedef void(^blk_t)(void);

@interface Person : NSObject
{
    blk_t blk_;
}

@implementation Person

- (instancetype)init
{
    self = [super init];
    blk_ = ^{
        NSLog(@"self = %@",self);
    };
    return self;
}

@end

Block blk_t持有self,而self也同时持有作为成员变量的blk_t

__weak修饰符

- (instancetype)init
{
    self = [super init];
    id __weak weakSelf = self;
    blk_ = ^{
        NSLog(@"self = %@",weakSelf);
    };
    return self;
}


typedef void(^blk_t)(void);

@interface Person : NSObject
{
    blk_t blk_;
    id obj_;
}

@implementation Person
- (instancetype)init
{
    self = [super init];
    blk_ = ^{
        NSLog(@"obj_ = %@",obj_);//循环引用警告
    };
    return self;
}

Block语法内的obj_截获了self,因为ojb_是self的成员变量,因此,block如果想持有obj_,就必须引用先引用self,所以同样会造成循环引用。就好比你如果想去某个商场里的咖啡厅,就需要先知道商场在哪里一样。

如果某个属性用的是weak关键字呢?

@interface Person()
@property (nonatomic, weak) NSArray *array;
@end

@implementation Person

- (instancetype)init
{
    self = [super init];
    blk_ = ^{
        NSLog(@"array = %@",_array);//循环引用警告
    };
    return self;
}

还是会有循环引用的警告提示,因为循环引用的是self和block之间的事情,这个被Block持有的成员变量是strong还是weak都没有关系,而且即使是基本类型(assign)也是一样。

@interface Person()
@property (nonatomic, assign) NSInteger index;
@end

@implementation Person

- (instancetype)init
{
    self = [super init];
    blk_ = ^{
        NSLog(@"index = %ld",_index);//循环引用警告
    };
    return self;
}

__block修饰符

- (instancetype)init
{
    self = [super init];
    __block id temp = self;//temp持有self
    
    //self持有blk_
    blk_ = ^{
        NSLog(@"self = %@",temp);//blk_持有temp
        temp = nil;
    };
    return self;
}

- (void)execBlc
{
    blk_();
}

所以如果不执行blk_(将temp设为nil),则无法打破这个循环。

一旦执行了blk_,就只有

  • self持有blk_
  • blk_持有temp

使用__block 避免循环比较有什么特点呢?

  • 通过__block可以控制对象的持有时间。
  • 为了避免循环引用必须执行block,否则循环引用一直存在。

所以我们应该根据实际情况,根据当前Block的用途来决定到底用__block,还是__weak或__unsafe_unretained。

GCD篇

Demo地址:gcd函数demo

队列

Dispatch Queue是执行处理的等待队列,按照任务(block)追加到队列里的顺序,先进先出执行处理。

而等待队列有两种

  • Serial Dispatch Queue:串行队列,等待当前执行任务处理结束的队列。
  • Concurrent Dispatch Queue:并发队列,不等待当前执行任务处理结束的队列。

串行队列

将任务追加到串行队列:

- (void)serialQueue
{
    dispatch_queue_t queue = dispatch_queue_create("serial queue", NULL);
    for (NSInteger index = 0; index < 6; index ++) {
        dispatch_async(queue, ^{
            NSLog(@"task index %ld in serial queue",index);
        });
    }
}

输出:

gcd_demo[33484:2481120] task index 0 in serial queue
gcd_demo[33484:2481120] task index 1 in serial queue
gcd_demo[33484:2481120] task index 2 in serial queue
gcd_demo[33484:2481120] task index 3 in serial queue
gcd_demo[33484:2481120] task index 4 in serial queue
gcd_demo[33484:2481120] task index 5 in serial queue

通过dispatch_queue_create函数可以创建队列,第一个函数为队列的名称,第二个参数是NULLDISPATCH_QUEUE_SERIAL时,返回的队列就是串行队列。

为了避免重复代码,我在这里使用了for循环,将任务追加到了queue中。

注意,这里的任务是按照顺序执行的。说明任务是以阻塞的形式执行的:必须等待上一个任务执行完成才能执行现在的任务。也就是说:一个Serial Dispatch Queue中同时只能执行一个追加处理(任务block),而且系统对于一个Serial Dispatch Queue只生成并使用一个线程。

但是,如果我们将6个任务分别追加到6个Serial Dispatch Queue中,那么系统就会同时处理这6个任务(因为会另开启6个子线程):

- (void)multiSerialQueue
{
    for (NSInteger index = 0; index < 10; index ++) {
        //新建一个serial queue
        dispatch_queue_t queue = dispatch_queue_create("different serial queue", NULL);
        dispatch_async(queue, ^{
            NSLog(@"serial queue index : %ld",index);
        });
    }
}

输出结果:

gcd_demo[33576:2485282] serial queue index : 1
gcd_demo[33576:2485264] serial queue index : 0
gcd_demo[33576:2485267] serial queue index : 2
gcd_demo[33576:2485265] serial queue index : 3
gcd_demo[33576:2485291] serial queue index : 4
gcd_demo[33576:2485265] serial queue index : 5

从输出结果可以看出来,这里的6个任务并不是按顺序执行的。

需要注意的是:一旦开发者新建了一个串行队列,并使用异步函数(dispatch_async),那么系统一定会开启一个子线程(这里感谢lmh_同学指正~),所以在使用串行队列的时候,一定只创建真正需要创建的串行队列,避免资源浪费。

并发队列

将任务追加到并发队列:

- (void)concurrentQueue
{
    dispatch_queue_t queue = dispatch_queue_create("concurrent queue", DISPATCH_QUEUE_CONCURRENT);
    for (NSInteger index = 0; index < 6; index ++) {
        dispatch_async(queue, ^{
            NSLog(@"task index %ld in concurrent queue",index);
        });
    }
}

输出结果:

gcd_demo[33550:2484160] task index 1 in concurrent queue
gcd_demo[33550:2484159] task index 0 in concurrent queue
gcd_demo[33550:2484162] task index 2 in concurrent queue
gcd_demo[33550:2484182] task index 3 in concurrent queue
gcd_demo[33550:2484183] task index 4 in concurrent queue
gcd_demo[33550:2484160] task index 5 in concurrent queue

可以看到,dispatch_queue_create函数的第二个参数是DISPATCH_QUEUE_CONCURRENT

注意,这里追加到并发队列的6个任务并不是按照顺序执行的,符合上面并发队列的定义。

扩展知识:iOS和OSX基于Dispatch Queue中的处理数,CPU核数,以及CPU负荷等当前系统的状态来决定Concurrent Dispatch Queue中并发处理的任务数。

队列的命名

现在我们知道dispatch_queue_create方法第一个参数指定了这个新建队列的名称,推荐使用逆序quan cheng全程域名(FQDN,fully qualified domain name)。这个名称可以在Xcode和CrashLog中显示出来,对bug的追踪很有帮助。

在继续讲解之前做个小总结,现在我们知道了:

  • 如何创建串行队列和并发队列。
  • 将任务追加到这两种队列里以后的执行效果。
  • 将任务追加到多个串行队列会使这几个任务在不同的线程执行。

实际上,系统给我们提供了两种特殊的队列,分别对应串行队列和并发队列:

系统提供的队列

Main Dispatch Queue

主队列:放在这个队列里的任务会追加到主线程的RunLoop中执行。需要刷新UI的时候我们可以直接获取这个队列,将任务追加到这个队列中。

Globle Dispatch Queue

全局并发队列:开发者可以不需要特意通过dispatch_queue_create方法创建一个Concurrent Dispatch Queue,可以将任务直接放在这个全局并发队列里面。

有一个常见的例子可以充分体现二者的使用方法:

//获取全局并发队列进行耗时操作 
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

          //加载图片
          NSData *dataFromURL = [NSData dataWithContentsOfURL:imageURL];
          UIImage *imageFromData = [UIImage imageWithData:dataFromURL];

      dispatch_async(dispatch_get_main_queue(), ^{

              //获取主队列,在图片加载完成后更新UIImageView
              UIImageView *imageView = [[UIImageView alloc] initWithImage:imageFromData];          
      });      
  });

GCD的各种函数

dispatch_set_target_queue

这个函数有两个作用:

  1. 改变队列的优先级。
  2. 防止多个串行队列的并发执行。

改变队列的优先级

dispatch_queue_create方法生成的串行队列合并发队列的优先级都是与默认优先级的Globle Dispatch Queue一致。

如果想要变更某个队列的优先级,需要使用dispatch_set_target_queue函数。
举个🌰:创建一个在后台执行动作处理的Serial Dispatch Queue

//需求:生成一个后台的串行队列
- (void)changePriority
{
    dispatch_queue_t queue = dispatch_queue_create("queue", NULL);
    dispatch_queue_t bgQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
    
    //第一个参数:需要改变优先级的队列;
    //第二个参数:目标队列
    dispatch_set_target_queue(queue, bgQueue);
}

防止多个串行队列的并发执行

有时,我们将不能并发执行的处理追加到多个Serial Dispatch Queue中时,可以使用dispatch_set_target_queue函数将目标函数定为某个Serial Dispatch Queue,就可以防止这些处理的并发执行。

代码:

 NSMutableArray *array = [NSMutableArray array];
for (NSInteger index = 0; index < 5; index ++) {
        //5个串行队列
        dispatch_queue_t serial_queue = dispatch_queue_create("serial_queue", NULL);
        [array addObject:serial_queue];
}
    
[array enumerateObjectsUsingBlock:^(dispatch_queue_t queue, NSUInteger idx, BOOL * _Nonnull stop) {
        
    dispatch_async(queue, ^{
        NSLog(@"任务%ld",idx);
    });
}];

输出:

gcd_demo[40329:2999714] 任务1
gcd_demo[40329:2999726] 任务0
gcd_demo[40329:2999717] 任务2
gcd_demo[40329:2999715] 任务3
gcd_demo[40329:2999730] 任务4

我们可以看到,如果仅仅是将任务追加到5个串行队列中,那么这些任务就会并发执行。

那接下来看看使用dispatch_set_target_queue方法以后:

//多个串行队列,设置了target queue
NSMutableArray *array = [NSMutableArray array];
dispatch_queue_t serial_queue_target = dispatch_queue_create("queue_target", NULL);

for (NSInteger index = 0; index < 5; index ++) {
      
    //分别给每个队列设置相同的target queue  
    dispatch_queue_t serial_queue = dispatch_queue_create("serial_queue", NULL);
    dispatch_set_target_queue(serial_queue, serial_queue_target);
    [array addObject:serial_queue];
}
    
[array enumerateObjectsUsingBlock:^(dispatch_queue_t queue, NSUInteger idx, BOOL * _Nonnull stop) {
        
    dispatch_async(queue, ^{
        NSLog(@"任务%ld",idx);
    });
}];

输出:

gcd_demo[40408:3004382] 任务0
gcd_demo[40408:3004382] 任务1
gcd_demo[40408:3004382] 任务2
gcd_demo[40408:3004382] 任务3
gcd_demo[40408:3004382] 任务4

很显然,这些任务就按顺序执行了。

dispatch_after

dispatch_after解决的问题:某个线程里,在指定的时间后处理某个任务:

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    NSLog(@"三秒之后追加到队列");
});

注意:不是在3秒之后处理任务,准确来说是3秒之后追加到队列。所以说,如果这个线程的runloop执行1/60秒一次,那么这个block最快会在3秒后执行,最慢会在(3+1/60)秒后执行。而且,如果这个队列本身还有延迟,那么这个block的延迟执行时间会更多。

dispatch_group

如果遇到这样到需求:全部处理完多个预处理任务(block_1 ~ 4)后执行某个任务(block_finish),我们有两个方法:

  • 如果预处理任务需要一个接一个的执行:将所有需要先处理完的任务追加到Serial Dispatch Queue中,并在最后追加最后处理的任务(block_finish)。
  • 如果预处理任务需要并发执行:需要使用dispatch_group函数,将这些预处理的block追加到global dispatch queue中。

分别详细讲解一下两种需求的实现方式:

预处理任务需要一个接一个的执行:

这个需求的实现方式相对简单一点,只要将所有的任务(block_1 ~ 4 + block_finish)放在一个串行队列中即可,因为都是按照顺序执行的,只要不做多余的事情,这些任务就会乖乖地按顺序执行。

预处理任务需要一个接一个的执行:

dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (NSInteger index = 0; index < 5; index ++) {
        dispatch_group_async(group, queue, ^{
            NSLog(@"任务%ld",index);
        });
}
    
dispatch_group_notify(group, queue, ^{
    NSLog(@"最后的任务");
});

输出:

gcd_demo[40905:3057237] 任务0
gcd_demo[40905:3057235] 任务1
gcd_demo[40905:3057234] 任务2
gcd_demo[40905:3057253] 任务3
gcd_demo[40905:3057237] 任务4
gcd_demo[40905:3057237] 最后的任务

因为这些预处理任务都是追加到global dispatch queue中的,所以这些任务的执行任务的顺序是不定的。但是最后的任务一定是最后输出的。

dispatch_group_notify函数监听传入的group中任务的完成,等这些任务全部执行以后,再将第三个参数(block)追加到第二个参数的queue(相同的queue)中。

dispatch_group_wait

dispatch_group_wait 也是配合dispatch_group 使用的,利用这个函数,我们可以设定group内部所有任务执行完成的超时时间。

一共有两种情况:超时的情况和没有超时的情况:

超时的情况:

- (void)dispatch_wait_1
{
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    for (NSInteger index = 0; index < 5; index ++) {
        dispatch_group_async(group, queue, ^{
            for (NSInteger i = 0; i< 1000000000; i ++) {
                
            }
            NSLog(@"任务%ld",index);
        });
    }
    
    dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 1ull * NSEC_PER_SEC);
    long result = dispatch_group_wait(group, time);
    if (result == 0) {
        
        NSLog(@"group内部的任务全部结束");
        
    }else{
        
        NSLog(@"虽然过了超时时间,group还有任务没有完成");
    }
    
}

输出:

gcd_demo[41277:3087481] 虽然过了超时时间,group还有任务没有完成,结果是判定为超时
gcd_demo[41277:3087563] 任务0
gcd_demo[41277:3087564] 任务2
gcd_demo[41277:3087579] 任务3
gcd_demo[41277:3087566] 任务1
gcd_demo[41277:3087563] 任务4

没有超时的情况:

- (void)dispatch_wait_2
{
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    for (NSInteger index = 0; index < 5; index ++) {
        dispatch_group_async(group, queue, ^{
            for (NSInteger i = 0; i< 100000000; i ++) {
                
            }
            NSLog(@"任务%ld",index);
        });
    }
    
    dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 1ull * NSEC_PER_SEC);
    long result = dispatch_group_wait(group, time);
    if (result == 0) {
        
        NSLog(@"group内部的任务全部结束");
        
    }else{
        
        NSLog(@"虽然过了超时时间,group还有任务没有完成");
    }
    
}

输出:

gcd_demo[41357:3092079] 任务2
gcd_demo[41357:3092076] 任务3
gcd_demo[41357:3092092] 任务1
gcd_demo[41357:3092077] 任务0
gcd_demo[41357:3092079] 任务4
gcd_demo[41357:3091956] group内部的任务全部结束,在超时的时间以内完成,结果判定为没有超时

注意:
一旦调用dispatch_group_wait以后,当经过了函数中指定的超时时间后 或者 指定的group内的任务全部执行后会返回这个函数的结果:

  • 经过了函数中指定的超时时间后,group内部的任务没有全部完成,判定为超时,否则,没有超时
  • 指定的group内的任务全部执行后,经过的时间长于超时时间,判定为超时,否则,没有超时。

也就是说:
如果指定的超时时间为DISPATCH_TIME_NOW,那么则没有等待,立即判断group内的任务是否完成。

可以看出,指定的超时时间为DISPATCH_TIME_NOW的时候相当于dispatch_group_notify函数的使用:判断group内的任务是否都完成。

然而dispatch_group_notify函数是作者推荐的,因为通过这个函数可以直接设置最后任务所被追加的队列,使用起来相对比较方便。

dispatch_barrier_async

关于解决数据竞争的方法:读取处理是可以并发的,但是写入处理却是不允许并发执行的。

所以合理的方案是这样的:

  • 读取处理追加到concurrent dispatch queue中
  • 写入处理在任何一个读取处理没有执行的状态下,追加到serial dispatch queue中(也就是说,在写入处理结束之前,读取处理不可执行)。

我们看看如何使用dispatch_barrier_async来解决这个问题。

为了帮助大家理解,我构思了一个例子:

  1. 3名董事和总裁开会,在每个人都查看完合同之后,由总裁签字。
  2. 总裁签字之后,所有人再审核一次合同。

这个需求有三个关键点:

  • 关键点1:所有与会人员查看和审核合同,是同时进行的,无序的行为。
  • 关键点2:只有与会人员都查看了合同之后,总裁才能签字。
  • 关键点3: 只有总裁签字之后,才能进行审核。

用代码看一下:

- (void)dispatch_barrier
{
    dispatch_queue_t meetingQueue = dispatch_queue_create("com.meeting.queue", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(meetingQueue, ^{
        NSLog(@"总裁查看合同");
    });
    
    dispatch_async(meetingQueue, ^{
        NSLog(@"董事1查看合同");
    });
    
    dispatch_async(meetingQueue, ^{
        NSLog(@"董事2查看合同");
    });
    
    dispatch_async(meetingQueue, ^{
        NSLog(@"董事3查看合同");
    });
    
    dispatch_barrier_async(meetingQueue, ^{
        NSLog(@"总裁签字");
    });
    
    dispatch_async(meetingQueue, ^{
        NSLog(@"总裁审核合同");
    });
    
    dispatch_async(meetingQueue, ^{
        NSLog(@"董事1审核合同");
    });
    
    dispatch_async(meetingQueue, ^{
        NSLog(@"董事2审核合同");
    });
    
    dispatch_async(meetingQueue, ^{
        NSLog(@"董事3审核合同");
    });
}

输出结果:

gcd_demo[41791:3140315] 总裁查看合同
gcd_demo[41791:3140296] 董事1查看合同
gcd_demo[41791:3140297] 董事3查看合同
gcd_demo[41791:3140299] 董事2查看合同
gcd_demo[41791:3140299] 总裁签字
gcd_demo[41791:3140299] 总裁审核合同
gcd_demo[41791:3140297] 董事1审核合同
gcd_demo[41791:3140296] 董事2审核合同
gcd_demo[41791:3140320] 董事3审核合同

在这里,我们可以将meetingQueue看成是会议的时间线。总裁签字这个行为相当于写操作,其他都相当于读操作。使用dispatch_barrier_async以后,之前的所有并发任务都会被dispatch_barrier_async里的任务拦截掉,就像函数名称里的“栅栏”一样。

因此,使用Concurrent Dispatch Queue 和 dispatch_barrier_async 函数可以实现高效率的数据库访问和文件访问。

dispatch_sync

到目前为止的所有例子都使用的是异步函数,有异步就一定会有同步,那么现在就来区分一下同步和异步函数的区别:

  • dispatch_async:异步函数,这个函数会立即返回,不做任何等待,它所指定的block“非同步地”追加到指定的队列中。
  • dispatch_sync:同步函数,这个函数不会立即返回,它会一直等待追加到特定队列中的制定block完成工作后才返回,所以它的目的(也是效果)是阻塞当前线程。

举个例子:

- (void)dispatch_sync_1
{
    //同步处理
    NSLog(@"%@",[NSThread currentThread]);
    NSLog(@"同步处理开始");
    
    __block NSInteger num = 0;
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_sync(queue, ^{
        //模仿耗时操作
        for (NSInteger i = 0; i< 1000000000; i ++) {
            num++;
        }
        NSLog(@"%@",[NSThread currentThread]);
        NSLog(@"同步处理完毕");
    });
    NSLog(@"%ld",num);
    NSLog(@"%@",[NSThread currentThread]);
}

输出结果:

gcd_demo[5604:188687] <NSThread: 0x60800006fa40>{number = 1, name = main}
gcd_demo[5604:188687] 同步处理开始
gcd_demo[5604:188687] <NSThread: 0x60800006fa40>{number = 1, name = main}
gcd_demo[5604:188687] 同步处理完毕
gcd_demo[5604:188687] 1000000000
gcd_demo[5604:188687] <NSThread: 0x60800006fa40>{number = 1, name = main}

在最开始的时候只打印前两行,循环完毕之后才打印后面的内容。
因为是同步函数,它阻塞了当前线程(主线程),所以只能等到block内部的任务都结束后,才能打印下面的两行。

但是如果使用异步函数会怎样呢?

- (void)dispatch_sync_2
{
    //异步处理
    NSLog(@"%@",[NSThread currentThread]);
    NSLog(@"异步处理开始");
    
    __block NSInteger num = 0;
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(queue, ^{
        //模仿耗时操作
        for (NSInteger i = 0; i< 1000000000; i ++) {
            num++;
        }
        NSLog(@"%@",[NSThread currentThread]);
        NSLog(@"异步处理完毕");
    });
    NSLog(@"%ld",num);
    NSLog(@"%@",[NSThread currentThread]);
}

输出:

gcd_demo[5685:194233] <NSThread: 0x600000071f00>{number = 1, name = main}
gcd_demo[5685:194233] 异步处理开始
gcd_demo[5685:194233] 0
gcd_demo[5685:194233] <NSThread: 0x600000071f00>{number = 1, name = main}
gcd_demo[5685:194280] <NSThread: 0x608000260400>{number = 3, name = (null)}
gcd_demo[5685:194280] 异步处理完毕

我们可以看到,不同于上面的情况,block下面的两个输出是先打印的(因为没有经过for循环的计算,num的值是0)。因为是异步处理,所以没有等待block中任务的完成就立即返回了。

了解了同步异步的区别之后,我们看一下使用同步函数容易发生的问题:如果给同步函数传入的队列是串行队列的时候就会容易造成死锁。看一下一个死锁的例子:

- (void)dispatch_sync_3
{
    NSLog(@"任务1");
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_sync(queue, ^{
        
        NSLog(@"任务2");
    });
    
    NSLog(@"任务3");
}

上面的代码只能输出任务1,并形成死锁。
因为任务2被追加到了主队列的最后,所以它需要等待任务3执行完成。
但又因为是同步函数,任务3也在等待任务2执行完成。
二者互相等待,所以形成了死锁。

dispatch_apply

通过dispatch_apply函数,我们可以按照指定的次数将block追加到指定的队列中。并等待全部处理执行结束。

- (void)dispatch_apply_1
{
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_apply(10, queue, ^(size_t index) {
        NSLog(@"%ld",index);
    });
    NSLog(@"完毕");
}


gcd_demo[6128:240332] 1
gcd_demo[6128:240331] 0
gcd_demo[6128:240334] 2
gcd_demo[6128:240332] 4
gcd_demo[6128:240334] 6
gcd_demo[6128:240331] 5
gcd_demo[6128:240332] 7
gcd_demo[6128:240334] 8
gcd_demo[6128:240331] 9
gcd_demo[6128:240259] 3
gcd_demo[6128:240259] 完毕

我们也可以用这个函数来遍历数组,取得下标进行操作:

- (void)dispatch_apply_2
{
    NSArray *array = @[@1,@10,@43,@13,@33];
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_apply([array count], queue, ^(size_t index) {
        NSLog(@"%@",array[index]);
    });
    NSLog(@"完毕");
}

输出:

gcd_demo[6180:244316] 10
gcd_demo[6180:244313] 1
gcd_demo[6180:244316] 33
gcd_demo[6180:244314] 43
gcd_demo[6180:244261] 13
gcd_demo[6180:244261] 完毕

我们可以看到dispatch_apply函数与dispatch_sync函数同样具有阻塞的作用(dispatch_apply函数返回后才打印完毕)。

我们也可以在dispatch_async函数里执行dispatch_apply函数:

- (void)dispatch_apply_3
{
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(queue, ^{
        
        NSArray *array = @[@1,@10,@43,@13,@33];
        __block  NSInteger sum = 0;
    
        dispatch_apply([array count], queue, ^(size_t index) {
            NSNumber *number = array[index];
            NSInteger num = [number integerValue];
            sum += num;
        });
        
        dispatch_async(dispatch_get_main_queue(), ^{
            //回到主线程,拿到总和
            NSLog(@"完毕");
            NSLog(@"%ld",sum);
        });
    });
}

dispatch_suspend/dispatch_resume

挂起函数调用后对已经执行的处理没有影响,但是追加到队列中但是尚未执行的处理会在此之后停止执行。

dispatch_suspend(queue);
dispatch_resume(queue);

dispatch_once

通过dispatch_once处理的代码只执行一次,而且是线程安全的:

- (void)dispatch_once_1
{
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    for (NSInteger index = 0; index < 5; index++) {
        
        dispatch_async(queue, ^{
            [self onceCode];
        });
    }
}


- (void)onceCode
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSLog(@"只执行一次的代码");
    });
}

输出:

gcd_demo[7556:361196] 只执行一次的代码

该函数主要用于单例模式的使用。