关于Block 你知道多少?

前言
  • 本文只是作为自己对学习的一个总结,如果有错误的地方欢迎各位大神提出
  • 文笔不行,可能会写的有点乱,请见谅!
先看几个面试题
  • block内部会捕获到外部局部变量吗?那static修饰的呢?全局变量呢?
  • block的原理是怎样的?本质是什么?
  • __block的使用注意点?
开始block
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        int a = 10;
        void(^block)(void) = ^{
            NSLog(@"%d",a);
        };
        block();
    }
    return 0;
}

定义一个简单的block,然后通过clang指令编译成c++代码

clang -rewrite-objc main.cpp

生成了大概10W行C++代码,我们直接找重点

struct __block_impl {
  void *isa;//结构体的地址,也是block的地址
  int Flags;
  int Reserved;
  void *FuncPtr;//保存block代码块的地址
};
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;
//构造函数,实例化block结构体,传入fp(block代码块函数)到impl这个结构体中
  __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;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_np_rq5pw07x2615y7592pjt2__c0000gn_T_main_a0acf2_mi_0,a);
}
//Block_size:通过sizeof(struct __main_block_impl_0)获取block的大小
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 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));

        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a)); 定义block &__main_block_impl_0()把结构体的地址赋值给block变量
__main_block_func_0:存储block代码块的执行函数
__main_block_desc_0_DATA:存放block信息的结构体(内存大小)
a : block内部访问的外部变量

其实block就是一个__main_block_impl_0这个结构体,在定义的时候传入block代码块的执行函数,和在block内部访问的局部变量等信息,并在构造的时候把执行函数保存到__block_impl里面的FuncPtr
在执行block的时候,通过block对象找到FuncPtr进行调用

接下来修改一些代码

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

控制台打印结果: 10;
为什么会这样呢?我明明是修改了变量a 的值;
因为block内部只是对变量a的值进行了捕获,在__main_block_func_0
int a = __cself->a; //bound by copy 只是把原来存储在block结构体中的值赋值给了一个新的变量,所以在block外部修改变量的值,block内部的变量值不受影响

修改代码

static int static_Global = 10;

int global = 20;

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

分别定义了全局变量global ,全局静态变量static_Global, 局部变量 autoA , 局部静态变量a,接下来看看编译后的代码

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *a;
  int autoA;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_a, int _autoA, int flags=0) : a(_a), autoA(_autoA) {
    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; // bound by copy
  int autoA = __cself->autoA; // bound by copy

            (*a) = 30;
            static_Global = 20;
            global = 30;
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_np_rq5pw07x2615y7592pjt2__c0000gn_T_main_4217e2_mi_0,autoA,(*a),static_Global,global);
        }

从上面代码中可以看出,block内部并没有把全局变量和全局静态变量进行捕获,因为是全局的,作用域很广,block内部可以获取到直接进行修改,而局部静态变量和局部变量是进行了捕获,静态变量捕获的是地址,而局部变量捕获的是值,都作为了__main_block_impl_0这个结构提的成员变量
小节一下: block会对在其内部用到的局部变量和局部静态变量进行捕获,局部变量捕获的是值,静态局部变量捕获的是地址,所以可以在其内部修改静态局部变量和全局变量,却不能对局部变量进行修改,并且只对其内部使用到的局部变量或者局部静态变量进行捕获

block的类型

在OC中一般情况下就分为以下三种block
__NSMallocBlock__,__NSGlobalBlock__,__NSStackBlock__

  • NSGlobalBlock : 没有访问外部的局部变量(访问全局变量或者全局静态变量也是如此)
  • NSStackBlock : 内部访问了外部局部变量,系统自动管理内存
  • NSMallocBlock : NSStackBlock类型的block调用了copy操作,或者有强指针指向,需要程序员自己管理内存

在这里解答一下:block的本质是什么?

NSLog(@"%@",[block class]); 
NSLog(@"%@",[[block class]superclass]);
NSLog(@"%@",[[[block class]superclass]superclass]);
NSLog(@"%@",[[[[block class]superclass]superclass]superclass]);
控制台输出

我们发现block可以调用class方法,而且还是NSObject的子类,所以说block的本质就是一个对象啊

__blcok

image.png

我们试着对block内部的局部变量进行修改,直接就会编译报错"Variable is not assignable (missing __block type specifier)"
如果我们换成static int age = 10;这样就可以在block内部进行修改了,因为用static修饰的变量传递的是age的地址

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进行修饰

struct __Block_byref_age_0 {
  void *__isa;
__Block_byref_age_0 *__forwarding;
 int __flags;
 int __size;
 int age;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_age_0 *age; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
    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_age_0 *age = __cself->age; // bound by ref

            (age->__forwarding->age) = 20;
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_np_rq5pw07x2615y7592pjt2__c0000gn_T_main_82c4e5_mi_0,(age->__forwarding->age));
        }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->age, (void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        __attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};
        MyBlock block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

从上面的代码中我们看到用__block修饰的局部变量生成了一个结构体__Block_byref_age_0,

image.png

__block int age = 10;就编译成了上面的样子,并把值都传进了结构体中;
在定义block的时候,把上面定义的结构体指针传递给了__main_block_impl_0,并进行初始化;
调用block的时候,通过block->FuncPtr((__block_impl *)block),找到block的执行函数,并把block的结构体指针传递过去;
block的执行函数__main_block_func_0,根据__cself结构体指针找到结构体中的age(__Block_byref_age_0);
通过age(__Block_byref_age_0)结构体指针找到__Block_byref_age_0结构体,再找到__forwarding,再找到其中的age成员,对他进行修改
我们通过下面的代码进行验证__forwarding指向的到底是哪

typedef void(^MyBlock)(void);

struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};
struct __Block_byref_age_0 {
    void *__isa;
    struct __Block_byref_age_0 *__forwarding;
    int __flags;
    int __size;
    int age;
};

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    struct __Block_byref_age_0 *age; // by ref
};
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //运行在ARC环境下
        __block int age = 10;
        NSLog(@"%p",&age);
        MyBlock block = ^{
            age = 20;
            NSLog(@"%p",&age);
        };
        struct __main_block_impl_0 *newBlock = (__bridge struct __main_block_impl_0 *)block;
        NSLog(@"%p",&age);
        block();
        
    }
    return 0;
}

控制台打印


image.png

我们可以看到第一次打印的和后面打印的地址是不一样的,因为在ARC下block被强指针引用或者赋值给__strong类型,都会被copy到堆上,__forwarding指针也就指向了被copy到堆上的新地址,所以会出现上面的打印地址不同的情况.
newBlock中的age地址是0x1007519a0,可以根据这个去计算一下

struct __Block_byref_age_0 {
    void *__isa; // 8 0x1007519a0
    struct __Block_byref_age_0 *__forwarding;//8 0x1007519a0+8=0x1007519a8
    int __flags; //4 0x1007519a8+8=0x1007519b0
    int __size; //4 0x1007519b0+4=0x1007519b4
    int age;// 4 0x1007519b4+4=0x1007519b8
};

从上面可以看出在block内部和最下面的&age取的是__Block_byref_age_0这个结构体里面的int age的地址;
但是上面的代码跑在MRC的环境下控制台打印就不一样了,每次打印的地址都是一样的,说明__forwarding一直指向的是自己,一直在栈上,如果调用了copy操作,还会出现上面的情况

image.png

从栈copy到堆的变化
在栈上

那什么情况下才会把栈上的block拷贝到堆上呢?

  • 栈上的block手动调用copy函数
  • block作为函数的返回值
  • block被强引用,或者赋值给__strong修饰的类型

block 的copy和dispose

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

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->objc, 3/*BLOCK_FIELD_IS_OBJECT*/);}
static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};

_Block_object_assign和_Block_object_dispose就对应着retain和release方法;
看下面的代码:

// 运行在MRC
auto NSObject *objc = [[NSObject alloc]init];
NSLog(@"%lu",(unsigned long)objc.retainCount);
void(^block)(void) = ^{
            
      NSLog(@"%lu",(unsigned long)objc.retainCount);
};
     NSLog(@"%lu",(unsigned long)objc.retainCount);
     block();

如果block在栈上不会对objc产生强引用,打印结果一直为1
如果block调用了copy就会被copy到堆上,就会调用block内部的__main_block_copy_0函数,__main_block_copy_0函数内部会调用
_Block_object_assign函数,根据objc变量的修饰符(__strong__weak__unsafe_unretained),做出相应的操作,形成强指针或者弱指针
如果block要销毁,会调用block内部的dispose函数,函数内部会调用
__main_block_dispose_0函数,调用_Block_object_dispose进行release操作;
注意哦如果上面的对象使用了__block进行修饰,引用计数是不会+1的

NSObject *objc = [[NSObject alloc]init];
       NSLog(@"%@",[objc valueForKey:@"retainCount"]);
        void(^block)(void) = [^{

            NSLog(@"%@",[objc valueForKey:@"retainCount"]);

        } copy];
       NSLog(@"%@",[objc valueForKey:@"retainCount"]);

        block();

MRC下:打印结果 1,2,2
ARC下: 打印结果 1,3,3
添加上__block以后;
MRC下:打印结果 1,1,1
ARC下: 打印结果 1,1,1
从上面的结果可以看出:

  • __block 不会让对象的引用计数发生改变
  • ARC和MRC下copy函数起到的作用是不一样的,具体什么原因,有知道的朋友可以留言,谢谢

block的循环引用

@interface Person : NSObject

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

@property (nonatomic, assign) int age;
@end
@implementation Person

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

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

在出了作用域以后,Person对象得到释放
修改代码

p.block = ^{
            NSLog(@"%d",p.age);
        };

Person对象没有得到销毁,因为循环引用


image.png

通过上图可以看出,存在互相引用,所以内存就得不到释放

int a = 10;
        Person *p = [[Person alloc]init];
        p.age = a;
        __weak Person *person = p;
        p.block = ^{
            NSLog(@"%d",person.age);
        };
        p.block();

通过__weak,把捕获到block内部的对象指针改变给弱指针就ok了

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  Person *__weak person;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__weak _person, int flags=0) : person(_person) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

以上就是我学习block的过程,如果有不正确的地方欢迎指出和交流

接下来解决一下上面的面试题

  • block内部会捕获到外部局部变量吗?那static修饰的呢?全局变量呢?
    block内部会捕获外部的局部变量和static修饰的局部变量,不会对全局变量和全局静态变量进行捕获
  • block的原理是怎样的?本质是什么?
    block主要就是把执行block需要用的数据进行封装(比如:外部变量,block的执行函数),其实block的本质就是一个OC对象
  • __block的使用注意点?
    使用block最主要的就是防止循环引用,可以使用__weak进行操作
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容