Block 深入研究

Block的本质

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

首先,我们利用clang 命令查看一下声明Block对应的c++代码(

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

)main.m 是文件名

image.png

转换为C++

image.png

block 声明删减一部分转化代码,如下

image.png

查找转换后的C++代码,其实block是有结构体组成的,具体形式如下:

image.png

也可以理解为

image.png

因此,这个block结构体 包含了isa 指针

再查找 __main_block_impl_0 函数

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

  }

block实现的函数 的isa 指向_NSConcreteStackBlock 对象,因此说明block 是oc对象

我们还可以自定义block结构体,用于探测block 内部实现过程

struct __main_block_desc_0 {

    size_t reserved;

    size_t Block_size;

};

struct __block_impl {

    void *isa;

    int Flags;

    int Reserved;

    void *FuncPtr;

};

struct __main_block_impl_0 {

    struct __block_impl impl;

    struct __main_block_desc_0* Desc;

    int age;

};

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

    @autoreleasepool {

        int age = 20;

        void (^block)(int, int) =  ^(int a , int b){

            NSLog(@"this is a block! -- %d", age);

            NSLog(@"this is a block!");

        };

        struct __main_block_impl_0 *blockStruct = (__bridge struct __main_block_impl_0 *)block;

         block(10, 10);

    }

    return 0;

}
image.png

为什么block内部修改变量没有效果

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

    @autoreleasepool {

        int age = 10;

       void (^block)(void) = ^{

            // age的值捕获进来(capture)

            NSLog(@"age is %d", age);

        };

        age = 20;

        block();

    }

    return 0;

}

为什么age的值被修改了,但是打印得到的是修改之前的值呢?

没有其他参考途径,我们只有接着cpp文件

//相当于OC对象

struct __main_block_impl_0 {

  struct __block_impl impl;

  struct __main_block_desc_0* Desc;

  int age;

    //相当于OC 构造函数

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

    // age(_age) 相当于 把_age的值赋给age 此时所生成的block对象中的age的值就是_age,也就是传递过来的10

  }

};

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

    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;

        int age = 10;

        void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));

        //相当于调用了__main_block_impl_0函数,并把age这个变量的值传递了过去,

//        void (*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, age);

        age = 20;

        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

    }

    return 0;

}

//因此在blcok 外部改变变量的值  block内部并没有发生改变

为什么 static 修饰的变量可以更改值

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

    @autoreleasepool {

        int age = 10;

        static int height = 10;

        void (^block)(void) = ^{

            // age的值捕获进来(capture)

            NSLog(@"age is %d, height is %d", age, height);

        };

        age = 20;

        height = 20;

        block();

    }

    return 0;

}

//c++ 

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

    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;

        int age = 10;

        static int height = 10;

        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;

}

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

  int age = __cself->age; // bound by copy

  int *height = __cself->height; // bound by copy

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_4z_4p5xc0z55l38rrjdfyrzdxbw0000gn_T_main_2d6064_mi_1, age, (*height));

}

通过以上代码,我们发现static 修饰的变量传递的是地址,在block内部函数也访问的是(*height)而没static修饰的age则直接访问的是变量

为什么static 修饰就传递地址呢? 因为static修饰的变量 属于静态变量,首次被加载时static定义的变量被分配空间,程序结束后由系统释放.所以它在内存中是一直存在的,而被修饰的变量,在一定条件下是会被释放的,这是系统为了保持正确性,就把它的值传递过去。

放上一个别人总结的图,我觉得他能把上面的意思完全概括:

image.png

block的类型

前面说过block是oc对象,所以我们可以调用oc方法来查看它的父类,以及父类的父类

        void (^block)(void) = ^{

            NSLog(@"Hello");

        };

        NSLog(@"%@", [block class]);//__NSGlobalBlock__

        NSLog(@"%@", [[block class] superclass]);//__NSGlobalBlock

        NSLog(@"%@", [[[block class] superclass] superclass]);//NSBlock

        NSLog(@"%@", [[[[block class] superclass] superclass] superclass]);//NSObject

所以很好的证明了block是OC对象。

但是在不同环境下,block的类型是不同的

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

*   __NSGlobalBlock__( _NSConcreteGlobalBlock)

*   __NSStackBlock__( _NSConcreteStackBlock )

*   __NSMallocBlock__( _NSConcreteMallocBlock )

        void (^block1)(void) = ^{

            NSLog(@"Hello");

        };

        int age = 10;

        void (^block2)(void) = ^{

            NSLog(@"Hello - %d", age);

        };

        NSLog(@"%@ %@ %@", [block1 class], [block2 class], [^{

            NSLog(@"%d", age);  //__NSGlobalBlock__ __NSMallocBlock__ __NSStackBlock__

        } class]);
image.png
image.png

何为auto变量?

其实我们在定义局部变量时,系统会默认的添加 auto 关键字

 //  特别注意是在MRC环境下,因为ARC环境,运行时会做一系列的操作

// Global:没有访问auto变量 

    void (^block1)(void) = ^{

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

    };

    // Stack:访问了auto变量

    int age = 10; // 相当于 auto int age = 10;

    void (^block2)(void) = ^{

        NSLog(@"block2---------%d", age);

    };

    NSLog(@"%@ %@", [block1 class], [block2 class]);//__NSGlobalBlock__ __NSStackBlock__

Block内部实现中(static struct __main_block_desc_0)的两种形态

第1种

static struct __main_block_desc_0 {

    size_t reserved;

    size_t Block_size;

};

**第二种**

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

相比较两种 __main_block_desc_0 会发现 第二种(__main_block_desc_0)多出来两个函数(copy,dispose),这是因为在block引用对象变量才会出现第二种情况(因为引用对象变量时,要涉及内存管理,要确保使用的对象不会出现内存问题,因此block内部也会对其使用的对象做【return/release】)

block的copy

在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,比如以下情况

block作为函数返回值时

将block赋值给__strong指针时

block作为Cocoa API中方法名含有usingBlock的方法参数时

block作为GCD API的方法参数时

如果block被拷贝到堆上

1、会调用block内部的copy函数

2、copy函数内部会调用_Block_object_assign函数

3、_Block_object_assign函数会根据变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(return)或弱引用

 block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, person, 570425344));

//__main_block_desc_0  相比较之前没有强弱引用变量时,多出来copy  dispose 函数

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

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

如果block从堆上移除

1、会调用block内部的dispose函数

2、dispose函数内部会调用_Block_object_dispose函数

3、_Block_object_dispose函数会自动释放引用的变量(release)

image.png

__block修饰符

有时候我们要在block 中修改block之外的变量值,可以用static修改此变量,也可以设置为全局变量,但是我们有时只是临时用下此变量,没有必要做如此复杂的操作

__block可以用于解决block内部无法修改auto变量值的问题

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

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

(分别讨论一下(基本数据类型) int 和 对象的区别)

我们都知道__block修饰的变量 可以再block内部对他进行修改,但这是为什么呢?

__block修饰的变量会在运行时,由系统封装成为 (Block_byref变量名序号)结构体类型的对象,我们在block内部和之后访问的变量 都是转换过后的类型

   __block int age = 10;

//        __attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};

        __block NSObject *obj = [NSObject new];

       //        __attribute__((__blocks__(byref))) __Block_byref_obj_1 obj = {(void*)0,(__Block_byref_obj_1 *)&obj, 33554432, sizeof(__Block_byref_obj_1), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("new"))};

        HFBlock block = ^{

            age = 20;

            obj = nil;

//  __Block_byref_age_0 *age = __cself->age; // bound by ref

//  __Block_byref_obj_1 *obj = __cself->obj; // bound by ref

//

//            (age->__forwarding->age) = 20;

//            (obj->__forwarding->obj) = __null;

            NSLog(@"age is %d", age);

        };

        NSLog(@"%p", &age);//对应转换后   

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_4z_4p5xc0z55l38rrjdfyrzdxbw0000gn_T_main_218c5b_mi_1, &(age.__forwarding->age));   我们确定他访问的是对象中的age

转换后

struct __Block_byref_age_0 {

  void *__isa;

__Block_byref_age_0 *__forwarding;

 int __flags;

 int __size;

 int age;

};

struct __Block_byref_obj_1 {

  void *__isa;

__Block_byref_obj_1 *__forwarding;

 int __flags;

 int __size;

 void (*__Block_byref_id_object_copy)(void*, void*);

 void (*__Block_byref_id_object_dispose)(void*);

 NSObject *__strong obj;

};

通过上述代码我们得出结论,变量在声明__block后 就会转为其他对象形式,并且在声明以后用到的次变量都是__Block_byref _ 变量名 _序号 类型的

相比较基本数据类型转换的类型,对象类型的属性转换后,会多出来(__Block_byref_id_object_copy,__Block_byref_id_object_dispose)两个函数,这是因为,对象类型要涉及到内存管理,这两个函数是内存管理所用到的

为了从内存地址角度证明 我们在block外访问的变量也是转换过的类型,

typedef void (^HFBlock) (void);

struct __Block_byref_age_0 {

    void *__isa;

    struct __Block_byref_age_0 *__forwarding;

    int __flags;

    int __size;

    int age;

};

struct __main_block_desc_0 {

    size_t reserved;

    size_t Block_size;

    void (*copy)(void);

    void (*dispose)(void);

};

struct __block_impl {

    void *isa;

    int Flags;

    int Reserved;

    void *FuncPtr;

};

struct __main_block_impl_0 {

    struct __block_impl impl;

    struct __main_block_desc_0* Desc;

    struct __Block_byref_age_0 *age;

};

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

    @autoreleasepool {

        __block int age = 10;

        __block NSObject *obj = [NSObject new];

        HFBlock block = ^{

            age = 20;

            obj = nil;

            NSLog(@"age is %d", age);

        };

        struct __main_block_impl_0 *blockImpl = (__bridge struct __main_block_impl_0 *)block;

        NSLog(@"%p", &age);//0x100425288

    }

    return 0;

}

我们需按照cpp代码的思路自定义几个机构体,来强制转换以此来窥看系统内部的操作,

(lldb) p/x blockImpl->age

(__Block_byref_age_0 *) $4 = 0x0000000100425270

(lldb) p/x &(blockImpl->age->age)

(int *) $5 = 0x0000000100425288

由此我们看到,block中age对象 距离访问的age的值 相差24个字节,我们再看age结构体

struct __Block_byref_age_0 {//假设这个对象的内存地址是0x0000000100425270

    void *__isa;//指针8个字节

    struct __Block_byref_age_0 *__forwarding;//指针8个字节

    int __flags;//4个字节

    int __size;//4个字节

    //age 的地址是 0x0000000100425270 + 8 + 8 + 4 + 4  = 0x0000000100425288

    int age;

};

这和我们的打印结果完全吻合,由此再次得到证明

__block的内存管理

当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变量

当block在栈上时,对它们都不会产生强引用

当block拷贝到堆上时,都会通过copy函数来处理它们

  • __block变量(假设变量名叫做a)

  • _Block_object_assign((void)&dst->a, (void)src->a, 8/BLOCK_FIELD_IS_BYREF/);

  • 对象类型的auto变量(假设变量名叫做p)

  • _Block_object_assign((void)&dst->p, (void)src->p, 3/BLOCK_FIELD_IS_OBJECT/);

当block从堆上移除时,都会通过dispose函数来释放它们

  • __block变量(假设变量名叫做a)

  • _Block_object_dispose((void)src->a, 8/BLOCK_FIELD_IS_BYREF*/);

  • 对象类型的auto变量(假设变量名叫做p)

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

被__block修饰的对象类型

  • 当__block变量在栈上时,不会对指向的对象产生强引用

  • 当__block变量被copy到堆时

  • 会调用__block变量内部的copy函数

  • copy函数内部会调用_Block_object_assign函数

  • _Block_object_assign函数会根据所指向对象的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用(注意:这里仅限于ARC时会retain,MRC时不会retain)

  • 如果__block变量从堆上移除

  • 会调用__block变量内部的dispose函数

  • dispose函数内部会调用_Block_object_dispose函数

  • _Block_object_dispose函数会自动释放指向的对象(release)

__block的__forwarding指针

image.png

你可能感兴趣的:(Block 深入研究)