阅读 574

Block 原来你是这样的(二)

本文依旧是对 iOS与OSX多线程和内存管理 的整理。

上一篇 Block 原来你是这样的(一) 介绍了什么是 Block、分析了 Block 如何截获自动变量、更改 Block 存在的变量值的方法,上篇遗留了三个问题:

  • __block 说明符修饰的变量 Clang 后是什么样;(第一大节已说明)
  • 为什么截获的自动变量可以超出其作用域;(2.4 节说明了)
  • self 什么时候会引起循环引用。(2.7 节说明了)

本篇内容将对这些内容进行说明。

1、__block 说明符

先看代码,再 Clang

int main(int argc, const char * argv[]) {
    // 这里添加了 __block
    __block int a = 10;
    void(^blk)(void) = ^{ a = 11; };
    blk();
    return 0;
}
复制代码

Clang后:

struct __Block_byref_a_0 {
    void *__isa;
    __Block_byref_a_0 *__forwarding;
    int __flags;
    int __size;
    int a;
};

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

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) = 11;
    
}

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, const char * argv[]) {
    
    __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};
    void(*blk)(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 *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}
复制代码

Clang 后的代码有点多了,分解看。

1、分析 __block 带来的变化

对比之前的代码发现:

  • 增加了 __Block_byref_a_0 结构体;
  • 增加了 __main_block_copy_0 函数;
  • 增加了 __main_block_dispose_0 函数;
  • __main_block_impl_0 截获的成员变量由 int a;__Block_byref_a_0 *a

2、__Block_byref_a_0 结构体

查看 __Block_byref_a_0 结构体,分析初始化代码:

__Block_byref_a_0 a = 
{
    (void*)0,
    (__Block_byref_a_0 *)&a, 
    0, 
    sizeof(__Block_byref_a_0), 
    10
};

struct __Block_byref_a_0 {
    void *__isa;
    __Block_byref_a_0 *__forwarding;
    int __flags;
    int __size;
    int a;
};
复制代码
  • isa:赋值为0,这里没有给类的地址;
  • __forwarding:将结构体自己的指针地址保存;
  • __flags:标志位;
  • __size:结构体大小;
  • a:保存 int a 原本的值。

3、__block 后函数调用区别

分析调用和没有 __block 说明符的代码有什么区别:

/// 没有 __block 说明符
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

    /// 直接使用__cself 获取 a 的值
    int a = __cself->a; 
}

/// 有 __block 说明符
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    /// 取出 __Block_byref_a_0 结构体
    __Block_byref_a_0 *a = __cself->a; 
    
    /// 结构体 a 取 __forwarding 再取 a
    (a->__forwarding->a) = 11;
}
复制代码

由上方结构体代码看到,没有 __block 说明符的 int a 是由 __main_block_impl_0 截获了,但是添加了 __block 说明符后,__main_block_impl_0 截获 __Block_byref_a_0 结构体指针,再由 __Block_byref_a_0 结构体保持 int a 的值,那为什么这么做呢?(1.4小节说明)

再看 int a 的取值,就会还有疑问,如果正常取值,我们应该 a->a(结构体 __Block_byref_a_0 a 直接取 int a的值),但是这里却是 int a = a->__forwarding->a;,这是为什么呢?(2大节说明)

image.png

4、__Block_byref_a_0 存在的原因

__Block_byref_a_0 结构体保持 int a 的值是因为 int a 可能由多个 Block 改动,看一段代码:

int main(int argc, const char * argv[]) {
    // 这里添加了 __block
    __block int a = 10;
    
    void(^blk)(void)  = ^{ a = 11; };
    void(^blk1)(void) = ^{ a = 12; };
    
    blk();
    blk1();
    
    return 0;
}
复制代码

Clang,主要代码如下:

__Block_byref_a_0 a = {0, &a, 0, sizeof(__Block_byref_a_0), 10};

blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &a, 570425344));

blk1 = &__main_block_impl_1(__main_block_func_1, &__main_block_desc_1_DATA, &a, 570425344));
复制代码

Block blkBlock blk1 都使用了 __Block_byref_a_0 结构体实例指针,这样一来就可以多个 Block 使用同一个 __block 变量,方便了值的改动,也节省了内存。

2、Block 的存储域

通过之前的说明可知 Block 也是 Objective-C 对象。将 Block 当作 Objective-C 对象来看时,,该 Blockisa 赋值为 __NSConcreteStackBlock (栈),也就是说 Block 的类为__NSConcreteStackBlock,但是 Block 不仅仅只有栈上的。

1、Block 类型

Block 总共有3种类型:

类                   设置对象的存储域         
__NSConcreteStackBlock
__NSConcreteGlobalBlock程序的数据区域(.data区)
__NSConcreteMallocBlock

应用程序内存分配如下图:

image.png

到现在为止出现的 Block 例子使用的都是 __NSConcreteStackBlock 类,且都设置在栈上。但实际上并非全是这样,在声明全局变量的地方使用 Block 时,生成的 Block__NSConcreteGlobalBlock 类对象。例如:

void(^blk)(void)  = ^{ printf("Global Block\n"); };
int main(int argc, const char * argv[]) {

}
复制代码

Clang 后:

__main_block_impl_1 -> impl -> isa = &__NSConcreteGlobalBlock;
复制代码

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

在使用 Block 的时候只要 Block 不截获自动变量,无论是否在使用全局变量的地方使用 Block 都会将 Block 的结构体实例设置在程序的数据区域。

虽然通过 Clange 转换的源代码通常是 __NSConcreteStackBlock 类对象,但是实际上却有不同。总结如下:

  • 使用全局变量的地方有 Block 语法时;
  • Block 语法表达式中不截获自动变量时。

以上这些情况,Block__NSConcreteGlobalBlock 类对象。即 Block 配置在程序的数据区域中。除此之外的 Block__NSConcreteStackBlock 类对象,且设置在栈上。

但是 __NSConcreteMallocBlock 是何时使用的呢?这个就和 Block 的作用域有关了。

2、Block 作用域

配置在全局变量上的 Block 在变量作用域外也可以通过指针安全的使用。但是设置在栈上的 Block ,如果其所属的变量作用域结束,该 Block 也就被废弃。由于 __block 变量也配置在栈上,同样地,如果其所属的变量作用域结束,则该 __block 变量也会被废弃。如下图所示:

image.png

为了解决这个问题,Block 提供了将 Block__block 变量从栈上复制到堆上的方法来解决这个问题。将栈上的 Block 复制到堆上,这样即使 Block 语法记述的变量作用域记述,堆上的 Block 还可以继续存在。如下图所示:

image.png

复制到堆上的 Block 的成员变量 isa 将会设置为 __NSConcreteMallocBlock。那么 Block 是如何复制的呢?

3、Block 复制到堆上

实际上当 ARC 有效的时候,大多情况下编译器会恰当的判断,自动生成将 Block 从栈上赋值到堆上的代码。看一下下面的代码:

typedef int (^blk_t)(int);
blk_t func(int rate);

int main(int argc, const char * argv[]) {
    blk_t blk = func(10);
    int result = blk(3);
    printf("%d",result);
}

blk_t func(int rate) {
    return ^(int count){ return rate * count; };
}
复制代码

上述代码在 C 语言下,编译器会报错的,说不能返回一个栈上的 Block

image.png

但是在 OC 语言 ARC 模式下是没有问题的,验证了当前情况下编译器会自动生成将 Block 从栈上赋值到堆上的代码。Clang 一下 (记得加上 ARC ,不然会报错的 clang -fobjc-arc -rewrite-objc main.m -o main.cpp):

blk_t func(int rate) {
    return ((int (*)(int))&__func_block_impl_0((void *)__func_block_func_0, &__func_block_desc_0_DATA, rate));
}
复制代码

这里书上说通过 ARC 编译器就可以转换成下方代码,但是我试过了,还是上方代码,所以个人猜测是因为书籍过时了。

/// 书上的代码,仅做参考
blk_t func(int rate) 
{
    blk_t tmp = &_func_block_impl_0(__func_block_func_0, &__func_block_desc_0_DATA, rate);
    tmp = objc_retainBlock(tmp);
    return objc_autoreleaseReturnValue(tmp);
}
复制代码

后来问了一下别人,别人说:returnBlock 在被赋值变量的时候 copy

上方 Clang 的结果显示,并没有在编译的时候调用 copy,由以下2点论证 returnBlock 在被赋值变量的时候 copy

  • 不调用 func 函数,直接运行:结果控制台并没有打印 Block 拷贝
  • 调用 func 函数,执行 return Block,结果如下:

image.png

image.png

image.png

上方程序执行调用了 objc_retainBlock 也就是 Block_copy 方法,此时就会把栈上的 Block 拷贝到堆上。

"大多情况下编译器会恰当的判断,并自动拷贝",那什么时候编译器不能进行判断的?

目前根据这位大佬的示例测试如下:

  • 作为变量:
    • 赋值给一个普通变量之后就会被 copy 到堆上
    • 赋值给一个 weak 变量不会被 copy
  • 作为属性:
    • 用 strong 和 copy 修饰的属性会被 copy 到堆上
    • 用 weak 和 assign 修饰的属性不会被 copy
  • 函数传参:
    • 作为参数传入函数会被 copy 到堆上 (这里和之前测试结果不同了。他的测试结果为不 copy ,我的测试结果为 copy,可能他的博客太早了)
    • 作为函数的返回值会被 copy 到堆上

copy 方法进行复制的动作总结如下表:

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

4、自动变量复制到堆上

Block 从栈上复制到堆上, 那么 Block 截获的自动变量肯定也需要复制到堆上,否则就会出现 BAD_ADDRESS 或者取值不对情况。接下来看一段代码:

typedef void (^blk_t)(id obj);
int main(int argc, const char * argv[]) {
    blk_t blk;
    
    /// 作用域编号1(方便下方描述)
    {
       NSMutableArray *array = [NSMutableArray array];
       blk = ^(id obj) {
           [array addObject:obj];
           NSLog(@"%d\n",array.count);
        };
    }
   
    blk([NSObject alloc]);
    blk([NSObject alloc]);
    blk([NSObject alloc]);
    blk([NSObject alloc]);
}
复制代码

我们知道,一个变量的生命周期是根据其所属的作用域而定的,那么不使用 Block 的情况下 NSMutableArray *array 的作用域为上方代码中标识的作用域编号1,出了作用域编号1 NSMutableArray *array对象就会被销毁,但是上方代码却能正常执行。

Clang 一下看看:(简化了一些代码)

typedef void (*blk_t)(id obj);

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    
    /// __strong 指向了
    NSMutableArray *__strong array;
    
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSMutableArray *__strong _array, int flags=0) : array(_array) {
        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 obj) {
    NSMutableArray *__strong array = __cself->array; // bound by copy
    
    [array addObject:obj];
    NSLog(@"%d\n",array.count);
}

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) 
{
    _Block_object_assign((void*)&dst->array, (void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

static void __main_block_dispose_0(struct __main_block_impl_0*src) 
{
    _Block_object_dispose((void*)src->array, 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[]) {
    blk_t blk;
    {
        NSMutableArray *array = [NSMutableArray array];
        blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, array, 570425344);
    }
    (*blk->FuncPtr)(blk, [NSObject alloc]);
    (*blk->FuncPtr)(blk, [NSObject alloc]);
    (*blk->FuncPtr)(blk, [NSObject alloc]);
    (*blk->FuncPtr)(blk, [NSObject alloc]);
    return 0;
}
复制代码

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

但是 OC 运行时库能够准确的把握 Block 从栈复制到堆以及堆上的 Block 被废弃的时机,因此 Block 用结构体中即使含有 __strong__weak 修饰符的变量,也可以恰当地进行初始化和废弃。

所以需要在 __main_block_desc_0 机构体中增加成员变量 copydispose ,以及作为指针赋值给改成员变量的 __main_block_copy_0 函数和 __main_block_dispose_0 函数。

由于源代码中,含有 __strong 修饰符的对象类型变量 array ,所以需要恰当管理赋值给变量 array 的对象。

因此需要 __main_block_copy_0 函数使用 _Block_object_assign 函数将对象类型的对象赋值给 Block 的结构体的成员变量 array 并持有该对象。 _Block_object_assign 函数相当于 retain ,将对象赋值在对象类型的结构体成员变量中。

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

另外,__main_block_dispose_0 函数使用 _Block_object_dispose 函数,释放赋值在 Block 的结构体成员变量 array 中的对象。_Block_object_dispose 函数相当于 release ,释放赋值在 Block 的结构体成员变量 array 中的对象。

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

但是发现 copydispose 函数并没有在 Clang 后的代码调用。其实它们实在 Block 从栈复制到堆上以及Block 从堆上销毁的时候调用的。

函数                 调用时机                    
copy 函数栈上的 Block 复制到堆上时
dispose 函数堆上的 Block 被废弃时

有了这两个方法截获的自动变量就可以超出其作用域使用了,对于 __block 的变量也是一样的。

_Block_object_assign_Block_object_dispose最后一个参数:

  • BLOCK_FIELD_IS_OBJECT:自动变量为对象时;
  • BLOCK_FIELD_IS_BYREF :自动变量为 __block 时;

注:int a = 10:不经过 __block 修饰的常量在常量区,随时可用。

5、__block 变量存储域

上述已经对 Block 和不含有 __block 修饰符的自动变量进行了说明,本小节就对 __block 变量存储域说明:

image.png

若在 1 个 Block 中使用 __block 变量,则当该 Block 从栈复制到堆时,使用的所有 __block 变量也必定配置在栈上。这些 __block 变量也全部被从栈复制到堆。此时,Block 持有 __block 变量。即使在该 Block 已复制到堆的情形下,复制 Block 也对所使用的 __block 变量没有任何影响。如下图所示。

image.png

在多个 Block 中使用 __block 变量时,因为最先会将所有的 Block 配置在栈上,所以 __block 变量也会配置在栈上。在任何一个 Block 从栈复制到堆时,__block 变量也会一并从栈复制到堆,并被该Block 所持有。当剩下的 Block 从栈复制到堆时,被复制的 Block 持有 __block变量, 并增加 __block 变量的引用计数。如下图所示。

image.png

如果配置再堆上的 Block 被废弃,那么它所使用的 __block 变量也就会被释放。如下图所示。

image.png

到这里,Block 的方式和 OC 的引用计数的内存管理完全相同。

6、__forwarding 取值

明白了 __block 变量的存储域之后,现在再看 __forwarding 变量存在的原因。

“不管上block 变量配置在栈 上还是在堆上,都能够 正确地访问该变量”。正如这句话所述,通过 Block 的复制,__block 变量也从栈复制到堆。此时可以同时访问栈上的 __block 变量和堆上的 __block 变量。

看一段代码:

__block int val = 0;

void (^blk)(void) = ^{ ++val; };

++val;

blk();

NSLog(@"%d", val);
复制代码

利用 copy 方法复制使用了 __block 变量的 Block 语法。Block__blok 变量两者均是从栈 复制到堆。此代码中在 Block 语法的表达式中使用初始化后的 __block 变量。

^{++val;}
复制代码

然后在 Block 语法之后使用与 Block 无关的变量。

++val;
复制代码

以上两种源代码均可转换为如下形式:

++(val.__forwarding->val);
复制代码

在变换 Block 语法的函数中,该变量 val 为复制到堆上的 __block 变量用结构体实例,而使用的与Block 无关的变量 val,为复制前栈上的 __block 变量用结构体实例。

但是栈上的 __block 变量用结构体实例在 __block 变量从栈复制到堆上时,会将成员变量 __forwarding 的值替换为复制目标堆上的 __block 变量用结构体实例的地址。如图下图所示。

image.png

通过该功能,无论是在 Block 语法中、Block 语法外使用 __block 变量, 还是 __block 变量配置在栈上或堆上,都可以顺利地访问同一个 _block 变量。

7、Block 循环引用

先看代码再分析:

typedef void (^blk_t)();

@interface TestClass : NSObject
{
    blk_t _blk;
    id _obj;
}

@end

@implementation TestClass

- (instancetype)init
{
    self = [super init];
    if (self) {
        _blk = ^{
            /// 标注点 1
            NSLog(@"_obj = %@",_obj);
            
            /// 上面代码使用编译器 fix 后会添上 self
            // NSLog(@"_obj = %@",self->_obj);
        };
    }
    return self;
}

@end
复制代码

上方的代码,在 标注点 1 的位置会有 2 个警告:

  • Block implicitly retains 'self'; explicitly mention 'self' to indicate this is intended behavior:提示开发者有隐式 self
  • Capturing 'self' strongly in this block is likely to lead to a retain cycle :提示开发者会造成循环引用而无法释放。

经过之前的分析知道,__main_block_impl_0 *__cself 会捕获 self__cself是结构体本身,self 又持有 _blk,所以造成循环引用了。

再看一个例子:

typedef void (^blk_t)();

@interface TestClass : NSObject
{
    blk_t _blk;
   
}
@end

@implementation TestClass

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

- (void)execBlock
{
    _blk();
}

- (void)dealloc
{
    NSLog(@"我想销毁");
}
@end

int main()
{
    id o = [TestClass alloc];
    [o execBlock];
    
    return 0;
}
复制代码

上方代码正常调用就不会引起循环引用,但是如果不执行 [o execBlock] 代码就会发生循环引用。

  • TestClass 的对象 o 持有 Block _blk
  • Block _blk 持有 __block 变量。
  • __block 变量持有 TestClass 的对象 o

image.png

如果正常调用,就因为 tmp = nil; 断开循环引用。

image.png

所以到这里,我相信你就想明白为什么 [UIView animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations] 这种系统库不会造成循环引用了吧。

3、结语

Block 的理论内容就到这里了,是对 iOS与OSX多线程和内存管理 第二章的内容整理,主要也是方便自己翻阅。

如果你发现这些内容对你有用,感谢点个赞。有问题欢迎指出,共同学习,一起进步。

文章分类
iOS
文章标签