-
强引用分析
-
示例代码
//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修饰也会被持有对象,引用计数一样会加一,所以只有释放变量才能够释放对象 -
强引用解决办法
-
退出前销毁
前文分析问题的原因我们知道就是应为timertimer持有的是当前对象所以对象不能被释放,所以解决办法其实也很简单就是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 -
在讲解 Block底层分析中的解决循环引用的方法的时候也提到过proxy虚基类的方式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对象会放到手动创建的自动释放池中此时该对象就会在手动创建的自动释放池作用域结束之后就会被释放,这样做可以降低内存峰值
-