Objective-C 中的 Blocks 功能

160 阅读5分钟

Blocks 功能概述

Blocks 是 Objective-C 对 C 语言的扩充功能,是可访问函数作用域的自动变量(局部变量)的匿名函数。等同于其它语言中的闭包和 lambda 计算。基本原理是编译器自动生成持有自动变量和函数实现的类,用简洁的代码量实现带自动变量的匿名函数。

Blocks 组成要素

Blocks 语法

  • 声名
typedef int (^Block)(int);
  • 定义
^ [返回值类型][(参数列表)] 表达式
^int (int count) { return count + 1; }
^(int count) { return count + 1; }
^{ pirntf("Blocks\n"); }

Blocks 截获自动变量

  • 不可变更值截获
int count = 0;
void (^Block)(void) = ^{ printf("count: %d\n", count); };
Block();
  • __block 可变更截获
__block int count = 0;
void (^Block)(void) = ^{ count += 1; };
Block();
printf("count: %d\n", count);
  • 对象类型截获
NSMutableArray *array = [NSMutableArray new];
void (^Block)(void) = ^{ [array addObject:@"Block"]; };

__block NSMutableArray *array;
void (^Block)(void) = ^{ array = [NSMutableArray new]; };

Block 不支持截获 C 语言的数组,需要使用指针解决

// const char text[] = "Block"; // 补支持截获数组
const char *text = "Block";
void (^Block)(void) = ^{ printf("%c\n", text[2]); };

Blocks 存储域

应用程序中内存被划分成几个区域:

  • 全局区:存放全局变量和静态(全局、局部)变量和字符串常量;
  • 栈区(stack):由编译器自动分配释放, 存放函数的参数值,局部变量等;
  • 堆区(heap):由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收

Blocks 本质是类对象,同样需要放在相应的存储区域,在不同的存储区域对应了不同的类型:

  • 栈区:_NSConcreteStackBlock
  • 全局区:_NSConcreteGlobalBlock
  • 堆区: _NSConcreteMallocBlock

Blocks 要素之间关联

Blocks 的实现

Blocks 做为 C 语言的扩充功能,Clang 可以将 Blocks 转换成普通的 C 语言代码。通过 "-rewrite-objc" 选项能将 Blocks 转换为基于 struct 实现的 C++ 源码,本质还是 C 代码

int main()
{
    __block int count = 10;
    void (^Block)(void) = ^{ count = 1; };
}

clang -rewrite-objc xxx.mm

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

struct __Block_byref_val_0 {
    void *__isa;
    __Block_byref_val_0 *__forwarding;
    int __flags;
    int __size;
    int val;
};

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0 *Desc;
    __Block_byref_val_0 *val;
    
    __main_block_imp_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags = 0) : val(_val->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_val_0 *val = __cself->val;
    (val->__forwarding->val) = 1;
}

static void __main_block_copy_0(struct __main_block_impl_0 *dst, struct __main_block_impl_0 *src) {
    _Block_object_assign(&dst->val, src->val, BLOCK_FIELD_IS_BYREF);
}

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

static struct __main_block_desc_0 {
    unsigned long reserved;
    unsigned long 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() {
    __Block_byref_val_0 val = {
        0, 
        &val,
        0,
        sizeof(__Block_byref_val_0),
        0
    };
    
    Block = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &val, 0x22000000);
}

Blocks 的作用域

上面的实现中 __block_impl 结构体是 Blocks 的基本结构,其中第一个成员为 isa ,了解 Objective-C 的类和对象的实质,可以知道 Blocks 实际就是 Objective-C 对象。

在前面存储域的对应的 Blocks 类型介绍中,isa 有三个 Blocks 类型:

  • 栈区:_NSConcreteStackBlock
  • 全局区:_NSConcreteGlobalBlock
  • 堆区:_NSConcreteMallocBlock

依据类型所对应的存储域,可以知道其作用域:

  • _NSConcreteStackBlock:局部作用域
  • _NSConcreteGlobalBlock:全局作用域
  • _NSConcreteMallocBlock:匿名函数作用域,在局部作用域结束后,仍然存活,需要被主动释放。

_NSConcreteStackBlock Block 通过 copy 在堆区产生 _NSConcreteMallocBlock Block.

objc_retainBlock();
  • _NSConcreteStackBlock:从栈复制到堆
  • _NSConcreteGlobalBlock:什么也不做
  • _NSConcreteMallocBlock:引用计数增加

截获变量作用域

  • 不可变更值截获变量为局部作用域
  • __block 修饰截获变量被 Block 持有,为匿名函数作用域

__block 修饰符与 __forwarding

__block 变量在 Block 复制时存在一个问题:__block 变量在 Block 被复制时,同样被复制到堆区,那么栈区和堆区存在两个 __block 变量,此时需要在函数内和 Block 堆都要正确的访问 __block 变量,则需使用到 __forwarding 变量。

当 __block 变量存放在栈区时,__forwarding 指向自己。当被复制到堆区后,栈区的 __block 变量的 __forwarding 指向堆区的 __block 变量,堆区的 __block 变量的 __forwarding 则仍然指向自己。这样在任何时间都能访问到正确的 __block 变量。

截获对象

C 语言的结构体的成员不能使用 __strong 进行修饰,所以也不能使用 Objective-C 的 ARC 进行自动内存管理截获的 Objective-C 的对象。

Block 提供来两个函数支持 Objective-C 对象的生命周期的管理:

  • __main_block_copy_0: 栈区 Block 复制到堆区时调用
  • __main_block_dispose_0:堆区 Block 被释放时调用

这两个函数的实现会调用下面两个方法

  • _Block_object_assign
  • _Block_object_dispose

截获对象: BLOCK_FIELD_IS_OBJECT

__block 变量: BLOCK_FIELD_IS_BYREF

Blocks 引用循环

  • ARC 时使用 __weak 修饰截获对象,避免引用循环
  • 非 ARC 时使用 __block 修饰截获对象,避免引用循环。

走过路过

抖音直播团队内推:job.toutiao.com/s/LG5ktFL

截屏2022-03-19 下午7.56.37.png

深圳、北京、杭州三地共建业务,各地均在大量招人,也欢迎大家投递和推荐

业务介绍

  • 直播团队负责为字节跳动旗下的所有 App 提供直播服务,包括但不限于抖音、火山、今日头条、西瓜视频、皮皮虾、懂车帝等,还有火遍全球的 TikTok。
  • 直播团队不仅仅负责直播平台技术研发,为直播提供稳定的基础服务;还负责直播业务研发,为用户提供完善的直播体验。

业务发展

  • 直播是字节的头部业务,用户与营收高速增长,两年不到的时间就跻身行业第一,直播体量目前已远超竞品。
  • 依托于抖音、火山、头条、西瓜等的巨大体量优势,直播业务仍有较大空间,一年翻番不是梦。
  • 直播属于中台研发模式,基础建设与架构等方面会面临和普通项目不一样的挑战,常规的业务开发上的设计也需要思考的更佳全面,此外还会涉及到性能优化、跨端技术、编译平台建设等,甚至还有手机投屏、游戏引擎打通等创新技术;总体来说,只要你敢于挑战,在技术深度与广度方面都会得到较好的锻炼。