【OC语法】block的本质

目录
一、block是什么
二、block的本质
三、block的类型


一、block是什么


简单地说,block跟Swift和Dart里的闭包(匿名函数)差不多,我们都知道在Swift和Dart里函数是一等公民,所以函数可以赋值给变量、也可以作为函数的参数、也可以作为函数的返回值,也就是说一个函数可以传来传去。但是在OC里函数不是一等公民,它不能传来传去,不过OC又提供了一种特殊的对象那就是block,它就像一个匿名函数一样可以可以赋值给变量、也可以作为函数的参数、也可以作为函数的返回值,但是要注意它本质上不是一个函数而是一个OC对象。

本质地说,block其实也是一个OC对象,所以它的内存里存储的是什么和普通OC对象差不多,只不过普通OC的对象用来包装一些普通的数据(如字符串数据、数组数据、字典数据等),而block则用来包装一段代码以及这段代码的调用环境。所谓包装一段代码,是指block内部会把block的参数、返回值、执行体封装成一个函数,并且存储该函数的内存地址;所谓包装这段代码的调用环境,是指block内部会捕获变量,并且存储这些捕获的变量。(这段话会在下一小节详细证明)

  • block的声明

block作为属性时,这样声明:

// block的类型为:int (^)(int a, int b)
// block的名字为:block
@property (nonatomic, copy) int (^block)(int a, int b);

block作为方法的参数时,这样声明:

// block的类型为:void (^)(NSData *data, NSError *error)
// block的名字为:completionHandler
- (void)fecthDataWithCompletionHandler:(void (^)(NSData *data, NSError *error))completionHandler;

如果项目中使用了大量相同类型的block,那为了使代码更简洁,我们可以先typedef一下block的类型,然后再声明。则上面两例可以写成这样:

typedef int (^Block)(int a, int b);

@property (nonatomic, copy) Block block;
typedef void (^Block)(NSData *data, NSError *error);

- (void)fecthDataWithCompletionHandler:(Block)completionHandler;
  • block的实现

箭头打头就代表block的实现,如果block没有返回值,可省略returnType,如果block没有参数,可省略params。

^returnType(params) {
    
    // block的执行体
};

不过通常我们都会把block的实现用一个变量记录下来,以便将来调用,就像函数那样。

returnType (^blockName)(params) = ^returnType(params) {
    
    // block的执行体
};
  • block的调用

像C语言那样加小括号就代表block的调用,如果block没有返回值,则不接收返回值,如果block没有参数,可省略params。

returnType v = blockName(params);
  • block代码的执行顺序

block代码的执行顺序永远都是:block的声明 --> block的实现 --> block的调用 --> 最后返回去真正去执行block的实现代码。举例如下:

#import "ViewController.h"

@interface ViewController ()

// 第一步:block的声明
@property (nonatomic, copy) int (^block)(int a, int b);

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 第二步:block的实现
    self.block = ^int(int a, int b) {
        
        // 第四步:最后返回去真正去执行block的实现代码
        return a + b;
    };
    
    // 第三步:block的调用
    int num = self.block(1, 2);
    NSLog(@"%d", num);
}

@end


二、block的本质


我们简单创建一个block,并调用它。

// 创建一个block
void (^block)(void) = ^{
    
    NSLog(@"11");
};

// 调用block
block();

接着用clang编译器把这段OC代码转换成C/C++代码,来窥探一下block的本质。(伪代码)

首先编译器会把block转换成它的本质——即一个C++的__block_impl_0结构体,该结构体内部有两个成员变量,第一个成员变量内部有一个isa指针指向block所属的类,这也证明我们上面所说的“block其实也是一个OC对象”,还有一个FuncPtr指针指向该block对应的函数;第二个成员变量内部则存储着该block的一些描述信息,如该block的实际大小、copy函数、dispose函数等。此外,该结构体内部还有一个block构造函数,它用来创建并初始化一个block——即一个__block_impl_0类型的结构体。当然我们也不能忘了,block结构体内部还可以有更多的成员变量,它们就是block捕获并存储的变量,也就是我们上面所说的“包装代码的调用环境”。

// block的本质,是一个C++结构体
struct __block_impl_0 {
    // 第一个成员变量
    struct __block_impl impl;
    // 第二个成员变量(这个不用太关心)
    struct __block_desc_0* Desc;
    
    /**
     * block的构造函数
     *
     * @param fp block对应函数的内存地址
     * @param desc block描述信息结构体的内存地址
     *
     * @return 返回一个当前类型的结构体————即返回一个__block_impl_0类型的结构体
     */
    __block_impl_0(void *fp, struct __block_desc_0 *desc, int flags=0) {
        impl.isa = &_NSConcreteStackBlock; // block所属的类
        impl.Flags = flags;
        impl.FuncPtr = fp; // 把block对应函数的内存地址存储在block内部
        Desc = desc; // 把block描述信息结构体的内存地址存储在block内部
    }
};

// block实现信息结构体
struct __block_impl {
    void *isa; // 指向block所属的类
    int Flags;
    int Reserved;
    void *FuncPtr; // 指向block对应的函数
};

// block描述信息结构体
struct __block_desc_0 {
  size_t reserved; // 预留
  size_t Block_size; // block的实际大小
};

然后编译器会把block的参数、返回值、执行体封装成一个__block_func_0函数,也就是我们上面所说的“包装一段代码”,将来创建block的时候,block内部的FuncPtr指针就会指向这个函数,并把block的描述信息封装进一个__block_desc_0结构体(这个不用太关心)。

// block对应的函数
void __block_func_0(struct __block_impl_0 *__cself) {
    
    NSLog("11");
}

// block对应的描述信息
struct __block_desc_0 {
  size_t reserved;
  size_t Block_size; // block的实际大小
} __block_desc_0_DATA = {
    0,
    sizeof(struct __block_impl_0) // 计算block的实际大小
};

知道了这些,上面那几行OC代码其实就对应着下面这几行C/C++代码,创建一个block的本质就是调用block的构造方法创建一个结构体,构造方法会接收block对应的函数的地址和block对应的描述信息的地址作为参数,接收后会存储在它内部,然后把这个结构体的内存地址赋值给*block这里的block指针变量;而调用block的本质就是找到block内部FuncPtr指针指向的函数来调用。

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

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


三、block的类型


1、全局block、栈block、堆block

(注意:这一小节的示例代码都是在MRC下的)

block有三种类型:全局block(__NSGlobalBlock__)、栈block(__NSStackBlock__)、堆block(__NSMallocBlock__),它们都继承自NSBlock,并最终继承自NSObject。那什么是全局block?什么是栈block?什么又是堆block?全局block是指存储在全局区的block,栈block是指存储在栈区的block,堆block是指存储在堆区的block,所以说看一个block是什么类型,不是看它在代码的什么位置定义的,而是看它存储在哪块内存分区中——即系统把它存储在哪块内存分区中了。这在代码中有什么体现呢?也就是说我们如何通过代码一眼就能知道这个block是什么类型的呢?

  • 全局block

没有访问外界普通局部变量的block就是全局block,系统会把这样的block放在全局区。

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

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        // 静态局部变量
        static int age = 25;
        
        void (^block)(void) = ^{
            // 访问外界的变量
            NSLog(@"%d", age);
        };
        
        NSLog(@"%@", [block class]); // __NSGlobalBlock__
        NSLog(@"%@", [[block class] superclass]); // __NSGlobalBlock
        NSLog(@"%@", [[[block class] superclass] superclass]); // NSBlock
        NSLog(@"%@", [[[[block class] superclass] superclass] superclass]); // NSObject
    }
    return 0;
}
  • 栈block

访问了外界普通局部变量的block就是栈block,系统会把这样的block放在栈区,可见栈block和全局block是完全对立的。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        // 普通局部变量
        int age = 25;
        
        void (^block)(void) = ^{
            // 访问外界的变量
            NSLog(@"%d", age);
        };
        
        NSLog(@"%@", [block class]); // __NSStackBlock__
        NSLog(@"%@", [[block class] superclass]); // __NSStackBlock
        NSLog(@"%@", [[[block class] superclass] superclass]); // NSBlock
        NSLog(@"%@", [[[[block class] superclass] superclass] superclass]); // NSObject
    }
    return 0;
}
  • 堆block

对栈block执行一下copy操作,copy方法返回的就是一个堆block,所以说堆block就是把栈block copy了一份到堆区。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        // 普通局部变量
        int age = 25;
        
        void (^block)(void) = [^{
            // 访问外界的变量
            NSLog(@"%d", age);
        } copy]; // 需适时的[block release]一下
        
        NSLog(@"%@", [block class]); // __NSMallocBlock__
        NSLog(@"%@", [[block class] superclass]); // __NSMallocBlock
        NSLog(@"%@", [[[block class] superclass] superclass]); // NSBlock
        NSLog(@"%@", [[[[block class] superclass] superclass] superclass]); // NSObject
    }
    return 0;
}

你会发现在平常的开发中,我们用到的总是堆block,而不是栈block。这是因为在ARC下我们使用block的时候,系统很多情况下都会自动帮我们复制一份栈block到堆区,而MRC下则需要我们手动调用copy方法让系统复制一份栈block到堆区。那为什么非要复制一份栈block到堆区?栈block有什么问题吗?

void (^block)(void);
void test() {
    
    // 普通局部变量
    int age = 25;
    
    block = ^{
        // 访问外界的变量
        NSLog(@"%d", age); // -272632440,不是25
    };
}

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

试看上面这段代码,我们定义了一个block,并且它的实现部分访问了外界的普通局部变量,所以它是一个栈block。而我们知道栈内存是由系统自己管理的,在出了相应的作用域后栈内存就会自动释放,可以供别人使用了,你的数据有可能就被别人替换掉。那么test函数执行完后,block超出作用域,已经被系统释放掉了,此时虽然说我们还能通过block这个全局变量去访问那块内存,但那块内存里存的很有可能已经是别人的数据了,所以block()这个调用本身其实已经没有意义了,可以根据block的执行顺序去分析一下这段代码。

总结一下:为什么要把栈block到copy到堆区?

block刚被创建出来时,若不是全局block就是栈block,而栈内存又是系统自动管理的,一旦超出变量的作用域,变量对应的内存就会被释放,所以如果不把栈block复制到堆区,就很有可能我们在调用栈block的时候它已经被销毁了,也就是说block内存里的数据已经都是垃圾数据了,即便能调用成功那也是毫无意义地调用,会导致数据错乱。

额外的考虑,拿来玩儿

上面我们知道了对栈block执行copy操作是在堆区复制出了一个新的block,那对全局block和堆block执行copy操作呢?

  • 全局block执行copy操作
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        // 静态局部变量
        static int age = 25;
        
        void (^block)(void) = ^{
            // 访问外界的变量
            NSLog(@"%d", age);
        };
        
        NSLog(@"%p", block); // 0x100001060
        NSLog(@"%p", [block copy]); // 0x100001060
    }
    return 0;
}
  • 堆block执行copy操作
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        // 普通局部变量
        int age = 25;
        
        void (^block)(void) = [^{
            // 访问外界的变量
            NSLog(@"%d", age);
        } copy];
        
        NSLog(@"%p", block); // 0x102005650
        NSLog(@"%p", [block copy]); // 0x102005650
    }
    return 0;
}

可见:

block类型 执行copy操作后的效果
全局block 什么也不做,不会产生新的block,旧block的引用计数也不会加1,因为内存是放在全局区的,生命周期跟App同步,你用就行了,不必增加引用计数
栈block 复制一份栈block到堆区
堆block 仅仅是block的引用计数加1,不会产生新的block

2、ARC下系统会在某些情况下自动copy一份栈block到堆区

(注意:这一小节的示例代码都是在ARC下的)

  • block赋值给一个强指针时(即__strong修饰的指针),系统会自动把该栈block复制到堆区,可以理解为Swift和Dart里函数复制给变量时
typedef void(^Block)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        int age = 25;
        
        Block block = ^{

            NSLog(@"%d", age);
        };
        
        // 等价于
//      __strong Block block = ^{
//
//            NSLog(@"%d", age);
//        };

        NSLog(@"%@", [block class]); // __NSMallocBlock__,是一个堆block
    }
    return 0;
}

上面代码中block变量是个强指针,等号右边的block本来是个栈block,但是在赋值给强指针时系统会自动把该栈block复制到堆区,所以就返回了一个堆block。当然如果我们把block变量变成弱指针,那block自然就还是栈block,系统不会自动复制了。

typedef void(^Block)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        int age = 25;
                 
        __weak Block block = ^{

            NSLog(@"%d", age);
        };

        NSLog(@"%@", [block class]);// __NSStackBlock__,是一个栈block
    }
    return 0;
}
  • block作为函数的参数时,系统会自动把该栈block复制到堆区,可以理解为Swift和Dart里函数作为函数的参数时
typedef void(^Block)(void);
void test(Block block) {
    
    NSLog(@"%@", [block class]); // __NSMallocBlock__,是一个堆block
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        int age = 25;
        
        Block block = ^{
            
            NSLog(@"%d", age);
        };
        
        test(block);
    }
    return 0;
}
  • block作为函数的返回值时,系统会自动把该栈block复制到堆区,可以理解为Swift和Dart里函数作为函数的返回值时
typedef void(^Block)(void);
Block test() {
    int age = 25;
    
    Block block = ^{
        
        NSLog(@"%d", age);
    };
    
    return block;
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        NSLog(@"%@", [test() class]);// __NSMallocBlock__,是一个堆block
    }
    return 0;
}

上面代码中test函数返回的block本来是个栈block,但是在作为函数的返回值系统会自动把该栈block复制到堆区,所以调用test函数时就得到了一个堆block。

  • GCD方法里的block,系统都会自动复制到堆区
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    
});

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    
    
});

// 等等......
  • Foundation框架usingBlock方法里的block,系统都会自动复制到堆区
[[NSArray alloc] enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
    
    
}];

[[[NSDictionary alloc] init] enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
    
    
}];

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

推荐阅读更多精彩内容