关于ARC,Block以及GCD,runloop
前言
最近又看了一遍 Objective-C高级编程 iOS与OS X多线程和内存管理 这本书。对一些问题有了一些新的认识。这篇文章将讲讲下面几个话题:
- ARC,MRC 与 引用计数
- Block的实现
- GCD与runloop
ARC, MRC 与 引用计数
引用计数
Objective C的生成的数据会存放在栈或者堆上面。对于存放在栈上面的数据(比如局部int变量等)在不需要时,系统会自动释放内存空间;对于存放在堆上面的数据(比如对象等)会使用引用计数来决定是否释放内存空间。
创建一个对象时,会malloc一块对象所需大小的内存空间;当有一变量持有该对象时(一般是有一指针指向该内存时)该对象的引用计数加一;当持有该对象的变量不再持有它时(一般是指针被销毁,或指向其他地方)该对象的引用计数减一。当对象的引用计数为0时,free这一块内存
MRC和ARC实质上都是对这个引用计数进行操作
手动引用计数 MRC
手动引用计数要确保在对象从被创建到销毁的一生中,该对象的引用计数也从1变成0。
与对象引用计数相关的函数只有3个:
- alloc/new/copy/mutableCopy
- retain
- release
alloc/new/copy/mutableCopy
这几个函数生成一个对象,并且该对象的引用计数为1
retain
retain
函数将对象的引用计数加1
release
release
函数将对象的引用计数减一
examples
autorelease函数
1 | NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init] // 1 |
- 注释2处,使用
alloc
方法新建一个对象,该对象的引用计数为1 - 注释3处,调用了
autorelease
函数;该函数并没有更改对象的引用计数,但是它将对象注册到自动释放池中 - 注释4处,因为注释3没有减少引用计数,所以对象的引用计数还是1。所以这个地方对象依然存在,可以正常使用对象
- 注释5处,因为在注释3时将对象注册进pool。此时
[pool drain]
方法会去调用对象的release
方法,将对象引用计数减1;此时引用计数为0,那么释放内存空间
所以从上面看出:**autorelease
函数并没有改变对象的引用计数,只是简单的将对象与释放池挂钩;当pool drain
时才调用对象的release
函数**。
所以 autorelease
函数实质就是延迟对象的release
方法;可以让你放心使用对象,但又防止了你忘记释放对象
array类对象
1 | id obj = [NSMutableArray array]; |
这里不需要调用对象的release
方法。因为在内部的实现是:
1 | + (id)array { |
array
类方法返回的对象,该对象的引用计数为1;但是已经注册到自动释放池中了,会在释放池结束时调用release
方法
MRC总结
对象一定成对的存在 alloc
和 release
方法;或者成对的存在retain
和 release
方法。只有这样才能对引用计数有增有减,才能保证对象能正确的释放。
如果没有成对的出现,但是程序又是正确的,那么这3个方法的调用肯定在某函数的内部。比如[pool drain]
或者 [NSMutableArray array]
等函数中。
自动引用计数 ARC
ARC不再需要你来手动调用retain
和 release
函数了,系统会自己调用这些函数。那么ARC需要注意的是什么呢:
- __strong
- __weak
- __autorelease
使用以上三个修饰符对对象进行修饰,那么就会自动增减引用计数
__strong
会自动release
1 | { |
可以看到__strong
会持有对象,并自动调用release将引用计数减一
会阻止对象注册到release pool中,并持有对象
1 | { |
可以看到,__strong会将即将注册到pool的对象抢过来,自己持有,并释放。
总结
__strong
修饰符会持有对象,并自动释放。有时会抢过pool的对象,来自己释放
__autorelease
不常用,基本原理:
- 对于已经/或即将注册到pool的对象,没有变化。会由pool来释放该对象
- 对于其他对象,会将对象retain,并强制注册到pool,由pool来release
__weak
weak变量不对引用计数产生影响。来看一下代码:
1 | id __weak obj1 = obj; |
APP管理一个全局的weak表,上面这两行代码,会往该表中插入2行记录:
每行记录的key是对象的首地址,也就是 obj
;value是存放weak变量的地址,也就是&obj1
和 &obj2
。
- 当weak变量obj1被废弃时,将weak表中的value为
&obj1
的记录删除 - 当对象被废弃时,这时候obj1,obj2应该指向nil。这时就将weak表中,key为
obj
的记录的key置成nil
所以寻找weak变量对应的对象地址时,其实就是寻找weak表中的key值
使用__weak变量时
1 | { |
可以看到使用weak变量时,其实是将该weak变量指向的对象交由一个tmp变量持有,并将该对象注册到pool中。这样可以保证对象万一被释放了,也至少还有tmp持有他,可以正常使用。
Block的实现
使用clang的-rewrite-objc
选项,可以将OC代码转换成C++代码。这样就可以看到Block的C++的实现
不截获任何变量的Block
1 | int main() { |
经过 clang -rewrite-objc main.m
转化之后,生成main.cpp
。来看看Block在C++中如何表现:
1 | // 只截取关键代码 |
可以发现Block实际上是一个结构体,blk变量是指向__main_block_impl_0
结构体的指针。该结构体中有一个函数指针 FuncPtr
,执行Block时,其实就是执行该结构体中函数指针对应的函数。
截获变量但不修改的Block
1 | int main() { |
相同的方法,来看一下转化之后的结果:
1 | struct __main_block_impl_0 { |
可以看到,对于截获但是未曾改变的变量val
,Block结构体会将该变量拷贝到结构体中的同名成员变量中。所以当main函数里面定义的val之后无论如何改变,都不会影响到Block中存储的val变量;因为他们两个是不同的两个变量。
截获__block变量
当要在Block里面改变截获的变量时,需要使用**__block**修饰符。
全局变量
1 | int global_val = 2; |
对于全局变量,Block可以直接访问,不需要做任何的改变
静态局部变量
1 | int main() { |
来看转化之后:
1 | struct __main_block_impl_0 { |
可以看到,对于静态局部变量,Block的结构体中存放的是指向该变量的指针,所以可以通过指针来改变该静态局部变量。这是因为,静态局部变量即使其作用域已经结束,但是它并不会被销毁,其内存空间是一直存在的
局部变量
1 | int main() { |
在Block中改变局部自动变量需要使用__block
修饰符;转化成C++代码:
1 | struct __Block_byref_val_0 { |
可以看到__block变量val其实是一个结构体 __Block_byref_val_0。在里面有val这个变量。
因为正常生成的Block都是在栈上面的,但是在合适的情况下面,栈上的Block会被拷贝到堆上;与此同时,Block所拥有的__block结构体也会被拷贝到堆上,相当于Block持有__block变量。
当block变量还在栈上时,它的forwarding指向自己;当被拷贝到堆上时,forwarding指向堆上的block变量。在使用时全部使用forwarding,这样可以保证改变的是同一个变量。
使用这么一个结构体来存放局部变量,可以保证局部变量可以时时刻刻被更改,也可以保证栈上局部变量被释放后,堆上的局部变量依然可以访问
其他
在堆上的Block或者block变量才谈论对象的retain以及release;在栈上的Block和block会被栈自动释放,也不存在对象的retain和release
Block会在一下情况下被复制到堆上面:
- 显式调用Block的
copy
方法 - Block作为函数返回值返回
- 将Block赋给
__strong
的id类型或者Block的类的成员变量时 (这时候要注意self和Block的循环引用) - 方法名中含有usingBlock的Cocoa框架方法,或者GCD中使用的Block
GCD 与 runloop
- GCD操作的是
queue
,然后由queue来确定block该由哪个线程来执行。 - runloop对应的则直接是线程。一个线程可以有0-1个runloop在上面执行。
runloop
runloop实际上就是一个运行在特定线程上面的do-while循环。在do-while循环里面,可以处理多种事件:
- port-based event,多半是系统事件
- 其他线程传递给当前线程来直线的block
- Timer,定时事件
- Observer,当runloop状态变化时,执行相应的监听器
有事件来时,runloop就执行事件;没有事件,就进入休眠,但是并没有退出do-while循环。只有当runloop到达限定时间,或者手动退出runloop;这个do-while循环才会结束
主线程与runloop
APP的主线程一直运行着一个runloop循环,这个runloop由系统运行,并且已经往该runloop中添加了一些事件。
硬件事件响应
当发生硬件事件时,也就是触摸,锁屏,摇动等事件时;这些事件都是系统事件,也就是port-based的类型。那么runloop会使用_UIApplicationHandleEventQueue()
该函数来处理这一类事件
手势识别
在_UIApplicationHandleEventQueue()
函数中,会识别出需要待处理的手势。当runloop进入休眠之前,会出发Observer,在这个Observer中会调用_UIGestureRecognizerUpdateObserver()
函数,该函数会处理手势(也就是这行手势的回调函数)
界面更新
每当runloop即将进入休眠,或者即将退出时,会执行一个Observer。在这个Observer里面会查看每个View的frame等属性,如果需要重新绘制,那么开始重新绘制的过程。(layout -> display -> draw 等等)
所以当其他的事件执行时间过长,超过16ms的话,就会推迟进入休眠,也就会推迟Observer的触发,也就会推迟界面的更新。那么就会造成卡顿的现象。