block的使用总结

前言

在我们使用OC进行iOS开发时,block的使用场景很多,特别是在GCD、网络访问(如框架AFNetworking)中出镜率很高,在ARC出来之前,一些资深面试官也喜欢问一些block深层次的问题,希望接下来的一些关于block的介绍能或多或少的帮助到各位。


block的本质

在这里有一篇文章可以帮助我们。博主通过将OC通过命令行clang的方式编译成C代码,发现block就是指向结构体的指针,block的执行体会生成对应的函数。也可以理解为“block就是能够读取其它函数内部变量的函数”,数据结构体定义如下:

struct Block_layout {
    void *isa;
    int flags;
    int reserved;
    void (*invoke)(void*,...);
    struct Block_descriptor *descriptor;
    /*Imported variables*/
};

struct Block_descriptor {
    unsigned long int reserved;
    unsigned long int size;
    void (*copy)(void *dst, void *src)
    void (*dispose)(void *);
};

从上面这段代码可以看出,一个block是由6部分组成:

  • isa指针:所有对象都具有的指针(表明block是一个对象),用于实现对象相关的功能;(isa指针实际上是指向它所属的类,当给对象发送某条消息时,就会沿着isa指针寻找消息的处理程序。)
  • flags: 用于按 bit 位表示一些 block 的附加信息;
  • reserved: 保留变量;
  • invoke:函数指针,指向具体的 block 实现的函数调用地址;
  • descriptor:表示该 block 的附加描述信息,主要是 size 大小,以及 copy 和 dispose 函数的指针;(dispose函数负责做与copy相反的工作,如释放掉block中捕获的变量)
  • variables:捕获(capture)到的变量,block 能够访问它外部的局部变量,就是因为将这些变量(或变量的地址)复制到了结构体中。

block的类型

这里是唐巧的一些总结,包括三种block类型的OC与C源码的分析。(ps:表示看得晕了!)

1.NSGlobalBlock:未引用任何外部变量,可当成函数使用,存储于程序代码区。就像这样的:

void (^test)() = ^{
    NSLog(@"This is execute statement");
}

2.NSStackBlock:存储于栈内存,系统自动管理,当其所位于的函数返回后,此类型的block将无效。这种block称为内联block,AFNetworking框架中的网络请求方法用这种block用得很多,就像这样:

NSString *str = @"string";
NSLog(@"block type is %@", ^{
    NSLog(@"str is %@", str);
});
/*
  打印结果:block is <__NSStackBlock__:0x7fff596b9968> 
  后面的十六进制数是block的内存地址.可以看到代码中的block内联
  在NSLog方法中,并访问了外界的变量,但是外界无论如何都不能有效 
  调用它,因为方法执后block将被销毁。
*/

3.NSMallocBlock:存储于堆内存,ARC管理它的内存,就像这样:

NSInteger test = 1;
void (^testBlock)(void) = ^(void) {
    NSLog(@"这是Block的执行体%zd", test);
};
NSLog(@"block is %@", testBlock);
testBlock();
/*
  打印结果:block is <__NSMallocBlock__: 0x7fdebac09580>
  testBlock中引用了外部变量,并且可以通过block名调用它。所以此
  类型的block可通过两点来判断:
  1.捕获使用了外部的变量
  2.可以通过block名调用它(注意block嵌套的情况)
*/

注意:NSMallocBlock要在ARC下才能判断出,在非ARC下打印的结果是NSStackBlock,也就是说ARC会自动将栈内存的block复制到堆内存中,目的就是让block的内存由程序员管理(实际上是ARC),并能够控制它的使用生命周期。如果是非ARC下,你就必须手动copy到堆内存。(在ARC如此成熟的今天,这种情况一般可以不用考虑,但还是需要有一定的了解。)对堆栈知识不怎么熟悉的兄弟们可以在这里看到。

前面已经提到在非ARC的状态下,需要手动将栈上的block copy到堆中,那么block的内存又如何来管理呢?请继续往下看。


block的copy、retain、release

  • Block_copy与OC对象的copy等效,Block_release与OC对象的release等效;
  • 对Block不管是retain、copy、release都不会改变它的引用计数retainCount,retainCount始终是1;
  • NSGlobalBlock:retain、copy、release操作都无效;
  • NSStackBlock:因为此类型的block在函数返回后,内存会被回收,即使事先release、retain也无效。在非ARC中容易出现的问题就是向可变数组中加入此类型的block,最终就会导致野指针错误。在ARC情况下,默认会copy到堆中,即使函数调用结束后栈上block被销毁也不会出现任何问题;
  • NSMallocBlock:可以将其看成是OC对象(分配的内存都是在堆中,由ARC管理),支持release、retain、copy,copy只是浅复制,增加一次引用,类似retain;
  • 尽量不要对Block进行retain操作。

注意:在对block进行copy时,该block内所引用到的所有block都将被copy,该block的变量引用到的block也会被copy,但是作为参数的block不会被 copy(因为参数只是局部变量,只在它所在block有效,如AFNetworking中网络请求时传入block参数success和failure),copy NSMallocBlock类型的block时,在copy方法结束后,它的引用计数又会降回去。(ps:没搞明白为什么设计这样的机制,而不是retain就增,release就减,而不是始终都是1,希望知道的兄弟们不吝赐教)

其实现在我们在实际开发中基本上没有对block手动copy、retain和release的情况,因为有ARC,但是ARC还是有解决不了的问题,当然这是我们程序员自己造成的。


block引用外部变量与循环引用(reference cycle)

基本类型

1.局部变量
局部变量在block中为只读,在block定义时就copy了它所要用的变量值做常量使用,它们的内存地址不同,此时在block执行体内不能对该常量进行修改,外部的局部变量的改变也不会影响block的执行结果。如下:

NSInteger i = 1;
NSInteger (^addBlock)(NSInteger, NSInteger) = ^(NSInteger a, NSInteger b) {
        //i++;//error:Variable is not assignable (missing __block type specifier)
        return i + a + b;
};
i = 2;
NSLog(@"result is %zd", addBlock(1, 2));//result is 4

2.Static修饰的变量和全局变量
因为它们的内存地址是固定的,block在读取它们的值时是从内存中直接读取,此时就不在是copy的常量,在block中也可修改它们的值,它们值得改变也会影响block的执行结果,如下:

static NSInteger i = 1;
NSInteger (^addBlock)(NSInteger, NSInteger) = ^(NSInteger a, NSInteger b) {
    i++;
    return i + a + b;
};
i = 2;
NSLog(@"result is %zd", addBlock(1, 2));//result is 6

3.block变量
被__block(双下滑线)修饰的变量,基本类型等同于全局变量和静态变量。

OC对象

1.全局对象、静态对象和__block对象测试代码如下:

NSString *_testString = nil;
- (void)testGlobalObj {
    _testString = @"1";
    void (^testBlock)(void) = ^ {
        NSLog(@"testString is %@", _testString);
};

_testString = nil;
testBlock();

}
//调用testGlobalObj结果:testString is null
- (void)testStaticObj {
    static NSString *_testString = nil;
    _testString = @"1";
    void (^testBlock)(void) = ^ {
        NSLog(@"testString is %@", _testString);
    };

    _testString = nil;

    testBlock();
}
//调用testStaticObj结果:testString is null
- (void)testBlockObj {
    __block NSString *_testString = nil;
    _testString = @"1";
    void (^testBlock)(void) = ^ {
        NSLog(@"testString is %@", _testString);
    };

    _testString = nil;

    testBlock();
}
//调用testBlockObj结果:testString is null

2.局部变量测试:

- (void)testLocalObj {
    NSString *_testString = nil;
    _testString = @"1";
    void (^testBlock)(void) = ^ {
        NSLog(@"testString is %@", _testString);
    };

    _testString = nil;

    testBlock();
}
//调用testLobalObj结果:testString is 1

3.__weak修饰的局部变量测试:

 - (void)testWeakLocalObj {
    NSString *_testString = nil;
    _testString = @"1";
    __weak NSString *_weakString = _testString;
    void (^testBlock)(void) = ^ {
        NSLog(@"_weakString is %@", _weakString);
    };

    _testString = nil;

    testBlock();
}
//调用testWeakLocalObj结果:_weakString is 1

结论:

  • 使用局部对象时,block会复制指针所指对象存储在新的内存中做常量使用,与基本类型的局部变量使用类似;
  • 使用全局对象、静态对象和block对象时,block只引用对象的指针,强引用该对象一次;
  • __weak修饰的局部变量,block依然会强引用该对象一次,保证block能正常使用变量。

引用循环

ARC所不能解决的问题就是引用循环,是指对象A引用对象B,对象B引用对象A,或者多个对象形成闭环的引用,对象A的内存释放依赖于对象B的内存释放,对象B的内存释放又依赖于对象A的内存释放,最终导致它们始终留在内存中,即使外界已经没有任何指针可以访问它们,这就是所谓的“僵尸对象(zombie object)”。在block中我们的一些行为会导致block的copy(ARC下自动完成),当Block被copy时,会对block中用到的对象强引用(Strong),当对象有强引用时是无法释放内存的。如下:

@property(nonatomic, copy) Block testBlock;
self.testBlock = ^ {
    if (self.state) {
        self.state(self.sendData);
    }
};

对象有一个testBlock属性,对象强引用这个属性,但是testBlock有强引用着对象的其他属性,相应的也就强引用着对象本身,这就构成了引用循环,在ARC下可改为:

@property(nonatomic, copy) Block testBlock;
__weak typeof(self) weakSelf = self;//对自身对象生成一个弱引用
self.testBlock = ^ {
    if (weakSelf.state) {
        weakSelf.state(weakSelf.sendData);
    }
};

testBlock执行体中弱引用这对象的属性,也就弱引用着对象,弱引用的对象是可以释放内存,释放之后弱引用就消失。

总结

其实关于block有很多东西是需要我们注意的,而它的内存管理又是最为复杂的,所以我们在不了解的情况下不要乱用它,Xcode在编译时的静态分析只能对很少的简单循环引用提出警告,尽量避免多层嵌套使用block。从系统的API可以看出,单层内联的block使用起来很多并且都很简单,它的内存管理也不需要ARC来管理,自然而然就避免了很多问题。这篇文章写的很略,想要深入的兄弟们请自行搜索相关文章,笔者功力尚浅,就写到这里,谢谢!


引用参考

Cooper’s Blog:正确使用Block避免Cycle Retain和Crash

你可能感兴趣的:(OC语法)