App 内存泄漏二三事

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

内容简介: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 中存在内存泄漏(以及循环持有)。


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

查看所有标签

猜你喜欢:

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

图解设计模式

图解设计模式

结城浩 / 杨文轩 / 人民邮电出版社 / 2017-1-1 / CNY 79.00

原版连续畅销12年、重印25次! 194张图表 + Java示例代码 = 轻松理解GoF的23种设计模式 《程序员的数学》《数学女孩》作者结城浩又一力作 ◆图文并茂 194张图表(包括57张UML类图)穿插文中,帮助理解各设计模式 ◆通俗易懂 用浅显的语言逐一讲解23种设计模式,读完此书会发现GoF书不再晦涩难懂 ◆专业实用 编写了Java程序代码来......一起来看看 《图解设计模式》 这本书的介绍吧!

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

在线 XML 格式化压缩工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具