首先,本文并不会涉及太多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
然后来看一段代码和其执行结过:
上边代码定义了一个私有局部block,在声明之初这个block是__NSGlobalBlock__类型的。以上述三种方式声明的block,在最初都是__NSGlobalBlock__类型的。
然后看另一段代码:
打印结果显示,在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都会有很多种情况造成循环引用。 比如:
- 在全部block内--使用外部全局属性
- 在全部block内--调用外部方法
- 在全部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中被调用了。
所以,如果你在写功能就尽量注意不要让自己的类循环引用了。如果你是准备封装一个功能,尽量不要暴露初始化方法给外边。
还有一种情况:
请不要画蛇添足,本来没有问题的代码,加上这句话就会有问题了。 这样子就可以了。时间关系没有用MRC做过测试,我觉着如果你是MRC,可以自己控制,如果你ARC,就不要这样写了。有志者、事竟成,破釜沉舟,百二秦关终属楚;
苦心人、天不负,卧薪尝胆,三千越甲可吞吴.