ijkplayer 源码分析
KVC 是一种不用通过调用 setter、getter 方法而是直接通过属性字符串名称key来存取属性的机制。
KVC 和 属性访问器的对比如下:
KVC是通过在运行时动态的访问和修改对象的属性而访问器是在编译时确定,单就这一点增加了访问属性的灵活性,
但是用访问器访问属性的时候编译器会做预编译处理,访问不存在的属性编译器会报错,使用KVC方式的时候如果有错误只能在运行的时候才能发现。
相比访问器KVC 效率会低一点。
KVC 可以访问对象的私有属性,修改只读属性
KVC 在字典转模型,集合类操作方面有着普通访问器不能提供的功能
总而言之 KVC的特点是在运行时访问,可以访问对象私有属性和修改只读属性,在字典转模型,集合类操作方面十分便捷。
首先先列下关键的一些方法,后续都会对这些方法进行介绍:
//通过KeyPath来取值 |
下面是KVC 关键用法的总结:
|
2019-06-21 11:27:36.860598+0800 KVC-Demo[42518:5605320] name = linxiaohai age= 0 readOnlyValue = (null) is Male = 0 |
首先搜索 setter 方法,有就直接赋值。 |
总结来说就是KVC设值的顺序如下: setKey -> _key -> _isKey -> key -> isKey |
* 首先查找 getter 方法,找到直接调用。如果是 bool、int、float 等基本数据类型,会做 NSNumber 的转换。 |
总结来说就是KVC取值的顺序如下: getKey -> _key -> _isKey -> key -> isKey |
keyPath 用法:
Teacher *teacher = [Teacher new]; |
如果都没找到对应的key就会调用setValue:forUndefinedKey: 和valueForUndefinedKey:
下面是一个例子:
增加 |
其他API
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key; |
使用字典来初始化模型
NSDictionary *catogiresDic = @{ |
通过key的集合来从一个model中取出值形成一个字典
NSDictionary *value = [catogires dictionaryWithValuesForKeys:@[@"name",@"image"]]; |
KVC对于数组而言最大的功能还是获取集合类的 count,max,min,avg,sum 这是一个很好用的功能
集合操作的keyPath整体格式如下图所示:
左边是当前对象中待操作的集合实例对象,如果是针对自身的某个属性做操作,则可以省略,”@”操作符右边的是需要操作的属性。如果操作符是count的话右边也可以省略,这时候相当于针对当前数量统计。
NSLog(@"count of book price : %@",[student valueForKeyPath:@"bookList.@count"]); |
如果自身是一NSNumber数组的话右边的可以为self,表示针对的是自身
NSLog(@"count of book price : %@",[student valueForKeyPath:@"@count"]); |
通过这个特性能够一个对象集合的所有某个特定字端的值组成一个数组
一共有两种:
@distinctUnionOfObjects |
它们的返回值都是NSArray,区别是前者返回的元素都是唯一的,是去重以后的结果;后者返回的元素是全集。例子如下:
NSArray* arrDistinct = [arrBooks valueForKeyPath: |
这类操作符的操作对象内对象必须是数组/集合,也就是元素为数组/集合的数组/集合:下面是一个简单例子:
IDLProductModel *productA = [[IDLProductModel alloc] init]; |
NSArray *distinctUnionOfArrays = [@[product, product] valueForKeyPath:@"@distinctUnionOfArrays.price"]; |
这种其实就是将操作对象的所有数据取出来,展开铺平为一个数组后,再对数组或者集合做操作。
KVC提供了验证Key对应的Value是否可用的方法,但是这个方法不会自动调用,必须在使用的时候手动调用:
- (BOOL)validateValue:(inoutid*)ioValue forKey:(NSString*)inKey error:(outNSError**)outError; |
如下例子:
- (BOOL)validateValue:(inout id _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError * _Nullable __autoreleasing *)outError { |
NSArray *arr = @[@"iPod", @"iPhone", @"iMac", @"iPhone8 Plus"]; |
NSArray *array = : : }, |
KVO 适合任何对象监听另一个对象的改变,这是一个对象与另外一个对象保持同步的一种方法。KVO 只能对属性做出反应,不会用来对方法或者动作做出反应
为某个对象的属性添加观察者:
[_person addObserver:self |
回调方法
- (void)observeValueForKeyPath:(NSString *)keyPath |
移除观察者
[self.person removeObserver:self forKeyPath:@"age"]; |
这里比较重要的是option这个参数:
NSKeyValueObservingOptionNew = 0x01 change字典包括改变后的值 |
假如有个 Person 类,类里有三个属性,fullName、firstName、lastName。这种情况如果需要观察名字的变化,就要分别添加 fullName、firstName、lastName 三次观察,非常麻烦。通过设置相互关联的属性就会在关联的属性发生变化的时候,另外的属性也受到通知。
@interface Person : NSObject |
上面的例子中我们不论设置Person类的firstName或者lastName 都会触发 fullName 的观察。
@interface Target : NSObject { |
如果我们想获取一个对象上有哪些观察者正在监听其属性的修改,则可以查看对象的observationInfo属性
id info = object.observationInfo; |
KVO 是通过 isa-swizzling 实现的,当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中做如下的几方面工作: |
一般来讲,开一个线程执行某项任务,在任务执行完成后线程就会退出。如果我们需要让线程能不退出一直常驻,随时处理事件这就需要消息循环来实现了。
Runloop 是 iOS中的消息循环,Android也有Looper/Handler一套消息循环机制,这到底是啥,以前玩过单片机的小伙伴肯定记得很清楚,我们在配置完硬件,对各个部件进行初始化完毕后,都会在main中执行一个死循环,然后等待硬件中断,软件中断,在对应的中断函数中处理外部事件,其实这个死循环就是我们今天要介绍的Runloop,它会保持整个应用进程处于存活状态,然后在每个消息循环中处理各个事件。Runloop是我们平常看不见摸不着,但是又比不可少的一个系统组件。
Runloop 的思想可以用下面的伪代码来表示:
int mai (void) { |
依赖NSRunloop的类和框架
NSTimer |
CFRunLoopRef基于C线程安全,NSRunLoop基于CFRunLoopRef面向对象的API是不安全的.
RunLoop和线程的一一对应的,对应的方式是以key-value的方式保存在一个全局字典中,
主线程的 Runloop 会在应用启动的时候启动,其他线程的 Runloop需要我们手动创建并启动,RunLoop在第一次获取时创建,在线程结束时销毁,我们只能在一个线程的内部获取其RunLoop,并且苹果系统不允许我们直接创建RunLoop对象,只能通过以下几个函数来获取RunLoop:
CFRunLoopRef CFRunLoopGetCurrent(void) |
Runloop Mode是 Source,Timer 和 Observer 的集合,不同的 Mode 把不同组的 Source,Timer 和 Observer隔绝开。Runloop 在某个时刻只能跑在一个Mode下,处理这一个Mode 当中的 Source,Timer 和 Observer,这也是为啥在滑动屏幕的时候有时候定时器会失效的原因,这个会在后续介绍。
Source0是内部事件 UIEvent、CFSocket都是source0,它区别于source1,它不是基于Port的,它不能主动触发事件。我们需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop处理这个事件。
Source1由RunLoop和内核管理,source1带有mach_port_t和一个回调,可以接收内核消息并触发回调,它能主动唤醒 RunLoop 的线程。
source0 有公开的 API 可供开发者调用,source1 却只能供系统使用
Timmer 包含一个时间长度和一个回调。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。
NSTimer的创建通常有两种方式,一种是以timer开头,另一种是以schedued开头,第一种是没有添加到Runloop中的,需要我们自己去手动调用代码添加,第二种会自动以NSDefaultRunLoopModeMode添加到当前线程RunLoop中。
一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环
struct __CFRunLoop { |
每个__CFRunLoop 会存储有:
当前RunLoop所对应的线程****_pthread****
当前RunLoop所能运行的所有模式****_modes以及它的锁_lock****
当前RunLoop当前运行模式****_modes****
当前RunLoop当前运行模式****_currentMode****
当前RunLoop的公用模式信息****_commonModes,_commonModeItems****
当前RunLoop唤醒端口****_wakeUpPort****
添加到当前Runloop的Block信息****_blocks_head,_blocks_tail****
当前RunLoop相关的统计信息****_runTime,_sleepTime****
__CFRunLoopMode
struct __CFRunLoopMode { |
__CFRunLoopMode 里面最关键的元素包括:****_sources0****,_sources1,_observers,_timers,_portSet
iOS 系统中定义了如下几种Mode:
struct __CFRunLoopSource { |
Source0是App内部事件,由App自己管理的UIEvent、CFSocket都是source0。当一个source0事件准备执行的时候,必须要先把它标记为signal状态,标记信息位于****_bits****
struct __CFRunLoopObserver { |
__CFRunLoopObserver 里面最关键的字段是****_callout****,它会在通知发出的时候被回调。
struct __CFRunLoopTimer { |
__CFRunLoopTimer 里面主要包含了触发的时机,以及对应的回调方法。
在针对RunLoop源码进行解析的时候大家可以针对下面这张图进行理解。下面的解析也是围绕者这张图来分析的:
前面提到除了主线程外,其他我们自己创建的线程是没有RunLoop的,我们可以通过currentRunLoop来创建,而主线程的RunLoop是通过mainRunLoop创建的,最终会调用CFRunLoopRef 下的CFRunLoopGetCurrent,CFRunLoopGetMain:
CFRunLoopRef CFRunLoopGetMain(void) { |
上面的这两个方法都会归到一处入口,****_CFRunLoopGet0****传入的是当前所在的线程:
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) { |
只要是第一次调用****_CFRunLoopGet0,不管是getMainRunloop还是get子线程的runloop,主线程的runloop总是会被创建,创建出来的RunLoop会被添加到__CFRunLoops这个全局字典里面。这个字典以线程指针作为key,Runloop作为value进行存储的,后续创建的一系列的Runloop都会添加到这里。每次调用_CFRunLoopGet会先从这里看下是否有已经创建好的RunLoop如果有就直接返回,如果没有则新建一个,然后存到__CFRunLoops****作为缓存。在最后的时候会判断如果传入线程就是当前线程,则会注册一个回调,当线程销毁时,销毁对应的RunLoop。
static CFRunLoopRef __CFRunLoopCreate(pthread_t t) { |
这里没啥亮点。我们继续看下一步:
在什么都没有指定的时候,CFRunLoopRun默认是跑在kCFRunLoopDefaultMode模式下的
void CFRunLoopRun(void) { /* DOES CALLOUT */ |
后续的各种run最终的入口都是归结到CFRunLoopRunSpecific:
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */ |
在CFRunLoopRunSpecific中会通过传入的modeName,找到对应的CFRunLoopModeRef,并将其传入****__CFRunLoopRun作为当前RunLoop要跑的模式。进入和退出__CFRunLoopRun会发出kCFRunLoopEntry,和kCFRunLoopExit****通知外部的Observer Runloop将要开始,以及Runloop已经退出。
__CFRunLoopRun方法有点长,我省略了一些不必要的代码:
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) { |
大体流程大家可以结合代码注释看下图中所示。
AutoreleasePool
RunLoop的进入的时候会调用objc_autoreleasePoolPush()创建新的自动释放池。
RunLoop的进入休眠的时候会调用objc_autoreleasePoolPop() 和 objc_autoreleasePoolPush() 销毁自动释放池,创建一个新的自动释放池。
RunLoop即将退出时会调用objc_autoreleasePoolPop() 释放自动自动释放池内对象。
GCD
当调用了dispatch_async(dispatch_get_main_queue())时libDispatch会向主线程RunLoop发送消息唤醒RunLoop,RunLoop从消息中获取block,并且在CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE回调里执行这个block。需要注意的是dispatch_async() 到其他线程是由libDispatch处理,并不涉及到RunLoop。
我们使用 performSelector:onThread: 或者 performSelecter:afterDelay: 时,实际上系统会创建一个Timer并添加到当前线程的RunLoop中
NSTimer 的执行必须依赖于 RunLoop,如果没有 RunLoop,NSTimer 是不会执行的。而GCD 的线程管理是通过系统来直接管理的。GCD Timer 是通过 dispatch port 给 RunLoop 发送消息,来使 RunLoop 执行相应的 block,如果所在线程没有 RunLoop,那么 GCD 会临时创建一个线程去执行 block,执行完之后再销毁掉,因此 GCD 的 Timer 是不依赖 RunLoop 的。CADisplayLink是一个执行频率和屏幕刷新相同的定时器,它也需要加入到RunLoop才能执行,通常情况下CADisaplayLink用于构建帧动画,看起来相对更加流畅。
子线程启动定时器需要按照如下方式启动Runloop
dispatch_async(dispatch_get_global_queue(0, 0), ^{ |
一般而言
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES]; |
等效于
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES]; |
当我们滚动ScrollView或者滑动屏幕的时候,RunLoop会切换到UITrackingRunLoopMode 模式,而定时器运行在defaultMode下面,系统一次只能处理一种模式的RunLoop,所以导致defaultMode下的定时器失效,所以这种情况的解决方式就是将定时器放到commonMode中,这样即使切换到UITrackingRunLoopMode也会被触发。
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; |
或者使用GCD定时器,它不会受RunLoop的影响:
// 获得队列 |
当一个硬件事件发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。随后用 mach port 转发给需要的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。
_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,
在我们修改了View frame、或者调整了UI层级,或者手动设置了setNeedsDisplay/setNeedsLayout之后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去,_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv 这个Observer会监听RunLoop即将进入休眠和退出状态,一旦进入这两种状态则会遍历所有的UI更新并提交进行实际绘制更新。facebook推出的AsyncDisplayKit的机制和它也类似,它将UI排版和绘制运算尽可能放到后台,将UI的最终更新操作放到主线程,在主线程RunLoop中增加了一个Observer监听即将进入休眠和退出RunLoop两种状态,收到回调时遍历队列中的待处理任务更新界面。
我们有时候需要创建一个在后台一直存在的程序,来做一些需要频繁处理的任务
- (void)run { |
当需要这个后台线程执行任务时,通过调用 [NSObject performSelector:onThread:..] 将这个任务扔到了后台线程的 RunLoop 中。
AFNetworking 就是通过这种方式接收 Delegate 回调
|
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler |
Block从C语言角度实质上是能够捕获上下文变量的匿名函数,在创建的时候会捕获所需要的上下文局部自动变量到闭包内部。Block的底层是作为C语言源代码来处理的,支持Block的编译器会将含有Block语法的源代码转换为C语言编译器能处理的源代码,当作C语言源码来编译。Block和__block 最终都会转换为一个C语言的结构体对象。
Block 在OC中的实现如下
struct Block_layout { |
Block 需要了解如下几个方面:
(1) 我们怎么把带有Block的 Objective C 代码转化为 C 代码
(2) 转换后的代码结构是怎样的
(3) Block对局部自动变量,局部静态变量,全局变量,对象,__block变量的捕获
(4) Block 与 __block 的存储特性
(5) Block的循环引用
(6) Block的内存布局
我们使用的转换block的命令行如下:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp |
一个简单常见的例子:
#import <UIKit/UIKit.h> |
转换后的代码:
int globleIntValue = 2; |
Block类型变量,一般结合用typedef定义:
typedef int (^blockType)(int,int) |
Block 变量可以作为自动变量,函数参数,函数返回值,静态局部变量,静态全局变量,全局变量
Block 定义:
block定义和普通的C语言函数定义类似,只不过多了一个^省去函数名称
^返回值类型 (参数列表) { |
struct __block_impl { |
void (^blk)(void) = ^ { |
转换成:
void (*blk)(void) = &__main_block_impl_0(__main_block_func_0,&__main_block_desc_0_DATA,auto_peram1,auto_peram2); |
blk() 实际上执行的是
(*blk->impl.FuncPtr)(blk); |
一个block的组成:
struct __block_impl impl; |
block接受的参数属于值传递,可以在block内修改,不对其进行捕获
局部变量的捕获: 当定义block的时候,Block中所使用的自动变量值被保存到Block的结构体实例中,也就是Block自身中,并且保存后就不能改写该值。但是还是可以调用变更该对象的方法。比如NSMutableArray 的 addObject
static局部变量的捕获: 局部静态变量 作用域在block之外,这种情况下block中存储的是静态局部变量的地址,所以当FuncPtr指向的函数调用的时候会通过取地址中存储的值
全局变量的捕获
全局变量并没有被block所捕获,因为全局变量存放在全局区,随时都可以访问,所以当FuncPtr指向的函数调用的时候就会直接取全局变量的值使用。而局部变量超过作用域就会自动回收所以block需要在自身存放一份,以保证其能准确访问。
对象类型的捕获:
在没有调用copy的情况下,还没有调用block()的时候对象就已经释放了,说明在栈上的block并没有对所使用的对象强引用,对block 进行一次copy操作会发现在block没有release之前,所引用的对象没有被释放,所以堆上的block强引用了所使用的对象,对对象执行一次release之后,对象的引用计数依然没有成为0,因为block还引用着它。这是因为当对block进行copy操作的时候,block会执行内部的__main_block_copy_0方法。__main_block_copy_0方法执行_Block_object_assign根据变量的修饰符判断对捕获的对象的引用情况(retain或者弱引用)。而当block从堆中移除的时候,会调用与__main_block_copy_0对应的_Block_object_dispose函数,该函数会自动释放引用的auto变量。(栈上block访问了对象类型的auto变量的时候不会对其发生强引用)
block从栈上copy到堆上的时候,block内部会执行copy操作,_Block_object_assign函数回通过auto变量的修饰符判断发生强弱引用。block从堆中移除的时候,block内部会执行dispose,将引用的对象进行释放。
简而言之:
局部变量在block中使用的时候会被block捕获,auto变量是值捕获,而static变量是地址捕获。全部变量不会被捕获。当Block从栈复制到堆上时,block会对id类型的变量产生强引用
在block中允许修改静态局部变量,静态全局变量和全局变量这几种类型,但是对于局部自动变量如果在block里面修改编译器会发出警告,这时候需要在局部自动变量之前添加__block 说明符,__block 可以指定任何类型的自动变量,通过__block修饰的变量将会变成一个结构体实例,只有这样这个值才能被block共享、并且不受栈帧生命周期的限制、在block被copy后,能够随着block复制到堆上
这时候被捕获的对象会转换为
struct __Block_byref_val_0 { |
__main_block_impl_0中也会多出一个
__Block_byref_val_0 *val; |
也就是说局部自动变量添加了__block后该变量将会以__Block_byref_val_0 形式被捕获
有时候__block变量配置在堆上的情况下,也可以访问栈上的__block变量,在这种情况下只要栈上的结构体实例成员变量__forwarding指向堆上的结构体实例,那么不管是从堆上的__block变量还是从栈上的__block变量都能正确得访问。简而言之:
__block 变量结构体成员变量__forwarding可以实现无论__block变量配置在栈上还是在堆上都能正确得访问__block变量
__block id __strong obj = [[NSObject alloc] init]; 和 id __strong obj = [[NSObject alloc] init] 一样在block没有copy的时候是不持有的,copy后block会持有对象引用
id array = [[NSMutableArray alloc] init];
id __weak array2 = array;
array2 是弱引用,当变量作用域结束,array 所指向的对象内存被释放,array2 指向 nil
如果 __weak 再改成 __unsafe_unretained ,__unsafe_unretained 修饰的对象变量指针就相当于一个普通指针。使用这个修饰符有点需要注意的地方是,当指针所指向的对象内存被释放时,指针变量不会被置为 nil。所以当使用这个修饰符时,一定要注意不要通过悬挂指针(指向被废弃内存的指针)来访问已经被废弃的对象内存,否则程序就会崩溃。
如果 __unsafe_unretained 再改成 __autoreleasing 会怎样呢?会报错,编译器并不允许你这么干!如果你这么写
__block id __autoreleasing obj = [[NSObject alloc] init];
编译器就会报下面的错误,意思就是 __block 和 __autoreleasing 不能同时使用。
Block 有如下几种存储类型:
_NSConcreteGlobalBlock 程序的数据区域
_NSConcreteStackBlock 栈
_NSConcreteMallocBlock 堆
整个存储区域如下图所示:
通过上述结果我们可以看出当block访问了auto变量的时候会变成__NSStackBlock__类型。而其他情况下是__NSGlobalBlock__类型,比较典型的是Block声明在全局区域,以及Block虽然不声明在全局区域但是Block不截获自动变量。
__NSGlobalBlock__类型存在于数据区,__NSStackBlock__存在于栈区。
而在ARC环境下原本__NSGlobalBlock__的block依然是__NSGlobalBlock__类型,而原本是__NSStackBlock__却变成了__NSMallocBlock__存放在堆区。这是因为当我们定义block的时候ARC默认为我们做了一次copy操作。
在开启 ARC 时,大部分情况下编译器通常会将创建在栈上的 block 自动拷贝到堆上,只有当
block 作为方法或函数的参数传递时,编译器不会自动调用 copy 方法这时候需要手动调用copy,但是比如Cocoa框架的方法且方法名中包含usingBlock等,或者GCD API的情况下,这些在方法或者函数中对传递过来的参数做了复制操作所以不需要copy
copy 的结果:
如果原先存储域处于_NSConcreteGlobalBlock 那么什么都不做
如果原先存储域处于 _NSConcreteMallocBlock 那么将会导致引用计数增加
如果原先存储域处于_NSConcreteStackBlock 那么会将block从栈复制到堆
下面是Block复制和释放的源码:
Block 复制
void *_Block_copy(const void *arg) { |
Block 释放
void _Block_release(const void *arg) { |
简而言之:
访问了auto变量的block是__NSStackBlock__类型,没有访问auto变量的block是__NSGlobalBlock__类型。而对__NSStackBlock__类型进行copy操作就会变为__NSMallocBlock__类型。
在如下情况下栈上的block会被复制到堆上:
在调用Block 的copy 实例方法的时候
Block 作为函数返回值返回时
将Block赋给带有__strong修饰符id 类型的类或者Block类型成员变量时(block作为强指针引用的时候也会自动调用copy)
在方法名中含有usingBlock的Cocoa框架方法,或者GCD API中传递Block时候
在谁都不持有Block的时候block将会被释放
copy 函数会持有截获的对象,以及所使用的__block变量,dispose函数会释放截获的对象以及__block变量,
所以Block中使用的赋给赋有__strong修饰符的自动变量的对象和复制到堆上的__block变量由于被堆上的Block所持有,因而可以超出其变量作用域而存在。
再简而言之:也就是block 被copy到堆上的时候,它所使用的strong类型的对象以及__block变量都会超出作用域而存在。
当Block从栈复制到堆上的时候,它所使用的所有__block变量也会被复制到堆上,并被Block持有。在多个Block中使用__block变量的时候,因为最先会将所有的Block配置在栈上,所以__block变量最初也会配置到栈上,在任何一个Block从栈上复制到堆上的时候,__block变量也会一起从栈复制到堆上,并被该Block 持有,当剩下的Block从栈复制到堆的时候,被复制的Block持有__block变量,并增加__block变量的引用计数。
如果配置在堆上的Block被废弃,那么它所使用的__block变量也会被释放。
如果用self引用了block,block又捕获了self,这样就会有循环引用。因此,需要用weak来声明self,如果捕获到的是当前对象的成员变量对象,同样也会造成对self的引用,同样也要避免。
使用__weak来声明self |
- (void)configureBlock { |
当struct第一次被创建时,它是存在于该函数的栈帧上的,其Class是固定的_NSConcreteStackBlock。其捕获的变量是会赋值到结构体的成员上,所以当block初始化完成后,捕获到的变量不能更改。
当函数返回时,函数的栈帧被销毁,这个block的内存也会被清除。所以在函数结束后仍然需要这个block时,就必须用Block_copy()方法将它拷贝到堆上。这个方法的核心动作很简单:申请内存,将栈数据复制过去,将Class改一下,最后向捕获到的对象发送retain,增加block的引用计数。