美文网首页iOSiOS Kit
ios底层原理-代码块(block)的本质(二)

ios底层原理-代码块(block)的本质(二)

作者: Yasuo_4a5f | 来源:发表于2019-05-30 18:19 被阅读17次

    问题

    1.什么是block,block的本质是什么?
    2.block的属性修饰词为什么是copy?使用block有哪些使用注意?
    3.block为什么会发生循环引用?
    4.block的变量捕获究竟是怎样进行的?
    5.block在修改NSMutableArray,需不需要添加__block?
    6.__block和__weak的作用是什么?有什么使用注意点?
    7.block中访问的对象和变量什么时候会销毁?

    上文ios底层原理-代码块(block)的本质(一)中已经解决了前三个问题,本文继续探寻block的本质,补充上面三个问题,以及解答下面的问题。

    block对变量的捕获

    为了保证block内部能够正常访问外部的变量,block有个变量捕获机制

    首先我们先了解一下有些什么变量

    • auto 离开作用域就销毁,局部变量前面自动添加auto关键字。
    • static 修饰的变量为指针传递,同样会被block捕获。
    • 全局变量 跨函数访问所以需要捕获,全局变量在哪里都可以访问 ,所以不用捕获。
    block的变量捕获

    我们来验证上图是否正确
    先新建一个Student类

    @interface Student : NSObject
    @property(nonatomic,assign)NSInteger age;
    @end
    
    @implementation Student
    - (void)dealloc
    {
        NSLog(@"%@ 对象销毁了",self);
    }
    @end
    

    写下如下代码 :

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            auto int a = 1;
            static int b = 2;
            Student * studentA = [[Student alloc] init];
            void(^block)(void) = ^{
                NSLog(@"number, a = %d, b = %d", a,b);
                NSLog(@"student : %ld",(long)studentA.age);
            };
            a = 11;
            b = 22;
            block();
        }
        return 0;
    }
    

    先转化为c++代码

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int a;
      int *b;
      Student *__strong studentA;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int *_b, Student *__strong _studentA, int flags=0) : a(_a), b(_b), studentA(_studentA) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      int a = __cself->a; // bound by copy
      int *b = __cself->b; // bound by copy
      Student *__strong studentA = __cself->studentA; // bound by copy
    
                NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_2rvx1kjx5p75z7v2crt0nw8h0000gn_T_main_1a94a7_mi_0, a,(*b));
                NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_2rvx1kjx5p75z7v2crt0nw8h0000gn_T_main_1a94a7_mi_1,(long)((NSInteger (*)(id, SEL))(void *)objc_msgSend)((id)studentA, sel_registerName("age")));
            }
    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->studentA, (void*)src->studentA, 3/*BLOCK_FIELD_IS_OBJECT*/);}
    
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->studentA, 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};
    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
            auto int a = 1;
            static int b = 2;
            Student * studentA = ((Student *(*)(id, SEL))(void *)objc_msgSend)((id)((Student *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Student"), sel_registerName("alloc")), sel_registerName("init"));
            void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a, &b, studentA, 570425344));
            a = 11;
            b = 22;
            ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
        }
        return 0;
    }
    

    直接找到__main_block_impl_0函数中的a,&b,studentA,而__main_block_impl_0结构体中直接定义了一个新的a把传入的a的值赋给了__main_block_impl_0结构体中a变量,把&b的地址传给了__main_block_impl_0结构体中b指针变量,把studentA指针变量赋值给了__main_block_impl_0结构体中的studentA指针变量。
    其中studentA也是auto变量。此时此刻可以看到__main_block_impl_0中的Student *__strong studentA是__strong类型的,所以block强引用studentA对象。

    那么为什么auto 和 static会有这样的差异呢,auto变量在栈中作用域结束就会被销毁,block在执行的时候有可能自动变量已经被销毁了,因此对于自动变量一定是值传递而不可能是指针传递了,而因为传递的是值得地址,所以在block调用之前修改地址中保存的值,block中的地址是不会变得。所以值会随之改变。归根结底还是为了防止野指针的出现和节约内存开销。

    全局变量

    typedef void (^Block)(void);
    int a = 1;
    static int b = 2;
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            void(^block)(void) = ^{
                NSLog(@"number, a = %d, b = %d", a,b);
            };
            a = 11;
            b = 22;
            block();
        }
        return 0;
    }
    

    转成c++

    typedef void (*Block)(void);
    int a = 1;
    static int b = 2;
    
    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;
      }
    };
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    
                NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_2rvx1kjx5p75z7v2crt0nw8h0000gn_T_main_883251_mi_0, a,b);
            }
    
    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; 
            void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
            a = 11;
            b = 22;
            ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
        }
        return 0;
    }
    

    此时此刻,__main_block_imp_0并没有添加任何变量,直接调用全局定义的a,b。

    block里访问self是否会捕获?

    写如下代码

    @interface Student : NSObject
    @property(nonatomic,assign)NSInteger age;
    @end
    
    @interface  Student()
    {
        NSInteger sId;
    }
    @end
    @implementation Student
    - (void)catchSelf
    {
        void(^block)(void) = ^{
            NSLog(@"%@",self);
            NSLog(@"%ld",(long)self.age);
            NSLog(@"%ld",(long)self->_age);
            NSLog(@"%ld",self->sId);
        };
        block();
    }
    - (void)dealloc
    {
        NSLog(@"%@ 对象销毁了",self);
    }
    + (void)staticFunc
    {
        NSLog(@"类方法");
    }
    @end
    

    转化为c++

    struct __Student__catchSelf_block_impl_0 {
      struct __block_impl impl;
      struct __Student__catchSelf_block_desc_0* Desc;
      Student *self;
      __Student__catchSelf_block_impl_0(void *fp, struct __Student__catchSelf_block_desc_0 *desc, Student *_self, int flags=0) : self(_self) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    static void __Student__catchSelf_block_func_0(struct __Student__catchSelf_block_impl_0 *__cself) {
      Student *self = __cself->self; // bound by copy
    
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_2rvx1kjx5p75z7v2crt0nw8h0000gn_T_Student_bb73ff_mi_0,self);
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_2rvx1kjx5p75z7v2crt0nw8h0000gn_T_Student_bb73ff_mi_1,(long)((NSInteger (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("age")));
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_2rvx1kjx5p75z7v2crt0nw8h0000gn_T_Student_bb73ff_mi_2,(long)(*(NSInteger *)((char *)self + OBJC_IVAR_$_Student$_age)));
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_2rvx1kjx5p75z7v2crt0nw8h0000gn_T_Student_bb73ff_mi_3,(*(NSInteger *)((char *)self + OBJC_IVAR_$_Student$sId)));
        }
    static void _I_Student_catchSelf(Student * self, SEL _cmd) {
        void(*block)(void) = ((void (*)())&__Student__catchSelf_block_impl_0((void *)__Student__catchSelf_block_func_0, &__Student__catchSelf_block_desc_0_DATA, self, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    

    找到_I_Student_catchSelf函数也就是我们定义的catchSelf函数其中默认传了两个参数Student * self, SEL _cmd

    static void _C_Student_staticFunc(Class self, SEL _cmd) {
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_2rvx1kjx5p75z7v2crt0nw8h0000gn_T_Student_bb73ff_mi_5);
    }
    

    可以发现我们定义的类方法也是默认传了这两个参数
    此时此刻看到我们的catchSelf方法

    - (void)catchSelf
    {
        void(^block)(void) = ^{
            NSLog(@"%@",self);
            NSLog(@"%ld",(long)self.age);
            NSLog(@"%ld",(long)self->_age);
            NSLog(@"%ld",self->sId);
        };
        block();
    }
    

    在block中我使用了sId成员变量self->_age实例变量以及age的get方法,但在__Student__catchSelf_block_impl_0中只是捕获了self(指向调用者)

    注意

    • self.age调用的是get方法
    • self->sId 分类的成员变量 通过地址获取
    • self->_age 属性的实例变量 通过地址获取

    block中访问对象和变量的销毁

    直接导入我们上边写过的Student类 写如下代码

    typedef void (^Block)(void);
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            Block block;
            {
                NSLog(@"开始");
                auto int a = 5;
                Student *student = [[Student alloc] init];
                student.age = 100;
    
                block = ^{
                    NSLog(@"------block内%ld",(long)student.age);
                };
            } // 执行完毕,student没有被释放,a释放
            NSLog(@"结束");
        } // student 释放
        return 0;
    }
    
    打印内容

    大括号执行完毕之后,student对象依然不会被释放,变量a处于作用域之外已经被释放了,上述block为堆block,block里面有一个student指针student指针指向student对象。只要block还在,student就还在。block强引用了student对象
    @autoreleasepool之后block被销毁student 对象被销毁。

    查看源代码,也发现block确实如上面说的一样强引用student

    __weak作用

    对student对象进行__weak操作

    typedef void (^Block)(void);
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            Block block;
            {
                NSLog(@"开始");
                auto int a = 5;
                Student *student = [[Student alloc] init];
                student.age = 100;
                __weak Student *weakStudent = student;
                block = ^{
                    NSLog(@"------block内%ld",(long)weakStudent.age);
                };
            } // 执行完毕,student被释放,a被释放
            NSLog(@"结束");
        }
        return 0;
    }
    
    打印内容

    可以发现__weak添加之后,student在作用域执行完毕之后就被销毁了。这是为什么呢?让我们把代码转化为c++,看看究竟做了些什么操作。
    __weak修饰变量,需要告知编译器使用ARC环境及版本号否则会报错。
    crun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m -o main-arm64-weak.cpp

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

    可以看到在__main_block_impl_0中依然是用的__weak修饰weakStudent

    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结构体__main_block_impl_0的描述结构体__main_block_desc_0中多了两个参数copydispose函数

    __main_block_copy_0 和 __main_block_dispose_0

    首先我们已经说过__main_block_desc_0是block的描述,储存着block的字节大小。那两个参数又有什么用呢?
    我们就点进__main_block_copy_0这个函数。

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

    __main_block_copy_0函数中传入的都是__main_block_impl_0结构体本身。

    copy本质就是__main_block_copy_0函数,__main_block_copy_0函数内部调用_Block_object_assign函数,_Block_object_assign中传入的是student对象的地址,student对象,以及3。

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

    __main_block_dispose_0函数中传入的都是__main_block_impl_0结构体本身。

    dispose本质就是__main_block_dispose_0函数,__main_block_dispose_0函数内部调用_Block_object_dispose函数,_Block_object_dispose函数传入的参数是student对象,以及3。

    __main_block_copy_0__main_block_dispose_0函数中会根据变量是强弱指针及有没有被__block修饰做出不同的处理,强指针在block内部产生强引用,弱指针在block内部产生弱引用。被__block修饰的变量最后的参数传入的是8,没有被__block修饰的变量最后的参数传入的是3。

    _Block_object_assign函数调用时机及作用

    上文已经说过ARC会在赋值给__strong指针时自动进行copy,而此时我们就需要对student对象的引用计数做修改,以确保这个对象在block存在的时候不会被销毁。
    当block进行copy操作的时候就会自动调用__main_block_desc_0内部的__main_block_copy_0函数,__main_block_copy_0函数内部会调用_Block_object_assign函数。
    _Block_object_assign函数会自动根据__main_block_impl_0结构体内部的student是什么类型的指针,对student对象产生强引用或者弱引用。可以理解为_Block_object_assign函数内部会对student进行引用计数器的操作,如果__main_block_impl_0结构体内student指针是__strong类型,则为强引用,引用计数+1,如果__main_block_impl_0结构体内student指针是__weak类型,则为弱引用,引用计数不变。而__main_block_impl_0结构体内部的student是什么类型的指针是由外面我们定义的对象的具体类型决定。

    _Block_object_dispose函数调用时机及作用

    当block从堆中移除时就会自动调用__main_block_desc_0中的__main_block_dispose_0函数,__main_block_dispose_0函数内部会调用_Block_object_dispose函数。
    _Block_object_dispose会对student对象做释放操作,类似于release,也就是断开对student对象的引用,而student究竟是否被释放还是取决于student对象自己的引用计数。

    总结
    • 栈block
    1. 如果block是在栈上,将不会对auto变量产生强引用
    2. 栈上的block随时会被销毁,也没必要去强引用其他对象
    • 堆block

    如果block被拷贝到堆上:

    1. 会调用block内部的copy函数
    2. copy函数内部会调用_Block_object_assign函数
    3. _Block_object_assign函数会根据auto变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用

    如果block从堆上移除

    1. 会调用block内部的dispose函数
    2. dispose函数内部会调用_Block_object_dispose函数
    3. _Block_object_dispose函数会自动释放引用的auto变量(release)
    如果block在栈空间,不管外部变量是强引用还是弱引用,block都会弱引用访问对象
    如果block在堆空间,如果外部强引用,block内部也是强引用;如果外部弱引用,block内部也是弱引用

    __block作用

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

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

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

    • __block修改变量:a->__forwarding->a

    • __Block_byref_a_0结构体内部地址和外部变量a是同一地址

    __forwarding指针指向

    先来__block修饰auto变量

    typedef void (^Block)(void);
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            Block block;
            {
                __block auto int a = 5;
                block = ^{
                    NSLog(@"------block内%ld",(long)a);
                };
            }
        }
        return 0;
    }
    

    转成c++代码

    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
            Block block;
            {
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_2rvx1kjx5p75z7v2crt0nw8h0000gn_T_main_b48be2_mi_0);
            __attribute__((__blocks__(byref))) auto __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 5};
            block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
            }
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_2rvx1kjx5p75z7v2crt0nw8h0000gn_T_main_b48be2_mi_2);
        }
        return 0;
    }
    

    可以发现__main_block_impl_0函数中传的不再是变量a而是__Block_byref_a_0类型的变量a的地址&a

    查看__Block_byref_a_0结构体

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

    此时此刻 已经很清晰了变量a包装成了__Block_byref_a_0 结构体,__forwarding指向的就是这个就是这个结构体自己的地址,__Block_byref_a_0 结构体中a变量才是存储值的地方

    • __isa指针 :__Block_byref_a_0中也有isa指针也就是说__Block_byref_a_0本质也一个对象。
    • __forwarding__forwarding__Block_byref_a_0结构体类型的,并且__forwarding存储的值为(__Block_byref_a_0 *)&a,即结构体自己的内存地址。
    • __flags :0
    • __sizesizeof(__Block_byref_a_0)__Block_byref_a_0所占用的内存空间。
    • a :真正存储变量的地方,这里存储局部变量5。
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      __Block_byref_a_0 *a = __cself->a; // bound by ref
    
                NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_2rvx1kjx5p75z7v2crt0nw8h0000gn_T_main_b48be2_mi_1,(long)(a->__forwarding->a));
            }
    

    调用block时,先取出__main_block_impl_0中的a,通过a结构体拿到__forwarding指针,上面提到过__forwarding中保存的就是__Block_byref_a_0结构体本身,这里也就是a(__Block_byref_a_0),在通过__forwarding拿到结构体中的a(5)变量并修改其值。

    __forwarding指针的作用

    __forwarding指针指向的是结构体自己。当使用变量的时候,通过结构体找到__forwarding指针,在通过__forwarding指针找到相应的变量。这样设计的目的是为了方便内存管理。block被复制到堆上时,会将block中引用的变量也复制到堆中。

    __forwarding指针
    此时此刻__forwarding函数的操作为了方便堆栈的切换,内存的管理。

    __block修饰对象变量
    先写如下代码

    typedef void (^Block)(void);
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            Block block;
            {
                __block Student *student = [[Student alloc] init];
                student.age = 100;
                block = ^{
                    NSLog(@"------block内%ld",(long)student.age);
                };
            }
        }
        return 0;
    }
    

    查看源码

    typedef void (*Block)(void);
    struct __Block_byref_student_0 {
      void *__isa;
    __Block_byref_student_0 *__forwarding;
     int __flags;
     int __size;
     void (*__Block_byref_id_object_copy)(void*, void*);
     void (*__Block_byref_id_object_dispose)(void*);
     Student *__strong student;
    };
    

    我们对比上面的__Block_byref_a_0结构体 ,__Block_byref_student_0中添加了内存管理的两个函数__Block_byref_id_object_copy__Block_byref_id_object_dispose

    上文提到当block中捕获对象类型的变量时,block中的__main_block_desc_0结构体内部会自动添加copydispose函数对捕获的变量进行内存管理。
    那么同样的当block内部捕获__block修饰的对象类型的变量时,__Block_byref_person_0结构体内部也会自动添加__Block_byref_id_object_copy__Block_byref_id_object_dispose对被__block包装成结构体的对象进行内存管理。

    block内存在栈上时,并不会对__block变量产生内存管理。当blcokcopy到堆上时
    会调用block内部的copy函数,copy函数内部会调用_Block_object_assign函数,_Block_object_assign函数会对__block变量形成强引用(相当于retain)

    __block copy内存管理

    当block从堆中移除的话,就会调用dispose函数,也就是__main_block_dispose_0函数,__main_block_dispose_0函数内部会调用_Block_object_dispose函数,会自动释放引用的__block变量。

    __block 释放内存管理

    block内部决定什么时候将变量复制到堆中,什么时候对变量做引用计数的操作。

    __block修饰的变量在block结构体中一直都是强引用,而其他类型的是由传入的对象指针类型决定。

    block在修改NSMutableArray,需不需要添加__block?

    typedef void (^Block)(void);
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            NSMutableArray *array = [NSMutableArray array];
            Block block = ^{
                [array addObject: @"c++"];
                [array addObject: @"c#"];
                NSLog(@"%@",array);
            };
            block();
        }
        return 0;
    }
    

    command + r

    打印内容

    在block块中仅仅是使用了array的内存地址,往内存地址中添加内容,并没有修改arry的内存地址,因此array不需要使用__block修饰也可以正确编译。同理add 和remove方法都不需要__block,同理 NSMutableDictionary,NSMutableSet,NSMutableStringNSMutable.....都是同一个道理。

    归根结底,了解这么多并不只是为了知道怎么防止循环引用,学到的应该是学习进阶的方式,还有设计模式和逻辑结构。

    到此block底层篇已经结束了,如有补充或交流请加微信


    微信二维码

    相关文章

      网友评论

        本文标题:ios底层原理-代码块(block)的本质(二)

        本文链接:https://www.haomeiwen.com/subject/eshytctx.html