effective-Objective-C-详解Block底层

文章目录

Block底层实现

在main函数中书写如下代码:

objc 复制代码
int main(int argc, const char * argv[]) {
  @autoreleasepool {
    void(^block)(void) = ^{
      printf("block~~~\n");
    };
    block();
  }
  return 0;
}

接下来我们在终端cd进入文件目录,执行

clang -rewrite-objc main.m -o main.cpp将OC文件编译为C++文件。

main.cpp文件内容删除掉其他系统代码之后,核心部分如下:

objc 复制代码
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) {
    printf("block~~~\n");
}

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[]) {

    void (*block)(void) = 
    ((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);

    return 0;
}

观察上面代码可以看到,Block本质是一个结构体对象。核心结构如下:

objc 复制代码
struct __block_impl {//不block基础结构体,可以理解为所有block的父结构体
    void *isa;//指向不Block类型
    int Flags;//block标识
    int Reserved;//预留字段
    void *FuncPtr;//block执行函数
};
objc 复制代码
struct __main_block_impl_0 //block结构体
void(^block)(void)
objc 复制代码
static void __main_block_func_0(...)//block被执行函数
  ^{
	printf("block~~~\n");
}
objc 复制代码
struct __main_block_desc_0//block描述结构,记录block大小,复制函数,销毁函数

block执行过程:

objc 复制代码
block();//实际编译如下
((__block_impl* )block)->FuncPtr(...)

block本质就是结构体加函数指针

我们看一下核心部分:

objc 复制代码
struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);//创建一个Block结构体对象,可以理解为 Block tmp;
struct __main_block_impl_0 *blk = &tmp;

Tmp:block对象

__main_block_func_0:block中的代码

__main_block_desc_0_DATA:block描述信息

假设一个block中捕获了一个int类型的变量,那么其C语言实现如下:

objc 复制代码
__main_block_func_0//clang自动生成的函数名

static void __main_block_func_0(struct __main_block_impl_0*__scelf) {
  printf("block~~~\n");
}

__文件名_block_func_编号

全局变量捕获

objc 复制代码
#import <Foundation/Foundation.h>
int c = 1000;
static int d = 10000;

int main(int argc, const char * argv[]) {
  @autoreleasepool {
    int a = 10;
    static int b = 100;
    void (^block)(void) = ^{
      NSLog(@"a = %d", a);
      NSLog(@"b = %d", b);
      NSLog(@"c = %d", c);
      NSLog(@"d = %d", d);
    };
    a = 20;
    b = 200;
    c = 2000;
    d = 20000;
    block();
  }
  return 0;
}

输出结果如下:

通过终端转换为C++代码如下:

objc 复制代码
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;
  int *b;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int *_b, int flags=0) : a(_a), b(_b) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

我们可以看见block中只捕获了两个变量,原因如下:

  • 全局变量捕获

因为是全局变量,无论静态全局变量或者是全局普通变量,在哪里都可以被直接访问,所以在block内部即便是不进行捕获也可以直接访问,所以我们打印这些变量时就是最新的值

  • 静态局部变量的捕获

定义的静态全局变量b被block捕获之后,在block结构体之中是以int* b的形式存储的,也就是说block捕获的其实是变量b的地址,在block内部是通过b的地址去获取修改b的值,所以block的外更改b的值会影响block内部捕获的b的值,block内部更改b的值也会影响block外面b的值。

  • 普通局部变量的捕获

普通局部变量的捕获就是在一个函数或者代码块中定义类似的auto类型的变量,和局部变量不同的是,普通局部变量被block捕获后,在block底层结构体中是以int a,形式存储的,值捕获,也就是内部重新创建了一个变量用来存储捕获的值,此时block内部于外部其实是两个不同的变量,存储着相同的值。

为什么这里只是存储值,不是存储地址呢,原因就是auto类型的变量出了作用域就被自动释放了,如果指针引用会造成悬空指针。

我们看一下下面这个问题:

objc 复制代码
- (void)blockTest{
    
    // 第一种
    void (^block1)(void) = ^{
        NSLog(@"%p",self);
    };
    
    // 第二种
    void (^block2)(void) = ^{
        self.name = @"Jack";
    };
    
    // 第三种
    void (^block3)(void) = ^{
        NSLog(@"%@",_name);//访问实例变量时,编译器会自动转换为self->_name
    };
    
    // 第四种
    void (^block4)(void) = ^{
        [self name];
    };
}

上述代码哪些捕获了self?

答案是全部捕获了self,OC的调用[self blockTest]时,底层都会被编译器转换成objc_msgSend(self, @selector(blockTest));可以看出,self实际是作为参数传给函数objc_msgSend的,也就是说在方法执行时,self的本质就是一个函数参数。在函数调用时,参数会被压入栈帧中,虽然self这个指针变量在方法结束后会被销毁,但是销毁的是栈上的指针变量self,而不是self指向的对象。

block类型总结

我们探索一下block的类:

objc 复制代码
int main(int argc, const char * argv[]) {
  @autoreleasepool {
    int num = 100;
    void (^block1)(void) = ^{
      NSLog(@"----");
    };
    NSLog(@"block1的类 ------ %@", [block1 class]);
    NSLog(@"block2的类 ------ %@", [^{
      NSLog(@"----%d", num);
    } class]);
    NSLog(@"block3的类 ------ %@", [[^{
      NSLog(@"----%d", num);
    } copy] class]);
  }
  return 0;
}

输出结果如下:


__NSGlobalBlock__

如果一个block中没有访问任何变量,那么该block就是__NSGlobalBlock__,在内存中是存在数据区的,即全局区或者静态区。__NSGlobalBlock__类型的block调用copy方法其实啥都没干。就相当于一个单例。

objc 复制代码
- (void)test{
    void (^block)(void) = ^{
        NSLog(@"-----");
    };
    NSLog(@"--- %@",[block class]);
    NSLog(@"--- %@",[[block class] superclass]);
    NSLog(@"--- %@",[[[block class] superclass] superclass]);
    NSLog(@"--- %@",[[[[block class] superclass] superclass] superclass]);
}

输出结果如下:

__NSStackBlock__

如果block捕获了局部变量,那么它就是一个__NSStackBlock__,他存储在栈区,栈区的特点就是自动释放,即他的内存不受开发者控制,系统自动释放。

NSMallocBlock__

一个__NSStackBlock__类型的block调用copy方法,那么就从栈上复制到堆上。如果对一个__NSMallocBlock__类型的变量做copy操作,那么该block的引用计数➕1。完整复制。

__NSStackBlock完整继承链:

__NSMallocBlock__


__NSMallocBlock__//这一步其实在runtime内部存在,是中间的私有类


NSBlock


NSObject

四种自动将栈上操作复制到堆上的操作

1.Block做函数或方法的返回值

objc 复制代码
- (void(^)(void))createBlock {
  int num = 101;
  return ^{
    NSLog(@"zl");
  };
}

编译器会在返回前自动调用copy操作,将其从栈上复制到堆上。

2.将Block需要强引用时

如果我们将一个栈上的Block赋值给一个使用strong修饰符修饰的变量,编译器同样会自动复制到堆上。

objc 复制代码
- (void)test {
    int a = 10;
    // 定义一个 block,并赋值给一个强指针变量 myBlock
    void (^myBlock)(void) = ^{
        NSLog(@"a = %d", a);
    };
    myBlock();
}

当block被赋值给一个strong类型的变量时,ARC会确保block的内存安全,y因此自动将其复制到堆上。

3.当Block作为参数传给Cocoa API时

许多Cocoa API都要求传入的block必须是堆上的Block,如果传入栈上的系统会自动将其复制。

objc 复制代码
[UIView animateWithDuration:1.0f animations:^{
    // 这里传入的 block 会被自动复制到堆上
}];

Block在这里不是立即执行,而是被保存起来,如果block还是在栈上的话,函数返回时栈空间释放,block失效

4.当Block作为参数传给GCD的API时

GCD的API也要求block必须在堆上,这样才能在异步执行中长期有效。

objc 复制代码
dispatch_async(dispatch_get_main_queue(), ^{
	//传递给GCD的block会自动从栈上复制到堆上,便于在调度队列中安全的使用
});

Block属性在MRC与ARC下的写法区别

MRC环境下

建议使用copy:栈上的block不会自动复制到堆上,所以最好使用copy修饰

objc 复制代码
@property (nonatomic, copy) void(^block)(void);

避免因为栈内存释放导致的崩溃或未定义行为

ARC环境下

copy和strong都可以,编译器都会自动将其从栈上复制到堆上。

objc 复制代码
@property (strong, nonatomic) void (^block)(void);
@property (copy, nonatomic) void (^block)(void);

示例

objc 复制代码
typedef int (^mutiplierBlock)(int);
mutiplierBlock createMutiplierBlock(int factor);

int main(int argc, const char * argv[]) {
  @autoreleasepool {
    //调用创建block,捕获factor的值为5
    mutiplierBlock multiplier =  createMutiplierBlock(5);
    int result = multiplier(4);//调用block,虽然创建函数中的factor局部变量早已释放,但是值拷贝了复制本
    NSLog(@"result = %d", result);//输出结果为5 * 4
  }
  return 0;
}
//定义一个函数用于创建一个block,并捕获局部变量factor
mutiplierBlock createMutiplierBlock(int factor) {
  mutiplierBlock block = ^(int number) {//block执行变量捕获,值拷贝副本
    return factor * number;
  };
  return [block copy];//返回前将栈上的block拷贝到堆上
}

Block存储转换

  • 默认情况下,block在函数中创建时位于栈上,生命周期与局部变量类似。调用copy将其复制到栈上


如图所示,被copy到堆上的block就是__NSConcreteMallocBlock类,多次重复拷贝不会导致重复复制,只是增加引用计数。

__block修饰符作用

常规的局部变量只能被block捕获初始值,在修改之后不会更新已捕获的值,使用静态局部变量的话,会消耗不必不要的内存,所以尽量不使用。使用__block关键字可以解决这个问题。

如果在一个Block中使用该修饰词,当该block从栈上复制到堆时,使用的所有__block变量也将全部从栈上复制到堆上,此时Block持有__block变量。

我们看一个例子:

objc 复制代码
__block int a = 10;
void (^block)(void) = ^{
    NSLog(@"%d",a);
};

编译器不会简单复制,而是生成一个byref结构体。

类似于__Block_byref_a

objc 复制代码
struct __Block_byref_a {
    void *__isa;
    __Block_byref_a *__forwarding;
    int __flags;
    int __size;
    int a;
};//Block捕获的是__Block_byref_a * 

在多个Block中使用__block变量时,由于最初所有的block都会被配置在栈上,所以__block变量也会配置在栈上,在copy时都会一并复制到堆上,并被Block持有。

这里第二个复制例子,只复制了Block1,却导致Block0也复制到了堆上,这是因为两个Block共享同一个__block变量结构,而该结构被迁移到堆时,引用它的栈上Block也被必须一起迁移。

__block不论修饰基础数据类型还是对象数据类型,底层都是将们包装成一个对象,这里我们暂时叫做__blockObj,block结构体中有一个指针指向该对象。当block在栈上时,block内部不会对__blockObj产生强引用。当block被copy到堆上时,__blockObj也会被拷贝到堆上,并对__blockObj产生强引用。

在OC中,_Block_object_assign_Block_object_dispose是与block内存管理相关的内部函数,用于处理block在运行时的内存管理。

_block_object_assign

作用:

用于将一个对象赋值给一个block中的局部变量(例如捕获的__block变量),当block捕获一个对象时,它需要确保对这个对象的引用在block执行期间有效。通过该函数即可实现block对捕获的对象进行管理。

主要用途:

  • 复制与引用计数管理:会增加对象的引用计数,确保不提前释放对象。
  • 引用传递:_Block_object_assign确保block在内部维护正确的对象引用,确保不会循环引用

_Block_object_dispose

作用:

是用来释放block内部捕获的对象引用的函数,会在block被销毁时正确的减少捕获对象的引用计数。避免造成内存泄漏

主要用途:

  • 释放捕获对,避免内存泄漏
  • 清理操作:在block销毁时进行清理,确保在block中使用的对象不在被引用时可以正确释放

__forwarding指针

objc 复制代码
__block int val = 10;
void (^blk) (void) = ^{
	val = 1;
};

我们使用__block修饰符修饰变量的时候,该变量底层会变成一个结构体类型的变量,__block_byref_val_0结构体类型的自动变量,即栈上生成的__Block_byref_val_0结构体实例

objc 复制代码
struct __Block_byref_val_0 {
  void* __isa;//用于兼容OC对象结构,不咋用
  struct __Block_byref_val_0* __forwarding;//指向真正的数据位置
  int __flags;//标志位,用于记录当前状态,是否复制到堆,是否需要释放,是否包含对象类型
  int __size;//bref结构体大小
  int val;//原始变量
}
/*
Stack
│
└── byref_va l
        forwarding → Heap_byref_val

Heap
│
├── HeapBlock
│
└── Heap_byref_va;
        forwarding → 自己
        
Stack_byref.forwarding → Heap_byref  //关键变化     

*/

当block从栈上复制到堆上后,局部__Block变量也必须复制到堆上。不然block执行时会访问无效内存。它确保了block内所有对__block变量的引用都指向堆上的有效拷贝,而不会停留在即将销毁的栈内存中。

无论block是在栈上执行还是在堆上执行,所有对该__block变量的访问都通过__forwarding指针,确保访问的是最新最有效的内存区域。

循环引用

示例如下:

objc 复制代码
- (void)test {
  self.name = @"pop";
  self.myBlock = ^(void) {
    NSLog(@"%@", self.name);
  };
}

我们可以看到如下警告:

上面警告内容就是提示我们在block中对self的捕获是强引用。可能会导致循环引用(retain cycle)

常用解决办法:

  • weak - strong
  • ___block修饰对象,同时置为nil
  • 递对象self作为block的参数,提供给block内部使用
  • 使用NSProxy
objc 复制代码
__weak typeof(self) weakSelf = self;
  self.viewModel.researchSong = ^{
		__strong typeof(self) strongSelf = weakSelf;
  }

如果block内部没有嵌套block,那么只用__weak就行,如果嵌套了那么就需要再使用__strong,后面这个strongSelf只是临时变量内部block执行完成之后就释放strongSelf。这种方式依赖于中介者模式,属于自动置为nil。

objc 复制代码
__block typeof(obj) blockObj = obj;
obj.block = ^{
   NSLog(@"Inside block: %@", blockObj);
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                NSLog(@"Inside delayed block: %@", blockObj);
                blockObj = nil;
            });//将对象复制给一个__block修饰的变量,在block结束后对其手动释放
}

需要手动只为nil(必须)

NSProxy虚拟类

一个抽象根类,与NSObject同级

特性 说明
根类 与 NSObject 同级
不能直接实例化 必须子类化
专门用于消息转发 不负责普通对象行为
实现代理对象 常用于 AOP、远程代理

就是一个专门用于拦截并转发消息的对象。

在OC的消息转发机制中,有三个阶段:

  • 动态方法解析:
objc 复制代码
resolveInstanceMethod:
  • 快速转发:
objc 复制代码
forwardingTargetForSelector:
  • 完整转发:
objc 复制代码
methodSignatureForSelector: forwardInvocation:

NSProxy主要依赖于第三阶段,

objc 复制代码
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel;//返回方法签名
- (void)forwardInvocation:(NSInvocation *)invocation;//真正的消息转发

三层拷贝详细讲解

objc 复制代码
__block NSObject *obj = [[NSObject alloc] init];

void (^block)(void) = ^{
    NSLog(@"%@",obj);
};

如果变量使用__block修饰,就会触发三层拷贝

  • 第一层拷贝
objc 复制代码
_Block_copy//将block从栈复制到堆上

Stack

├─ Block

└─ __block struct

拷贝后:

Heap

├─ Block

└─ __block struct (引用栈结构体)

此时__NSStackBlock__ → __NSMallocBlock__

  • 第二层拷贝
objc 复制代码
_Block_byref_copy//复制__block结构体
objc 复制代码
Stack                         Heap
┌───────────────────────┐     ┌───────────────────────┐
│ byref_obj_stack       │     │ byref_obj_heap        │
│ obj                   │     │ obj                   │
└───────────────────────┘     └───────────────────────┘

同时更新__forwarding指针,让所有访问都堆结构体

  • 第三层拷贝
objc 复制代码
_Block_object_assign//处理结构体中的对象变量,retain对象
objc 复制代码
objc_retainBlock//触发block copy
_Block_copy//复制block本身,如果没有捕获变量,这一步结束
_Block_object_assign//如果Block捕获变量,Runtime会调用该方法判断变量类型,并处理(普通对象、__block变量、block)
_Block_byref_copy//如果捕获的是__block变量,调用该方法复制__block结构体
_Block_object_dispose//如果__block变量还有,上一步copy内部还会调用_Block_object_assign
_Block_byref_release//释放block捕获的变量
相关推荐
少云清1 小时前
【UI自动化测试】2_IOS自动化测试 _使用模拟器
ui·ios
枷锁—sha1 小时前
【CTFshow-pwn系列】03_栈溢出【pwn 056-057】详解:32位 与64位Shellcode 与 Linux 系统调用底层原理剖析
linux·运维·网络·笔记·安全·网络安全·系统安全
测试_AI_一辰2 小时前
AI测试工程笔记 04:Codex + Playwright 自动修复 UI 自动化脚本
人工智能·笔记·自动化
2501_915909062 小时前
iOS 开发编译与真机调试流程的新思路,用快蝎 IDE 构建应用
ide·vscode·ios·objective-c·个人开发·swift·敏捷流程
小小unicorn2 小时前
[微服务即时通讯系统]语音子服务的实现与测试
c++·算法·微服务·云原生·架构·xcode
MOON404☾2 小时前
R语言EDA学习笔记
笔记·学习·数据分析·r语言·eda
2501_915106322 小时前
iOS 应用打包流程,不用 Xcode 生成安装包
ide·vscode·macos·ios·个人开发·xcode·敏捷流程
IT界的老黄牛2 小时前
【IT老齐098 笔记】京东实例讲解如何进行系统架构容量评估
笔记·系统架构
2401_832298102 小时前
证书异常检测有什么用?如何实时监控证书异常状态以及iOS企业证书异常检测与监控详解
ios
sealaugh322 小时前
react native(学习笔记第一课)环境构筑(hello,world)
笔记·学习·react native