美文网首页iOS开发进阶
iOS原理篇(五):Block探究

iOS原理篇(五):Block探究

作者: 75b9020bd6db | 来源:发表于2019-09-27 09:30 被阅读0次
    • Block原理
    • Block变量捕获
    • Block类型
    • copy操作和Block内部访问对象类型的变量
    • __block修改变量及其本质
    • __block内存管理
    • Block循环引用问题

    Block是一种可以在CC++以及Objective-C代码中使用,类似于“闭包(closure)”的代码块,借助Block机制,开发者可以将代码像对象一样在不同的上下文环境中进行传递。
    (这里说的不同上下文环境,我举个例子:比如在A函数中定义了一个变量,它是一个局部变量,那么我要在B函数中去访问,这里就属于两个不同的上下文环境)

    一、Block原理

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

    将上面main.m编译生成C++代码:

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

    main()函数

    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
            int age = 20;
            void (*block)(int,int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
            ((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 3, 5);
        }
        return 0;
    }
    

    __main_block_impl_0结构体

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int 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;
      }
    };
    
    struct __block_impl {
        void *isa;
        int Flags;
        int Reserved;
        void *FuncPtr;
    };
    
    struct __maib_block_desc_0 {
        size_t reserved;
        size_t Block_size;
    };
    

    我们定义block变量,其实下面这句代码:

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

    就是调用结构体__main_block_impl_0内部的构造函数初始化一个结构体出来,然后取结构体地址&__main_block_impl_0赋给block指针,所以block底层是下面结构体;调用构造函数传了三个参数:
    (void *)__main_block_func_0&__main_block_desc_0_DATAage

    其中(void *)__main_block_func_0是下面函数的地址,这个函数就是封装了block执行逻辑的函数,通过上面的构造函数传给__block_impl结构体的FuncPtr

    static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) {
      int age = __cself->age; // bound by copy
    
        NSLog((NSString *)&__NSConstantStringImpl__var_folders__h_yv1h9mrx1155q6tq7brn8b9m0000gp_T_main_b54551_mi_0,a,b,age);
    }
    

    同样,第二个参数类型&__main_block_desc_0_DATA是下面结构体地址,最终通过构造函数赋给了Desc,其中Block_size表示block结构体的大小;

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

    main()函数中调用block(3, 5)最终转化为下面代码,通过将block强制转换为__block_impl(这里__block_impl类型是__main_block_impl_0结构体第一个成员,所以可以转) ,最终直接找到impl中的FuncPtr进行调用

    ((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 3, 5);
    

    二、Block变量捕获

    Block变量捕获是指在Block内部访问外部变量时,如果外部变量是局部变量,则Block内部会将其捕获,具体捕获形式看外部的这个局部变量是auto类型还是static类型:
    如果是auto类型,直接将变量的值传递给Block内部,Block结构体内部会生成一个变量来存储传进来的值,所以在Block外边改变age=20,调用block()时内部打印的结果依然是age=10,因为此时进行的是值传递;
    如果是static类型,会将变量的地址传递给Block内部,block结构体内部会生成一个指针变量来存储传进来的地址值,所以在block外边改变height=20,调用block()时内部打印的结果是height=20,因为此时进行的是指针传递;

    下面进行验证:

    1. 局部变量两种情况:
    // 局部变量两种情况
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // case-1: auto变量,离开作用域就销毁
            auto int age = 10; //等价于 int age = 10;
            // case-2: static变量
            static int height = 10;
            
            void (^block)(void) = ^{
                NSLog(@"age is %d, height is %d",age, height);
            };
            age = 20;
            height = 20;
            
            block();
        }
        return 0;
    }
    

    打印结果:

    age is 10, height is 20
    

    编译成C++:

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int age;
      int *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;
      }
    };
    

    从编译生成的结构体看出,age是值传递,height是指针传递;定义完block就将10&height捕获到block内部,后边调用block时访问的结构体内部age是捕获到的值10,height是捕获到的地址&height

    1. 全局变量:因为是在全局区,所以任何函数内部可以直接访问

    总结一下:


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

    三、Block类型

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

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            void (^block)(void) = ^{
                NSLog(@"Hello World!");
            };
            
            NSLog(@"%@",[block class]);
            NSLog(@"%@",[[block class] superclass]);
            NSLog(@"%@",[[[block class] superclass] superclass]);
            NSLog(@"%@",[[[[block class] superclass] superclass] superclass]);
        }
        return 0;
    }
    
    打印结果:
    05-block类型[46881:69217078] __NSGlobalBlock__
    05-block类型[46881:69217078] __NSGlobalBlock
    05-block类型[46881:69217078] NSBlock
    05-block类型[46881:69217078] NSObject
    

    三种类型:

    • __NSGlobalBlock__ (_NSConcreteGlobalBlock)
    • __NSStackBlock___NSConcreteStackBlock
    • __NSMallocBlock___NSConcreteMallocBlock
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            void (^block1)(void) = ^{
                NSLog(@"Hello");
            };
            int age = 10;
            void (^block2)(void) = ^{
                NSLog(@"Hello - %d", age);
            };
            NSLog(@"%@ %@ %@",[block1 class], [block2 class], [^{
                NSLog(@"%d",age);
            } class]);
        }
        return 0;
    }
    
    打印结果:
    05-block类型[47475:69339707] __NSGlobalBlock__ __NSMallocBlock__ __NSStackBlock__
    
    

    那不同类型Block分别对应什么情况呢?

    static int height = 30;
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            
            // 没有访问外部变量
            void (^block1)(void) = ^{
                NSLog(@"--------------");
            };
            
            // 访问 auto 变量
            int age = 10;
            void (^block2)(void) = ^{
                NSLog(@"--------------%d", age);
            };
            
            // 访问 static 变量
            void (^block3)(void) = ^{
                NSLog(@"=-------------%d", height);
            };
            
            NSLog(@"%@ %@ %@",[block1 class], [block2 class], [block3 class]);
        }
        return 0;
    }
    
    打印结果:
    05-block类型[48630:69576321] __NSGlobalBlock__ __NSMallocBlock__ __NSGlobalBlock__
    
    

    可以看出,在没有访问外部变量的情况下,block1是一个__NSGlobalBlock__类型,存放在数据区,此时的block1就相当于我们定一个了一个函数,函数中的代码没有访问另外一个函数(此处为main())中的变量;同理,block3虽然访问外部变量,但static变量是全局的,同样相当于单独拿出去定义一个和main()函数上下文无关的函数;
    由于block2访问了auto变量,相当于在block2封装的函数中访问了另外一个函数内部的变量(main()函数中的局部变量age),此时block2变为__NSStackBlock__,因为它需要保存这个局部变量,由于是在ARC环境,会自动对__NSStackBlock__类型进行copy操作,所以 block2打印类型是一个 __NSMallocBlock__类型;

    关闭ARCMRC环境下打印:

    打印结果:
    05-block类型[49786:69814242] __NSGlobalBlock__ __NSStackBlock__ __NSGlobalBlock__
    

    可以看出block2确实是一个__NSStackBlock__类型;

    四、copy操作和Block内部访问对象类型的变量


    copy操作分MRCARC两种情况:
    • MRC环境:
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            
            int age = 10;
            
            // 情况一:没有访问外部 auto 变量
            // 它是一个 NSGlobalBlock
            // 内存是放在数据段
            void (^block)(void) = ^{
                NSLog(@"---------");
            };
            
    
            // 情况二:访问外部 auto 变量 age
            // 它是一个 NSStackBlock 随时会被回收
            // 内存是在栈上
            // 通过 copy 操作转变为 NSMallocBlock 把它放到堆上保活
            void (^block2)(void) = [^{
                NSLog(@"---------%d",age);
            } copy];
            
            // 因为在 MRC 环境 不用时要进行 release 操作
            [block2 release];
        
        }
        return 0;
    }
    
    • ARC环境:
      ARC环境下,编译器会根据情况自动将栈上的Block拷贝到堆上,即自动进行一次copy操作,比如以下情况:
    1. 情况一:Block作为函数返回值
    // 定义一个block类型
    typedef void (^DJTBlock)(void);
    
    // block作为函数返回值
    DJTBlock myblock()
    {
        // case1: 这里没有访问auto变量 是一个NSGlobalBlock
        return ^{
            NSLog(@"------------");
        };
        // 相当于下面这样写
        // DJTBlock block = ^{
        //   NSLog(@"------------");
        // };
        // return block;
        
        //-----------------------------------------------------------------
        
        // case2: 这里访问了auto 是一个NSSackBlock 作为函数返回值ARC下自动copy成NSMallocBlock
        // int age = 10;
        // return ^{
        //   NSLog(@"------------%d",age);
        // };
    }
    
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            
            // ARC环境下 调用myblock()函数
            // DJTBlock作为myblock函数的返回值 编译器自动进行一次 copy 操作
            // 所以 block变量指向的 DTJBlock 此时已经在堆上
            DJTBlock block = myblock();
            block();
            
            // 打印 block 类型
            NSLog(@"%@",[block class]);
        
        }
        return 0;
    }
    
    打印结果:
    05-block--copy[64907:9167520] ------------
    05-block--copy[64907:9167520] __NSGlobalBlock__
    

    打印结果是一个NSGlobalBlock类型,这是因为在函数my block()内部没有访问auto变量(上面block类型有阐述),而对NSGlobalBlock类型的Block执行copy操作生成的Block还是NSGlobalBlock,所以如果将返回改为myblock()函数内注释部分,就会打印__NSMallocBlock__

    1. 情况二:将Block赋值给__strong强指针时
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            
            int age = 10;
            // Block被强指针指着
            DJTBlock block = ^{
                NSLog(@"------------%d",age);
            };
            block();
           
            // 打印 block 类型
            NSLog(@"%@",[block class]);
        }
        return 0;
    }
    
    打印结果:
    05-block--copy[69520:9293376] ------------10
    05-block--copy[69520:9293376] __NSMallocBlock__
    
    1. 情况三:Block作为Cocoa API 中方法各含有usingBlock的方法参数时:
    NSArray *array = @[];
    [array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
    
    }];
    

    这个参数Block也是一个堆上的block;

    1. 情况四:Block作为GCD API的方法参数时:
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
                
    });
        
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0*NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                
    });
    

    Block内部访问对象类型的变量

    先看一个有趣的现象:

    // DJTPerson.h
    @interface DJTPerson : NSObject
    @property(nonatomic, assign) int age;
    @end
    
    // DJTPerson.m
    @implementation DJTPerson
    - (void)dealloc
    {
        NSLog(@"DJTPerson----dealloc");
    }
    @end
    
    // main.m
    #import "DJTPerson.h"
    
    typedef void (^DJTBlock)(void);
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            {
                DJTPerson *person = [[DJTPerson alloc] init];
                person.age = 10;
            }
            NSLog(@"-----------------");// 打断点
        }
        return 0;
    }
    

    在上面NSLog(@"-----------------");处打断点,运行程序发现控制台打印:

    05-block访问对象类型的auto变量[77563:9561984] DJTPerson----dealloc
    (lldb) 
    

    说明在断点前的中括号结束,person变量就已经释放,接着我们定义一个block,在内部访问personage属性:

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            
            DJTBlock block;
            
            {
                DJTPerson *person = [[DJTPerson alloc] init];
                person.age = 10;
                
                block = ^{
                    NSLog(@"-----------%d",person.age);
                };
            }
            
            NSLog(@"-----------------");// 打断点
            
        }
        return 0;
    }
    

    通用在NSLog(@"-----------------");处打断点,运行程序发现控制台无打印,说明person没被回收。

    为什么被第二种情况下person没有被回收呢?为了验证我们将代码简化并编译成C++来进行底层原理分析:

    typedef void (^DJTBlock)(void);
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            
            DJTPerson *person = [[DJTPerson alloc] init];
            person.age = 10;
            
            DJTBlock block = ^{
                NSLog(@"-----------%d",person.age);
            };
        }
        return 0;
    }
    

    日常操作命令:

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

    在编译生成的C++文件中查看生成的block结构体:

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      DJTPerson *person;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, DJTPerson *_person, int flags=0) : person(_person) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    由于personDJTPerson *类型,所以捕获到block内部也是DJTPerson *类型,即struct __main_block_impl_0结构体内部可以看到有一个DJTPerson *类型变量person;下面先从一个角度理解为什么person没有被释放:

    在上面代码中,我们定义的Block是被一个DJTBlock类型的变量block强引用的,即这句代码:

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

    ARC环境下,被强引用的这个Block(访问了auto变量)会自动拷贝到堆上,而这个Block内部(编译成C++即为struct __main_block_impl_0结构体)又有一个DJTPerson*类型的指针指向外面这个person对象,所以只要这个Block在,那么这个强指针就在,所以外边的person对象不会被释放;

    换成MRC环境:

    // DJTPerson.h
    @interface DJTPerson : NSObject
    @property(nonatomic, assign) int age;
    @end
    
    // DJTPerson.m
    @implementation DJTPerson
    - (void)dealloc
    {
       [super dealloc];
        NSLog(@"DJTPerson----dealloc");
    }
    @end
    
    // main.m
    #import "DJTPerson.h"
    
    typedef void (^DJTBlock)(void);
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            DJTBlock block;
            {
                DJTPerson *person = [[DJTPerson alloc] init];
                person.age = 10;
                
                block = ^{
                    NSLog(@"-----------%d",person.age);
                };
                
                [person release];
            }
            NSLog(@"---------------");// 打断点
        }
        return 0;
    }
    

    依然在NSLog(@"---------------");打断点,发现控制台打印结果:

    05-block访问对象类型的auto变量[83896:9803518] DJTPerson----dealloc
    

    发现person被释放,这是因为即使block内部访问了person对象,MRC环境下,block内部访问了auto变量,它是一个栈上block,但并不会自动拷贝到堆上,由于它是一个NSStackBlock,内部并不会对外部person强引用(这里说强引用并不准确,在MRC环境没有强引用说法,应该描述为没有对外边person进行retain操作,但为了好理解 so...),所以在执行完[person release]以后,虽然Block还没有离开其作用域(Block作用域到return 0;前到大括号),但person就被释放;可以通过[block copy]将其复制到堆上,这样内部就会对外边的person强引用(其实是retain操作)从而保活person,当然在Block销毁的时候,内部对person还会进行一次release操作,这样一加一减,就保持了平衡;

    要点:栈空间的BlockNSStackBlock)是不会对外边auto对象进行保活(ARC环境表现为不会强引用,MRC下表现为不会进行retain操作),只有拷贝到堆上(NSMallocBlock)才会对其自动保活。

    回到ARC环境:
    看一下__weak作用:

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            
            DJTBlock block;
            {
                DJTPerson *person = [[DJTPerson alloc] init];
                person.age = 10;
                
                // 这里使用 __weak 修饰
                __weak DJTPerson *weakPerson = person;
                block = ^{
                    NSLog(@"-----------%d",weakPerson.age);
                };
                
            }
            NSLog(@"---------------"); // 打断点
        }
        return 0;
    }
    

    依然在NSLog(@"---------------");处打断点,打印结果为:

    05-block访问对象类型的auto变量[87323:9930285] DJTPerson----dealloc
    

    这说明,即使在ARC环境,Block被拷贝到堆上,由于我们用__weak类型的__weakPerson访问了外部auto变量,它也不会对外部person进行强引用。

    同样我们把上述代码编译成C++,由于弱引用需要运行时机制来支持,所以我们不能进行静态编译,还需要运行时调用,指定运行时系统版本,所以编译命令如下:

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

    在生成的C++代码中找到__main_block_impl_0结构体,发现是一个弱引用:

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

    总结一下:

    • Block在栈上,无论 ARCMRC环境,block内部都不会对外部对象类型的auto变量产生强引用,就算Block内部生成强指针,也不会对外部person产生强引用,因为Block自己就在栈上,随时可能被销毁;
    • Block在堆上:
      ARC环境下,访问外部对象类型的auto变量,编译后:
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      DJTPerson *__strong person;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, DJTPerson *__strong _person, int flags=0) : person(_person) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    __main_block_desc_0结构体中,多了两个函数:__main_block_copy_0__main_block_dispose_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_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*/);}
    
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);}
    
    

    当对Block进行copy操作时,会调用这个__main_block_copy_0函数,在它内部调用_Block_object_assign((void*)&dst->person, (void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/),会对外部person对象产生强引用或者弱引用,这取决于block内部使用__strong指针还是__weak指针访问。

    Block从堆上移除,会调用__main_block_dispose_0函数,它内部调用_Block_object_dispose((void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);,会对外部person对象进行一次release操作。

    MRC环境下,也是由这两个函数决定是否进行retainrelease操作。

    五、__block修改变量及其本质

    我们先看下面一段代码:

    typedef void (^DJTBlock)(void);
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            
            int age = 10;
            
            DJTBlock block = ^{
                age = 20; // 报错:Variable is not assignable (missing __block type specifier)
                NSLog(@"-----------%d",age);
            };   
        }
        return 0;
    }
    

    ARC下直接在Block内部修改age会报错,这就相当于在block生成的结构体中FuncPtr指向的函数中去修改main函数中的局部变量(如果这里agestatic或者全局变量,可以修改,因为这两种变量一直在内存中),上下文环境发生了改变,所以不能直接访问age;我们使用__block修饰age变量,然后编译成C++

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

    会生成下面结构体:

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

    可以看到在Block结构体中多了__Block_byref_age_0 *age;,看一下 __Block_byref_age_0发现它也是一个结构体:

    struct __Block_byref_age_0 {
        void *__isa;
        __Block_byref_age_0 *__forwarding;
        int __flags;
        int __size;
        int age;
    };
    

    这里看出编译器将 __block修饰的变量(这里是age)包装成一个对象__Block_byref_age_0(因为它内部有isa指针,所以可以认为它是个对象),Block内部(__main_block_impl_0结构体中)并不会直接拥有这个变量age,而是拥有__Block_byref_age_0这个结构体,然后__Block_byref_age_0结构体中有一个int age变量,我们在Block内部改变age = 20,实际上就是赋值给__Block_byref_age_0结构体中的age变量。

    我们对__block int age = 10转化成的C++代码进行简化:

    // __block int age = 10;对应下面c++代码:
    __attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};
    
    // 进行简化
    __Block_byref_age_0 age = {
         0,
         &age,
         0,
         sizeof(__Block_byref_age_0),
         10    
     };
    

    对应到下面结构体初始化:

    struct __Block_byref_age_0 {
       void *__isa;
       __Block_byref_age_0 *__forwarding;
       int __flags;
       int __size;
       int age;
    };
    

    __forwarding指针传入&age指向__Block_byref_age_0 age结构体自己(这里&age是结构体地址,不要混淆),10赋值给了__Block_byref_age_0结构体内部的age变量;我们再看下修改age为20的代码:

    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      __Block_byref_age_0 *age = __cself->age; // bound by ref
     // 这里是 age = 20;
      (age->__forwarding->age) = 20;
       NSLog((NSString *)&__NSConstantStringImpl__var_folders__h_yv1h9mrx1155q6tq7brn8b9m0000gp_T_main_05a705_mi_0,(age->__forwarding->age));
    }
    

    可以发现先通过 __cself->age找到__Block_byref_age_0结构体,然后(age->__forwarding->age) = 20;通过__forwarding指针修改结构体内部的age变量,__forwarding指向结构体自己,那为什么要多此一举通过__forwarding指针去修改内部age,而不通过结构体指针直接去修改呢?这是为了保证Blockcopy到堆上时,不管访问栈上还是堆上Block,通过forwarding指针都是找到堆上。

    这里如果__block修饰的是一个对象类型,比如下面代码:

    typedef void (^DJTBlock)(void);
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            __block int age = 10;
            __block NSObject *obj = [[NSObject alloc] init];
            DJTBlock block = ^{
                obj = nil;
                age = 20;
            };
        }
        return 0;
    }
    
    

    转换为C++同样会多生成一个对应的结构体,只不过内部会多出两个方法copy``和dispose方法来负责相应的内存管理:

    // __block age 对应的结构体
    struct __Block_byref_age_0 {
      void *__isa;
    __Block_byref_age_0 *__forwarding;
     int __flags;
     int __size;
     int age;
    };
    
    // __block NSObject 对应的结构体
    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;
    };
    

    下面看一个例子:

    typedef void (^DJTBlock)(void);
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            NSMutableArray *mutarray = [NSMutableArray array];
            DJTBlock block = ^{
                [mutarray addObject:@(12)];
                [mutarray addObject:@(13)];
            };
        }
        return 0;
    }
    

    这里不会报错,是因为我们并没有修改mutarray指针,而是在使用mutarray指针,除非我们修改mutarray指针的值,比如 mutarray = nil;才需要__block来修饰;

    六、__block的内存管理

    我们知道,当Block内部访问外部对象类型的变量时,如下面简单代码:

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

    block编译成C++后的结构:

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      NSObject *__strong object; //内部强引用外部变量
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSObject *__strong _object, int flags=0) : object(_object) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    可以看到在block结构体内部会生成一个强指针指向外边的object对象,并且在block被拷贝到堆上时,调用__main_block_desc_0中的copy函数,对这个指针指向的对象进行一次retain操作,即引用计数+1,当然如果用__weak修饰object会生NSObject *__weak object;此时不会强引用;
    那当我们用__block修饰变量时,比如分别修饰基础数据类型age,和对象类型obj1,如下代码:

    typedef void (^DJTBlock)(void);
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
           
            __block int age = 10;
            __block NSObject *obj1 = [[NSObject alloc] init];
            NSObject *object = [[NSObject alloc] init];
            DJTBlock block = ^{
                NSLog(@"%d %p %p", age,obj1, object);
            };
            block();
        }
        return 0;
    }
    

    编译成C++:

    struct __Block_byref_age_0 {
      void *__isa;
    __Block_byref_age_0 *__forwarding;
     int __flags;
     int __size;
     int age;
    };
    struct __Block_byref_obj1_1 {
      void *__isa;
    __Block_byref_obj1_1 *__forwarding;
     int __flags;
     int __size;
     void (*__Block_byref_id_object_copy)(void*, void*);
     void (*__Block_byref_id_object_dispose)(void*);
     NSObject *__strong obj1;
    };
    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      NSObject *__strong object;
      __Block_byref_age_0 *age; // by ref
      __Block_byref_obj1_1 *obj1; // by ref
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSObject *__strong _object, __Block_byref_age_0 *_age, __Block_byref_obj1_1 *_obj1, int flags=0) : object(_object), age(_age->__forwarding), obj1(_obj1->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    __block修饰的变量ageobj1分别生成构体__Block_byref_age_0__Block_byref_obj1_1,它们的本质就是OC对象,所以在block对应的结构体内部生成两个结构体指针指向这两个对象,即

    __Block_byref_age_0 *age; // by ref
    __Block_byref_obj1_1 *obj1; // by ref
    

    它们其实和object一样,因为__block修饰的变量也是转换成结构体,而且内部有isa指针,其实就是OC对象,所以也会在__main_block_desc_0中生成两个函数:copydispose,来管理对象的内存,可以看下结构:

    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) 
    {
      _Block_object_assign((void*)&dst->age, (void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);
      _Block_object_assign((void*)&dst->obj1, (void*)src->obj1, 8/*BLOCK_FIELD_IS_BYREF*/);
      _Block_object_assign((void*)&dst->object, (void*)src->object, 3/*BLOCK_FIELD_IS_OBJECT*/);
    }
    
    static void __main_block_dispose_0(struct __main_block_impl_0*src) 
    {
      _Block_object_dispose((void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);
      _Block_object_dispose((void*)src->obj1, 8/*BLOCK_FIELD_IS_BYREF*/);
      _Block_object_dispose((void*)src->object, 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};
    

    所以__block内存管理可以总结一下:

    • Block在栈上时,内部并不会对__block修饰的外部变量产生强引用

    • Blockcopy到堆上时,会调用Block内部的copy函数,而copy函数内部会调用_Block_object_assign函数,它内部会对__block变量形成强引用(retain)。


    • Block从堆上移除时,会调用Block内部的dispose函数,dispose函数内部会调用_Block_object_dispose函数,在_Block_object_dispose函数中会对引用的__block变量进行引用计数-1release


    下面我们对比下Block内部访问外部变量几种情况:

    typedef void (^DJTBlock)(void);
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
          
             //直接将20存放在Block生成的结构体中
            int num = 20; 
            //Block结构体内部生成一个强指针 强引用object对象
            NSObject *object = [[NSObject alloc] init]; 
            // Block内部生成一个弱指针 弱引用object对象
            __weak NSObject *weakObject = object; 
            // Block内部生成一个结构体指针,指针指向的结构体内部存储着变量age
            __block int age = 10; 
            //Block内部生成一个结构体指针,指针指向的结构体内部存储着变量obj1
            __block NSObject *obj1 = [[NSObject alloc] init];
            
            DJTBlock block = ^{
                NSLog(@"%d %d %p %p %p",num, age, obj1, object, weakObject);
            };
            block();
        }
        return 0;
    }
    

    编译成C++看看block结构体,和上边注释的一致:

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int num;
      NSObject *__strong object;
      NSObject *__weak weakObject;
      __Block_byref_age_0 *age; // by ref
      __Block_byref_obj1_1 *obj1; // by ref
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _num, NSObject *__strong _object, NSObject *__weak _weakObject, __Block_byref_age_0 *_age, __Block_byref_obj1_1 *_obj1, int flags=0) : num(_num), object(_object), weakObject(_weakObject), age(_age->__forwarding), obj1(_obj1->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    这里主要对比一下 对象类型的auto变量和__block修饰的变量内存管理的区别:

    • 相同点:
      • Block在栈上时,对它们都不会产生强引用
      • Block拷贝到堆上时,都会通过copy函数来处理它们:
        (1)__block变量(假设变量名叫做a
        _Block_object_assign((void*)&dst->a, (void*)src->a,   8/*BLOCK_FIELD_IS_BYREF*/);
        
        (2)对象类型的auto变量(假设变量名叫做p
        _Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
        
      • Block从堆上移除时,都会通过dispose函数来释放它们
        (1)__block变量(假设变量名叫做a
        _Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*
        
        (2)对象类型的auto变量(假设变量名叫做p
        _Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
        
    • 不同点(主要是在引用的问题上)
      (1)对象类型的auto变量,根据传进来时是__strong还是__weak类型决定调用copy函数时Block内部对传进来的变量进行强还是弱引用。
      (2)如果时__block类型的变量,比如__block int age = 20;,它被封装成一个OC对象,调用copy函数时Block内部直接对它产生强引用,对它的内存进行管理,不存在__weak修饰int age这种操作,所以没有弱引用这一说。(这里强引用的是age转换成的结构体对象,真正的age变量的值存储在结构体里边);

    但是如果是下面代码

    typedef void (^DJTBlock)(void);
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
           
            NSObject *object = [[NSObject alloc] init];
            __block __weak NSObject *weakObject = object;
            DJTBlock block = ^{
                NSLog(@"%p %p", object, weakObject);
            };
            block();
        }
        return 0;
    }
    

    编译成C++:

    struct __Block_byref_weakObject_0 {
      void *__isa;
    __Block_byref_weakObject_0 *__forwarding;
     int __flags;
     int __size;
     void (*__Block_byref_id_object_copy)(void*, void*);
     void (*__Block_byref_id_object_dispose)(void*);
     NSObject *__weak weakObject;
    };
    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      NSObject *__strong object;
      __Block_byref_weakObject_0 *weakObject; // by ref
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSObject *__strong _object, __Block_byref_weakObject_0 *_weakObject, int flags=0) : object(_object), weakObject(_weakObject->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    可以看到在__block修饰变量生成的结构体 __Block_byref_weakObject_0内部,通过__weak弱引用变量weakObject,即Block结构体内部是一个强指针指向__block生成的结构体,即这句代码

    __Block_byref_weakObject_0 *weakObject;
    (注意虽然名字中有`weak`但这是一个强指针)
    

    而在结构体__Block_byref_weakObject_0内部:

    NSObject *__weak weakObject;
    

    这才是一个弱指针,指向外部传入的弱引用对象weakObject,它表达了外部传入变量的类型是__weak还是__strong

    注意:这里在MRC下有个特殊情况,在__block生成的结构体内部,始终都是弱引用,不会对外边对象进行强引用。


    MRC环境下验证, 下面代码在block();调用前person就已经挂了,说明确实内部没有强引用:
    #import "DJTPerson.h"
    
    typedef void (^DJTBlock)(void);
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
           
            __block DJTPerson *person = [[DJTPerson alloc] init];
            DJTBlock block = [^{
                NSLog(@" %p", person);
            } copy];
            [person release];
            block();
        }
        return 0;
    }
    

    七、Block相关问题

    • Block的原理是怎样的?本质是什么?
    • __block的作用是什么?有什么使用注意点?
    • Block的属性修饰词为什么是copy?使用Block有哪些使用注意?
    • Block在修改NSMutableArray,需不需要添加__block

    理解上边原理再回答这些问题应该不难吧。

    相关文章

      网友评论

        本文标题:iOS原理篇(五):Block探究

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