App 内存泄漏二三事

栏目: IOS · 发布时间: 7年前

内容简介: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 之后,你可以在视图调试器中看到这样的效果:

App 内存泄漏二三事

无论你用不用 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
  1. 这个属性就是用来弱引用 handler 的属性,它保存了一个对 handler 的弱引用。类型是 id,因为 addScriptMessageHandler 方法需要一个 WKScriptMessageHandler 对象作为参数。
  2. 这个对象对 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 中存在内存泄漏(以及循环持有)。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Introduction to Linear Optimization

Introduction to Linear Optimization

Dimitris Bertsimas、John N. Tsitsiklis / Athena Scientific / 1997-02-01 / USD 89.00

"The true merit of this book, however, lies in its pedagogical qualities which are so impressive..." "Throughout the book, the authors make serious efforts to give geometric and intuitive explanations......一起来看看 《Introduction to Linear Optimization》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具