workLanguageiOS精品文章

Block 是如何实现的?如何避免循环引用?

2017-06-25  本文已影响584人  Lefe

本文是作者Lefe所创,转载请注明出处,如果你在阅读的时候发现问题欢迎一起讨论。本文会不断更新。

说明:

使用 Block 的时候,我们通常会有以下几点疑问,我们带着这种疑问来阅读本文,本文难免会有遗漏或者错误,望读者朋友们提出来。Lefe 在使用 Block 的时候主要遇到了以下问题:

带着这些问题,我们一块来揭开 Block 的真实面目,本文篇幅较长,可以分段阅读,建议读者耐心阅读,很枯燥的,如果能动手实现以下,会有趣很多。在阅读之前我们先了解下 Clang

Clang

本文主要用到了 Clang,那什么是 Clang 呢?它是 Xcode 默认的编译器。更多关于Clang 可以参考 本文 。这里我们主要用 Clang 把 Block 的实现转换成 C++ ,其实和 C 差不多,除了构造函数外。

打开 shell,进入 Lefe 的测试项目中,输入:

clang -rewrite-objc HelloLefe.m,这是会在当前目录下生产一个对应的 HelloLefe.cpp 文件,打开它就对了。截个图看看,别光看美女。

屏幕快照 2017-06-25 上午9.36.43.png

内存分配

在阅读下文前,我们需要对内存分配有一定的了解

屏幕快照 2017-06-25 上午10.20.09.png

关于内存分配的阅读 本文

Block 是如何实现的

掌握了 Clange 的基本使用,那我们就看看 Block 究竟做了什么。从一个简单的例子开始。

Lefe 在 HelloLefe.m 文件中,写了一个 Block,使用 clang -rewrite-objc HelloLefe.m 转换,转换后可以看到 Block 的具体实现。

- (void)lefeTestComplete
{
    void (^complete)(void) = ^(void){
        NSLog(@"Block\n");
    };
    complete();
}

@end

转换后的代码如下:

struct __HelloLefe__lefeTestComplete_block_impl_0 {
    struct __block_impl impl;
    struct __HelloLefe__lefeTestComplete_block_desc_0* Desc;
    
    // 构造方法
    __HelloLefe__lefeTestComplete_block_impl_0(void *fp, struct __HelloLefe__lefeTestComplete_block_desc_0 *desc, int flags=0) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

struct __block_impl {
  void *isa;  // isa 指针,block 其实也是一个 OC 对象,每个类都有一个指向其实例的一个指针
  int Flags;
  int Reserved;
  void *FuncPtr; // 相当于 block 中要执行的函数的指针
};
static struct __HelloLefe__lefeTestComplete_block_desc_0 {
    size_t reserved;
    size_t Block_size;
} __HelloLefe__lefeTestComplete_block_desc_0_DATA = { 0, sizeof(struct __HelloLefe__lefeTestComplete_block_impl_0)};

static void __HelloLefe__lefeTestComplete_block_func_0(struct __HelloLefe__lefeTestComplete_block_impl_0 *__cself) {
    
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_7__cv2l59cn5x90wh88hrkp35x80000gp_T_HelloLefe_b006ae_mi_0);
}
static void _I_HelloLefe_lefeTestComplete(HelloLefe * self, SEL _cmd) {

     // 这段代码是对 void (^complete)(void) = ^(void){ NSLog(@"Block\n");}; 的转换
     
    void (*complete)(void) = ((void (*)())&__HelloLefe__lefeTestComplete_block_impl_0((void *)__HelloLefe__lefeTestComplete_block_func_0, &__HelloLefe__lefeTestComplete_block_desc_0_DATA));
    
    // 这段代码相当于对 complete(); 的转换,
    ((void (*)(__block_impl *))((__block_impl *)complete)->FuncPtr)((__block_impl *)complete);
}

从上面的转换过程可以看出,声明一个 block 首先调用结构体 __HelloLefe__lefeTestComplete_block_impl_0 的构造函数,得到一个 IMP,相当于 OC 中的 IPM,它保存了这个 block 所需要的信息,当调用 block 的时候,直接调用 IPM-> FuncPtr。

到这里相信读者还是对 Block 的实现很陌生,很正常,坚持阅读一会,试试看。头脑中试着把 Block 就当做是一个 NSObject 对象。

Block 捕获变量

记得刚接触 Block 的时候,只是隐约听到 Block 可以自动捕获 Block 中使用的变量。是的,Block 可以捕获它所用到的自动变量或对象,但是它只是捕获了它所用到的变量,其他用不到的变量它并不会捕获,这里就是引起循环引用的一个重点,下文会详细将到。对应全局变量 Block 并不或去捕获。

以上的 block 的实现多少有点眉目了,那么 block 是如何捕获变量的,我把将要转换的代码改为:

- (void)lefeTestComplete
{
    int dmy = 256;
    int val = 10;
    const char *fmt = "val = %d\n";
    
    void (^complete)(void) = ^(void){
        printf(fmt, val);
    };
    complete();
}

转换后的代码如下,观察的实现发现多了
const char *fmt; int val; 这就是 block 捕获的变量,但我们发现 dmy 这个变量并没有捕获,因为在 block 中压根就没使用。结构体的构造方法也需要传入捕获的变量来构造结构体。

struct __HelloLefe__lefeTestComplete_block_impl_0 {
  struct __block_impl impl;
  struct __HelloLefe__lefeTestComplete_block_desc_0* Desc;
  const char *fmt;
  int val;
  __HelloLefe__lefeTestComplete_block_impl_0(void *fp, struct __HelloLefe__lefeTestComplete_block_desc_0 *desc, const char *_fmt, int _val, int flags=0) : fmt(_fmt), val(_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __HelloLefe__lefeTestComplete_block_func_0(struct __HelloLefe__lefeTestComplete_block_impl_0 *__cself) {
  const char *fmt = __cself->fmt; // bound by copy
  int val = __cself->val; // bound by copy

        printf(fmt, val);
    }

static struct __HelloLefe__lefeTestComplete_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __HelloLefe__lefeTestComplete_block_desc_0_DATA = { 0, sizeof(struct __HelloLefe__lefeTestComplete_block_impl_0)};

static void _I_HelloLefe_lefeTestComplete(HelloLefe * self, SEL _cmd) {
    int dmy = 256;
    int val = 10;
    const char *fmt = "val = %d\n";

    void (*complete)(void) = ((void (*)())&__HelloLefe__lefeTestComplete_block_impl_0((void *)__HelloLefe__lefeTestComplete_block_func_0, &__HelloLefe__lefeTestComplete_block_desc_0_DATA, fmt, val));
    ((void (*)(__block_impl *))((__block_impl *)complete)->FuncPtr)((__block_impl *)complete);
}

修改 Block 中的捕获的变量

上面的例子中,并不能在 Block 中修改所捕获的变量,那么如何修改 Block 中所捕获的变量呢?可以使用 __block。如果修改 Block 中的变量,编译器会直接报错。比如:

- (void)leftTestBlock
{
    int age = 0;
    void (^block)(void) = ^{
        age = 10;
    };
}

这段代码编译器直接会报错,可能有些同学会说直接用 __block,但是为什么使用 __block 就可以呢?再看一下下面的代码:

// 全局变量
int global_val = 1;
// 全局静态变量
static int static_global_val = 2;

- (void)lefeTestComplete
{
    // 静态变量
    static int static_val = 3;
    void (^complete)(void) = ^{
        global_val *= 1;
        static_global_val *= 2;
        static_val *= 3;
    };
    complete();
}

这段代码是没有任何问题的,它可以正常的编译通过,它没有使用 __block。详细大部分的同学读到这里都会有一个疑惑,这是为什么呢?我们不妨来看一下他的具体实现。发现全局变量并没有被捕获到 __HelloLefe__lefeTestComplete_block_impl_0 中,仅仅捕获了 static_val,想想也是,全局变量直接可以获取到,为什么还要捕获他呢?但捕获静态变量和以前不一样的是它捕获的是一个指针 int *static_val; 哦,对啊,直接使用它的指针就可以修改它了,但是为什么普通变量不可以使用其指针呢?因为一个 block 必须存在即使它所捕获变量的作用域释放掉,作用域释放掉后其变量也随之销毁,这意味着 block 就不能访问所捕获的自动变量了,如何修改?但是静态变量和全局变量不会释放啊!

int global_val = 1;
static int static_global_val = 2;


struct __HelloLefe__lefeTestComplete_block_impl_0 {
  struct __block_impl impl;
  struct __HelloLefe__lefeTestComplete_block_desc_0* Desc;
  int *static_val;
  __HelloLefe__lefeTestComplete_block_impl_0(void *fp, struct __HelloLefe__lefeTestComplete_block_desc_0 *desc, int *_static_val, int flags=0) : static_val(_static_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __HelloLefe__lefeTestComplete_block_func_0(struct __HelloLefe__lefeTestComplete_block_impl_0 *__cself) {
  int *static_val = __cself->static_val; // bound by copy

        global_val *= 1;
        static_global_val *= 2;
        (*static_val) *= 3;
    }

static struct __HelloLefe__lefeTestComplete_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __HelloLefe__lefeTestComplete_block_desc_0_DATA = { 0, sizeof(struct __HelloLefe__lefeTestComplete_block_impl_0)};

static void _I_HelloLefe_lefeTestComplete(HelloLefe * self, SEL _cmd) {
    static int static_val = 3;
    void (*complete)(void) = ((void (*)())&__HelloLefe__lefeTestComplete_block_impl_0((void *)__HelloLefe__lefeTestComplete_block_func_0, &__HelloLefe__lefeTestComplete_block_desc_0_DATA, &static_val));
    ((void (*)(__block_impl *))((__block_impl *)complete)->FuncPtr)((__block_impl *)complete);
}

通过上面的学习我们可以了解到,修改 Block 中捕获的变量,可以使用一下几种方式:

读到这里,相信你已经明白如何捕获自动变量了,也知道如何修改 Block 中所捕获的变量了,难道你不想知道为啥使用 __block 修饰后就可以修改 Block 中所捕获的变量吗?哈哈,坚持一下!

__block 究竟是如何实现的呢?

__block 如同 static, auto 等修饰符,主要作用是觉得某一变量该保存到哪里。看看它是如何实现的。把下面的代码转化:

- (void)lefeTestComplete
{
    __block int val = 10;
    void (^complete)(void) = ^{val = 1;};
    
    complete();
}

转换后发现多了很多内容,为什么使用 __block 需要增加这么多代码呢?Lefe 表示很好奇。当使用 __block 变量时,会将 __block 变量从栈拷贝的堆上。当多个 block 共用一个 __block 变量时,__block 变量有一个计数器来记录有多少个 block 引用了它,block 释放掉的时候,__block 变量的引用计数将减1,直到为0时,__block 变量才会释放。

struct __Block_byref_val_0 {
  void *__isa;
  // __forwarding 主要用来获取 __block 变量的值,它的指向会根据 block 所处的内存位置不同,所指向的也不同。
  __Block_byref_val_0 *__forwarding; 
  int __flags;
  int __size;
  int val; // 值
};
struct __HelloLefe__lefeTestComplete_block_impl_0 {
  struct __block_impl impl;
  struct __HelloLefe__lefeTestComplete_block_desc_0* Desc;
  __Block_byref_val_0 *val; // by ref
  
  __HelloLefe__lefeTestComplete_block_impl_0(void *fp, struct __HelloLefe__lefeTestComplete_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __HelloLefe__lefeTestComplete_block_func_0(struct __HelloLefe__lefeTestComplete_block_impl_0 *__cself) {
  __Block_byref_val_0 *val = __cself->val; // bound by ref
(val->__forwarding->val) = 1;
}
static void __HelloLefe__lefeTestComplete_block_copy_0(struct __HelloLefe__lefeTestComplete_block_impl_0*dst, struct __HelloLefe__lefeTestComplete_block_impl_0*src) {
_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
}

static void __HelloLefe__lefeTestComplete_block_dispose_0(struct __HelloLefe__lefeTestComplete_block_impl_0*src) {
_Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
}

static struct __HelloLefe__lefeTestComplete_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __HelloLefe__lefeTestComplete_block_impl_0*, struct __HelloLefe__lefeTestComplete_block_impl_0*);
  void (*dispose)(struct __HelloLefe__lefeTestComplete_block_impl_0*);
} __HelloLefe__lefeTestComplete_block_desc_0_DATA = { 0, sizeof(struct __HelloLefe__lefeTestComplete_block_impl_0), __HelloLefe__lefeTestComplete_block_copy_0, __HelloLefe__lefeTestComplete_block_dispose_0};
static void _I_HelloLefe_lefeTestComplete(HelloLefe * self, SEL _cmd) {
    __attribute__((__blocks__(byref))) __Block_byref_val_0 val = {
        (void*)0,
        (__Block_byref_val_0 *)&val, 
        0, 
        sizeof(__Block_byref_val_0), 
        10
    };
    
    void (*complete)(void) = ((void (*)())&__HelloLefe__lefeTestComplete_block_impl_0((void *)__HelloLefe__lefeTestComplete_block_func_0, &__HelloLefe__lefeTestComplete_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));

    ((void (*)(__block_impl *))((__block_impl *)complete)->FuncPtr)((__block_impl *)complete);
}

Block 的内存段

下图主要说明 block 主要保存在栈,堆和数据区。


屏幕快照 2017-06-23 下午10.06.35.png

那什么样的变量分派到栈中、堆中或数据区呢?

除了以上2中情况外,其他的都分派到栈区,分派到栈区的 block ,当作用域结束后,它所捕获的变量也就释放掉了。为了解决这个问题,Blocks 提供了一个函数,可以把栈上的 block 拷贝到堆上。这样即使作用域结束也不会使 block 被释放。被 copy 后的 block ,它的 isa 指针就会变成 impl.isa = &_NSConcreteMallocBlock。Block 也就成了堆上的 Block。

使用 ARC 后,编译器会自动把栈中的 block 复制到堆上。

typedef int (^blk_t)(int);
blk_t func(int rate) {
    return ^(int count){return rate * count;}; 
}
blk_t func(int rate) {
    blk_t tmp = &__func_block_impl_0(
    __func_block_func_0, &__func_block_desc_0_DATA, rate);
    // 直接复制了一个 block,也就是拷贝到了堆上,即使当这个函数结束后,这个 block 任然不会被销毁 
    tmp = objc_retainBlock(tmp);
    return objc_autoreleaseReturnValue(tmp); }

但是不是所有的时候,编译器都会执行 copy 操作的,以下情况编译器不会执行 copy 操作的

举个例子,下面这个例子会直接 crash,所以需要给数组中的 block 要执行 copy 操作

+ (id)getBlockArray {
    int val = 10;
    return [[NSArray alloc] initWithObjects:
            ^{NSLog(@"blk0:%d", val);},
            ^{NSLog(@"blk1:%d", val);},
            nil
            ];
}

+ (void)lefeTestComplete
{
    id obj = [self getBlockArray];
    typedef void (^blk_t)(void);
    blk_t blk = (blk_t)[obj objectAtIndex:0];
    blk();
}

但是使用系统提供的方法不需要执行 copy 操作,比如 GCD,因为在函数内部自己已经实现了 copy。

捕获对象:

前面都在讲捕获的是基本类型的变量,那么 Block 是如何捕获对象的呢?下面的例子中的数组中,打印结果为:

Array: (
    Lefe,
    Wang,
    Su,
    Yan
)

说明数组没有被释放掉。Block 内部会强引用对象,直到 Block 被释放,被引用的对象也将被释放。

@implementation HelloLefe

LefeBlock block;

+ (void)lefeTestComplete
{
    NSMutableArray *array = [NSMutableArray array];
    block = ^(NSString *name){
        [array addObject:name];
        
        NSLog(@"Array: %@", array);
    };
}

+ (void)addObject
{
    block(@"Lefe");
    block(@"Wang");
    block(@"Su");
    block(@"Yan");
    
}

@end

循环引用一:

- (void)testMemoryLeakCase1
{
    self.logId = @"Hello logId";
    
    /**
     这种情况最容易发现,因为编译器会自动提示出现循环引用
     Why?
     self(SecondViewController)持有了 finshBlock,你可以把它当作一个普通的属性,是强引用
     而 finshBlock 又引用了 self,这样就形成了一个闭环。
     How?
     既然是因为出现了闭环,我们只需要打破这层闭环就可以,让 finshBlock 持有一个弱引用,这样 self(SecondViewController)持有了 finshBlock,但是 finshBlock 没有持有 self
     */
    
    // __weak typeof(self) weakSelf = self; 一般的宏定义是这样的
    __weak SecondViewController *wSelf = self;
    
    self.finshBlock = ^(BOOL isSuccess) {
        [wSelf loginTest];
    };
    
    /**
     在我们的应用中一般是下面这种方式写,为啥使用了 __weak 和 __strong ?
     有人可能会问,先 weak 后 strong,那相当于还是强引用了 self,你确定 strong的是 self?
     */
    
    /**
     打印:
     (lldb) p weakSelf
     (SecondViewController *) $0 = 0x0000000101c16f10
     (lldb) p self
     (SecondViewController *) $1 = 0x0000000101c16f10
     (lldb) p strongSelf
     (SecondViewController *) $2 = 0x0000000101c16f10
     (lldb)
     
     发现 weakSelf self 和 strongSelf 的内存地址是一样的,只是一次浅拷贝;
     */
    __weak typeof(self) weakSelf = self;
    
    self.finshBlock = ^(BOOL isSuccess) {
        // 如果没有这句话,当 self 被释放后,weakSelf 就变为了空,所以关于 weakSelf 的一些操作也就没什么意义了,如果还想让 weakSelf 所调用的一些方法有意义那么久需要强引用 weakSelf;
        __strong typeof(self) strongSelf = weakSelf;
        
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"weakSelf.logId: %@", strongSelf.logId);
            NSString *name = strongSelf.logId;
            if (name.length > 0) {
                NSLog(@"Hello world");
            }
            [strongSelf loginTest];
        });
    };
    self.finshBlock(YES);
    
    /**
     修改前的:
     self.finshBlock = ^(BOOL isSuccess) {
        [self loginTest];
     };
     */
}

循环引用二:

- (void)testMemoryLeakCase2
{
    /**
     这里面出现了两个对象的内存泄漏: task 和 self
     task的内存泄漏:
     task 有个属性叫 blcok,但是在 block 中又捕获了 task,这样就形成了一个闭环
     self 的内存泄漏:
     因为这个 block 中捕获了 self,block 没有释放那么 self 咋么能释放呢?
     所以只要打破这个闭环,self 就释放了。
     
     */
    AsyncTask *task = [AsyncTask new];
    
    __weak AsyncTask *wTask = task;
    task.block = ^(BOOL isFinish) {
        NSString *name = wTask.lastLoginId;
        self.logId = name;
    };
    [task sendLogin];
    
    /**
     AsyncTask *task = [AsyncTask new];
     task.block = ^(BOOL isFinish) {
     NSString *name = task;
     self.logId = name;
     };
     [task sendLogin];
     */
}

循环引用三:

其实实例变量是通过 self->name 访问的,所以也可能造成循环引用。

- (void)testMemoryLeakCase3
{
    /**
     这里可能不太容易看出来,访问 name 实例变量相当于 self->name
     这样 self 持有 finshBlock, finshBlock 持有 self,形成闭环,造成循环引用
     */
    
    __weak SecondViewController *wSelf = self;
    self.finshBlock = ^(BOOL isFinish) {
        /*
         Dereferencing a __weak pointer is not allowed due to possible null value caused by race condition, assign it to strong variable first
         */
        // 发现这样写不行,还报错,它的意思是 __weak 指针可能为空,必须要强引用
        // wSelf->name = @"Hello lefe";
        
        /**
         那么为什么在 testMemoryLeakCase1 中 wSelf.logId = @"Hello logId"; 没有编译错误呢?我想
         估计 wSelf.logId 等价于 [wSelf logId],相当于调用了一个方法,
         nil 调用方法是没有错误的。你知道属性和实例变量的区别吗?
         
         下面这行代码也会报错的:
         __weak AsyncTask *task;
         task->_sex;
         
         */
        wSelf.logId = @"Hello logId";
        
        __strong SecondViewController *strongSelf = wSelf;
        strongSelf->_name = @"Hello lefe";
    };
    
    /**
    也可以使用下面方法来解除循环引用
    __block id temp = self;
    self.finshBlock = ^(BOOL isFinish) {
        temp = nil;
    };
    self.finshBlock(YES);
    */
    
    
    /**
     修改前的代码:
     self.finshBlock = ^(BOOL isFinish) {
        name = @"Hello lefe";
     };
     */
}

===== 我是有底线的 ======
喜欢我的文章,欢迎关注我的新浪微博 Lefe_x,我会不定期的分享一些开发技巧

上一篇下一篇

猜你喜欢

热点阅读