谈block、__weak和__strong

最近在"翻新"公司的老项目的时候,发现一个奇怪的问题:

在一个 block 中,我使用了 RAC 为了避免 block 循环引用而定义的两个宏: @weakify@strongify,但是如果在 block 内部使用下划线属性(成员变量),还是会导致循环引用。

很多人都知道怎么处理这个问题,在使用了@weakify@strongify的情况下,在 block 内部像self -> ivar这样使用成员变量就可以避免循环引用了,但是为什么这样用就没问题呢?使用了@weakify@strongify两个宏之后发生了什么呢?带着你在使用 block 时出现过的疑问,在后面的内容中你可能会得到答案。

block是什么

block 是用于创建匿名函数的 C 语言扩展。用户使用 block 指针与 block 对象进行交互并传输 block 对象,block 指针表示为普通指针。block 可以从局部变量中捕获值;发生这种情况时,必须动态分配内存。初始分配在栈上完成,但 runtime 提供了一个Block_copy函数,给定一个 block 指针,将底层 block 对象复制到堆中,将其引用计数设置为1并返回新的 block 指针,或者(如果 block 对象已经在堆上)将其引用计数增加1.配对函数是Block_release,它将引用计数减少1并在计数达到零并且在堆上时销毁对象。翻译自苹果文档

上面的翻译来自于 谷歌翻译~。我对于 block 的理解就是一个指针,指向一个带有函数指针 (用于执行block内的代码) 的结构体,该结构体内有许多捕获的成员变量。在 ARC 环境下 block 会从 栈中自动复制到堆中,方便 runtime 管理内存生命周期;如果内部有全局变量则复制到数据区,生命周期为程序创建到程序结束。

[站外图片上传中...(image-1d1a52-1561035272667)]

block的数据结构

block 的数据结构定义如下
[站外图片上传中...(image-f5b34b-1561035272667)]

结构体定义:

struct Block_layout {
    void *isa;
    int flags;
    int reserved;
    void (*invoke)(void *, ...);
    struct Block_descriptor *descriptor;
    /* Imported variables. */
};

struct Block_descriptor {
    unsigned long int reserved;
    unsigned long int size;// sizeof(struct Block_layout)
    void (*copy)(void *dst, void *src);
    void (*dispose)(void *);
};

通过它的数据结构,我们知道一个 block 实际上是由5部分组成的

  1. isa 指针,所有对象都有该指针,用于实现对象相关的功能
  2. flags,用于按 bit 位表示一些 block 的附加信息
  3. reserved,保留变量
  4. invoke,函数指针,指向具体的 block 实现的函数调用地址
  5. descriptor, 表示该 block 的附加描述信息,主要是 size 大小,以及 copy 和 dispose 函数的指针

block的几种的类型

常见的 block 有下面三种,不用类型的 block 存放不同的区域,在 ARC 环境下只有_NSConcreteGlobalBlock_NSConcreteMallocBlock两种类型的 block

  • _NSConcreteGlobalBlock 全局的静态 block,不会访问任何外部变量。
  • _NSConcreteStackBlock 保存在栈中的 block,当函数返回时会被销毁。
  • _NSConcreteMallocBlock 保存在堆中的 block,当引用计数为 0 时会被销毁。

下面是详细的介绍

_NSConcreteStackBlock

该类型的 block 仅存在在 MRC 环境中,数据存放在栈区,当函数返回时会被销毁。在 ARC 环境中,不存在_NSConcreteStackBlock类型,只存在_NSConcreteGlobalBlock_NSConcreteMallocBlock 两个类型。在下面的例子中, block 的类型的打印结果是__NSMallocBlock__。原因可能是因为c语言的结构体中,编译器不能很好地管理初始化和销毁,这样对内存管理来说很不方便,所以就将 block 放到堆上,使用 runtime 来管理它们的生命周期。

int val = 1;
    
void(^textBlock)(void) = ^{
    NSLog(@"[block] val<%p>: %d", &val, val);
    NSLog(@"val: %d", val);
};
NSLog(@"val<%p>: %d", &val, val);
textBlock();
NSLog(@"textBlock: %@", textBlock);

打印结果为:

val<0x16b523d1c>: 1
[block] val<0x280a9fcb0>: 1
textBlock: <__NSMallocBlock__: 0x28076a4c0>

下面使用 clang -rewrite-objc filename 将代码转换成 C++ 的实现, 下面是关键部分的代码

struct __MyObject__test_block_impl_0 {
  struct __block_impl impl;
  struct __MyObject__test_block_desc_0* Desc;
  int val;
  __MyObject__test_block_impl_0(void *fp, struct __MyObject__test_block_desc_0 *desc, int _val, int flags=0) : val(_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __MyObject__test_block_func_0(struct __MyObject__test_block_impl_0 *__cself) {
  int val = __cself->val; // bound by copy

    }

static struct __MyObject__test_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __MyObject__test_block_desc_0_DATA = { 0, sizeof(struct __MyObject__test_block_impl_0)};

static void _I_MyObject_test(MyObject * self, SEL _cmd) {
    static int static_v = 1;
    int val = 1;

    void(*textBlock)(void) = ((void (*)())&__MyObject__test_block_impl_0((void *)__MyObject__test_block_func_0, &__MyObject__test_block_desc_0_DATA, val));
    ((void (*)(__block_impl *))((__block_impl *)textBlock)->FuncPtr)((__block_impl *)textBlock);

}

  • 其中__MyObject__test_block_impl_0是 block 的结构体类型
  • __MyObject__test_block_func_0 是 block 实现的函数,在 __MyObject__test_block_impl_0内有一个指针FuncPtr指向该函数
  • __MyObject__test_block_desc_0 是 block 附件描述信息的结构体,包含着 block 结构体大小, copy 和 dispose 函数指针(这两个函数后面后讲到)等的描述信息,在 __MyObject__test_block_impl_0内有一个指针Desc指向该结构体
  • _I_MyObject_test函数内可以看到 block 的初始化,void(*textBlock)(void)说明 textBlock 是一个指向该 block 结构体的指针

首先观察这个__MyObject__test_block_impl_0的结构体:

struct __MyObject__test_block_impl_0 {
  struct __block_impl impl;
  struct __MyObject__test_block_desc_0* Desc;
  int val;
  __MyObject__test_block_impl_0(void *fp, struct __MyObject__test_block_desc_0 *desc, int _val, int flags=0) : val(_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

  • 使用 clang 转换过的实现是 MRC 环境的,所以 isa 指针指向 _NSConcreteStackBlock 类型
  • 在这个结构体中可以看到一个成员变量int val;,没错,它就是 block 捕获的局部变量,从构造函数 __MyObject__test_block_impl_0(void *fp, struct __MyObject__test_block_desc_0 *desc, int _val, int flags=0) : val(_val) 中可以看到,block 仅仅捕获了该变量的值
  • __MyObject__test_block_impl_0中由于增加了一个变量 val,所以结构体的大小变大了,结构体大小被写在了__MyObject__test_block_desc_0
  • block 捕获外部变量仅仅只 block 闭包里面会用到的值,其他用不到的值,它并不会去捕获。

再看一下__MyObject__test_block_func_0这个函数的实现:

static void __MyObject__test_block_func_0(struct __MyObject__test_block_impl_0 *__cself) {
  int val = __cself->val; // bound by copy

    }

我们可以发现,系统自动给我们加上的注释,bound by copy,自动变量 val 虽然被捕获进来了,但是是用 __cself->val 来访问的。block 仅仅捕获了 val 的值,并没有捕获 val 的内存地址。所以在__MyObject__test_block_func_0 这个函数中即使我们重写这个自动变量 val 的值,依旧没法去改变block外面变量 val 的值。

小结一下:
基本数据类型的变量是以值传递方式传递到 block 的构造函数里面去的。block 只捕获 block 中会用到的变量。由于只捕获了自动变量的值,并非内存地址,所以 block 内部不能改变变量的值。

_NSConcreteMallocBlock

修改一下上面的代码:

 __block int val = 1;
void(^textBlock)(void) = ^{
    val++;
    NSLog(@"[block] val<%p>: %d", &val, val);
};
NSLog(@"val<%p>: %d", &val, val);
textBlock();
NSLog(@"val<%p>: %d", &val, val);
NSLog(@"textBlock: %@", textBlock);

打印输出为:

val<0x282db0858>: 1
[block] val<0x282db0858>: 2
val<0x282db0858>: 2
textBlock: <__NSMallocBlock__: 0x2823d3450>

重新用 clang 生成的c++实现

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

struct __MyObject__test_block_impl_0 {
  struct __block_impl impl;
  struct __MyObject__test_block_desc_0* Desc;
  __Block_byref_val_0 *val; // by ref
  __MyObject__test_block_impl_0(void *fp, struct __MyObject__test_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 __MyObject__test_block_func_0(struct __MyObject__test_block_impl_0 *__cself) {
  __Block_byref_val_0 *val = __cself->val; // bound by ref

        (val->__forwarding->val)++;
    }
static void __MyObject__test_block_copy_0(struct __MyObject__test_block_impl_0*dst, struct __MyObject__test_block_impl_0*src) {_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __MyObject__test_block_dispose_0(struct __MyObject__test_block_impl_0*src) {_Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __MyObject__test_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __MyObject__test_block_impl_0*, struct __MyObject__test_block_impl_0*);
  void (*dispose)(struct __MyObject__test_block_impl_0*);
} __MyObject__test_block_desc_0_DATA = { 0, sizeof(struct __MyObject__test_block_impl_0), __MyObject__test_block_copy_0, __MyObject__test_block_dispose_0};

static void _I_MyObject_test(MyObject * self, SEL _cmd) {
    __attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 1};
    void(*textBlock)(void) = ((void (*)())&__MyObject__test_block_impl_0((void *)__MyObject__test_block_func_0, &__MyObject__test_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)textBlock)->FuncPtr)((__block_impl *)textBlock);
}

在重新生成的代码中,我们看到新增了一个名为__Block_byref_val_0的结构体,它是用来替代我们__block修饰的变量 val 的。

  • 它的第一个指针是 isa,说明它也是一个对象。
  • 第二个指针是指向自身类的指针__forwarding
  • 第三个是一个标记 flag
  • 第四个是结构体的大小
  • 第五个是变量 val 的值

在函数static void _I_MyObject_test(MyObject * self, SEL _cmd)我们可以看到该结构体的初始化代码:

    __attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 1};


  • 在初始化时, isa 指向了一个空指针
  • __forwarding指向了自己的地址
  • 1是变量 val 的值。

使用 __block修饰的变量,无论是基本数据类型还是 OC 的类,在编译之后都是转换成一个新的结构体,该结构体的__forwarding指针会指向自己的地址,而成员变量 val 则为编译前的类型和值。至于这样的目的是什么,可以接着看下面。


static struct __MyObject__test_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __MyObject__test_block_impl_0*, struct __MyObject__test_block_impl_0*);
  void (*dispose)(struct __MyObject__test_block_impl_0*);
} __MyObject__test_block_desc_0_DATA = { 0, sizeof(struct __MyObject__test_block_impl_0), __MyObject__test_block_copy_0, __MyObject__test_block_dispose_0};

__MyObject__test_block_desc_0这个结构体中,我们发现比之前的代码多了一个 copydispose 的函数指针。在c语言的结构体中,编译器没有很好地进行初始化和销毁,这样对内存管理来说很不方便,所以就在增加了这两个函数指针,方便进行内存管理。copy函数把block从栈上拷贝到堆上,dispose函数是把堆上的函数在废弃的时候销毁掉。

  • copydispose这两个函数指针对应的两个函数实现
static void __MyObject__test_block_copy_0(struct __MyObject__test_block_impl_0*dst, struct __MyObject__test_block_impl_0*src) {_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __MyObject__test_block_dispose_0(struct __MyObject__test_block_impl_0*src) {_Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}


  • __MyObject__test_block_copy_0函数实现中出现了方法_Block_object_assign,
  • __MyObject__test_block_dispose_0函数实现中出现了方法_Block_object_dispose

下面是这两个方法的申明:

#define Block_copy(...) ((__typeof(__VA_ARGS__))_Block_copy((const void *)(__VA_ARGS__)))
#define Block_release(...) _Block_release((const void *)(__VA_ARGS__))

// Create a heap based copy of a Block or simply add a reference to an existing one.
// This must be paired with Block_release to recover memory, even when running
// under Objective-C Garbage Collection.
BLOCK_EXPORT void *_Block_copy(const void *aBlock)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);

// Lose the reference, and if heap based and last reference, recover the memory
BLOCK_EXPORT void _Block_release(const void *aBlock)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);

// Used by the compiler. Do not call this function yourself.
BLOCK_EXPORT void _Block_object_assign(void *, const void *, const int)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);

// Used by the compiler. Do not call this function yourself.
BLOCK_EXPORT void _Block_object_dispose(const void *, const int)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);

下面是这两个方法的实现:

static void *_Block_copy_internal(const void *arg, const int flags) {
    struct Block_layout *aBlock;
    const bool wantsOne = (WANTS_ONE & flags) == WANTS_ONE;
    
    // 1
    if (!arg) return NULL;
    
    // 2
    aBlock = (struct Block_layout *)arg;
    
    // 3
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // latches on high
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    
    // 4
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;
    }
    
    // 5
    struct Block_layout *result = malloc(aBlock->descriptor->size);
    if (!result) return (void *)0;
    
    // 6
    memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
    
    // 7
    result->flags &= ~(BLOCK_REFCOUNT_MASK);    // XXX not needed
    result->flags |= BLOCK_NEEDS_FREE | 1;
    
    // 8
    result->isa = _NSConcreteMallocBlock;
    
    // 9
    if (result->flags & BLOCK_HAS_COPY_DISPOSE) {
        (*aBlock->descriptor->copy)(result, aBlock); // do fixup
    }
    
    return result;
}

void _Block_release(void *arg) {
    // 1
    struct Block_layout *aBlock = (struct Block_layout *)arg;
    if (!aBlock) return;
    
    // 2
    int32_t newCount;
    newCount = latching_decr_int(&aBlock->flags) & BLOCK_REFCOUNT_MASK;
    
    // 3
    if (newCount > 0) return;
    
    // 4
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        if (aBlock->flags & BLOCK_HAS_COPY_DISPOSE)(*aBlock->descriptor->dispose)(aBlock);
        _Block_deallocator(aBlock);
    }
    
    // 5
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        ;
    }
    
    // 6
    else {
        printf("Block_release called upon a stack Block: %p, ignored\n", (void *)aBlock);
    }
}

  • _Block_copy_internalBlock_copy的一个实现,实现了从_NSConcreteStackBlock复制到_NSConcreteMallocBlock的过程,有9个步骤。
    • 在第8步中我们可以看到 isa 指针指向了_NSConcreteMallocBlock
  • _Block_releaseBlock_release的一个实现,实现了一个block释放的过程,有6个步骤

扯的有点远了,现在让我们总结一下 __block 修饰的变量在block内发生了什么。

  • block 会在栈中被创建,然后通过Block_copy函数复制到堆中。由 runtime 管理它的生命周期
  • 使用 __block 修饰的变量,在编译后会变成一个新的对象。在初始化时,成员变量__forwarding会指向栈中该变量的地址,val 为该变量原本的值。当 block 的成员变量 __Block_byref_val_0从栈中复制到堆中时,成员变量 __Block_byref_val_0的地址可能改变了,但是 __forwarding指针指向的结构体是不会变的,仍然在栈中。
  • block 的实现函数__MyObject__test_block_func_0,block 通过 __Block_byref_val_0 *val = __cself->val;(val->__forwarding->val)++ 变量的地址修改 val,所以在 block 内部修改变量 val 是会影响到 block 外部的变量。
  • 这就是为什么 block 内部和外部 val 的地址不同的原因(一个在栈上,一个在堆上)。因为他们__forwarding指向的结构体是一样的,所以在 block 内部修改变量会影响到外部,

_NSConcreteGlobalBlock

block 内部只用到全局变量,包括全局变量静态全局变量静态变量,以及上述 block 的 copy 版本。数据存放在数据区,生命周期从应用创建到应用结束。

int global_v = 1;
static int static_global_v = 1;

@implementation MyObject

- (void)test
{
    static int static_v = 1;
        
    NSLog(@"val<%p>: %d", &static_v, static_v);
    NSLog(@"global_v<%p>: %d", &global_v, global_v);
    NSLog(@"static_global_v<%p>: %d", &static_global_v, static_global_v);
    
    void(^textBlock)(void) = ^{
        static_v++;
        global_v++;
        static_global_v++;
        NSLog(@"[block] val<%p>: %d", &static_v, static_v);
        NSLog(@"[block] global_v<%p>: %d", &global_v, global_v);
        NSLog(@"[block] static_global_v<%p>: %d", &static_global_v, static_global_v);
    };
    textBlock();
    NSLog(@"textBlock: %@", textBlock);
}

打印信息为:

val<0x1034b8114>: 1
global_v<0x1034b8110>: 1
static_global_v<0x1034b8118>: 1

[block] val<0x1034b8114>: 2
[block] global_v<0x1034b8110>: 2
[block] static_global_v<0x1034b8118>: 2

textBlock: <__NSGlobalBlock__: 0x10343da40>

clang 之后 C++ 实现:

int global_v = 1;
static int static_global_v = 1;


struct __MyObject__test_block_impl_0 {
  struct __block_impl impl;
  struct __MyObject__test_block_desc_0* Desc;
  int *static_v;
  __MyObject__test_block_impl_0(void *fp, struct __MyObject__test_block_desc_0 *desc, int *_static_v, int flags=0) : static_v(_static_v) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __MyObject__test_block_func_0(struct __MyObject__test_block_impl_0 *__cself) {
  int *static_v = __cself->static_v; // bound by copy

        (*static_v)++;
        global_v++;
        static_global_v++;        
    }

static struct __MyObject__test_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __MyObject__test_block_desc_0_DATA = { 0, sizeof(struct __MyObject__test_block_impl_0)};

static void _I_MyObject_test(MyObject * self, SEL _cmd) {
    static int static_v = 1;
}

  • block 仅仅捕获了静态变量 static_v 的地址作为自己的成员变量,因此在内部修改该变量可以影响到 block 外部。block 内部和外部该变量的地址相等
  • 全局变量 global_v 和全局静态变量 static_global_v 并没有被 block 捕获,因为他们已经被保存在数据区中,可以直接使用

由于 clang 改写的方式跟 LLVM 不太一样,在这里并没有开启ARC,所以这里我们看到 isa 指向的还是 _NSConcreteStackBlock,但在开启ARC的时候,block 应该是 _NSConcreteGlobalBlock 类型。

block 与 self

在前面的部分,我们已经分析过 局部变量,静态变量,全局变量,全局静态变量在 block 时的情况,那么,还有一种特殊的变量 self,它在 block 内部时又是怎么样运行的呢?

@interface MyObject () {
    NSString *_age;
}
@property (nonatomic, strong) NSString *name;
@end

@implementation MyObject

- (void)test
{
    
    self.name = @"n";
    _age = @"10";
    NSLog(@"self: %@", self);

    void(^textBlock)(void) = ^{
        self.name = @"a";
        _age = @"11";
        
        NSLog(@"[block] self: %@", self);
        NSLog(@"[block] name<%p>: %@", self.name, self.name);
        NSLog(@"[block] age<%p>: %@", _age, _age);
    };
    NSLog(@"name<%p>: %@", self.name, self.name);
    NSLog(@"age<%p>: %@", _age, _age);
    textBlock();
    NSLog(@"name<%p>: %@", self.name, self.name);
    NSLog(@"age<%p>: %@", _age, _age);
}

打印结果:

name<0x102804818>: n
age<0x102804838>: 10
[block] name<0x102804858>: a
[block] age<0x102804878>: 11
name<0x102804858>: a
age<0x102804878>: 11

clang之后的C++实现

struct __MyObject__test_block_impl_0 {
  struct __block_impl impl;
  struct __MyObject__test_block_desc_0* Desc;
  MyObject *self;
  __MyObject__test_block_impl_0(void *fp, struct __MyObject__test_block_desc_0 *desc, MyObject *_self, int flags=0) : self(_self) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __MyObject__test_block_func_0(struct __MyObject__test_block_impl_0 *__cself) {
  MyObject *self = __cself->self; // bound by copy

        ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)self, sel_registerName("setName:"), (NSString *)&__NSConstantStringImpl__var_folders_xy_nncr8hn96cd0_rdymw2f0qcw0000gn_T_MyObject_2519b6_mi_2);
        (*(NSString **)((char *)self + OBJC_IVAR_$_MyObject$_age)) = (NSString *)&__NSConstantStringImpl__var_folders_xy_nncr8hn96cd0_rdymw2f0qcw0000gn_T_MyObject_2519b6_mi_3;

    }
static void __MyObject__test_block_copy_0(struct __MyObject__test_block_impl_0*dst, struct __MyObject__test_block_impl_0*src) {_Block_object_assign((void*)&dst->self, (void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __MyObject__test_block_dispose_0(struct __MyObject__test_block_impl_0*src) {_Block_object_dispose((void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static struct __MyObject__test_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __MyObject__test_block_impl_0*, struct __MyObject__test_block_impl_0*);
  void (*dispose)(struct __MyObject__test_block_impl_0*);
} __MyObject__test_block_desc_0_DATA = { 0, sizeof(struct __MyObject__test_block_impl_0), __MyObject__test_block_copy_0, __MyObject__test_block_dispose_0};

static void _I_MyObject_test(MyObject * self, SEL _cmd) {

    ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)self, sel_registerName("setName:"), (NSString *)&__NSConstantStringImpl__var_folders_xy_nncr8hn96cd0_rdymw2f0qcw0000gn_T_MyObject_2519b6_mi_0);
    (*(NSString **)((char *)self + OBJC_IVAR_$_MyObject$_age)) = (NSString *)&__NSConstantStringImpl__var_folders_xy_nncr8hn96cd0_rdymw2f0qcw0000gn_T_MyObject_2519b6_mi_1;

    void(*textBlock)(void) = ((void (*)())&__MyObject__test_block_impl_0((void *)__MyObject__test_block_func_0, &__MyObject__test_block_desc_0_DATA, self, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)textBlock)->FuncPtr)((__block_impl *)textBlock);
}

  • __MyObject__test_block_impl_0中我们可以看到self也被 block 捕获成了成员变量
  • __MyObject__test_block_impl_0的构造函数中我们可以看到 self 被当做参数被传入,而不是 self 的地址
  • 因为 block 在内部和外部 self 指向的是相同的 MyObject 结构体,所以在 block 内部对 self 成员变量进行修改会影响到 block 外部
  • block 的结构体会强引用 self,所以需要小心使用,否则会引起循环应用
static void __MyObject__test_block_func_0(struct __MyObject__test_block_impl_0 *__cself) {
  MyObject *self = __cself->self; // bound by copy

((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)self, sel_registerName("setName:"), (NSString *)&__NSConstantStringImpl__var_folders_xy_nncr8hn96cd0_rdymw2f0qcw0000gn_T_MyObject_2519b6_mi_2);
(*(NSString **)((char *)self + OBJC_IVAR_$_MyObject$_age)) = (NSString *)&__NSConstantStringImpl__var_folders_xy_nncr8hn96cd0_rdymw2f0qcw0000gn_T_MyObject_2519b6_mi_3;
}

block 内部使用属性和成员变量是不一样的。直接使用属性时,走的是 obj_msgSend 消息发送(具体可以研究这篇博客),而在使用成员变量时,应该是先通过 self 得到结构体的首地址,然后通过成员变量的偏移量然直接使用这个成员变量(其实我也没很理解。。。)

小结一下:

  • block 内部使用 self 时的情况跟使用局部变量的情况是比较类似的,block 会捕获 self 的值而不是地址当做成员变量
  • 在 block 内部使用属性和成员变量的情况是不一样的

__weak与__strong

我们都知道使用__weak和__strong修饰符可以避免在block的使用中出现循环引用的问题,这是为什么呢?先让我们了解一下这两个修饰符吧!

ARC 环境下,OC的对象面前都需要加上所有权的修饰符,所有的修饰符有以下4种

  • __strong修饰符
  • __weak修饰符
  • __unsafe_unretained修饰符
  • __autoreleasing修饰符

默认的修饰符是__strong。

ARC下,self既不是strong也不是weak,而是unsafe_unretained的,也就是说,入参的self被表示为:(init系列方法的self除外)来源:博客

- (void)start {
   const __unsafe_unretained MyObject *self;
}

想要弄清__weak与__strong的实现原理,需要研究一下clang中关于ARC的文档,有兴趣可以点进去仔细看看。

__strong

    id __strong object = [[NSObject alloc] init];

在终端使用命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 MyObject.m转换成 C++ 的实现

    id __attribute__((objc_ownership(strong))) object = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));

= 右边的代码意思应该是对 NSObject 这个类发送 alloc 消息,然后再对生成的对象发送 init 消息,这两个方法的实现可以在 runtime 中找到,代码我也贴到下面了
= 左边的代码,我不大理解objc_ownership这个函数,查了下搜不到是啥意思,看字面意思应该是两个对象间的持有关系,也就是自己持有自己的意思。

+ alloc
{
    return (*_zoneAlloc)((Class)self, 0, malloc_default_zone()); 
}
- init
{
    return self;
}

__weak

    id __strong object = [[NSObject alloc] init];
    id __weak weakSelf = object;

在终端使用命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 MyObject.m转换成 C++ 实现

id __attribute__((objc_ownership(strong))) object = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
id __attribute__((objc_ownership(weak))) weakSelf = object;

相应的会调用

objc_initWeak(&weakSelf,object);
objc_destoryWeak(&weakSelf);

objc_initWeak方法的文档说明
Precondition: object is a valid pointer which has not been registered as a __weak object. value is null or a pointer to a valid object.
If value is a null pointer or the object to which it points has begun deallocation, object is zero-initialized. Otherwise, object is registered as a __weak object pointing to value. Equivalent to the following code:

id objc_initWeak(id *object, id value) {
*object = nil;
return objc_storeWeak(object, value);
}

这个函数会把传入的 object 置为nil,然后执行objc_storeWeak函数。


那么objc_storeWeak函数是干什么的呢?下面是这个方法的说明

Precondition: object is a valid pointer which either contains a null pointer or has been registered as a __weak object. value is null or a pointer to a valid object.
If value is a null pointer or the object to which it points has begun deallocation, object is assigned null and unregistered as a __weak object. Otherwise, object is registered as a __weak object or has its registration updated to point to value.
Returns the value of object after the call.

objc_storeWeak函数的用途就很明显了。由于weak表也是用Hash table实现的,所以objc_storeWeak函数就把第一个入参的变量地址注册到weak表中,然后根据第二个入参来决定是否移除。如果第二个参数为0,那么就把__weak变量从weak表中删除记录,并从引用计数表中删除对应的键值记录

所以如果__weak引用的原对象如果被释放了,那么对应的__weak对象就会被指为nil。原来就是通过objc_storeWeak函数这些函数来实现的。


接下来是 objc_destoryWeak 函数的实现

void objc_destroyWeak(id *object) { 
    objc_storeWeak(object, nil);
}

还是调用上面的objc_storeWeak函数,因为传入的value为nil,所以object将从weak表中删除并且置为nil

__weak与__strong的作用

终于讲到这两个所有权修饰符的作用了。


首先是不使用这两个修饰符时的情况。在上面我们已经讲到过 block 存在 self 的一种情况了,下面我们要讲一下 block 存在 self 并且 self 强应用 block 时的情况

@interface MyObject ()
@property (nonatomic, strong) NSString *name;
@property (nonatomic, copy) void(^textBlock)(void);
@end

@implementation MyObject
- (void)test
{
    self.textBlock = ^{
        self.name = @"n";
    }
}

@end

@implementation OneViewController 

- (void)viewDidLoad {
    [super viewDidLoad];

    self.object = [[MyObject alloc] init];
    [self.object test]; 
}


对于 MyObject 来说是造成了循环引用的,因为它强引用了 block,而 block 内部也强引用着 self,所以 MyObject 是不能被dealloc的,但奇怪的是,将 MyObject 当做属性的 OneViewController 竟然可以dealloc,这估计是另一个问题了,等我有空再去研究一下这个。。。

使用 clang 得到的C++实现,这边只截取了block结构体和初始化block部分

struct __MyObject__test_block_impl_0 {
  struct __block_impl impl;
  struct __MyObject__test_block_desc_0* Desc;
  MyObject *self;
  __MyObject__test_block_impl_0(void *fp, struct __MyObject__test_block_desc_0 *desc, MyObject *_self, int flags=0) : self(_self) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
// 初始化
((void (*)())&__MyObject__test_block_impl_0((void *)__MyObject__test_block_func_0, &__MyObject__test_block_desc_0_DATA, self, 570425344)

在这个部分中可以看到 block 将 self(MyObject *指针)捕获成了自己的成员变量了(强引用), 而self指针的成员变量又包含block,造成循环引用。


仅仅使用__weak

@property (nonatomic, strong) NSString *name;
@property (nonatomic, copy) void(^textBlock)(void);
@end

@implementation MyObject

- (void)test
{
    __weak typeof(self) weakSelf = self;
    self.textBlock = ^{
        weakSelf.name = @"n";
        NSLog(@"hh");
    };
    self.textBlock();
}

使用xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 MyObject.m得到C++实现

struct __MyObject__test_block_impl_0 {
  struct __block_impl impl;
  struct __MyObject__test_block_desc_0* Desc;
  MyObject *const __weak weakSelf;
  __MyObject__test_block_impl_0(void *fp, struct __MyObject__test_block_desc_0 *desc, MyObject *const __weak _weakSelf, int flags=0) : weakSelf(_weakSelf) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __MyObject__test_block_func_0(struct __MyObject__test_block_impl_0 *__cself) {
  MyObject *const __weak weakSelf = __cself->weakSelf; // bound by copy

        ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)weakSelf, sel_registerName("setName:"), (NSString *)&__NSConstantStringImpl__var_folders_xy_nncr8hn96cd0_rdymw2f0qcw0000gn_T_MyObject_970d18_mi_0);
    }
static void __MyObject__test_block_copy_0(struct __MyObject__test_block_impl_0*dst, struct __MyObject__test_block_impl_0*src) {_Block_object_assign((void*)&dst->weakSelf, (void*)src->weakSelf, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __MyObject__test_block_dispose_0(struct __MyObject__test_block_impl_0*src) {_Block_object_dispose((void*)src->weakSelf, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static struct __MyObject__test_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __MyObject__test_block_impl_0*, struct __MyObject__test_block_impl_0*);
  void (*dispose)(struct __MyObject__test_block_impl_0*);
} __MyObject__test_block_desc_0_DATA = { 0, sizeof(struct __MyObject__test_block_impl_0), __MyObject__test_block_copy_0, __MyObject__test_block_dispose_0};

static void _I_MyObject_test(MyObject * self, SEL _cmd) {

    __attribute__((objc_ownership(weak))) typeof(self) weakSelf = self;
    ((void (*)(id, SEL, void (*)()))(void *)objc_msgSend)((id)self, sel_registerName("setTextBlock:"), ((void (*)())&__MyObject__test_block_impl_0((void *)__MyObject__test_block_func_0, &__MyObject__test_block_desc_0_DATA, weakSelf, 570425344)));
    ((void (*(*)(id, SEL))())(void *)objc_msgSend)((id)self, sel_registerName("textBlock"))();
}

苹果使用一个全局的 weak 表来保存所有的 weak 引用。并将对象作为键,weak_entry_t 作为值。weak_entry_t 中保存了所有指向该对象的 weak 指针。当被指向的对象执行 dealloc 时候,将所有指向该对象的 weak 指针的设置为nil。

  • block 将 __weak 修饰的 self 捕获为成员变量
  • 当 self 执行dealloc时,block 内的 self 置为nil,从而打破循环引用
  • 当 self delloac 之后,在调用 block 的函数指针,block 内部的self置为nil。

同时使用__weak与__strong

@interface MyObject ()
//{
//    NSString *_age;
//}
@property (nonatomic, strong) NSString *name;
@property (nonatomic, copy) void(^textBlock)(void);
@end

@implementation MyObject

- (void)test
{
    __weak typeof(self) weakSelf = self;
    self.textBlock = ^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        strongSelf.name = @"n";
        NSLog(@"hh");
    };
    self.textBlock();
}

使用xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 MyObject.m得到C++实现

struct __MyObject__test_block_impl_0 {
  struct __block_impl impl;
  struct __MyObject__test_block_desc_0* Desc;
  MyObject *const __weak weakSelf;
  __MyObject__test_block_impl_0(void *fp, struct __MyObject__test_block_desc_0 *desc, MyObject *const __weak _weakSelf, int flags=0) : weakSelf(_weakSelf) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __MyObject__test_block_func_0(struct __MyObject__test_block_impl_0 *__cself) {
  MyObject *const __weak weakSelf = __cself->weakSelf; // bound by copy

        __attribute__((objc_ownership(strong))) typeof(weakSelf) strongSelf = weakSelf;
        ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)strongSelf, sel_registerName("setName:"), (NSString *)&__NSConstantStringImpl__var_folders_xy_nncr8hn96cd0_rdymw2f0qcw0000gn_T_MyObject_0010b9_mi_0);
    }
static void __MyObject__test_block_copy_0(struct __MyObject__test_block_impl_0*dst, struct __MyObject__test_block_impl_0*src) {_Block_object_assign((void*)&dst->weakSelf, (void*)src->weakSelf, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __MyObject__test_block_dispose_0(struct __MyObject__test_block_impl_0*src) {_Block_object_dispose((void*)src->weakSelf, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static struct __MyObject__test_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __MyObject__test_block_impl_0*, struct __MyObject__test_block_impl_0*);
  void (*dispose)(struct __MyObject__test_block_impl_0*);
} __MyObject__test_block_desc_0_DATA = { 0, sizeof(struct __MyObject__test_block_impl_0), __MyObject__test_block_copy_0, __MyObject__test_block_dispose_0};

static void _I_MyObject_test(MyObject * self, SEL _cmd) {

    __attribute__((objc_ownership(weak))) typeof(self) weakSelf = self;
    ((void (*)(id, SEL, void (*)()))(void *)objc_msgSend)((id)self, sel_registerName("setTextBlock:"), ((void (*)())&__MyObject__test_block_impl_0((void *)__MyObject__test_block_func_0, &__MyObject__test_block_desc_0_DATA, weakSelf, 570425344)));
    ((void (*(*)(id, SEL))())(void *)objc_msgSend)((id)self, sel_registerName("textBlock"))();
}

  • __MyObject__test_block_impl_0 block 仍然是将 __weak 修饰的 self 捕获为成员变量
  • 当 self 执行dealloc时,block 内的 self 会被置为nil,从而打破循环引用
  • block 内的代码在__MyObject__test_block_func_0函数内,当使用strongSelf时,会先取出__weak修饰的成员变量self:MyObject *const __weak weakSelf = __cself->weakSelf;, 然后再生成一个__strong修饰的局部变量__attribute__((objc_ownership(strong))) typeof(weakSelf) strongSelf = weakSelf;,self 的引用计数 +1。这样的目的是在 block 内的代码块执行完之前避免 self 被dealloc掉。当 block 执行完毕之后,局部变量 strongSelf 被释放,self 的引用计数 -1。

@weakify 和 @strongify

这两个是RAC中避免Block循环引用而开发的2个宏,实现过程很牛,值得我们学习。限于篇幅,我就不分析了,想了解可以点开这篇博客
这两个宏展开下来就相当于:

@weakify(self) = @autoreleasepool{} __weak __typeof__ (self) self_weak_ = self;
@strongify(self) = @autoreleasepool{} __strong __typeof__(self) self = self_weak_;


回到开头

好了,不知道你看了这么多头晕了没有。。。下面让我们回到开头我碰到的那个问题,为什么我使用了 @weakify 和 @strongify,然后直接使用下划线的成员变量还是会造成循环引用。原因就是_ivar直接使用成员变量,self 跟 weakSelf会同时被 block 捕获成 block 的成员变量,注意:self 还是会被 block 捕获的(前面好像没写例子,不过你可以自己写写看),导致 block 还是强引用着 self,导致循环引用。解决办法就是 strongSelf -> ivar这样使用成员变量

总结

  • block 会捕捉 block 内部的变量

    • 当变量类型是局部变量(基本数据类型时或 oc 类),仅捕获该变量的值,所以 block 内部和外部这两个变量的地址是不一样的,在block 内部修改变量的值也不会影响 block 外部的变量
    • 当变量是 self 时的情况跟 局部变量时是差不多的
    • 当变量类型是__block修饰的布局变量(基本数据类型或者 oc 类),会新构建一个结构体,其中成员变量__forwarding会指向栈中该变量的地址,因此在 block 内部修改该变量会影响 block 外部的变量
    • 当变量是全局变量或者全局静态变量时,block 不会捕获该变量,因为变量已经存在在数据区,可以直接调用。此时 block 也保存在数据区
    • 当变量是静态变量时,block 会捕获该变量的地址,因此在 block 内部修改该变量会影响 block 外部的变量
  • block 结构体中的成员变量 descriptor 包含着 copydispose 两个函数指针。copy 函数把 block 从栈上拷贝到堆上,dispose函数是把堆上的函数在废弃的时候销毁掉。

  • 苹果使用一个全局的 weak 表来保存所有的 weak 引用。并将对象作为键,weak_entry_t 作为值。weak_entry_t 中保存了所有指向该对象的 weak 指针。当被指向的对象执行 dealloc 时候,将所有指向该对象的 weak 指针的设置为nil。

  • 在 block 外部使用 __weak 的原因是,让 block 将这个 __weak修饰的变量捕获成自己的成员变量,这样当外面的变量被 dealloc 后,block 内的该成员变量也将置为 nil,避免循环引用

  • 在 block 里面使用的 __strong 修饰的 weakSelf 是为了在函数生命周期中防止 self 提前释放。strongSelf是一个局部变量,当block内的代码执行完毕就会释放,不会对 self 进行一直进行强引用。

引用

ARC对self的内存管理
深入研究 Block 捕获外部变量和 __block 实现原理
深入研究 Block 用 weakSelf、strongSelf、@weakify、@strongify 解决循环引用
谈Objective-C block的实现

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容