内容简介:App 内存泄漏二三事
AFHTTP 内存泄漏问题
这是 AFHTTP 框架的通病。这个问题很常见,也最好解决,网上也有不少的解决方案。主流的解决方案就是使用单例。定义一个单例对象 SessionManager:
@interface SessionManager : NSObject @property(nonatomic,strong)AFHTTPSessionManager *manager; +(SessionManager *)share; @end @implementation SessionManager +(SessionManager *)share{ static SessionManager *shareObj = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ shareObj = [[SessionManager alloc] init]; AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; /** 设置超时*/ [manager.requestSerializer willChangeValueForKey:@"timeoutInterval"]; manager.requestSerializer.timeoutInterval = 10; [manager.requestSerializer didChangeValueForKey:@"timeoutInterval"]; shareObj.manager = manager; }); return shareObj; } @end
然后这样使用它:
AFHTTPSessionManager* manager = [SessionManager share].manager; manager.requestSerializer = [AFHTTPRequestSerializer new]; ……
环信 UI 框架中的内存泄漏问题
环信框架中,有一个对 UIViewController 的扩展(Category) :UIViewController+HUD,它对 MBHUD 进行了二次封装,通过它可以使你的 MBHUD 的调用变得更简单,比如显示一个 HUD 你可以这样:
[self showHudInView:self.view hint:@""];
但是这个方法中有一个严重的内存泄漏问题。当你在一个 View Controller 中多次显示 HUD 之后(比如反复下拉刷新表格),用视图调试器查看 UIView,你会发现视图树中显示了多个 HUD 对象。也就是说每次 showHudInView 之后都会重新生成一个新的 HUD,而原来的 HUD 虽然被隐藏了,但它们在内存中仍然是持续存在的。每次 showHudInView 调用大概会导致 400-500 k 的内存泄漏。如果你反复刷新表格(比如 5 分钟或更长)直到内存撑爆,app 崩溃。
解决的方法很简单,在 showHudInView 方法中加入一句:
HUD.removeFromSuperViewOnHide = YES;
这样,被隐藏的 HUD 会自动从 subviews 中移除,不再占用内存。
O-C 块中对 self 强引用导致的内存泄漏问题
在 View Controller 类的 O-C 块中,如果你直接引用了 self,则会导致 View Controller 被强引用(因为块的参数都是以 copy 引用的,会导致 retained count 加 1)。这样,当 View Controller 被 pop 出导航控制器栈后不会被释放,导致内存泄漏。这个泄漏就比较严重了,少则几百 K,多则几兆。
一个比较明显的例子就是 MJRefresh。在 View Controller 中,如果我们想支持下拉刷新,通常会这样使用 MJRefresh:
self.tableView.mj_header=[MJRefreshNormalHeader headerWithRefreshingBlock:^{ [self.tableView.mj_header endRefreshing]; currentPage = 1; [self loadNoticeList:1 success:^(NSArray<CampusNoticeModel *> *data) { [self.models removeAllObjects]; [self.models addObjectsFromArray:data]; [self.tableView reloadData]; } failure:^(NSString *msg) { }]; }];
注意,O-C 块中对 View Controller 进行了强引用,比如:self.tableView 和 self.models。
原则上,当我们在 O-C 块中引用 self 时,应当使用弱引用,比如上面的代码应当改为:
__weak __typeof(self) weakSelf=self; self.tableView.mj_header=[MJRefreshNormalHeader headerWithRefreshingBlock:^{ [weakSelf.tableView.mj_header endRefreshing]; currentPage = 1; [weakSelf loadNoticeList:1 success:^(NSArray<CampusNoticeModel *> *data) { [weakSelf.models removeAllObjects]; [weakSelf.models addObjectsFromArray:data]; [weakSelf.tableView reloadData]; } failure:^(NSString *msg) { }]; }];
也就是将 O-C 块中所有的 self 改成 weakSelf。这里有一个例外,如果引用的是实例变量而不是属性,原则上是不需要 weakSelf 的。比如 currentPage 在 View Controller 中是以实例变量形式定义的(也就是说没有用 @property 进行声明),那么我们不需要通过 weakSelf 来进行引用。
但是,如果你在项目中使用 MLeaksFinder 来检测内存泄漏时,MLeaksFinder 仍然会认为 O-C 块中对 currentPage 的引用存在问题。因此,为了让 MLeaksFinder 彻底“闭上嘴”,我们最好也将 currentPage 修改为属性(使用 @property 声明),然后将 O-C 块中的引用方式修改为:weakSelf.currentPage。
CADisplayLink 导致的内存泄漏
在使用 CADisplayLink 时,如果不释放 CADisplayLink,很容易出现内存泄漏。以自定义 UIView 为例,我们会使用定时器进行某些自定义的绘图和动画操作。这时我们会用到 CADisplayLink :
displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(animateDashboard:)];
当我们需要开启定时器时,可以将它添加到 runloop:
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
但是 displayLink 会持有 UIView 对象,导致 UIView 永远不会被释放。因此我们需要在一个适当的时机释放 displayLink,比如在 CADisplayLink 的 action 方法中根据一定的条件来 invalidate 它:
-(void)animateDashboard:(CADisplayLink *)sender{ if( endValue <= self.value){// 到达终点值,停止动画 ...... [displayLink invalidate]; ...... }else{ ...... } }
另外,CADisplayLink 最好不要复用。也就是说,每次启动 CADisplayLink 时都重新初始化并将它添加到 runloop,而每次停止动画时都 invalidate:
-(void)animating{ if(_stopped == YES){ displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(blink:)]; [displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; _stopped = NO; [self setNeedsDisplay]; } } -(void)stopAnimating{ if(_stopped == NO){ [displayLink invalidate]; _stopped = YES; } }
reloadRowsAtIndexPaths 导致的内存泄漏
UITableView 的 reloadRowsAtIndexPaths 的行为非常奇怪,在刷新 cell 时,它并不会重用原有的 cell,而是重新创建新的 cell 覆盖在原来的 cell 上,这会导致额外的内存开销。当重复多次调用 reloadRowsAtIndexPaths 之后,你可以在视图调试器中看到这样的效果:
无论你用不用 beginUpdates/endUpdates,结果都是一样。
解决的办法目前只有一个,不要用 reloadRowsAtIndexPaths,而是使用 reloadData,当然会有一点性能上的代价,但也是没有办法的事情。
定时器导致的内存泄漏问题
有时候 NSTimer (尤其是 repeated 为 YES 时)会导致内存泄漏问题。因为定时器是在另外一个线程中运行的,当界面消失后,定时器仍然还在运行,如果在定时器任务中引用了 UI 元素,则这些视图都会被强引用,从而导致界面消失后 view 无法释放,导致内存泄漏。
因此,如果在你的 UIViewController 中使用了定时器,一定要记得在 viewWillDisappear 方法中 invalidate 它。
addScriptMessageHandler 导致的内存泄漏
WKUserContentController 的 addScriptMessageHandler 方法会导致一个对 handler 对象的强引用,从而导致 handler (通常是 webView 所在的 ViewController)不会被释放,于是内存泄漏。
解决的办法是 removeScriptMessageHandlerForName。根据官方文档,当你 addScriptMessageHandler 之后,需要在不再需要 handler 时,需要调用 removeScriptMessageHandlerForName 解除 handler 的强引用。
问题在于,“当你不在需要它的时候”到底是什么时候?我们一般会在 viewDidLoad 中 addScriptMessageHandler,按道理应该在 dealloc 中 removeScriptMessageHandlerForName。但由于内存都已经泄漏了,ViewContoller 的 dealloc 根本不会调用,这个方法是无效的。
解决的办法有两个,一个是将 addScriptMessageHandler 放到 viewDidAppear 中执行,那么我们就可以在 viewDidDisappler 中 removeScriptMessageHandlerForName 了。
另一个方法是将 handler 弱引用。这需要新建一个类,创建一个弱引用的属性,用这个属性来包装 handler 对象:
@interface WeakScriptMessageDelegate : NSObject<WKScriptMessageHandler> // 1 @property (nonatomic, weak) id<WKScriptMessageHandler> scriptDelegate; - (instancetype)initWithDelegate:(id<WKScriptMessageHandler>)scriptDelegate; @end @implementation WeakScriptMessageDelegate - (instancetype)initWithDelegate:(id<WKScriptMessageHandler>)scriptDelegate { self = [super init]; if (self) { _scriptDelegate = scriptDelegate; } return self; } // 2 - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message { [self.scriptDelegate userContentController:userContentController didReceiveScriptMessage:message]; } @end
- 这个属性就是用来弱引用 handler 的属性,它保存了一个对 handler 的弱引用。类型是 id,因为 addScriptMessageHandler 方法需要一个 WKScriptMessageHandler 对象作为参数。
- 这个对象对 WKScriptMessageHandler 进行了封装,它同样实现了 WKScriptMessageHandler 协议,这个协议中有一个唯一的方法需要实现,即 userContentController 方法。在方法内部,我们可以直接调用 handler 的同名方法实现(因为二者的行为是一致的)。
然后这样使用它:
// 将 handler 转换成一个若引用的 handler,从而避免内存泄漏 WeakScriptMessageDelegate* weakHandler = [[WeakScriptMessageDelegate alloc] initWithDelegate:handler]; [webView.configuration.userContentController addScriptMessageHandler:weakHandler name:methodName];
这种办法也可以用来解决许多强引用导致的内存泄漏。
MLeaksFinder
内存泄漏问题多种多样,它们经常以出乎人意料的形式存在,我们无法以一种固定的模式来判断 app 中存在的内存泄漏问题。我们常常需要使用多个 工具 和手段来检查 app 中的内存问题,比如可以用 Xcode 的 Analyze 工具对代码进行静态语法分析,用 Instrument 的 Leaks/Allocations 工具进行动态内存检查分析,用视图调试器查看 UI 问题等等。
但我们还可以用许多第三方内存泄漏检测框架,比如:MLeaksFinder 和 HeapInspector-for-iOS,尤其是前者(后者目前会导致 App “冻死”的问题,作者还在解决这个问题)。
MLeaksFinder 是一个专门用于检测 UI 类内存泄漏的工具,我们可以利用它来检测 UIViewController 和 UIView 中未 dealloc 的 subview。
它的使用非常简单,直接 pod MLeaksFinder,然后找到 MLeaksFinder.h 头文件,将其中的 MEMORY_LEAKS_FINDER_ENABLED 宏和 MEMORY_LEAKS_FINDER_RETAIN_CYCLE_ENABLED 宏打开(设置为 1)就可以了。
编译运行 app,测试各种操作,切换到不同的 view controller,当 MLeaksFinder 发现内存泄漏会弹出一个 alert(同时控制台会有输出),告诉你哪个类和 UIView 中存在内存泄漏(以及循环持有)。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
算法分析导论
(美)Robert Sedgewick、(法)Philippe Flajolet / 冯舜玺、李学武、裴伟东、等其他 / 机械工业出版社 / 2006-4 / 38.00元
本书阐述了用于算法数学分析的主要方法,所涉及的材料来自经典数学课题,包括离散数学、初等实分析、组合数学,以及来自经典的计算机科学课题,包括算法和数据结构,本书内容集中覆盖基础、重要和有趣的算法,前面侧重数学,后面集中讨论算法分析的应用,重点的算法分的的数学方法。每章包含大量习题以及参考文献,使读者可以更深入地理解书中的内容。 本书适合作为高等院校数学、计算机科学以及相关专业的本科生和研究生的......一起来看看 《算法分析导论》 这本书的介绍吧!
CSS 压缩/解压工具
在线压缩/解压 CSS 代码
HTML 编码/解码
HTML 编码/解码