数据结构iOS进阶OC基础

OC-Block

2020-07-01  本文已影响0人  xiaoyouPrince

Block

一个最简单的block,将源码编译后如下

// 基本使用 block
void (^block)() = ^{
    NSLog(@"block - 调用");
};
    
// 调用
block();

编译成C++后,其具体定义的类型如下,命令如下: xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp

struct __block_impl {
  void *isa;        // 其 isa 并非指向 Class,表明其为匿名对象
  int Flags;        // 系统传值默认为0
  int Reserved;     // 系统传值默认为0
  void *FuncPtr;    // 内部保存一个回调函数地址,可以在外界回调
};

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

// block 调用时候的具体函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_3l_tslpwwd93px60pg28clgyfvh0000gn_T_main_654937_mii_3);
        }

// block 描述信息 __main_block_desc_0_DATA
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)};

重写为 C++ 代码后。block 的定义 & 调用

void (*block)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

// 移除类型转换如下
void (*block)() = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);

(block->FuncPtr)(block); // 找到block本身保存的 funcPtr 直接调用
15934170754263.jpg

Block 捕获变量

为了保证 block 能正常的访问外部的变量,其有一个变量捕获的机制

15934190192032.jpg

Block 的类型

Block 根据其存储域,分为三种类型

15934223328514.jpg

没有访问 Auto 变量 -> global 类型 -> 就相当于一个函数,全局只有一份
访问了 Auto 变量 -> stack 类型 -> 实际就是一个局部变量,出了其变量作用域直接就被销毁了
stack 类型调用 copy -> malloc 类型

block 的 copy

在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,如下

1. 将block作为返回值返回时
2. 将block赋值给 __strong 指针时候,会自动copy,block的声明周期就是 strong的生命周期
3. block 作为 Cocoa Api 中方法,方法名中含有 usingBlock 的方法参数时
4. block 作为 GCD API 的方法参数时

MRC 下block属性的建议写法,直接拷贝到堆
@property (copy, nonatomtic) void (^block)(void);

ARC 下block属性的建议写法
@property (strong, nonatomtic) void (^block)(void);
@property (copy, nonatomtic) void (^block)(void);

Block 捕获对象类型的 auto 变量

当 block 内部访问了对象类型的 auto 变量时

1. 如果block 在栈上,将不会对auto变量产生强引用
2. 如果block被拷贝到堆上
    1. 会调用block内部的copy函数
    2. copy 函数内部会调用_Block_object_assign函数
    3. _Block_object_assign函数会根据auto变量的修饰符(__strong,__weak,__unsage_unretained)做出响应的操作,类似retain(简单来说常用的 weakself 就是防止强引用)

3. 如果 block 从堆上移除
    1. 会调用block内部的 dispose 函数
    2. dispose 函数内部会调用_Block_object_dispose 函数
    3. _Block_object_dispose 函数会自动释放引用的 auto 变量,类似release 

Block 修改外界局部变量的值

通常情况下,block 无法修改外界变量的值,原因是 block 只是捕获了 auto 变量,实际在内部函数调用时候是一个新函数,无法访问外部变量。 如果是 static 变量或者全局变量就可以正常修改外部变量值,因为它们的访问方式不一样.

要实现修改外部的 auto 变量,可以增加 __block 修饰符

__block int b = 10;
void (^block)() = ^{
    b = 20;
};

可以看到,增加 __block 后代码经过重写为 C++

// block 源码如下:

struct __Block_byref_b_0 {
  void *__isa;
__Block_byref_b_0 *__forwarding;
 int __flags;
 int __size;
 int b;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_b_0 *b; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_b_0 *_b, int flags=0) : b(_b->__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_b_0 *b = __cself->b; // bound by ref

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

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->b, 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};

// 声明和调用如下:
__attribute__((__blocks__(byref))) __Block_byref_b_0 b = {(void*)0,(__Block_byref_b_0 *)&b, 0, sizeof(__Block_byref_b_0), 10};
void (*block)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_b_0 *)&b, 570425344));

((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

// 移除类型转换,简化如下:
__Block_byref_b_0 b = {(void*)0,(__Block_byref_b_0 *)&b, 0, sizeof(__Block_byref_b_0), 10};
block = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &b, 570425344));
(block->FuncPtr)(block);

可以发现block结构体 __main_block_impl_0 内多了一个__Block_byref_b_0 *b; 指针,外界变量 b 就相当于被封装成了一个对象,其指针被block引用,真正修改值操作是在该 b 对象内部修改的。

对象类型的 auto 变量,__block 变量

1. 当block 在栈上时候,对它们都不会产生强引用
2. 当block 被拷贝到堆上时候,会通过 copy 函数来处理它们,计数加1
3. 当block 被移除的时候,会通过 dispose 函数来移除它们,计数减1

被 __block 修饰的对象类型

1. 当__block 变量在栈上时候,不会对指向的对象产生强引用
2. 当__block 变量被 copy 到堆上时候
    1. 会调用 __block 变量内部的 copy 函数
    2. copy 函数内部会调用 _Block_object_assign 函数
    3. _Block_object_assign 函数会根据指向类型的修饰符(__strong,__weak,__unsage_unretained)做响应的持有操作,// 这里只有 ARC 时候会retain持有该对象,MRC时候不会持有该对象
3. 如果__block 变量从堆上移除
    1. 会调用 __block 变量内部的 dispose 函数
    2. dispose 函数内部会调用 _Block_object_dispose 函数
    3. _Block_object_dispose 函数会自动释放指向的对象

可以看出:Block 捕获变量的时候无论是否是 __block 变量其操作逻辑一样
__block 变量的作用为修改 Block持有其捕获变量的结构,如图

15935849845676.jpg

__block 变量的__forwording指针

Block 本身是可能存在堆和栈上的。 __block 变量也会因为Block从栈拷贝到堆上而赋值到堆上,所以它本身也是可能存在栈上和堆上的。

__block int b = 10;
void (^block)() = ^{
    b++; // 变量b为复制到堆上__block变量结构体的实例
};
b++;    // 变量b为复制到堆上之前,栈上__block 变量结构体的示例

// 以上两个地方的使用都可以转化为
(b.__forwording->b)++; // 通过__forwording指向无论是栈/堆上的结构体实例。

如图:

831593572769_.pic.jpg

Block 循环引用处理

由 Block 捕获外部 auto 变量的原理来看,如果 auto 变量持有 Block 且被 Block 捕获就会发生循环引用。处理方式如下:

ARC下:

1. __weak 修饰符修饰 auto 变量
2. __unsafe_unretained 修饰符修饰 auto 变量(其为弱引用,不安全,不会在变量被销毁时候被设置为nil)
3. __block 修饰,这样会修改 Block 持有该对象的数据结构,但是必须调用Block,并且在函数调用内部给变量手动置空。(不安全,容易发生泄漏)

MRC 下:

1. __block 修饰,在MRC下Block不会retain被__block修饰的对象
2. __unsafe_unretained 修饰符修饰

面试题------------

Block 原理是怎么样的,本质是什么?

Block 本质是一个封装了函数调用以及调用环境的 OC 对象,
原理如上面....

__block 的作用是什么,有什么注意点

作用:是的Block可以修改其捕获的外部变量
注意点: 内存管理,注意循环引用。。。在MRC下不会retain被捕获的对象,这点和ARC不同

Block 修饰词为什么是Copy,使用Block有哪些需要注意?

block 如果不 copy,其作用域在栈(数据区)上,为了给Block保活,在使用的时候不会被释放掉。

注意:内存管理,不要发生循环引用

block 在修改 NSMutableArray时候,是否需要加 __block 修饰符

如果是修改其实例指针的值,需要。如: array = nil;

如果是修改实例的内容,如添加新元素,不需要。如:[array addObject:obj];
上一篇下一篇

猜你喜欢

热点阅读