block 源码分析 底层原理

block底层原理是什么?

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

block

将main.m文件转换成C++文件,当前文件夹下

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

通过分析main.cpp 我们可以看到编译后的block 。

block编译

我们可以看出block进过编译后生成一个__main_block_impl_0 的结构体,内部有一个__block_impl结构体变量,而且__block_impl内部有一个isa指针,说明block其实也是属于OC对象的,而且我们看到block内部使用的age变量,也存在于block内部,说明当block内部使用变量的时候,会将变量也传入block内部进行使用。下面我们就具体来分析下block内部的本质。

block的调用流程:

通过编译文件我们可以看到几个结构体

__block_impl  :isa指针、FuncPtr 指针:指向的block需要执行的代码地址
__main_block_impl_0 :结构体内部存在一个__main_block_impl_0函数,这是属于C++的构造器函数,返回是当前的一个结构体,相当于OC里面的init函数
__main_block_func_0 : 执行block内部的需要执行的代码
__main_block_desc_0 : block的描述信,第一个参数是0,第二个是block的 sizeof 内存大小。

block定义

void(*block)(void) = ((void(*)())&__main_block_impl_0((void*)__main_block_func_0, &__main_block_desc_0_DATA, age));
//去除 强制转换类型代码 伪代码 :
void(*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, age))
//我们可以看出 block内部 是通过调用__main_block_impl_0函数 返回来的结构体,并取其地址,__main_block_func_0 和 &__main_block_desc_0_DATA, age是传递的三个参数

block对变量的捕获(可以根据上述方法,查看编译文件)

类型 是否捕获 原因
局部变量 出了作用域会销毁,需要捕获保留,值传递
局部(全局)常量static static创建在程序退出之前始终存在内存中,所以采用指针传递
全局变量 在当前作用域是不会销毁的,即使销毁了,block也会一起销毁,所以不需要捕获
self 每个函数都有隐式参数(Class *self,SEL _cmd)所以self属于局部变量,需要捕获
成员变量 成员变量的本质就是 self->name访问,所以也是需要捕获self变量来进行访问,注意捕获的是self,而并非是_name
函数调用 A函数调用B函数 ([self b])会进行消息转发机制 objc_msgSend(self,SEL b,参数),所以也是捕获的self
int global = 10;
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 10;
static int height = 10;
void (^block)(void) = ^{
NSLog(@"age :----%d",age);
NSLog(@"height :----%d",height);
NSLog(@"global :----%d",global);
};
age = 20;
height =20;
global = 20;
block();
}
return 0;}

#interface Person :NSobject
@property (nomotic,assign)int age;
#end
#implentation Person
- (void)test {      --》隐式参数- (void)test:(Person * self,SEL _cmd)
}
#end
//从上一部分我们可以看出,block内部存在外部的变量,block内部会创建相应的变量来接受外部变量,此时block内部的变量已经不是外部变量了
  区别是,
  auto属性(默认属性) 属于值传递 所以输出是10
  static 属性是指针传递 输出是20
  全局变量 不会捕获到block内部,直接调用 输出是20
  局部变量因为作用域问题,aoto局部变量出了作用域会自动销毁,所以block需要及时捕获值
  static局部变量 是一直储存在内存中的,所以采用指针访问。
  全局变量,可以直接访问,所以不需要捕获也能访问。
  self是否会捕获? 隐式参数(每个函数都会有2个默认参数 就是当前 调用者self,SEL _cmd(方法名)),所以self是属于局部变量,所以会捕获
  成员变量(_name),本质是调用self->name   所以也会捕获.

block分类

block分类以及继承关系

不同的block分布在内存中的位置不同

类型 内存中位置 特点
NSGlobalBlock data段 没有访问auto变量,跟全局变量在一块,由系统管理
NSMallocBlock 需要手动释放,NSStackBlock 调用copy生成
NSStackBlock 访问了auto变量, 系统管理释放,超过作用域就释放

block-copy 操作

在ARC环境下,系统默认会对block进行copy操作的几种情况:
1.block作为函数的返回值的时候。
2.将block赋值给__strong指针时。
3.block作为cocoa API中方法名含有usingBlock的方法参数时。
4.block作为GCD API的方法参数时。
copy内部原理:当block从栈copy到堆上之后,如果存在__block、__weak、__strong修饰的对象,在__main_block_desc_0函数内部会增加copy跟dispose函数,copy函数内部会根据修饰类型对对象进行强引用还是弱引用,当block释放之后会进行dispose函数,release掉修饰对象的引用,如果都没有引用对象,将对象释放

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-copy操作

block 对象类型的auto变量捕获

1.如果block在栈上,将不会对auto变量产生强引用。
2.如果block被copy到堆上,会调用block内部的copy函数,copy内部函数会调用_Block_object_assign 函数,_Block_object_assign函数会根据auto变量的修饰符(__strong, __weak,__unsafe_unretaineaod)做出相应的操作,类似于tain(形成强引用,弱引用)


对象类型的auto变量

block引用对象类型的auto变量的时候,ARC会对当前对象进行内存管理操作,如果用__weak修饰的对象,不会增加其引用计数,出了作用域对象就会被释放,当用__strong修饰对象,会增加其引用计数,block执行之后会进行一次release操作。

__block 详解

我们知道 __block的修饰变量之后是就可以修改其值了,但是原理是什么呢?我们先看下代码

typedef void(^JWBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block int age = 10;
        JWBlock block = ^{
            age = 20;
            NSLog(@"ageage---------%d",age);
        };
        block();
        
    }
    return 0;
}

我们转换成 C++代码之后(xcrun -sdk -iphoneos clang -arch arm64 -rewrite-objc main.m)


block修改值过程

__block修饰对象类型

总结:__block修饰的auto变量,编译器会包装成一个__Block_byref_age_0(根据变量名可变)的对象类型的结构体,将指向自己的指针传递给__forwarding指针(这样做的目的是为了当多个block都使用__block修饰的变量的时候,能够始终指向堆中的变量),__Block_byref_age_0结构体内部存在的变量age才是真正__block修饰的变量,通过__Block_byref_age_0 -->__forwarding-->age改变变量的值。如果变量是NSObject对象,还会处理内存管理的问题,如上图,对象类型会生成__Block_byref_id_object_copy 跟__Block_byref_id_object_dispose这两个函数,这两个函数会对当前对象进行内存内存管理工作,下面会细讲这两个函数的作用

block循环引用问题

Person * person = [[Person alloc]init];
        person.block = ^{
            NSLog(@"%d",person.age);
        };

原因:block内部用到了外部的auto对象,block内部实现会对person进行强引用,person的block成员变量也会对block进行强引用,当person超出作用域之后,被回收,但是此时block强引用着Person,Person强引用着block 导致无法释放,造成循环引用,内存泄漏。

循环引用问题

一般我们希望block跟person的周期是一致的,所以最好将block内部引用person的指针换成__weak弱引用是最好的。这样就不会造成互相引用,导致内存无法释放

      Person * person = [[Person alloc]init];
     __weak Person * weakPerson = person;
//__weak typeof(person) weakPerson = person;
//typeof作用是保持person 跟weakPerson是相同类型的。
//也可以用__unsafe_unretained 来修饰
        person.block = ^{
            NSLog(@"%d",weakPerson.age);
        };

区别:__weak :当指向的指针没有强指针指向的时候,会将当前对象置为nil,__unsafe_unretained:当指向的指针没有强指针指向的时候,会将当前对象内存地址不变,容易造成野指针,访问错误的情况,所以不常用。
__block : 也可以解决循环引用的问题,但是使用__block时候必须执行block,并且在block内部将对象置为nil。

面试题

  • block本质是什么?
    封装了函数调用以及调用环境的OC对象
  • __block的作用是什么?
    1.如果__block在栈上,将不会对指向的对象产生强引用。
    2.如果__block被copy到堆上,会调用block内部的copy函数,copy内部函数会调用_Block_object_assign 函数,_Block_object_assign函数会根据auto变量的修饰符(__strong, __weak,__unsafe_unretaineaod)做出相应的操作,类似于retain(形成强引用,弱引用)(这里只是针对ARC时会retain,在MRC下不会进行retain操作
    3.如果变量从堆中移除,会调用block内部的dispose函数,dispose内部会调用_Block_object_dispose函数会自动释放其指向的函数
  • block使用修饰词为什么用copy,注意的细节
    block如果没有进行copy操作,就不会在堆上,无法控制block的生命周期,违背了block得初衷。
    应避免循环引用的问题
  • block在修改NSMutableArray的时候,需要增加__block么?
    不需要,修改可变数组内容,只是对其内容的操作,并没有对指针方面的修改,是对数组的使用并没有重新赋值操作。

你可能感兴趣的:(block 源码分析 底层原理)