【iOS】Block 核心原理

204 阅读8分钟

目标:

  1. 熟悉Block底层原理
  2. 熟悉内存拷贝相关的知识

如何深入学习相关概念呢? 将OC代码转化成C++代码,查看其内部实现。 面试过程中,每道题都是有对应分数的,一定要尽可能答的漂亮。

什么是Block

常见面试题:

  1. 什么是Block?
  2. 你对Block的调用是怎么理解的?
  3. 截获变量是Block的一大特性,系统对于Block的截获是怎么实现的呢?
  4. 我们添加__block修饰符做什么事情呢?
  5. 什么时候需要对Block 进行一个copy操作,栈Block和堆Block是否了解呢?
  6. Block的循环引用如何解决
  7. block使用注意事项有哪些

知识相关性,引用计数相关(修饰符,访问安全,循环引用)__block修饰符的本质

1. 什么是Block?

Block是一种带有自动变量(局部变量)的匿名函数,功能上看类似于C语言上的函数指针,但比函数指针更强大,它可以捕获其定义时所在作用域的自动变量。本质上是一个对象,封装了执行函数和上下文。底层上看Block时一个struct结构体,包含了函数指针、捕获结构体指针以及一些额外信息。 分为三种类型:

内存管理:Block 有三种类型,分别对应不同的内存区域:

  • _NSConcreteStackBlock:栈上的 Block,当定义 Block 的函数返回时,该 Block 会被销毁。这种类型的 Block 通常在函数内部定义且未进行 copy 操作时存在。
  • _NSConcreteGlobalBlock:全局的 Block,存放在数据段,类似于全局变量。当 Block 没有捕获任何自动变量时,它会被优化为全局 Block。
  • _NSConcreteMallocBlock:堆上的 Block,需要程序员手动或者自动调用 copy 操作将栈上的 Block 复制到堆上,以延长其生命周期。ARC 环境下,编译器会自动在合适的地方插入 copy 操作。

捕获方式分为: 无捕获、值捕获、引用捕获。

源码解析

+ (void)testBlock1{
    int multiplier = 6;
    int (^Block)(int) = ^int(int num) {
        return num *multiplier;
    };
    Block(2);

}

使用: clang -rewrite-objc file.m 查看编译后的文件内容

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

struct __TestObject__testBlock1_block_impl_0 {
  struct __block_impl impl;
  struct __TestObject__testBlock1_block_desc_0* Desc;
  int multiplier;
  __TestObject__testBlock1_block_impl_0(void *fp, struct __TestObject__testBlock1_block_desc_0 *desc, int _multiplier, int flags=0) : multiplier(_multiplier) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static int __TestObject__testBlock1_block_func_0(struct __TestObject__testBlock1_block_impl_0 *__cself, int num) {
  int multiplier = __cself->multiplier; // bound by copy


        return num *multiplier;
    }

static struct __TestObject__testBlock1_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __TestObject__testBlock1_block_desc_0_DATA = { 0, sizeof(struct __TestObject__testBlock1_block_impl_0)};

// _C 表示类方法,_I 实例方法 ,TstObject 表示类名,testBlock1表示函数名
static void _C_TestObject_testBlock1(Class self, SEL _cmd) {
    int multiplier = 6;

    int (*Block)(int) = ((int (*)(int))&__TestObject__testBlock1_block_impl_0((void *)__TestObject__testBlock1_block_func_0, &__TestObject__testBlock1_block_desc_0_DATA, multiplier));

    ((int (*)(__block_impl *, int))((__block_impl *)Block)->FuncPtr)((__block_impl *)Block, 2);


}

Block调用即是函数的调用。 从Block中取出函数指针传递两个参数Block和需要的参数。

截获变量

在这里插入图片描述

  • 对于基本数据类型的局部变量截获其值。
  • 对于对象类型的局部变量连同所有权修饰符一起截获(强引用)。
  • 以指针形式截获局部静态变量。
  • 不截获全局变量、静态全局变量。

使用

- (void)method {
    // 基本类型的局部变量
    int var =1;
    // 对象类型的局部变量
    __unsafe_unretained id unsafe_obj = nil;
    __strong id strong_obj = nil;
    
    //静态局部变量
    static int static_var = 3;
    
    void(^Block)(void) = ^{
        NSLog(@"局部变量<基本数据类型> var %d",var);
        NSLog(@"局部变量<__unsafe_unretained 对象类型> var %@",unsafe_obj);
        NSLog(@"局部变量<__strong 对象类型> var %@",strong_obj);
        
        NSLog(@"静态变量 %d",static_var);
        
        NSLog(@"全局变量<基本数据类型> var %d",global_var);
        NSLog(@"静态全局变量<基本数据类型> var %d",static_global_var);
        
        // 使用对象类型的  会防止循环引用  从而进行 提示
//        NSLog(@"TestObject 对象类型成员 num %d",self.num);
    };
    Block();
    
}


clang -rewrite-objc -fobjc-arc file.m

struct __TestObject__method_block_impl_0 {
  struct __block_impl impl;
  struct __TestObject__method_block_desc_0* Desc;
  int var;
  __unsafe_unretained id unsafe_obj;
  __strong id strong_obj;
  int *static_var;
  __TestObject__method_block_impl_0(void *fp, struct __TestObject__method_block_desc_0 *desc, int _var, __unsafe_unretained id _unsafe_obj, __strong id _strong_obj, int *_static_var, int flags=0) : var(_var), unsafe_obj(_unsafe_obj), strong_obj(_strong_obj), static_var(_static_var) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __TestObject__method_block_func_0(struct __TestObject__method_block_impl_0 *__cself) {
  int var = __cself->var; // bound by copy
  __unsafe_unretained id unsafe_obj = __cself->unsafe_obj; // bound by copy
  __strong id strong_obj = __cself->strong_obj; // bound by copy
  int *static_var = __cself->static_var; // bound by copy

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_89_hqtxy5r12y19qtg10mhvjrgr0000gn_T_TestObject_86265e_mi_0,var);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_89_hqtxy5r12y19qtg10mhvjrgr0000gn_T_TestObject_86265e_mi_1,unsafe_obj);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_89_hqtxy5r12y19qtg10mhvjrgr0000gn_T_TestObject_86265e_mi_2,strong_obj);

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_89_hqtxy5r12y19qtg10mhvjrgr0000gn_T_TestObject_86265e_mi_3,(*static_var));

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_89_hqtxy5r12y19qtg10mhvjrgr0000gn_T_TestObject_86265e_mi_4,global_var);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_89_hqtxy5r12y19qtg10mhvjrgr0000gn_T_TestObject_86265e_mi_5,static_global_var);



    }

__block修饰符

使用场景: 一般情况下,对被截获变量进行赋值操作需要添加__block修饰符。 __block修饰符用于实现对变量的引用捕获,使得block内部可以修改外部变量的值。 在底层__block修饰的变量会被封装成一个结构体,结构体内部包含一个指向变量值的指针。block通过这个指针来访问和修改变量的值,从而实现引用捕获。

__block修饰的变量变成了对象。

+ (void)testBlock1{
    __block int multiplier = 6;
    int (^Block)(int) = ^int(int num) {
        return num *multiplier;
    };
    Block(2);

}

// 编译成C++文件后
struct __Block_byref_multiplier_0 {
  void *__isa;
__Block_byref_multiplier_0 *__forwarding;
 int __flags;
 int __size;
 int multiplier;
};

struct __TestObject__testBlock1_block_impl_0 {
  struct __block_impl impl;
  struct __TestObject__testBlock1_block_desc_0* Desc;
  __Block_byref_multiplier_0 *multiplier; // by ref
  __TestObject__testBlock1_block_impl_0(void *fp, struct __TestObject__testBlock1_block_desc_0 *desc, __Block_byref_multiplier_0 *_multiplier, int flags=0) : multiplier(_multiplier->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void _C_TestObject_testBlock1(Class self, SEL _cmd) {
    __attribute__((__blocks__(byref))) __Block_byref_multiplier_0 multiplier = {(void*)0,(__Block_byref_multiplier_0 *)&multiplier, 0, sizeof(__Block_byref_multiplier_0), 6};
    
    int (*Block)(int) = ((int (*)(int))&__TestObject__testBlock1_block_impl_0((void *)__TestObject__testBlock1_block_func_0, &__TestObject__testBlock1_block_desc_0_DATA, (__Block_byref_multiplier_0 *)&multiplier, 570425344));
    
    ((int (*)(__block_impl *, int))((__block_impl *)Block)->FuncPtr)((__block_impl *)Block, 2);

}

使用 不等于 赋值

笔试题

    // 问题:此处是否需要对array 使用__block进行修饰呢
    NSMutableArray * array = [[NSMutableArray alloc] init];
    void (^Block)(void) = ^{
        [array addObject:@123];
    };
    Block();
    NSLog(@"array = %@",array);

此处不需要,仅仅为使用。 下面这个呢: 局部变量不管对象类型是基本数据类型还是对象类型,进行赋值操作都需要添加__block修饰符。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LyQwDFqe-1598110556261)(evernotecid://E87D830C-ABD9-4F44-8969-DDFD5EDD63B4/appyinxiangcom/16371682/ENResource/p295)]

对变量进行赋值时, 在这里插入图片描述 需要__block修饰符 在这里插入图片描述

Block内存管理

Block的Copy操作 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z9sQGbNX-1598110556266)(evernotecid://E87D830C-ABD9-4F44-8969-DDFD5EDD63B4/appyinxiangcom/16371682/ENResource/p299)]

在这里插入图片描述

为什么会有成员变量__forwarding呢?
  1. __forwarding持有指向该实例自身的指针,通过成员变量__forwarding访问成员变量val。这样做是为了在多个Block中使用__block变量。
  2. 可以实现无论__block变量配置在栈上还是堆上时都可以正确的访问__block变量。

不论在任何内存位置,都可以顺利的访问同一个__block变量。

什么时候发生copy?什么时候从栈copy到堆上?

总共有4种情况

  1. 调用Block 的copy实例方法时
  2. Block作为函数返回值返回时
  3. 将Block赋值给附有__strong修饰符id类型的类或Block类型成员变量时
  4. 在方法名中含有usingBlock的Cocoa框架方法或GCD(Grand central dispatch)的API中传递Block时。

循环引用

    _array = [NSMutableArray arrayWithObject:@"block"];
    _strBlk = ^NSString *(NSString *num) {
        return [NSString stringWithFormat:@"helloc_%@",_array[0]];
    };
    
    _strBlk(@"hello");
    
    // 解决方式
    _array = [NSMutableArray arrayWithObject:@"block"];
    __weak NSArray * weakArray = _array;
    _strBlk = ^NSString *(NSString *num) {
        return [NSString stringWithFormat:@"helloc_%@",weakArray[0]];
    };
    _strBlk(@"hello");

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q08bQNJh-1598110556268)(evernotecid://E87D830C-ABD9-4F44-8969-DDFD5EDD63B4/appyinxiangcom/16371682/ENResource/p300)]

在MRC下,不会产生循环引用。 在ARC下,会产生循环引用,引起内存泄漏。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OToxJaSr-1598110556269)(evernotecid://E87D830C-ABD9-4F44-8969-DDFD5EDD63B4/appyinxiangcom/16371682/ENResource/p301)] [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-onf8YuR6-1598110556270)(evernotecid://E87D830C-ABD9-4F44-8969-DDFD5EDD63B4/appyinxiangcom/16371682/ENResource/p302)]

此时如果不调用或者是长时间不调用的话,环就会一直存在。

block使用注意事项有哪些

  1. 循环引用 (_weak 解除循环引用,内部防止调用期间释放,__strong强引用一下 )
  2. 初始化问题,防止崩溃(确保初始化 使用时候要检查)
  3. 捕获引用时候,值捕获问题。(__block)
  4. 多线程情况下静态(使用锁)

避免在多线程环境下未保护的 Block 调用:如果在多线程环境中使用 Block,要注意对共享资源的访问控制。例如,如果 Block 访问并修改共享变量,可能会导致数据竞争问题。可以使用锁(如 NSLockdispatch_semaphore 等)来保护共享资源。

循环引用造成内存泄露,常见使用检测方法

image.png