OC底层学习-Block

1.Block的本质

1.1 block的底层结构

  • OC中block代码转换成C++代码:
//OC代码:
void(^block)(void) = ^(){
            NSLog(@"this a block");
        };
block();

//转换后的C++代码
int main(int argc, const char * argv[]) {

    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        //定义block变量
        void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
        //执行block内部的代码
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}
//上述main函数对应的是OC代码中的两句
//block的底层代码结构:
//__main_block_impl_0 这个结构体的地址就是__block_impl impl的地址
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
	
	//C++中的结构体的构造函数 返回一个结构体对象(类似于oc的init方法)
	//第一个参数是封装执行代码函数的地址,把这个地址复制给了FuncPtr属性
  __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;
  }
};
//struct __block_impl
struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;//指向了将来要执行的代码地址,block会将要执行的代码放到一个函数中去,函数地址就赋值给这个指针,将来用过这个地址去调用这个函数,就可以执行里面的代码
};

//struct __main_block_desc_0*   block中的描述信息
static struct __main_block_desc_0 {
  size_t reserved;//保留
  size_t Block_size;//block大概占多少内存
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};//sizeof是计算这个结构体占多大的内存

由此可见Block底层是一个结构体,Block会把需要执行的代码封装成一个函数,然后把这个函数的地址复制给FuncPtr属性,将来使用这个地址来调用这个函数,就可以执行里面的代码。

上述是没有参数的Block,那如果是带有参数的Block,那么底层的代码会有什么变化吗

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

int main(int argc, const char * argv[]) {

    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 


        void(*block)(int, int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
		//首先block的调用方式是不一样了
        ((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 10, 20);
    }
    return 0;
}
//封装的调用函数多了两个参数
static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) {

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_8s_wljbbn3d6bd0mt_kk8cxxdlc0000gn_T_main_f57476_mi_0,a,b);
        }
        

1.2 block变量捕获-auto

  • Block内部访问外部变量:
//C语言: 有个默认关键字auto ==》 auto int age = 10; 默认可以省略(自动变量:离开作用域就销毁)
int age = 10;
void(^block)(void) = ^{
     NSLog(@"block 访问外部变量===%d",age);
 };
age = 20;
block();
//执行代码 打印结果 age = 10;
//下面我们看看底层的C++代码结构
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  //底层结构体中多了一个成员变量age
  int age;
  // age(_age)  : 意思是传递进来的age参数将来赋值给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;
  }
};

int main(int argc, const char * argv[]) {

    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        int age = 10;
        //在执行构造函数的时候传递的参数age是10 ,所以Block的内部的成员age 是10 值传递
        void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
        
        //修改外部的age变量值,但是block内部的成员age却没有发生变化 值还是 10,因为一开始的时候我们已经将10这个值赋值给age了
        age = 20;
        
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
//当调用函数的时候,取出blcok内部的age成员的值
  int age = __cself->age; // bound by copy

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_8s_wljbbn3d6bd0mt_kk8cxxdlc0000gn_T_main_a407cb_mi_0,age);
        }
        

由此可知,当我们创建block对象的时候,age的值10 已经别储存在block的内部了(age的值被block捕获进来),所以当你修改age的值时仅仅是修改了外部的age的值,但是block的值却没有发生改变,所以打印结果是10

1.3 Block变量捕获(capture)-static

 int age = 10;
        static int height = 10;
        void(^block)(void) = ^{
            NSLog(@"block 访问外部变量===%d   height=====%d",age,height);
        };
        age = 20;
        height = 20;
        block();
 //打印结果: block 访问外部变量===10   height=====20

//C++底层代码
int main(int argc, const char * argv[]) {

    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        int age = 10;
        static int height = 10;
       //定义block的时候,height传递进去的是height的地址值
        void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age, &height));
        age = 20;
        height = 20;
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int age;
  int *height; //缓存height的地址值
  
  //height的地址值传递进来赋值给block内部的成员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;
  }
};
//block调用代码
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int age = __cself->age; // bound by copy
  //取出的是height的地址值
  int *height = __cself->height; // bound by copy
	//参数运算的是 *height 也就是放问成员height地址值的实际值,也就是外部变量的height的值,所以是20
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_8s_wljbbn3d6bd0mt_kk8cxxdlc0000gn_T_main_44654d_mi_0,age,(*height));
        }
        

由此可见,当block访问外部的static修饰的局部变量时,内部会产生一个成员来储存这个外部变量的地址,所以你在外部修改static修饰变量的值,那么blcok被调用的时候,内部对于static修饰的变量是根据地址值去访问,所以最后访问到值是你在外部修改了值,也是height打印20的原因

1.4 block变量捕获-全局变量

block访问外部的全局变量:

int age_ = 10;
static int height_ = 10;
int main(int argc, const char * argv[]) {
    
    @autoreleasepool {
        
        void(^block)(void) = ^{
            NSLog(@"block 访问外部变量===%d   height=====%d",age_,height_);
        };
        block();
    }
    return 0;
}

//C++的底层代码:
int age_ = 10;
static int height_ = 10;

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

int main(int argc, const char * argv[]) {

    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

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

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_8s_wljbbn3d6bd0mt_kk8cxxdlc0000gn_T_main_bcbbee_mi_0,age_,height_);//直接访问外部的变量 
        }
        

由此可见如果是全局变量,block不会把变量捕获到block的内部(不需要捕获,因为是全局变量,哪个函数都可以访问,而局部变量需要跨函数访问所以需要捕获),block执行的时候是 直接访问外部的变量

1.5 Block访问self或类中的属性

@interface GYPerson : NSObject

/** <#descrption#> */
@property (nonatomic, strong) NSString *name;

- (void)test;
@end
@implementation GYPerson
- (void)test {
    
    void(^block)(void) = ^{
        NSLog(@"block=======%@",self);
    };
    block();
}
@end

//C++代码
struct __GYPerson__test_block_impl_0 {
  struct __block_impl impl;
  struct __GYPerson__test_block_desc_0* Desc;
  //由此block的底层结构代码可知 block对self进行了捕获
  GYPerson *self;
  
  __GYPerson__test_block_impl_0(void *fp, struct __GYPerson__test_block_desc_0 *desc, GYPerson *_self, int flags=0) : self(_self) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __GYPerson__test_block_func_0(struct __GYPerson__test_block_impl_0 *__cself) {
  GYPerson *self = __cself->self; // bound by copy

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_8s_wljbbn3d6bd0mt_kk8cxxdlc0000gn_T_GYPerson_2066b6_mi_0,self);
    }

//如果block内部是访问_name成员变量,我们来看看底层代码结构
struct __GYPerson__test_block_impl_0 {
  struct __block_impl impl;
  struct __GYPerson__test_block_desc_0* Desc;
  GYPerson *self;
  __GYPerson__test_block_impl_0(void *fp, struct __GYPerson__test_block_desc_0 *desc, GYPerson *_self, int flags=0) : self(_self) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __GYPerson__test_block_func_0(struct __GYPerson__test_block_impl_0 *__cself) {
  GYPerson *self = __cself->self; // bound by copy

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_8s_wljbbn3d6bd0mt_kk8cxxdlc0000gn_T_GYPerson_74dab0_mi_0,(*(NSString * _Nonnull *)((char *)self + OBJC_IVAR_$_GYPerson$_name)));//调用的时候确实调用了name
    }
    

由此可见,Block访问当前self或是方位类中的成员变量,都是直接捕获当前self,我们在OC中定义的方法,其实前面两个参数是 self(当前对象) 和 _cmd(当前方法对象),所有其实self是一个形参,也就是局部变量,所以你访问self的时候肯定会先把self对象给捕获到block中

访问成员变量的时候,blcok内部不会直接捕获成员变量,也是首先是把整个self对象捕获到block中,然后在通过self去访问成员变量

1.6 Block变量捕获(capture)总结

  • 为了保证block内部能够正常访问外部的变量,block有个变量捕获机制
  • block内部会专门新增一个成员来储存外边变量的值,我们称之为捕获

在这里插入图片描述

  • auto和static为什么会有这个差异?
    1. auto因为是自动变量,可能会销毁的,变量的内存可能会消失,所以blcok内部执行的时候不可能去访问变量的内存
    2. static修饰的变量是一直都保留在内存中的,所以执行block调用的时候还可以根据地址值去访问这个变量在内存中的值
  • 结论: 只要是局部变量,block访问局部变量,就会把这个变量捕获进去
  • block的内存布局图:
    在这里插入图片描述

1.7 blcok的类型

  1. block实质是一个OC对象,其有以下特征:
    • block内部有一个isa指针
    • block可以调用class方法,产看其类型

代码验证block的继承关系

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

        block();
        NSLog(@"type=====%@",[block class]);
        NSLog(@"type=====%@",[[block class] superclass]);
        NSLog(@"type=====%@",[[[block class] superclass] superclass]);
        NSLog(@"type=====%@",[[[[block class] superclass] superclass] superclass]);
//打印结果:
/**
2020-08-13 09:41:06.592599+0800 Block[3809:51707] this a block
2020-08-13 09:41:06.593054+0800 Block[3809:51707] type=====__NSGlobalBlock__
2020-08-13 09:41:06.593114+0800 Block[3809:51707] type=====__NSGlobalBlock
2020-08-13 09:41:06.593143+0800 Block[3809:51707] type=====NSBlock
2020-08-13 09:41:06.593170+0800 Block[3809:51707] type=====NSObject
*/


由此可知block确实是一个OC对象,继承体系:__NSGlobalBlock__ : __NSGlobalBlock : NSBlock : NSObject最终是继承自NSObject

  1. block的类型:

    • NSGlobalBlock (_NSConcreteGlobalBlcok):没有访问auto变量的block
    • NSMallocBlock (_NSConcreteMallocBlcok):__NSStackBlock__调用copy方法
    • NSStackBlock (_NSConcreteStackBlcok)访问了auto变量
    • 图解:在这里插入图片描述
  2. 三种block的在内存放的地方图解:在这里插入图片描述

首先我们关闭ARC环境,使用MRC环境来执行下面代码
示例代码:

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

int age = 10;
        void(^block2)(void) = ^{
            NSLog(@"block type====%d",age);
        };
NSLog(@"%@   %@   %@",[block class],[block2 class],[^{
            NSLog(@"block======%d",age);
        } class]);

//打印结果: __NSGlobalBlock__   __NSStackBlock__   __NSStackBlock__
  1. __NSStackBlock__访问变量容易出现的问题:
void(^block)(void);

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

//执行结果: block=====-272632552 没有打出我们预料的20 的值

因为block访问了auto变量,我们称之为__NSStackBlock__,它存放在栈区上,当test()被调用完成之后,这个栈区上的内存可能被销毁,这块内存里面的数据可能变成了垃圾,直接影响了block内部捕获的age的值(因为age是存在栈上的,block也是分布在栈上的,函数调用完毕,栈内存的数据可能是个垃圾数据了),而我们在此去调用block去访问,可能访问到的是一个垃圾值,栈上的特点是自动销毁

  1. 如何解决上述block存在栈区上,访问值出现问题的情况?

    • 想方设法把block的内存放到堆区上,就是把__NSStackBlock__ 类型变成__NSMallocBlock__类型(只要当前block调用copy方法)
    • 关于三种block在内存中的存放区域和复制效果图:在这里插入图片描述
  2. 在ARC环境下,属性声明block时,使用copy关键字修饰?

    • 是为了保存bloc,__NSStackBlock__的内存存在去栈区,随时可能销毁,为了保证block不销毁,需要把block从栈区复制到堆区去,使用copy修饰。
    • 在ARC环境下,系统同会自动为block调用copy方法,这样__NSStackBlock__类型的block会复制到堆区,变成了__NSMallocBlock__类型的blcok,如果是在MRC环境下block使用copy之后,如果在不用之后,需要调用release方法释放,不然会有内存泄漏,ARC环境下却不需要

1.8 答疑

  1. block底层结构中FuncPtr指向的方法(block将来需要执行的代码)放在代码区
  2. 函数调用栈就是在栈区开辟了一块区域,当调用函数了时候,会指定一块栈区的内存给这个函数用,单是函数一旦调用完毕,这个快栈区的内存就会回收,将来变成垃圾数据,后面可能给别的用,会被别的覆盖
  3. 函数外面定义的变量称之为全局变量,函数里面定义的是局部变量

1.9 block-copy(ARC环境下自block对象copy到堆上的情况)

  • 在ARC环境下,编译器会根据情况自动将栈上的block赋值到堆上,比如以下情况
    • block作为函数参数返回值时
    • 将block赋值给__strong指针(强指针)时,也会自动进行一次copy操作
    • block作为CocoaAPI中方法含有usingBlock的方法参数时
    • block作为GCD API的方法参数时
//block作为函数返回值
typedef void(^block)(void);
block test(){
    int age = 10;
    return ^{
        NSLog(@"block=========%d",age);
    };
}
//测试代码
block block2 = test();
block2();
NSLog(@"block type=======%@",[block2 class]);
//打印结果:block type=======__NSMallocBlock__ 由此可见block作为返回值时,ARC下系统回族东copy到堆上

//block有强引用时
void(^block1)(void);
int age = 10;
        block1 = ^{
            NSLog(@"--------%d",age);
        };
        NSLog(@"type=====%@",[block1 class]);
//打印结果:type=====__NSMallocBlock__  可见当block有强引用指向它时,ARC下系统会自动copy到堆上


1.10 block访问对象类型的auto变量

  • 示例代码:

    • 首先我们需要把OC转换成C++代码
    • 在使用xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m转换代码的时候,可能会遇到以下问题cannot create __weak refrence in file using manual refrence
      • 解决方案:支持ARC、指定运行时系统版本,比如
      • xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0 main.m
  • 在ARC环境下的代码转换和实现:

typedef void(^GYBlock)(void);
int main(int argc, const char * argv[]) {
    
    @autoreleasepool {
        GYBlock block;
        
        {
            GYPerson *person = [[GYPerson alloc] init];
            person.name = @"zhangsan";
            
            //__weak GYPerson *weakPorson = person;
            block = ^{
                NSLog(@"=========%@",person.name);
            };
            
        }
        
    }
    return 0;
}

//转换之后的C++代码
//block直接访问person对象时,对person有强引用
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  GYPerson *__strong person; //对person的强引用
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, GYPerson *__strong _person, int flags=0) : person(_person) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
//如果使用weakPorson来访问person,是不会对这个对象产生强引用的
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  GYPerson *__weak weakPorson;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, GYPerson *__weak _weakPorson, int flags=0) : weakPorson(_weakPorson) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

//在ARC环境下block内部捕获引用对象的变化

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->weakPorson, (void*)src->weakPorson, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->weakPorson, 3/*BLOCK_FIELD_IS_OBJECT*/);}

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


  1. 当处在栈上的Block内访问外部对象时,不会影响外部对象的生命周期(ARC和MRC环境)
  2. 当处在堆空间的Block访问外部对象时:
    • 当Block内访问的person对象(强引用修饰person)时,只要Block对象没有销毁,person对象是不会销毁的
    • 当Block内部访问的perosn对象(弱引用修饰person)时,只要出了person对象的作用域,person对象就会销毁,不受Block的影响
  • 当block内部访问了对象内类型的auto变量时:

    1. 如果block是在栈上,将不会对auto变量产生强引用
    2. 如果block被拷贝到堆上
      • copy函数内部会调用_Block_object_assign函数(copy函数调用时机:栈上的Block赋值到堆时
      • _Block_object_assign函数会根据auto变量的修饰符(__strong__weak__unsafe_unretained)做出相应的操作,类似于retain(形成强引用,和弱引用)
    3. 如果block从堆上移除
      • 会调用block内部的dispose函数(dispose调用时机:堆上的Block被废弃时
      • dispose函数内部会调用_Block_object_dispose函数
      • _Block_object_dispose函数会自动释放引用的auto变量(断开Block对auto的引用),类似于release
  • Block内部只有访问引用对象的时候会出现copydispose函数来管理auto对象的内存。

1.11 block修改auto变量的值、__block修饰符

  • 怎么在Block内部修改auto变量的值:

    1. 使用static来修饰这个auto变量
    2. 把这个auto变量修改成为全局变量
    3. 使用__blcok来修饰这个auto变量,但不会修改变量的性质
  • __blcok可以用于解决block内部无法修改auto变量值的问题

  • __block不能修饰全局变量、静态变量(static)

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

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

//使用__block修饰的auto变量在block内部的底层结构
struct __Block_byref_age_0 {
  void *__isa;
__Block_byref_age_0 *__forwarding;//这个指针指向它自己
 int __flags;
 int __size;
 int age;//储存外部变量的值 
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_age_0 *age; // by ref //block内部这个指针指向  __Block_byref_age_0这个结构体
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

int main(int argc, const char * argv[]) {

    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        __attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};
        GYBlock block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));

        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

//那么底层是如何修改变量值的
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_age_0 *age = __cself->age; // bound by ref
	//通过指针拿到封装age值对象(age->__forwarding->age),然后在把这个对象内部的age值修改成20 ,在打印出来
            (age->__forwarding->age) = 20;
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_8s_wljbbn3d6bd0mt_kk8cxxdlc0000gn_T_main_59e0d5_mi_0,(age->__forwarding->age));
        }

  • 使用__blcok修改变量的本质
    • 当使用__block修饰变量的时候,首先会把变量包装一个对象(struct __Block_byref_age_0),__Block_byref_age_0对象中有一个__forwarding成员指向它自己,还有一个isa指针,然后对象中还有一个成员就是__block修饰的对象(这个变量就是当初需要修改的变量),现在存储在这个结构体对象中,而这个整体现在又被我们的block的对象拥有着。
    • 修改过程:通过blcok内的包装对象的指针,找到包装对象,然后修改包装对象中变量的值,这样就修改完成了__main_block_impl_0->__Block_byref_age_0->__forwarding-auto变量
  • 结构图在这里插入图片描述

1.12 __block的内存管理

1.12.1block的调用过程

  • 当block在栈上时,并不会对__block变量产生引用

  • 当block被copy到堆时,会调用block内部的 copy函数,copy函数内部会调用_Block_object_assign函数,_Block_object_assign函数会对__block变量形成强引用(retain)

  • 过程图:在这里插入图片描述

  • block从堆中移除时,会调用block内部的dispose函数,dispose函数内部会调用_Block_object_dispose函数,而_Block_object_dispose函数会自动释放引用的__block变量(release)

  • 过程图:在这里插入图片描述

  • 对象类型的auto变量、__block变量的区别

    1. 相同点:
      • 当block在栈上时,对外部的变量都不会产生强引用
      • 当block拷贝到堆上时,都会内部copy函数来处理他们,当block从堆上移出时,会通过dispose函数来释放他们
    2. 不同点:
      • 当内部调用copy函数的时候,如果是对象类型的auto变量,内部对auto变量的引用,会根据当前变量的修饰符来决定是强引用,还是弱引用,而对于__block类型的变量,都是强引用
      • 修饰符:在这里插入图片描述

block一开始被申明时,__block和block都是在栈上的,由于ARC环境下对block的对象的强引用,会自动把block和__block包装的对象都拷贝(copy)到堆上,对__block包装对象是强引用

1.12.2 __block中的__forwarding指针

struct __Block_byref_age_0 {
  void *__isa;
__Block_byref_age_0 *__forwarding;//这个就是__block包装对象中的__forwarding指针
 int __flags;
 int __size;
 int age;
};

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) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};


  • 为什么需要__forwarding指针了?
    • 刚开始__blcok对象在栈上,__forwarding指针是指向自己,当blcok被复制到堆上的时候,那么__block对象也会随之赋值到堆上, 此时栈上__blcok包装对象内部的 __forwarding指针是指向复制到堆上的__block变量包装的对象,所以就保证了你无论是访问栈上还是堆上__block,只要是通过__forwarding指针访问就可以访问到堆上的__blcok对象
    • 示意图:
      在这里插入图片描述

1.12.3__blcok修饰的对象类型

OC代码:

typedef void(^GYBlock)(void);
int main(int argc, const char * argv[]) {
    
    @autoreleasepool {
        __block GYPerson *person = [[GYPerson alloc] init];
        
        GYBlock block = ^{
            NSLog(@"%p=====",person);
        };
        
        block();
        
        
    }
    return 0;
}


转换之后的block的C++代码:

typedef void(*GYBlock)(void);
struct __Block_byref_person_0 {
  void *__isa;
__Block_byref_person_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 GYPerson *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) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};


  • 如果person对象前面加上一个__weak来修饰看转换的C++代码:
typedef void(^GYBlock)(void);
int main(int argc, const char * argv[]) {
    
    @autoreleasepool {
        GYPerson *person = [[GYPerson alloc] init];
        
        __block __weak GYPerson *weakPerson = person;
        
        GYBlock block = ^{
            NSLog(@"%p=====",weakPerson);
        };
        
        block();
        
        
    }
    return 0;
}


typedef void(*GYBlock)(void);
struct __Block_byref_weakPerson_0 {
  void *__isa;
__Block_byref_weakPerson_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 GYPerson *__weak weakPerson;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_weakPerson_0 *weakPerson; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_weakPerson_0 *_weakPerson, int flags=0) : weakPerson(_weakPerson->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};


  • 由此可见 block内部有一个__Block_byref_person_0 *person指针是指向__block包装的结构体对象struct __Block_byref_person_0,而该对象中有一个GYPerson *person指针或则是GYPerson *__weak weakPerson指正指向真正的person对象(该指针的强弱和该访问外界person对象的强、弱指针有关,如果外界是强引用,这里也是强引用,反之则是弱引用(注意:这里仅限于ARC时会retain,MRC是不会reatin)也就是说在MRC的环境下struct __Block_byref_person_0对象在copy到堆上的时候,对象中的person指针不会对Person对象产生一个强引用,而是弱引用,在MRC的环境下,这个指针都是弱指针,不会因为访问对象时的强弱引用,而产生强弱之分)。

1.13 block的循环引用问题

  • 测试代码:

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN
typedef void(^testBlock)(void);
@interface GYPerson : NSObject

/** <#descrption#> */
@property (nonatomic, assign) int age;


/** <#descrpition#> */
@property (nonatomic, copy) testBlock block;

@end

NS_ASSUME_NONNULL_END

#import "GYPerson.h"

@implementation GYPerson

- (void)dealloc
{
    NSLog(@"%s",__func__);
}
@end

//测试代码
int main(int argc, const char * argv[]) {
    
    @autoreleasepool {
        GYPerson *person = [[GYPerson alloc] init];
        person.age = 10;
        person.block = ^{
            NSLog(@"ags is a  === %d",person.age);
        };
    
    }
    
    NSLog(@"111111111111111");
    return 0;
}

当我们执行完成程序之后, person对象并没有销毁,接下我们看看底层结构代码

//block的底层结构
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  //block内部有一个强引用引用这person对象
  GYPerson *__strong person;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, GYPerson *__strong _person, int flags=0) : person(_person) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};


由此可以产生循环引用的本质: 两个对象互相强引用对方
在这里插入图片描述

1.13.1 ARC环境下如何解决循环引用

在ARC环境下可以使用__weak__unsafe_unretained解决循环引用的问题

 GYPerson *person = [[GYPerson alloc] init];
        person.age = 10;
        
        //__unsafe_unretained typeof(person) weakPerson = person;
        __weak typeof(person) weakPerson = person;
        person.blcok = ^{
            NSLog(@"age is a===%d",weakPerson.age);
        };

//__unsafe_unretained 修饰
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  GYPerson *__unsafe_unretained weakPerson;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, GYPerson *__unsafe_unretained _weakPerson, int flags=0) : weakPerson(_weakPerson) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

//__weak修饰
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  GYPerson *__weak weakPerson;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, GYPerson *__weak _weakPerson, int flags=0) : weakPerson(_weakPerson) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};


代码运行结果:在这里插入图片描述

由上述代码我们可以看出,当我们使用__weak__unsafe_unretained来修饰的person变量的时候, 我们可以发现block内部对person对象是一个弱引用,这样就解决了循环引用的问题

1.13.2 __weak__unsafe_unretained的区别

  • __weak:不会产生强引用,当__weak指向的对象销毁了,系统把把__weak的指针储存的地址值设置成为nil,不会产生野指针的问题(__weak自己有一套管理机制)
  • __unsafe_unretained 不会产生强引用,不安全,当指向的对象销毁时,系统不会把指针指向的地址值设置为nil,如果这个时候还访问该指针指向的对象,会发生野指针现象

1.13.3 使用__block来解决循环引用(必须调用block)

  • 使用__block来解决循环引用,代码分析:
int main(int argc, const char * argv[]) {
    
    @autoreleasepool {
        //使用__block来解决循环引用
        GYPerson *person = [[GYPerson alloc] init];
        __block GYPerson* weakPerson = person;
        person.age = 10;
        person.block = ^{
            NSLog(@"ags is a  === %d",weakPerson.age);
            weakPerson = nil;
        };
        
        person.block();
    
    }
    
    NSLog(@"111111111111111");
    return 0;
}


上述代码执行结果:在这里插入图片描述

但是 如果我们不写weakPerson = nil;person.block();这两句代码,执行结果表示person对象并没有被释放在这里插入图片描述

  • 由此可知使用__block来解决循环引用 是必须要执行block代码块的,这也意味着如果你的block可能永远不执行,那么程序就会产生内存泄漏的问题(这也是该方法的缺点,但是这个方法确实可以解决循环引用)

下面我们来分析下这段代码的底层结构代码:


//__block包装的对象
struct __Block_byref_weakPerson_0 {
  void *__isa;
__Block_byref_weakPerson_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 //person对象
 GYPerson *__strong weakPerson;
};

//person中的blcok对象
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_weakPerson_0 *weakPerson; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_weakPerson_0 *_weakPerson, int flags=0) : weakPerson(_weakPerson->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};


  • 总结:上述代码实际产生了三个对象,__block、person、block,person对象持有block对象,而block对象又持有__block对象,而__block包装的对象内部又持有person对象,这样就形成了一个三角形的循环引用关系,而我们上面添加的weakPerson = nil;person.block();这两句代码实际上是断开__block包装对象内部对person对象的强引用(上面讲过,使用__blcok修饰的变量,就是__block
    包装对象内部的那个对象),导致三角循环引用关系断开,从而解决循环引用的问题

  • 关系示意图:
    在这里插入图片描述

1.13.4 MRC下的循环引用关系

  • 在MRC下面我们可以使用__unsafe_unretained来解决循环引用(注意:MRC不支持__weak
 GYPerson *person = [[GYPerson alloc] init];
        __unsafe_unretained GYPerson* weakPerson = person;
        person.age = 10;
        person.block = [^{
            NSLog(@"ags is a  === %d",weakPerson.age);
            
        } copy];
        
       [person release];

  • 使用__unsafe_unretained来修饰(不会产生强引用),就是block内部不会对person强引用,那么person的引用计数器一直到是1,当调用release方法,person就会销毁

  • MRC环境也可以使用__block来解决循环引用,( 因为我们上面讲过,在MRC环境下__blcok的包装对象是不会对person对象进行强引用的(在ARC环境下会,系统会自动执行类似reatin操作,进行强用用,尽管block是在堆上,也不会产生强引用),那么也就是说__blcok强引用person 是不存在的,导致没有构成三角循环引用的关系,所以当person当调用release方法,person就会销毁

2. 面试题

2.1 block的原理是怎么样的?本质是什么?

  • block的本质上也是一个OC对象,它的内部也有个isa指针
  • block是封装了函数调用以及函数调用环境的OC对象

2.2 _block的作用是什么?有什么使用注意点?

  • 会把修饰的变量包装成一个对象,可以解决blcok内部无法修改auto变量值的问题
  • 注意点:会对修饰的变量进行一个相应的内存管理,而且在MRC的环境下是不会对修饰的oc对象产生强引用

2.3 block的属性修饰词为什么是copy?使用block有哪些注意点?

  • block一旦没有进行copy操作,就不会再堆上,方便我们对block的声明周期进行管理
  • 使用注意循环引用的问题

2.4 block在修改NSMutableArray.需不需要添加__blcok?

  • 不需要加上__block

3. 答疑

3.1block使用strong和copy修饰的区别

  • 在ARC环境下是没有区别的,系统都把block对象copy到堆上,但是在MRC下是有区别,MRC下copy是会把block对象copy到堆上,但是strong是不会copy的
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值