Block底层学习

Block底层本质

  • block就是Objective-C对闭包的实现,闭包就是一个没有名字的函数或者指向函数的指针。block本质上也是一个OC对象,它内部有isa指针;
  • block是封装了函数调用以及函数调用环境(参数)的OC对象;

我们来看一段代码

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int a = 10;
        void(^block)(int, int) = ^(int b,int c){
            NSLog(@"%d",a);
            NSLog(@"Hello World!");
        };       
        block(10,10);
    }
    return 0;
}

把上面这段代码转化为C++底层语言,转化后:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        int a = 10;
        //定义block变量
        void(*block)(int, int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
        //执行block内部的代码
        ((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 10, 10);
    }
    return 0;
}

在C++代码中,block代码块底层调用__main_block_impl_0。这句代码调用以下代码

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) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

我们发现,block的底层也是一个结构体。搜索struct __block_impl

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

block的第一个结构体成员是一个isa指针。这说明,block也是一个OC对象。
__main_block_desc_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_func_0函数内部封装了block执行逻辑的函数

static void __main_block_func_0(struct __main_block_impl_0 *__cself, int b, int c) {
  int a = __cself->a; // bound by copy
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_2n_ksb7n0n131n2y7v9xcf411fm0000gn_T_main_7cbb05_mi_0,a);
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_2n_ksb7n0n131n2y7v9xcf411fm0000gn_T_main_7cbb05_mi_1);
        }

struct __block_impl结构体内,有一个成员void *FuncPtr,

Block变量捕获

为了保证block内部能够正常访问外部的变量,block有个变量捕获机制。
先来看一段代码

#import <Foundation/Foundation.h>

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

执行上面这段代码,打印值是10,而不是20。之所以执行block(),结果是10,而不是20,这个就使用了变量捕获
我们来看下C++底层代码

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

在上面的代码中,编译时,在block内部已经捕获到a值。然后传递到__main_block_impl_0

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) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

在上面代码中,根据传递过来的值,赋值给NSLog(@"%d",a);。当a = 20;时,仅仅是改变int a = 10;的值。而block内部获取不到a的值。

变量捕获

  • int a = 10;默认是auto修饰,是值传递;auto,自动变量,auto修饰的变量,内存会自动消失。所以,block内部不会捕获会自动消失的内存。
  • 如果int a = 10;使用static修饰,则传递的是地址,block内部捕获到变量的地址,如果在外部修改变量的值,则根据地址找到变量存储的值。
  • 全局变量并不会被捕获到block内部。在block内部会直接访问全部变量。
  • self是一个局部变量,在block内部,也会捕获self

Block类型

因为block是一个对象,所以block也是有类型的。block有三种类型,可以通过class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型。

  • NSGlobalBlock
    存储在数据区域,没有访问auto。
  • NSStackBlock
    存储在栈区,会自动销毁,访问了auto。
  • NSMallocBlock
    存储在堆区,需要程序员销毁,NSStackBlock调用了copy(ARC环境下,block会自动调用copy,从栈上赋值到堆上,所以一般block类型是NSMallocBlock)。

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

  • block作为函数返回值时;

  • 将block赋值给__strong指针时;

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

  • block作为GCD API的方法参数时。

  • MRC下block属性的建议写法
    @property (copy,nonatomic) void (^block)(void);

  • ARC下block属性的建议写法
    @property (copy,nonatomic) void (^block)(void);
    @property (strong,nonatomic) void (^block)(void);

对象类型的auto变量以及Block的内存管理

类似于局部变量,有auto修饰的对象在block内部,也会存在block类型。来看一段代码

#import <Foundation/Foundation.h>

@interface Person : NSObject
@property (nonatomic,  assign) int age;
@end

#import "Person.h"

@implementation Person
- (void)dealloc{
    NSLog(@"delloc--Person");
}
@end

main.m
#import <Foundation/Foundation.h>
#import "Person.h"
typedef void(^HYBlock) (void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        HYBlock myBlock;
        {
            Person *p = [[Person alloc] init];
            p.age = 10;
            myBlock = ^{
                NSLog(@"%d",p.age);
            };
            myBlock();
        }
    }
    return 0;
}
  • 当block内部访问了对象类型的auto变量时
    如果block是在栈上,将不会对auto变量产生强引用。

  • 当block被拷贝到堆上
    ✔️会调用block内部的copy函数;
    ✔️copy函数内部会调用_Block_object_assign

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

✔️_Block_object_assign函数会根据auto变量的修饰符(__strong、__weak、__unsafe unretained)做出相应的操作,形成强引用(retain)或者弱引用。

  • 如果block从堆上移除
    ✔️会调用blokc内部的dispose函数
    ✔️dispose函数内部会调用_Block_object_dispose函数
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

✔️_Block_object_dispose函数会自动释放引用的auto变量。

Block修饰符

我们知道不能再block内部修改外部变量的值,我们来看下原因:

#import <Foundation/Foundation.h>
typedef void(^HBlock) (void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int a = 10;
        HBlock  block= ^{
            NSLog(@"%d",a);
        };
        block();
    }
    return 0;
}

转化为C++代码

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

在上面的代码中,定义了int a = 10;。而输出这个变量值是在下面这个函数中

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_2n_ksb7n0n131n2y7v9xcf411fm0000gn_T_main_5b923f_mi_0,a);
        }

因为变量a不是全局变量,只是局部变量,所以不能在另外一个函数,修改变量值。

  • 如果int a = 10;使用static修饰,可以在block内部修改变量的值。
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) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

在上面的代码中,int *a;传递的是变量的地址值,在block内部,先找到变量的地址值,直接修改变量a的值,而不是直接修改变量值。

  • 如果int a = 10;是全局变量,则在当前文件的函数中,都可以修改变量值。
  • 使用static修饰变量,或者使用全局变量,则这个变量一直在内存中。如果使用__block修改,也可以在block内部修改变量值,并且,变量会自动释放,不会一直存在内存中。
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_a_0 *a; // by ref
...
};

从上面的代码可以看出,使用__block修饰变量,在__main_block_impl_0内部,变量a为__Block_byref_a_0 *a; // by ref。而__Block_byref_a_0是一个对象(内部有isa指针)。在这个结构体内部,有成员变量,存储变量值。

struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};

当修改变量值时,利用__Block_byref_a_0指针先找到结构体,通过变量名找到__forwarding,在通过__forwarding找到变量,来修改变量值。

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_a_0 *a = __cself->a; // bound by ref

            (a->__forwarding->a) = 20;
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_2n_ksb7n0n131n2y7v9xcf411fm0000gn_T_main_b8c75c_mi_0,(a->__forwarding->a));
        }
  • 注意
    ✔️使用__block修饰int a,则在block内部a成为对象。
    ✔️创建NSMutableArray *array = [NSMutableArray array];,在block内部使用[array addObject:@123]是使用这个指针,而不是改变array的值。

block循环引用

循环引用是指两个或以上对象互相强引用,导致所有对象无法释放的现象。这是内存泄露的一种情况。

#import <Foundation/Foundation.h>

typedef void(^HYBlock)(void);
@interface Person : NSObject
@property (nonatomic, assign) int age;
@property (nonatomic, copy) HYBlock block;
@end

#import "Person.h"

@implementation Person
- (void)dealloc{
    NSLog(@"%s",__func__);
}
@end


#import <Foundation/Foundation.h>
#import "Person.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        person.age = 20;
        person.block = ^{
            NSLog(@"%d", person.age);
        };
        person.block();
    }
    return 0;
}

在上面的代码中,当执行person.block();时,Person对象并没有释放,产生循环引用。
我们来看下,产生循环引用的原因,首先转化为C++代码

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  Person *__strong person;
...
};

在ARC环境下,HYBlock被拷贝到堆上,当内部调用person时,则在函数__main_block_impl_0内部,Person对象生成Person *__strong person;也即是强引用这个对象。Person对象强引用HYBlockHYBlock又强引用Person对象,则HYBlock不释放,Person对象也不会释放。

解决循环引用

  • 使用__weak,__unsafe_unretained解决;
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  Person *__weak weakPerson;
...
};

使用weak修饰对象,则在函数__main_block_impl_0内部,不在强引用Person对象。__unsafe_unretained同理,也不在强引用Person对象。

  • 使用__block解决(必须调用block);
#import <Foundation/Foundation.h>
#import "Person.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
       __block Person *person = [[Person alloc] init];
        person.age = 20;
        person.block = ^{
            NSLog(@"%d", person.age);
            person = nil;
        };
        person.block();
    }
    return 0;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容