面试遇到block的第二天-类型和__block

1,264 阅读7分钟

通过上一篇文章对block本质的分析,我们可以了解到,block的本质就是一个OC对象,拥有一个isa指针,那么block就肯定有自己的类型。上一篇文章中通过C++代码我们也看到了,isa指向一个&_NSConcreteStackBlock,本文就继续分享block的类型,继承链等知识点。

继承关系

block有三种类型,可以通过isa或者调用class方法获取,最终都继承自NSObject. 下面我们通过class获取到block的类型

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void(^myBlock)(void) = ^{
            NSLog(@"this is a block ");
        };
        NSLog(@"%@",[myBlock class]);
        NSLog(@"%@",[[myBlock class] superclass]);
        NSLog(@"%@",[[[myBlock class] superclass] superclass]);
        NSLog(@"%@",[[[[myBlock class] superclass] superclass] superclass]);
    }
    return 0;
}

结果如下,也更加印证了block是一个OC对象

block[12750:370967] __NSGlobalBlock__
block[12750:370967] __NSGlobalBlock
block[12750:370967] NSBlock
block[12750:370967] NSObject

block类型

上面我们分析出了block都继承自NSBlock,最终继承自NSObject,下面继续看看block的三种类型

  • NSGlobalBlock
  • NSMallocBlock
  • NSStackBlock
    还是通过class的方式获取block的类型
    //void (^block1)(void) 起个别名
        void (^block1)(void) = ^{
            NSLog(@"this is a block");
        };//全局 静态 :__NSGlobalBlock__  0x1开头
        
        int age = 10;
        void (^block2)(void) = ^{
            NSLog(@"this is a block  %d",age);
        };//__NSMallocBlock__ 堆block  0x6开头
        
        NSLog(@"%@ --- %@ --- %@",[block1 class],[block2 class],[^{
            NSLog(@"this is a block  %d",age);  //__NSStackBlock__ 栈block  0x7开头
        } class]);

输出: __NSGlobalBlock__ --- __NSMallocBlock__ --- __NSStackBlock__
但是我们还是需要编辑成C++代码再看一下isa指向的类型,就不贴代码了,直接说结果,都是_NSConcreteStackBlock;

这里要说一下一切以运行时的结果类型为准,因为clang这个命令编译的结果可能跟我们最初写的代码有些出入,只能作为学习的参考。

那么上面的三个block为什么是这三种类型呢,下面我们继续从内存分布上去分析。

内存分布

一图胜前言

  • 编写的代码都放在程序区域

  • 全局变量一般放在数据区域

  • 程序区域和数据区域的内存, 都是编译器自动处理的

  • 堆:动态分配内存 比如[NSObject alloc]malloc(20) 需要开发者申请内存也需要自己管理内存

  • 栈:局部变量,函数参数等,系统自动分配内存,自动释放内存

  • NSGlobalBlock就放在数据区域,NSMallocBlock放在堆区 ,NSStackBlock放在栈区

那么我们开发中写的很多block,我们怎么知道他是什么类型的block呢?

下面总结几点判断的规则:

  1. NSGlobalBlock:没有访问auto变量就是NSGlobalBlock,对NSGlobalBlock进行copy操作依然是NSGlobalBlock
  2. NSStackBlock:访问了auto变量就是NSStackBlock(需要在MRC环境测试,因为ARC环境编译器在打印的时候帮我们做了copy操作)
  3. NSMallocBlock:NSStackBlock做了copy操作就是NSMallocBlock

为什么上面要强调一下copy操作呢,因为在栈上的block调用copy之后会从复制一份到上,在堆上的blockcopy,则会引用计数加1,而在数据区的NSGlobalBlock调用copy则不会有任何操作。

block的copy

上面内存分部中说过NSStackBlock做了copy操作就是NSMallocBlock,在ARC下,系统会根据情况自动把栈上的block copy到堆上,那是什么情况系统会做这件事呢?下面举例说一说

  1. block作为函数的返回值
typedef void (^MyBlock)(void);
MyBlock returnBlock() {
    int age = 20;
    //自动调用copy
    return ^{
        NSLog(@"----- %d",age);
    };
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MyBlock block = returnBlock();
        block();
        NSLog(@"%@",[block class]);//NSMallocBlock
    }
    return 0;
}

根据我们上面分析的结果,调用了auto变量,是一个NSStackBlock,但是最后打印的结果是一个NSMallocBlock,说明编译器帮我们做了一次copy操作,当然在ARC下我们也不需要关心release,在作用域结束,编译器也帮我们做了release操作。

  1. block赋值给__strong指针
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int age = 20;
        MyBlock block = ^{
            NSLog(@"----- %d",age);
        };
        block();
        NSLog(@"%@",[^{
            NSLog(@"----- %d",age);
        } class]);//NSStackBlock
        NSLog(@"%@",[block class]); //NSMallocBlock
    }
    return 0;
}
  1. API中方法名含有usingBlock的方法
 NSArray *array = [NSArray array];
        [array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            
        }];
  1. GCD中的block
static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            <#code to be executed once#>
        });

总结一下:通过表面看本质,栈区的block是不会对外面的变量执行retain操作的(MRC),或者说不会对外面变量发生强引用(ARC),只有堆区的block才有可能持有变量。

__block修饰符

说到__block这个修饰符,大家开发中一定是用过很多次了,咱们先从为什么要用开始说起,再说__block加了这个修饰词之后到底发生了什么。

为什么要用__block呢?最常见的一个使用就是在block里面想修改block外部的局部变量,前面我们也已经分析过了,auto变量是通过值传递,被记录在block的结构体中的,而且是跨函数访问,我们如何能在block执行的函数中,修改另外一个函数中的auto变量呢,当然是改不了的。然后我们又知道了只要用__block修饰这个想改的auto变量,我们就可以在block中修改他的值了,那么这里就可以想一想__block这个修饰词到底是做了什么呢?也许他就是把之前的值传递改成了地址传递呢?

修改auto变量

想要修改auto变量,我们先说一种不是使用__block的方法

  1. static修饰 前面我们也分析过static修饰的变量被block捕获是地址传递,只要是地址传递我们能拿到地址,就可以根据地址修改对应内存中的内容,可以肯定,static修饰,是可以修改auto变量的
  2. 全局变量 就更不用说了,谁都可以改,都不用通过block去捕获
  3. __block 除了上面两种方法(可能会有内存问题,我们一般不用),__block才是我们最优的一种解决方法
__block原理

还是通过clang编译成C++代码区分析__block的原理,先写一段测试代码

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block int age = 20;
        MyBlock block = ^{
            age = 33;
            NSLog(@"----- %d",age);
        };
        block();
    }
    return 0;
}

__block int age = 20;这句代码最终被编译成了

__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 20};

其实这里可以理解为,添加了__block修饰之后的age,被编译成了一个__Block_byref_age_0对象

struct __Block_byref_age_0 {
  void *__isa;
__Block_byref_age_0 *__forwarding;
 int __flags;
 int __size;
 int age;
};

其中__forwarding被赋值了&age,也就是__Block_byref_age_0对象本身的地址,age的值(20)也被存储在了这个对象中。然后就是block中修改age的值,并打印age,使用age->__forwarding->age取到age的值

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_age_0 *age = __cself->age; // bound by ref

            (age->__forwarding->age) = 33;
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_86_ljphgvys0hzg8hfxdtv5q5gm0000gn_T_main_76267f_mi_0,(age->__forwarding->age));
        }

到这里我们应该已经清楚了,为什么__block修饰的变量在block中可以被修改,其实说白了,还是变相的实现了地址的传递,只有拿到地址才可以改变相应地址指向的内存区域中的数据。

__forwarding

这里为什么要把__forwarding那出来说一说呢,肯定有人在好奇,(age->__forwarding->age) = 33;这个取值方式,还有结构体里面为什么要有一个__forwarding,__forwarding不就是指向自己吗,到底有什么用非要这样设计?

__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 20};

这里可以看到,给__forwarding赋值了&age,就是他自己的内存地址,然而这里__forwarding指针真的永远指向自己么?我们来做一个实验。

//以下代码在MRC中运行
    __block int i = 0;
    NSLog(@"Block 外面 %p",&i);
    
    void (^myBlock)(void) = [^{
        i ++;
        NSLog(@"Block 里面 %p",&i);
    }copy];

把Block拷贝到了堆上,这个时候打印出来的2个i变量的地址就不同了。

Block 外面 0x7fff5fbff818
Block 里面 0x1002038a8

地址不同就可以很明显的说明__forwarding指针并没有指向之前的自己了。那__forwarding指针现在指向到哪里了呢?下面看图说话吧

这样应该就能明白__forwarding的存在就是为了,当block从栈上copy到堆上之后,还能继续指向堆上的__block变量的。

文字最后再说一个block中使用array的小问题,也是一个容易理解上混淆的问题:

在block中调用addObject编译不会报错,但是给array赋值成nil就报错,这是为什么呢?

先说赋值nil报错,根据上面的分析,因为我们并没有用__block修饰array,block中不能修改auto对象,所以报错了。

再说addObject为什么不会报错,因为addObject其实并不是去改变array的值,只是去操作array,而block只是不能修改auto变量,并不是不能操作变量,所以不会报错。