【OC语法】block捕获变量

目录
一、block捕获普通变量
  1、block会捕获局部变量,普通则值传递,静态则指针传递
  2、block不会捕获全局变量
二、block捕获指针变量
  1、block会捕获局部对象类型的指针变量,强指针(__strong)则持有对象、弱指针(__weak)则不持有对象
  2、block会捕获self,强指针(__strong)则持有对象、弱指针(__weak)则不持有对象

这个一定要记住,拿着这个分析面试题很稳:block捕获变量是指,如果block的执行体里使用了外界的局部变量,那么block内部就会生成一个与局部变量同名的成员变量,并且局部变量还会把值传递给这个成员变量,当然可能是值传递——使用了外界的普通局部变量时,也有可能是指针传递——使用了外界的静态局部变量时。那么接下来block执行体里使用的这个变量就不是外界的局部变量了,而是block体内的成员变量。而如果block的执行体里使用了外界的全局变量,那block是不会捕获它们的,会直接使用它们。所以要想知道一个变量会不会被block捕获,你只需要搞清变量是个局部变量还是个全局变量就行了,别去管block是什么类型的block。

那为什么系统要给block添加捕获变量机制呢?又为什么只捕获局部变量而不捕获全局变量呢?实际开发中,我们难免要在block的执行体里使用外界的局部变量,我们知道block其实是把block的参数、返回值、执行体封装成一个函数,而这个函数在调用时却仅仅接收了block本身作为参数,

来自上一篇

// 创建一个block
void (*block)(void) = &__block_impl_0(
                                      __block_func_0,// 把函数的地址传进去
                                      &__block_desc_0_DATA // 把结构体的地址传进去
                                      );

// 调用block
block->impl.FuncPtr(block);

并没有接收额外的参数,所以一个函数怎么可能无缘无故就访问到函数外部的变量呢。于是系统就为block设计了捕获变量机制,把局部变量捕获到block体内,以便函数仅仅接收block本身作为参数就能正常使用外界的局部变量。而全局变量存储在全局区,block能直接访问到,所以不需要捕获。


一、block捕获普通变量


  • block会捕获局部变量
  • block不会捕获全局变量

1、block会捕获局部变量,普通则值传递,静态则指针传递

  • block会捕获普通局部变量,且局部变量与成员变量之间是值传递
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        // 普通局部变量
        int age = 25;
        
        void (^block)(void) = ^{ // ARC下这是个堆block,因为block赋值给强指针,系统会自动复制一份到堆区
            
            NSLog(@"%d", age); // 25
        };
        
        age = 26;
        
        block();
    }
    return 0;
}

按正常逻辑来说,上面的代码应该打印“26”,因为在block调用之前age被改成“26”了,但实际上却打印“25”,为什么?我们看看这段代码的C/C++实现(伪代码)。

// block对应的结构体
struct __block_impl_0 {
    struct __block_impl impl;
    struct __block_desc_0* Desc;
    
    int age; // 多了一个成员变量
    
    // : age(_age),C++的语法,意思是直接把_age参数的值赋值给age成员变量,相当于下面又多了一句赋值语句
    __block_impl_0(void *fp, struct __block_desc_0 *desc, int _age, int flags=0) : age(_age) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
        
//      age = _age;// 相当于这样
    }
};

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
        
        int age = 25;
        
        // 创建block
        void (*block)(void) = &__block_impl_0(
                                              __block_func_0,
                                              &__block_desc_0_DATA,
                                              age // 多了一个参数
                                              );
        
        age = 26;
        
        // 调用block
        block->impl.FuncPtr)(block);
    }
    return 0;
}

void __block_func_0(struct __block_impl_0 *__cself) {
    
    int age = __cself->age; // 获取age成员变量的值
    
    NSLog(age);
}

我们看到block内部多了一个成员变量age

也看到在创建block的时候,block构造函数多了一个age参数,直接把变量的值“25”给传进去了,并赋值给block的成员变量age

然后外界把变量age的值改为“26”。

调用block时,系统读取的是block内部那个成员变量的值,所以打印了“25”。

  • block会捕获静态局部变量,但局部变量与成员变量之间是指针传递
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        // 静态局部变量
        static int height = 25;
        
        void (^block)(void) = ^{ // 这是个全局block
            
            NSLog(@"%d", height); // 26
        };
        
        height = 26;
        
        block();
    }
    return 0;
}

打印“25”红还是“26”😄?直接看C/C++实现吧(伪代码)。

// block对应的结构体
struct __block_impl_0 {
    struct __block_impl impl;
    struct __block_desc_0* Desc;
    
    int *height; // 多了一个成员变量,注意是个指针
    
    // : height(_height),C++的语法,意思是直接把_height参数的值赋值给height成员变量,相当于下面又多了一句赋值语句
    __block_impl_0(void *fp, struct __block_desc_0 *desc, int *_height, int flags=0) : height(_height) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
        
//      height = _height; // 相当于这样
    }
};

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
        
        static int height = 25;
        
        // 创建block
        void (*block)(void) = &__block_impl_0(
                                              __block_func_0,
                                              &__block_desc_0_DATA,
                                              &height // 多了一个参数,注意是个地址
                                              );
        
        height = 26;
        
        // 调用block
        block->impl.FuncPtr)(block);
    }
    return 0;
}

void __block_func_0(struct __block_impl_0 *__cself) {
    
    int *height = __cself->height; // 获取height成员变量的值
    
    NSLog(*height);
}

没问题,我们看到block内部多了一个成员变量height,但要注意它是个指针类型

也看到在创建block的时候,block构造函数多了一个height参数,但这里不是直接把变量的值传进去,而是把变量的地址给传进去了,并赋值给block的成员变量height

然后外界把变量height的值改为“26”。

调用block时,系统读取的是block内部那个成员变量的值没问题,但因为它是个指针,指向外界的那个变量,所以打印了“26”。

1、再加深一下印象:block会捕获局部变量

  • block会捕获普通局部变量,局部变量与成员变量之间是值传递
  • block会捕获静态局部变量,局部变量与成员变量之间是指针传递

2、那系统为什么要这样设计呢?同样都是局部变量,为什么普通局部变量是值传递,而静态局部变量是指针传递?

void (^block)(void);
void test() {
    
    // 普通局部变量
    int age = 25;
    // 静态局部变量
    static int height = 25;
    
    block = ^{ // ARC下这是个堆block,因为block赋值给强指针,系统会自动复制一份到堆区
        
        NSLog(@"%d %d", age, height);
    };
        
    age = 26;
    height = 26;
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        test();
        block();
    }
    return 0;
}

一看上面这段代码,你就明白了。

  • test函数执行完,也就是说出了test函数的作用域,
  • 普通局部变量age就释放了,也就是说它对应的那块栈内存就释放了,有可能被别人征用,里面填充别的数据,那内存释放后你再去访问这块内存,访问到不一定是原来的数据,所以普通局部变量采用指针传递根本没有意义,因为它对应的那块内存说不定什么时候(即有可能在我们使用它之前)就释放掉了,所以还不如趁早把局部变量的值给存下来。
  • 而静态局部变量就不一样了,出了test函数的作用域,height变量虽然也被释放掉了,但这仅仅是表明在代码层我们无法再继续通过height变量去访问它对应的那块内存而已,并不代表那块内存也释放了,因为这块内存是静态全局区的一块内存,所以我们只要用一个指针变量来记住这块内存的地址,那height变量释放后,我们依旧可以通过自己的指针变量去访问那块内存。

2、block不会捕获全局变量

// 普通全局变量
int age = 25;
// 静态全局变量
static int height = 25;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        void (^block)(void) = ^{ // 这是个全局block
            
            NSLog(@"%d %d", age, height); // 26, 26
        };
        
        age = 26;
        height = 26;
        
        block();
    }
    return 0;
}

C/C++实现(伪代码)。

// block对应的结构体
struct __block_impl_0 {
    struct __block_impl impl;
    struct __block_desc_0* Desc;

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

int age = 25;
static int height = 25;

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
        
        // 创建block
        void (*block)(void) = &__block_impl_0(
                                              __block_func_0,
                                              &__block_desc_0_DATA,
                                              );
        
        age = 26;
        height = 26;
        
        // 调用block
        block->impl.FuncPtr)(block);
    }
    return 0;
}

void __block_func_0(struct __block_impl_0 *__cself) {
    NSLog(age, height); // 直接访问全局变量
}

我们看到block内部并不会多出成员变量,而且调用block时,是直接通过全局变量访问对应内存里的数据。


二、block捕获指针变量


1、block会捕获局部对象类型的指针变量,强指针(__strong)则持有对象、弱指针(__weak)则不持有对象

block会捕获局部对象类型的指针变量,而且捕获后如果发现它是个强指针(即__strong修饰),block还会强引用(即持有)它指向的对象,如果发现它是个弱指针(即__weak修饰),block则会弱引用(即不持有)它指向的对象。(如果更严谨一点的话,栈block永远只是弱引用对象,只不过因为我们是ARC下,用的基本上都是堆block,所以就故意忽略掉了这一点,免得大家混淆)

创建一个Person类,简单实现一下,来验证上面这条结论。

// INEPerson.h
@interface INEPerson : NSObject

@property (nonatomic, assign) NSInteger age;

@end


// INEPerson.m
@implementation INEPerson

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

@end
  • block捕获强指针
// main.m
typedef void (^INEBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool
    { // 作用域2起点
        
        INEBlock block;
        
        { // 作用域1起点
            
            INEPerson *person;
            
            person = [[INEPerson alloc] init];
            person.age = 25;
            
            block = ^{
                
                NSLog(@"%ld", person.age);
            };
        } // 作用域1终点
        
        NSLog(@"11");
    } // 作用域2终点
    
    return 0;
}

控制台打印:

11
INEPerson dealloc

block的C/C++实现(伪代码):

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
    
  INEPerson *__strong person; // 确实捕获了,是个强指针
    
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, INEPerson *__strong _person, int flags=0) : person(_person) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

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

我们知道person指针变量是个局部变量,所以它肯定会被block捕获,而且person指针变量默认是个强指针,所以block内部生成的同名成员变量也是一个强指针,于是block就通过它内部的那个强指针强引用了person指针变量指向的Person对象。

所以出了作用域1后,虽然person指针变量销毁了,但此时block还没销毁,它还强引用着Person对象,所以这个时候就不会走Person对象dealloc方法,而是继续往下走,打印完“11”、出了作用域2后,block销毁,同时也就释放了对Person对象的强引用,所以此时才走Person对象的dealloc方法打印了“INEPerson dealloc”。

  • block捕获弱指针
// main.m
typedef void (^INEBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool
    { // 作用域2起点
        
        INEBlock block;
        
        { // 作用域1起点
            
            __weak INEPerson *person;
            
            person = [[INEPerson alloc] init];
            person.age = 25;
            
            block = ^{
                
                NSLog(@"%ld", person.age);
            };
        } // 作用域1终点
        
        NSLog(@"11");
    } // 作用域2终点
    
    return 0;
}

控制台打印:

INEPerson dealloc
11

block的C/C++实现(伪代码):

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
    
  INEPerson *__weak person; // 确实捕获了,是个弱指针
    
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, INEPerson *__weak _person, int flags=0) : person(_person) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

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

block确实会捕获person指针变量,但因为它是个弱指针,所以block就通过它内部的那个弱指针弱引用了person指针变量指向的Person对象。

所以出了作用域1后,person指针变量销毁,Person对象身上就没有强引用了,所以这个时候就走Person对象的dealloc方法打印了“INEPerson dealloc”,然后继续往下走,打印完“11”、出了作用域2后,block销毁。

此时,你可能会问:block捕获指针倒是没问题,但你凭什么说捕获到强指针就持有对象,捕获到弱指针就不持有对象,上面虽然通过代码验证了,但这底层是怎么实现的?

从上面的代码中,我们可以看到只要是block捕获了对象类型的指针变量,那它结构体内第二个成员变量里就会多出两个函数,copy函数和dispose函数,这两个函数是专门用来负责对象的内存管理的,这也是为什么block捕获基本数据类型的变量时,它内部不会生成这两个函数。

持有不持有主要靠的是block内部的copy函数和dispose函数,当我们把block从栈区copy到堆区时,系统就会自动调用block内部的copy函数,该函数内部会根据捕获到的是个强指针还是弱指针来决定要不要把对象的引用计数加1,而当block销毁的时候,系统又会自动调用内部的dispose函数,来解除对对象的引用。

2、block会捕获self(指针变量),强指针(__strong)则持有对象、弱指针(__weak)则不持有对象

创建一个Person类,简单实现一下,来验证上面这条结论。

// INEPerson.m
@implementation INEPerson

- (void)test {
    
    void (^block)(void) = ^{
        
        NSLog(@"%@", self);
    };
    block();
}

@end

block的C/C++实现(伪代码)。

struct __INEPerson__test_block_impl_0 {
  struct __block_impl impl;
  struct __INEPerson__test_block_desc_0* Desc;
    
  INEPerson *__strong self;
    
  __INEPerson__test_block_impl_0(void *fp, struct __INEPerson__test_block_desc_0 *desc, INEPerson *const __strong _self, int flags=0) : self(_self) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

可见self被block捕获了,那为什么会捕获self呢?这是因为所有的OC方法其实都有两个默认的参数:self指针变量和_cmd selecotr,即该方法调用者和该方法的selector,而方法的参数也是一种局部变量,所以self会被block捕获。上面的test方法其实就是这样(伪代码):

- (void)test(id self, SEL _cmd) {
    
    void (^block)(void) = ^{
        
        NSLog(@"%@", self);
    };
    block();
}

self指针默认也是个强指针,所以block会持有它指向的对象,而如果把self指针变成弱指针,block就不会持有它指向的对象了。

// INEPerson.m
@implementation INEPerson

- (void)test {
    
    __weak INEPerson *weakSelf = self;
    void (^block)(void) = ^{
        
        NSLog(@"%@", weakSelf);
    };
    block();
}

@end

block的C/C++实现(伪代码)。

struct __INEPerson__test_block_impl_0 {
  struct __block_impl impl;
  struct __INEPerson__test_block_desc_0* Desc;
    
  INEPerson *__weak weakSelf;
    
  __INEPerson__test_block_impl_0(void *fp, struct __INEPerson__test_block_desc_0 *desc, INEPerson *__weak _weakSelf, int flags=0) : weakSelf(_weakSelf) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 227,428评论 6 531
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 98,024评论 3 413
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 175,285评论 0 373
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 62,548评论 1 307
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 71,328评论 6 404
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 54,878评论 1 321
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 42,971评论 3 439
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 42,098评论 0 286
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 48,616评论 1 331
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 40,554评论 3 354
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 42,725评论 1 369
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 38,243评论 5 355
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 43,971评论 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 34,361评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 35,613评论 1 280
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 51,339评论 3 390
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 47,695评论 2 370

推荐阅读更多精彩内容