Files
knowledge-kit/Chapter1 - iOS/1.89.md
2022-05-24 13:00:23 +08:00

27 KiB
Raw Blame History

block 底层原理

block 本质

block 本质上就是一个 oc 对象,也有 isa 指针

block 是封装了函数调用和函数调用环境的 OC 对象

int age = 27;
void(^block)(void) = ^(){
    NSLog(@"age:%d", age);
};
block();

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp

转换为 c++ 如下

int age = 27;
void(*block)(void) = &__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);


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

可以看到 __main_block_impl_0 是一个结构体有3个变量其中 __main_block_impl_0 是一个构造方法接收4个参数第四个参数 flags 是非必须的。

__main_block_func_0 参数是一个方法实现。

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int age = __cself->age; // bound by copy

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_z5_ksvb7q252lbdfg78236t7tt00000gn_T_main_eb3c55_mi_0, age);
        }
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)};

__main_block_desc_0 是一个 block 信息的描述,占用了 sizeof(struct __main_block_impl_0) 大小的空间。

void(*block)(void) = &__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age)); 第一行代码也就是将构造一个 struct 给 block 变量。

((**void** (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block); 第二行代码其实就是 block->FuncPtr(block) 根据 block 内部 FuncPtr 方法并调用。

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

为什么 block->FuncPtr 可以直接访问,而不是 block 先访问 impl再访问 FuncPtr因为 __block_impl 就是 __main_block_impl_0 这个结构体的第一个变量地址(结构体特性)

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

类似于下面代码

struct __main_block_impl_0 {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
  struct __main_block_desc_0* Desc;
  int age;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
}; 

block 变量捕获

int age = 27;
void(^block)(void) = ^(){
    NSLog(@"age:%d", age);
};
age = 30;
block();

输出27。因为 Block 会对变量进行捕获。

int age = 27;
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
age = 30;
 ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

变量分为static、auto、register。

static表示作为静态变量存储在数据区。

auto一般的变量不加修饰词则默认为 autoauto 表示作为自动变量存储在栈上。意味着离开作用域变量会自动销毁。

register这个关键字告诉编译器尽可能的将变量存在CPU内部寄存器中而不是通过内存寻址访问以提高效率。是尽可能不是绝对。如果定义了很多 register 变量可能会超过CPU 的寄存器个数,超过容量。所以只是可能。

作用域 捕获到 block 内部 访问方式
局部变量 auto YES 值传递
局部变量static YES 指针传递
全局变量 NO 直接访问

Demo2

auto int age = 27;
static int height = 176;
void(^block)(void) = ^(){
    NSLog(@"age:%d, height: %d", age, height);
};
age = 30;
height = 177;
block();
// age:27, height: 177

clang 转为 c++

auto int age = 27;
static int height = 176;
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age, &height));
age = 30;
height = 177;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);


struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int age;
  int *height;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_height, int flags=0) : age(_age), height(_height) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

可以看到 static 修饰的 height 在 c++ 代码底层用指针被 block 捕获,所以值修改后,是最终的 177static 修饰的 age被 block 捕获是值传递方式所以还是27

Demo3

int age = 27;
static int height = 176;
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void(^block)(void) = ^(){
            NSLog(@"age:%d, height: %d", age, height);
        };
        age = 26;
        height = 177;
        block();
    }
    return 0;
}

转为 c++

void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
age = 26;
height = 177;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);


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_z5_ksvb7q252lbdfg78236t7tt00000gn_T_main_65da50_mi_0, age, height);
}

可以看到全局变量是直接访问的,不进行拷贝。

为什么这么设计?

因为 auto 修饰的变量一出作用域马上回收,所以 block 为了自身运行信息的完整性所以会捕获。static 修饰的变量,数据一直在内存中,所以执行 block 代码的时候是用指针获取内存中最新的数据。全局变量不会捕获。

self 会捕获吗?

会,因为 self 就是局部变量。 一个 oc 方法转换为 void test(Person *self, SEL _cmd) 形式,所以 self 也是局部变量,会被捕获

block 类型

block 的类型可以通过 isa 或者 class 方法查看,最终都是继承自 NSBlock 类型

__NSGlobalBlock__ (_NSConcreteGlobalBlock):程序的数据区域(.data 区)

__NSStackBlock__ (_NSConcreteStackBlock)

__NSMallocBlock__(_NSConcreteMallockBlock)

void(^block)(void) = ^(){
    NSLog(@"Hello block");
};
NSLog(@"%@", [block class]); // __NSGlobalBlock__
NSLog(@"%@", [[block class] superclass]); // NSBlock
NSLog(@"%@", [[[block class] superclass] superclass]); // NSObjec

代码存放在 text 段static 修饰的数据存放在 data 区,程序员手动申请的内存存放在堆,局部变量存放在栈区

如何判断 block 属于什么类型

Demo1

void(^block)(void) = ^{
    NSLog(@"");
};
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"%@", [block class]);
    }
    return 0;
}

__NSGlobalBlock__ ,此类型的 block 用结构体实例的内容不依赖于执行时的状态所以整个程序只需要1个实例。因此存放于全局变量相同的数据区域即可。

void(^block)(void);
void test()
{
    int age = 22;
    block = ^ {
        NSLog(@"ag:%d", age);
    };
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        test(); 
        block();   // age:-1074793800  
    }
    return 0;
}

为什么会打印出 age:-1074793800。 因为 block 访问了auto 变量,所以是 __NSStackBlock__。那么 block 内部的数据在栈上维护,当出了 test 方法后block 内部变量的地址到底指向什么是不确定的,可能会出现异常。

block 类型 环境
__NSGlobalBlock__ 没有访问 auto 变量
__NSStackBlock__ 访问了 auto 变量
__NSMallocBlock__ __NSStackBlock__ 调用了 copy 方法

Demo1:

MyBlock block;
{
    Person *person = [[Personalloc] init];
    block = ^{
        NSLog(@"block called");
    };
    NSLog(@"%@", [block class]); 
};

MRC 环境: 如果 block 不访问外部局部变量,则__NSGlobalBlock__

ARC 环境:如果 block 不访问外部局部变量,则__NSGlobalBlock__

Demo2:

typedef void(^MyBlock)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MyBlock block;
        {
            auto Person *person = [[Person alloc] init];
            person.age = 10;
            block = ^{
                NSLog(@"age:%zd", person.age);
            };
            NSLog(@"%@", [block class]);    // __NSStackBlock__
        };
    }
    return 0;
}

MRC 环境下:如果访问了 auto 变量,则为 __NSStackBlock__

ARC 环境下:ARC 下面比较特殊,默认局部变量对象都是强指针,存放在堆里面。所以 block 为 __NSMallocStack__

Demo3:

typedef void(^MyBlock)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MyBlock block;
        {
            auto Person *person = [[Person alloc] init];
            person.age = 10;
            block = [^{
                NSLog(@"age:%zd", person.age);
            } copy];
            NSLog(@"%@", [block class]);    // __NSMallocBlock__
        };
    }
    return 0;
}

MRC 下:如果 block 调用 copy 方法,则 block 为 __NSMallocStck__

ARC 下:如果 block 调用 copy 方法,则 block 仍旧为 __NSMallocBlock____NSMallocBlock__ 调用 copy 仍旧为 __NSMallocBlock__

在 ARC 下,如果有一个强指针引用 block则 block 会被拷贝到堆上,成为 __NSMallocStck

Block 类 原本位置 复制效果
__NSConcreteStackBlock__ 栈复制到堆
__NSConcreteGlobalBlock__ 程序的数据段 什么也不做
__NSConcreteMallocBlock__ 引用计数+1

内存管理

思考题

查看 block 编译成 c++ 代码的源码可以发现 __main_block_desc_0 结构体内部是变化的。什么意思呢reserved、Block_size 是一直有的void (*copy)、void (*dispose) 只有在修饰对象的时候才有。为什么这么设计?

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};

因为 block 会对变量进行内存管理。void *copyvoid *dispose 都是内存管理的方法。

如果 block 访问的不是对象,则结构体没有 void *copyvoid *dispose

Demo1:

{
    Person *person = [[Person alloc] init];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"1---%@", person);
    }); 
}

1s 后 person 执行了 dealloc 方法

Demo2

{
    __weak Person *person = [[Person alloc] init];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"1---%@", person);
    });
}

马上执行了 Person 的 dealloc 方法。因为 __weak 修饰block 内部的 _Block_object_assign 会根据 __strong 为对象引用计数 +1__weak 则引用计数不变。所以是 __weak 修饰,出离作用域则立马会释放 Person 对象。

_Block_object_assign 会根据内存修饰符来对内存进行操作。

Demo3

{
    Person *person = [[Person alloc] init];
    __weak Person *weakP = person;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"1---%@", person);
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"2---%@", person);
        });
    });
}

3s 后执行 Person 的 dealloc

Demo4

{
    Person *person = [[Person alloc] init];
    __weak Person *weakP = person;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"1---%@", weakP);
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"2---%@", person);
        });
    });
}

3s 后执行 Person 的 dealloc 方法

Demo5

{
    Person *person = [[Person alloc] init];
    __weak Person *weakP = person;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"1---%@", person);
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"2---%@", weakP);
        });
    });
}

1s 后执行 Person 的 dealloc 方法。

结论block 作为 GCD API 的方法参数时,如果 block 内部访问了对象,对象的生命周期结束需要查看强引用结束时刻。

ARC 环境下编译器会自动会 block copy 复制到堆上

  1. block 作为函数返回值时(如果栈 block离开方法作用域之后return 给新的变量区使用,由于栈变化了,所以不安全。比如访问 auto 变量的栈 block可能某个变量已经不是之前的某个值了

  2. 将 block 赋值给 __strong 指针时

  3. block 作为 Cocoa API 中方法名含有 usingBlock 的方法参数时

  4. block 作为 GCD API 的方法参数时

栈空间的 block 不会对变量进行 copy 操作

堆空间的 block 会堆变量自动进行 copy 操作

__NSStackBlock__ 内部访问了对象,默认是 __strong 修饰。如果对象是 __weak 则 block 转换 c++ 内部捕获的对象,也用 weak 修饰

typedef void(^MyBlock)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MyBlock block;
        {
            __weak Person *person = [[Person alloc] init];
            person.age = 27;
            block = ^{
                NSLog(@"age:%zd", person.age);
            };
            person.age = 28;
        };
    }
    return 0;
}

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  Person *__weak person;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__weak _person, int flags=0) : person(_person) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

block 修改变量

Demo1

typedef void(^MyBlock)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int age = 27;
        MyBlock block = ^{
            age = 28;
        };
        NSLog(@"%zd", age);
    }
    return 0;
}

运行会报错 // Variable is not assignable (missing __block type specifier) 为什么不能修改?继续查看 c++ 源代码

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int age = __cself->age; // bound by copy
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_z5_ksvb7q252lbdfg78236t7tt00000gn_T_main_f31a48_mi_0, age);
}

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; 
        int age = 27;
        MyBlock block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_z5_ksvb7q252lbdfg78236t7tt00000gn_T_main_f31a48_mi_1, age);
    }
    return 0;
}

可以看到在 block 内部修改外部变量也就是在新创建的函数内部,修改 main 函数内部的变量 😂 这怎么可能?

全局变量、static 变量在 block 内部可以修改。

  • __block 用于解决 block 内部无法修改 auto 变量的问题。

  • __block 不能修饰 static、全局变量

  • 编译器会将 __block 修饰的变量包装为一个对象(后续修改则通过指针找到结构体对象,结构体对象再修改里面的值)

Demo

__block int age = 27;
MyBlock block = ^{
    age = 28;
};

转为 C++

__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 27};
        MyBlock block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_z5_ksvb7q252lbdfg78236t7tt00000gn_T_main_f0f60a_mi_0, (age.__forwarding->age));

struct __Block_byref_age_0 {
  void *__isa;
__Block_byref_age_0 *__forwarding;
 int __flags;
 int __size;
 int age;
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_age_0 *age = __cself->age; // bound by ref
  (age->__forwarding->age) = 28;
}

可以看到 __block int age = 27; 变为了 __Block_byref_age_0 age 结构体。block 内部的函数在修改 age 的时候其实就是通过 __main_block_impl_0 结构体的 age 找到 __Block_byref_age_0,然后访问 __Block_byref_age_0 中的成员变量 __forwarding 访问成员变量 age并修改值。

__block 修饰基本数据类型和对象,对于生成的结构体也不一样。

QA为什么__block 变量的 __Block_byref_age_0 结构体并不在 block 结构体 __main_block_impl_0 中?

因为这样做可以在多个 block 中使用 __block 变量。

Demo

struct __Block_byref_age_0 {
  void *__isa;
__Block_byref_age_0 *__forwarding;
 int __flags;
 int __size;
 int age;
};

struct __Block_byref_p_1 {
  void *__isa;
__Block_byref_p_1 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 NSObject *p;
};

block 只要和对象打交道,结构体里面要管理内存,所以会有 void *copy ,void *dispose

block 内部操作数组等类型,不需要加 __block

Demo知道 __block 的本之后,下面打印的 age 的地址是 struct 里面哪个的值?

__block int age = 27;
MyBlock block = ^{
    age = 28;
};
NSLog(@"%p", &age);

知道转换为c++后的效果,我们可以在代码中按照结构体,自己定义并转接到 block

struct __Block_byref_age_0 {
  void *__isa;  // 0x0000000105231f70 +8
 struct __Block_byref_age_0 *__forwarding; // 0x0000000105231f78 + 8
 int __flags; // 0x0000000105231f80 +4
 int __size; // 0x0000000105231f84 + 4
 int age;   // 0x0000000105231f88
};

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

struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(void);
    void (*dispose)(void);
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  struct __Block_byref_age_0 *age; // by ref
};


typedef void(^MyBlock)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block int age = 27;
        MyBlock block = ^{
            age = 28;
        };
        struct __main_block_impl_0 *blockImpl = (__bridge struct __main_block_impl_0 *)block;
        NSLog(@"%p", &age);
    }
    return 0;
}

我们将断点设置到 NSLog 这里,打印出自定义结构体 __main_block_impl_0 中的 age 。

// 0x0000000105231f70
struct __Block_byref_age_0 {
  void *__isa;  // 地址0x0000000105231f70 长度:+8
 struct __Block_byref_age_0 *__forwarding; //  地址0x0000000105231f78 长度:+8
 int __flags; // 地址0x0000000105231f80 长度:+4
 int __size; // 地址0x0000000105231f84 长度:+4
 int age;   // 地址0x0000000105231f88
};

将地址打印出来。该地址就是 __Block_byref_age_0 结构体的地址,也就是结构体内第一个 isa 的地址。我们计算下,规则如下:

  • 指针长度8个字节

  • int 长度4个字节

算出来 age 的地址为 0x0000000105231f88 ,此时 Xcode 打印出的地址也是 0x105231f88。其实也就是 blockImple->age->age 的地址

block 内部对变量的值修改其实就是对 block 内部自定义结构体内部的变量修改。

当 block 被 copy 到堆上

  • 会调用 block 内部的 copy 函数

  • copy 函数内部会调用 _Block_object_assign 函数

  • _Block_object_assign 函数会根据所指向对象的修饰符__strong、__weak、__unsafe_unretained做出相应的操作形成强引用retain或者弱引用注意这里仅限于ARC时会retainMRC时不会retain

当 block 从堆中移除

  • 会调用 block 内部的 dispose 函数

  • dispose 函数会调用 _Block_object_dispose 函数

  • _Block_object_dispose 函数会自动释放 __block 修饰的变量release

__forwarding 的设计

当block在栈中时__Block_byref_age_0结构体内的__forwarding指针指向结构体自己。

而当block被复制到堆中时栈中的__Block_byref_age_0结构体也会被复制到堆中一份,而此时栈中的__Block_byref_age_0结构体中的__forwarding指针指向的就是堆中的__Block_byref_age_0结构体,堆中__Block_byref_age_0结构体内的__forwarding指针依然指向自己。

Block 内存引用

__block 修饰符修饰的对象在内存中如下


int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
       __attribute__((__blocks__(byref))) __Block_byref_p_0 p = {(void*)0,(__Block_byref_p_0 *)&p, 33554432, sizeof(__Block_byref_p_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"))};
        void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_p_0 *)&p, 570425344));
    }
    return 0;
}


static void __Block_byref_id_object_copy_131(void *dst, void *src) {
 _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}

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

如果 __block 修饰 __strong 则表示 block_impl 结构体中的 person 成员变量指向一个新的结构体 __Block_byref_person_0。这个线是强引用。

__Block_byref_person_0 结构体成员变量 person 真正的 Person 对象的引用关系要看 block 外部 person 的修饰是 __strong 还是 __weak,因为从栈上拷贝到堆上,会调用 block 的 desc 的 __main_block_copy_0,本质上调用的是 _Block_object_assign

__Block_byref_id_object_copy_131 方法里的 40 代表什么?


struct __Block_byref_p_0 {
    void *__isa; 8
__Block_byref_p_0 *__forwarding;    8
    int __flags; 4
    int __size; 4
    void (*__Block_byref_id_object_copy)(void*, void*); 8
    void (*__Block_byref_id_object_dispose)(void*); 8
    Person *p;
};


__attribute__((__blocks__(byref))) __Block_byref_p_0 p = {
    0,
    &p,
    33554432,
    sizeof(__Block_byref_p_0),
    __Block_byref_id_object_copy_131,
    __Block_byref_id_object_dispose_131,
    ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"))
    
};

__Block_byref_p_0 结构体地址上偏移40就是 p 对象。

循环引用

self 是一个局部变量block 访问 self即存在捕获变量的效果。

ARC 下

__weak__unsafe_unretained、`__block``

区别在于:__weak 不会产生强引用,指向的对象销毁时,会自动给指针置为 nil

__unsafe_retained 不会产生强引用,不安全。当指向的对象销毁时,指针地址值不变。

@interface Person : NSObject
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, copy) void (^block)(void);
- (void)test;
@end

@implementation Person
- (void)dealloc
{
    NSLog(@"%s", __func__);
}
- (void)test
{
    
    __weak typeof(self) weakself = self;
    self.block = ^{
        weakself.age = 23;
    };
    self.block();
    NSLog(@"age:%ld",  (long)self.age);
}
@end

Person *p = [[Person alloc] init];
[p test];

方法1: __weak 修饰。__weak typeof(self) weakself = self;

方法2: __unsafe_retained 修饰。**__unsafe_unretained** **typeof**(**self**) weakself = **self**;

方法3: __block 修饰。因为此时会构成3角关系。所以需要调用 block。block 内部需要将对象设置为 nil。

__block Person *weakself = [[Person alloc] init];
p.block = ^{
    weakself.age = 23;
    NSLog(@"%ld", weakself.age);
    weakself = nil;
};
p.block();

__unsafe_retained 因为不安全所以不推荐,__block 因为使用繁琐,且必须等到调用 block 才会释放内存所以不推荐。ARC 下最佳用 __weak

MRC 下

方法1: __unsafe_retained 修饰。**__unsafe_unretained** **typeof**(**self**) weakself = **self**;

方法2: __block 修饰。MRC 下不会对 block 内部的对象引用计数 +1

总结

block 本质是什么?

封装了函数调用及其调用环境的 OC 对象

__block 的作用是什么?

可以对 block 外部的变量进行捕获,可以修改。但是需要注意内存管理相关问题。比如__weak__unsafe_unretained__block

修改 NSMutableArray 不需要加 __block?

是的