Think in Block(下)

这一篇主要浅析下Block和Objc的关系,更加偏向于实际应用。其实大多数时间我们是和Objc对象打交道,知道一点原理,能避免死的不明不白。

在这个过程中也会回答前面留下的坑- -,探究下来感觉,纸上得来终觉浅,以前也看过这本书,但是一直没有动手敲代码,浮于表面了。

截获Objc对象

先看下源程序,声明了一个Block对象,这次用到了copy,然后最后调用了三次,输出的结果是1,2,3。这一次转换后的代码非常多,不过抽离出来后也很简洁。

typedef void(^blk_t)(id obj);
int main(){

    blk_t blk;
    {
        id array = [[NSMutableArray alloc] init];

        blk = [^(id obj){
            [array addObject:obj];

            NSLog(@"array count = %ld", [array count]);
        } copy];
    }

    blk([[NSObject alloc] init]);
    blk([[NSObject alloc] init]);
    blk([[NSObject alloc] init]);
}

在这里,我们能够看出来,Block对于Objc对象的截获是通过指针,并且和外部修饰符一致,也就是说外部是strong,内部也是strong,然后c结构体中并不能很好地管理内存,所以其实是通过运行时来掌控的,也就是后面会看到的copy和dispose函数的作用。

typedef void(*blk_t)(id obj);
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  id array;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, id _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) {
      id array = __cself->array; // bound by copy

      ((void (*)(id, SEL, id))(void *)objc_msgSend)((id)array, sel_registerName("addObject:"), (id)obj);

      NSLog((NSString *)&__NSConstantStringImpl__var_folders_hv_3dth3by12sg3t1g37hq5plkw0000gn_T_main_18c6fa_mi_0, ((NSUInteger (*)(id, SEL))(void *)objc_msgSend)((id)array, sel_registerName("count")));
}
  1. 在把Block对象拷贝到堆上的时候copy函数会被调用,而在Block从堆上释放的时候dispose会被调用。
  2. 在调用copy函数的时候_Block_object_assign将外部对象赋值给Block结构体的成员变量,并持有它。
  3. 在调用dispose函数的时候_Block_object_dispose将释放Block结构体的成员变量。

有了这些隐藏在背后的操作后,通过使用strong类型的局部对象,Block截获后能够在超出其变量作用域而存在。

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

其实回过头去看一下截获__block变量的时候也生成了两个类似的函数,唯一的区别是BLOCK_FIELD_IS_OBJECT和BLOCK_FIELD_IS_BYREF,这只是用来标示这是一个对象,还是一个变量。

int main(){

    blk_t blk;
    {
        id array = ((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("alloc")), sel_registerName("init"));

        blk = (blk_t)((id (*)(id, SEL))(void *)objc_msgSend)((id)&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, array, 570425344), sel_registerName("copy"));
    }


    ((void (*)(__block_impl *, id))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, ((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 *)blk)->FuncPtr)((__block_impl *)blk, ((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 *)blk)->FuncPtr)((__block_impl *)blk, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init")));

}

tips:上面这些代码其实和书上的有一定的区别,也即是前面说得,我们的技术在提升,当然苹果没有理由不提升,不管是编译器还是底层实现。其实这里加不加copy都一样,因为赋值给strong类型的变量会自动copy。

循环引用

由于上边所示那样的内存管理机制,当我们截获strong类型的对象时,很容易出现循环引用,但是也不用担心,没必要无时不刻的用一个__weak来避免,盲目地使用只会暴露出并没有深入地理解。

这里给出三种方法来避免循环引用:

  1. __weak 修饰符

通过weak肯定能解除循环引用,但也有缺点,有时候我们会在内部使用strong来保证执行block的时候外部变量不会释放,但这也不能避免在block执行前已经释放了的这种情形。

typedef void(^blk_t)();
@interface Block ()
@property (nonatomic, strong) NSMutableArray *array;
@property (nonatomic, strong)blk_t blk;
@end

@implementation Block
- (instancetype)init{
    if (self = [super init]) {
        __weak typeof(self) weakSelf = self;
        self.blk = ^{
            NSLog(@"test%@", weakSelf.array);
        };
    }
    return self;
}
@end
  1. __block修饰符+nil操作

这种情况则也能避免循环引用,但是有个前提,这个Block必须在某个地方执行,否则还是不能打破循环引用环,但是这么做的好处还是很多的,首先可以延长生命周期,其次在执行Block的时候可以决定是否是将nil赋予,还是给另一个值。

typedef void(^blk_t)();
@interface Block ()
@property (nonatomic, strong) NSMutableArray *array;
@property (nonatomic, strong)blk_t blk;
@end

@implementation Block
- (instancetype)init{
    if (self = [super init]) {

        __block typeof(self) weakSelf = self;

        self.blk = ^{
            NSLog(@"test%@", weakSelf.array);
            weakSelf = nil;
        };
    }
    return self;
}
@end
  1. unsafeunretain修饰符

这其实只是用在一些weak不支持的场合,现在几乎不怎么用了。

三者各有优缺点,weak不能够延长生命周期,block修饰符,如果最后没有执行,导致赋值nil不能够执行则还是会循环引用,unsafeunretain和weak相似,但是有可能会出现野指针的情况。

MRC下地Block

  • 在mrc下需要手动的调用copy和release,只要是copy到堆上了,则可以使用retain来持有,否则retain并没有效果。
  • 在C语言中也支持,使用的是Block_copy和Block_release函数。
  • 在ARC无效的时候我们只需要使用block就能避免循环引用了,这是因为,在mrc下block对象不会被retain。

结束语

关于Block的探究也差不多了,接下来就是实战中去体会了,不过在探究的过程中发现,是时候去深入地理解一下Objc的内存管理机制,同时也发现在这本书中的ARC讲解已经有点过时了,有些和真实地情况有出入。