《Objective-C高级编程》Blocks

《Objective-C高级编程:iOS与OS X多线程和内存管理》

2.1 Blcoks概要

2.1.1 什么是Blocks

Blocks是C语言的扩充功能——“带有自动变量(即局部变量)的匿名函数”。

使用Blocks可以不声明C++和Objective-C类,也没有使用静态变量、静态全局变量或全局变量时的问题,仅用编程C语言函数的源代码量即可使用带有自动变量值的匿名函数。

2.2 Blocks模式

2.2.1 Block语法

  • Block常量表达式:

图片来自:《Objective-C高级编程:iOS与OS X多线程和内存管理》

🌰:

^ int (int count){
        return count +1;
    };
  • 省略返回值类型的Block常量表达式:

图片来自:《Objective-C高级编程:iOS与OS X多线程和内存管理》

省略返回值类型时,如果表达式有return语句就返回该返回值的类型。如果表达式没有return语句就返回void类型。如果表达式有多个return语句,所有的return语句返回值的类型必须相同。
🌰:

^ int (int count){
        return count +1;
    };

省略int类型返回值:

^ (int count){
        return count +1;
    };

🌰:

^ void (int count){
        count +1;
        NSLog(@"%@",count+1);
    };

省略void类型:

^ (int count){
        count +1;
        NSLog(@"%@",count+1);
    };
  • 省略返回值类型和参数列表的Block常量表达式:

图片来自:《Objective-C高级编程:iOS与OS X多线程和内存管理》

如果不使用参数,参数列表也可省略。
🌰:

^ void (void){
        NSLog(@"Hello world");
    };

省略void类型:

^ {
        NSLog(@"Hello world");
    };

2.2.2 Block类型变量

在Block语法下,可将Block语法赋值给声明为Block类型的变量中(即源代码中一旦使用Block语法就相当于生成了可赋值给Block类型变量的“值”)。
🌰:

int (^blk) (int);
“Block”即指源代码中的Block语法,也指由Block语法所生成的值。
  • 使用Block语法将Block赋值为Block类型变量。

🌰:

int (^blk) (int) = ^ int (int count){
        return count +1;
    };
  • 由Block类型变量向Block类型变量赋值。

🌰:

int (^blk1) (int) = blk;
int (^blk2) (int);
 blk2 = blk1;
  • Block类型变量作为函数参数传递。

🌰:

-(void)blockFunc:(int (^)(int))blk;
  • Block类型变量作为函数返回值返回。

🌰:

-(int)blockFunc1{
    int (^blk) (int)  = ^(int count){
        return count +1;
    };
    return blk(1);
}
//等同于:
-(int)blockFunc1{
    return ^(int count){
        return count +1;
    }(1);
}
  • Block类型变量作为函数参数和返回值时,可以通过typedef为Block类型提供别名,从而起到简化块类型变量名的作用。

🌰:

typedef int (^BLK) (int);//
-(void)blockFunc:(BLK)blk;//作为函数参数
-(int)blockFunc1{
    BLK blk = ^(int count){
        return count +1;
    };
    return blk(1);
}//作为返回值

2.2.3 截获自动变量值

Blocks中,Block常量表达式会截获所使用的自动变量的值(即保存该自动变量的瞬间值),从而在执行块时使用。
🌰:

{
    int val = 10;
    void (^blk)() = ^{
       NSLog(@"%d",val);
    };
    val = 2;
    blk();
}//输出为10;不是2;

2.2.4 __block说明符()

使用附有 __block说明符的自动变量可在Block中赋值该变量称为 __block 变量。__block说明符也被称之为存储类型修改符。
🌰:

{
   __block int val = 10;
    void (^blk)() = ^{
        var = 1;
       NSLog(@"%d",val);
    };
    val = 2;
    blk();
}//输出为1;不是2或者10;

2.2.5 截获的自动变量

  • 如果给Block中截获的自动变量赋值,需要给截获的自动变量附加__block说明符。
  • 截获Objective-C对象,调用变更该对象的方法并不会产生编译错误,但是,向截获的自动变量(即所截获的Objective-C对象)赋值则会产生错误。
  • 在使用C语言数组时,Block中的截获自动变量的方法并没有实现对C语言数组的截获,需要通过指针实现对C语言数组自动变量的截获。

🌰:

const char *text = "hello";
void (^blk)(void) = ^{
printf("%c\n", text[2]);
};
blk();

2.3 Blocks的实现

2.3.1 Block的实质

Block实质是Objective-C对闭包的对象实现,简单说来,Block就是对象。
将Objective-C的代码转化为C++的代码来理解Block的实现。
Objective-C 转 C++的方法:
- 在OC源文件block.m写好代码。
- 打开终端,cd到block.m所在文件夹。
- 输入clang -rewrite-objc block.m,就会在当前文件夹内自动生成对应的block.cpp文件。

Objective-C中Block的实现:
int main()
{
    void (^blk)(void) = ^{
        printf("Block\n");
    };
    blk();
    return 0;
}
转化为C++代码:
// 结构体 __block_impl
struct __block_impl {
    void *isa;
    int Flags;      // 标志
    int Reserved;   // 今后版本升级所需的区域
    void *FuncPtr;  // 函数指针
};


// block结构体 __main_block_impl_0
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){
        // _NSConcreteStackBlock用于初始化__block_impl结构体的isa成员。(将Block指针赋值给Block的结构体成员变量isa)
        impl.isa = &_NSConcreteStackBlock; 
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};


// 将来被调用的block内部的代码:block值被转换为C的函数代码
// __ceself为指向Block值的变量。
static void __main_block_func_0(struct __main_block_impl_0 *__cself)
{
    printf("Block\n");
}

// 静态结构体 __main_block_desc_0
static struct __main_block_desc_0{
    unsigned long reserved;     // 今后版本升级所需的区域
    unsigned long Block_size;   // Block的大小
} __mian_block_desc_0_DATA = { // 该结构体实例的初始化部分
    0,
    sizeof(struct __main_block_impl_0) // 使用Block(即__main_block_impl_0结构体实例)的大小进行初始化
};

// main函数
int main()
{
    // 调用结构体__main_block_impl_0的构造函数__main_block_impl_0
    void (*blk)(void) =
        (void (*)(void)) & __main_block_impl_0(
            (void *)__main_block_func_0, &__mian_block_desc_0_DATA);

    //调用block
    ((void (*)(struct __block_impl *))(
        (struct __block_impl *)blk)->FuncPtr) ((struct __block_impl *)blk);
    return 0;
}
其中,Objective-C 中Block值转化而来的C++代码为:
// 将来被调用的block内部的代码:block值被转换为C的函数代码
// __ceself为指向Block值的变量。那么,*__cself 就是是指向Block的值的指针,也就相当于是Block的值它自己(OC里的self)。
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

    printf("Block\n");
}
可以看出,Block结构体就是__main_block_impl_0结构体。Block的值就是通过__main_block_impl_0构造出来的。Block结构体的声明:
// block结构体 __main_block_impl_0
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){
        // _NSConcreteStackBlock用于初始化__block_impl结构体的isa成员。(将Block指针赋值给Block的结构体成员变量isa)
        impl.isa = &_NSConcreteStackBlock; 
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};
其中,__main_block_impl_0结构体有三个部分:
  • 第一个是成员变量impl,它是实际的函数指针,它指向__main_block_func_0。impl结构体的声明:
// 结构体 __block_impl
struct __block_impl {
    void *isa;
    int Flags;      // 标志
    int Reserved;   // 今后版本升级所需的区域
    void *FuncPtr;  // 函数指针
};
  • 第二个是成员变量是指向__main_block_desc_0结构体的Desc指针,是用于描述当前这个block的附加信息。
// 静态结构体 __main_block_desc_0
static struct __main_block_desc_0{
    unsigned long reserved;     // 今后版本升级所需的区域
    unsigned long Block_size;   // Block的大小
} __mian_block_desc_0_DATA = { // 该结构体实例的初始化部分
    0,
    sizeof(struct __main_block_impl_0) // 使用Block(即__main_block_impl_0结构体实例)的大小进行初始化
};
  • 第三个部分是__main_block_impl_0结构体的构造函数,__main_block_impl_0 就是该 block 的实现。
 // Block的构造函数
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags = 0){
        // _NSConcreteStackBlock用于初始化__block_impl结构体的isa成员。(将Block指针赋值给Block的结构体成员变量isa)
        impl.isa = &_NSConcreteStackBlock; 
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
在这个结构体的构造函数里:
  • isa指针保持这所属类的结构体的实例的指针。
  • __main_block_imlp_0结构体就相当于Objective-C类对象的结构体
  • _NSConcreteStackBlock相当于Block的结构体实例
也就是说Block实质是Objective-C对闭包的对象实现,简单说来,Block就是对象。
Objective-C类与对象的实质是什么呢?
Objective-C类与对象的实质:
  • Objective-C中的对象就是一个结构体,并且所有的对象都有一个相同的结构体(即Class是类,id是对象)。而且每一个对象都有一个isa指针,这个isa指向生成该对象的类。

  • Objective-C类与对象的实现中最基本的结构体为objc_object结构体和objc_class结构体。其中:

  • id类型是objc_object结构体的指针类型。

typedef struct objc_object {
       Class isa;
} *id;
  • Class是objc_class结构体的指针类型。
typedef struct objc_class *Class;
struct objc_class {
       Class isa;
} ;

通过一个简单的MyObject类来说明Objective-C类与对象的实质:
🌰:

@interface MyObject : NSObject
{
 int val0;
 int val1;
}

基于objc_object结构体,该类的对象的结构体如下:

struct MyObject {
  Class isa; // 成员变量isa持有该类的结构体实例指针
  int val0;  // 原先MyObject类的实例变量val0和val1被直接声明为成员变量
  int val1;
}

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


图片来自:《Objective-C高级编程:iOS与OS X多线程和内存管理》

各类的结构体是基于objc_class结构体的class_t结构体:

struct class_t {
  struct class_t *isa;
  struct class_t *superclass;
  Cache cache;
  IMP *vtable;
  uintptr_t data_NEVER_USE;
}

在Objective-C中,比如NSObject的class_t结构体实例以及NSMutableArray的class_t结构体实例等,均生成并保持各个类的class_t结构体实例。
该实例持有声明的成员变量、方法的名称、方法的实现(即函数指针)、属性以及父类的指针,并被Objective-C运行时库所使用。

2.3.2 截获自动变量值

使用Block的时候,不仅可以使用其内部的参数,还可以使用Block外部的局部变量。而一旦在Block内部使用了其外部变量,这些变量就会被Block保存。
Objective-C代码:
int main()
{
    int dmy = 256;
    int val = 10;

    const char *fmt = "var = %d\n";

    void (^blk)(void) = ^{
        printf(fmt,val);
    };

    val = 2;
    fmt = "These values were changed. var = %d\n";

    blk();

    return 0;
}
转化而成的C++代码:
// 结构体 __main_block_impl_0
struct __main_block_impl_0 {
    // 成员变量
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    const char *fmt; // Block语法表达式“使用的自动变量”被追加到该结构体
    int val;

    // 构造函数
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags = 0) : fmt(_fmt), val(_val) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

// 静态函数 __main_block_func_0
static void __main_block_func_0(struct __main_block_impl_0 *__cself)
{
    const char *fmt = __cself->fmt;
    int val = __cself->val;
    /*
    理解:
    1. __main_block_impl_0结构体实例(即Block)所截获的自动变量在Block语法表达式执行之前就被声明定义,所以,在Objective-C的源代码中,执行Block语法表达式时无需改动便可使用截获的自动变量值。
    2. "截获自动变量值"意味着在执行Block语法时,Block语法表达式所使用的自动变量值被保存到Block的结构体实例(即Block自身)中。
    3. Block不能直接使用“C语言数组类型的自动变量”,所以,截获自动变量时,会将其值传递给结构体的构造函数进行保存
    */

    printf(fmt, val);
}

// 静态结构体 __main_block_desc_0
static struct __main_block_desc_0{
    unsigned long reserved;
    unsigned long Block_size;
} __mian_block_desc_0_DATA = {
    0,
    sizeof(struct __main_block_impl_0)
};

int main()
{
    int dmy = 256;
    int val = 10;
    const char *fmt = "val = %d\n";
    // 调用结构体__main_block_impl_0的构造函数初始化该结构体实例
    void (*blk)(void) = &__main_block_impl_0(__main_block_func_0, &__mian_block_desc_0_DATA, fmt, val);

    return 0;
}
  • 在初始化结构体实例时,会根据传递给构造函数的参数对由自动变量追加的成员变量进行初始化。即执行Block语法使用的自动变量(即截获的自动变量)fmt和val会初始化结构体实例,被作为成员变量追加到了__main_block_impl_0结构体中。
__main_block_impl_0结构体如下:
struct __main_block_impl_0 {
 struct __block_impl impl;
 struct __main_block_desc_0* Desc;
 const char *fmt; //截获的自动变量
 int val;         //截获的自动变量

 __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags=0) : fmt(_fmt), val(_val) {
   impl.isa = &_NSConcreteStackBlock;
   impl.Flags = flags;
   impl.FuncPtr = fp;
   Desc = desc;
 }
};

注意1:block没有使用的自动变量不会被追加,如dmy变量。

注意2: 在初始化block结构体实例时,增加了被截获的自动变量,block的体积会变大。

  • 执行Block语法使用的自动变量fmt和var都是从__cself里面获取的,更说明了二者是属于block的。而且从注释来看(注释是由clang自动生成的),这两个变量是值传递,而不是指针传递,也就是说Block仅仅截获自动变量的值,所以这就解释了即使改变了外部的自动变量的值,也不会影响Block内部的值。
函数体的代码如下:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

 const char *fmt = __cself->fmt; // bound by copy
 int val = __cself->val; // bound by copy
 printf(fmt,val);
}

2.3.3 __block说明符

Block中所使用的被截获的自动变量就如“带有自动变量值的匿名函数”所说,仅截获自动变量的值。Block中使用自动变量后,在Block结构体实例中重写该自动变量也不会改变原先截获的自动变量。
为了在Block中保存值,有两种方案:
  1. 改变存储于特殊存储区域的变量。
  2. 通过__block修饰符来改变。

1. 改变存储于特殊存储区域的变量

  • 全局变量,可以直接访问。
  • 静态全局变量,可以直接访问。
  • 静态变量,直接指针引用。(不适用)
Objective-C具体的实现的代码:
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; // 静态全局变量

// 结构体 __block_impl
struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};

// 结构体 __main_block_impl_0
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;
    }
};

// 静态函数 __main_block_func_0 (Block语法表达式发生的转换)
static void __main_block_func_0(struct __main_block_impl_0 *__cself)
{
    int *static_val = __cself->static_val;

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

    /*
     1. 使用静态变量static_val的指针对静态变量进行访问
     2. 将静态变量static_val的指针传递给__main_block_impl_0结构体的构造函数并保存,这是超出作用域,使用静态变量的最简单方法
     */

}

// 静态结构体 __main_block_desc_0
static struct __main_block_desc_0{
    unsigned long reserved;
    unsigned long Block_size;
} __mian_block_desc_0_DATA = {
    0,
    sizeof(struct __main_block_impl_0)
};

// 主函数,从这里开始阅读源代码
int main()
{
    static int static_val = 3; // 静态变量

    // 调用__main_block_impl_0结构体实例的构造函数,并将静态变量static_val的指针作为参数传递给构造函数
    blk = &__main_block_impl_0(__main_block_func_0, &__mian_block_desc_0_DATA, &static_val);

    return 0;
}
  • 全局变量和全局静态变量没有被截获到block里面,它们的访问是不经过block的(与__cself无关):
// 静态函数 __main_block_func_0 (Block语法表达式发生的转换)
static void __main_block_func_0(struct __main_block_impl_0 *__cself)
{
    int *static_val = __cself->static_val;

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

    /*
     1. 使用静态变量static_val的指针对静态变量进行访问
     2. 将静态变量static_val的指针传递给__main_block_impl_0结构体的构造函数并保存,这是超出作用域,使用静态变量的最简单方法
     */

}
  • 访问静态变量(static_val)时,将静态变量的指针传递给__main_block_impl_0结构体的构造函数并保存:
// 结构体 __main_block_impl_0
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;
    }
};

由上述可知, 超出作用域使用指针访问静态变量的这种方法并不适用于自动变量的访问。

实际上,在由Block语法生成的值Block上,可以存有超过其变量作用域的被截获对象的自动变量。变量作用域结束的同时,原来的自动变量被废弃,因此Block中超过变量作用域而存在的变量,将不能通过指针访问原来的自动变量。

2. 通过__block修饰符来改变。

__block说明符用于指定将变量值设置到哪个存储区域中,也就是说,当自动变量加上__block说明符之后,会改变这个自动变量的存储区域。

__block说明符用来指定Block中想变更值的自动变量,加上__block之后的变量称之为__block变量

Objective-C中__block变量具体的实现的代码:
int main()
{
    __block int val = 10;

    void (^blk)(void) = ^{
        val = 1;
    };
    return 0;
}
转换而成的C++代码:
// 结构体 __Block_byref_val_0
struct __Block_byref_val_0 {
    // 成员变量
    void *__isa;
    __Block_byref_val_0 *__forwarding;
    int __flags;
    int __size;
    int val;  // 相当于原自动变量的成员变量
};

// 结构体 __main_block_impl_0
struct __main_block_impl_0 {
    // 成员变量
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    __Block_byref_val_0 *val; //“持有相当于原自动变量的成员变量”的“__main_block_impl_0结构体实例”被追加到成员变量中

    // 构造函数
    __main_block_impl_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;
    }
};

// 静态函数 __main_block_func_0 (Block语法表达式发生的转换)
static void __main_block_func_0(struct __main_block_impl_0 *__cself)
{
    __Block_byref_val_0 *val = __cself->val;

    (val->__forwarding->val) = 1;
    /*
    1. Block的__main_block_impl_0结构体实例持有指向“__block变量的__Block_byref_val_0结构体实例”的指针(即__Block_byref_val_0 *val)
    2. __Block_byref_val_0结构体实例的成员变量__forwarding持有指向”该实例自身“的指针
    3. 因此,通过__Block_byref_val_0结构体实例的成员变量__forwarding可以访问该结构体实例的成员变量val
    */
}

// 静态函数 __main_block_copy_0
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);
}

// 静态函数 __main_block_dispose_0
static void __main_block_dispose_0(struct __main_block_impl_0*src){
    _Block_object_dispose(src->val, BLOCK_FIELD_IS_BYREF);
}

// 静态结构体 __main_block_desc_0
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*);
} __mian_block_desc_0_DATA = {
    0,
    sizeof(struct __main_block_impl_0),
    __main_block_copy_0,
    __main_block_dispose_0
};

// 主函数
int main()
{
 __attribute__((__blocks__(byref))) __Block_byref_val_0 val = {
        0,
        &val,
        0,
        sizeof(__Block_byref_val_0),
        10
    };
    /*
    1. __block变量会变成__Block_byref_val_0结构体类型的自动变量(即栈上生成的__Block_byref_val_0结构体实例)。
    2. 该自动变量被初始化为10,这个值也出现在结构体实例的初始化中,意味着该结构体持有相当于原自动变量的成员变量。
    */

    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));

    return 0;
}
  • 结构体__main_block_impl_0里面增加了一个成员变量,它是一个结构体指针,指向了__Block_byref_val_0结构体的一个实例。

注意1: __Block_byref_val_0结构体并不在__main_block_impl_0结构体中,目的是为了使得多个Block中使用 __block变量。


注意2:__Block_byref_val_0结构体这个结构体是变量val在被__block修饰后生成的。

结构体__main_block_impl_0的声明:
struct __main_block_impl_0 {
    // 成员变量
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    __Block_byref_val_0 *val; //“持有相当于原自动变量的成员变量”的“__main_block_impl_0结构体实例”被追加到成员变量中

    // 构造函数
    __main_block_impl_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;
    }
};
  • 结构体__Block_byref_val_0两个成员变量需要特别注意:
  1. val:保存了最初的val变量,也就是说原来单纯的int类型的val变量被__block修饰后生成了一个结构体。这个结构体其中一个成员变量持有原来的val变量。
  2. __forwarding:通过__forwarding,可以实现无论__block变量配置在栈上还是堆上都能正确地访问__block变量,也就是说__forwarding是指向自身的。
结构体__Block_byref_val_0的声明:
// 结构体 __Block_byref_val_0
struct __Block_byref_val_0 {
    // 成员变量
    void *__isa;
    __Block_byref_val_0 *__forwarding;
    int __flags;
    int __size;
    int val;  // 相当于原自动变量的成员变量
};
__forwarding成员变量的实现 :
图片来自:《Objective-C高级编程:iOS与OS X多线程和内存管理》
  • 最初,block变量在栈上时,它的成员变量forwarding指向栈上的__block变量结构体实例。

  • 在__block被复制到堆上时,会将forwarding的值替换为堆上的目标block变量用结构体实例的地址。而在堆上的目标block变量自己的forwarding的值就指向它自己。

2.3.4 Block存储域

Block转换为Block的结构体类型的自动变量,__block变量转换为 __block的结构体类型的自动变量。
所谓结构体类型的自动变量,即栈上生成的该结构体的实例。而Block共有三种类型。

Block与__block变量的实质:
图片来自:《Objective-C高级编程:iOS与OS X多线程和内存管理》
Block的类有三种:
图片来自:《Objective-C高级编程:iOS与OS X多线程和内存管理》

注意:在 ARC 开启的情况下,将只会有 NSConcreteGlobalBlock 和 NSConcreteMallocBlock 类型的 block。

三种Block在内存中的位置:
图片来自:《Objective-C高级编程:iOS与OS X多线程和内存管理》

全局Block:_NSConcreteGlobalBlock

Block为_NSConcreteGlobalBlock类对象(即Block配置在程序的数据区域中)的情况有两种:

  • 记述全局变量的地方有Block语法时🌰:
void (^blk)(void) = ^{printf("Global Block\n");};
int main()
{
   blk();
}
这里通过clang转换成的C++代码中Block结构体的声明是:
struct __blk_block_impl_0 {
  struct __block_impl impl;
  struct __blk_block_desc_0* Desc;
  __blk_block_impl_0(void *fp, struct __blk_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteGlobalBlock;//全局
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

Block结构体构造函数里面isa指针被赋予的是&_NSConcreteGlobalBlock,说明它是一个全局Block。

  • Block语法表达式中不使用“应截获的自动变量”时

🌰:

int(^block)(int count) = ^(int count) {
        return count;
    };
 block(2);
这里通过clang转换成的C++代码中Block结构体的声明是:
struct __blk_block_impl_0 {
  struct __block_impl impl;
  struct __blk_block_desc_0* Desc;
  __blk_block_impl_0(void *fp, struct __blk_block_desc_0 *desc, int flags=0) {
    impl.isa = & _NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

Block结构体构造函数里面isa指针被赋予的是& _NSConcreteStackBlock

这里引用巧神的一段话:由于 clang 改写的具体实现方式和 LLVM 不太一样,并且这里没有开启 ARC。所以这里我们看到 isa 指向的还是_NSConcreteStackBlock。但在 LLVM 的实现中,开启 ARC 时,block 应该是 _NSConcreteGlobalBlock 类型

栈Block:_NSConcreteStackBlock

  • 在生成Block以后,如果这个Block不是全局Block,那么它就是为_NSConcreteStackBlock对象。
  • 配置在全局区的block,从变量作用域外也可以通过指针安全地使用。但是设置在栈上的block,如果其作用域结束,该block就被销毁。
图片来自:《Objective-C高级编程:iOS与OS X多线程和内存管理》
  • 同样的,由于__block变量也配置在栈上,如果其作用域结束,则该__block变量也会被销毁。

堆Block:_NSConcreteMallocBlock

但是,如果Block变量和__block变量复制到了堆上以后,则不再会受到变量作用域结束的影响了,因为它变成了堆Block。


图片来自:《Objective-C高级编程:iOS与OS X多线程和内存管理》

将栈block复制到堆以后,block结构体的isa成员变量变成了_NSConcreteMallocBlock。

__block变量的结构体成员变量__forwarding可以实现无论__block变量配置在栈上还是堆上时都能够正确地访问__block变量。
图片来自:《Objective-C高级编程:iOS与OS X多线程和内存管理》
ARC有效时,大多数情况下Block从栈上复制到堆上的代码由编译器实现:
  • block作为函数值返回的时候
typedef int (^blk_t)(int);
blk_t func(int rate)
{
  return ^(int count){return rate * count;};
}

该源代码中的函数会返回配置在栈上的Block。即当程序执行从该函数返回函数调用方时,变量作用域结束,因此栈上的Block也被废弃。但该源代码通过对应ARC的编译器可转换如下:

blk_t func(int rate)
{
  blk_t tmp = &__func_block_impl_0(__func_block_func_0, &__func_block_desc_0_DATA, rate);

  /*
    * ARC有效时,blk_t tmp 相当于blk_t __strong tmp.
   * 将通过Block语法生成的Block(即配置在栈上的Block结构体实例)
   * 赋值给相当于Block类型的变量tmp
   */

  tmp = objc_retainBlock(tmp);

  /*
   * objc_retainBlock实际上是_Block_copy函数
   * 将栈上的Block复制到堆上
   * 复制后,将堆上的地址作为指针赋值给变量tmp
   */

  return objc_autoreleaseReturnValue(tmp);

  /*
   * 将堆上的Block作为Objective-C对象
   * 注册到autoreleasepool中,然后返回该对象
   */
}
  • 部分情况下向方法或函数中传递block的时候
    • Cocoa框架的方法而且方法名中含有usingBlock等时。
  • Grand Central Dispatch 的API。
除了以上情况,需要我们手动复制block。

2.3.5 __block变量存储域

  • 任何一个block被复制到堆上时,__block变量也会一并从栈复制到堆上,并被该Block持有。
图片来自:《Objective-C高级编程:iOS与OS X多线程和内存管理》
图片来自:《Objective-C高级编程:iOS与OS X多线程和内存管理》
  • 如果接着有其他Block被复制到堆上的话,被复制的Block会持有__block变量,并增加block的引用计数。
图片来自:《Objective-C高级编程:iOS与OS X多线程和内存管理》
  • 如果Block被废弃,它所持有的__block也就被释放(不再有block引用它)。
图片来自:《Objective-C高级编程:iOS与OS X多线程和内存管理》

2.3.6 截获对象

首先,我们来看一下生成并持有NSMutableArray类的对象的代码🌰:
{
  id array = [[NSMutableArray alloc] init];
}

变量作用域结束的同时,附有__strong修饰符的变量array被立即释放并废弃

而在block里截获array对象的代码🌰:
blk_t blk;
{
    id array = [NSMutableArray new];
    blk = [^(id object){
        [array addObject:object];
        NSLog(@"array count = %ld",[array count]);

    } copy];
}

blk([NSObject new]);
blk([NSObject new]);
blk([NSObject new]);

其输出:

 array count = 1
 array count = 2
 array count = 3

赋值给变量array的NSMutableArray类的对象在Block的执行部分超出其变量作用域而存在。即array超过了其作用域存在。

通过clang转换成的C++代码中Block结构体的声明:
// 结构体 __main_block_impl_0
struct __main_block_impl_0 {
    // 成员变量
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    id __strong array; 
    // 构造函数
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, id __strong_array, int flags =0) : array(_array){
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

被NSMutableArray类对象并被截获的自动变量array,是附有__strong修饰符的成员变量。在Objective-C中,C语言结构体不能含有附有__strong修饰符的变量。因为编译器不知道何时进行C语言结构体的初始化和废弃操作,不能很好地管理内存。


但是,Objective-C的运行时库能准确把握Block从栈复制到堆以及堆上的Block被废弃的时机,因此Block的结构体即时含有附有__stong修饰符或__weak修饰符的变量,也可以恰当地进行初始化和废弃。

在实现上是通过__main_block_copy_0函数和__main_block_dispose_0函数进行的:
// 静态结构体 __main_block_desc_0
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*);
} __mian_block_desc_0_DATA = {
    0,
    sizeof(struct __main_block_impl_0),
    __main_block_copy_0,
    __main_block_dispose_0
};
  1. __main_block_copy_0函数(copy函数)和__main_block_dispose_0函数(dispose函数)指针被赋值__main_block_desc_0结构体成员变量copy和dispose中,但是在转换后的源代码中,这些函数包括使用指针全都没有被调用。
  1. 而是,在Block从栈复制到堆时以及堆上的Block被废弃时会调用这些函数。
调用copy函数和dispose函数的时机
图片来自:《Objective-C高级编程:iOS与OS X多线程和内存管理》

2.3.7 __block变量和对象

__block可以指定任何类型的自动变量。下面指定用于赋值Objective-C对象的id类型自动变量:

__block id obj = [[NSObject alloc] init];

等同于:

 // ARC有效时,id类型以及对象类型变量默认附加__strong修饰符。
__block id __strong obj = [[NSObject alloc] init];

由clang转换成C++代码:

/* __block变量的结构体部分 */

// 结构体 __Block_byref_obj_0
struct __Block_byref_obj_0 {
    void *__isa;
    __Block_byref_obj_0 *__forwarding;
    int __flags;
    int __size;
    void (*__Block_byref_id_object_copy)(void*, void*);
    void (*__Block_byref_id_object_dispose_)(void*);
    __strong id obj; // __block变量被追加为成员变量
};

// 静态函数 __Block_byref_id_object_copy_131
static void __Block_byref_id_object_copy_131(void *dst, void *src){
    _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}

// 静态函数 __Block_byref_id_object_dispose_131
static void __Block_byref_id_object_dispose_131(void *src){
    _Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}
/* __block变量声明部分 */

__Block_byref_obj_0 obj = {
    0,
    &obj,
    0x20000000,
    sizeof(__Block_byref_obj_0),
    __Block_byref_id_object_copy_131,
    __Block_byref_id_object_dispose_131,
    [[NSObject alloc] init]
};
  • 在Block中使用“附有__strong修饰符的id类型或对象类型自动变量”的情况下:
  • 当Block从栈复制到堆时,使用_Block_object_copy函数,持有Block截获的对象。

  • 当堆上的Block被废弃时,使用_Block_object_dispose函数,释放Block截获的对象。

  • 在__block变量为“附有 __strong修饰符的id类型或对象类型自动变量”的情形下会发生同样的过程。
  • 当__block变量从栈复制到堆时,使用_Block_object_copy函数,持有赋值给__block变量的对象。

  • 当堆上的__block变量被废弃时,使用_Block_object_dispose函数,释放赋值给__block变量的对象。

由此可知:只要__block变量在堆上继续存在,那么该对象就会继续处于被持有的状态。这与在Block中,对象赋值给“附有__strong修饰符的对象类型自动变量”相同。
  • 对象赋值给“附有__weak修饰符的id类型或对象类型__block自动变量”时,__block变量对该对象持有弱引用。此时nil赋值在自动变量上。
  • 在使用附有__unsafe_unretained修饰符的变量时,注意不要通过悬挂指针访问已被废弃的对象,否则程序可能会崩溃!
  • __autoreleasing修饰符与__block说明符同时使用会产生编译错误。

2.3.8 Block循环引用

如果在Block内部使用__strong修饰符的对象类型的自动变量,那么当Block从栈复制到堆的时候,该对象就会被Block所持有。

何时栈上的Block会复制到堆:
  • 调用Block的copy实例方法时
  • Block作为函数返回值返回时
  • 将Block赋值给附有__strong修饰符id类型的类或Block类型成员变量时
  • 在方法名中含有usingBlock的Cocoa框架方法或GCD的API中传递Block时

如果这个对象还同时持有Block的话,就容易发生循环引用。

  • 使用Block类型成员变量和附有__strong修饰符的self出现循环引用
typedef void(^blk_t)(void);

@interface Person : NSObject
{
    blk_t blk_;
}

@implementation Person

- (instancetype)init
{
    self = [super init];
    blk_ = ^{
        NSLog(@"self = %@",self);
    };
    return self;
}

@end

Block blk_t持有self,而self也同时持有作为成员变量的blk_t


图片来自:《Objective-C高级编程:iOS与OS X多线程和内存管理》
  • Block中没有使用self,但是截获了self
@interface MyObject : NSObject
{
    blk_t blk_;
    id obj_;
}
@end

@implementation MyObject
- (id)init
{
    self = [super init];

    blk_ = ^{NSLog(@"obj_ = %@", obj_);};

    return self;
}

Block语法中使用的obj_实际上截获了self,而对编译器来说,obj_只不过是对象的结构体的成员变量。

blk_ = ^{NSLog(@"obj_ = %@", self->obj_);};
有两种方法来解决Block循环引用:
  • 使用__block变量避免循环引用
  • 使用Block类型成员变量和附有__weak修饰符的对象(通常是self)避免循环引用
使用__block变量避免循环引用
typedeft void (^blk_t)(void);

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

@implementation MyObject
- (id)init
{
    self = [super init];

    __block id blockSelf = self;

    blk_ = ^{
          NSLog(@"self = %@", blockSelf);
          blockSelf = nil; // 记得清零
        };

    return self;
}

- (void)execBlock
{
    blk_();
}

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

int main()
{
    id o = [[MyObject alloc] init];

    [o execBlock];

    return 0;
}

如果不调用execBlock实例方法(即不执行赋值给成员变量blk_的Block),便会循环引用并引起内存泄露。


使用__block变量不恰当会出现循环引用

在生成并持有对象的状态下会引起以下循环引用


图片来自:《Objective-C高级编程:iOS与OS X多线程和内存管理》

如果不执行execBlock实例方法,就会持续该循环引用从而造成内存泄露。

通过执行execBlock实例方法,Block被执行,nil被赋值在__block变量blockSelf中,__block变量blockSelf对MyObject类对象的强引用失效,从而避免了循环引用


图片来自:《Objective-C高级编程:iOS与OS X多线程和内存管理》

使用__block变量避免循环引用的优缺点
优点:

  • 通过__block变量可控制对象的持有期间
  • 在不能使用__weak修饰符的环境中使用

缺点:

  • 为避免循环引用必须执行Block
使用Block类型成员变量和附有__weak修饰符的对象(通常是self)避免循环引用
- (id)init
{
    self = [super init];

    __weak __typeof(&*self)weakSelf = self;

    blk_ = ^{NSLog(@"self = %@", weakSelf);};

    return self;
}
图片来自:《Objective-C高级编程:iOS与OS X多线程和内存管理》

若由于Block引发了循环引用时,在ARC有效时,使用__weak修饰符来避免Block中的循环引用。ARC无效时,__block说明符被用来避免Block中的循环引用。

总结

Blocks是“带有自动变量(即局部变量)的匿名函数”。实质是Objective-C对闭包的对象实现,简单说来,Block就是对象。
在Blocks中,Block常量表达式会截获所使用的自动变量的值(即保存该自动变量的瞬间值)。附有 __block说明符的自动变量可在Block中赋值。
栈上的Block会复制到堆时,Block上的__block变量跟着复制到堆上。只要__block变量在堆上继续存在,那么对象就会继续处于被持有的状态。
ARC有效时,大多数情况下Block从栈上复制到堆上的代码由编译器实现:
  • block作为函数值返回的时候
  • 部分情况下向方法或函数中传递block的时候:
  • Cocoa框架的方法而且方法名中含有usingBlock等时。
  • Grand Central Dispatch 的API。
若由于Block引发了循环引用时,在ARC有效时,使用__weak修饰符来避免Block中的循环引用。ARC无效时,__block说明符被用来避免Block中的循环引用。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容