iOS-block

199 阅读7分钟

一.block的底层结构及调用

我们简单实现一个block

void(^block)(void) = ^{

     NSLog(@"hello world");

};

block();

转C++代码后

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

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

可以看到block是一个__main_block_impl_0的结构体,接下来继续查看

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;

  }
};

struct __block_impl {

  void *isa;

  int Flags;

  int Reserved;

  void *FuncPtr;

};

static struct __main_block_desc_0 {

  size_t reserved;

  size_t Block_size;
}

可以看到__main_block_impl_0的结构体包含了__block_impl的结构体和__main_block_desc_0结构体,__block_impl又包含了isa指针和方法地址FuncPtr,__main_block_desc_0主要包含了__main_block_impl_0的结构体大小Block_size

从底层结构可以看出,block其实是一个OC对象,那block中的代码是如何执行的呢

((__block_impl *)block)->FuncPtr

从这句代码我们可以看到是把block强转成了(__block_impl *)类型,这里因为block底层结构体的第一个元素为struct __block_impl impl,因此__main_block_impl_0的地址就是impl的地址,所以可以强转,然后通过impl找到方法地址FuncPtr进行调用

总结: block本质上是一个对象,有自己的isa指针,block封装了函数调用及函数调用环境)

二.block的三种类型

从上面我们可以得知block有自己的isa指针,那接下来我们打印下面这几种情况的block的class

int main(int argc, const char * argv[]) {

    @autoreleasepool {

        int age = 10;

        void(^block)(void) = ^{

        };

        NSLog(@"%@",[block class]);


        NSLog(@"%@",[^{

            NSLog(@"%d",age);

        } class]);
    

        void(^block3)(void) = ^{

            NSLog(@"%d",age);

        };

        NSLog(@"%@",[block3 class]);

    }

    return 0;

}

得到打印信息为 NSGlobalBlockNSStackBlockNSMallocBlock

总结

截屏2022-03-29 下午2.25.32.png

没有访问auto变量的为 NSGlobalBlock,存储在全局区,访问了auto变量的为 NSStackBlock,存储在栈区,对 NSStackBlock 进行copy操作的为 NSMallocBlock,存储在堆区

在ARC环境下,有些情况会自动对 NSStackBlock 进行copy操作,都有哪些情况呢?

block作为函数返回值时
block强引用时
block作为Cocoa API中方法名含有usingBlock的方法参数时
block作为GCD API的方法参数时

三.block的捕获

测试auto临时变量的捕获

int age = 10;

void(^block)(void) = ^{

    NSLog(@"age:%d",age);

};

block();

我们发现可以打印age:10,通过上面的分析我们知道此时age作为临时变量存储在栈区,block在ARC下因为被block指针强应用,此时会copy到堆上,那我们的block是如何捕捉到临时变量age的呢?接下来我们转成C++代码进行分析

int age = 10;
void(*block)(void) = &__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));

struct __main_block_impl_0 {

  struct __block_impl impl;

  struct __main_block_desc_0* Desc;

  int age;
}

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

  int age = __cself->age; // bound by copy

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_ry_hyx4zvkj45s9nd1fvr1pw_3r0000gn_T_main_4a1515_mi_0,age);

}

这时我们可以看到__main_block_impl_0结构体新增一个age成员变量,初始化的时候把临时变量int age = 10的值赋值给了内部的成员变量age,在执行block内部代码的时候是从__main_block_impl_0里取出了age的值进行打印

测试static变量的捕获

static int age = 10;

void(^block)(void) = ^{

    NSLog(@"age:%d",age);

};

block();

经过C++转换

struct __main_block_impl_0 {

  struct __block_impl impl;

  struct __main_block_desc_0* Desc;

  int *age;
}

static int age = 10;

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

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

  int *age = __cself->age; // bound by copy

  NSLog((NSString *)&__NSConstantStringImpl__var_folders_ry_hyx4zvkj45s9nd1fvr1pw_3r0000gn_T_main_203c2d_mi_0,(*age));

}

我们可以看到如果是static变量,block捕获的是age的指针变量

测试全局变量的捕获

int age = 10;

int main(int argc, const char * argv[]) {

    @autoreleasepool {

        void(^block)(void) = ^{

            NSLog(@"age:%d",age);
        };
        block();
    }
    return 0;
}

转换C++后

struct __main_block_impl_0 {

  struct __block_impl impl;

  struct __main_block_desc_0* Desc;
}

发现如果是全局变量,不会进行任何捕获,直接访问

总结:

截屏2022-03-29 下午2.55.34.png 对auto变量是值传递,static变量是指针传递,全局变量不进行捕获

四.__block

运行如下的代码

int main(int argc, const char * argv[]) {

    @autoreleasepool {

        int age = 10;

        void(^block)(void) = ^{

            age = 20;

        };
        block();
    }
    return 0;
}

我们发现报错missing __block,接下来我们分析一下为什么会报错,此时age作为临时变量作用域是main函数内,block里的代码是作为另一个函数封装在block对象内,临时变量不能跨函数调用,所以报错了,接下来我们在int age=10前面加一个__block的修饰词 __block int age = 10;我们发现神奇的不报错了,而且我们发现外面的局部变量age的值被修改为20,接下来我们通过转成C++代码来看一下底层做了什么

//main函数执行流程
__Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};

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

((__block_impl *)block)->FuncPtr)((__block_impl *)block);

//block结构体
struct __main_block_impl_0 {

  struct __block_impl impl;

  struct __main_block_desc_0* Desc;

  __Block_byref_age_0 *age; // by ref
}

//__Block_byref_age_0类型结构体
struct __Block_byref_age_0 {

  void *__isa;

  __Block_byref_age_0 *__forwarding;

 int __flags;

 int __size;

 int age;

};

//block块执行的方法
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) = 20;
}

我们发现底层把age封装成了一个__Block_byref_age_0的对象,age的地址传给了__forwarding指针,age的值传给了__Block_byref_age_0结构体里的成员变量age,然后把这个对象传入了block结构体里的age指针

当age的值进行修改的时候是从当前结构体拿到__Block_byref_age_0类型的age指针,然后再通过__forwarding指针拿到age,然后进行值的修改

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

五.block的内存管理

__block的内存管理

转换下面的代码

int main(int argc, const char * argv[]) {

    @autoreleasepool {

        __block int age = 10;

        void(^block)(void) = ^{
        
            age = 20;
            
        };
        
        block();
    }
    return 0;
}

我们发现__main_block_desc_0结构体多了2个函数__main_block_copy_0和__main_block_dispose_0,里面分别调用了_Block_object_assign和_Block_object_dispose函数,我们知道在ARC下,当前的block因为被强指针指向,会执行copy操作,此时会调用这个__main_block_copy_0函数,执行_Block_object_assign对__Block_byref_age_0对象进行强引用

auto对象的内存管理

转换下面的代码

int main(int argc, const char * argv[]) {

    @autoreleasepool {

        Person *p = [[Person alloc] init];

        void(^block)(void) = ^{
        
            NSLog(@"%@",p);
            
        };
        block();
    }
    return 0;
}

我们发现结构和上面差不多,只是__main_block_copy_0和__main_block_dispose_0函数的参数略有不同,还有就是此时执行_Block_object_assign函数会根据所指向的对象的修饰符(__strong,__weak,__unsafe_unretained)进行强引用或弱引用

__block修饰的auto对象的内存管理

转换下面的代码

__block Person *p = [[Person alloc] init];

void(^block)(void) = ^{

   NSLog(@"%@",p);

};

block();

我们发现Person对象被封装成了__Block_byref_p_0结构体

struct __Block_byref_p_0 {

  void *__isa;

__Block_byref_p_0 *__forwarding;

 int __flags;

 int __size;

 void (*__Block_byref_id_object_copy)(void*, void*);

 void (*__Block_byref_id_object_dispose)(void*);

 Person *__strong p;

};

当block从栈copy到堆的时候,会调用__main_block_desc_0里的__main_block_copy_0->_Block_object_assign函数对__Block_byref_p_0 *p进行强引用,然后__Block_byref_p_0会调用内部的__Block_byref_id_object_copy函数根据所指向的对象的修饰符(__strong,__weak,__unsafe_unretained)进行强引用或弱引用

总结:

__block修饰基础类型变量的内存管理:

在栈上时不会产生强引用

当block拷贝到堆时,会调用内部的_Block_object_assign函数进行强引用

当block从堆移除时,会调用内部的_Block_object_dispose函数进行释放

auto对象类型的内存管理:

在栈上时不会产生强引用

当block拷贝到堆时,会调用内部的_Block_object_assign函数根据所指向的对象的修饰符(__strong,__weak,__unsafe_unretained)进行强引用或弱引用

当block从堆移除时,会调用内部的_Block_object_dispose函数进行释放

__block修饰的auto对象的内存管理:

会把对象类型包装成__Block_byref_xxx的结构体

在栈上时不会产生强引用

当block拷贝到堆上时,会调用内部的_Block_object_assign函数对封装的__Block_byref_xxx进行强引用,然后__Block_byref_xxx会调用内部的_Block_object_assign函数根据所指向的对象的修饰符(__strong,__weak,__unsafe_unretained)进行强引用或弱引用

当block从堆移除时,会调用内部的_Block_object_dispose函数进行释放,并且__Block_byref_xxx也会调用内部的_Block_object_dispose函数进行释放

六.block造成的循环引用

执行下面的代码

@interface BlockController ()

@property (nonatomic, assign) int age;

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

@end

- (void)viewDidLoad {

    [super viewDidLoad];

    self.block = ^{

        self.age = 10;

    };
    self.block();
}

xcode会给出警告Capturing 'self' strongly in this block is likely to lead to a retain cycle,并且监听delloc方法发现不会执行,这时,就因为我们的不当操作造成了循环引用,从而导致内存泄漏,那我们来分析一下上面的这段代码

首先控制器强引用block属性,block属性捕获了self,这时block从栈copy到堆时,执行_Block_object_assign函数根据self进行引用,因为当前self是__strong类型,所以会强引用当前控制器,控制器强引用block,block又强引用控制器,所以造成了循环引用,那如何解决此类问题呢,只需要把当前对象进行弱引用即可,例如当前代码可以改为

- (void)viewDidLoad {

    [super viewDidLoad];

    __weak typeof(self)weakSelf = self;
    
    self.block = ^{

        weakSelf.age = 10;

    };
    self.block();
}