底层二: block

转换解决方案:支持ARC、指定运行时系统版本,比如

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m

普通转换

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

block 的底层实现

block是封装了函数调用以及函数调用参数的OC对象

后面复习,可以自己先动手转换一个CPP代码,再对照下面的注释学习

Block 的定义以及实现代码

int age = 20;
void (^block)(void) = ^{
      NSLog(@"this is a block %d",age);
};
age = 10;
 block();

直接转换代码(去掉强制转换部分)

int age = 20;

// 1. 通过构造函数创建
// 2.1 这里可以看出,age 之前只是作为值传递到block中,后面修改age的值,不会对block产生任何影响
void(*block)(void) = 
   &__main_block_impl_0(
        __main_block_func_0,           
        &__main_block_desc_0_DATA, 
        age
   );
// 2.2 age在这里修改,不会对block内部的age产生作用
age = 10;

// 3. 这里直接使用__main_block_impl_0 类型的 block 去访问 __block_impl对象中的FuncPtr ,可以从内存的层面去考虑
 block->FuncPtr(block);

转换后的C++代码,分成三个部分

struct __main_block_impl_0

struct __block_impl

struct __main_block_desc_0

下面进行分别介绍:
struct __main_block_impl_0 : Block的具体实现,包含struct __block_implstruct __main_block_desc_0

struct __main_block_impl_0 {
    // 注意这里不是指针,可以直接把结构体变量拿过来.
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    int age;
};

struct __block_impl: 这里是Block的具体实现细节

struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    // block 的函数实现
    void *FuncPtr;
};

struct __main_block_desc_0: Block 的描述

struct __main_block_desc_0 {
    size_t reserved;
    // block的尺寸
    size_t Block_size;
};

重写结构体

// 这里可以看出block的本质,是 __main_block_impl_0 结构体指针
        struct __main_block_impl_0 *impBlock = (__bridge struct __main_block_impl_0 *)block;

通过断点可以获取到block的一些内部信息,通过断点在Block实现部分,反编译到汇编,看最上面的一行调用栈,可以得出结论,__block_impl 内的void *FuncPtr 是函数的实现
屏幕快照 2018-11-21 20.18.08.png

block底层实现图解(如果block捕获参数,会把对应的参数放到指定的位置上)
blcok底层实现.png

这里的参数指的是捕获的变量

block 的变量捕获( capture )

auto: 自动变量,离开作用域自动销毁(无法定义全局变量)
static:修饰局部变量的时候,产生静态局部变量

静态局部变量有以下特点:

1. 该变量在全局数据区分配内存;

2. 静态局部变量在程序执行到该对象的声明处时被首次初始化,即以后的函数调用不再进行初始化;

3. 静态局部变量一般在声明处初始化,如果没有显式初始化,会被程序自动初始化为0;

4. 它始终驻留在全局数据区,直到程序运行结束。但其作用域为局部作用域,当定义它的函数或语句块结束时,其作用域随之结束;

应用

int main()
{
    fn();
    fn();
    fn();
}
void fn()
{
    static int n = 10;
    printf("%d", n);
    n++;
}
变量类型 捕获到block内部 访问方式
局部变量 auto类型 True 值传递
局部变量 static类型 True 指针传递
全局变量 false 直接访问

局部变量传递原因: auto变量出了作用域就进行了释放,这个时候block内要访问这个变量,就必须进行值的捕获,否则会造成坏内存访问。而static始终驻留在全局数据区,直到程序运行结束。

全局变量不会捕获原因: 静态局部变量虽然不会销毁,但是静态局部变量作用域会进行销毁,导致外部无法访问,所以捕获指向静态局部变量的指针。而全局变量作用域也不会进行销毁,可以直接访问,不需要捕获。

int age = 20;
static int height = 30;
void(^block)(void) = ^{
    NSLog(@"age = %d, height = %d",age, height);
};

age = 10;
height = 10;
block();

转化后

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int age;
  int *height;
  // 看出赋值过程 age(_age), height(_height),因为是指针传递,所以Block内部捕获到的值会跟随改变
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_height, int flags=0) : age(_age), height(_height)
  ...
};

block 的类型

block有3种类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型

  • NSGlobalBlock ( _NSConcreteGlobalBlock )
  • NSStackBlock ( _NSConcreteStackBlock )
  • NSMallocBlock ( _NSConcreteMallocBlock )

ARC环境下验证

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

void(^block1)(void) = ^{
    NSLog(@"two");
};
        
NSLog(@"\n %@ \n %@ \n %@ \n", [block class],[block1 class], [^{
    NSLog(@"one = %d",age);
} class]);

打印结果

 __NSMallocBlock__ 
 __NSGlobalBlock__ 
 __NSStackBlock__

block的类型.png

int height = 20;
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        int age = 10;
        
        NSLog(@"数据段 - %p", &height);
        NSLog(@"栈段 - %p", &age);
        NSLog(@"堆段 - %p", [[NSObject alloc] init]);
        // 猜测某个类型在哪个字段?
        NSLog(@"%p", [JXPerson class]);
    }
    return 0;
}

打印结果如下

 数据段 - 0x1000011d0
 栈段 - 0x7ffeefbff54c
 堆段 - 0x101807ae0
 // 可以推测出类对象保存在数据区域
---- 0x1000011a8

程序区域一般放代码,包括main函数这些
数据区域一般放全局变量
段放alloc出来的对象: 动态分配内存,要程序猿自己管理内存(申请、释放)
空间一般放局部变量,自己分配、释放

block 类型的判断标准

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

在 MRC 环境下验证

void(^block)(void) = ^{
    NSLog(@"one");
};

int age = 10;
void(^block2)(void) = ^{
    NSLog(@"%d",age);
};

Block block3 = [block2 copy];

NSLog(@"\n%@ \n%@ \n%@", [block class], [block2 class], [block3 class]);

block的copy

每一种类型的block调用copy后的结果如下所示

Block的类 副本源的配置存储域 复制效果
_NSConcreteStackBlock 从栈复制到堆
_NSConcreteGlobalBlock 程序的数据区域 什么也不做
_NSConcreteMallocBlock 引用计数增加

ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,比如以下情况(自动调用一次copy)

  • block作为函数返回值时 (masory)
  • block赋值给__strong指针时
  • block作为Cocoa API中方法名含有usingBlock的方法参数时(数组遍历)
  • block作为GCD API的方法参数时

MRCblock属性的建议写法

@property (copy, nonatomic) void (^block)(void);

ARCblock属性的建议写法(在ARC情况下会自动复制strongcopy两种类型引用效果相同),

@property (strong, nonatomic) void (^block)(void);
@property (copy, nonatomic) void (^block)(void);

对象类型的auto变量


// 将person对象捕获
JXPerson *person = [[JXPerson alloc] init];
person.age = 10;

JXBlock block= ^{
    NSLog(@"------%d", person.age);
};

转换为CPP代码

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  // 捕获的person变量是通过auto修饰的,如果通过static修饰,会变成 JXPerson ** person
  JXPerson *person;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, JXPerson *_person, int flags=0) : person(_person) {
    ... ...
  }
};

这里将带有weak的变量,转换成CPP代码

__weak JXPerson *weakPerson = person;
block= ^{
    NSLog(@"------%d", weakPerson.age);
} ;

转换代码

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  // 注意这里会有一个__weak 修饰,同外面 对象类型的auto变量的修饰符 相同
  JXPerson *__weak weakPerson;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, JXPerson *__weak _weakPerson, int flags=0) : weakPerson(_weakPerson) {
    ... ...
  }
};

将带有对象类型的auto变量的block,转换成CPP代码,可以看出结构体__main_block_desc_0 会有所改变

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 结构体多出两个函数指针,对应两个函数如下

如果Block被拷贝到堆上,会调用内部的__main_block_copy_0 函数(以下简称copy函数),copy函数会调用 _Block_object_assign 函数, 会根据auto变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用



static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
//调用 _Block_object_assign 函数
_Block_object_assign((void*)&dst->weakPerson, (void*)src->weakPerson, 3/*BLOCK_FIELD_IS_OBJECT*/);

}

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

static void __main_block_dispose_0(struct __main_block_impl_0*src) {

_Block_object_dispose((void*)src->weakPerson, 3/*BLOCK_FIELD_IS_OBJECT*/);

}

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

  • 无论MRC或者ARC,如果block是在栈上,将不会对auto变量产生强引用 < 栈空间的Block没法保住对象类型的auto变量的命 > (在MRC环境通过栈block来进行验证,或者在ARC环境通过没有强引用指针指向的Block来验证)

//MRC 环境
JXBlock block;
{
    JXPerson *person = [[JXPerson alloc] init];
    person.age = 10;

    block= ^{
        NSLog(@"------%d", person.age);
    };
    // 如果调用了copy,将block转换为__NSMallocBlock__,将保住person的命
    // block= [^{
    //    NSLog(@"------%d", person.age);
    // } copy];
    
    [person release];
}

NSLog(@"-------");

NSLog(@"-------"); 处打断点,重写JXPersondealloc方法,打印结果如下:

// 重写dealloc的打印信息
deallOC - JXPerson 释放
函数 调用时机
copy函数 栈上的Block复制到堆时
dispose函数 堆上的Block被废弃时

总结:

如果Block是在空间,不会对内部的对象类型的auto变量产生强引用. 一旦Block堆空间,会根据变量自身的修饰符(强引用或者弱引用)来进行相应的引用操作。

__block

block 无法修改局部变量,转换成CPP代码可以看到本质,就是两者并不在同一个函数内,如果想要修改可以使用静态局部变量(通过指针引用,可修改) 或者使用__block

1.__block可以用于解决block内部无法修改auto变量值的问题
2.__block不能修饰全局变量、静态变量(static)

编译器会将__block变量包装成一个对象

    __block int age = 10;
     void(^myBlock)(void) = ^{
            NSLog(@"%d",age);
     };

转换: 这里是block所转换的结构体,可以看出对应的age是一个指向__Block_byref_age_0 结构体的指针.从构造函数的赋值 age(_age->__forwarding) 可以看出最后是通过结构体的fowarding指针进行的

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    __Block_byref_age_0 *age; // by ref
     __main_block_impl_0(
            void *fp, 
            struct __main_block_desc_0 *desc, 
            __Block_byref_age_0 *_age, 
            int flags=0) : age(_age->__forwarding) {
             ... ...
         }
}

我们找到 __Block_byref_age_0 的定义后发现,真实age的值,被放到结构体中

     // __ block 会单独生成一个结构体
     struct __Block_byref_age_0 {
        // 因为包含isa,所以__block 相当于一个对象
         void *__isa; 
        __Block_byref_age_0 *__forwarding;
        int __flags;
        int __size;
        int age;
     };

然后找到函数中 __block int age = 10; 转换的代码.我们发现了如下的一个结构体赋值过程,10被赋值给结构体内的age,而__forwarding指向自身


__Block_byref_age_0 age = {
            0,
            &age,
            0,
            sizeof(__Block_byref_age_0),
            10
        };

内存管理

__block 修改普通变量

我们可以看到刚刚在block中引用带有__block的代码,转换后的代码描述部分如下,生成了管理对象的函数copydispose,原因是因为__block相当于一个对象,在block内部,需要block进行内存管理

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

block在栈上时,并不会对__block变量产生强引用(可以通过MRC,使用栈空间block验证)

当block被copy到堆时

  • 会调用block内部的copy函数
  • copy函数内部会调用_Block_object_assign函数
  • _Block_object_assign函数会对__block变量形成强引用(retain)

__block copy.png

当block从堆中移除时

  • 会调用block内部的dispose函数
  • dispose函数内部会调用_Block_object_dispose函数
  • _Block_object_dispose函数会自动释放引用的__block变量(release)

__block dispose.png

总结:对象类型的auto变量和__block修饰的变量

相同点

  • Block在栈上时,不会对auto对象类型__block修饰的变量产生强引用
  • Block拷贝到堆上时,会对 auto对象类型__block修饰的变量 调用copy函数。
  • 如果Block从堆中移除,会对 auto对象类型__block修饰的变量 调用dispose函数。

区别

  • 使用__block修饰的基本数据类型在block中肯定是强引用,基本数据类型没法使用 __weak 修饰, 但是使用__block修饰的对象如果使用了weakunsafe_retain的话,会是弱引用,对象使用了strong的话,就是强引用

__forwarding 指针

__block forwarding指针.png

__block 修饰的对象类型

__block JXPerson *person = [[JXPerson alloc] init];
void(^myBlock)(void) = ^{
    NSLog(@"%@",person);
};

转换后的代码

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_person_0 *person; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_person_0 *_person, int flags=0) : person(_person->__forwarding) {
    ... ... 
  }
};

__Block_byref_person_0 是一个单独的结构体, 当__Block_byref_person_0 被拷贝到堆上时,会调用 __Block_byref_id_object_copy 函数,将对应的person调用_Block_object_assign 方法,相反释放的时候,调用dipose函数 (在MRC情况下,不会对person形成强引用,肯定是弱引用)

struct __Block_byref_person_0 {
  void *__isa; // 8
__Block_byref_person_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
 // 这里是对应的对象变量(比基本数据类型多了类型修饰符)
 JXPerson *__strong person;
};

通过查看main函数的CPP源码,可以拿到__Block_byref_person_0 对应的两个函数指针对应的函数实现__Block_byref_id_object_copy_131__Block_byref_id_object_dispose_131

static void __Block_byref_id_object_copy_131(void *dst, void *src) {
// 这里的dst指的是结构体的首地址,+40 正好对应到  JXPerson *__strong person; 的位置
// 所以这里的目的是对person对象,调用 assign 函数
 _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
static void __Block_byref_id_object_dispose_131(void *src) {
 _Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}

__blcok JXPerson.png

ARC 情况下

__block都是强引用

对象是按照对象的修饰词weakstrong决定

__block对象 这种类型是怎么修饰的,应该是按照__block 结构体对象强引用,结构体对象内部的类对象是按照对象的weakassign来决定引用类型

MRC情况下

__Block person = [Person new]
JXBlock myBlock = ^{
    nslog(@"%@",person);
}
person 永远都是弱引用

block 循环应用

循环应用.png

__unsafe_unretained 对象销毁后不会置空
__weak 对象销毁后置为nil

ARC

1.除了弱引用,还可以使用__block来解决循环引用,但是需要进行释放操作,需要调用block
2.在block内部使用__ strong 修饰 weakSelf 的目的: 避免在使用中weakSelf释放掉,举例子: weakSelf -> age 无法通过编译(解决编译器问题)

ARC解决循环引用.png

MRC

MRC解决循环引用.png

posted @ 2021-01-26 14:34  康王驾到  阅读(84)  评论(0)    收藏  举报
Language: HTML