Description
- Swift 是⾃动管理内存的,我们不再需要操⼼内存的申请和分配。当我们通过初始化创建⼀个对象时,Swift 会替我们管理和分配内存。
释放的原则遵循了⾃动引⽤计数 (ARC) 的规则:当⼀个对象没有引⽤的时候,其内存将会被⾃动回收。
这简化了编码,我们只需要保证在合适的时候将引⽤置空 (⽐如超过作⽤域,或者⼿动设为 nil 等),就可以确保内存使⽤不出现问题。
循环引⽤
A B has strong reference with each other
- weak - 类似 weak in Objective-C
- 在引⽤的内容被释放后,标记为 weak 的成员将会⾃动地变成 nil (因此被标记为 @ weak 的变量⼀定 需要是 Optional 值)
- unowned - 类似 unsafe_unretained in Objective-C
- 就是 unowned 设置以后即使它原来引⽤的内容已经被释放了,它仍然会保持对被已经释放了的对象的⼀个 "⽆效的" 引⽤,它不能是 Optional 值,也不会被指向 nil 。如果你尝试调⽤这个引⽤的⽅法或者访问成员属性的话,程序就会崩溃
// example
lazy var printName: ()->() = {
[weak self] in
if let strongSelf = self {
print("The name is \(strongSelf.name)")
}
}
// 这种在闭包参数的位置进⾏标注的语法结构是将要标注的内容放在原来参数的前⾯,
// 并使⽤中括号扩起来
// 如果有多个需要标注的元素的话,在同⼀个中括号内⽤逗号隔开
// 标注前
{ (number: Int) -> Bool in
//...
return true
}
// 标注后
{ [unowned self, weak someObject] (number: Int) -> Bool in
//...
return true
}
@autoreleasepool
When object instance is marked as autoreleased, it will have a retain count of +1 at that moment in time.
At the end of the run loop, the pool is drained, and any object marked autorelease then has its retain count decrease.
It's a way of keeping an object around while you prepare whatever will retain it for itself.
- Features 不适用于大内存 Extend tmp variable's life time
Swift 在内存管理上使⽤的是⾃动引⽤计数 (ARC) 的⼀套⽅法,在 ARC 中虽然不需要⼿动地调⽤
像是 retain , release 或者是 autorelease 这样的⽅法来管理引⽤计数,但是这些⽅法还是都会
被调⽤的 -- 只不过是编译器在编译时在合适的地⽅帮我们加⼊了⽽已。
retain / release - 将对象的引⽤计数加⼀或者减⼀。
autorelease - 特殊⼀些,它会将接受该消息的对象放到⼀个预先建⽴的⾃动释放池 (auto release pool) 中,并在 ⾃动释放池收到drain 消息时将这些对象的引⽤计数减⼀,然后将它们从池⼦中移除 (这⼀过程形象地称为“抽⼲池⼦”)。
在 app 中,整个主线程其实是跑在⼀个⾃动释放池⾥的,并且在每个main Runloop 结束时进⾏drain 操作。
这是⼀种必要的延迟释放的⽅式,因为我们有时候需要确保在⽅法内部初始化的⽣成的对象在被返回后别⼈还能使⽤,⽽不是⽴即被释放掉。
// dataWithContentsOfFile 返回的是 autorelease 的对象,因为我们⼀直处在循环中,
// 因此它们将⼀直没有机会被释放。如果数量太多⽽且数据太⼤的时候,很容易因为内存不⾜⽽崩溃。
func loadBigData() {
if let path = NSBundle.mainBundle().pathForResource("big", ofType: "jpg") {
for i in 1...10000 {
let data = NSData.dataWithContentsOfFile(
path, options: nil, error: nil)
NSThread.sleepForTimeInterval(0.5)
}
}
}
// swfit autorelaeasepool
func autoreleasepool(code: () -> ())
//利⽤尾随闭包的写法,很容易就能在 Swift 中加⼊⼀个类似的⾃动释放池了:
func loadBigData() {
if let path = NSBundle.mainBundle().pathForResource("big", ofType: "jpg") {
for i in 1...10000 {
autoreleasepool {
let data = NSData.dataWithContentsOfFile(
path, options: nil, error: nil)
NSThread.sleepForTimeInterval(0.5)
}
}
}
}
// 这⾥我们每⼀次循环都⽣成了⼀个⾃动释放池,虽然可以保证内存使⽤达到最⼩,
// 但是释放过于频繁也会带来潜在的性能忧虑。
// ⼀个折衷的⽅法是将循环分隔开加⼊⾃动释放池,
// ⽐如每 10 次循环对应⼀次⾃动释放,这样能减少带来的性能损失。
After Swift 1.1, for this example
// 从 Swift 1.1 开始,因为加⼊了可以返回 nil 的初始化⽅法,
// 像上⾯例⼦中那样的⼯⼚⽅法都已经从 API 中删除了。今后我们都应该这样写:
let data = NSData(contentsOfFile: path)
//使⽤初始化⽅法的话,我们就不需要⾯临⾃动释放的问题了,
// 每次在超过作⽤域后,⾃动内存管理都将为我们处理好内存相关的事情。
黑幕背后的Autorelease
from http://blog.sunnyxx.com/2014/10/15/behind-autorelease/
prefix
- MRC中,调用[obj autorelease]来延迟内存的释放是一件简单自然的事,
- ARC下,我们甚至可以完全不知道Autorelease就能管理好内存。
- 这背后,objc和编译器都帮我们做了哪些事呢,here is 黑幕背后的Autorelease机制。
when we release autorelease object.
- 在没有手加Autorelease Pool的情况下,Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop
// 小实验
weak var reference: NSString? = nil
fun viewDidLoad() {
super.viewDidLoad()
var str: NSString = NSString.stringWithFormat("sunnyxx")
// str是一个autorelease对象,设置一个weak的引用来观察它
reference = str
}
fun viewWillApear(animated: Bool) {
super.viewWillAppear(animated: animated)
print(reference) // console: summyxx
}
fun viewDidAppear(animated: Bool) {
super.viewDidAppear(animated: animated)
print(reference) // console: nil
}
// viewDidLoad和viewWillAppear是在同一个runloop调用的,
// 而viewDidAppear是在之后的某个runloop调用的。
// 由于这个vc在loadView之后便add到了window层级上,所以viewDidLoad和
// viewWillAppear是在同一个runloop调用的,因此在viewWillAppear中,这个autorelease的变量依然有值。
//当然,我们也可以手动干预Autorelease对象的释放时机:
fun viewDidLoad() {
super.viewDidLoad()
@autoreleasepool {
var str = NSString.stringWithFormat("sunnyxxx")
}
print(str) // Console: nil
}
Autorelease原理
AutoreleasePoolPage
ARC下,我们使用@autoreleasepool{}来使用一个AutoreleasePool,随后编译器将其改写成下面的样子:
fun autoreleasepool(code: ()->()) {
let context = objc_autoreleasePoolPush();
code()
objc_autoreleasePoolPop(context);
}
而这两个函数都是对AutoreleasePoolPage的简单封装,所以自动释放机制的核心就在于这个类。AutoreleasePoolPage是一个C++实现的类
// size = 4096 = head + space store object
struct AutoreleasePoolPage {
magic_t const magic;
id* next; // point to next possible pos of autorelease object
pthread_t const thread; // current thread
AutoreleasePoolPage* const parent; // parent page
AutoreleasePoolPage* child; // next page
uint32_t const depth;
uint32_t hiwat;
}
- AutoreleasePool并没有单独的结构,而是由若干个AutoreleasePoolPage以双向链表的形式组合而成(分别对应结构中的parent指针和child指针)
- AutoreleasePool是按线程一一对应的(结构中的thread指针指向当前线程)
- AutoreleasePoolPage每个对象会开辟4096字节内存(也就是虚拟内存一页的大小),除了上面的实例变量所占空间,剩下的空间全部用来储存autorelease对象的地址
- 上面的id *next指针作为游标指向栈顶最新add进来的autorelease对象的下一个位置
- 一个AutoreleasePoolPage的空间被占满时,会新建一个AutoreleasePoolPage对象,连接链表,后来的autorelease对象在新的page加入
所以,若当前线程中只有一个AutoreleasePoolPage对象,并记录了很多autorelease对象地址时内存如图
图中的情况,这一页再加入一个autorelease对象就要满了(也就是next指针马上指向栈顶),这时就要执行上面说的操作,建立下一页page对象,与这一页链表连接完成后,新page的next指针被初始化在栈底(begin的位置),然后继续向栈顶添加新对象。
所以,向一个对象发送- autorelease消息,就是将这个对象加入到当前AutoreleasePoolPage的栈顶next指针指向的位置
释放时刻
每当进行一次objc_autoreleasePoolPush调用时,runtime向当前的AutoreleasePoolPage中add进一个哨兵对象,值为0(也就是个nil),那么这一个page就变成了下面的样子:
objc_autoreleasePoolPush的返回值正是这个哨兵对象的地址,被objc_autoreleasePoolPop(哨兵对象)作为入参,于是:
- 根据传入的哨兵对象地址找到哨兵对象所处的page
- 在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次- release消息,并向回移动next指针到正确位置
- 补充2:从最新加入的对象一直向前清理,可以向前跨越若干个page,直到哨兵所在的page
刚才的objc_autoreleasePoolPop执行后,最终变成了下面的样子:
嵌套的AutoreleasePool
知道了上面的原理,嵌套的AutoreleasePool就非常简单了,pop的时候总会释放到上次push的位置为止,多层的pool就是多个哨兵对象而已,就像剥洋葱一样,每次一层,互不影响。
其他Autorelease相关知识点
使用容器的block版本的枚举器时,内部会自动添加一个AutoreleasePool:
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
// 这里被一个局部@autoreleasepool包围着
}];
//func enumerateObjects(_ block: (Any, Int, UnsafeMutablePointer<ObjCBool>) -> Void)
array.enumerateObjects(_ block: {
(obj, idx, stop) -> Void in
// here is a hidden @autoreleasepool {}
})
当然,在普通for循环和for in循环中没有,所以,还是新版的block版本枚举器更加方便。for循环中遍历产生大量autorelease变量时,就需要手加局部AutoreleasePool咯。