OC底层-深入理解Block

基本使用

block常见的使用方式如下:

// 无参无返回值
void(^MyBlockOne)(void) = ^(void) {
    NSLog(@"无参数, 无返回值");
};
MyBlockOne();

// 有参无返回值
void (^MyBlockTwo)(int a) = ^(int a) {
    NSLog(@"a = %d", a);
};
MyBlockTwo(10);

//有参有返回值
int (^MyBlockThree)(int, int) = ^(int a, int b) {
    
    NSLog(@"return %d", a + b);
    return 10;
};
MyBlockThree(10, 20);

// 无参有返回值
int (^MyBlockFour)(void) = ^(void) {
    NSLog(@"return 10");
    return 10;
};

// 声明为某种类型
typedef int (^MyBlock) (int, int);
@property (nonatomic, copy) MyBlock block;

Block的本质 - OC对象

结论: block的内部存在isa指针,其本质就是封装了函数调用函数调用环境OC对象

证明方法一:底层结构窥探

main函数中定义一个block,如下

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

终端进入项目所在目录,通过xcrun 命令将OC代码转为C++代码:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp

转换结果如下:

// 1. block 的结构体
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;
  }
};

// block 内部impl结构体,存储isa指针,block方法的地址。
struct __block_impl {
  void *isa;      
  int Flags;
  int Reserved;
  void *FuncPtr;  // 方法地址
};

// block 的描述信息,如:block的大小
![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c441779a345740a98accb31ac195d61f~tplv-k3u1fbpfcp-zoom-1.image)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)};

// 2. block 的方法实现
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_hc_wwwl26516td3w0ds9cx80c280000gp_T_main_cf18a7_mi_0);
}

// 3. main方法的实现
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

将生成的main方法简化后得:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        void(*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
        block->FuncPtr(block); 
    }
    return 0;
}

看简化后的代码,你是不是有疑问, 为什么block->FuncPtr(block) 这句话能调用成功,明明FuncPtr__block_impl 类型里的成员,为什么可以直接使用block调用

原因其实很简单,因为在block结构体__main_block_impl_0内,__block_impl是第一个成员变量,因此block的地址和impl的地址是相同的。两者可以进行强制转换。

根据转换结果:

  1. OC中定义的block底层其实就是一个C++ 的结构体__main_block_impl_0。结构体有两个成员变量implDesc,分别是结构体类型 __block_impl__main_block_desc_0
  2. 结构体__block_impl内包含了isa指针和指向函数实现的指针FuncPtr
  3. 结构体__main_block_desc_0Block_size成员存储着Block的大小
image

由上可知,block内部有一个isa指针,因此,block本质其实就是一个OC对象

证明方法二:代码层面

如果block是一个OC对象,那它最终肯定继承自NSObject类(NSProxy除外),因此我们可以直接打印出block继承链看一下就知道了。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void(^block)(void) = ^(void) {
            NSLog(@"this is first block");
        };
        
        NSLog(@"class = %@", [block class]); 
        NSLog(@"superclass = %@", [block superclass]); 
        NSLog(@"superclass superclass = %@", [[block superclass] superclass]);
        NSLog(@"superclass superclass superclass = %@", [[[block superclass] superclass] superclass]);
    }
    return 0;
}
输出结果:
2020-07-28 19:25:24.475317+0800 LearningBlock[39445:591948] class = __NSGlobalBlock__
2020-07-28 19:25:24.475707+0800 LearningBlock[39445:591948] superclass = __NSGlobalBlock
2020-07-28 19:25:24.475762+0800 LearningBlock[39445:591948] superclass superclass= NSBlock
2020-07-28 19:25:24.475808+0800 LearningBlock[39445:591948] superclass superclass superclass= NSObject

block 的继承链: __NSGlobalBlock -> NSBlock -> NSObject

可以看出block最终继承自NSObject的isa指针其实就是由NSObject来的。 因此block本质就是一个OC对象。

Block 的变量捕获(Capture)

为了保证block内部能够正常访问外部的值,block有个变量捕获的机制。下面来一起来探索一下block的变量捕获机制

代码:

int a = 10;   // 全局变量, 程序运行过程一直存在内存。
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        auto int b = 20;       // 局部变量,默认是auto修饰,一般可以不写auto,所在作用域结束后会被销毁。
        static int c = 30;     // 静态变量,程序运行过程中一直存在内存。
        
        void(^block)(void) = ^(void) {
            NSLog(@"a = %d, b = %d, c = %d", a, b, c);
        };
        
        // 观察调用block时,a,b,c 的值是多少呢?
        a = 11;
        b = 21;
        c = 31;   
        
        block();  // 调用block
    }
    return 0;
}

打印输出:
2020-07-28 19:43:41.729849+0800 LearningBlock[39648:603167] a = 11, b = 20, c = 31

由打印结果来看,b没有改变, 而ac 的值都发生了变化。 原因是什么呢?下面一起看下

运行下面的转换语句,将当前的OC代码转换C++, 方便我们看到更本质的东西:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp

转换后的代码如下:


int a = 10;  // 全局变量

struct __main_block_impl_0 {   // block的结构体
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int b;    // 新生成的成员变量b,用于存放外部局部变量b的值
  int *c;   // 新生成的成员变量c,指针类型, 用于存储外部静态局部变量c的引用。
  
  // 构造函数
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _b, int *_c, int flags=0) : b(_b), c(_c) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int b = __cself->b;  // 通过cself进行访问内部的成员变量b
  int *c = __cself->c;   // 通过cself获取静态局部变量c的引用
  
  // 直接访问全局变量a
  NSLog((NSString *)&__NSConstantStringImpl__var_folders_vt_j2sf07q142992_z55yg_170w0000gp_T_main_256a11_mi_0, a , b, (*c)); 
}

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 argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        auto int b = 20;
        static int c = 30;

        void (*Myblock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, b, &c));

        a = 11;
        b = 21;
        c = 31;

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

    }
    return 0;
}

有上可以观察到:

block结构体__main_block_impl_0内部生成了新的成员变量b*c, 分别用于存放外部传进来的bc的地址,这就是我们所说的捕获。而对于全局变量a则没有进行捕获,在使用时是直接访问的。

由此可得出:

  1. block 内部对autostatic类型的变量进行了捕获,但是不会捕获全局变量
  2. 虽然block对autostatic变量都进行了捕获,但是不同的是,auto 变量是值传递,而static变量则是地址传递。因此当外部的static变量值发生变化时,block内部也跟着会改变,而外部的auto变量值发生变化,block内部的值不会发生改变。

[图片上传失败...(image-3381f6-1601373145542)]

思考🤔

相信你会有这样的疑问,为什么block会捕获autostatic类型的局部变量,而不会捕获全局变量呢?(全局变量表示不服,block你怎么搞区别对待呢?), 那么block的变量捕获究竟有什么讲究呢?

其实是这样的

  • 首先对于auto类型的局部变量,其生命周期太短了,离开了其所在的作用域后,auto变量的内存就会被系统回收了,而block的调用时机是不确定的,如果block不对它进行捕获,那么当block运行时再访问auto变量时,因为变量已被系统回收,那么就会出现坏内存访问或者得到不正确的值
  • 对于局部的static变量,因为其初始化之后,在程序运行过程中就会一直存在内存中,而不会被系统回收,但是由于因为是局部变量的原因,其访问的作用域有限,block想访问它就要知道去哪里访问,所以block才需要对其进行捕获,但与auto变量不同的是,block只需捕获static变量的地址即可。
  • 对于全局变量,因为其在程序运行过程一直都在,并且其访问作用域也是全局的,所以block可以直接找到它,而不需要对它进行捕获。

所以,block的变量捕获原则其实很简单,如果block内部能直接访问到的变量,那就不捕获(捕获也是浪费空间), 如果block内部不能直接访问到变量,那就需要进行捕获(不捕获就没得用)。

Block的类型

block有3种类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型.

  • NSGlobalBlock
  • NSStackBlock
  • NSMallocBlock

为了准确分析block的类型,先把ARC给关闭,使用MRC
[图片上传失败...(image-dd67c0-1601373145542)]

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        auto int age = 10;       // 局部变量,默认是auto,一般可以不写auto,所处作用域结束后会被销毁。
        static int height = 20;  // 静态变量,程序运行过程中一直存在内存。

        void(^block1)(void) = ^(void) {
            NSLog(@"1111111111");  // 没有捕获了auto变量
        };
        
        void(^block2)(void) = ^(void) {
            NSLog(@"age = %d", age);   // 捕获了auto变量
        };
        
        void(^block3)(void) = ^(void) {
            NSLog(@"height = %d", height);   // 捕获了static变量
        };
        
        NSLog(@"block1 class: %@", [block1 class]);             // __NSGlobalBlock__
        NSLog(@"block2 class: %@", [block2 class]);             // __NSStackBlock__
        NSLog(@"block2 copy class: %@", [[block2 copy] class]); //__NSMallocBlock__
        NSLog(@"block3 class: %@", [block3 class]);             //__NSGlobalBlock__
    }
    return 0;
}

// 输出结果:
2020-07-28 20:41:43.283331+0800 LearningBlock[40390:637401] block1 class: __NSGlobalBlock__
2020-07-28 20:41:43.283755+0800 LearningBlock[40390:637401] block2 class: __NSStackBlock__
2020-07-28 20:41:43.283877+0800 LearningBlock[40390:637401] block2 copy class: __NSMallocBlock__
2020-07-28 20:41:43.283924+0800 LearningBlock[40390:637401] block3 class: __NSGlobalBlock__

由上可知:

  1. block类型取值如下:

    • 没有捕获auto变量,那么block的为__NSGlobalBlock__类型。
    • 捕获了auto变量,那么block__NSStackBlock__类型。
    • __NSStackBlock__类型的block进行copy操作,则block就会变成__NSMallocBlock__ 类型.
      image
  2. block 这几种类型的主要区别是:在内存中的存放区域不同。(即生命的周期不同)

    • __NSGlobalBlock__ 存在数据段。
    • __NSStackBlock__ 存放在栈空间。
    • __NSMallocBlock__ 存放在堆空间。
image

检验题:

新建一个Person类, 如下:

@interface Person : NSObject

@property (nonatomic, strong) NSString *name;

- (void)test;

@end

@implementation Person

- (void)test {
    void (^block)(void) = ^{
        NSLog(@"person name = %@", _name);
    };
}
@end

问题: 在Person.mtest方法中的blockself有没有进行捕获呢?

答案是有,block会捕获self. 分析如下:

首先将Person.m 通过xcrun命令转换为C++, 得到如下内容:

//test 方法内的block方法
struct __Person__test_block_impl_0 {
  struct __block_impl impl;
  struct __Person__test_block_desc_0* Desc;
  Person *self;
  __Person__test_block_impl_0(void *fp, struct __Person__test_block_desc_0 *desc, Person *_self, int flags=0) : self(_self) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

//test 方法
static void _I_Person_test(Person * self, SEL _cmd) {
    void (*block)(void) = ((void (*)())&__Person__test_block_impl_0((void *)__Person__test_block_func_0, &__Person__test_block_desc_0_DATA, self, 570425344));

    NSLog((NSString *)&__NSConstantStringImpl__var_folders_hc_wwwl26516td3w0ds9cx80c280000gp_T_Person_14871d_mi_1, ((Class (*)(id, SEL))(void *)objc_msgSend)((id)block, sel_registerName("class")));
}

观察转换后的代码可以看到:

  1. 我们平常写的OC方法,其实默认就有隐藏的两个参数,(Person *self, SEL _cmd), 分别是方法的调用者 self方法选择器 sel
  2. 方法的参数一般是局部变量,block会对局部变量进行捕获的。

Block的copy操作

我们日常使用的block一般是__NSMallocBlock__类型的,原因有如下:

  • 对于__NSGlobalBlock__类型的block, 因为没有捕获auto变量, 所以正常一般都是直接使用函数实现。
  • 对于__NSStackBlock__类型的block, 因为其存放在栈上,其内部使用变量容易被系统回收掉,从而导致一些异常的情况。比如下面:(要先将项目切成MRC,因为ARC下编译器会根据情况做copy操作,会影响分析)
typedef void (^MJBlock)(void);
MJBlock block;
void test() {
    int a = 10;  // test方法结束后,a的内存就被回收了。
    block = ^(void) {
        NSLog(@"a = %d", a);
    };
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        test();
        block();   // block里打印的是被回收了的a
    }
    return 0;
}
输出结果:
2020-09-27 10:05:28.616920+0800 Interview01-block的copy[7134:29679] a = -272632776
  • 对于__NSMallocBlock__类型的block, 因为它是存储在堆上,所以就不存在__NSStackBlock__类型block的问题。

上面演示的是在MRC环境下的, 那么在ARC环境下又是如何的呢?

ARC环境下,编译器会自动根据情况将栈上的block复制到堆上。比如一下情况:

  • block作为函数返回值时。
  • block赋值给__strong指针时。
  • block作为Cocoa API中方法名含有usingBlock的方法参数时。
  • block作为GCD API方法参数时。
typedef void (^MJBlock)(void);
MJBlock myblock()
{
    int a = 10;
    return ^{
        NSLog(@"--------- %d", a);   // 1. 作为方法返回值时。会自动copy
    };
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int age = 10;
        MJBlock block = ^{    // 2.赋值给strong指针时,会自动copy
            NSLog(@"---------%d", age);
        };
        
        NSArray *arr = @[@10, @20];
        [arr enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            // 3. block作为Cocoa API中方法名含有usingBlock的方法参数时。会自动copy
        }];

        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            // 4. block作为GCD API方法参数时。会自动copy
        });
    }
    return 0;
}

根据上面的情况,在MRCARCblock属性的写法可以有差异:

MRC下block属性的建议写法
@property (copy, nonatomic) void (^block)(void);  // 赋值时会自动copy到堆上

ARC下block属性的建议写法
@property (strong, nonatomic) void (^block)(void);
@property (copy, nonatomic) void (^block)(void);

对象类型的auto变量

基本数据类型auto变量我们已经分析了,那么对象类型auto变量是不是和基本数据类型的一样还是有什么特别之处呢?下面我们一起来分析下:(记得先将工程切回ARC模式)

如下代码:

@interface LCPerson : NSObject
@property (nonatomic, assign) int age;
@end

@implementation LCPerson
- (void)dealloc {
    NSLog(@"%s", __func__);   // 销毁代码
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"11111111");
        
        {
            LCPerson *person = [[LCPerson alloc] init];
            person.age = 10;
        }
        
        NSLog(@"22222222");
    }
    return 0;
}

// 输出结果:
2020-09-27 10:36:43.856070+0800 LearningBlock[16016:56873] 11111111
2020-09-27 10:36:43.856442+0800 LearningBlock[16016:56873] -[LCPerson dealloc]
2020-09-27 10:36:43.856474+0800 LearningBlock[16016:56873] 22222222

我们定义了一个LCPerson类,在main.m中做测试,由输出结果可以看出,person对象的释放是在111111122222222之间, 这我们应该都可以理解。(局部作用域)

我们继续~

加入Block之后,我们再观察一下。

typedef void (^MyBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"11111111");
        
        MyBlock block;
        {
            LCPerson *person = [[LCPerson alloc] init];
            person.age = 10;
            
            block = ^(void){
                NSLog(@"person age = %d", person.age);
            };
        }
        
        NSLog(@"22222222");
    }
    
    NSLog(@"3333333");
    return 0;
}

输出结果:

2020-09-27 10:52:27.578241+0800 LearningBlock[20478:70040] 11111111
2020-09-27 10:52:27.578627+0800 LearningBlock[20478:70040] 22222222
2020-09-27 10:52:27.578688+0800 LearningBlock[20478:70040] -[LCPerson dealloc]
2020-09-27 10:52:27.578729+0800 LearningBlock[20478:70040] 3333333

根据结果,我们可以发现加入了block之后,person的销毁是在222222之后发生的,即person所在的作用域结束后,person对象没有立即释放。 那么block究竟对person干了什么,导致person对象没能及时释放呢? 为了分析,我们将上面的代码先简化一下。简化如下

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LCPerson *person = [[LCPerson alloc] init];
        person.age = 10;
        
        void (^block)(void) = ^(void){
            NSLog(@"person age = %d", person.age);
        };
        
        block();
    }
    return 0;
}

将上面OC代码转换为C++代码:(支持ARC、指定运行时系统版本)

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m

转换后的C++代码如下:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  LCPerson *__strong person; // strong类型的指针
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, LCPerson *__strong _person, int flags=0) : person(_person) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  LCPerson *__strong person = __cself->person; // bound by copy

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_vt_j2sf07q142992_z55yg_170w0000gp_T_main_5882d6_mi_0, ((int (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("age")));
        }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->person, (void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);}

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

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(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        LCPerson *person = ((LCPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((LCPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LCPerson"), sel_registerName("alloc")), sel_registerName("init"));
        ((void (*)(id, SEL, int))(void *)objc_msgSend)((id)person, sel_registerName("setAge:"), 10);

        void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, person, 570425344)); 

        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

通过观察可以发现,block内部对person进行了捕获。并且与捕获基本数据类型的auto变量不同的是,捕获对象类型时__main_block_desc_0结构体多了两个函数,分别是copydispose,这两个函数与被捕获对象的引用计数的处理有关。

  • block上拷贝到上时,copy函数被调用,接着它会调用_Block_object_assign函数,处理被捕获对象的引用计数,如果捕获变量时是使用__strong修饰,那么对象的引用计数就会+1. 如果捕获时是__weak修饰,则引用计数不变。(下面会验证)
  • block被回收,即释放时,dispose函数被调用,接着它会调用_Block_object_dispose函数,如果捕获变量时是使用__strong修饰,那么对象的引用计数就会-1. 如果捕获变量时是__weak修饰,则引用计数不变。(下面会验证)

我们知道,在ARC环境下,将block赋值给__strong指针,block会自动调用copy函数。所以 person对象离开了局部作用域后没有释放的原因就很明确了,是因为block调用copy函数时,将person对象的引用计数增加了1,所以当局部作用域结束时,person对象的引用计数并不为0,因此不会释放。 而当block的作用域结束,block调用dispose函数,将person的引用计数减为0,然后person才会释放。

如上面所说,那如果是在MRC环境下,person对象离开局部作用域后就会销毁了, 因为在MRC环境下,将block赋值给__strong指针是不会触发copy函数的,所以person对象应该可以正常释放。

验证一: 将工程切换到MRC模式下,测试刚才的代码,如下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"11111111");
        
        MyBlock block;
        {
            LCPerson *person = [[LCPerson alloc] init];
            person.age = 10;
            
            block = ^(void){
                NSLog(@"person age = %d", person.age);
            };
            
            [person release]; // MRC下需要手动管理内存
        }
        
        NSLog(@"22222222");
    }
    
    NSLog(@"3333333");
    return 0;
}

// 输出结果:
2020-09-27 11:39:05.493388+0800 LearningBlock[33422:105156] 11111111
2020-09-27 11:39:05.493800+0800 LearningBlock[33422:105156] -[LCPerson dealloc]
2020-09-27 11:39:05.493833+0800 LearningBlock[33422:105156] 22222222
2020-09-27 11:39:05.493857+0800 LearningBlock[33422:105156] 3333333

观察输出结果,和预料中的一样。person对象离开局部作用域后正常释放。

验证二:weak修饰的对象类型的auto变量. (记得切回ARC环境)

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"11111111");
        
        MyBlock block;
        {
            LCPerson *person = [[LCPerson alloc] init];
            person.age = 10;
            
            // 弱指针
            __weak LCPerson *weakPerson = person;
            block = ^(void){
                NSLog(@"person age = %d", weakPerson.age);
            };
        }
        
        NSLog(@"22222222");
    }
    
    NSLog(@"3333333");
    return 0;
}
// 输出结果:
2020-09-27 12:00:20.461929+0800 LearningBlock[39325:122309] 11111111
2020-09-27 12:00:20.462321+0800 LearningBlock[39325:122309] -[LCPerson dealloc]
2020-09-27 12:00:20.462361+0800 LearningBlock[39325:122309] 22222222
2020-09-27 12:00:20.462391+0800 LearningBlock[39325:122309] 3333333

观察输出结果,和预料中的一样。person对象离开局部作用域后正常释放。

总结:

  • block内部访问了对象类型的auto变量时

    • 如果block是在栈上,将不会对auto变量产生强引用
  • 如果block被拷贝到堆上

    • 会调用block内部的copy函数
    • copy函数内部会调用_Block_object_assign函数
    • _Block_object_assign函数会根据auto变量的修饰符(__strong__weak__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用
  • 如果block从堆上移除

    • 会调用block内部的dispose函数
    • dispose函数内部会调用_Block_object_dispose函数
    • _Block_object_dispose函数会自动释放引用的auto变量(release

__block修饰符

  • __block可以用于解决block内部无法修改auto变量值的问题
  • __block不能修饰全局变量静态变量static
  • 编译器会将__block变量包装成一个对象.

下面一起验证一下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block int a = 10;
        void (^block)(void) = ^{
            a = 20;
            NSLog(@"a = %d", a);
        };
        block();
    }
    return 0;
}
// 输出结果:
a = 20

将上面OC代码转换为C++代码:(支持ARC、指定运行时系统版本)

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m

得到转换后结果:

struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_a_0 *a; // by ref   这就捕获到的a的引用
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__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_a_0 *a = __cself->a; // bound by ref

            (a->__forwarding->a) = 20;     // 修改值a的值。
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_vt_j2sf07q142992_z55yg_170w0000gp_T_main_ca9eb0_mi_0, (a->__forwarding->a));
        }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 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(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};  // 这就是__block 修饰的a变量。

        void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));  // 传入的是a变量的地址。

        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

由上面可以看到,OC代码 __block int a = 10 转换为C++之后变为了:

    __Block_byref_a_0 a = {0, &a, 0, sizeof(__Block_byref_a_0), 10};

__Block_byref_a_0是一个结构体,结构如下:

struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
}

所以在OC中用__block修饰一个变量, 编译器会自动生成一个全新的OC对象。

image

__block的内存管理

__block 的在block中的内存管理和对象类型的auto变量类似(但也有区别)。

  • block在栈上时,并不会对__block变量产生强引用

  • blockcopy到堆时

    • 会调用block内部的copy函数
    • copy函数内部会调用_Block_object_assign函数
    • _Block_object_assign函数会对__block变量形成强引用(retain)。(这点就是和对象类型的auto变量有区别的地方,对于对象类型的auto变量, _Block_object_assign函数会根据auto变量的修饰符(__strong__weak__unsafe_unretained)做出相应的操作, 而__block则是直接强引用 )
      image
  • block从堆中移除时

    • 会调用block内部的dispose函数
    • dispose函数内部会调用_Block_object_dispose函数
    • _Block_object_dispose函数会自动释放引用的__block变量(release
image

__block的__forwarding指针

image

被__block修饰的对象类型

通过上面我们知道了用__block修饰的基本数据类型的处理。那用__block修饰的对象类型的处理是不是一样的呢? 下面我们一起看下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        __block LCPerson *person = [[LCPerson alloc] init];
        person.age = 10;
        
        void(^block)(void) = ^(void) {
            NSLog(@"person age %d", person.age);
        };
        block();
    }
    return 0;
}

通过xcrun命令:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m

转换成C++后,得到结果如下:

struct __Block_byref_person_0 {
  void *__isa;
__Block_byref_person_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);  // 管理person的内存
 void (*__Block_byref_id_object_dispose)(void*);      // 管理person的内存
 LCPerson *__strong person;   //arc环境下, copy 和 dispose函数,会根据person的修饰类型(__strong、__weak)来对person做相应的内存管理。
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_person_0 *person; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_person_0 *_person, int flags=0) : person(_person->__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_person_0 *person = __cself->person; // bound by ref  // 这里就是强引用

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_hc_wwwl26516td3w0ds9cx80c280000gp_T_main_213c56_mi_0, ((int (*)(id, SEL))(void *)objc_msgSend)((id)(person->__forwarding->person), sel_registerName("age")));
        }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->person, (void*)src->person, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->person, 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(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        __attribute__((__blocks__(byref))) __Block_byref_person_0 person = {(void*)0,(__Block_byref_person_0 *)&person, 33554432, sizeof(__Block_byref_person_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((LCPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((LCPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LCPerson"), sel_registerName("alloc")), sel_registerName("init"))};
        ((void (*)(id, SEL, int))(void *)objc_msgSend)((id)(person.__forwarding->person), sel_registerName("setAge:"), 10);

        void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_person_0 *)&person, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

block拷贝到上时,会调用blockcopy方法,同时还会调用__Block_byref_person_0结构体里的__Block_byref_id_object_copy方法,__Block_byref_id_object_copy内部会调用_Block_object_assign方法,处理结构体__Block_byref_person_0内部的person指针所指对象的引用计数。

总结如下:

  • __block变量在栈上时,不会对指向的对象产生强引用

  • __block变量被copy到堆时

    • 会调用__block变量内部的copy函数
    • copy函数内部会调用_Block_object_assign函数
    • _Block_object_assign函数会根据所指向对象的修饰符(__strong__weak__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用(注意:这里仅限于ARC时会retain,MRC时不会retain
  • 如果__block变量从堆上移除

    • 会调用__block变量内部的dispose函数
    • dispose函数内部会调用_Block_object_dispose函数
    • _Block_object_dispose函数会自动释放指向的对象(release

对象类型的 auto变量 和 __block变量处理的异同:

  • 当block在栈上时,对它们都不会产生强引用

  • 当block拷贝到堆上时,都会通过copy函数来处理它们

    • __block变量(假设变量名叫做a)
    • _Block_object_assign((void)&dst->a, (void)src->a, 8/BLOCK_FIELD_IS_BYREF/);
  • 对象类型的auto变量(假设变量名叫做p)

    • _Block_object_assign((void)&dst->p, (void)src->p, 3/BLOCK_FIELD_IS_OBJECT/);
  • 当block从堆上移除时,都会通过dispose函数来释放它们

    • __block变量(假设变量名叫做a)
    • _Block_object_dispose((void)src->a, 8/BLOCK_FIELD_IS_BYREF*/);
  • 对象类型的auto变量(假设变量名叫做p)

    • _Block_object_dispose((void)src->p, 3/BLOCK_FIELD_IS_OBJECT*/);

循环引用问题

在开发过程中我们经常会遇到block循环引用的问题, 如下:

typedef void (^MyBlock)(void);

@interface LCPerson : NSObject
@property (nonatomic, assign) int age;
@property (nonatomic, copy) MyBlock block;
@end

@implementation LCPerson
- (void)dealloc {
    NSLog(@"%s", __func__);
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        LCPerson *person = [[LCPerson alloc] init];
        person.age = 10;
        person.block = ^{
            NSLog(@"person age %d", person.age);
        };
        
        NSLog(@"211212121122");
    }
    return 0;
}

// 输出结果:
2020-09-28 20:01:48.358822+0800 LearningBlock[41115:298402] 211212121122

由打印结果可以看出,person并没有释放(没有调用person的dealloc方法)。那是什么原因导致的呢?是循环引用。 下面我们来分析一下:

  • @property (nonatomic, copy) MyBlock block;从这句话可以看出,person 强引用着block.
  • block内部访问了person对象的age属性,根据上面所学,我们知道block会对person进行捕获,并且在arc环境下,block赋值给__strong指针时会自动调用copy方法,将block从栈拷贝到堆上, 这样会导致person的引用计数加1,即block强引用着person
image

所以personblock相互强引用着,出现了循环引用,所以person对象不会释放。

那么该如何解决呢? 下面说下在ARC环境和MRC环境分别如何处理?

解决循环引用问题 - ARC

ARC环境下,我们可以通过使用关键字__weak__unsafe_unretained来解决。如下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        LCPerson *person = [[LCPerson alloc] init];
        person.age = 10;

        __weak LCPerson *weakPerson = person;
        // 或者 __unsafe_unretained LCPerson *weakPerson = person;
        person.block = ^{
            NSLog(@"person age %d", weakPerson.age);
        };
        
        NSLog(@"211212121122");
    }
    return 0;
}
// 打印结果:
2020-09-28 20:30:19.659679+0800 LearningBlock[41212:307877] 211212121122
2020-09-28 20:30:19.660256+0800 LearningBlock[41212:307877] -[LCPerson dealloc]

示意图如下:


image

还可以使用__block解决, 如下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        __block LCPerson *person = [[LCPerson alloc] init];
        person.age = 10;
        
        person.block = ^{
            NSLog(@"person age %d", person.age);
            person = nil;
        };
        
        person.block();  // 必须调用
        NSLog(@"211212121122");
    }
    return 0;
}

// 打印结果:
2020-09-28 20:35:32.531704+0800 LearningBlock[41256:310297] person age 10
2020-09-28 20:35:32.532221+0800 LearningBlock[41256:310297] -[LCPerson dealloc]
2020-09-28 20:35:32.532310+0800 LearningBlock[41256:310297] 211212121122

使用__block解决,必须调用block,不然无法将循环引用打破。

image

疑问: __weak__unsafe_unretained关键字有什么区别呢?

使用__weak__unsafe_unretained关键字都能达到弱引用的效果。这两者主要的区别在于,使用__weak关键字修饰的指针,在所指的对象销毁时,指针存储的地址会被清空(即置为nil), 而__unsafe_unretained则不会。

解决循环引用问题 - MRC

  • MRC环境是没有__weak关键字的,所以可以使用__unsafe_unretained关键字解决。(与ARC差不多,这里就不演示了)
  • 同样也可以是__block关键字解决。如下:
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block LCPerson *person = [[LCPerson alloc] init];
        person.age = 10;
        
        person.block = ^{
            NSLog(@"person age %d", person.age);
            person = nil;
        };
        
        [person release];  // MRC需要手动添加内存管理代码
        NSLog(@"211212121122");
    }
    return 0;
}

ARC不同的是,MRC下使用__block解决循环引用问题,不要求一定要调用block。原因上面__block修饰的对象类型里有说到:

_Block_object_assign函数会根据所指向对象的修饰符(__strong__weak__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用(注意:这里仅限于ARC时会retain,MRC时不会retain

后话

这篇文章有点乱,还有待改进。写博客真的费时间,不过能加深印象,也不错。

参考

  1. MJ 底层原理
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345