iOS 内存泄漏分析


Leaks


苹果官方有关于内存分析说明,在文档中把内存泄漏情况分为4中:

Overall Memory Use. Monitor at a high level how your app uses memory and compare it to the memory usage of other active processes on the system. Look for areas of large or unexpected memory growth

Leaked Memory. This is memory that was allocated at some point, but was never released and is no longer referenced by your app. Since there are no references to it, there’s now no way to release it and the memory can’t be used again. For example, suppose you’ve written an app that creates rectangle objects in a drawing, but never releases the objects when the drawing is closed. In this case, your app would leak more and more memory whenever a drawing containing rectangles is closed. To fix the leak, you need to figure out which object isn’t being released, and then update your app to release it at the appropriate time.

Abandoned Memory. This is memory that your app has allocated for some reason, but it’s not needed and won’t be referenced. For example, suppose your app adds images to a cache after they’ve already been cached—using double the memory for the same images. Or, maybe your app maintains an array of objects in case you need to access them later, but you never actually do. Unlike leaked memory, abandoned memory like this is still referenced somewhere in your app. It just serves no purpose. Since it’s still technically valid, it’s more difficult for Instruments to identify and requires more detective work on your part to find

Zombies. This is memory that has been released and is no longer needed, but your code still references it somewhere. For example, suppose your app contains an image cache. Once the cache has been cleared, your app shouldn’t attempt to refer to the images that it previously contained. Calls to these nonexistent images are considered zombies—references to objects that are no longer living

Overall Memory Use: 内存总体使用情况
leaked Memory: 在MRC中会经常出现,当retain后忘记发送Release消息,导致一个对象没有指针引用但retainCount != 0retainCount == 0才能让对象销毁),ARC中很少出现。
Abandoned Memory: 在ARC中会经常出现,当retainrelease不在被用户手动触发,导致很多很多循环引用不为开发者所感知,而导致Abandoned memory。
Zombies: 僵尸对象,当对象已经被释放,但我们仍然用指针调用时会出现这种情况。

内存查看工具


Zombies

Xcode有自带的Zombies exception提醒.
1、在 manager scheme -> run -> Diagnostics 中开启即可。

Zombies

2、在断点栏点击左下角的+号,添加一个 Add Exception Breakpoint

Add Exception Breakpoint

Leaked Memory

利用 xcode -> product ->profile -> leaks 查看在ARC中用的比较少,这里不做介绍

Abandoned Memory(重点介绍)

对于Abandoned memory,可以用instrument 的 Allocation方法检测出来。主要原理:利用Mark Generation 对某一个操作进行操作前后的快照对比,通过堆中OC对象数量变化来判断是否产生了内存泄漏。

WWDC官方视频:
Advanced Memory Analysis with Instruments

WWDC视频中介绍了一个简单的案例:在平时开发时,我们常常以一个UIViewController作为一个基础元素,因此我们可以对一个UIViewController进行观察。理论上,我们push这个VC,这个VC中所有的子View,ViewModel,Model会分配一部分的内存空间,而在Pop之后应该全部被dealloc掉。因此我们可以在Push之前,Push之后,Pop之后都设置一个快照,通过查看3个快照之间的差异判断是否有内存泄漏。

使用方法
1、利用 xocode -> product -> profile 工具(command + i)开启instrument。

Profile

2、选择Leaks查看工具

Leaks

3、打开后的界面选择Allocations

Allocations

All Heap & Anonymous VM 里面能看到堆中的使用情况。在右上角能进行筛选,筛选后能看到具体细节:

现在占有的个数(Persistent)
占有内存大小(Persistent Bytes),
总创建的个数(Total),
总创建所占内存大小(Persistent)

这样我们就可以开始通过操作页面,动态监测一个项目是否存在内存泄漏。

常见内存对象不销毁情况

1、UIViewController 和 子UIView中双向持有对方
@interface MyViewController: UIViewController   
     //1.MyViewController 中强引用 childView 
    @property (nonatomic, strong) ChildView *childView;    
@end
----------------------------------------------------- 
@interface ChildView : UIView
    @property (nonatomic, strong) MyViewController *controller;
    -(instancetype)initWithController:(MyViewController *)controller;
@end
//implement
    -(instancetype)initWithController:(MyViewController *)controller{
        self = [super init];
        if(self){
            //2.这一步让View,强引用 MyViewController
            self.controller = controller;
        }
        return self;
    }

如上代码所示,12中让两个对象双向强引用,导致两者都不会被释放。
解决方案:
2这一步换成weak

@property (nonatomic, weak) MyViewController *controller;
2、Delegate循环引用

还是引用上面的案例,这次我们改成delegate版本

@interface MyViewController: UIViewController 
//1.MyViewController 中强引用 childView 
    @property (nonatomic, strong) ChildView *childView; 
@end 
//implement 
self.childView = [[ChildView alloc]init]; 
//2.delegate 设置为 MyViewController 
self.childView.delegate = self; 
----------------------------------------------------- 
@protocol ChildViewProtocol 
@end 
@interface ChildView : UIView 
    //strong delegate代理器 
    @property(nonatomic, strong) id<ChildViewProtocol> *delegate 
@end

1MyViewController强引用持有一个ChildView
2ChildView.delegate 设置为 MyViewController
3、这里可以看出ChildView强引用id<ChildViewProtocol> == MyViewController
因此也造成了循环引用,导致不能被销毁
解决方案
3中强引用改为弱引用

@property(nonatomic, weak) id<ChildViewProtocol> *delegate
3、block使用双向持有

这个比较复杂,首先我们了解下block原理:
A look inside blocks: Episode 1
A look inside blocks: Episode 2
这儿有两篇文章对block具体实现讲的很详细。总结一下文章的观点,block的struct声明如下:

struct Block_descriptor { 
    unsigned long int reserved; 
    unsigned long int size; 
    void (*copy)(void *dst, void *src); 
    void (*dispose)(void *); 
};  
struct Block_layout { 
    void *isa; 
    int flags; 
    int reserved; 
    void (*invoke)(void *, ...); 
    struct Block_descriptor *descriptor; 
    /* Imported variables. */ 
};

在这个struct,我们看到一个isa指针,这里和OC对象很类似。在OC对象中isa指向的是其Class或者metaClass, 在这里isa指针,指向3种类型的Block

_NSConcreteGlobalBlock 全局Block,在编译的时候则已经存在
_NSConcreteStackBlock 栈上分配的Block,也就是在方法体内部声明的block
_NSConcreteMallocBlock 堆上分配的Block,也就是一个对象的成员变量或属性

而出现循环引用的情况,大多数都是_NSConcreteMallocBlock使用不恰到导致。下面看一个具体案例:

//1.定义一个TestBlock 
typedef void (^TestBlock)();  
//2.TestBlock的初始化 
TestBlock getBlock() { 
    char e = 'E'; 
    void (^returnedBlock)() = ^{ 
        printf("%c\n", e); 
    }; 
    return returnedBlock; 
}

首先我们先看下2,在栈空间创建一个returnedBlock,这个block在方法体执行完后会自动销毁。在return returnedBlock,在ARC中其实系统会自动帮你做一次copy操作,而这次copy操作则让block从_NSConcreteStackBlock变为了_NSConcreteMallocBlock
如果还不清楚,可以看下MRC情况下,block一般使用方案:

TestBlock getBlock() { 
    char e = 'E'; 
    void (^returnedBlock)() = ^{ 
        printf("%c\n", e); 
    }; 
        //3.手动copy,然后autoRelease
   return [[returnedBlock copy] autorelease]; 
}

3中,很容易看出需要手动进行一次copy操作,而这次copy操作让这个block 的retainCount属性 +1.

block 循环引用 案例1
所以block本质上类似上面第1,2案例的ChildView

//1、self有个指针强引用 completionBlock 
@property(nonatomic, readwrite, copy) completionBlock completionBlock; 
@property(nonatomic, strong) UIView *successView;      
self.completionBlock = ^ { 
    //2、在这里使用self,则堆中block空间会生成一个指针指向self,形成了一个双向强引用 
    self.successView.hidden = YES;
};

如注释1,2所示,隐形中形成双向引用,解决方案:

//生成一个对 self 的弱引用 
__weak typeof(self) weakSelf = self; 
self.completionBlock = ^ {  
    weakSelf.successView.hidden = YES;
};

block 循环引用 案例2
最近在研究 ReactiveCocoa,这个框架对 self 弱引用,强引用进行封装,如@weakify(self) @strongify(self),这有篇文章对着两个宏定义进行剖析 剖析@weakify 和 @strongify,文章分析最终结果为:

"@weakify(self)" == "__weak __typeof__ (self) self_weak_ = self;"
"@strongify(self)" == "__strong __typeof__ (self) self = self_weak_;"

所以案例1方案另一种写法为:

//利用 ReactiveCocoa 方案 
@weakify(self) 
self.completionBlock = ^ {  
    @strongify(self)  
    self.successView.hidden = YES; 
};

block 循环引用 案例3
下面继续介绍@weakify(self) @strongify(self)在使用中遇到的坑。如果block里面嵌套block,那该如何解决,先看下面案例:

@weakify(self) 
//1、给selectedButton绑定一个点击事件
[[self.selectedButton rac_signalForControlEvents:UIControlEventTouchUpInside] 
subscribeNext:^(id x) { 
//2、点击以后做数据操作 
    self.do(^(BOOL result){ 
        //3、操作完成后,回调处理 
        @strongify(self) 
         self.success = result; 
    }) 
}];

1、创建一个selectedButton的点击事件
2、点击事件触发后,进行do操作
3、最后对操作后的事件进行处理 self.success = result

当利用instrument测试代码的时,我们会发现这个block会造成循环引用。如果对@weakify(self) @strongify(self)不理解,很难发现其中问题。原因在哪里?我们按照上面宏替换标准对这个进行替换。

__weak __typeof__ (self) self_weak_ = self;  
[[self.selectedButton rac_signalForControlEvents:UIControlEventTouchUpInside]         //1 
subscribeNext:^(id x) { 
    self.do(^(BOOL result){         //2 
        __strong __typeof__ (self) self = self_weak_;  
        self.success = result;         //3 
    }) 
}];

我们对上面代码中self进行分析

  1. 这里是强引用self持有一个block
  2. block 持有一个strongself 因此会导致循环引用
  3. 这里的self实际上是self_weak_ 没有问题

所以问题出现在2处!那下面我进行多次尝试修复这个问题。

尝试修改1

@weakify(self) 
[[self.selectedButton rac_signalForControlEvents:UIControlEventTouchUpInside]       //1 
subscribeNext:^(id x) { 
    @strongify(self) 
    @weakify(self) 
    self.do(^(BOOL result){     //2  
        @strongify(self)  
        self.success = result;      //3 
    }) 
}];

这样修改最能适应@weakify(self) @strongify(self)ReactiveCocoa成对出现的理念,且在2处使用的肯定是 weak 的self,确实没有问题。

但总感觉有点奇怪,利用上面的宏替换很容易看出,在第一次@strongify(self)时,self == self_weak_ self已经是weak,所以我们没必要再在后面进行新的 weakifystrongify,对上面的代码进行改进,如下

尝试修改2

@weakify(self) 
[[self.selectedButton rac_signalForControlEvents:UIControlEventTouchUpInside]  
subscribeNext:^(id x) { 
    @strongify(self) 
    self.do(^(BOOL result){  
        self.success = result;  
    }) 
}];

总结:多层嵌套的block,只需要对最外层block传入的self进行weak化即可。

block 循环引用 案例4

再列举一个容易犯的错误,代码如下:

@property (nonatomic, assign) BOOL success; 
@weakify(self)     
[[self.selectedButton rac_signalForControlEvents:UIControlEventTouchUpInside]  
subscribeNext:^(id x) { 
    @strongify(self) 
    _success = false;       //1 
}];

问题就出现在1处。我们知道对属性的访问,可以使用 self.success_success两种方式,当然两者有一些区别

self.success 能支持懒加载方式 调用 successget 方法。
_success是对实例变量的访问。
iOS 5.0 之后已经支持属性到实例的映射,也就是省略 @sychronise _success = self.success;

但在block中使用,得特别注意,self.success 会使用 @strongify(self) 所生成的self_weak_ ,而_success 不会!不会!不会!
所以block 强引用指向 strong 的 self,调用其实例变量。所以上诉代码 _success会造成循环引用。

4、NSNotificationCenter,KVO 问题

关于事件监听,属性监听,会自动retain self,所以在 dealloc 时需要对监听进行释放。

[[NSNotificationCenter defaultCenter] removeObserver:self]; 
[self removeObserver:self forKeyPath:@"" context:nil];
5、NSTimer Animator 停止

对于NSTimer持续的Animator动画,需要在 dealloc时停止。

[NSTimer invalidate];
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容