美文网首页程序员iOS DeveloperiOS程序猿
Block学习笔记4-Block存储域

Block学习笔记4-Block存储域

作者: Shirley_y | 来源:发表于2017-02-18 11:50 被阅读98次

    回到前文中演示的__main_block_impl_0的代码

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

    这里讲解一下block结构体的__main_block_impl_0的构造函数中isa指针的值类型及其含义。
    首先isa指向实例对象,正好表明Block也跟一般的OC对象类似,拥有isa指针,共有三种block类型:

    • NSConcreteStackBlock:设置在栈上
    • NSConcreteGlobalBlock:设置在全局范围上,即与全局变量一样,设置在程序的数据区域
    • NSConcreteMallocBlock:设置在堆上
      下面分别举例说明这三种情况在什么时候发生,以及对变量访问的限制区别

    NSConcreteGlobalBlock

    OC源码:
    #import <Foundation/Foundation.h>
    void (^bBlock)(void)=^{NSLog(@"hahaha");};
    int main(int argc, char * argv[]) {
    ...
    C源码:
    struct __bBlock_block_impl_0 {
      struct __block_impl impl;
      struct __bBlock_block_desc_0* Desc;
      __bBlock_block_impl_0(void *fp, struct __bBlock_block_desc_0 *desc, int flags=0) {
        impl.isa = &_NSConcreteGlobalBlock;//全局block,无法截获自动变量
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    当将block声明在main函数之前即函数全局变量时,impl.isa的值即为NSConcreteGlobalBlock,但是由于相当于全局变量,所以无法截获自动变量,这种block不会在执行时发生任何改变,所以将其存储于数据区域中。
    所以可以推断出除了在函数的全局变量处声明block能将block类型设置为NSConcreteGlobalBlock外,在函数中声明block时,只要block的执行函数中不包含任何自动变量,即其内容不会在执行时发生改变时,就可以得到NSConcreteGlobalBlock类型的block。作者试验过,的确如此。

    NSConcreteStackBlock

    非某些情况下,在函数中声明的block均为NSConcreteStackBlock类型,正如前面所列举的所有例子均是生成的NSConcreteStackBlock类型。

    OC源码:
            __block int i = 1;
            void (^aBlock)() = ^{
                NSLog(@"%d",i);
            };//aBlock已经被复制到了堆上
            i=2;
            aBlock();
    

    在上述源码中,按之前所理解的,这里应该生成的是NSConcreteStackBlock类型的block,但是,在aBlock();处打上断点,Xcode截取到的aBlock类型竟然为NSConcreteMallocBlock,如下图所示:


    stackBlock1.png

    这是因为在ARC的环境下,声明block时默认为__strong,所以编译器就自动的将block复制到了堆上,所以在运行时得到的block为NSConcreteMallocBlock,将ARC关闭,则可得到想要的NSConcreteStackBlock。

    附上ARC关闭开起的设置:


    ARC设置.png

    由于NSConcreteStackBlock是生成在栈上的,当其所属的变量作用域结束时,该block就会被废弃,同时配置在栈上的block变量也会被废弃,所以Block提供了将Block与__block变量复制到堆上的方法,来防止因变量作用域结束而废弃的情况,而被复制后的Block的impl.isa值即为NSConcreteMallocBlock了。
    附上代码举例:
    (为了验证NSConcreteStackBlock下列代码在MRC的环境下运行)

    typedef void (^blk_t)(void);
    @implementation MyViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        blk_t blk = [self testBlk];
        blk();
    }
    - (blk_t)testBlk{
        int j = 0;
        blk_t tttt = ^{NSLog(@"tttt%d",j);};
        return tttt;
    }
    

    这段代码中先定义了一个blk_t类型的block,然后在私有方法中构建一个block,将这个block作为返回值。然后在viewDidLoad 中调用testBlk获取到其返回的blk。程序运行结果:

    访问NSConcreteStackBlock崩溃.png

    由此可得到当testBlk函数运行结束时,在其函数作用域上的NSConcreteStackBlock类型的block tttt也被释放了,所以在后面运行blk时会出现EXC_BAD_ACCESS的崩溃。

    NSConcreteMallocBlock

    正如前面所列举的例子所示,在ARC有效的环境下,编译器会自动将block复制到堆上(大多数情况下)。
    在此情况下编译器无法自动判断,需手动调用copy函数,将block复制到堆上:

    • 向方法或函数的参数中传递Block时

    但下列两种传递参数的情况除外,编译器会自动复制block

    • Cocoa框架的方法且方法名中含有usingBlock等时
    • GCD的API

    附上代码举例:(将编译环境修改回ARC)

    typedef void (^blk_t)(void);
    @implementation MyViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        blk_t blk = [self testBlk];
        blk();
    }
    - (blk_t)testBlk{
        int j = 0;
        blk_t tttt = ^{NSLog(@"tttt%d",j);};
        return tttt;
    }//输出tttt0
    

    同样的一段代码,在ARC环境下能正常运行,因为在ARC下声明tttt时,已经将其复制到堆上了,所以当testBlk运行结束时,由于blk持有了testBlk的返回结果,所以引用计数加一,tttt就没有被释放掉,所以可以正常访问。
    下列代码为书中的例子,作者在运行时遇到了跟书中例子解释不同的地方,在此贴出试验代码与结果:

    - (void)viewDidLoad {
        [super viewDidLoad];
        NSDictionary *tt = [self getDic];
        blk_t blk = [tt objectForKey:@"2"];
        blk();
    }
    
    - (NSDictionary* )getDic {
        int i = 1;
        NSDictionary *ttt = [[NSDictionary alloc] initWithObjectsAndKeys:^{NSLog(@"first %d",i);},@"1",^{NSLog(@"second %d",i);} ,@"2", nil];
        return ttt;
    }
    

    上述代码中,为字典初始化了两个键值对,值均为block,但是根据书中所述,获取到的字典值应该均为NSConcreteStackBlock类型,但是经过重复试验,在ARC有效的环境下,无论是对NSArray还是NSDictionary通过initwithobject或initWithObjectsAndKeys的方式传入block时,第一个总是为NSConcreteMallocBlock,后面block的均为NSConcreteStackBlock如下图所示:

    代码示例.png

    输出结果如下:

    • 情况一:
    blk_t blk = [tt objectForKey:@"2"];
    blk();
    

    访问的是NSConcreteStackBlock,程序崩溃在blk()处,与书中的描述相符,这是因为在NSDictionary *tt = [self getDic]执行结束时,栈上的Block被废弃,访问不存在的变量引起的崩溃。

    • 情况二:
    blk_t blk = [tt objectForKey:@"1"];
    blk();
    

    访问的是NSConcreteMallocBlock,程序在执行完blk();后,崩溃在main.m的return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));函数上,目前尚未找出原因,如果有人知晓原因的话欢迎留言探讨!
    作者猜测可能是NSArray与NSDictionary通过initwithobject或initWithObjectsAndKeys的方式传入变量时,NSArray与NSDictionary会对首元素进行某种操作,使其不会存储在栈上,但是既然为NSArray与NSDictionary对象所持有,那为什么在访问后会崩溃呢?这是作者一直未想明白的地方。

    相关文章

      网友评论

        本文标题:Block学习笔记4-Block存储域

        本文链接:https://www.haomeiwen.com/subject/mbdkwttx.html