×
广告

iOS 内存泄漏分析

96
iven_zf
2016.08.12 17:23* 字数 2340

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];
iOS
Web note ad 1