美文网首页
Objective-C--Block

Objective-C--Block

作者: 人生看淡不服就干 | 来源:发表于2017-08-05 13:01 被阅读71次

    参考文章:深入研究Block捕获外部变量和__block实现原理

    Block是什么?

    Blocks是C语言的扩充功能,在OS X v10.6 和iOS 4中被引入,并在系统API中被广泛使用。它很像标准的C函数,但是它除了包含可执行代码外,还包含了执行时需要访问的变量(栈上或堆上)。

    简而言之,Block是能够捕获当前作用域变量的匿名函数。也可以理解为一个可以延迟执行的代码片段。

    如何学习Block

    Working with Blocks
    Blocks Programming Topics
    llvm-project开源 - BlocksRuntime

    Block的特征

    • Block允许你创建一段代码并能像变量一样传参、返回、存储,然后在适当的时机被执行。这为异步编程打下了基础
    • Block还能从当前作用域中捕获变量,类似于其他语言的“闭包(closure)”、“ lambdas表达式”等概念。某些情况下Block还能修改被捕获的原始变量(比如使用__block)

    Block语法

    • 声明Block引用
    void (^blockReturningVoidWithVoidArgument)(void);
    int (^blockReturningIntWithIntAndCharArguments)(int, char);
    void (^arrayOfTenBlocksReturningVoidWithIntArgument[10])(int);
    

    可以使用typedef简化声明

    typedef float (^MyBlockType)(float, float);
    MyBlockType myFirstBlock = // ... ;
    
    • 创建Block
    ^(float aFloat) {
        float result = aFloat - 1.0;
        return result;
    };
    

    注意创建时^左侧不需要指明返回类型,且如果没有参数的话,^右侧可以把括号省略。

    • Block参数类型
    - (void)enumerateObjectsUsingBlock:(void (^)(id obj, NSUInteger idx, BOOL *stop))block;
    
    • Block返回类型

    • Block属性

    @property (copy) void (^blockProperty)(void);
    

    Block的应用

    • 将代码逻辑传入API,自定义算法实现:
    char *myCharacters[3] = { "TomJohn", "George", "Charles Condomine" };
    
    qsort_b(myCharacters, 3, sizeof(char *), ^(const void *l, const void *r) {
      char *left = *(char **)l;
      char *right = *(char **)r;
      return strncmp(left, right, 1);
    });
    // myCharacters is now { "Charles Condomine", "George", "TomJohn" }
    
    • 作为回调参数传入API,用来实现异步调用
    [self beginTaskWithName:@"MyTask" completion:^{
    
      NSLog(@"The task is complete");
    
    }];
    
    • 局部封装代码,实现代码复用和延迟执行
      void (^printTip)() = ^{
            NSLog(@"hello word");
        };
    
        if (needPrintDirectly) {
            printTip();
        }else{
            [self beginTaskWithName:@"MyTask" completion:^{
                printTip();
            }];
        }
    
    • 构造可分发的Task,实现并发编程
    NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
        ...
    }];
    NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
    [mainQueue addOperation:operation];
    
    • 能够捕获和修改变量,从而延长变量的作用域和生命周期

    Block是对象吗?

    Block在OC中的实现如下:

    struct Block_layout {
        void *isa;
        int flags;
        int reserved;
        void (*invoke)(void *, ...);
        struct Block_descriptor *descriptor;
        /* Imported variables. */
    };
    
    struct Block_descriptor {
        unsigned long int reserved;
        unsigned long int size;
        void (*copy)(void *dst, void *src);
        void (*dispose)(void *);
    };
    

    从结构图中很容易看到isa,所以OC处理Block是按照对象来处理的。在iOS中,isa常见的就是_NSConcreteStackBlock,_NSConcreteMallocBlock,_NSConcreteGlobalBlock这3种。

    即Block是一种OC对象,它也可以存放在NSArray或NSDictionary这样的集合中。

    Block三种类型的区别

    三种类型的Block存储在哪呢?

    顾名思义,_NSConcreteStackBlock存储在栈上,_NSConcreteMallocBlock存储在堆上,_NSConcreteGlobalBlock存储在全局区

    那这三种类型怎样产生的呢?

    • Block中没有用到外部变量,或只用到全局变量、静态变量(包括局部静态变量)的都是_NSConcreteGlobalBlock。

    • 除了_NSConcreteGlobalBlock外,刚创建的Block都是_NSConcreteStackBlock。

    • 对_NSConcreteStackBlock进行copy后,就变成了_NSConcreteMallocBlock

    以下是Block_copy的一个实现,实现了从_NSConcreteStackBlock复制到_NSConcreteMallocBlock的过程。对应有9个步骤。

    static void *_Block_copy_internal(const void *arg, const int flags) {
        struct Block_layout *aBlock;
        const bool wantsOne = (WANTS_ONE & flags) == WANTS_ONE;
    
        // 1
        if (!arg) return NULL;
    
        // 2
        aBlock = (struct Block_layout *)arg;
    
        // 3
        if (aBlock->flags & BLOCK_NEEDS_FREE) {
            // latches on high
            latching_incr_int(&aBlock->flags);
            return aBlock;
        }
    
        // 4
        else if (aBlock->flags & BLOCK_IS_GLOBAL) {
            return aBlock;
        }
    
        // 5
        struct Block_layout *result = malloc(aBlock->descriptor->size);
        if (!result) return (void *)0;
    
        // 6
        memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
    
        // 7
        result->flags &= ~(BLOCK_REFCOUNT_MASK);    // XXX not needed
        result->flags |= BLOCK_NEEDS_FREE | 1;
    
        // 8
        result->isa = _NSConcreteMallocBlock;
    
        // 9
        if (result->flags & BLOCK_HAS_COPY_DISPOSE) {
            (*aBlock->descriptor->copy)(result, aBlock); // do fixup
        }
    
        return result;
    }
    

    对Block进行Copy的意义?

    由于_NSConcreteStackBlock所属的变量域一旦结束,那么该Block就会被销毁,但很多情况下我们想延长Block的生命周期:

    在MRC下,我们可以通过将block从栈copy到堆上,来延长block的生命周期,所以一般block类型的属性都会使用copy描述

    在ARC下,编译器会自动的判断,把block自动从栈copy到堆上,如以下四种情况:

    1. 手动调用copy
    2. Block是函数的返回值
    3. Block被强引用,Block被赋值给__strong或者id类型
    4. 调用系统API入参中含有usingBlcok的方法

    因此,ARC下,block类型的属性直接用strong描述即可

    Block如何捕获变量

    我们来测一下Block中引用四种类型的变量:

    1. 全局变量
    2. 静态全局变量
    3. 静态局部变量
    4. 自动变量
    int global_i = 1;
    
    static int static_global_j = 2;
    
    int main(int argc, const char * argv[]) {
    
        static int static_k = 3;
        int val = 4;
    
        void (^myBlock)(void) = ^{
            global_i ++;
            static_global_j ++;
            static_k ++;
            NSLog(@"Block中 global_i = %d,static_global_j = %d,static_k = %d,val = %d",global_i,static_global_j,static_k,val);
        };
    
        global_i ++;
        static_global_j ++;
        static_k ++;
        val ++;
        NSLog(@"Block外 global_i = %d,static_global_j = %d,static_k = %d,val = %d",global_i,static_global_j,static_k,val);
    
        myBlock();
    
        return 0;
    }
    

    转换成C++源码如下

    int global_i = 1;
    
    static int static_global_j = 2;
    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int *static_k;
      int val;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_k, int _val, int flags=0) : static_k(_static_k), val(_val) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      int *static_k = __cself->static_k; // bound by copy
      int val = __cself->val; // bound by copy
    
            global_i ++;
            static_global_j ++;
            (*static_k) ++;
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_6fe658_mi_0,global_i,static_global_j,(*static_k),val);
        }
    
    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[]) {
    
        static int static_k = 3;
        int val = 4;
    
        void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_k, val));
    
        global_i ++;
        static_global_j ++;
        static_k ++;
        val ++;
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_6fe658_mi_1,global_i,static_global_j,static_k,val);
    
        ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
    
        return 0;
    }
    

    在__main_block_impl_0中,可以看到局部静态变量static_k和自动变量val,被Block从外面捕获进来,成为__main_block_impl_0这个结构体的成员变量了。

    总结一下

    • 能在Block中被修改的变量:全局变量、静态全局变量、静态局部变量
    • 能被Block捕获的变量:静态局部变量、自动变量
    • Block捕获外部变量仅仅只捕获Block闭包里面会用到的值,其他用不到的值,它并不会去捕获。

    相关解释:

    • 首先全局变量和静态全局变量可以被修改,是因为他们作用域很广,所以在Block中和Block外被操作,它们的值依旧可以得以保存下来。
    • 静态局部变量可以被修改,是因为被捕获的是变量地址,在Block中通过变量地址可以修改原始变量。
    • 自动变量不可以被修改,是因为Block是通过拷贝值的方式捕获的自动变量,因此不能修改原始变量。OC在编译的层面就防止开发者可能犯的错误,因为自动变量没法在Block中改变外部变量的值,所以编译过程中就报编译错误。

    如何在Block中修改捕获的变量

    通过上述例子,我们知道除了全局变量、静态全局变量可以被修改外,静态局部变量也可以被修改,因为捕获的是变量地址。

    而根据官方文档我们可以了解到,在自动变量前加入 __block关键字,就可以在Block里面改变外部自动变量的值了。

    总结一下在Block中改变变量值有2种方式:

    1. 传递内存地址指针到Block中
    2. 改变存储区方式(__block)。

    __block实现原理

    我们继续研究一下__block实现原理。

    先来看看普通变量的情况。

    int main(int argc, const char * argv[]) {
    
        __block int i = 0;
    
        void (^myBlock)(void) = ^{
            i ++;
            NSLog(@"%d",i);
        };
    
        myBlock();
    
        return 0;
    }
    
    

    把上述代码用clang转换成源码。

    struct __Block_byref_i_0 {
      void *__isa;
      __Block_byref_i_0 *__forwarding;
      int __flags;
      int __size;
      int i;
    };
    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      __Block_byref_i_0 *i; // by ref
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_i_0 *_i, int flags=0) : i(_i->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      __Block_byref_i_0 *i = __cself->i; // bound by ref
      (i->__forwarding->i) ++;
      NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_3b0837_mi_0,(i->__forwarding->i));
    }
    
    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
      _Block_object_assign((void*)&dst->i, (void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);
    }
    
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {
      _Block_object_dispose((void*)src->i, 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[]) {
        __attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 0};
    
        void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_i_0 *)&i, 570425344));
    
        ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
    
        return 0;
    }
    
    

    从源码我们能发现,带有 __block的变量也被转化成了一个结构体__Block_byref_i_0,这个结构体有5个成员变量。第一个是isa指针,第二个是指向自身类型的__forwarding指针,第三个是一个标记flag,第四个是它的大小,第五个是变量值,名字和变量名同名。

    __attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,
    (__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 0};
    

    源码中是这样初始化的。__forwarding指针初始化传递的是自己的地址。然而这里__forwarding指针真的永远指向自己么?

    我们做了个实验,将Block进行copy后再打印,发现__forwarding指向了堆上的地址,我们猜想__block也被拷贝到堆上了。

    我们把Block通过copy到了堆上,堆上也会重新复制一份__block,并且该Block也会继续持有该__block。当Block释放的时候,__block没有被任何对象引用,也会被释放销毁。

    __forwarding指针这里的作用就是针对堆的Block,把原来__forwarding指针指向自己,换成指向_NSConcreteMallocBlock上复制之后的__block自己。然后堆上的变量的__forwarding再指向自己。这样不管__block怎么复制到堆上,还是在栈上,都可以通过(i->__forwarding->i)来访问到变量值。


    所以在__main_block_func_0函数里面就是写的(i->__forwarding->i)。

    Block造成的环引用

    为何会造成环引用呢?

    //MRC下运行
    __block id block_obj = [[NSObject alloc]init];
    id obj = [[NSObject alloc]init];
    NSLog(@"***Block前****block_obj = [%p , %lu] , obj = [%p , %lu]", &block_obj ,(unsigned long)[block_obj retainCount] , &obj,(unsigned long)[obj retainCount]);
    void (^myBlock)(void) = ^{
         NSLog(@"***Block中****block_obj = [%p , %lu] , obj = [%p , %lu]", &block_obj ,(unsigned long)[block_obj retainCount] , &obj,(unsigned long)[obj retainCount]);
    };
    myBlock();
        
    void (^myBlockCopy)(void) = [myBlock copy];
    NSLog(@"***BlockCopy前****block_obj = [%p , %lu] , obj = [%p , %lu]", &block_obj ,(unsigned long)[block_obj retainCount] , &obj,(unsigned long)[obj retainCount]);
    myBlockCopy();
    

    输出

    ***Block前****block_obj = [0x7fff5d509bd8 , 1] , obj = [0x7fff5d509ba8 , 1]
    ***Block中****block_obj = [0x7fff5d509bd8 , 1] , obj = [0x7fff5d509b80 , 1]
    ***BlockCopy前****block_obj = [0x608000243238 , 1] , obj = [0x7fff5d509ba8 , 2]
    ***Block中****block_obj = [0x608000243238 , 1] , obj = [0x6080002433b0 , 2]
    

    在MRC下,对于普通的对象,_NSConcreteStackBlock是不强持有的,而_NSConcreteMallocBlock是强持有的。也可以认为对一个_NSConcreteStackBlock进行copy时会强持有已捕获的普通对象(引用计数增加)。

    而ARC下经常会自动将一个Block进行copy到堆上,因此很容易强引用了已捕获的普通对象,从而可能造成环引用问题。

    在MRC下,__block修饰的对象在整个block进行copy时也会被copy到堆上,但是它的引用计数没有变化,即没有被强持有,这一点可以用来避免环引用。

    而在ARC下,__block修饰的对象在整个Block进行copy时,引用计数会增加,即仍然会被强持有。

    如何打破环引用呢?

    在MRC下:

    1. __block方式
    myViewController * __block  myController = [[MyViewController alloc] init];
    myController.completionHandler =  ^(NSInteger result) {
        [myController dismissViewControllerAnimated:YES completion:nil];
    };
    

    在ARC下:

    1. 主动打破环的方式
    __block MyViewController * myController = [[MyViewController alloc] init];
    myController.completionHandler =  ^(NSInteger result) {
        [myController dismissViewControllerAnimated:YES completion:nil];
        myController = nil;  // 注意这里,保证了block结束myController强引用的解除
    };
    
    1. 弱引用的方式(推荐)
    MyViewController *myController = [[MyViewController alloc] init];
    MyViewController * __weak weakMyViewController = myController;
    myController.completionHandler =  ^(NSInteger result) {
        [weakMyViewController dismissViewControllerAnimated:YES completion:nil];
    };
    

    思考ARC下的弱引用方式是否很完善?会不会在Block执行到一半时weak变量就被释放掉了?在多线程环境下这种情况是可能发生的。

    解决方法就是我们在block内新定义一个强引用strongMyController来指向weakMyController指向的对象,这样多了一个强引用,就能保证block执行时weakMyController指向的对象不会被释放。

    strongMyController 虽然是强引用,但是它属于bolck新声明的变量,存在于栈中。当函数执行完成后,引用被销毁,引用关系也被解除了。

    最终代码如下:

    MyViewController *myController = [[MyViewController alloc] init…];
    MyViewController * __weak weakMyController = myController;
    myController.completionHandler =  ^(NSInteger result) {
        MyViewController *strongMyController = weakMyController;
    
      if (strongMyController) {
            [strongMyController dismissViewControllerAnimated:YES completion:nil];
        } else {
            // Probably nothing...
        }
    };
    

    捕获变量的总结

    对于非对象的变量来说

    自动变量的值,被copy进了Block,不带__block的自动变量只能在里面被访问,并不能改变值。


    带__block的自动变量 和 静态变量 就是直接地址访问。所以在Block里面可以直接改变变量的值。


    而剩下的静态全局变量,全局变量,函数参数,也是可以在直接在Block中改变变量值的,但是他们并没有变成Block结构体__main_block_impl_0的成员变量,因为他们的作用域大,所以可以直接更改他们的值。

    值得注意的是,静态全局变量,全局变量,函数参数他们并不会被Block持有,也就是说不会增加retainCount值。

    对于对象来说

    对于不用__block修饰的普通对象,一开始会像自动变量一样被拷贝到_NSConcreteStackBlock中,引用计数无变化;但当Block被copy到堆上时,被捕获的对象引用计数增加。

    对于__block修饰的对象,一开始会像自动变量一样被拷贝到_NSConcreteStackBlock中,引用计数无变化;但当Block被copy到堆上时,分两种情况:

    • MRC下,被捕获的对象引用计数不变。
    • ARC下,被捕获的对象引用计数增加。

    __block作用的总结:

    MRC下

    • 说明变量可改
    • 说明指针指向的对象不做这个隐式的retain操作,打破环引用

    ARC下

    • 说明变量可改

    相关文章

      网友评论

          本文标题:Objective-C--Block

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