-
强引用分析
-
示例代码
//B页面中添加timer和对应的执行方法 A页面就仅仅添加push到B页面的代码 @property (nonatomic, strong) NSTimer *timer; self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES]; - (void)fireHome{ num++; NSLog(@"hello word - %d",num); } 复制代码
-
强引用出现的情况及原因分析
先在
B
页面创建一个timer
,然后从A
页面push
到B
此时timer
开始执行然后再pop
回到A
页面,部分人可能会觉得此时timer
会暂停执行,因为timer
是B
页面持有,pop
回来之后B
页面也就销毁了所以相应的timer
也因该被销毁,所以对应的应该是timer
停止执行。但是结果其实不然。可以看一下运行结果
可以发现
pop
回来之后timer
一样还在执行。
首先简单粗略的分析一下原因:猜测是循环引用造成了B
不能释放,看一下下面的官方文档
官方文档中明确说明了
timer
会对self
进行强持有,而此时self
有持有timer
所以造成了循环引用,也就造成了B
页面不能释放,所以即使pop
计时器还在执行。
在文章 Block的底层分析中我们知道了循环引用的解决办法,__weak
去修饰self
,此时self
的引用计数不会加一,所以不会造成循环引用问题,在这里不妨试一下用__weak
去修饰然后再看执行结果
发现这个地方
__weak
修饰并不能解决循环引用的问题。同样的在文章Block的底层分析我们知道,用__weak
修饰的话底层block
会走到_Block_object_assign
方法,发现block
底层其实仅仅存储了对象的指针地址也就是weakSelf
的地址。这里我们先分别打印一下self
的引用计数和__weak
修饰之后的引用计数,然后在分别打印一下self
和weakSelf
和这两者的地址
首先可以确定的是__weak
修饰的变量指向对象并不会造成引用计数加一的情况,其次通过地址打印、值打印我们可以确定的是self
和weakSelf
是两个变量指向了同一片的内存空间如下图所示
所以block
能通过存储的weakSelf
的地址找到对象的地址从而获取对象的属性修改对象相关的属性等。并且也能够解决循环引用的问题。
但是timer
就不一样了,上图的官方文档我们可以知道,timer
强持有的是对象,并不是对象的指针地址了,所以timer
的引用脸就是
timer -> weakSelf -> 对象
最终还是会找到对应的对象进行持有,然后呢timer
又被runloop
持有,引用链如下:
runloop -> timer -> weakSelf -> 对象
runloop
的生命周期又很长(大于对象和timer
的生命周期)runloop
没有停那么timer
就不会被释放,进而导致weakSelf
以及对象都不会释放. 也就导致了不同于block
的解决循环引用的方法也就是__weak
不能解决强持有的问题。
结论:强持有导致就算用__weak修饰也会被持有对象,引用计数一样会加一,所以只有释放变量才能够释放对象 -
强引用解决办法
-
退出前销毁
timer
前文分析问题的原因我们知道就是应为
timer
持有的是当前对象所以对象不能被释放,所以解决办法其实也很简单就是pop
出去的时候只需要释放timer
就行。上文的官方文档也有提到
只要释放
timer
对象也就会被释放。所以只需要在didMoveToParentViewController
方法中调用[self.timer invalidate];
和self.timer = nil;
就行了效果如下
这样强持有后不能释放的问题也就解决了
-
timer
回调方法判断同样的解决问题最根本的方法还是释放
timer
但是除了didMoveToParentViewController
方法中释放还可以考虑专门创建一个添加timer
的类,在该类中新建一个方法,然后和传入的方法做交换,该方法中需要判断传入的target
是否为空了,如果不为空则使用传入的target
调用传入的方法。如果为空则释放timer
。释放timer
对应target
引用计数就会减一。如果减到0就会被正常释放。同样的也可以解决问题具体代码如下#import "LGTimerWapper.h" #import <objc/message.h> @interface LGTimerWapper() @property (nonatomic, weak) id target; @property (nonatomic, assign) SEL aSelector; @property (nonatomic, strong) NSTimer *timer; @end @implementation LGTimerWapper - (instancetype)lg_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo{ if (self == [super init]) { self.target = aTarget; // vc self.aSelector = aSelector; // 方法 -- vc 释放 if ([self.target respondsToSelector:self.aSelector]) { Method method = class_getInstanceMethod([self.target class], aSelector); const char *type = method_getTypeEncoding(method); class_addMethod([self class], aSelector, (IMP)fireHomeWapper, type); self.timer = [NSTimer scheduledTimerWithTimeInterval:ti target:self selector:aSelector userInfo:userInfo repeats:yesOrNo]; } } return self; } void fireHomeWapper(LGTimerWapper *warpper){ if (warpper.target) { // vc - dealloc void (*lg_msgSend)(void *,SEL, id) = (void *)objc_msgSend; lg_msgSend((__bridge void *)(warpper.target), warpper.aSelector,warpper.timer); }else{ // warpper.target [warpper.timer invalidate]; warpper.timer = nil; } } - (void)lg_invalidate{ [self.timer invalidate]; self.timer = nil; } - (void)dealloc{ NSLog(@"%s",__func__); } @end 复制代码
-
proxy
虚基类的方式在讲解 Block底层分析中的解决循环引用的方法的时候也提到过
proxy
这里其实也类似,这里使用proxy
的思想主要是想使用一个中间者,这样timer
不会再持有对象而是proxy
,所以对象的引用计数不会再加一,从而对象释放的时候对应的timer
和proxy
也就释放了也就解决了强持有的问题。具体代码如下;#import "LGProxy.h" @interface LGProxy() @property (nonatomic, weak) id object; @end @implementation LGProxy + (instancetype)proxyWithTransformObject:(id)object{ LGProxy *proxy = [LGProxy alloc]; proxy.object = object; return proxy; } // 仅仅添加了weak类型的属性还不够,为了保证中间件能够响应外部self的事件,需要通过消息转发机制,让实际的响应target还是外部self,这一步至关重要,主要涉及到runtime的消息机制。 // 转移 // 强引用 -> 消息转发 -(id)forwardingTargetForSelector:(SEL)aSelector { return self.object; } 复制代码
-
-
-
AutoReleasePool
-
自动释放池介绍
从这个官方文档中我们可以知道,在Runloop
开始的时候会自动创建一个自动释放池,当Runloop
这次循环结束的时候,那么就会销毁自动释放池,从而释放所有autorelease
对象,当然如果在一个事务中需要创建多个临时变量此时就可以自己手动创建一个自动释放池来管理这些对象可以很大程度地减少内存峰值。(例如一个代码块中需要创建循环创建10000个image
对象然后渲染出来,此时完全可以使用自动释放池,正常情况下不使用自动释放池的话会等到这个代码块执行完成之后才能释放这10000个对象,而是用自动释放池之后每次循环完成自动释放池的代码也执行完成那么该对象也就会被释放。这样就减少了内存峰值)
结合文档和上图的理解总结:- 每次用户出发一个时间都会启动一次
runloop
,创建完事件之后会创建一个自动释放池 - 此次循环中会将所有延迟释放的对象也就是
autorelease
对象放到自动释放池中去 - 在一次完整的
runloop
结束之前,会向自动释放池中所有对象发送release
消息,然后销毁自动释放池
- 每次用户出发一个时间都会启动一次
-
新老xcode创建的项目中
main
函数中使用自动释放池的区别xcode11
之前创建的项目是这样的
xcode11
之后创建的工程是这样的
可以发现xcode11
之前整个程序都是放在自动释放池中的,当runloop
启动会再创建一个自动释放池嵌套在main
函数的这个释放池中,这样使用的结果是main
函数自动释放池中创建的对象只有程序结束之后才能被释放,再看xcode11
之后创建的main
函数发现程序在自动释放池的外面,所以在自动释放池中创建的对象只要程序启动就能被释放,这样节省了程序的内存 -
Clang
分析可以将
main
文件clang
一下看编译后的源码
发现底层其实就是一个__AtAutoreleasePool
对象。然后再全局搜索__AtAutoreleasePool
并且自动释放池中的代码是使用{}
包裹的
不出意外的是个结构体,里面有构造函数
objc_autoreleasePoolPush
返回了atautoreleasepoolobj
对象,还有一个析构函数objc_autoreleasePoolPop
需要传入atautoreleasepoolobj
对象,上文也说了自动释放池的代码是在一个作用域中的,所以开始的时候就会调用构造方法,作用域结束的时候就会调用析构方法也可以通过断点调试查看汇编代码验证此结论
-
源码分析
上文通过
clang
查看编译后的代码得知自动吃其实也就是个对象,就是个结构体,其中有构造方法和析构方法,接下来就可以通过源码查询构造和析构方法看源码是如何实现的同时也可以深入探索自动释放池这个对象-
AutoreleasePoolPage
源码中全局搜索构造方法发现
构造和析构方法其实都是调用的是
AutoreleasePoolPage
中的方法点击AutoreleasePoolPage
查看源码
发现自动释放池就是通过
AutoreleasePoolPage
来实现的注释中也说道了自动释放池的实现方法大概意思如下:- 线程的自动释放池是指针的堆栈
- 每个指针都是要释放的对象,或者是
POOL_BOUNDARY
,它是自动释放池的边界。 - 池令牌是指向该池的POOL_BOUNDARY的指针。弹出池后,将释放比哨点更热的每个对象
- 堆栈分为两个双向链接的页面列表。根据需要添加和删除页面。
- 线程本地存储指向热页面,该页面存储新自动释放的对象。
首先看该类的定义:
从这个结构中也可以看出是个双向链表应为有父节点和子节点。
整个程序的运行中可能会有多个AutoreleasePoolPage
对象,从定义中可以看出AutoreleasePoolPage
是以栈为结点通过双向链表的形式组合而成,每个页的大小是4096
,再看AutoreleasePoolPageData
结构
发现一共
56
字节所以一般情况下共有4096-56=4040
字节存储autorelease
对象也就是一共可以存4040/8=505
个对象,但是从定义中知道还有一个POOL_BOUNDARY
(注意哨兵对象只有在第一页中存在)所以第一页可以存储504
个对象剩下的可以存储505
个对象,这里可已通过打印自动释放池的情况验证(_objc_autoreleasePoolPrint
方法打印自动释放池的情况)
此时是创建了504个对象
多加一个对象则又创建了一页,并且把新创建的页设置成hot
,然后第二页的第一个对象不再是哨兵对象直接就是autorelease
对象
具体内存分布图如下:
-
objc_autoreleasePoolPush
源码分析
先看创建页面的源码
这里知道AutoreleasePoolPage
是通过构造方法创建的
再看autoreleaseFullPage
方法
这个方法就比较简单了就是一个链表的查询工作,查到了则设置成聚焦页面并添加对象,没查到则新创建一个页面并插入到链表中,新页面设置成聚焦页面然后添加对象。
最后再看add
方法
,这里就是将对象存到
next
指针,然后next++
。
具体流程图如下:
-
autorelease
源码分析
跟到最后发现autorelease
底层实现就是调用autoreleaseFast
方法 -
objc_autoreleasePoolPop
源码分析
再看releaseUntil
方法
kill
方法
具体流程图如下:
-
-
总结
AutoreleasePool
底层就是一个AutoreleasePoolPage
对象AutoreleasePoolPage
对象又是一个栈结构并且是个双向两边(应为每一个AutoreleasePoolPage
都是有大小限制的超出了再添加对象则需要创建新的页,所以是个双向链接结构)- 既然
AutoreleasePool
是个栈结构并且是双向链表结构,所以push
可添加对象就是压栈,栈压满了则创建新页面对象压栈到新页面中去,然后将新页面插入到链表结构中。pop
就是出栈然后释放对象,释放page AutoreleasePool
会在每次runloop
启动的时候自动创建一个自动释放池,然后在此次循环结束的时候释放自动释放池,所以如果对象添加__autoreleasing
属性修饰则将对象添加到了系统创建的自动释放池中,那么该对象的释放也就是系统干预释放了,也就是要等到此次runloop
结束之后释放对象,AutoreleasePool
还一种情况是手动创建自动释放池也是就是通过@autoreleasepool
创建自动释放池,在该作用域中创建的autorelease
对象会放到手动创建的自动释放池中此时该对象就会在手动创建的自动释放池作用域结束之后就会被释放,这样做可以降低内存峰值
-
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END
喜欢就支持一下吧