Block

本文要思考的问题

本文将会解决的如下疑问
1. block内存如何管理
2. 为什么会造成循环引用
3. 延时释放的原理
4. block的实现原理的本质推演

Block概念

一句话:”一个能捕获局部变量,且保存了可执行代码的一个对象“,对象类型有isa指向,__NSGlobalBlock__,__NSMallocBlock__,__NSStckBlock__。

使用clang 将OC代码转换成C++文件。在命令行输入clang -rewrite-objc需要编译的OC文件.m

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSLog(@"Hello, World!");
        int a = 1;
        int (^RJBblock)(void) = ^(void){
            NSLog(@"这是值%d",a);
            return 1;
        };
        RJBblock();
    }
    return 0;
}
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;
  //在c++中,结构体是可以用构造函数初始化的
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static int __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_jv_wzmk5z514_17z63gkvg7fk0h0000gn_T_main_20bcfd_mi_1,a);
            return 1;
        }

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[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_jv_wzmk5z514_17z63gkvg7fk0h0000gn_T_main_20bcfd_mi_0);
        int a = 1;
        //c++里的一个对象的初始化,用void *fp 指向函数的指针进行初始化。__main_block_func_0 就是一函数。也就是block括号里的东西。
        //普通的传值,是会将这个变量传进去的
        int (*RJBblock)(void) = ((int (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));

        //
        ((int (*)(__block_impl *))((__block_impl *)RJBblock)->FuncPtr)((__block_impl *)RJBblock);
    }
    return 0;
}

上面的表示,就是一个Block的实现。

Block内存管理

block存储区域

  • block没有访问外部变量
    block始终在Global区,无论当前环境是ARC还是MRC

  • MRC

    • block 如若果访问外部变量,block在栈里
    • 只要使用copy,才能放到堆里
  • ARC

    • block如果访问外部变量,block在堆里(因为arc默认将其拷贝了)
    • block可以使用strong,copy,并且block是同一个对象

__block变量

我们通过clang -rewrite-objc 查看如何实现的。由下面的可以看出。__block也有自己的结构体实现,传进去的是传值。再通过 __block结构体力的_forwarding,保证了访问时,block拷贝还是没拷贝的_block变量访问的正确性。拷贝后_forwarding指向堆里,没拷贝_forwarding指向栈里。

_struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_a_0 *a; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static int __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_a_0 *a = __cself->a; // bound by ref

            (a->__forwarding->a) = 2;
            return 1;
        }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 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};
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_jv_wzmk5z514_17z63gkvg7fk0h0000gn_T_main_e97dd6_mi_0);
        __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 1};
        int (*RJBblock)(void) = ((int (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
        ((int (*)(__block_impl *))((__block_impl *)RJBblock)->FuncPtr)((__block_impl *)RJBblock);
    }
    return 0;
}

Block循环引用

  • 费曼输出:self引用block,block引用self
    如果对此理解的不是很清楚,而只是一味按照其他人说的避免循环引用的方式。就没有抓住本质,也就不能创造性的解决问题。
    self引用block,就是self持有这个block,block引用self,是因为block代码里使用到了self,或者self.property,_ivr,block默认会对这个self,实现强引用。
    解决的办法是不让block对self进行强引用。也就是block不对self进行一次release的权限。(这里的self指的是直接强引用block的对象)

  • 延时释放(重点,这个问题我想了好久,是如何办到的?)
    但有时会遇到,需要在block执行时,self一定要存在。即需求是延时self的释放。尽管有时,self释放后,执行block时,不会对逻辑造成影响(对nil发起的操作不做任何反应)。这时。我们需要在在block里对self的一次强引用。那有人会问,在block外面进行强引用,与在block里面进行强引用,不都是强引用吗?_为啥,在外面强引用会造成循环引用,在block里面做强引用不会造成循环引用呢?__
    因为Block的实现原理导致。请看Block的实现原理(高潮来了)

Block的实现原理

  • 费曼输出:Block在内存里实际是一个对象,而在Object-C里,对象实际是一个结构体。 Block结构体里保存了“实现”,“描述”,“捕获的变量”。“实现”里保存了isa,funcPtr,而isa是这个对象类型,funcPtr就是函数指针,指向那个可执行的一个函数。函数的数据来源就是这个Block里捕获的变量

代码如下
oc代码

test1() {
    int a = 10;
    void (^block)()= ^{
        NSLogt("%d",a);
    }
    a = 20;
    block();
}

int main(int argc,const char *argv[]){    
    test1();
    return 0;
}

编译成c++代码

struct __test1_block_impl_0 {
        struct __block_imple impl;
        struct__test1_block_desc_0 *Desc_0;
        _int a;
        __test1_block_impl_0(void *fp,struct_test1_block_desc_0*Desc,int _a,int flag =0):a(_a) {
            impl.isa = &_NSConcreteStackBlock;
            impl.Flags = flags;
            impl.FuncPtr = fp;
            Desc = desc;
        }
    };
    
static void __test1_block_func_0(struct __test1_block_imp_0 *__cself) {
    int a = __cself->a;
    NSLog(a);
}

void test1 () {
    int a  = 10;
    //block的定义转换成c++的定义,这是block的初始化(c++结构体的初始化),生成block。
    void(*block)() = (void (*)())&__test1_block_imp_0((void *))__test1_block_func_0,&__test1_block__desc_0_DATA,a);
    a = 20;
     //执行block   
    ((void (*)(__block_impl *))((__block_imp *)block)->FuncPtr)((_block_impl *)block);
}
int main(itn argc,const char *argv[]){
    test1();
    return 0;
}

简单推演
1. _int a; 是捕获的变量
2. __test1_block_func_0 ;是可执行的代码
3.执行block时,实际是执行声明的block里的FuncPtr,并将自己当做参数传了进去。

我们回到延时释放的解释:从这里就能解释为什么从外面强引用会造成循环引用,因为block的结构体里实现了对self的捕获。并强引用了。从里面实现强引用,为什么不造成循环引用,是因为,从里面是在__test1_block_func_0函数里了。是一个局部变量了。block执行完时,自然会对这个局部变量实行释放。哈哈。终于揭开了疑惑。!!!

对于__block变量的捕获原理,其实本质是一样的。只不过,是在捕获int a是传值进去。而__block变量是传地址进去。(特别说明:__block,static的变量,全局变量都是传指针)

这里是block的实现。(这里有个遗憾,没有模拟出??)
当外面的对象捕获是强引用时。在block的实现里也是强引用。这就导致当引用的是“对block有强引用的对象”时出现循环引用情况。若果对外面是弱引用。在block里就是弱引用。当想延迟时释放时。可以在block里加强引用。这会将其当做是局部变量,也就是在栈上。

//RJBObject.h
typedef void(^RJBBlock)(void);

@interface RJBObject : NSObject
@property (nonatomic, strong) RJBBlock block;
@property (nonatomic, assign) int a;
@end

//main.m
#import "RJBObject.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        RJBObject *objct = [[RJBObject alloc]init];
        objct.block = ^{
            NSLog(@"这是a的值%d",weakObj.a);
        };
    }
    return 0;
}
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  RJBObject *objct;//此处是捕获的变量。因为捕获时时强引用,则这里就是强引用导致了+1;若果外面是弱引用,则这里就不会+1.就不会造成强引用。
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, RJBObject *_objct, int flags=0) : objct(_objct) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

block实践感悟

block是强大的。可以做保存代码,可以当返回值,可以当参数,总之,将其当一般对象使用,外加他能保存代码并执行,那就简直强大无比了。Masory里将block做返回值实现链式编程,就是一个很好的实践。

2018/8/19 posted in  iOS基础概念