深入了解iOS中的Block

block是在iOS开发中经常使用到的技术,我们可以通过block传值或回调,响应适当时候该响应的事件。那block具体是怎么实现的,原理是什么,了解这些,能够帮助我们更好、更合适的使用block,可以规避使用block可能带来的循环引用,了解变量捕获机制,正确使用__block!


01

block本质上也是一个OC对象,它内部也有个isa指针,block是封装了函数调用以及函数调用环境的OC对象。

了解block的底层结构

block的变量捕获(capture)

为了保证block内部能够正常访问外部的变量,block有个变量捕获机制

变量类型是否捕获到block内部访问方式
auto修饰符(局部变量)会捕获值传递
static修饰符(局部变量)会捕获指针传递
全局变量不会捕获直接访问,变量的地址

局部变量auto(自动变量)

平时写的局部变量,默认就有auto(自动变量,离开作用域 { } 就会自动销毁),该类型变量一般存储在Stack栈上。

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 实际上是auto int age = 10, 表示自动释放的局部变量,离开作用域就会被释放。
        int age = 10;
        void(^block)(void) = ^{
            NSLog(@"age is %d", age);
        };
        block();
    }
    return 0;
}

// 将代码转为编译后代码
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        int age = 10;
        void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}
// 找到block最后生成的结构体,这里可以看到age成为该结构体的一个变量。
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int age;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

对象类型的auto变量

02

block内部调用对象时,会自动生成相应的内存管理方法。并在适当的时候retainrelese

block内部访问了对象类型的auto变量时,如果block是在栈上,将不会对auto变量产生强引用。

调用copy方法将会调用block内部的_Block_object_assign函数,_Block_object_assign会根据auto变量的修饰符(__strong, __weak, __unsafe_unretained)做出相应的操作。形成强引用(retain)或者弱引用。

block从堆上移除,会调用blcok内部的dispose函数,dispose函数内部会调用_Block_object_dispose函数,_Block_object_dispose会自动释放引用的auto变量。

局部变量static(静态局部变量)

修饰符 extern/staic/const/UIKIT_EXTERN(OC extern)的使用

静态变量(static 修饰的变量)都在全局数据区分配内存,包括静态全局变量和静态局部变量。直到程序结束运行,它才会被释放。

static int height = 20;

// static 局部变量也捕获,但和auto变量不同,static存储的是指针,指向->height内存地址
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int age;
  int *height;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_height, int flags=0) : age(_age), height(_height) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

block的类型

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

NSGlobalBlock_NSConcreteGlobalBlock

NSStackBlock_NSConcreteStackBlock

NSMallocBlock_NSConcreteMallocBlock

block环境内存区、释放机制
NSGlobalBlock没有访问auto变量全局数据区
程序运行结束时才会被释放
NSStackBlock访问了auto变量栈区
变量在作用域结束后就会被释放
NSMallocBlock_NSStackBlock_ 调用了copy堆区
需要程序员手动管理内存

block的copy

block类型副本源的配置存储域复制效果
_NSConcreteStackBlock从栈复制到堆
_NSConcreteGlobalBlock程序的数据区什么也不做
_NSConcreteMallocBlock引用计数器增加

03

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

block作为函数返回值时

typedef void (^Block)(void);

- (Block)doSomething {
	Block b = ^{
		
	};
	return b;
}

block赋值给__strong指针时

int a = 10;
// b访问了auto变量,理论上是__NSStackBlock__, 但是b 默认被__strong修饰
// 所以ARC中会被copy操作上堆。
// 实际上是 __strong Block b = ...
Block b = ^{
		NSLog(@"a is %d", a);
};
// weakb 访问了auto变量,所以在栈上__NSStackBlock__
__weak Block weakb = ^{
    NSLog(@"a is %d", a);
};
// ^{} 没有访问auto变量,所以为:__NSGlobalBlock__
NSLog(@"b class is %@, and block2 class is %@, weakb class is %@",
 [b class], [^{} class], [weakb class]);
// 打印输出
b class is __NSMallocBlock__, 
and block2 class is __NSGlobalBlock__, 
block3 class is __NSStackBlock__

block作为Cocoa API中方法名含有usingBlock的方法参数时

[[[NSArray alloc] init] enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            
    }];

block作为GCD API的方法参数时

// dispatch_async(dispatch_queue_t  _Nonnull queue, <#^(void)block#>)
    dispatch_async(dispatch_get_main_queue(), ^{
        
    });

MRC下block属性的建议写法

@property (copy, nonatomic) void (^block)(void);

ARC下block属性的建议写法

@property (strong, nonatomic) void (^block)(void);
@property (copy, nonatomic) void (^block)(void);

__block的本质是什么?

__block会将修饰对象封装成__Block_byref_a_0结构体,将该对象作为自己的成员变量,如果是对象类型,同时会生成__Block_byref_id_object_copy方法和__Block_byref_id_object_dispose,对其内存进行管理。

// 修饰基本数据类型
__block int a = 10;
NSLog(@"a is %d", a);
// 转为源码
struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};

// 修饰对象
__block NSObject *obj = [[NSObject alloc] init];
NSLog(@"str is %@", obj);
struct __Block_byref_obj_0 {
  void *__isa;
__Block_byref_obj_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 NSObject *obj;
};

__block的使用

04

编译器会将__block变量包装成一个对象

__block可以用于解决block内部无法修改auto变量值的问题

int age = 10;
void (^block)(void) = ^{
    NSLog(@"age is %d", age);
};
age = 20;
block();

// 打印输出
2021-05-30 21:44:34.484323+0800 Strong&Weak[7070:466915] age is 10

这种情况,block捕获age,并将age的值赋值给了block对象中的age。所以之后改变age,也不改变block中的age。通过__block修饰,则会生成一个__Block_byref_age_1结构体

__block int age = 10;

struct __Block_byref_age_1 {
  void *__isa;
__Block_byref_age_1 *__forwarding; // 指向自己的指针
 int __flags;
 int __size;
 int age;
};

// 此时age = 20,在源码中是,第一个age(__Block_byref_age_1)
// 相当于age.age,将值赋值给age对象中的age。
// age.age指向的地址没有改变
(age.__forwarding->age) = 20;

// 所以打印输出自然是20
2021-05-30 22:03:02.218831+0800 Strong&Weak[7264:480507] age is 20

__block不能修饰全局变量、静态变量(static

__block的内存管理

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

blockcopy到堆时

  • 会调用block内部的copy函数
  • copy函数内部会调用_Block_object_assign函数
  • _Block_object_assign函数会对__block变量形成强引用(retain

block从堆中移除时

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

__block的__forwarding指针

循环引用

循环引用的产生

对象持有block,而block持有对象就会造成,双方都无法释放,导致循环应用。

对象持有block,block持有__block变量,__block持有对象也会造成循环引用

循环引用的解决

__weak__unsafe_unretained解决

__block解决(必须要调用block)

解决循环引用问题 – MRC

By:

Posted in:


留下评论

通过 WordPress.com 设计一个这样的站点
从这里开始