Skip to content

Objc Block

Deepin edited this page Sep 1, 2021 · 1 revision

一、Block概述

block是带有自动变量的匿名函数。

匿名函数: 没有函数名的函数,一对{}包裹的内容是匿名函数的作用域。

自动变量: 栈上声明的一个变量不是静态变量和全局变量,是不可以在这个栈内声明的匿名函数中使用的,但在Block中却可以。虽然使用block不用声明类,但是block提供了类似Objective-C的类一样可以通过成员变量来保存作用域外变量值的方法,那些在block的一对{}里使用到但却是在{}作用域以外声明的变量,就是block捕获的自动变量。

根据作用域不同,block分为如下三类:

  • _NSConcreteGlobalBlock - 全局的静态block,不会访问任何外部变量(ARC&MRC)
  • _NSConcreteStackBlock - 保存在栈中的block,当函数返回时会被销毁(MRC)
  • _NSConcreteMallocBlock - 保存在堆中的block, 当引用计数为0时会被销毁(ARC&MRC)

Q: 为何要用copy修饰block呢?(严格来说这个问题在ARC环境下是有问题的或者说过时的。)

A1: 其实这个表述方式指的是MRC环境下,block中使用了外部变量,默认是_NSConcreteStackBlock,存放在栈区,栈区的特点就是创建的对象随时可能被销毁,一旦被销毁后续再次调用空对象就可能会造成程序崩溃; 这个时候使用copy修饰,这可以让block从栈中复制到堆中,变为_NSConcreteMallocBlock,程序员自己管理,就可以避免block对象随时被销毁了。

A2: 实际上在ARC下,通常只会看到_NSConcreteGlobalBlock与_NSConcreteMallocBlock, 因为系统会默认对使用外部变量的block进行copy操作,无论是用copy修饰还是strong修饰,最终都是先看是否引用了外部变量,之后不论是copy还是strong都执行了copy操作,新的类型是根据是否引用外部变量来决定的。

!!!当然使用copy修饰,更加符合代码统一风格。

二、Block的使用

1. block的语法

block表达式语法 ^ 返回值类型 (参数列表) {表达式}

block变量语法 返回值类型 (^变量名)(参数列表) = Block表达式

2. block的声明

自定义了一个继承至NSObject的block研究类BlockResearch, 在里面编写了block的各种情况

typedef void(^Success)(NSData *response);
typedef void(^Failure)(NSString *message, int code);

@interface BlockResearch : NSObject

@property (nonatomic, copy) void(^show)(); /// 无返回值,无参(参数没有写void),会有警告信息`This block declaration is not a prototype`
@property (nonatomic, copy) void(^animation)(void); /// 无返回值,无参(参数有写void),不会有警告信息
@property (nonatomic, copy) void(^completion)(BOOL finished); /// 无返回值,有一个参数
@property (nonatomic, copy) Success success; /// 使用typedef别名定义的block,无返回值, 有一个参数;
@property (nonatomic, copy) Failure failure; /// 使用typedef别名定义的block,无返回值, 有两个参数;

@property (nonatomic, copy) BOOL(^addition)(int a, int b); /// 有返回值,有两个参数

@end

3. block的使用

在比如ViewController.m中#import BlockResearch.h之后,开始编写具体使用的代码

- (void)testBlock {
    
    BlockResearch *br = [[BlockResearch alloc]init];
    
    // MARK: - 1. 无返回值,无参(参数没有写void),会有警告信息
    /// 调用
    br.show = ^{
        NSLog(@"show");
    };
    /// 回调执行
    br.show(); 
    
    // MARK: - 2. 无返回值,无参(参数没有写void),会有警告信息
    /// 调用
    br.animation = ^{
        NSLog(@"hide");
    };
    /// 回调执行
    br.animation();

    // MARK: - 3. 无返回值,有一个参数
    /// 调用
    br.completion = ^(BOOL finished) {
        NSString *result = finished == YES ? @"is finished" : @"not finished";
        NSLog(@"animation %@", result);
    };
    /// 回调执行
    br.completion(YES);
    
    // MARK: - 4. 使用typedef别名定义的block,无返回值, 有一个参数;
    /// 调用
    br.success = ^(NSData *response) {
        NSLog(@"the response data is: %@",response);
    };
    /// 回调执行
    br.success([NSData data]);
    
    // MARK: - 5. 使用typedef别名定义的block,无返回值, 有两个参数;
    /// 调用
    br.failure = ^(NSString *message, int code) {
        NSString *result = code == 200 ? @"success" : @"failure";
        NSLog(@"the %@ request code is: %@", result, message);
    };
    /// 回调执行
    br.failure(@"bad gateway", 400);
    
    // MARK: - 6. 有返回值,有两个参数
    /// 对于block有返回值这种,有后面会介绍一种方法,模仿swift高级函数如map, filter等, 优点会体现的很明显,当然日常也可以使用
    /// 调用
    br.addition = ^BOOL(int a, int b) {
        NSLog(@"addition result is %d", a+b);
        return YES;
    };
    /// 回调执行
    br.addition(1, 2);
}

4. block当参数

// MARK: - block做为参数
- (void)requestWith:(NSString *)url
            success:(void(^)(id response))success
            failure:(void(^)(NSString *message, int code))failure {

    ///NSURL *ur = [[NSURL alloc]initWithString:url];
    
    /// 其他逻辑
    /// 判断是否成功
    
    /// 以下仅为了代码可以执行,随意写的,不做实际用途
    BOOL isSuccess = [url isEqualToString:@""] ? YES : NO;
    
    if (isSuccess) { /// 如果成功
        id response = (id)@"{data:{}}";
        success(response);
    } else { /// 如果失败
        failure(@"bad gateway",400);
    }
}

实际调用

   [self requestWith:@"https://www.baidu.com"
              success:^(id response) {
        NSLog(@"success %@",response);
    } failure:^(NSString *message, int code) {
        NSLog(@"failure %@-%d",message, code);
    }];

三、block的特性

捕获外部变量

block表达式可以捕获外部自动变量的值,这个是值是瞬间值,所以那怕声明block之后,在block外修改自动变量的值,也不会对block内部捕获的自动变量值产生影响。

- (void)testCaptureVariable {
    int a = 10;
    void(^block)(void) = ^{
        NSLog(@"within block, a = %d", a);
    };
    a = 20;
    block();
    NSLog(@"a = %d", a);
}

输出结果如下:

within block, a = 10
a = 20

若是我们变量声明前面加上_ _block修饰,

- (void)testCaptureVariable {
    __block int a = 10;
    void(^block)(void) = ^{
        NSLog(@"within block, a = %d", a);
    };
    a = 20;
    block();
    NSLog(@"a = %d", a);
}

输出结果如下:

within block, a = 20
a = 20

block变量复制

对于 block 外的变量引用,block 默认是将其复制到其数据结构中来实现访问的,如下图所示

对于用 _ _block 修饰的外部变量引用,block 是复制其引用地址来实现访问的,如下图所示

以上的结论会在block探索章节得到验证

block的循环引用

如下样例,定义了一个cycleRetainBlock, 然后执行表达式

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

self.cycleRetainBlock = ^{
   NSLog(@"self %@",self);
};

会显示警告信息:Capturing 'self' strongly in this block is likely to lead to a retain cycle, 这就是block的循环引用。

self拥有成员变量cycleRetainBlock--->就持有cycleRetainBlock, 在cycleRetainBlock代码块中打印self的信息,就会使cycleRetainBlock---->持有self, 这就导致了两个相互持有,不能在作用域结束后正常释放。

解决原理:打破两个的相互持有,self对cycleRetainBlock的持有不好打破,因为得使用这个block, 那么选择在block内打破持有。

解决方法:

    1. 使用__weak typeof(self)
- (void)testCycleRetainOfBlock {
    
    __weak typeof(self) weakSelf = self;
    self.cycleRetainBlock = ^{
        NSLog(@"self %@",weakSelf);
    };
    self.cycleRetainBlock();
}
    1. Reactive Cocoa中的@weakify@strongify
- (void)testCycleRetainOfBlock {
    @weakify(self);
    self.cycleRetainBlock = ^{
        @strongify(self)
        NSLog(@"self %@",self);
    };
    self.cycleRetainBlock();
}

block类别

有以下样例代码:

@property (nonatomic, copy) void(^copyBlock)(void);
@property (nonatomic, strong) void(^strongBlock)(void);
- (void)testBlockTypes {
    
    ///1.  没有引用外部变量
    void(^block01)(void) = ^{
        NSLog(@"within block01");
    };
    NSLog(@"block01-%@",block01);
    
    ///2.  引用外部变量
    int a = 1;
    void(^block02)(void) = ^{
        NSLog(@"within block02-%d",a);
    };
    NSLog(@"block02-%@",block02);
        
    ///3.  直接打印输出引用外部变量的block copy
    NSLog(@"block01 copy-%@",[block01 copy]);
    
    ///4.  定义了一个新的变量来接受,block的copy
    void(^block03)(void) = [block02 copy];
    NSLog(@"block03-%@",block03);
    
    ///5.  没有引用外部变量,但是是copy修饰的成员变量
    self.copyBlock = ^{
        NSLog(@"within self.copyBlock");
    };
    NSLog(@"copyBlock-%@",self.copyBlock);
    
    ///6.  没有引用外部变量,但是是strong修饰的成员变量
    self.strongBlock = ^{
        NSLog(@"within self.strongBlock");
    };
    NSLog(@"strongBlock-%@",self.strongBlock);
}

执行结果:

1. block01-<__NSGlobalBlock__: 0x100e74b88>
2. block02-<__NSMallocBlock__: 0x600001b701b0>
3. block01 copy-<__NSGlobalBlock__: 0x100e74b88>
4. block03-<__NSMallocBlock__: 0x600001b701b0>
5. copyBlock-<__NSGlobalBlock__: 0x100e74bc8>
6. strongBlock-<__NSGlobalBlock__: 0x100e74be8>

通过上面例子,我们可以看出: 在默认ARC环境下,对于没有使用外部变量的block,都是__NSGlobalBlock__,保存在全局的静态block; 此时无论是copy操作,还是用copy, strong操作,最后都是__NSGlobalBlock__类型,这个通过上面3,5,6都可以看出来; 对于使用外部变量,以及对其进行copy复制的,都会复制到堆区,变成__NSMallocBlock__.

以上就可以验证开头的QA问答了!!!

对于要验证MRC的,可以: 在工程-->Build Phases-->Compile Sources中,将需要切换为MRC的类文件后加上-fno-objc-arc即可 把上面的样例代码再执行一遍,得到结果如下:

1. block01-<__NSGlobalBlock__: 0x10bcbbb98>
2. block02-<__NSStackBlock__: 0x7ffee4203980>
3. block01 copy-<__NSGlobalBlock__: 0x10bcbbb98>
4. block03-<__NSMallocBlock__: 0x6000017215c0>
5. copyBlock-<__NSGlobalBlock__: 0x10bcbbbd8>
6. strongBlock-<__NSGlobalBlock__: 0x10bcbbbf8>

可以看到2输出的结果就是__NSStackBlock__了,对其复制操作的4,得到结果就是__NSMallocBlock__。

以上就是对开头QA的验证!

四、block的探索

创建一个demo来了解block的内部结构, oc或者c的皆可。这里以oc创建的demo为例;

若是使用c的demo,使用LLVM编译器的clang命令可将含有block的Objective-C代码转换成C++的源代码, 命令 clang -rewrite-objc main.c -o main.cpp 或者 clang -rewrite-objc 源码文件名

1. 不使用外部变量的block

1.1 在main.m文件的main函数中,编写一个block如下:

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {

        void(^testBlock)(void) = ^() {
            NSLog(@"test");
        };
        testBlock();
        
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

1.2. 在命令行cd到main.m所在的目录
1.3. 使用clang工具,在命令行中执行其命令:xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
1.4. 使用命令ls,在当前目录中可以查看到多了一个文件:main.cpp
1.5. 执行命令open main.cpp,就可以打开该文件
1.6. 该文件中内容很多,可以从上往下过一遍,也可以通过关键字"StackBlock"搜索,快速定位到__main_block_impl_0 就是对应block的结构体代码了,如下:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc; // 描述block大小、版本等信息
  //构造函数函数
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock; /// isa指向栈
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

我们可以看到__main_block_impl_0结构体,包含了另外两个结构体,__block_impl__main_block_desc_0

__block_impl结构体如下:

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

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

还可以看到其他源码

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
   NSLog((NSString *)&__NSConstantStringImpl__var_folders_zz_jksy2fhn6kv3_yf8_wfwjhqh0000gq_T_main_9d24b6_mi_0);
}

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        void(*testBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
        ((void (*)(__block_impl *))((__block_impl *)testBlock)->FuncPtr)((__block_impl *)testBlock);

        appDelegateClassName = NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class")));
    }
    return UIApplicationMain(argc, argv, __null, appDelegateClassName);
}

其中,涉及我们写的block被转成了如下

void(*testBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
        ((void (*)(__block_impl *))((__block_impl *)testBlock)->FuncPtr)((__block_impl *)testBlock);

2. 使用外部变量的block

接下来,我们在上一节block基础上做一些调整,增加一个外部变量a

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {

        int a = 1;
        void(^testBlock)(void) = ^() {
            NSLog(@"test a: %d",a);
        };
        testBlock();
        
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

再重复操作一遍上面的步骤, 最后得到的main.cpp文件,同样的查看方式,发现__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;
  }
};

__main_block_impl_0中多了int a;这证明了前面的结论:对于 block 外的变量引用,block 默认是将其复制到其数据结构中来实现访问的, 同时也可以证明捕获自动变量如何实现的,因为瞬间将变量捕获,所以不受外部修改的影响, 当然捕获了变量,复制到block内,block的size也会变化。

其他源码中,也可以看到该变量a的存在

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

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_zz_jksy2fhn6kv3_yf8_wfwjhqh0000gq_T_main_5879a6_mi_0,a);
        }

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, char * argv[]) {
    NSString * appDelegateClassName;
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 


        int a = 1;
        void(*testBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
        ((void (*)(__block_impl *))((__block_impl *)testBlock)->FuncPtr)((__block_impl *)testBlock);

        appDelegateClassName = NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class")));
    }
    return UIApplicationMain(argc, argv, __null, appDelegateClassName);
}

3. 使用_ _block变量的block

这一步,我们以在block中修改外部变量的值为例,来说明

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        
        __block int a = 1;
        void(^testBlock)(void) = ^() {
            a = 10;
            NSLog(@"test a: %d",a);
        };
        testBlock();
        
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

输入结果test a: 10, 说明外部变量已经被修改;

再重复执行前面的步骤,获得新的main.cpp, 我们可以看到这次的内容与之前有很大区别

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; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__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_a_0 *a = __cself->a; // bound by ref

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

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 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, char * argv[]) {
    NSString * appDelegateClassName;
    /* @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};
        void(*testBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)testBlock)->FuncPtr)((__block_impl *)testBlock);

        appDelegateClassName = NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class")));
    }
    return UIApplicationMain(argc, argv, __null, appDelegateClassName);
}

从代码中我们可以看到:

  1. 源码中增加一个名为 __Block_byref_i_0 的结构体,用来保存我们要 capture 并且修改的变量 i。
  2. __main_block_impl_0 中引用的是 __Block_byref_i_0 的结构体指针,这样就可以达到修改外部变量的作用。
  3. __Block_byref_i_0 结构体中带有 isa,说明它也是一个对象。
  4. 我们需要负责 __Block_byref_i_0 结构体相关的内存管理,所以 __main_block_desc_0 中增加了 copy 和 dispose 函数指针,对于在调用前后修改相应变量的引用计数。

以上也验证了前面的结论: 对于用 _ _block 修饰的外部变量引用,block 是复制其引用地址来实现访问的

五、block拓展

iOS开发-由浅至深学习block - 涉及模仿swift高级函数, 可以看一下这篇文章,思路很好!

六、block参考资料

谈Objective-C block的实现
iOS Block用法和实现原理
iOS开发-由浅至深学习block
Block为什么使用copy修饰
iOS探索 全方位解读Block
一道Block面试题的深入挖掘

Clone this wiki locally