iOS 底层原理|Block 详解

一、Block 简介块对象是 C 级语法和运行时功能。它们类似于标准 C 函数,但是除了可执行代码之外,它们还可能包含与自动(堆栈)或托管(堆)内存的变量绑定。因此,一个块可以维护一组状态(数据),当执行时它可以...

一、Block 简介

块对象是 C 级语法和运行时功能。它们类似于标准 C 函数,但是除了可执行代码之外,它们还可能包含与自动(堆栈)或托管(堆)内存的变量绑定。因此,一个块可以维护一组状态(数据),当执行时它可以用来影响行为。

您可以使用块来构成函数表达式,这些函数表达式可以传递给API,可以选择存储并由多个线程使用。块作为回调特别有用,因为该块既包含要在回调上执行的代码,又包含执行期间所需的数据。

OS X v10.6 开发人员工具附带提供了 GCC 和 Clang 中的块。您可以在 OS X v10.6 和更高版本以及 iOS 4.0 和更高版本中使用块。块运行时是开源的,可以在 LLVM 的 compile-rt 子项目存储库中找到。块也已作为 N1370 提交给 C 标准工作组:Apple对C的扩展。由于 Objective-C 和 C++ 都从 C 派生而来,因此块被设计为可用于所有三种语言(以及 Objective-C++ )。

Block 本质是一个封装了函数调用以及函数调用环境的 OC 对象,它主要由一个 isa 指针和一个 impl 函数指针和一个 descriptor 组成。 有点类似 C 里面的函数指针。

二、Block 用法

Block 基础表达式:

/**
 * return_type 表示返回的对象/关键字等(可以是 void,并省略)
 * blockName   表示 block 的名称
 * var_type    表示参数的类型(可以是 void,并省略)
 * varName     表示参数名称
 */
return_type (^blockName)(var_type) = ^return_type (var_type varName) { ... };
blockName(var);

1. Block 语法

1.1 当返回类型为 void

void (^blockName)(var_type) = ^void (var_type varName) { ... };
blockName(var);
// 可省略成
void (^blockName)(var_type) = ^(var_type varName) { ... };
blockName(var);

1.2 当参数类型为 void

return_type (^blockName)(void) = ^return_type (void) { ... };
blockName();
// 可省略成
return_type (^blockName)(void) = ^return_type { ... };
blockName();

1.3 当返回类型和参数类型都为 void

void (^blockName)(void) = ^void (void) { ... };
blockName();
// 可省略成
void (^blockName)(void) = ^{ ... };
blockName();

1.4 匿名 Block

Block 实现时,等号右边就是一个匿名 Block,它没有 blockName,称之为匿名Block。

^return_type (var_type varName) { ... };

1.5 typedef 简化 Block 的声明

typedef return_type (^BlockTypeName)(var_type);

例子

// 声明
typedef void(^ClickBlock)(NSInteger index); 
// 使用 1
@property (nonatomic, copy) ClickBlock clickBlock;
// 使用 2
(void)methodHandle:(ClickBlock)handle { ... }

2. 应用场景

2.1 响应事件

2.2 传递数据

2.3 链式语法

核心思想为将 Block 作为方法的返回值,且返回值的类型为调用者本身,并将该方法以 setter 的形式返回,这样就可以实现了连续调用,即为链式编程。

CaculateMaker.h 文件中声明一个方法 add

//  CaculateMaker.h 
@interface CaculateMaker : NSObject 

@property (nonatomic, assign) CGFloat result;

- (CaculateMaker *(^)(CGFloat num))add; 

@end

CaculateMaker.m 文件中实现 add 方法

#import "CaculateMaker.h" 
@implementation CaculateMaker
- (CaculateMaker *(^)(CGFloat num))add { 
return ^CaculateMaker *(CGFloat num) {
        _result += num; 
        return self;
    };
} 
@end

使用该类

CaculateMaker *maker = [[CaculateMaker alloc] init];
maker.add(20).add(30);

三、Block 深入探究

1. Block 组成

我们在 main.m 写入代码:

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {        
        int age = 20;
        void (^block)(void) =  ^{
             NSLog(@"age is %d", age);
         };
        block();
    }
    return 0;
}

单独编译一下 main.m 文件,在终端执行命令:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m

得到 main.cpp 文件,我们从文件中提取有用信息,整理上面的代码得

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

其中 __main_block_impl_0__main_block_func_0 内容为

struct __block_impl { // __block_impl impl
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};
static struct __main_block_desc_0 { // __main_block_desc_0* Desc
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

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;
    ...
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int age = __cself->age; // bound by copy
  NSLog((NSString *)&__NSConstantStringImpl__var_folders_3j_fvy599js5nnglvw7jslq09t80000gn_T_main_cef707_mi_0, age);
}

从上面我们可以看出 Block 内部能够正常访问外部的变量,这就引到了 Block 变量捕获机制( capture )。

2. Block 的变量捕获

2.1 局部变量 auto

局部变量 auto (自动变量) ,我们平时写的局部变量,默认就有 auto (自动变量,离开作用域就销毁)。比如:

int age = 20; // 等价于 auto int age = 20;

2.2 局部变量 static

static 修饰的局部变量 ,不会被销毁。由下面代码可以看得出来,Block 外部修改static 修饰的局部变量,依然能影响 Block 内部的值:

static int height  = 30;
int age = 20;
void (^block)(void) =  ^{
     NSLog(@"age is %d height = %d",age,height);
};
age = 25;
height = 35;
block();

// 执行结果
age is 20 height = 35

这是为什么会改变呢?
因为 age 是直接值传递,height 传递的是 *height,也就是说直接把内存地址传进去进行修改了。大家可以根据上面讲解内容尝试编译成 cpp 文件,去了解详情。缺点是会永久存储,内存开销大。

2.3 全局变量

全局变量 不能也无需捕获到 Block 内部,因为全局变量是存放在全局静态区的,直接访问就完事了。缺点也是内存开销大。

3. Block 类型

block类型 环境
__NSGlobalBlock__ 没有访问auto变量
__NSStackBlock__ 访问了auto变量
__NSMallocBlock__ __NSStackBlock__ 调用了copy

每一种类型的 block 调用 copy 后的结果如下所示

Block 的类 副本源的配置存储域 复制效果
_NSConcreteGlobalBlock 程序的数据区域 什么也不做
_NSConcreteStackBlock 从栈复制到堆
_NSConcreteMallocBlock 引用计数增加

在 ARC 开启时候,只会有 _NSConcreteGlobalBlock_NSConcreteMallocBlock 类型的 Block,因为在 ARC 模式下,编译器会根据情况自动将栈上的 Block 复制到堆上(Block 自动调用了 copy 方法)。情况如下:

  • Block 作为函数返回值时;
  • 将 block 赋值给 __strong 指针时;
  • Block 作为 Cocoa API 中方法名含有 usingBlock 的方法参数时;
  • Block 作为 GCD API 的方法参数时;

所以在 ARC 下建议 Block 的声明写法:

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

3.1 Block 类型最终继承于 NSObject

我们通过一段代码验证一下:

void (^block)(void) =  ^{
     NSLog(@"Hello Block");
 };
NSLog(@"%@", [block class]);
NSLog(@"%@", [[block class] superclass]);
NSLog(@"%@", [[[block class] superclass] superclass]);
block();

打印结果是:

2021-04-10 01:24:01.844162+0800 studyOC[40328:4640930] Hello Block
2021-04-10 01:24:01.844502+0800 studyOC[40328:4640930] __NSGlobalBlock__
2021-04-10 01:24:01.844541+0800 studyOC[40328:4640930] NSBlock
2021-04-10 01:24:01.844566+0800 studyOC[40328:4640930] NSObject

Block 的类型都是继承于 NSBlock,最终是继承于 NSObject。上面是验证了__NSGlobalBlock__,Block 其他类型自行验证,方法一样。
这里需要注意一下,不要通过 clang 方式去将 OC 代码转换成 C++ 代码去验证上述结论,在 llvm x.0 开始实际上编译的成的文件不是 C++ 文件,是一个中间文件。这里牵扯到了 Runtime,所以转成 C++ 代码会有些不准确,以运行时为准。

3.2 Block 内部访问对象类型的 auto 变量

如果 Block 是在栈上,不会对 auto 变量产生强引用;但是
如果 Block 被拷贝到堆上时,

  • 会调用 Block 内部的 copy 方法;

  • copy 方法内部会调用 _Block_object_assign 函数;

  • _Block_object_assign 函数会根据 auto 变量的修饰符(__strong__weak__unsafe_unretained)做出相应的操作,如引用计数加一,形成强引用(retain)或者弱引用。

    _Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
    

如果 Block 从堆上移除,

  • 会调用 Block 内部的 dispose 函数;

  • dispose 函数内部会调用 _Block_object_dispose 函数;

  • _Block_object_dispose 函数会自动释放引用的 auto 变量(release

    _Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
    

4. __block 修饰符

__block 会在编译时候将修饰的变量包装成一个对象。我们来验证一下,我们把以下代码转成 C++ 代码:

__block int age = 18;
void (^block)(void) =  ^{
     NSLog(@"Hello Block %d", age);
 };
block();

可观察 int 类型的 age__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
  ...
}

__block 可以用于解决 Block 内部无法修改 auto 变量值的问题;但是 __block 不能修饰全局变量、静态变量(static)。

4.1 __block 的内存管理

  • 当 Block 在栈上时,并不会对 __block 变量产生强引用;

  • 当 Block 被 copy 到堆时,
    会调用 Block 内部的 copy 方法;
    copy 方法内部会调用 _Block_object_assign 函数;
    _Block_object_assign 函数会对 __block 变量形成强引用(retain)。

    _Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
    
  • 当 Block 从堆中移除时,
    会调用 Block 内部的 dispose 方法;
    dispose 方法内部会调用 _Block_object_dispose 函数;
    _Block_object_dispose 函数会自动释放引用的 __block 变量(release);

    _Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
    
    

4.2 __block__forwarding 指针

5. Block 循环引用

很多人在使用 Block 的时候经常发生循环引用,那这个循环引用怎么发生的,又是如何解决的,我们来分析分析。

5.1 导致循环引用的原因

循环引用问题就是两个对象相互持有,导致两个对象无法被释放。

同理,Block 发生循环引用就是,Block 持有(强引用)对象,对象持有(强引用) Block,导致对象 A 在堆中将要被释放时,发现还持有 B 对象,同时 B 对象也持有 A 对象,僵持在那里,谁都无法被释放。如图:

5.2 如果解决循环引用问题

那我们应该如何去解决这个问题呢?

a. 通过 __weak__unsafe_unretained 使 Block 对对象弱引用
  • 使用 __weak 解决循环引用
    1. __weak 修饰的对象释放后会自动致为 nil
    2. __weak 修饰的对象注册到 autoreleasepool 中;
    3. __weak 只能在 ARC 模式下使用,也只能修饰对象,不能修饰基本数据类型。
__weak typeof(self) weakSelf = self;
void (^block)(void) =  ^{
     NSLog(@"Hello Block, %p", weakSelf);
 };
block();

__weak 对性能有一定影响,一个对象有大量 __weak 引用时候,当对象被废弃,要给所有 __weak 引用过它的对象赋 nil,消耗 CPU 资源。话虽如此,不过该用的时候就用。
关于 __weak 底层原理大家可以参看一下这篇文章:《Objective-C weak关键字实现源码解析》

  • 使用 __unsafe_unretained 解决循环引用
__unsafe_unretained id weakSelf = self;
void (^block)(void) =  ^{
     NSLog(@"Hello Block, %p", weakSelf);
 };
block();

__weak 只支持 iOS 5.0+ 和 OS X Mountain Lion+ 作为部署版本。__unsafe_unretained 兼容性更好一些。
虽然 __unsafe_unretained__weak 都能防止对象持有,但是对于 __weak,指针的对象在它指向的对象释放的时候回转换为 nil ,这是一种特别安全的行为;而 __unsafe_unretained 会继续指向对象存在的那个内存,即使是在它已经销毁之后。这会导致因为访问那个已释放对象引起的崩溃,当然相对而言,__unsafe_unretained 对性能影响没那么大。

a. 用 __block 解决(必须要调用 Block)

参考文档

  1. 《Blocks Programming Topics》 苹果官方文档
  2. 《iOS block深入理解(一)》三秋树下
  3. 《iOS中Block的用法,举例,解析与底层原理(这可能是最详细的Block解析)》
  4. 《在block内如何修改block外部变量》
  5. 《Objective-C weak关键字实现源码解析》

本文标题为:iOS 底层原理|Block 详解

基础教程推荐