本文章参考自iOS性能分析和优化,主要学习如何使用性能分析工具Instruments。
一.基本概念
1.内存空间的划分
一个进程占用的内存空间,包含5种不同的数据区:
(1) BSS段:通常存放未初始化的全局变量
(2)数据段:通常存放已初始化的全局变量
(3)代码段:通常存放程序执行代码
(4)堆:通常存放进程运行中被动态分配的内存段,OC对象(所有继承自NSObject的对象)就存放在堆里。
(5)栈:由编译器自动分配释放,存放函数的参数值,局部变量等值。
其中,栈内存是由系统来管理的,因此我们常说的内存管理,是指堆内存的管理,也就是所有OC对象的创建和销毁的管理。随着苹果推出了ARC,编译器会自动生成内存管理的代码,因此我们的内存泄漏通常来源于堆内存。
2.内存泄漏(Memory Leak)
用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元,即该内存空间使用完毕之后未回收。在ios的内存泄漏原因一般有循环引用、错用Strong/copy等。
二.Instruments多功能检测
1.静态分析(Analyze)
不需要运行程序,就可以检查到存在内存泄漏的地方。
常见的三种泄露情形:
- 创建了一个对象但是并没有使用,提示:
Value Stored to 'number' is never read
- 创建了一个(指针可变的)对象,且初始化了,但是初始化的值一直没读取过,提示:
Value Stored to 'str' during its initialization is never read
- 调用了让某个对象引用计数加一的函数,但没有调用相应的让其引用计数减一的函数,提示:
Potential leak of an object stored into 'subImageRef'
通过测试可以发现,程序中有这样的泄露:
原来是因为这个指针初始化之后没有使用,其实在这里如果要赋值的话,直接变成这样:
NSDictionary *cover = self.covers[indexPath.item];
2.内存泄漏检查工具——Leak
参考Xcode结合Leaks检测内存泄露
中途有点报错, 关闭SIP就好了。
哇,吓我一跳,这么多内存泄漏的吗..赶紧参考了 AFNetworking 3.0中调用[AFHTTPSessionManager manager]方法导致内存泄漏的解决办法,发现问题在于,由于ARC的机制,导致每当实例化
Session
类后都没有地方释放掉实例,需要把
Session
类的实例都改成单例模式。
(附)科普:单例模式
参考iOS-单例模式简单使用
1.概念
一个类只允许有一个实例,在整个程序中需要多次使用,共享同一份资源的时候,就可以创建单例,一般封装成工具类使用,如UIApplication
,NSUserDefaults
,NSNotificationCenter
,NSFileManager
等。
2.单例优缺点
优点:单例模式会使类只有一个实例,所以方便使用,并且节省内存资源的分配。因为使用GCD的方式是线程安全的,所以会避免资源的多重使用。
缺点:单例创建的内存只有在程序结束的时候才会被释放。由于单例不能被继承(因为返回的是同一个实例),所以扩展性很不好。
由此,我们首先建立一个AFSessionSingleton
文件,里面包含的是单例模式的声明。
#import "AFSessionSingleton.h"
@implementation AFSessionSingleton
static AFHTTPSessionManager *manager;
+ (AFHTTPSessionManager *) sharedHttpSessionManager
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
manager = [AFHTTPSessionManager manager];
manager.requestSerializer.timeoutInterval = 10.0;
});
return manager;
}
@end
在调用的时候变成这一句:
// AFHTTPSessionManager *session = [AFHTTPSessionManager manager];
AFHTTPSessionManager *session = [AFSessionSingleton sharedHttpSessionManager];
就可以用单例模式避免内存泄漏了!
其他问题:还有一个Leak,但是通过debug发现没什么事,就不管了。
3.自己制造一个Leak出来
参考iOS开发中本人或同事碰到的内存泄漏及解决办法
循环引用
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
NSMutableArray *arr1 = [NSMutableArray array];
NSMutableArray *arr2 = [NSMutableArray array];
[arr1 addObject: arr2];
[arr2 addObject: arr1];
}
他说的那种方法还是会导致内存泄漏啊..以后还是尽量避免循环引用吧。
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
NSMutableArray *arr1 = [NSMutableArray array];
NSMutableArray *arr2 = [NSMutableArray array];
__weak typeof (arr1) weakArr1 = arr1;
[weakArr1 addObject: arr2];
[arr2 addObject: weakArr1];
}
4.Allocations——内存分配
用于检测程序运行过程中的内存分配情况。
5.Time Profiler
分析代码的执行时间,以便找到导致程序变慢的原因。
当点击Time Profiler应用程序开始运行后.就能获取到整个应用程序运行消耗时间分布和百分比.为了保证数据分析在统一使用场景真实行有如下点需要注意:
在开始进行应用程序性能分析前,请一定要使用真机,因为模拟器运行在Mac上,然而Mac上的CPU往往比iOS设备要快。相反,Mac上的GPU和iOS设备的完全不一样,模拟器不得已要在软件层面(CPU)模拟设备的GPU,这意味着GPU相关的操作在模拟器上运行的更慢,尤其是使用CAEAGLLayer来写一些OpenGL的代码时候。这就导致模拟器性能数据和用户真机使用性能数据相去甚远。
可以看到,加载视频是最花时间的,然后就是加载瀑布流图片。
三.一些总结
1.以下情况会增加APP的内存占用
- 创建对象,定义变量。
- 调用一个函数或方法。
2.以下情况会增加CPU的消耗
- 创建对象、调整对象属性、销毁对象。
- 布局计算和AutoLayout。
- 文本的计算和渲染。
- 图片的解码和绘制。
3.如何避免内存泄漏
- 做好cell等可复用对象的重用
- 可以只创建一次的对象,不要创建多次(如页面的某个功能弹窗)
- 用较少的对象和方法调用去实现功能
- 将耗时的操作放在子线程
4.一些关于ViewController的内存泄漏
参考iOS学习——内存泄漏检查及原因分析
在目前主要以ARC进行内存管理的开发模式,导致内存泄漏的根本原因是代码中存在循环引用,从而导致一些内存无法释放,这就会导致dealloc()方法无法被调用。主要原因大概有一下几种类型。
1) ViewController中存在NSTimer
NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:@selector(updateTime:)
userInfo:nil
repeats:YES];
如果你的ViewController中有NSTimer,那么你就要注意了,因为当你调用target:self
时,就会增加ViewController
的return count
,如果不将这个Timer invalidate
,将不会调用dealloc
。
2) ViewController中的代理delegate
一个比较隐秘的因素,要找一下这个类有关的代理,看看有没有强引用属性,如果这个View Controller
需要外部传入某个Delegate
进来,来通过Delegate+protocol
的方式传参数给其他对象,那么这个delegate
一定不要强引用,尽量assign
或者weak
,否则整个View Controller
会持续持有这个delegate
,直到自身被释放。
3) ViewController中的Block
这个问题会比较容易犯,Block
体内使用实例变量也会造成循环引用,使得拥有这个实例的对象不能释放。因为该Block
本来就是当前View Controller
的一部分,现在Block
又强引用self
,导致循环引用无法释放。
例如:有一个类叫OneViewController
,有个属性是NSString *name
,如果在block
体内使用了self.name
或者_name
,这个类就无法释放了。
解决方法就是在Block
之前声明当前的self
为弱引用。
__weak ViewController *weasSelf = self;
4) ViewController的子视图对self的持有
我们有时候需要在自视图或者某个cell
中点击跳转等操作,需要在自视图或者cell
中持有当前的ViewController
对象,这样跳转之后的back
键才能直接返回该页面,同时不销毁当前的ViewController
。此时,就需要注意在子视图或者cell
中对当前页面的持有对象不能是强引用,尽量用assign
或者weak
,否则会造成循环引用,导致内存无法释放。