一.概念
内存(Memory)是计算机的重要部件之一,也称内存储器和主存储器,它用于暂时存放CPU中的运算数据,与硬盘等外部存储器交换的数据。
运行时由应用产生的,应用结束后消失的,就是内存。
二.缓存与内存
缓存分为内存缓存和磁盘缓存。
内存缓存
常见的内存缓存有NSCache、TMMemoryCache、PINMemoryCache、YYMemoryCache。
以及存在堆、栈中的变量,也都可以称作为内存或者缓存。
磁盘缓存
而文件的储存,则可以称为磁盘缓存。
常见的磁盘缓框架存有TMDiskCache、PINDiskCache、YYCache。
三.内存分区
- 栈区(stack):由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
- 堆区(heap): 一般由程序员分配释放, 若程序员不释放,程序结束时由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
- 全局区(静态区)(static):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放。int a;未初始化的。int a = 10;已初始化的。
- 文字常量区:常量字符串就是放在这里的。 程序结束后由系统释放 。
- 程序代码区:存放函数体的二进制代码。
关于NSString内存分配
代码:
NSString *str1 = @"a string";
NSString *str2 = [NSString stringWithFormat:@"a string"];
NSString *str3 = [[NSString alloc] initWithFormat:@"a string"];
NSString *str4 = [NSString stringWithString:@"a string"];
log:
(lldb) p str1
(__NSCFConstantString *) $0 = 0x00000001038c2068 @"a string"
(lldb) p str2
(NSTaggedPointerString *) $1 = 0xa0020f3041412da8 @"a string"
(lldb) p str3
(NSTaggedPointerString *) $2 = 0xa0020f3041412da8 @"a string"
(lldb) p str4
(__NSCFConstantString *) $3 = 0x00000001038c2068 @"a string"
总结:我们可以看到,用
@""
直接分配的字符串为__NSCFConstantString
类型,而用initWithFormat
和stringWithFormat
创建的为NSTaggedPointerString
类型。TaggedPointer
是苹果针对NSString
和NSNumber
以及NSData
类型所做出的优化,更方便、拥有更快的读写速度。
其中__NSCFConstantString
类型存在于文字常量区,也就是说str1
指针存在于栈,对应内容存在于文字常量,而str2
的内容直接存在于栈,仅应用于小于10位的字符串。
另外,我们可以看到栈中存放指针和值类型的值,以及地址。
(lldb) p age
(int) $0 = 123444
(lldb) p &age
(int *) $1 = 0x00007ffeea7a3bec
(lldb) p str1
(__NSCFConstantString *) $2 = 0x000000010545b068 @"a string"
(lldb) p &str1
(NSString **) $3 = 0x00007ffeea7a3c08
(lldb) p str2
(NSTaggedPointerString *) $4 = 0xa0020f3041412da8 @"a string"
(lldb) p &str2
(NSString **) $5 = 0x00007ffeea7a3c00
(lldb) p &label
(UILabel **) $6 = 0x00007ffeea7a3bd8
(lldb) po label
>
(lldb) p num
(int) $1 = 1
(lldb) p &num
(int *) $2 = 0x000000010f3ebe48
(lldb) p str
(NSString *) $3 = nil
(lldb) p &str
(NSString **) $4 = 0x000000010f3ebf10
由高到低(自下往上为由低地址到高地址):
- 0x00007ffeea7a3bec 栈地址
- 0x00007f9a71605ef0 堆地址
- 0x000000010f3ebf10 BSS(全局区)
- 0x000000010f3ebe48 数据区(全局区)
- 0x000000010545b068 文字常量区地址
四.栈与堆
堆和栈的区别可以用如下的比喻来看出:
使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。
使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。
OC对象需要进行内存管理,而其它非对象类型比如基本数据类型就不需要进行内存管理
Objective-C的对象在内存中是以堆的方式分配空间的。堆里面的内存是动态分配的,所以也就需要程序员手动的去添加内存、回收内存。
非OC对象一般放在栈里面(栈内存会被系统自动回收)
举例说明:
int main(int argc, char *argv[])
{
@autoreleasepool {
int a = 10;
int b = 20;
Car *c = [[Car alloc] init];
}
}
当代码块一过,里面的a,b,*c指针都会被系统编译器自动回收,因为它存放在栈里面,而OC对象则不会被系统回收,因为它存放堆里面,堆里面的内存是动态存储的,所以需要程序员手动回收内存。
注意:
对象被创建并被赋值时,数据可能从栈复制到堆。类似,也可能会从堆复制到栈。
例如将int类型赋值给对象某一属性时。
五.自动释放池块
概念
OC 对象的生命周期取决于引用计数,我们有两种方式可以释放对象:一种是直接调用 release 释放;另一种是调用 autorelease 将对象加入自动释放池中。自动释放池用于存放那些需要在稍后某个时刻释放的对象。
创建与释放
App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。
第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。
总结
我们的Mac以及iOS系统会自动创建一些线程,例如主线程和GCD中的线程,都默认拥有自动释放池。每次执行 “事件循环”(event loop)时,就会将其清空,这一点非常重要,请务必牢记!因此我们一般不需要手动创建自动释放池,通常只有一个地方需要它,那就是在main()函数里。
实际应用
需改良代码:
for (int i = 0; i < 1000000; i++) {
NSString *str = @"abc";
str = [str lowercaseString];
str = [str stringByAppendingString:@"xyz"];
}
在执行 for 循环时,会有持续不断的新的临时对象被创建出来,并加入自动释放池。而释放池却要等到该线程执行下一次事件循环时才会清空,也就是要等到结束 for 循环才会释放。因此在 for 循环中内存用量会持续上涨,而等到结束循环后,内存用量又会突然下降。
改良后:
for (int i = 0; i < 1000000; i++) {
@autoreleasepool {
NSString *str = @"abc";
str = [str lowercaseString];
str = [str stringByAppendingString:@"xyz"];
}
}
将 @autoreleasepool 放在循环内部可以在每次循环后释放内存。而将 @autoreleasepool 放在循环外(其实等同于不使用 @autoreleasepool ),会导致会在循环全部结束时才释放。
合理的使用 @autoreleasepool 会需求更少的内存,同时降低内存峰值。
结合YTKNetwork进一步理解
YTKNetwork源码:
- (void)requestDidSucceedWithRequest:(YTKBaseRequest *)request {
// 加入手动创建的自动释放池中,执行完池中方法后相应内存直接释放
@autoreleasepool {
[request requestCompletePreprocessor];
}
dispatch_async(dispatch_get_main_queue(), ^{
[request toggleAccessoriesWillStopCallBack];
[request requestCompleteFilter];
if (request.delegate != nil) {
[request.delegate requestFinished:request];
}
if (request.successCompletionBlock) {
request.successCompletionBlock(request);
}
[request toggleAccessoriesDidStopCallBack];
});
}
总结:
请求成功后的数据先存入内存,导致内存升高,然后通过requestCompletePreprocessor
方法写入文件缓存。写入成功后内存数据不再使用,可以直接释放降低内存,所以放入创建的自动释放池中。如果不放入,则会在请求完成之后才释放,浪费资源。
对于内存而言,方法中的私有变量会在方法结束后释放,类中的全局变量会在类释放时释放,加入自动释放池在其内部实例化的变量可在自动释放池结束后立即释放,最大限度降低不必要内存浪费
六.ARC与引用类型
ARC的规则
- 不能实现或者调用 retain 、release 、autorelease 或 retainCount 方法
- 可以实现但是不能调用 dealloc 方法
- 属性的访问器不能以 new 开头
但是可以property (getter=getNewTitle) NSString *newTitle;
变量限定符
- __strong ,默认,强引用类型
retain 的 ARC 版本 - __weak ,特殊,弱引用类型
assign 的 ARC 版本 - __unsafe_unretained,与 __weak 类似,区别在于没有强引用的时候不会置为 nil
- __autoreleasing, 相当于这个对象在MRC下被发送了autorelease消息,也就是说它被注册到了autorelease pool中
Person * __strong p1 = [[Person alloc] init]; //引用计数为1,对象在引用期间不会被回收
Person * __weak p2 = [[Person alloc] init]; //引用计数为0,对象立即释放,p2为nil
Person * __unsafe_unretained p3 = [[Person alloc] init]; //引用计数为0,对象立即释放,但p3不为nil,且不能访问p3中内容。
Person * __autoreleasing p4 = [[Person alloc] init]; //引用计数为1,当方法返会同时对象立即释放,p4为nil
属性限定符
strong
strong 表示属性对所赋的值持有强引用表示一种“拥有关系”(owning relationship),会先保留新值即增加新值的引用计数,然后再释放旧值即减少旧值的引用计数。只能修饰对象。如果对一些对象需要保持强引用则使用 strong 。
weak
weak 表示对所赋的值对象持有弱引用表示一种“非拥有关系”(nonowning relationship),对新值不会增加引用计数,也不会减少旧值的引用计数。所赋的值在引用计数为0被销毁后,weak 修饰的属性会被自动置为nil能够有效防止野指针错误。
weak 常用在修饰 delegate 等防止循环引用的场景。
atomic/nonatomic
指定合成存取方法是否为原子操作,可以理解为是否线程安全,但在 iOS 上即使使用 atomic 也不一定是线程安全的,要保证线程安全需要使用锁机制,超过本文的讲解范围,可以自行查阅。
可以发现几乎所有代码的属性设置都会使用 nonatomic,这样能够提高访问性能,在 iOS 中使用锁机制的开销较大,会损耗性能。
readwrite/readonly
readwrite 是编译器的默认选项,表示自动生成 getter 和setter,如果需要 getter 和 setter 不写即可。
readonly 表示只合成 getter 而不合成 setter。
assign
assign 表示对属性只进行简单的赋值操作,不更改所赋的新值的引用计数,也不改变旧值的引用计数,常用于标量类型,如NSInteger,NSUInteger,CGFloat,NSTimeInterval等。
assign 也可以修饰对象如 NSString 等类型对象,上面说过使用 assign 修饰不会更改所赋的新值的引用计数,也不改变旧值的引用计数,如果当所赋的新值引用计数为0对象被销毁时属性并不知道,编译器不会将该属性置为 nil,指针仍旧指向之前被销毁的内存,这时访问该属性会产生野指针错误并崩溃,因此使用 assign 修饰的类型一定要为标量类型。
unsafe_unretained
使用 unsafe_unretained 修饰时效果与 assign 相同,不会增加引用计数,当所赋的值被销毁时不会被置为nil可能会发生野指针错误。unsafe_unretained 与 assign 的区别在于,unsafe_unretained 只能修饰对象,不能修饰标量类型,而assign 两者均可修饰。
copy
copy 修饰的属性会在内存里拷贝一份对象,两个指针指向不同的内存地址。
一般用来修饰有对应可变类型子类的对象。
如:NSString/NSMutableString,NSArray/NSMutableArray,NSDictionary/NSMutableDictionary等。
为确保这些不可变对象因为可变子类对象影响,需要 copy 一份备份,如果不使用 copy 修饰,使用 strong 或 assign 等修饰则会因为多态导致属性值被修改。
另外:_block 是用来修饰一个变量时,这个变量就可以在 block 中被修改
_weak 是用来修饰一个变量时,这个变量不会在 block 代码块中被retain,用来避免循环引用。
五.僵尸对象
通常情况下,当引用计数为0时对象会立即被释放,使得我们调试变得困难,无法知道被释放的对象是什么。如果开启了僵尸对象,那么对象不会被立即释放,而是被标记为僵尸对象,任何试图访问僵尸对象的行为都会被记录,从而定位问题所在。
Product -> Scheme -> Edit Scheme -> Diagnostics 中打开 Zombie Objects 选项。
仅用于调试,由于不会真正释放,注意发布包中一定要禁用。
七.循环引用
委托
典型例子
[self.updateOp startWithDelegate: self withSelector: @selector(onDataAvailable:)];
将 self 传入 updateOp 方法,同时self又持有对 updateOp 的引用,这样就造成了循环引用。
解决方案即使用代理:
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[Person alloc] init];
self.person.delegate = self;
[self.person start];
// Do any additional setup after loading the view, typically from a nib.
}
- (void)doSomethingWithName:(NSString *)name
{
NSLog(@"hello %@",name);
}
- (void)start
{
if (self.delegate && [self.delegate respondsToSelector:@selector(doSomethingWithName:)]) {
[self.delegate doSomethingWithName:self.name];
}
}
block
简而言之 block 使用__weak typeof(self) weakSelf = self
语句声明 weak 类型的 weakSelf 变量,然后在 block 内部使用 weakSelf 避免循环引用。
NSTimer
NSTimer 也是造成循环引用很多的一个地方。
self.timer = [NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(countdown:) userInfo:nil repeats:NO];
可以看到在 self 持有 timer 的同时,timer 也持有 self。在这种情况下 self 对应的类的 dealloc 方法是不会走的,因此如果在 dealloc 里调用 timer 的 invalidate 是没有用的。
因此可以采用以下两种方式:
- (void)backBtnClick
{
[self.timer invalidate];
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)didMoveToParentViewController:(UIViewController *)parent{
if (parent == nil) {
[self.timer invalidate];
}
}
通过拦截返回按钮,或重写 didMoveToParentViewController 在离开其父视图时手动调用。
更好的解决方案:
#import "CPTimerTask.h"
@interface CPTimerTask()
@property (nonatomic, strong) NSTimer *timer;
@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL selector;
@end
@implementation CPTimerTask
- (instancetype)initWithTimeInterval:(NSTimeInterval)interval target:(id)target selector:(SEL)selector
{
if (self = [super init]) {
self.target = target;
self.selector = selector;
self.timer = [NSTimer scheduledTimerWithTimeInterval:interval target:self selector:@selector(update:) userInfo:nil repeats:YES];
}
return self;
}
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
- (void)update:(NSTimer *)timer
{
NSObject *obj = [[NSObject alloc] init];
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
__strong typeof(self) strongSelf = weakSelf;
if (!strongSelf) {
return ;
}
if (!strongSelf.target) {
return;
}
if ([strongSelf.target respondsToSelector:strongSelf.selector]) {
[strongSelf.target performSelector:strongSelf.selector withObject:obj];
}
});
}
- (void)shutDown
{
[self.timer invalidate];
self.timer = nil;
}
八.单例与全局变量
全局变量。
- static 修饰的是私有全局变量,只用于本文件。
- extern 修饰的是全局变量,用于整个工程。
- const 修改不可变的常量,可以用在 static 和 extern 中,注意放在 NSString * 后修饰字符串,而不是修饰地址。
合理的使用全局变量要满足以下条件:
- 没有被其他对象所持有
- 不是常量
- 整个应用只有一个,而不是每个组件
单例
单例是长寿对象的典型——只创建一次,从不销毁。日志器是典型的单例。
单例极为有用,应该在以下情形中使用单例:
- 队列操作(如日志埋点)
- 访问共享资源(如缓存)
- 资源池(如线程池和链接池)
注意:尽可能避免使用单例
单例通常会在启动时初始话,会增加启动时间。
单例会在运行过程中不被销毁,占用内存。
九.内存问题
1.SDWebImage在加载大图时会造成内存爆表。
在SDWebImage中大图禁用解压缩,可以防止内存暴涨:
[[SDImageCache sharedImageCache]setShouldDecompressImages:NO];
[[SDWebImageDownloader sharedDownloader]setShouldDecompressImages:NO];
2.UIWebview使用时内存暴涨
改用WKWebView后发现内容显示偏小,根本原因为:HTML5缺少meta标签,可以让后台添加完整的HTML5标签,也可以在客户端添加代码调整
NSString *jScript = @"var meta = document.createElement('meta'); meta.setAttribute('name', 'viewport'); meta.setAttribute('content', 'width=device-width'); document.getElementsByTagName('head')[0].appendChild(meta);";
WKUserScript *wkUScript = [[WKUserScript alloc] initWithSource:jScript injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
WKUserContentController *wkUController = [[WKUserContentController alloc] init];
[wkUController addUserScript:wkUScript];
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
config.userContentController = wkUController;
3.频繁刷新UI
- 能用用方法设置的尽量不要去刷新整个控件
- 能不用属性的尽量不要用属性(例如我当前就是用到属性的设置的)
毕竟多一个属性也是多一份的消耗的 - 只是修改单个控件内容 建议提供单个方法进行修改
4.drawRect会造成内存飙升
画板视图的-drawRect:方法的背后实际上都是底层的CALayer进行了重绘和保存中间产生的图片,CALayer的delegate属性默认实现了CALayerDelegate协议,当它需要内容信息的时候会调用协议中的方法来拿。当画板视图重绘时,因为它的支持图层CALayer的代理就是画板视图本身,所以支持图层会请求画板视图给它一个寄宿图来显示,生成的空寄宿图内存是相当巨大的,它就是本次内存问题的关键。
解决:求直接用专有图层CAShapeLayer。
5.tableview里make约束
实际上,多次make重复的约束,不仅会让内存提高,还会导致约束计算过程复杂化,也会更加地耗时。
将约束代码移到cell创建的地方,内存飙升消失。
6.tableview读data图片
将图片使用UIIMageGraph重新绘制,生成一个很小但是够用可以用来显示的小图片,强引用这张小图片,以后在调用,直接使用存在内存中的小图片。
总结
- 避免大量使用单例,不要出现上帝对象。
- 对子对象使用 __strong
- 对父对象使用 __weak
- 对会造成循环引用的对象使用 __weak
- 对值属性(NSInterger、SEL、CGFloat 等),使用 assign 限定符
- 对于 block 属性,使用 copy 限定符。
- 对于 NSError 参数的方法使用
NSError * __autoreleasting *
- 对于 block 引用外部变量,在外部将他们 weakify,并在内部使其 strongify 。
- 注意销毁计时器、移除观察者和解除回调。