iOS 如果你使用过block,最好看一下这篇文章

644 阅读7分钟

首先,本文并不会涉及太多block的源码,更多的是block使用方面的一些东西。 新人写文,如有错误,敬请斧正!

原理

使用也好认识也好,首要任务就是搞懂block到底是个什么东西,其中的内容我们使用到的都有哪些,所以先介绍一下block的本体。block的本体是一个结构体,长这样:

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

1.第一个成员变量:impl也是一个结构体,长这样:

struct __block_impl {  
    voidvoid *isa;  
    int Flags;  
    int Reserved;  
    voidvoid *FuncPtr;  
};  

impl结构体第一个成员变量是isa指针,与NSObject及其派生类对象的isa不同,impl指向的是block三种类型的其中一种。 impl第二第三个成员变量可以忽略过去。 第四个成员变量是一个函数指针,也就是block所执行的代码段的真正地址。

block的三种类型(重点)

1._NSConcreteGlobalBlock,保存在数据区域 2._NSConcreteMallocBlock,保存在堆控件 3._NSConcreteStackBlock,保存在栈控件

2.第二个成员变量:Desc直接跳过,用不到 3.第三个成员变量:第三个成员变量是一个结构体函数,如果细说也会有些篇幅,所以就不多做解释了,我们需要知道的是:block声明的时候会调用这个结构体函数进行赋值,内存地址会指向impl结构体的isa指针,需要执行代码块地址会指向impl结构体的函数指针FuncPtr,其他的忽略过去就好了。

声明

1.block被声明的时候会实例化类型为__NSGlobalBlock的block,根据后续使用会转换成不同类型的block。 2.block被声明的时候block块内代码会转换成__main_block_func_0函数,参数就是__main_block_impl_0结构体。 3.block被声明实际上就是将__main_blcok_impl_0结构体实例化。 4.block被声明之后的调用是通过impl结构体的FuncPtr指针调用,参数就是结构体本身。

以上就是block声明会干的事情

使用

属性的声明有三种方式,所以block的声明也有三种方式

1.私有局部block 2.私有全局block 3.公有全局block

然后来看一段代码和其执行结过:

定义.png
结果.png

上边代码定义了一个私有局部block,在声明之初这个block是__NSGlobalBlock__类型的。以上述三种方式声明的block,在最初都是__NSGlobalBlock__类型的

然后看另一段代码:

使用外部局部私有属性.png
结果.png

打印结果显示,在block内部输出block本身为空,因为block是局部变量,在block内部输出block本身的时候局部block已经被释放,所以在block内部输出block本身为空。block外部输出block本身的时候,类型已经变为__NSMallocBlock__。表明block的存储地址已经被修改了,并且存放空间已经由数据区域(全局区域,后不做表述)转移到堆区域,然后解释一下为什么会这样。

无论是哪种方式声明的block在使用外部变量或属性的时候,block默认会将其复制一份加入到自己的结构体中。所以block内部使用的变量越多其体积就会越大。并且block只能访问这个局部变量,不能对其作出任何修改

如果想对这个变量做出修改需要在局部变量声明的时候加上__block修饰符,__block的原理可以看我这篇文章,解释同样简单粗暴。这里只做简单解释,被__block修饰后的变量在block内部会变成名为Block_byref_(变量名)_0的结构体实例,该结构体会包含一个对该结构体实例本身的引用,此时对变量做修改的时候会通过Block_byref_(变量名)_0结构体中名为forwarding的成员变量间接访问这个变量。

block的内存地址

block的内存地址位置需要仔细说一下,这会涉及到后边循环引用的问题

1.初始化刚刚结束的时候,block会被放到数据区。 2.在block内部访问外界变量,block会被转移到栈空间存储。 3.在ARC环境下,访问外界变量的block会先被转移到栈空间,然后copy到堆空间。

在MRC环境下,开发者可以手动管理block的引用计数,可以避免栈空间的block被提前释放影响后续使用。但是在ARC环境下开发者不能管理其引用计数,所以block默认会被copy到堆空间。

会引起循环引用的block使用方式

局部block

我在局部block做出了以下尝试,都没有引起循环引用。

1.在局部block内--使用外部临时变量 2.在局部block内--使用外部私有全局变量 3.在局部block内--使用外部全局私有变量 4.在局部block内--修改其他类属性 5.在局部block内--调用全局方法 3.在局部block内--使用全局block回调到来源类执行耗时操作 3.在局部block内--夸三界面使用block执行耗时操作

根据上述内容得知,在block内部使用的属性会被copy一份加入到自己的结构体中,而局部block早在其所在方法体结束的时候就被释放了。所以在此大胆猜测,局部block无论任何情况都不会引起循环引用。如果其结构体成员变量指向对象有延时(或耗时)操作,会在操作结束后做释放操作。(敬请斧正!)

全局block

全局block分两种,一种是私有全局block,另一种是公有全局block,两种block的情况基本相同,本不应该分开叙述,但是公有全局block会涉及到其他情况,所以还是分开描述一下。接下来就全局block会引起循环引用的情况做一下说明。

1.私有全局block 只要是全局block,无论哪种全局block都会有很多种情况造成循环引用。 比如:

  1. 在全部block内--使用外部全局属性
  2. 在全部block内--调用外部方法
  3. 在全部block内--修改其他类属性(或调用其他类方法、实例方法)

2.公有全局block 公有全局block的循环引用情况和私有的基本差不多,唯一的区别就是公有block会被其他类调用,如果其他类里边有循环引用,就会导致当前本来没有循环引用的类也不被释放。 比如: A类有循环引用,调用了B类,会导致B类也无法释放。

其实解决block循环引用的方法特别多,众所周知的是这个方法:

__weak __typeof(self) weakSelf = self;

为自己做一个弱引用指针确实可以解决当前类的循环引用,注意:是解决当前类的循环引用。就像上述所说的问题,如果你有一个公有全局block,你的类本身没有循环引用问题,但是如果其他类实现了你的block,如果它有循环引用的问题,就会导致你的类也无法释放,所以还有另外一种解决循环引用的方法。

引用一下SDWebImage的代码:

- (void)startPrefetchingAtIndex:(NSUInteger)index {
    if (index >= self.prefetchURLs.count) return;
    self.requestedCount++;
    [self.manager downloadImageWithURL:self.prefetchURLs[index] options:self.options progress:nil completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
        if (!finished) return;
        self.finishedCount++;

        if (image) {
            if (self.progressBlock) {
                self.progressBlock(self.finishedCount,[self.prefetchURLs count]);
            }
        }
        else {
            if (self.progressBlock) {
                self.progressBlock(self.finishedCount,[self.prefetchURLs count]);
            }
            // Add last failed
            self.skippedCount++;
        }
        if ([self.delegate respondsToSelector:@selector(imagePrefetcher:didPrefetchURL:finishedCount:totalCount:)]) {
            [self.delegate imagePrefetcher:self
                            didPrefetchURL:self.prefetchURLs[index]
                             finishedCount:self.finishedCount
                                totalCount:self.prefetchURLs.count
             ];
        }
        if (self.prefetchURLs.count > self.requestedCount) {
            dispatch_async(self.prefetcherQueue, ^{
                [self startPrefetchingAtIndex:self.requestedCount];
            });
        } else if (self.finishedCount == self.requestedCount) {
            [self reportStatus];
            if (self.completionBlock) {
                self.completionBlock(self.finishedCount, self.skippedCount);
                self.completionBlock = nil;
            }
            self.progressBlock = nil;
        }
    }];
}

看这一段代码:

            if (self.completionBlock) {
                self.completionBlock(self.finishedCount, self.skippedCount);
                self.completionBlock = nil;
            }

在block被执行之后将block释放掉,当然有几种情况这样也不能解决你的类的循环引用。

1.你的类被其他类声明成全局变量。 2.你的类被声明为临时变量,但是在block中被调用了。

所以,如果你在写功能就尽量注意不要让自己的类循环引用了。如果你是准备封装一个功能,尽量不要暴露初始化方法给外边。

还有一种情况:

画蛇添足.png
请不要画蛇添足,本来没有问题的代码,加上这句话就会有问题了。
image.png
这样子就可以了。时间关系没有用MRC做过测试,我觉着如果你是MRC,可以自己控制,如果你ARC,就不要这样写了。



有志者、事竟成,破釜沉舟,百二秦关终属楚;

苦心人、天不负,卧薪尝胆,三千越甲可吞吴.