OC

iOS中的block

Posted by sunzhongliang on September 6, 2019

block

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

int age = 20;
    
void (^block)(int, int) =  ^(int a , int b){
    NSLog(@"this is a block! -- %d", age);
    NSLog(@"this is a block!");
    NSLog(@"this is a block!");
    NSLog(@"this is a block!");
};

通过clang指令将其转成C语言函数看看生成了什么东西

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

生成了这些

int age = 20;

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

查看__main_block_impl_0底层,是一个结构体定义

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;
  }
};

结构体的第一个成员__block_impl再查看底层,也是一个结构体变量

struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;  // 指向了将来要执行block代码的函数地址
};

两者合并以及移除一些不相干的东西

struct __main_block_desc_0 {
    size_t reserved; // 没什么用
    size_t Block_size;  // block占用的空间大小
};

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

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    int age;
};

以上就是block的生成内容,总结来说block就是一个OC对象,isa也是一个对象,OC对象封装了函数的地址、block大小以及外部的局部变量

block 本质

定义一个这样的block,利用clang指令生成C++代码看看是什么:

void (^block)(void) = ^{
    NSLog(@"Hello, World!");
};

block();

删除强制转换的代码后,生成如下:

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

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    // 构造函数(类似于OC的init方法),返回结构体对象
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
        impl.isa = &_NSConcreteStackBlock; // 指向block的类型
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

// 封装了block执行逻辑的函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_c60393_mi_0);
}
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)};

// __main_block_impl_0返回了一个结构体,在将结构体的指针赋值给了block
// -------定义block变量-------------
void (*block)(void) = &__main_block_impl_0(
                                    __main_block_func_0,
                                    &__main_block_desc_0_DATA
                                    );
// 找到block的FunPtr,执行block内部的代码
block->FuncPtr(block);

block 变量捕获

为了保证block内部能够正常访问外部的变量,block有个变量捕获机制
假设有这样一段代码,执行结果NSLog会输出age is 10,为什么输出10呢,这就需要了解block的变量捕获机制了

int age = 10;
void (^block)(void) = ^{
    // age的值捕获进来(capture)
    NSLog(@"age is %d", age);
};

age = 20;
block();

利用clang命令生成代码如下:

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    int age;
    // age(_age) 代表传进来的_age参数会自动赋值给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;
    }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int age = __cself->age; // bound by copy
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_sy_rn7c3q7j76d2vf92dj0yl1g00000gn_T_main_0b78ef_mi_0, age);
}

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 age = 10;

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

age = 20;

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

这时候发现block内部多了一个age的成员,传进来的age参数将会赋值给block内部的age成员
那假如是static类型变量呢?

// auto:自动变量,离开作用域就销毁
int weight = 10; // 也代表auto意思
auto int age = 10;
static int height = 10;

void (^block)(void) = ^{
    // age的值捕获进来(capture)
    NSLog(@"age is %d, height is %d", age, height);
};

age = 20;
height = 20; // block将不会捕获static变量,使用指针访问的方式,因此height能够修改

block();

输出:age is 10, height is 20
查看C++代码:

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;
    }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int age = __cself->age; // bound by copy
    int *height = __cself->height; // bound by copy
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_sy_rn7c3q7j76d2vf92dj0yl1g00000gn_T_main_7bfbdf_mi_0, age, (*height));
}

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 weight = 10;
auto int age = 10;
static int height = 10;

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

age = 20;
height = 20;

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

注意看height访问方式,是使用*访问

总结如下:

block 类型

block也有类型, 可以通过class查看具体的类型:

// __NSGlobalBlock__ : __NSGlobalBlock : NSBlock : NSObject
void (^block)(void) = ^{
    NSLog(@"Hello");
};

NSLog(@"%@", [block class]);
NSLog(@"%@", [[block class] superclass]);
NSLog(@"%@", [[[block class] superclass] superclass]);
NSLog(@"%@", [[[[block class] superclass] superclass] superclass]);
// -------------输出为:-------------
__NSGlobalBlock__
__NSGlobalBlock
NSBlock
NSObject

可以看到block最终也是继承自NSObject,也是一个OC对象

block的其他类型取决于变量访问方式

// 堆:动态分配内存,需要程序员申请申请,也需要程序员自己管理内存
void (^block1)(void) = ^{
    NSLog(@"Hello");
};

int age = 10;
void (^block2)(void) = ^{
    NSLog(@"Hello - %d", age);
};

NSLog(@"类型为:%@ %@ %@", [block1 class], [block2 class], [^{
    NSLog(@"%d", age);
} class]);

// 输出为:
类型为:__NSGlobalBlock__ __NSMallocBlock__ __NSStackBlock__

通过clang指令生成C++代码后查看isa指针的类型,发现生成了三个block(__main_block_impl_0, __main_block_impl_1, __main_block_impl_2)
但三个block的isa指针都是_NSConcreteStackBlock,但为什么通过[block class]; 查看类型会发生变化呢?
首先,我们一切以运行时的结果为准,编译完是这样子,但是真正在运行过程中,可能会通过runtime动态修改一些东西,这也会导致程序运行过程中结果与编译其实是有点变化的

总结

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

  • NSGlobalBlock ( _NSConcreteGlobalBlock )
  • NSStackBlock ( _NSConcreteStackBlock )
  • NSMallocBlock ( _NSConcreteMallocBlock )

MRC环境下如果创建这样一个block

void (^block)(void);
void test2()
{
    
    // NSStackBlock
    int age = 10;
    block = [^{
        NSLog(@"block---------%d", age);
    }];
}
// 最终打印结果不是 10

这样一个block因为它访问了auto变量,所以是一个栈block,函数调用完毕出了作用域,那么栈内存的数据就会被释放,最终打印出来的数据就会错乱
解决办法就是将block进行copy操作,那么block就会变成堆block, 堆上面的空间就需要程序员自己去处理

block的copy

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

  • block作为函数返回值时
  • 将block赋值给__strong指针时
  • block作为Cocoa API中方法名含有usingBlock的方法参数时, 如调用数组的遍历方法
  • block作为GCD API的方法参数时

MRC下block属性的建议写法

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

ARC下block属性的建议写法

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

对象类型的auto变量

当block内部访问了对象类型的auto变量时会发生什么

  • 如果block是在栈上,将不会对auto变量产生强引用
  • 如果block被拷贝到堆上
    • 会调用block内部的copy函数
    • 会调用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)
函数 调用时机
copy函数 栈上的block复制到堆上时
dispose函数 堆上的block被废弃时

在使用clang转换OC为C++代码时,可能会遇到以下问题
cannot create __weak reference in file using manual reference
解决方案:支持ARC、指定运行时系统版本,比如
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m

block修改变量

当我们定义一个block时,并不能直接修改auto声明的变量值,如:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        int age = 10;
        
        typedef void (^MyBlock)(void);
        MyBlock block1 = ^{
            age = 20;
            NSLog(@"age is %d", age);
        };
        
        block1();
        
    }
    return 0;
}

其实不能改的本质原因是,age10是在main函数里面声明的,而block要执行的代码是在__main_block_func_0函数里面,__main_block_func_0函数拿到的age变量只是block的age成员,要从__main_block_func_0函数直接修改main函数声明的变量是不可取的
要想改,可以采用static声明、或者是全局变量、或者__block的方式

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        __block int age = 10;
        
        typedef void (^MyBlock)(void);
        MyBlock block1 = ^{
            age = 20;
            NSLog(@"age is %d", age);
        };
        
        block1();
        
    }
    return 0;
}

而且这种做法并没有修改age的访问方式,还是一个auto声明的变量

__block修饰符

通过clang指令生成C++代码后,发现__main_block_impl_0 里面生成了 __Block_byref_age_0 *age; // by ref
再查看__Block_byref_age_0 *age定义

struct __Block_byref_age_0 {
    void *__isa;
    __Block_byref_age_0 *__forwarding; // __forwarding:指向自己

    int __flags;
    int __size;
    int age;
};

查看main函数调用代码:

__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};
// 精简之后变为
__Block_byref_age_0 age = {
    0,
    &age,
    0,
    sizeof(__Block_byref_age_0),
    10
}

block的执行函数生成的C++代码: 查看main函数调用代码:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    __Block_byref_age_0 *age = __cself->age; // bound by ref
    NSObject *p = __cself->p; // bound by copy
    (age->__forwarding->age) = 20;
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_e2457b_mi_0, p);
}

利用age拿到__forwarding指针,在通过__forwarding指针访问age,然后直接修改值

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

block的内存管理

当block在栈上时,并不会对__block拥有的变量产生强引用
当block被copy到堆时
会调用block内部的copy函数
copy函数内部会调用_Block_object_assign函数
_Block_object_assign函数会对__block拥有的变量形成强引用(retain)
当block从堆中移除时
会调用block内部的dispose函数
dispose函数内部会调用_Block_object_dispose函数
_Block_object_dispose函数会自动释放引用的__block变量(release)

本文首次发布于 孙忠良 Blog, 作者 [@sunzhongliang] , 转载请保留原文链接.