背景

即使到今天,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);
}
}];

参考文章

【YFMemoryLeakDetector】人人都能理解的 iOS 内存泄露检测工具类的更多相关文章

  1. 精准 iOS 内存泄露检测工具

    MLeaksFinder:精准 iOS 内存泄露检测工具 发表于 2016-02-22   |   zepo   |   23 Comments 背景 平常我们都会用 Instrument 的 Lea ...

  2. vld,Bounds Checker,memwatch,mtrace,valgrind,debug_new几种内存泄露检测工具的比较,Valgrind Cheatsheet

    概述 内存泄漏(memory leak)指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况,在大型的.复杂的应用程序中,内存泄漏是常见的问题.当以前分配的一片内存不再需要使用或无法访问时,但是却 ...

  3. vld(Visual Leak Detector) 内存泄露检测工具

    初识Visual Leak Detector 灵活自由是C/C++语言的一大特色,而这也为C/C++程序员出了一个难题.当程序越来越复 杂时,内存的管理也会变得越加复杂,稍有不慎就会出现内存问题.内存 ...

  4. Android内存泄露---检测工具篇

    内存使用是程序开发无法回避的一个问题.如果我们毫不在意肆意使用,总有一天会为此还账,且痛不欲生...所以应当防患于未然,把内存使用细化到平时的每一行代码中. 内存使用概念较大,本篇先讲对已有app如何 ...

  5. Linux C 编程内存泄露检测工具(一):mtrace

    前言 所有使用动态内存分配(dynamic memory allocation)的程序都有机会遇上内存泄露(memory leakage)问题,在Linux里有三种常用工具来检测内存泄露的情況,包括: ...

  6. 内存泄露检测工具Valgrind

    内存泄露简介 什么是内存泄漏 内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因,程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果. 内存泄 ...

  7. memwatch内存泄露检测工具

    工具介绍 官网 http://www.linkdata.se/sourcecode/memwatch/ 其功能如下官网介绍,挑选重点整理: 1. 号称功能: 内存泄露检测 (检测未释放内存, 即 动态 ...

  8. kmemleak的使用---内存泄露检测工具【转】

    转自:http://blog.csdn.net/lishenglong666/article/details/8287783 版权声明:本文为博主原创文章,未经博主允许不得转载.   目录(?)[-] ...

  9. linux下内存泄露检测工具Valgrind介绍

    目前在linux开发一个分析实时路况的应用程序,在联合测试中发现程序存在内存泄露的情况. 这下着急了,马上就要上线了,还好发现了一款Valgrind工具,完美的解决了内存泄露的问题. 推荐大家可以使用 ...

随机推荐

  1. jQuery form插件使用详解

    点击打开: jquery选择器全解 jquery中的style样式操作 jquery中的DOM操作 jquery中的事件操作全解 jquery中的动画操作全解 jquery中ajax的应用 自定义jq ...

  2. (转载)RESTful架构风格下的4大常见安全问题

    转载自<RESTful架构风格下的4大常见安全问题>,作者:马伟 伴随着RESTful架构风格的大量应用微服务架构的流行,一些本来难以察觉到的安全问题也逐渐开始显现出来.在我经历过的各种采 ...

  3. Linux Redis集群搭建与集群客户端实现(Python)

    硬件环境 本文适用的硬件环境如下 Linux版本:CentOS release 6.7 (Final) Redis版本: Redis已经成功安装,安装路径为/home/idata/yangfan/lo ...

  4. 搭建Hadoop平台(新手入门)

    刚刚大学毕业,接触大数据有一年的时间了,把自己的一些学习笔记分享给大家,希望同热爱大数据的伙伴们一起学习,成长! 资料准备: Hadoop-2.7.1下载:http://pan.baidu.com/s ...

  5. win7下安装Ubuntu后进不去win7的解决方法

    win7下安装Ubuntu后进不去win7的解决方法 刚刚给同学在win7下安装了Ubuntu16.04,结果在安装完后竟然无法在电脑重启后,找到win7的进入选项. 在网上找了找,都不行!就差点重装 ...

  6. mysql执行sql脚本

    最近用mysql执行sql脚本,遇到一些问题,顺便记录一下笔记. 首先,先开启mysql服务,创建一个空数据库(脚本里没有创建数据库) 执行脚本有两个方法 1.未连接数据库:在Windows下使用cm ...

  7. C#控件基础

    在说控件之前,还是有必要说一下如何创建项目的. 现在我们就不用创建控制台应用程序了,而是文件>新建>C#>Windows窗体应用程序.名称,位置自己选择. 创建好了大致就是这样了,可 ...

  8. 赋值运算符函数__from <剑指Offer>

    前段时间忙于项目,难得偷得几日闲,为即将到来的就业季做准备.在面试时,应聘者要注意多和考官交流,只有具备良好的沟通能力,才能充分了解面试官的需求,从而有针对性地选择算法解决问题. 题目来源于<剑 ...

  9. 使用EF操作Mysql数据库中文变问号的解决方案

    问题场景:使用Entity Framework 6.0 操作Mysql数据库,中文保存至数据库后全部变成问号.但是使用Mysql API却不会. 原因排查:首先想到的肯定是数据库编码问题,一次查询了表 ...

  10. LeetCode 405. Convert a Number to Hexadecimal (把一个数转化为16进制)

    Given an integer, write an algorithm to convert it to hexadecimal. For negative integer, two’s compl ...