【YFMemoryLeakDetector】人人都能理解的 iOS 内存泄露检测工具类

背景

即使到今天,iOS 应用的内存泄露检测,仍然是一个很重要的主题。我在一年前,项目中随手写过一个简单的工具类,当时的确解决了大问题。视图和控制器相关的内存泄露,几乎都不存在了。后来想着一直就那个工具,写一篇文章,不过一直没有写。

时过境迁,今天在网上搜了下 “iOS 内存泄露检测”,各种讨论技术文章,有点头大。我忍不住看了下自己当时的代码,突然感觉自己的思路好特别,好有创意。我真的就是在“创建”时把数据记录到一个字典里,在“释放”时,从字典里移出对象;所谓的检测,其实就是打印那个字典,仍然在字典中的很有可能就是泄露喽。

当然,还是有一些技术细节的。我把旧代码适度拆分整理为一个开源库了,取名为 YFMemoryLeakDetector。本篇,将着重讲述简洁之下,可能不易察觉的一些考量。

注意:这个库,相当程度上是为当时的项目量身定制的,你可能需要适当修改,才能在自己的项目中真正发挥出它的力量。

核心技术分析

AOP 机制,借助 Aspects 库实现

Aspects 这个库的基本用法,我专门说过,大家可以参考 Aspects– iOS的AOP面向切面编程的库。当然,用黑魔法直接操作运行时,也是很酷的。不过我当时的确是因为偷懒,才用的 Aspects。一直到现在,我依然觉得,它可能比黑魔法更可靠些。

在字典中直接存储指针地址,而不是直接存储对象自身

存储指针地址的好处是,就是不会因为存储本身影响对象的引用计数。当然,指针地址本身,在 OC 中,其实就是对象自身。而要想得到存地址,不存对象的效果,就要祭出整个工具库的灵魂函数:

NSValue * key = [NSValue valueWithPointer: (__bridge const void * _Nullable)(info.instance)];

将对象转换为 NSValue,直接以 NSValue 为键,来标记对象。这句代码,是整个机制的灵魂所在,也是比其他类似的内存泄露分析库更简洁的重要原因之一。我当时也是搜遍的整个网络,才知道自己要的究竟是什么。

另外,还有一点必须提一下, NSValue 是可以在反向转换为 oc 对象的,这有利于你在拿到工具库提供的泄露信息后,进一步定位和分析问题:

UIViewController * vc = (UIViewController *)[key pointerValue];

对控制器和视图,采用不同的拦截策略

  • 对象销毁,统一拦截的是 dealloc。现在网上的很多策略,基本也是这样。
  • 对象创建,对于视图,拦截的是 willMoveToSuperview: ;对于控制器拦截的是 viewDidLoad 。直到现在,我依然以为,没有调用过这两个方法的视图或控制器对象,本身没有多大的拦截价值。当然,这依然因项目而异。作为一个工具类,只要它能解决大多数场景下的问题,我觉得就可以了。

load 时,自动开启监测

所以,你只要把工具库源码拖拽到项目中,不需要任何修改,就可以自动监测内存泄露情况了。然后在需要的地方,在合适的时候,去读取 YFMemoryLeakDetector 的单例属性,分析结果即可。当然,这是我今天重构优化过的版本。原来是需要手动初始化的,好 Low,当时写的!

+ (void)load
{
    [[YFMemoryLeakDetector sharedInstance] setup];
}

“见码如晤”

YFMemoryLeakDetector.h 头文件部分,主要简化为暴露了存储可能有内存泄露情况的视图和控制器的字典属性;同时提供了一个单例方法,以便于具体分析和操作内存分析情况。

#import <Foundation/Foundation.h>

/**
 *  分析页面和页面内视图是否有内存泄露的情况.
 */
@interface  YFMemoryLeakDetector: NSObject

#pragma mark - 属性.

/*
  已加载,但尚未正确释放,有内存风险的控制器对象.
 
 以指针地址为key,以对象字符串为值.所以不用担心因为记录本身而引起的内存泄露问题.
 
 必要时,可以使用类似 (UIViewController *)[key pointerValue] 的语法来获取原始的 OC对象来进一步做些过滤操作.
 */
@property (strong, atomic) NSMutableDictionary * loadedViewControllers;

/*
 已加载,但尚未正确释放,有内存风险的视图对象.
 
 以指针地址为key,以对象字符串为值.所以不用担心因为记录本身而引起的内存泄露问题.
 
 必要时,可以使用类似 (UIView *)[key pointerValue] 的语法来获取原始的 OC对象来进一步做些过滤操作.
 */
@property (strong, atomic) NSMutableDictionary * loadedViews; //!< 已加载的视图.



#pragma mark - 单例方法.
+(YFMemoryLeakDetector *) sharedInstance;
@end

YFMemoryLeakDetector.m 实现,借助于 AspectsvalueWithPointer: 代码大大简化。

#import <objc/runtime.h>
#import <UIKit/UIKit.h>

#import "YFMemoryLeakDetector.h"
#import "Aspects.h"

@interface  YFMemoryLeakDetector()
@end

@implementation  YFMemoryLeakDetector

static YFMemoryLeakDetector * sharedLocalSession = nil;

+ (void)load
{
    [[YFMemoryLeakDetector sharedInstance] setup];
}

+(YFMemoryLeakDetector *) sharedInstance{
    @synchronized(self){
        if (sharedLocalSession == nil) {
            sharedLocalSession = [[self alloc] init];
        }
    }
    return  sharedLocalSession;
}


- (void)setup
{
    self.loadedViewControllers = [NSMutableDictionary dictionaryWithCapacity: 42];
    self.loadedViews = [NSMutableDictionary dictionaryWithCapacity:42];
    
    /* 控制器循环引用的检测. */
    [UIViewController aspect_hookSelector:@selector(viewDidLoad) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> info) {
        NSValue * key = [NSValue valueWithPointer: (__bridge const void * _Nullable)(info.instance)];

        [self.loadedViewControllers setObject:[NSString stringWithFormat:@"%@", info.instance] forKey:key];
    }error:NULL];
    
    [UIViewController aspect_hookSelector:NSSelectorFromString(@"dealloc") withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> info) {
        NSValue * key = [NSValue valueWithPointer: (__bridge const void * _Nullable)(info.instance)];

        [self.loadedViewControllers removeObjectForKey: key];
    }error:NULL];
    
    /* 视图循环引用的检测. */
    /* 只捕捉已经从父视图移除,却未释放的视图.以指针区分. */
    [UIView aspect_hookSelector:@selector(willMoveToSuperview:) withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> info, UIView * superview){
        /* 过滤以 _ 开头的私有类. */
        NSString * viewClassname = NSStringFromClass(object_getClass(info.instance));
        if ([viewClassname hasPrefix:@"_"]) {
            return;
        }
        
        /* 兼容处理使用了KVO机制监测 delloc 方法的库,如 RAC. */
        if ([viewClassname hasPrefix:@"NSKVONotifying_"]) {
            return;
        }
        
        NSValue * key = [NSValue valueWithPointer: (__bridge const void * _Nullable)(info.instance)];
        
        /* 从父视图移除时,就直接判定为已释放.
         这样做的合理性在于:当视图从父视图移除后,一般是很难再出发循环引用的条件了,所以可适度忽略.
         */
        if (!superview) {
            [self.loadedViews removeObjectForKey: key];
        }
        
        NSMutableDictionary * obj = [self.loadedViews objectForKey: key];
        
        if (obj) { /* 一个 UIView 视图,只记录一次即可.因为一个UIView,最多只被 delloc 一次. */
            return;
        }
        
        [self.loadedViews setObject: [NSString stringWithFormat:@"%@", info.instance] forKey:key];
        
        /* 仅对有效实例进行捕捉.直接捕捉类对象,会引起未知崩溃,尤其涉及到和其他有KVO机制的类库配合使用时. */
        [info.instance aspect_hookSelector:NSSelectorFromString(@"dealloc") withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> info){
            [self.loadedViews removeObjectForKey: key];
        }error:NULL];
    }error:NULL];
}
@end

使用示例:

这里展示一个基于工具类,二次分析的示例:

YFMemoryLeakDetector * memoryLeakDetector = [YFMemoryLeakDetector sharedInstance];
        
/* 控制器检测结果的输出. */
[memoryLeakDetector.loadedViewControllers enumerateKeysAndObjectsUsingBlock:^(NSValue *  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
    UIViewController * vc = (UIViewController *)[key pointerValue];
    if (!vc.parentViewController) { /* 进一步过滤掉有父控制器的控制器. */
        NSLog(@"有内存泄露风险的控制器: %@", obj);
    }
}];
    
/* 视图检测结果的输出. */
[memoryLeakDetector.loadedViews enumerateKeysAndObjectsUsingBlock:^(NSValue *  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
    UIView * view = (UIView *)[key pointerValue];
    if (!view.superview) { /* 进一步过滤掉有父视图的视图,即只输出一组视图的根节点,这样便于更进一步定位问题. */
        NSLog(@"有内存泄露风险的视图: %@", obj);
    }
}];

参考文章

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Golang语言社区

利用Go语言实现简单Ping过程的方法

、准备工作 安装最新的Go 1、由于Google被墙的原因,如果没有VPN的话,就到这里下载:http://www.golangtc.com/download ...

4105
来自专栏walterlv - 吕毅的博客

Roslyn 入门:使用 .NET Core 版本的 Roslyn 编译并执行跨平台的静态的源码

发布于 2018-05-25 13:24 更新于 2018-06...

531
来自专栏大内老A

.NET Core跨平台的奥秘[下篇]:全新的布局

从本质上讲,按照CLI规范设计的.NET从其出生的那一刻就具有跨平台的基因,这与Java别无二致。由于采用了统一的中间语言,微软只需要针对不同的平台设计不同的虚...

2206
来自专栏草根专栏

使用静态基类方案让 ASP.NET Core 实现遵循 HATEOAS Restful Web API

Hypermedia As The Engine Of Application State (HATEOAS) HATEOAS(Hypermedia as t...

3995
来自专栏逸鹏说道

AutoMapper 使用实践

一. 使用意图 常常在开发过程中,碰到一个实体上的属性值,要赋值给另外一个相类似实体属性时,且属性有很多的情况。一般不利用工具的话,就要实例化...

35213
来自专栏王清培的专栏

.NET/ASP.NET Routing路由(深入解析路由系统架构原理)

阅读目录: 1.开篇介绍 2.ASP.NET Routing 路由对象模型的位置 3.ASP.NET Routing 路由对象模型的入口 4.ASP.NET R...

2289
来自专栏跟着阿笨一起玩NET

ReSharper 配置及用法

1:安装后,Resharper会用他自己的英文智能提示,替换掉 vs2010的智能提示,所以我们要换回到vs2010的智能提示

691
来自专栏walterlv - 吕毅的博客

C#/.NET 中的契约

发布于 2017-12-20 15:04 更新于 2018-04...

601
来自专栏风口上的猪的文章

.NET面试题系列[1] - .NET框架基础知识(1)

面试出现频率:从来没人问过。事实上我都不知道怎么问,考背书吗?倒是可以问问知不知道现在.NET最新版本是什么,考察面试者是否对新技术足够敏感。

1401
来自专栏逸鹏说道

Dapper where Id in的解决方案

简单记一下,一会出去有点事情~ 我们一般写sql都是==》update NoteInfo set NDataStatus=@NDataStatus where ...

28110

扫码关注云+社区