Blocks的实现


之前写过Blocks,

Block的实质

先写一个简单的block

int main() {
    void (^blk)(void) = ^{
        printf("Block\n");
    };
    blk();
    return 0;
}

该代码转化为C++可变换为以下形式:

struct __block_impl {  //block的一些基础属性,像是block的基类
    void *isa; //block存放位置,取值为_NSConcretGlobalBlock(全局区)、_NSConcretStackBlock(栈区)、_NSConcretMallocBlock(堆区)
    int Flags; //用于按bit位表示一些block的附加信息,block copy的实现代码可以看到对该变量的使用。
    int Reserved; //保留变量
    void *FuncPtr; 
}

struct __main_block_impl_0 {  //block变量 命名规则:“main”为block所在函数名,如果定义在函数外为block的名字,末尾的“0”表示为当前函数中第“0”个block
    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 __main_block_impl_0 简化以后为
/* 
struct __main_block_impl_0 {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
    size_t reserved;
    size_t Block_size;
    __main_block_impl_0(#block匿名函数的指针#,#__main_block_desc_0的实例指针#, int flags=0);
};
*/

static void __main_block_func_0(struct __main_block_impl_0 *__cself) { //block的匿名函数。存放block内的语句
    printf("Block\n");
}

static struct __main_block_desc_0 {  //block的描述,他有一个实例__main_block_desc_0_DATA
    unsigned long reserved;
    unsigned long Block_size;
} __main_block_desc_0_DATA = {
    0,
    sizeof(struct __main_block_impl_0)
};

int main() {
    void (*blk)(void) =
        (void (*)(void))& __main_block_impl_0 (
            (void *)__main_block_func_0, &__main_block_desc_0_DATA);
        ((void (*)(struct __block_impl *))(
            (struct __block_impl *)blk) -> FuncPtr)((struct __block_impl *)blk);
    return 0;
}

两个main函数对照:

int main() {
    //定义block
    void (^blk)(void) = ^{
        printf("Block\n");
    };
    //执行block
    blk();
    return 0;
}

//为了更容易观察对比,C++部分去掉转化的部分
int main() {
   
    void (*blk)(void) =
        //定义block
       &__main_block_impl_0 (__main_block_func_0, &__main_block_desc_0_DATA);

       //执行block*blk -> impl.FuncPtr)(blk);
    return 0;
}

上述定义代码中,可以发现,block定义中调用了__main_block_impl_0函数,并且将__main_block_impl_0函数的地址赋值给了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;
    }
};

代入main函数中,得知__main_block_func_0存在impl.FuncPtr中,我们再回顾一下struct __block_impl的内部结构


static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    printf("Block\n");
}

而在__main_block_func_0中可以看到有我们的输出语句,说明__main_block_func_0用于存放我们在block中的代码


static struct __main_block_desc_0 {
    unsigned long reserved;
    unsigned long Block_size;
} __main_block_desc_0_DATA = {
    0,
    sizeof(struct __main_block_impl_0)
};

可以看出,__main_block_desc_0的参数reserved默认值是0,我们暂时不管它,而Block_size则是存放结构体的大小。最后看__main_block_impl_0的第一个参数的结构体

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

有isa指针说明block的本质是OC对象

接着通过上面对__main_block_impl_0结构体构造函数三个参数的分析我们可以得出结论:

  1. __block_impl结构体中isa指针存储着&_NSConcreteStackBlock地址,可以暂时理解为其类对象地址,block就是_NSConcreteStackBlock类型的。
  2. block代码块中的代码被封装成__main_block_func_0函数,FuncPtr则存储着__main_block_func_0函数的地址。
  3. Desc指向__main_block_desc_0结构体对象,其中存储__main_block_impl_0结构体所占用的内存。

__block说明符

首先我们知道,如果block外有一auto变量,需要在block中改变变量的值,例如:

int main(int argc, const char *argv[]) {
    @autoreleasepool {
        int a = 1;
        void (^blk)(void) = ^{
            a = 2;
            NSLog("a = %d", a);
        };
        blk();
    }
    return 0;
}

这样的话就会报错
我们看一下C++代码,为什么不能更改呢,在这里我只截取了部分C++代码

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0 *Desc;
    int a;

    __main_block_impl_0 (void *fp, struct __main_block_desc_0 *desc, int a, int flags = 0) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};


static void __main_block_func_0(struct __main_block_impl_0 *_cself) {
    int a = __cself -> a; //这里的a是从__main_block_impl_0访问到的
    NSLog((NSString *)&__NSConstantStringImpl__val_floders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_d84812_mi_0, a);
}

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

很明显,我们不能在一个函数里改变另外一个函数中的值,访问不到那块地址,所以这样会报错。
如果为了解决这一点,可以把变量设为全局变量或者静态局部变量。
如果变为全局变量或者静态局部变量,以上C++代码会变为

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0 *Desc;
    int *a;

    __main_block_impl_0 (void *fp, struct __main_block_desc_0 *desc, int *a, int flags = 0) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};


static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int *a = __cself -> a;
    (*a) = 2;
    NSLog((NSString *)&__NSConstantStringImpl__val_floders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_d84812_mi_0,*a));
}

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

变成了指针,我们就可以通过指针来改变对应的值
但有时我们并不希望变量成为全局变量或是静态局部变量,就需要用到__block关键字

int main(int argc, const char *argv[]) {
    @autoreleasepool {
        __block int a = 1;
        void (^blk)(void) = ^{
            a = 2;
            NSLog("a = %d", a);
        };
        blk();
    }
    return 0;
}

这样就可以运行成功了,而且a只是一个局部变量
注意:__block不能修饰全局变量,静态变量
这时候看对应的C++代码,就会变成:

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;

    __main_block_impl_0 (void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *a, int flags = 0) {
        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;
    (a -> __forwarding -> a) = 2;
    NSLog((NSString *)&__NSConstantStringImpl__val_floders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_d84812_mi_0, (a -> forwarding -> a));
}

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), 1);
//为了方便阅读,去掉转化
/*
    __Block_byref_a_0 a = {
        0,
        &a, //把自己的地址传给forwarding
        0,
        sizeof(__Block_byref_a_0),
        1
    };
*/
    void (*blk)(void) =
        ((void (*)())& __main_block_impl_0 (
            (void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0)&a, 578425344));
//去掉转化
/*
    void (*blk)(void) =
       & __main_block_impl_0 (
           __main_block_func_0, 
           &__main_block_desc_0_DATA, 
           &a, 
           578425344));
*/
        ((void (*)(struct __block_impl *))(
            (__block_impl *)blk) -> FuncPtr)((__block_impl *)blk);
    }
    return 0;
}

编译器会将__block包装成一个对象,就可以修改block外面的变量了
注意:

int main(int argc, const char *argv[]) {
    @autoreleasepool {
        NSMutableArray *array = [NSMutableArray array];
        void (^blk)(void) = ^{
            [array addObject: @"1"];
        };
        blk();
    }
    return 0;
}

不会报错,因为它没有修改指针对应的值,而是使用这个指针,当array = nil时才会报错。

Block循环引用

举个例子

@interface MyObject : NSObject {
    Block blk;
}
@end

@implementation MyObject

- (id)init {
    self = [super init];
    
    blk = ^{
        NSLog(@"self = %@", self);
    };
    
    return self;
}

- (void)dealloc {
    NSLog(@"dealloc");
}

@end

int main() {
    id o = [[MyObject alloc] init];
    NSLog(@"%@", o);
}

在这里插入图片描述
由打印结果可知,该段代码的dealloc一定没有被调用
编译器对循环引用的警告:
在这里插入图片描述
MyObject类对象的Block类型成员变量blk持有赋值为Block的强引用。即MyObject类对象持有Block。init实例方法中执行的Block语法使用附有__strong修饰符的id类型变量self。并且由于Block语法赋值在了成员变量blk中,因此通过Block语法生成在栈上的Block此时由栈复制到堆,并持有所使用的self。self持有Block,Block持有self。这正是循环引用。
在这里插入图片描述
为避免此循环引用,可声明附有__weak修饰符的变量,并将self赋值使用

- (id)init {
    self = [super init];
    
    id __weak tmp = self;
    
    blk = ^{
        NSLog(@"self = %@", tmp);
    };
    
    return self;
}

在这里插入图片描述

在该源代码中,由于Block存在时,持有该Block的MyObject类对象即赋值在变量tmp中的self必定存在,因此不需要判断tmp的值是否为nil,也可使用__unsafe_unretained修饰符。

另外,以下源代码中Block内没有使用self也同样截获了self,引起了循环引用

@interface MyObject : NSObject {
    Block blk;
    id obj;
}
@end

@implementation MyObject

- (id)init {
    self = [super init];
    
    blk = ^{
        NSLog(@"obj = %@", obj);
    };
    
    return self;
}

- (void)dealloc {
    NSLog(@"dealloc");
}

@end

在这里插入图片描述
由警告信息可知Block语法内使用的obj实际上截获了self,对编译器来说只不过是对象用结构体对成员变量,该源代码和上面的一样,声明附有 __weak修饰符的变量并赋值obj使用来避免循环引用,同理,也可用__unsafe_unretained
我认为这里与内存管理中的用弱引用避免保留环相似

另外,还可以使用 __block变量来避免循环引用

typedef void (^Block)(void);

@interface MyObject : NSObject {
    Block blk;
}
@end

@implementation MyObject

- (id)init {
    self = [super init];
    
    __block id tmp = self;
    
    blk = ^{
        NSLog(@"self = %@", tmp);
        tmp = nil;
    };
    
    return self;
}

- (void)execBlock {
    blk();
}

- (void)dealloc {
    NSLog(@"dealloc");
}

@end

int main() {
    id o = [[MyObject alloc] init];
    [o execBlock];
    return 0;
}

该源代码没有引起循环引用。但是如果不调用execBlock实例方法,即不执行赋值给成员变量blk的Block,便会循环引用并引起内存泄漏。在生成并持有MyObject类对象的状态下会引起以下循环引用

  • MyObject类对象持有Block
  • Block持有 __block变量
  • __block变量持有MyObject类对象

在这里插入图片描述
如果不执行execBlock实例方法,就会持续该循环引用从而造成内存管理
通过执行execBlock实例方法,Block被实行,nil被赋值在__block 变量tmp中,因此, __block变量tmp对MyObject类对象的强引用失效,过程如下:

  • MyObject类对象持有Block
  • Block持有 __ block变量
    在这里插入图片描述
    下面我们对使用__block变量避免循环引用的方法和使用__weak修饰符及__unsafe_unretained修饰符避免循环引用的方法做个比较
    使用 __block变量的优点:
  • 通过__block变量可控制对象的持有期间
  • 在不能使用 __weak修饰符的环境中也可不使用__unsafe_unretained修饰符(不必担心悬垂指针)
  • 在执行Block时可动态地决定是否将nil或其他对象赋值在__block变量中
    使用 __block变量的缺点:
  • 为避免循环引用必须执行Block
    存在执行了Block语法,却不执行Block的路径时,无法避免循环引用。若由于Block引发了循环引用时,根据Block的用途选择使用 __block变量,__weak修饰符或__unsafe_unretained修饰符来避免循环引用

block的copy/release

我们先回顾一下Block类型

NSGlobalBlock 没有访问auto变量
NSStackBlock 访问了auto变量
NSMallocBlock NSStackBlock 调用了copy

在这里插入图片描述
在这里插入图片描述

copy/release

ARC无效时,一般需要手动将Block从栈复制到堆。另外,由于ARC无效,所以肯定要释放复制的Block。这时我们用copy实例方法用来复制,release实例方法用来实现
即:

void (^blk_on_heap)(void) = [blk_on_stack copy];

[blk_on_heap release];

只要Block有一次复制并配置在堆上,就可通过retain实例方法持有。

[blk_on_heap retain];

但是对于配置在栈上的Block调用retain实例方法不起任何作用

[blk_on_stack retain];

该源代码中,虽然对赋值给blk_on_stack栈上的Block调用了retain实例方法,但实际上对此源码不起任何作用。因此推荐使用copy实例方法来持有Block。
另外,由于Blocks是C语言对扩展,所以在C语言中也可以使用Blocks语法。此时使用“Block_copy函数”和“Block_release函数”代替copy和release实例方法。使用方法以及引用计数的思考方法和OC相同

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

block作为函数返回时

// 先定义一个Block
typedef void (^Block)(void);
Block blk(){
       int a = 0;
    return ^{
        NSLog(@"blk, %d", a);
    };
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //Block作为函数返回
        Block useBlk = blk();
        useBlk();
        NSLog(@"%@", [useBlk class]);
    }
    return 0;
}

在这里插入图片描述
正常打印出结果
但是如果是在MRC情况下,我们定义Block时等同于在Block里定义了一个block然后返回赋值给useBlk,没有自动copy的话内存在blk使用完就已经释放了,再在useBlk里调用肯定就不对了
从打印出的__NSMallocBlock__ 我们也能知道useBlk对blk进行了copy操作

在Block中使用附有__strong修饰符的对象类型自动变量

首先,当block里不访问强指针,即:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void (^blk)(void) = ^{
            NSLog(@"Block");
        };
        blk();
        NSLog(@"%@", [blk class]);
        
    }
    return 0;
}

打印结果:
在这里插入图片描述

而当访问强指针时:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //将__block赋值给 __strong指针
        int a = 0;
        void (^blk)(void) = ^{
            NSLog(@"Block, a = %d", a);
        };
        blk();
        NSLog(@"%@", [blk class]);
        
    }
    return 0;
}

打印结果
在这里插入图片描述
而当我们在MRC环境下,同样的代码,打印结果:
在这里插入图片描述

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

block作为GCD API的方法参数时

最后

正好在ARC有效时能够同 __unsafe_unretained 修饰符一样来使用。由于ARC有效时和无效时 __block说明符的用途有很大的区别,因此在编写源代码时,必须知道该源代码时在ARC有效情况下编译还是在无效情况下编译

猜你喜欢

转载自blog.csdn.net/streamery/article/details/105353041