面试遇到block的次日-类型和__block

经过上一篇文章对block本质的分析,咱们能够了解到,block的本质就是一个OC对象,拥有一个isa指针,那么block就确定有本身的类型。上一篇文章中经过C++代码咱们也看到了,isa指向一个&_NSConcreteStackBlock,本文就继续分析block的类型,继承链等知识点。bash

继承关系

block有三种类型,能够经过isa或者调用class方法获取,最终都继承自NSObject. 下面咱们经过class获取到block的类型函数

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
       
        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对象post

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) = ^{
            NSLog(@"this is a block");
        };
        
        int age = 10;
        void (^block2)(void) = ^{
            NSLog(@"this is a block %d",age);
        };
        
        NSLog(@"%@ --- %@ --- %@",[block1 class],[block2 class],[^{
            NSLog(@"this is a block %d",age);
        } class]);
复制代码

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

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

内存分布

一图胜前言 this

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

  • 全局变量通常放在数据区域3d

  • 程序区域和数据区域 都是编译器自动处理的区域指针

  • 堆:动态分配内存 好比[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以后会从栈赋值到堆上,在堆上的block再copy,则会引用计数加1,而在数据区的NSGlobalBlock调用block则不会有任何操做。

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

对象类型的auto变量

总结一下:经过表面看本质,栈区的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中能够被修改,其实说白了,仍是变相的实现了地址的传递,只有拿到地址才能够改变相应地址指向的内存区域中的数据。

下面再说一个block中使用array的小问题:

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

先说赋值nil报错,根据上面的分析,由于咱们并无用__block修饰array,block中不能修改auto对象,因此报错了。

再说addObject为何不会报错,由于addObject其实并非去改变array的值,只是去操做array,而block只是不能修改auto变量,并非不能操做变量,因此不会报错。

相关文章
相关标签/搜索