【iOS】Blocks

【iOS】Blocks

文章目录

前言

笔者最近看了小白书的Blocks的板块,这篇博客简单总结一下有关于Blocks的内容,笔者也是初次较为深入的学习Block。如有错误,请不吝赐教。

什么是Blocks

用小白本里的话来讲就是:带有自动变量(局部变量)的匿名函数

我们认识Blocks从C语言的函数开始认识他:

c 复制代码
int func(int count);

这是我们C语言的函数内容,这时候我们从C语言的函数指针开始:

c 复制代码
int (*funptr)(int) = &func;
int result = funptr(1);
NSLog(@"%d", result);

这样我们可以通过函数指针不需要知道函数名都可以调用这个函数,而Block则可以运行我们直接使用匿名函数。我们首先要认识一下我们的C语言函数中可能用到的一些变量类型:

  • 自动变量
  • 函数的参数
  • 静态局部变量
  • 静态全局变量
  • 全局变量

能在函数的多次调用之间传递值的有后面三者。虽然这些变量的作用域不同,但在整个程序当中,一个变量总保持在一个内存区域。因此,虽然多次调用函数,但该变量值总能保持不变,在任何时候以任何状态调用,使用的都是同样的变量值。

objc 复制代码
int buttonId;
void buttonCallback (int event)
{
        printf ("buttonId:&d event=&d\n" , buttonId, event) ;
}
void setButtonCallbacks () {
    for (int i = 0; i < 30; ++i) {
        buttonid = 1;
        setButtonCallback (BUTTON_IDOFFSET + i, &buttonCallback) ;
    }
}

这里用到了一个回调函数,这个回掉函数有一个问题,那就是我们所有的最后的回掉结果都会是我们最后的那个置,但是如果我们把他变成函数内的变量在进行一个参数传递就不会出现那中问题了。

objc 复制代码
void buttonCallback (int event, int buttonId)
{
        printf ("buttonId:&d event=&d\n" , buttonId, event) ;
}

用Block来写就相当于:

objc 复制代码
void setButtonCallbacks()
{
    for (int i = 0; i < BUTTON_MAX; ++i) {
        setButtonCallbackUsingBlock(BUTTON_IDOFFSET + i, ^(int event) {
            printf("buttonId:&d event=%d\n", i, event);
        });
    }
}

Block又被称为闭包,在不同的语言中都有应用。

Block语法

这里我们认识一下语法格式:

objc 复制代码
^int(int count) {
            return 1;
        };

然后我们可以省略返回值类型:

objc 复制代码
^(int count) {
            return 1;
        };

如果我们不需要用到参数那么可以仅仅保留表达式:

objc 复制代码
^{
            return 1;
        };

Block类型变量

我们先看之前的函数指针的内容:

objc 复制代码
int (*funptr)(int) = &func;
int result = funptr(1);
NSLog(@"%d", result);

Block也是这样子应用的。

同样地,在Block 语法下,可将 Block 语法赋值给声明为 Block 类型的变量中。即源代码中一旦使用Block语法就相当于生成了可赋值给 Block 类型变量的"值"。Blocks 中由Block 语法生成的值也被称为"Block"。在有关Blocks 的文档中,"Block"既指源代码中的Block 语法,也指由Block 语法所生成的值。

objc 复制代码
int (^blk)(int);

与前面的使用函数指针的源代码对比可知,声明Block类型变量仅仅是将声明函数指针类型变量的"*"变为"^"。该Block类型变量与一般的C语言变量完全相同,可作为以下用途使用。

  • 自动变量
  • 函数参数
  • 静态变量
  • 静态全局变量
  • 全局变量

我们这里就不多赘述,主要展示一下我们在函数中使用Block,Block作为参数:

objc 复制代码
void func(int (^blk)(int))
{
    // 在函数体中,可以调用 blk 这个 Block。
}

Block作为返回值:

objc 复制代码
int (^func())(int) {
    return ^(int count) { return count + 1; };
}

这时候我们一般会采用typdef来定义不同的内容:

objc 复制代码
typedef int (^blk_t)(int);

这样让我们的代码更加直观,更方便我们去理解这种类似的代码

截获自动变量

objc 复制代码
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int val = 10;
        int fmt = 15;
        void (^blk)(void) = ^ {
            NSLog(@"%ld %ld",val, fmt);
        };
        fmt = 20;
        val = 30;
        blk();
    }
    return 0;
}

该源代码中,Block语法的表达式使用的是之前声明的目动变量 fmt 和 val。 Blocks 中,Block表达式截获所使用的自动变量的值,即保存该自动变量的瞬间值。因为Block表达式保存了自动变量的值,所以在执行 Block 语法后,即使改写Block 中使用的自动变量的值也不会影响Block 执行时自动变量的值。该源代码就在 Block 语法后改写了 Block 中的自动变量 val 和 fmt。

下面我们一起看一下执行结果。

objc 复制代码
10 15

__block修饰符

如果我们想让我们的Block中的自动变量在Block中进行修改的,那么我们只需要给他加上__block的修饰符就可以了,这样就可以在Blcok中修改我们设置在外面的局部变量了。

objc 复制代码
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block int val = 10;
        int fmt = 15;
        void (^blk)(void) = ^ {
            val = 30;
            NSLog(@"%ld %ld",val, fmt);
        };
        fmt = 20;
        blk();
    }
    return 0;
}
objc 复制代码
30 15
Type: Notice | Timestamp: 2025-02-18 19:09:28.135305+08:00 | Process: BStudy | Library: BStudy | TID: 0x286c5b7

截获的自动变量

我们不能在block中变更我们的int值,那能不能给数组添加数据呢,答案是显而易见的,这是可以的:

objc 复制代码
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block int val = 10;
        int fmt = 15;
        NSMutableArray* array = [@[] mutableCopy];
        void (^blk)(void) = ^ {
            [array addObject:@"1"];
        };
        fmt = 20;
        blk();
    }
    return 0;
}

这是没有问题的,而向截获的变量 array 赋值则会产生编译错误。该源代码中截获的变量值为 NSMutableArray 类的对象。如果用C语言来描述,即是截获 NSMutableArray 类对象用的结构体实例指针。虽然赋值给截获的自动变量 array 的操作会产生编译错误,但使用截获的值却不会有任何问题。下面源代码向截的自动变量进行赋值,因此会产生编译错误。

同样添加__block修饰符就可以变更:

这里主要是两个C语言的字符串和字符数组的区别:

objc 复制代码
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block int val = 10;
        int fmt = 15;
        const char text[] = "hello";
        void (^blk)(void) = ^ {
            printf("%c", text[2]);
        };
        fmt = 20;
        blk();
    }
    return 0;
}

这种方式会报错,但是下面这种方式则不会报错:

objc 复制代码
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block int val = 10;
        int fmt = 15;
        const char* text = "hello";
        void (^blk)(void) = ^ {
            printf("%c", text[2]);
        };
        fmt = 20;
        blk();
    }
    return 0;
}

这是因为现在的OC的block没有实现对于我们的C语言数组的自动截获,而使用指针则可以解决这个问题。

Block的实质

这不过是概念上的问题,在实际编译时无法转换成我们能够理解的源代码,但 clang (LLVM编译器)具有转换为我们可读源代码的功能。通过"-rewrite-objc"选项就能将含有 Block 语法的源代码变换为C++的源代码。说是C++,其实也仅是使用了struct结构,其本质是C语言源代码。

首先我们看一下这段最简单的代码:

objc 复制代码
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void (^blk)(void) = ^{
            printf("123");
        };
        blk();
    }
    return 0;
}

他通过clang编译后变成这个部分:

c++ 复制代码
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

            printf("123");
        }

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, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    }
    return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

转化后的代码,我们可以很清晰的看到我们写在Block中的内容:

c++ 复制代码
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

            printf("123");
        }

这里我们显而易见可以看见_cself是一个block_impl的一个指针,这个指针指向下面这个结构体:

objc 复制代码
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
};

impl的结构体如下:后面两个是升级的大小和函数指针

objc 复制代码
struct __mian_block_impl_0 {
	void *isa; 
  int Flags;
	int Reserved; 
  void *FuncPtr;
};

第二个成员变量如下:

c++ 复制代码
static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
}

他的结构为今后升级的大小和Block的大小。

初始化Block的构造函数如下:

c++ 复制代码
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }

现在这里我们看一下调用构造函数的一个过程:

objc 复制代码
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

去掉转化的一个过程,我们可以看到其实是:

objc 复制代码
struct __main_block_impl_0 tmp = 
    __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
struct __main_block_impl_0 *blk = &tmp;

这里其实就是创建一个局部变量,然后把他赋值给这个我们的blk。这段代码对应:

objc 复制代码
void (^blk)(void) = ^{
            printf("123");
        };

将Block语法生成的Block赋值给Block类型变量,就相当于吧结构体实例指针复制给blk指针

这时候我们看一下他生成实例变量的一个过程:

objc 复制代码
__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);

第一个参数是由 Block 语法转换的C语言函数指针。第二个参数是作为静态全局变量初始化

的_ _main_block_desc_O结构体实例指针。以下为__main_block_dese_0结构体实例的初始化部分代码:

objc 复制代码
static struct __main_block_desc_0 __main_block_desc_0_DATA = {
    0,
    sizeof(struct __main_block_impl_0),
};

这部分代码就是根据__main_block_impl_0的大小来分配一个内存大小。

下面我们看一下之前的__main_block_impl _ 是怎么实现的初始化的

objc 复制代码
isa = &NSConcreteStackBlock;
Flags = 0;
Reserved = 0;
FuncPtr = __main_block_func_0;
Desc = &__main_block_desc_0_DATA;

把一个函数指针__main_block_func_0赋值给FuncPtr这个成员变量。这个函数也就是我们之前写在Block中的函数内容:

c++ 复制代码
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

            printf("123");
        }

这时候我们看一下使用blk()的部分:

c++ 复制代码
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

实际上代表这部分代码:

c++ 复制代码
(*blk->impl.FuncPtr)(blk);

这就是简单地使用函数指针调用函数。正如我们刚才所确认的,由Block 语法转换的_main_block_ func_0 函数的指针被赋值成员变量 FuncPtr 中。另外也说明了,_main_block func. _0函数的参数__cself 指向Block值。在调用该函数的源代码中可以看出 Block 正是作为参数进行了传递。

这里我们在认识一下之前提到的&NSConcreteStackBlock

这里就是把Block的指针赋值给isa指针,这里其实就是展示出我们的Block其实是一个对象。

这里我们重新回忆一下isa指针。

下面这个代码是类的结构体

c++ 复制代码
struct MyObject {
  Class isa;
  int valo;
  int vall;
}

MyObject类的实例变量val0 和val1 被直接声明为对象的结构体成员。"Objective-C 中由类生成对象"意味着,像该结构体这样"生成由该类生成的对象的结构体实例"。生成的各个对象,即由该类生成的对象的各个结构体实例,通过成员变量isa 保持该类的结构体实例指针。

其实这里就相当于我们:

c++ 复制代码
struct __mian_block_impl_0 {
	void *isa; 
  int Flags;
	int Reserved; 
  void *FuncPtr;
};

这个结构体其实就是一个对象基于objc_object的对象

截获自动变量值

现在我们吧我们之前截获变量的代码重新编译一下:

c++ 复制代码
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int val;
  int fmt;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _val, int _fmt, int flags=0) : val(_val), fmt(_fmt) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int val = __cself->val; // bound by copy
  int fmt = __cself->fmt; // bound by copy

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_rb_0ts7dvhs3zg9fc6kk2nvtlr00000gn_T_main_f22e73_mi_0,val, fmt);
        }

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, const char * argv[]) {
        int val = 10;
        int fmt = 15;
        void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, val, fmt));
        fmt = 20;
        val = 30;
        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

这里我们可以很清楚的看到我们这里已经把对应的一个自动变量,添加到我们的block的结构体中充当一个成员变量了:

c++ 复制代码
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int val;
  int fmt;
};

这里注意Block捕获的变量仅仅限制于我们的一个Block中需要用到的一些变量

这里注意我们这里的Block函数部分:

c++ 复制代码
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int val = __cself->val; // bound by copy
  int fmt = __cself->fmt; // bound by copy

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_rb_0ts7dvhs3zg9fc6kk2nvtlr00000gn_T_main_f22e73_mi_0,val, fmt);
        }

这里我们发现一个事情,就是我们这里的一个我给使用到的成员变量,是通过Block自己这个结构体中之前保存的一个结构来赋值的,这时候我们重新看一下初始化函数:

c++ 复制代码
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _val, int _fmt, int flags=0) : val(_val), fmt(_fmt) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }

这里我们就可以发现这段代码里面我们直接给内部的变量赋值好了

这时候我们前后对比一下就可以发现下面的这个道理:

在转换后的源代码中,截获到__main_ block_impl_O结构体实例的成员变量上的自动变量,这些变量在Block语法表达式之前被声明定义。因此,原来的源代码表达式无需改动便可使用截获的自动变量值执行。

__block说明符

我们之前提到了可以通过__block修饰符来修改Block中捕获的变量。

其实还有另一种解决方式:

那就是采用静态变量,静态全局变量,全局变量着三个变量,这样就可以修改了:

objc 复制代码
int global_val = 1;
static int static_global_val = 2;

int main()
{
    static int static_val = 3;

    void (^blk)(void) = ^{
        global_val *= 1;
        static_global_val *= 2;
        static_val *= 3;
    };

    return 0;
}

然后重新编译一下这段代码:

c++ 复制代码
int global_val = 1;
static int static_global_val = 2;

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *static_val;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_val, int flags=0) : static_val(_static_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int *static_val = __cself->static_val; // bound by copy

        global_val *= 1;
        static_global_val *= 2;
        (*static_val) *= 3;
    }

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()
{
    static int static_val = 3;
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_val));
    return 0;
}

这里的静态全局变量和全局变量还是和之前一样,没有什么区别。但是对于静态变量是又区别的。

c++ 复制代码
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int *static_val = __cself->static_val; // bound by copy

        global_val *= 1;
        static_global_val *= 2;
        (*static_val) *= 3;
    }

这里我们可以发现通过静态变量的指针传递给构造函数并且保存,这种方式是最简单的超出作用于使用变量的方法

接下来我们在看有关于__block这个修饰符的内容:

__block这个说明符主要是用于指定将变量值设置到那个存储域中

先看这段比较简单的代码:

objc 复制代码
int main(int argc, const char * argv[]) {
        __block int val = 10;
        int fmt = 15;
        void (^blk)(void) = ^ {
            val = 12;
            NSLog(@"%ld %ld",val, fmt);
        };
        fmt = 20;
        val = 30;
        blk();
    return 0;
}

然后重新编译下:

c++ 复制代码
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;
  int fmt;
  __Block_byref_val_0 *val; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _fmt, __Block_byref_val_0 *_val, int flags=0) : fmt(_fmt), 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; // bound by ref
  int fmt = __cself->fmt; // bound by copy

            (val->__forwarding->val) = 12;
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_rb_0ts7dvhs3zg9fc6kk2nvtlr00000gn_T_main_de84d3_mi_0,(val->__forwarding->val), fmt);
        }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->val, 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, const char * argv[]) {
        __attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};
        int fmt = 15;
        void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, (__Block_byref_val_0 *)&val, 570425344));
        fmt = 20;
        (val.__forwarding->val) = 30;
        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

这里我们来看一下由__block修饰的变量的代码在哪里:

c++ 复制代码
struct __Block_byref_val_0 {
  void *__isa;
__Block_byref_val_0 *__forwarding;
 int __flags;
 int __size;
 int val;
};

这个val变成了一个结构体,然后val成为了结构体中的某一个数值

这里我们重新看一下这段代码,这段代码是在block中给变量赋值的代码:

c++ 复制代码
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_val_0 *val = __cself->val; // bound by ref
  int fmt = __cself->fmt; // bound by copy

            (val->__forwarding->val) = 12;
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_rb_0ts7dvhs3zg9fc6kk2nvtlr00000gn_T_main_de84d3_mi_0,(val->__forwarding->val), fmt);
        }

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int fmt;
  __Block_byref_val_0 *val; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _fmt, __Block_byref_val_0 *_val, int flags=0) : fmt(_fmt), val(_val->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};//这里是结构体

刚刚在 Block 中向静态变量赋值时,使用了指向该静态变量的指针。而向_ block 变量赋值要比这个更为复杂。Block 的_ main_block_impl_0结构体实例持有指向__block 变量的 _ Block_byref_val_0结构体实例的指针。

__Block_byref _ val _0结构体实例的成员变量_forwarding 持有指向该实例自身的指针。通过成员变量 forwarding 访问成员变量val。(成员变量val是该实例自身持有的变量,它相当于原自动变量。)

这里注意我的__block的变量的结构体并不储存在我们的Block结构体中,这是为了让他多个Block中被使用,所以他只是保留一个指针。

Block的存储域

我们之前提到过我们的Block的类型是:_NSConcreteStackBlock,我们会发现有一些和他类似的类:

  • _ NSConcreteStackBlock(分布在栈上)
  • _NSConcreteGlobalBlock(分布在程序的数据区的位置)
  • NSConcreteMallocBlock(分布在堆的位置)

如果定义一个我们的全局变量的Block:

objc 复制代码
void (^blk)(void) = ^ {
    NSLog(@"Block");
};

该 Block 的类为__NSConcreteGlobalBlock 类。此Block 即该 Block 用结构体实例设置在程序

的数据区域中。因为在使用全局变量的地方不能使用自动变量,所以不存在对自动变量进行截获。由此Block用结构体实例的内容不依赖于执行时的状态,所以整个程序中只需一个实例。因此将 Block 用结构体实例设置在与全局变量相同的数据区域中即可。

只在截获自动变量时,Block 用结构体实例截获的值才会根据执行时的状态变化。例如以下源代码中,虽然多次使用同一个 Block 语法,但每个 for 循环中截获的自动变量的值都不同。

objc 复制代码
typedef int (^blk_t)(int);

for (int rate = 0; rate < 10; ++rate) {
    blk_t blk = ^(int count) { return rate * count; };
}

上面这段代码需要截获自动变量:

objc 复制代码
typedef int (^blk_t)(int);

for (int rate = 0; rate < 10; ++rate) {
    blk_t blk = ^(int count) { return count; };
}

上面这段代码则不需要截获自动变量。

这里也就是说如果不需要截获自动变量的话,可以把Block用结构体实例放在程序的数据区域。

所以只有两种情况下是_NSConcreteGlobalBlock(分布在程序的数据区的位置):

  • 记述全局变量的地方有Block语法
  • Block语法的表达式中不使用应截获的自动变量的

初次之外,生成Block的都是_ NSConcreteStackBlock。最后我吗现在再来讨论堆上的Block

这里Block就用姜Blcok拷贝到堆上的方法来解决这个问题,解决这给__block变量被废弃的问题。

这里我们主要认识一下不同Blockcopy的一个差异:

以及注意下某些情况需要我们自己手动复制一下栈上的Block否则这个Block就会被释放掉,这里就注意一个点:在向方法和函数中传递Block的时候,要自己手动复制一下,除了以下的方法不需要我们进行手动复制:

  • Cocoa 框架的方法且方法名中含有 usingBlock 等时
  • Grand Central Dispatch 的 API

其他方法我们记得要去手动复制一下Block。

__block变量存储域

我们在上面提到一个复制的时候,一个Block使用__block变量的代码部分,我们现在看一下这个图:

这其实就是正常的一个拷贝的过程

如果有多个Block使用这种变量的时候,那么就是看一下下面这张图

在多个 Block 中使用_block 变量时,因为最先会将所有的Block 配置在栈上,所以__block

变量也会配置在栈上。在任何一个 Block 从栈复制到堆时,_block 变量也会一并从栈复制到堆并被该 Block 所持有。当剩下的 Block 从栈复制到堆时,被复制的Block 持有_block 变量,并增加

block变量的引用计数。

如果配置在堆上的Block被释放,那么我们这里的__block变量也会被释放

其实这就和正常的引用计数一样,引用计数为0就自动释放了。

这里我们在重新认识一下我们的forwarding指针的作用,前面提到了forwording指针是为了让我们的__block变量的值可以一直被访问到

截获对象

这里我们看一下这段OC代码:

objc 复制代码
int main(int argc, const char * argv[]) {
    
    NSMutableArray* ary = [NSMutableArray array];
        void (^blk)(id object) = [^(id object) {
            [ary addObject:object];
            NSLog(@"%ld", ary.count);
        } copy];
    blk([[NSMutableArray alloc] init]);
    blk([[NSMutableArray alloc] init]);
    blk([[NSMutableArray alloc] init]);
    return 0;
}

这段代码的输出结果是:

objc 复制代码
1
Type: Notice | Timestamp: 2025-02-19 19:02:59.452110+08:00 | Process: BStudy | Library: BStudy | TID: 0x28bf361
2
Type: Notice | Timestamp: 2025-02-19 19:02:59.452182+08:00 | Process: BStudy | Library: BStudy | TID: 0x28bf361
3
Type: Notice | Timestamp: 2025-02-19 19:02:59.452235+08:00 | Process: BStudy | Library: BStudy | TID: 0x28bf361

这一结果意味着这里NSmutablearray并没有被释放,说明array超出变量作用域而存活

c 复制代码
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSMutableArray *__strong ary;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSMutableArray *__strong _ary, int flags=0) : ary(_ary) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself, __strong id object) {
  NSMutableArray *__strong ary = __cself->ary; // bound by copy

            ((void (*)(id, SEL, ObjectType  _Nonnull __strong))(void *)objc_msgSend)((id)ary, sel_registerName("addObject:"), (id)object);
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_rb_0ts7dvhs3zg9fc6kk2nvtlr00000gn_T_main_2edec4_mi_0, ((NSUInteger (*)(id, SEL))(void *)objc_msgSend)((id)ary, sel_registerName("count")));
        }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->ary, (void*)src->ary, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->ary, 3/*BLOCK_FIELD_IS_OBJECT*/);}

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, const char * argv[]) {
    NSMutableArray* ary = ((NSMutableArray * _Nonnull (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("array"));
        void (*blk)(id object) = (void (*)(__strong id))((id (*)(id, SEL))(void *)objc_msgSend)((id)((void (*)(__strong id))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, ary, 570425344)), sel_registerName("copy"));
    ((void (*)(__block_impl *, __strong id))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, ((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("alloc")), sel_registerName("init")));
    ((void (*)(__block_impl *, __strong id))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, ((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("alloc")), sel_registerName("init")));
    ((void (*)(__block_impl *, __strong id))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, ((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("alloc")), sel_registerName("init")));
    return 0;
}

这里我们可以看到这里的Block的结构体:

c++ 复制代码
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSMutableArray *__strong ary;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSMutableArray *__strong _ary, int flags=0) : ary(_ary) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

这里其实是采用了__strong修饰的成员变量。

按照1.3.4节,在 Objective-C 中,C语言结构体不能含有附有__strong 修饰符的变量。因为编译器不知道应何时进行C语言结构体的初始化和废弃操作,不能很好地管理内存。

但是 Objective-C 的运行时库能够准确把握Block 从栈复制到堆以及堆上的Block 被废弃的时机,因此 Block 用结构体中即使含有附有__strong 修饰符或__weak 修饰符的变量,也可以恰当地进行初始化和废弃。为此需要使用在__main_block _dese_0结构体中增加的成员变量 copy和dispose,以及作为指针赋值给该成员变量的_main_block copy_0函数和__main_block_dispose 0函数。

由于在该源代码的Block 用结构体中,含有附有__strong 修饰符的对象类型变量 array,所以需要恰当管理赋值给变量 array 的对象。因此__main_block_copy_0函数使用_Block_object_assign 函数将对象类型对象赋值给 Block 用结构体的成员变量 array 中并持有该对象。

这段代码是引言的最后一段的解释

c++ 复制代码
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->ary, (void*)src->ary, 3/*BLOCK_FIELD_IS_OBJECT*/);}

_Block_object_assign这个方法其实相当于调用retain方法,将对象赋值在对象类型的结构体中,同时有一个释放内存的函数:

c++ 复制代码
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->ary, 3/*BLOCK_FIELD_IS_OBJECT*/);}

这个方法其实是Block在废弃后会调用的函数,用来释放内存。

这些函数分别会在Block复制到堆上被调用,和Block被废弃的时候调用

在下面这种情况下Block会被复制到堆上:

  • 调用 Block 的 copy 实例方法时
  • Block 作为函数返回值返回时
  • 将 Block 赋值给附有 __strong 修饰符 id 类型的类或 Block 类型成员变量时
  • 在方法名中含有 usingBlock 的Cocoa 框架方法或 Grand Central Dispatch 的API 中传递
    Block

在调用 Block 的copy 实例方法时,如果 Block 配置在栈上,那么该Block 会从栈复制到堆。

Block 作为函数返回值返回时、将Block 赋值给附有__strong 修饰符id类型的类或Block 类型成员变量时,编译器自动地将对象的Block作为参数并调用_Block_copy 函数,这与调用Block的 copy 实例方法的效果相同。在方法名中含有 usingBlock 的 Cocoa 框架方法或 Grand CentralDispatch 的API 中传递 Block 时,在该方法或函数内部对传递过来的Block 调用 Block 的 copy 实列方法或者_Block _copy 函数。

也就是说,虽然从源代码来看,在上面这些情况下栈上的Block 被复制到堆上,但其实可归结为_Block_copy 函数被调用时Block 从栈复制到堆。

有了这种构造,通过使用附有__strong 修饰符的自动变量,Block 中截获的对象就能够超出其变量作用域而存在。

这里注意截获变量的结构体虽然大致相同但是还是有区别的:

通过这两个不同的内容来区分捕获的是对象类型还是__block变量。

Block 中使用的赋值给附有_strong 修饰待的自动变量的对象和复制到堆上的_-block 变量由于被堆上的Block 所持有,因而可超出其变量作用域而存在。

只有调用Block_copy 函数才能持有截获的附有__strong 修饰符的对象类型的自动变量值,所以像上面源代码这样不调用_Block_copy 函数的情况下,即使截获了对象,它也会随着变量作用域的结束而被废弃。

因此,Block 中使用对象类型自动变量时,除以下情形外,推荐调用 Block 的 copy 实例方法。

  • Block 作为函数返回值返回时
  • 将Block赋值给类的附有__strong修饰符的id 类型或Block 类型成员变量时
  • 向方法名中含有 usingBlock 的Cocoa 框架方法或Grand Central Dispatch 的API 中传递
    Block 时

__block变量和对象

我们如果用__block来修饰对象的话,我们先看一下这两段代码:

objc 复制代码
int main(int argc, const char * argv[]) {
    __block NSMutableArray* ary = [NSMutableArray array];
        void (^blk)(void) = ^ {
            NSLog(@"1");
        };
        blk();
    return 0;
}

然后转化一下

c++ 复制代码
struct __Block_byref_ary_0 {
  void *__isa;
__Block_byref_ary_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 NSMutableArray *__strong ary;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_rb_0ts7dvhs3zg9fc6kk2nvtlr00000gn_T_main_a8ae80_mi_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)};
int main(int argc, const char * argv[]) {
    __attribute__((__blocks__(byref))) __Block_byref_ary_0 ary = {(void*)0,(__Block_byref_ary_0 *)&ary, 33554432, sizeof(__Block_byref_ary_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((NSMutableArray * _Nonnull (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("array"))};
        void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

这里同样出现了我们之前看到的void (*__Block_byref_id_object_copy)(void*, void*); void (*__Block_byref_id_object_dispose)(void*);这两个函数来把Block拷贝到堆上,以及负责对应的一个释放的操作

在block 变量为附有strong 修饰符的id 类型或对象类型自动变量的情形下会发生同样的

过程。当__block 变量从栈复制到堆时,使用_Block_object_assign函数,持有赋值给__block变量的对象。当堆上的__block 变量被废弃时,使用_Block_object_dispose 函数,释放赋值给__block变量的对象。

由此可知,即使对象赋值复制到堆上的附有__strong 修饰符的对象类型__block 变量中,只要_block 变量在堆上继续存在,那么该对象就会继续处于被持有的状态。这与Block 中使用赋值给附有_strong 修饰符的对象类型自动变量的对象相同。

这里注意 _ _weak和 __block一起联合使用,他还是会在该作用域结束的时候被释放,

Block循环引用

我们经常在Block中会写出有关于循环引用的内容:

objc 复制代码
- (id) init {
	self = [super init];
	blk_ = ^(NSLog(@"self = 8@", self) ;):
	return self;
}

MyObject 类对象的Block 类型成员变量blk_持有赋值为 Block 的强引用。即MyObject 类对象持有 Block。ini 实例方法中执行的 Block 语法使用附有__strong 修饰符的id 类型变量 self。并且由于Block语法赋值在了成员变量blk_中,因此通过 Block 语法生成在栈上的Block 此时由栈复制到堆,并持有所使用的 self。 self持有Block,Block 持有 self。这正是循环引用。如图2-13所示。

这里我们需要用weak变量来修饰一下self。

objc 复制代码
id __weak tmp = self;

在经过这个操作之后我们就可以避免了一个循环引用的问题,这里也可以使用__block变量来避免一个循环引用

objc 复制代码
typedef void (^blk_t)(void);

@interface MyObject : NSObject {
    blk_t blk;
}
@end

@implementation MyObject

- (id)init {
    self = [super init];
    __block id tmp = self;
    blk = ^{
        NSLog(@"self = %@", tmp);
        tmp = nil;
    };
    return self;
}

- (void)execBlock {
    blk();
}

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

int main() {
    id o = [[MyObject alloc] init];
    [o execBlock];
    return 0;
}

这里虽然有一个循环引用的问题,就是在我们不执行- (void)execBlock这个函数的前提下就会出一个循环引用的问题,只有执行了一个函数之后才可以解决循环引用的问题。下面这个图的标优点问题,Block持有__block变量

调用这个函数之后会把__block变量置为nil。

所以为了避免循环引用必须执行那个函数。

小结

主要简单总结了一下之前学习的内容,但是对于Block之后还有继续学习,而且这段代码看的也比较迷茫,还需要多加整理复习

相关推荐
我要最优解6 小时前
关于在mac中配置Java系统环境变量
java·flutter·macos
a小胡哦7 小时前
Windows、Mac、Linux,到底该怎么选?
linux·windows·macos·操作系统
獨枭7 小时前
如何在 Mac 上安装并配置 JDK 环境变量
java·macos·jdk
OKXLIN19 小时前
IOS UITextField 无法隐藏键盘问题
ios·objective-c
Macdo_cn1 天前
My Metronome for Mac v1.4.2 我的节拍器 支持M、Intel芯片
macos·音视频
AL.千灯学长1 天前
DeepSeek接入Siri(已升级支持苹果手表)完整版硅基流动DeepSeek-R1部署
人工智能·gpt·ios·ai·苹果vision pro
吹泡泡的派大星1 天前
从0-1搭建mac环境最新版
macos
zhouwu_linux1 天前
MT7628基于原厂的SDK包, 修改ra1网卡的MAC方法。
linux·运维·macos
丁总学Java1 天前
在 macOS 的 ARM 架构上按住 Command (⌘) + Shift + .(点)。这将暂时显示隐藏文件和文件夹。
macos