Objective-C Block深入源码

1. Block捕获自动变量

如何捕获自动变量?

Block转换为C函数之后,Block中使用的自动变量会被作为成员变量追加到__X_block_impl_Y结构体中,其中X一般是函数名,Y是第几个Block,比如main函数中的第0个结构体:__main_block_impl_0

typedef void (^MyBlock)(void);

int main(int argc, const char * argv[])
{
  @autoreleasepool
  {
     int age = 10;
     MyBlock block = ^{
         NSLog(@"age = %d", age);
     };
     age = 18;
     block();
  }
  return 0;
}

经过clang -w -rewrite-objc main.m处理后:

我们看到age变量被作为成员变量追加到了结构体中。

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

下面是适当去掉类型转换的main函数:

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

我们可以看到age的值引用,在Block初始化的时候被赋值了。

为什么Block语法中不能使用数组?

因为结构体中的成员变量与自动变量类型完全相同。使用数组捕获数组值,然后调用时再赋值给另一个数组。也就是数组赋值给数组,这在C语言中是不被允许的。

2. __Block说明符

Block中修改捕获的自动变量有两种方法

  • 静态变量、静态全局变量、全局变量

    从Block语法转化为C语言函数中访问静态全局变量、全局变量,没有任何不同,可以直接访问。而静态变量使用的是静态变量的指针来进行访问。

    自动变量不能采用静态变量的做法进行访问。原因是,自动变量是在存储在栈上的,当超出其作用域时,会被栈释放。而静态变量是存储在堆上的,超出作用域时,静态变量没有被释放,所以还可以访问。

  • __block修饰符

    __block存储域类说明符。存储域说明符会指定变量存储的域,如栈auto、堆static、全局extern,寄存器register。

    比如刚刚的代码加上__block说明符:

    typedef void (^MyBlock)(void);
    
    int main(int argc, const char * argv[])
    {
      @autoreleasepool
      {
         __block int age = 10;
         MyBlock block = ^{
             age = 18;
         };
          block();
      }
      return 0;
    }
    

    经过clang -w -rewrite-objc main.m处理后,同样main函数进行了适当简化:

    
    typedef void (*MyBlock)(void);
    
    struct __Block_byref_age_0 {
        void *__isa; //isa指针,指向一个存储自身基础信息的地址
        __Block_byref_age_0 *__forwarding; //指向自己的指针
        int __flags; //标记
        int __size; //结构体大小
        int age; //成员变量,存储age值
    };
    
    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;
        }
    };
    
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
        __Block_byref_age_0 *age = __cself->age; // bound by ref
        age->__forwarding->age = 18;
    }
    
    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*/); }
    
    static void __main_block_dispose_0(struct __main_block_impl_0*src) { _Block_object_dispose((void*)src->age, 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;
            __Block_byref_age_0 age = {(void*)0, &age, 0, sizeof(__Block_byref_age_0), 10};
            MyBlock block = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &age, 570425344));
              (*block->FuncPtr)(block);
        }
        return 0;
    }
    

    __block修饰的变量变成了__Block_byref_age_0结构体类型的自动变量,即存储域在栈上的__Block_byref_age_0结构体实例。

    原来的赋值代码age = 18;被转换为:

    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
        __Block_byref_age_0 *age = __cself->age; // bound by ref
        age->__forwarding->age = 18;
    }
    

    Block执行时会传入__main_block_impl_0结构体实例变量的指针,让后把成员变量__cself->age赋值给__Block_byref_age_0结构体指针。再通过age->__forwarding->age进行赋值。即最终Block是通过__Block_byref_age_0结构体的指针来进行访问的。

3. Block存储域

上面,从C代码中我们可以看到Block的是指是Block结构体实例__block变量实质是栈上__block变量结构体实例。从初始化函数中我们可以看到,impl.isa = &_NSConcreteStackBlock;,即之前我们使用的是栈Block。其实,Block有3中类型:

  • _NSConcreteGlobalBlock类对象存储在程序的数据区(.data区)
  • _NSConcreteStackBlock类对象存储在栈上
  • _NSConcreteMallocBlock类对象存储在堆上
typedef int (^blk_t)(int);      
for (int rate = 0; rate < 10; rate++) {
    blk_t block = ^(int count) { return rate * count; };
}

上面的代码,每次for循环中的rate会发生变化,Block实例捕获的值都会不同,但把return rate * count;改为return count;,则Block不发生捕获。

声明全局变量的地方使用Block语法,Block为_NSConcreteGlobalBlock对象。其他情况下,Block类为_NSConcreteStackBlock对象,存储在栈上。用过copy方法可以将Block复制到堆上,变为_NSConcreteMallocBlock对象。

设置在栈上的Block,如果超出作用域,Block就会被释放。若__block变量也配置在栈上,也会有被释放的问题。所以,copy方法调用时,__block变量也被复制到堆上,同时impl.isa = &_NSConcreteMallocBlock;。复制之后,栈上__block变量的__forwarding指针会指向堆上的对象。因此__block变量无论被分配在栈上还是堆上都能够正确访问。

编译器如何判断何时需要进行copy操作呢?

在ARC开启时,自动判断进行copy

  • 将Block作为函数参数返回值返回时,编译器会自动进行copy
  • 将Block赋值给copy修饰的id类或者Block类型成员变量,或者__strong修饰的自动变量。
  • 方法名含有usingBlockCocoa框架方法或GCD相关API传递Block。

如果不能自动copy,则需要我们手动调用copy方法将其复制到堆上。比如向不包括上面提到的方法或函数的参数中传递Block时。

ARC环境下,返回一个对象时会先将该对象复制给一个临时实例指针,然后进行retain操作,再返回对象指针。runtime/objc-arr.mm提到,Block的retain操作objc_retainBlock函数实际上是Block_copy函数。在实行retain操作objc_retainBlock后,栈上的Block会被复制到堆上,同时返回堆上的地址作为指针赋值给临时变量。

4. __block变量存储域

__forwarding

当Block从栈复制到堆上时候,__block变量也被复制到堆上并被Block持有。若此时__block变量已经在堆上,则被该Block持有。若配置在堆上的Block被释放,则它所持有的__block变量也会被释放。

__block int val = 0;
void (^block)(void) = [^{ ++val; } copy];
++val;
block();

利用copy操作,Block和__block变量都从栈上被复制到了堆上。无论是{ ++val; }还是++val;都转换成了++(val->__forwarding->val);

Block中的变量val为复制到堆上的__block变量结构体实例,而Block外的变量val则为复制前栈上的__block变量结构体实例,但这个结构体的__forwarding成员变量指向堆上的__block变量结构体实例。所以,无论是是在Block内部还是外部使用__block变量,都可以顺利访问同一个__block变量。

5. Block捕获对象

NSMutableArray *array = [NSMutableArray array];
void (^block)(id) = ^(id obj){
    [array addObject:obj];
    NSLog(@"array count = %ld", array.count);
};
block([[NSObject alloc] init]);
block([[NSObject alloc] init]);
block([[NSObject alloc] init]);

//输出
array count = 1
array count = 2
array count = 3

意味着,添加到array中的NSObject对象,在超出其作用域后仍然存在。

经过clang -w -rewrite-objc main.m处理后,同样main函数进行了适当简化:

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

static void __main_block_func_0(struct __main_block_impl_0 *__cself, id obj) {
    NSMutableArray *array = __cself->array; // bound by copy
    
    ((void (*)(id, SEL, ObjectType _Nonnull))(void *)objc_msgSend)((id)array, sel_registerName("addObject:"), (id)obj);
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_1n_h6mmhvr555l7gjmzld98s9wh0000gp_T_main_6a51ec_mi_0, ((NSUInteger (*)(id, SEL))(void *)objc_msgSend)((id)array, sel_registerName("count")));
}

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

static void __main_block_dispose_0(struct __main_block_impl_0*src) { _Block_object_dispose((void*)src->array, 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;
        NSMutableArray *array = ((NSMutableArray * _Nonnull (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("array"));
        void (*block)(id) = ((void (*)(id))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, array, 570425344));
        ((void (*)(__block_impl *, id))((__block_impl *)block)->FuncPtr)((__block_impl *)block, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init")));
        ((void (*)(__block_impl *, id))((__block_impl *)block)->FuncPtr)((__block_impl *)block, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init")));
        ((void (*)(__block_impl *, id))((__block_impl *)block)->FuncPtr)((__block_impl *)block, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init")));
    }
    return 0;
}

//简化main
int main(int argc, const char * argv[])
{
    /* @autoreleasepool */
    { __AtAutoreleasePool __autoreleasepool;
        NSMutableArray *array = [NSMutableArray array];
        void (*block)(id) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, array, 570425344);
        (*block->FuncPtr)(block, [[NSObject alloc] init]);
        (*block->FuncPtr)(block, [[NSObject alloc] init]);
        (*block->FuncPtr)(block, [[NSObject alloc] init]);
    }
    return 0;
}

Block结构体为了正确的进行初始化和释放,在__main_block_impl_0结构体中增加了copydispose函数指针指向__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->array, (void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/); }

Block结构体重需要管理array对象,所以__main_block_copy_0通过_Block_object_assign函数将对象赋值给Block结构体成员变量array并持有改对象。

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

__main_block_dispose_0函数使用_Block_object_dispose函数释放在Block结构体成员变量array中的对象、

Block的copy与dispose函数的调用时机

什么时候栈上的Block会复制到堆上?

  • 将Block作为函数参数返回值返回时,编译器会自动进行copy
  • 将Block赋值给copy修饰的id类或者Block类型成员变量,或者__strong修饰的自动变量。
  • 方法名含有usingBlockCocoa框架方法或GCD相关API传递Block。
  • 手动调用copy方法。

Block从栈复制到堆时copy被调用。释放复制到堆上的Block,Block的引用计数为0时调用dispose函数。因此,Block捕获的的对象就可以超出其作用域存在。

7. __block变量和对象

我们知道__block变量从栈复制到堆上时,使用_Block_object_assign函数持有赋值给__block变量的对象。当堆上的__block变量释放时,使用_Block_object_dispose是释放赋值给__block变量的对象。

那如果使用__weak修饰id类型的变量会怎么样?

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[])
{
  @autoreleasepool
  {
      NSMutableArray *array = [NSMutableArray array];
      id __weak array2 = array;
      void (^block)(id) = ^(id obj){
          [array2 addObject:obj];
          NSLog(@"array2 count = %ld", array2.count);
      };
      block([[NSObject alloc] init]);
      block([[NSObject alloc] init]);
      block([[NSObject alloc] init]);
  }
  return 0;
}

//输出
array2 count = 0
array2 count = 0
array2 count = 0

因为__strong修饰的变量在超出作用域后被释放,array2是弱引用,此时也被赋值为nil。即使在array2前加上__block修饰符也一样,array超出作用域后被释放,array2被赋值为nil

如果使用__unsafe_unretained修饰符,和指针相同,没有__strong__weak修饰符那些处理,所以通过这个指针访问被释放的对象会发生崩溃,也就是我们常说的野指针

如果同一个对象使用__autoreleasing__block修饰符会引起编译错误。

Block循环引用

说的人太多了,这里简单介绍一下。

如果Block中使用__strong修饰的对象类型变量,那么Block从栈复制到堆时,该对象被Block持有,若对象同时又持有该Block,就会发生循环引用。

我们一般使用__weak修饰符来修饰对象,避免循环引用。另外使用__block变量对象也可以避免循环引用,但需要在Block中手动将__block变量对象置为nil,且需要至少调用一次Block才能避免循环引用。

ARC无效时Block的copy/release

ARC无效时,需要手动调用copy将Block从栈复制到堆。不用时,需要手动调用release释放堆上的Block。

由于Block是C语言的扩展,在C语言中同样可以使用Block语法。此时调用Block_copyBlock_release函数代替copyrelease实例方法。

ARC无效时,Block从栈复制到堆,若Block使用的变量为__block修饰的对象类型自动变量,不会被retain。因此,ARC无效时,__block修饰符可以用来避免Block的循环引用。若Block使用的变量没有__block修饰的对象类型自动变量,无论ARC是否有效,都会被retain

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • Blocks Blocks Blocks 是带有局部变量的匿名函数 截取自动变量值 int main(){ ...
    南京小伙阅读 4,545评论 1 3
  • 摘要block是2010年WWDC苹果为Objective-C提供的一个新特性,它为我们开发提供了便利,比如GCD...
    西门吹雪123阅读 4,468评论 0 4
  • 0.前言 日常开发中经常会用到 Block,但如果对它的底层实现没有深入地挖掘过,就不能算是真正掌握,本篇就来探究...
    RiverSea阅读 4,302评论 0 13
  • iOS进阶之Block的本质及原理 前言 相信稍微有点开发经验的开发者,应该都对block有一定的了解。刚开始使用...
    枫叶无处漂泊阅读 3,647评论 0 3
  • block.png iOS代码块Block 概述 代码块Block是苹果在iOS4开始引入的对C语言的扩展,用来实...
    全栈农民工阅读 3,674评论 0 1

友情链接更多精彩内容