【iOS】内存走漏检查及原因分析

打印 上一主题 下一主题

主题 806|帖子 806|积分 2420

前言
   

  • 内存溢出(out of memory):是指步伐在申请内存时,没有富足的内存空间供其利用,出现out of memory。通俗明白就是内存不敷,通常在运行大型软件或游戏时,软件或游戏所需要的内存远远超出了你主机内安装的内存所承受大小,就叫内存溢出。
  • 内存泄露( memory leak):是指步伐在申请内存后,无法开释已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严峻,无论多少内存,早晚会被占光。
  
为什么要检测内存走漏?

敏捷膨胀的内存可以很快让步伐毙命,以是要多加防范。即使有 ARC(自动引用计数)内存管理机制,但在现实中对象之间引用复杂,循环引用导致的内存走漏仍然难以制止,以是关键时候还要独立更生。分析内存泄露不能把全部的内存泄露查出来,有的内存泄露是在运行时,用户操作时才产生的。
什么是内存走漏?

内存走漏指的是一块内存被分配后不再利用,但是没有被步伐正确开释回系统,从而导致该内存继承占用在步伐中,无法被其它任务利用。
这通常发生在利用了动态内存分配但未及时或正确开释,或者由于编程逻辑错误导致。假如不加以管理,会导致步伐斲丧过多的内存,甚至导致应用步伐崩溃。
比如下MRC中如下代码会造成走漏:
  1. NSString* string = [[NSString alloc] init];
  2. ...
  3. // [string release];  //ARC下,编译器自动添加此代码
复制代码
但由于ARC机制,编译器会在得当的时机帮我们加上release代码,制止了内存走漏。不外即使在ARC中也有肯能因对象不开释而引起内存走漏,比如利用CF框架下的对象而没有做CFRelease操作。
固然string所占的内存很小可以忽略不计,但也是有安全隐患的,就像前言所述,代码中这里走漏一点内存,那里又走漏一点内存,反反复复,内存总会有用尽的那一刻。
究竟系统自己内存有限,分配给每个App的内存更加有限,当系统内存逐步不足时,我们的App会变得越来越卡顿。
当系统内存告急时,App中首先会收到didRecieveWarning提醒,假如我们不第一时间采取步伐开释内存,那么系统就会把我们的App Kill掉,以是我们应该重视内存走漏问题。
didRecieveWarning调用流程看这篇文章:【iOS】didReceiveMemoryWarning实例方法
内存走漏排查方法

1. 利用Zombie Objects

偶然间我们会收到EXC_BASD_ACCESS错误提示,但没能跳到详细的堕落代码行,此时可以启用Zombie Objects功能,来探求那些已被开释的对象。
进入edit Scheme:

选中僵尸对象选项:

按照以上步骤开启Zombies Objects,而后Memory查看器变为disable:

系统在即将回收对象时,假如发现通过环境变量启用了僵尸对象功能,那么还将执行一个附加步骤,即把对象转化为僵尸对象,而不彻底回收。
测试代码:
  1. void PrintClassInfo(id obj) {
  2.     Class cls = object_getClass(obj);
  3.     Class superCls = class_getSuperclass(cls);
  4.     NSLog(@"=== %s : %s ===", class_getName(cls), class_getName(superCls));
  5. }
  6. - (void)viewDidLoad {
  7.     [super viewDidLoad];
  8.     // Do any additional setup after loading the view.
  9.    
  10.     UIView* view = [[UIView alloc] init];
  11.     NSLog(@"Before release:");
  12.     PrintClassInfo(view);
  13.    
  14.     [view release];
  15.    
  16.     NSLog(@"After release:");
  17.     PrintClassInfo(view);
  18. }
复制代码

2. 静态分析

打开Xcode项目,并点击Product->Analyze:

静态内存走漏分析如下:


静态分析方法能发现大部分的问题,但是只能是静态分析结果,有一些并不准确,另有一些动态分配内存的情形并没有举行分析。以是仅仅利用静态内存走漏分析得到的结果并不是非常可靠,假如需要,我们需要将对项目举行更为美满的内存走漏分析和排查。那就需要用到我们下面要介绍的动态内存走漏分析方法Instruments中的Leaks方法举行排查。
3. 动态分析方法

打开Xcode项目,点击Product->rofile:

选择Leaks,这时项目也在模仿器或真机上运行起来了:

或者直接在自己的项目中运行步伐,选中Memory点击右上角的Profile in instruments:

都可以进入下面的页面:

由于 Leaks 是动态监测,以是我们需要手动操作 APP,举行测试,一边操作 APP,一边观察 Leaks 的变化,在 暂停按钮 的右边 我们可以选择正在 运行的步伐 & 选择设备 & App, 之后点击 红点 Record(红色圆圈按钮)运行。

观察,假如发如今 Leaks 里面有一个 红色X,这阐明白我们的 APP 存在内存泄露。
就像如许

点击暂停,点击此中一个,然后我们开始分析。
定位修改

此时选中有红色叉的 Leaks,下面有个Leaks 字方格,点开,选中 Call Tree。

接着就是最关键的一步,在这个界面的右下角有多少选框,选中Invert Call Tree(从上到下跟踪堆栈信息) 和 Hide System Libraries(表现隐蔽系统的函数)

在详情面板选中体现的多少条中的一条,双击,会自动跳到内存泄露代码处,然后点击右上角 Xcode 图标举行修改。
Leaks界面分析

Leaks 启动后会开始录制,随着对模仿器运行的 App 的操作,可以在 Leaks 中查看内存占用的情况。
Leaks顶部分为两栏:Allocations 和 Leaks,右侧的曲线代表内存分配和内存走漏曲线。
Call Tree的四个选项:



  • Separate By Thread: 线程分离,只有如许才能在调用路径中可以或许清楚看到占用CPU最大的线程.每个线程应该分开考虑。只有如许你才能揪出那些大量占用CPU的"重"线程,按线程分开做分析,如许更容易揪出那些吃资源的问题线程。特殊是对于主线程,它要处理和渲染全部的接口数据,一旦受到阻塞,步伐肯定卡顿或停止相应。
  • Invert Call Tree: 从上到下跟踪堆栈信息.这个选项可以快捷的看到方法调用路径最深方法占用CPU耗时(这意味着你看到的表中的方法,将已从第0帧开始取样,这通常你是想要的,只有如许你才能看到CPU中话费时间最深的方法),比如FuncA{FunB{FunC}},勾选后堆栈以C->B->A把调用层级最深的C体现最表面.反向输出调用树。把调用层级最深的方法体现在最上面,更容易找到最耗时的操作。
  • 表现隐蔽系统的函数,调用这个就更有用了,勾选后耗时调用路径只会体现app耗时的代码,性能分析普遍我们都比力关系自己代码的耗时而不是系统的.基本是必选项.留意有些代码耗时也会纳入系统层级,可以举行勾选前后前后对执行路径举行比对会非常有用.因为通常你只关心cpu花在自己代码上的时间不是系统上的,隐蔽系统库文件。过滤掉各种系统调用,只体现自己的代码调用。隐蔽缺失符号。假如 dSYM 文件或其他系统架构缺失,列表中会出现许多希奇的十六进制的数值,用此选项把这些干扰元素屏蔽掉,让列表回归清爽。
  • 递归函数, 每个堆栈跟踪一个条目,拼合递归。将同一递归函数产生的多条堆栈(因为递归函数会调用自己)合并为一条。
内存走漏原因分析

在目前主要以ARC举行内存管理的开发模式,导致内存走漏的根本原因是代码总存在循环引用,从而导致一些内存无法开释,这就会导致dealloc方法无法被调用。
开启了ARC并不是就不会存在内存问题,苹果有句名言:ARC is only for NSObject。
利用ARC的项目,一般内存走漏都是 malloc、自定义结构、资源引起的,多留意这些地方举行分析。
注:假如你的项目利用了ARC,随着你的操作,不停开启或关闭视图,内存可能持续上升,但这不肯定表现存在内存走漏,ARC开释的时机是不固定的。
引起内存走漏的几种原因:
1. Leaked Memory:应用步伐未引用的、不能再次利用或开释的内存。



  • 在OC的ARC机制下,利用CF或CG对象时,忘记手动调用CFRelease或CGRelease。
    1.     CGImageRef* imageRef = CGImageCreateWithImageInRect(someImage, someRect);
    2. // ...
    3. //    CGImageRelease(imageRef); //  释放内存
    复制代码
  • for循环中超多次加载比力占内存的对象:频繁创建大量占用内存的对象,假如不利用@autorelease,会导致内存无法及时开释。
    1. for (int i = 0; i < 1000; ++i) {
    2.    @autoreleasepool {
    3.        UIImage* image = [UIImage imageNamed: @"largeImage"];
    4.        // ...
    5.    }
    6. }
    复制代码
2. Abandoned Memory: 内存仍被应用步伐所引用,没有任何有用的用途。

大多是OC对象、block、timer、delegate等循环引用问题,造成引用计数不停不为零。


  • OC对象循环引用:
    1. @interface Person : NSObject
    2. @property (nonatomic, strong)Person* friends;
    3. @end
    4. @implementation Person
    5. - (void)dealloc {
    6.     NSLog(@"Person 对象被释放");
    7. }
    8. @end
    9. int main(int argc, const char * argv[]) {
    10.     @autoreleasepool {
    11.         Person* person1 = [[Person alloc] init];
    12.         Person* person2 = [[Person alloc] init];
    13.         person1.friends = person2;
    14.         person2.friends = person1;
    15.     }
    16.     return 0;
    17. }
    复制代码
    person1和person2互相引用,形成了循环(强)引用,这两个对象的引用计数不会降为0,dealloc方法并没有被执行。
    解决办法就是利用弱引用冲破循环:@property (nonatomic, weak)Person* friends。
    - Block循环引用:
    1. @interface MyClass : NSObject
    2. @property (nonatomic, strong) void (^myBlock)(void);
    3. - (void)setupBlock;
    4. @end
    5. @implementation MyClass
    6. - (void)setupBlock {
    7.     //  __weak typeof(self) weakSelf = self;
    8.     self.myBlock = ^{
    9.         //  __strong typeof(weakSelf) strongSelf = weakSelf;
    10.         NSLog(@"%@", self); // 造成强引用循环
    11.         //  if (strongSelf) {
    12.         //      NSLog(@"%@", strongSelf);
    13.         //  }
    14.     };
    15. }
    16. @end
    17. MyClass *obj = [[MyClass alloc] init];
    18. [obj setupBlock];
    复制代码
    假如没有解释掉的那段代码,self.myBlock对self产生了强引用,导致self的引用计数永久不会为零,从而引起循环引用。

  • Timer循环引用:
    1. @interface MyClass : NSObject
    2. @property (nonatomic, strong) NSTimer *timer;
    3. @end
    4. @implementation MyClass
    5. - (void)startTimer {
    6.     // __weak typeof(self) weakSelf = self;
    7.     self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerFired) userInfo:nil repeats:YES];
    8. }
    9. - (void)timerFired {
    10.     // Timer fired actions
    11. }
    12. @end
    复制代码
    假如没有解释掉的那段代码,NSTimer对self保持强引用(target: self增加了ViewController的引用计数,假如不举行[timer invalidate];,就别想调用dealloc了),而self对NSTimer也保持强引用,形成循环引用。
    timer属性也最好设置为弱引用(weak)。
  • Delegate引起的循环引用:
    1. @interface MyObject : NSObject
    2. @property (nonatomic, strong)id<MyDelegate> delegate;
    3. @end
    4. @interface MyViewController : UIViewController <MyDelegate>
    5. @property (nonatomic, strong) MyObject *myObject;
    6. @end
    7. @implementation MyViewController
    8. - (void)viewDidLoad {
    9.     [super viewDidLoad];
    10.     self.myObject.delegate = self;
    11. }
    12. @end
    复制代码
    与Timer同理,myObject对delegate利用强引用,delegate又对myObject保持强引用,形成循环引用。
    解决方案是利用弱引用:@property (nonatomic, weak)id<MyDelegate> delegate;。
  • ViewController的子视图对self的持有
    我们偶然间需要在子视图或者某个cell中点击跳转等操作,需要在子视图或cell中持有当前的ViewController对象,如许跳转之后的back键才能直接返回该页面,同时也不烧毁当前ViewController。此时,你就要留意在子视图或者cell中对当前页面的持有对象不能是强引用,尽量assign或者weak,否则会造成循环引用,内存无法开释。
3. Cached Memory:内存仍然由应用步伐引用,可以再次利用以得到更好的性能。

为了快速访问而存储起来的对象。
以缓存图片提高性能为例:
  1. @class UIImage;
  2. @interface ImageCache : NSObject
  3. @property (nonatomic, strong) NSMutableDictionary* cache;
  4. + (instancetype)sharedInstance;
  5. - (UIImage *)imageForKey:(NSString *)key;
  6. - (void)setImage:(UIImage *)image forKey:(NSString *)key;
  7. @end
  8. //  ImageCache.m
  9. #import "ImageCache.h"
  10. #import "UIKit/UIImage.h"
  11. @implementation ImageCache
  12. + (nonnull instancetype)sharedInstance {
  13.     static ImageCache *sharedInstance = nil;
  14.     static dispatch_once_t onceToken;
  15.     dispatch_once(&onceToken, ^{
  16.         sharedInstance = [[self alloc] init];
  17.         sharedInstance.cache = [NSMutableDictionary dictionary];
  18.     });
  19.     return sharedInstance;
  20. }
  21. - (void)setImage:(nonnull UIImage *)image forKey:(nonnull NSString *)key {
  22.     self.cache[key] = image;
  23. }
  24. - (nonnull UIImage *)imageForKey:(nonnull NSString *)key {
  25.     return self.cache[key];
  26. }
  27. @end
  28. // 使用缓存
  29. UIImage* image = [UIImage imageNamed: @"example.png"];
  30. [[ImageCache sharedInstance] setImage: image forKey: @"example"];
  31. // 之后访问缓存的图片
  32. UIImage* cachedImage = [[ImageCache sharedInstance] imageForKey: @"example"];
  33. ```
  34. 示例中,图片被缓存以便快速访问,从而提高性能。缓存图片使用的内存不是泄漏,因为这戏内存是有意保留以供将来使用的。
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

刘俊凯

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表