【OC内存管理】实际开发需注意

目录
一、定时器的循环引用
二、copymutableCopy
三、创建大量autorelease对象时,最好自己创建一个@autoreleasepool {}
四、其它一些注意


一、定时器的循环引用


我们只以NSTimer举例好了,CADisplayLink会遇见同样的问题,解决方案也是一样的。

1、NSTimer的循环引用

使用NSTimer,写法通常如下:

-----------ViewController.m-----------

#import "ViewController.h"
#import "ViewController1.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    
    ViewController1 *vc = [[ViewController1 alloc] init];
    [self.navigationController pushViewController:vc animated:YES];
}

@end
-----------ViewController1.m-----------

#import "ViewController1.h"

@interface ViewController1 ()

@property (nonatomic, strong) NSTimer *timer;

@end

@implementation ViewController1

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 创建timer
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(test) userInfo:nil repeats:YES];
}

- (void)test {
    
    NSLog(@"11");
}

- (void)dealloc {
    
    NSLog(@"%s", __func__);
    
    // 退出界面时,使timer失效从而销毁
    [self.timer invalidate];
}

@end

运行,点击ViewController进入ViewController1timer跑起来了,1秒钟打印一次“11”。此时我们点击返回按钮,返回ViewController,按理说ViewController1应该销毁,走dealloc方法,timer也跟着失效从而销毁,但实际上ViewController1没有销毁,没走dealloc方法,timer也还一直跑着,这是因为timerViewController1形成了循环引用导致的内存泄漏。

查看timer的创建方法,可以知道:timer会强引用target也就是说timer确实强引用着ViewController1

ViewController1又强引用着timer

那怎么打破NSTimer的循环引用呢?我们知道__weak是专门用来打破循环引用的,那它是不是也能打破NSTimer的循环引用?

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 尝试用__weak打破NSTimer的循环引用
    __weak typeof(self) weakSelf = self;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:weakSelf selector:@selector(test) userInfo:nil repeats:YES];
}

运行,发现没有效果,那为什么__weak不能打破NSTimer的循环引用?毫无疑问__weak的确是把self搞成了弱指针,但因为NSTimer内部有一个强指针类型的target变量

@property (nonatomic, strong) id target;

来接收这个传进来的地址值,所以无论你外界是传进来强指针还是弱指针,它内部都是一个强指针接收,就总是会强引用target,所以用__weak不能打破NSTimer的循环引用。

那再试试另一条引用线吧,让ViewController1弱引用timer

@interface ViewController1 ()

// 尝试用weak修饰timer来打破NSTimer的循环引用
@property (nonatomic, weak) NSTimer *timer;

@end

运行,发现没有效果,奇了怪了,怎么回事呢?查看官方对NSTimer的说明,可以知道:timer添加到RunLoop之后,RunLoop会强引用timer,并且建议我们不必自己强引用timer,而解除RunLoop对timer强引用的唯一方式就是调用timerinvalidate方法使timer失效从而销毁。

也就是说,实际的引用关系如下:

所以我们使用weak修饰timer是正确的,但这还是不能打破NSTimer的循环引用——更准确地说,这可以解决NSTimer的循环引用,但还是没有解决NSTimer内存泄漏的问题。因为[self.timer invalidate]的调用——即timer的销毁——最好就是发生在ViewController1销毁时,而ViewController1要想销毁就必须得timer先销毁,还是内存泄漏。

倒腾来倒腾去,还是得timer强引用target这条引用线下手,把它搞成弱引用,__weak不起作用,那我们想想别的方案呗。

2、打破NSTimer的循环引用

  • 方案一:使用block的方式创建timer
- (void)viewDidLoad {
    [super viewDidLoad];

    __weak typeof(self) weakSelf = self;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        
        [weakSelf test];
    }];
}

为什么能解决呢?因为此时timer是强引用block的,而__weak可以打破block的循环引用,所以block是弱引用self的,所以最终的效果就类似于timer弱引用self。解决是能解决,但用这种方式创建timer要iOS10.0以后才能使用。

  • 方案二:创建一个中间对象——代理

我们可以把方案一的思路自己实现一下嘛,即创建一个中间对象(方案一的中间对象就是block嘛),把这个中间对象作为timertarget参数传进去,让timer强引用这个中间对象,而让这个中间对象弱引用ViewController1,这样ViewController1就能正常释放,NSTimer就能正常调用失效方法,RunLoop就能正常解除对NSTimer的强引用,NSTimer就能正常解除对中间对象的强引用,内存泄漏就解决了。当然由于中间对象没有target——即ViewController1——的方法,所以我们还要做一步消息转发。

-----------INETimerProxy.h-----------

#import <Foundation/Foundation.h>

@interface INETimerProxy : NSObject

+ (instancetype)proxyWithTarget:(id)target;

@end


-----------INETimerProxy.m-----------

#import "INETimerProxy.h"

@interface INETimerProxy ()

/// 弱引用target所指向的对象
@property (nonatomic, weak) id target;

@end

@implementation INETimerProxy

+ (instancetype)proxyWithTarget:(id)target {
    
    INETimerProxy *proxy = [[INETimerProxy alloc] init];
    proxy.target = target;
    return proxy;
}

// 直接消息转发
- (id)forwardingTargetForSelector:(SEL)aSelector {
    
    return self.target;
}

@end
-----------ViewController1.m-----------

#import "INETimerProxy.h"

- (void)viewDidLoad {
    [super viewDidLoad];

    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:[INETimerProxy proxyWithTarget:self] selector:@selector(test) userInfo:nil repeats:YES];
}

为了提高消息转发效率,我们可以让代理直接继承自NSProxy,而不是NSObjectNSProxy是专门用来做消息转发的,继承自NSObject的类调用方法时会走方法查找 --> 动态方法解析 --> 直接消息转发、完整消息转发这套流程,而继承自NSProxy的类调用方法时只会走方法查找 --> 完整消息转发这两个流程,消息转发效率更高,所以以后但凡要做消息转发就直接继承自NSProxy好了,而不是NSObject

-----------INETimerProxy.h-----------

#import <Foundation/Foundation.h>

@interface INETimerProxy : NSProxy

+ (instancetype)proxyWithTarget:(id)target;

@end


-----------INETimerProxy.m-----------

#import "INETimerProxy.h"

@interface INETimerProxy ()

/// 弱引用target所指向的对象
@property (nonatomic, weak) id target;

@end

@implementation INETimerProxy

+ (instancetype)proxyWithTarget:(id)target {
    
    // NSProxy类是没有init方法的,alloc后就可以直接使用
    INETimerProxy *proxy = [INETimerProxy alloc];
    proxy.target = target;
    return proxy;
}

// 完整消息转发
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    
    return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    
    [invocation invokeWithTarget:self.target];
}

@end


二、copymutableCopy


1、深拷贝与浅拷贝

  • 深拷贝,是指内容拷贝,会产生新的对象,新对象的引用计数为1;浅拷贝,是指指针拷贝,不会产生新的对象,旧对象的引用计数加1,浅拷贝其实就是retain,深拷贝的话新对象和旧对象互不影响,浅拷贝的话改变一个另一个也跟着变了。

  • 只有不可变对象的不可变拷贝是浅拷贝,其它的都是深拷贝。

- (void)viewDidLoad {
    [super viewDidLoad];
  
    NSString *str1 = @"11";
    NSString *str2 = [str1 copy]; // 不可变对象的不可变拷贝 --> 浅拷贝
    NSMutableString *str3 = [str1 mutableCopy]; // 深拷贝
    NSLog(@"%p %p %p", str1, str2, str3);
    
    NSMutableString *str4 = [@"11" mutableCopy];
    NSString *str5 = [str4 copy]; // 深拷贝
    NSMutableString *str6 = [str4 mutableCopy]; // 深拷贝
    NSLog(@"%p %p %p", str4, str5, str6);
}


控制台打印:
0x1025260b0 0x1025260b0 0x600003bc0ab0
0x600003b992c0 0xc91f17b5d8b748d0 0x600003b99890

2、不可变属性最好用copy修饰,而可变属性坚决不能用copy修饰、只能用strongretain修饰

copy拷贝出来的东西是不可变对象,是不能修改的;mutableCopy拷贝出来的东西是可变对象,是能修改的。

  • 不可变属性最好用copy修饰

不可变属性最好用copy修饰,因为用strongretain修饰的话,setter方法内部仅仅是retain,那当我们把一个可变对象赋值给这个不可变属性时,不可变属性仅仅是指针指向了可变对象,修改可变对象的值,也就是不可变属性指向对象的值就发生了变化,这不是我们希望看到的,我们直观的感觉应该是“不可变属性指向的对象应该不随着别人改变它而改变才对”。

@interface ViewController ()

@property (nonatomic, strong) NSString *name;
//@property (nonatomic, retain) NSString *name;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
  
    // 可变对象
    NSMutableString *mutableName = [@"张三" mutableCopy];
    // 可变对象赋值给这个不可变属性
    self.name = mutableName;
    NSLog(@"%@", self.name); // 张三
    
    // 修改可变对象的值
    [mutableName appendString:@"丰"];
    NSLog(@"%@", self.name); // 张三丰,不可变属性的值也会跟着变化,这不是我们希望看到的
}

@end

而用copy修饰的话,setter方法内部就是copy,那不管你外界传给它一个可变对象还是不可变对象,该属性最终都是深拷贝出一份不可变的,这样外界就无法影响这个属性的值,除非我们主动修改属性的值,符合我们的预期。

@interface ViewController ()

@property (nonatomic, copy) NSString *name;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
  
    // 可变对象
    NSMutableString *mutableName = [@"张三" mutableCopy];
    // 可变对象赋值给这个不可变属性
    self.name = mutableName;
    NSLog(@"%@", self.name); // 张三
    
    // 修改可变对象的值
    [mutableName appendString:@"丰"];
    NSLog(@"%@", self.name); // 张三,外界无法影响这个属性的值
    
    self.name = @"张三丰";
    NSLog(@"%@", self.name); // 张三丰,我们主动修改属性的值,符合我们的预期
}

@end
  • 而可变属性坚决不能用copy修饰、只能用strongretain修饰

可变属性坚决不能用copy修饰,只能用strongretain修饰,和上面是同样的道理,copy修饰的属性最终在setter方法里copy出来的是一份不可变的,如果你非要用它修饰可变属性,那从外在看来好像可以修改这个属性,结果你一修改就崩溃,因为找不到方法。

@interface ViewController ()

@property (nonatomic, copy) NSMutableString *name;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
  
    self.name = [@"张三" mutableCopy];
    [self.name appendString:@"丰"]; // 一修改,就崩溃,因为NSString根本没有appendString:方法
}

@end


三、创建大量autorelease对象时,最好自己创建一个@autoreleasepool {...}


只要不是用allocnewcopymutableCopy方法创建的对象,而是用类方法创建的对象,方法内部都调用了autorelease,都是autorelease对象,例如:

NSString *str = [NSString string];
NSArray *arr = [NSArray array];
NSDictionary *dict = [NSDictionary dictionary];
UIImage *image = [UIImage imageNamed:@"11"];

因为类方法的内部实现大概如下:

- (id)object {
    
    id obj = [[NSObject alloc] init];
    [obj autorelease];
    
    return obj;
}

allocnewcopymutableCopy方法的内部实现大概如下:

- (id)allocObject {
    
    id obj = [[NSObject alloc] init];
    
    return obj;
}

所以在创建大量autorelease对象时,最好自己创建一个@autoreleasepool {...}。因为如果主线程RunLoop的某次循环一直忙着处理事情,线程没有休眠或退出,那本次循环的autoreleasepool就迟迟无法销毁,这就导致这次循环里的autorelease对象也迟迟无法及时释放掉,因此就有可能导致内存使用峰值过高,从而导致内存溢出。而自己创建@autoreleasepool {...}后,每一次for循环就都会出一次@autoreleasepool {...}的作用域而销毁一波autorelease对象,这就可以降低内存使用的峰值。

for (int i = 0; i < 100000; i ++) {
    
    @autoreleasepool {
        
        NSString *string = [NSString stringWithFormat:@"%d", i];
        NSLog(@"%@", string);
    }
}

autoreleasepool的实现原理简述:autoreleasepool其实也是一个对象,它在创建后,内部会有一堆AutoReleasePoolPage对象,这一堆AutoReleasePoolPage对象是通过双向链表组织起来的——即AutoReleasePoolPage对象1的child属性指向AutoReleasePoolPage对象2,AutoReleasePoolPage对象2的child属性指向AutoReleasePoolPage对象3,而AutoReleasePoolPage对象3的parent属性指向AutoReleasePoolPage对象2,AutoReleasePoolPage对象2的parent属性指向AutoReleasePoolPage对象1,这样通过child属性和parent两个属性关联起来的双向数据结构就是双向链表,而每一个AutoReleasePoolPage对象内部都有4040个字节用来存放autorelease对象的内存地址,如果项目里一个AutoReleasePoolPage对象存不下所有的autorelease对象的内存地址,那autoreleasepool在创建的时候就会创建两个AutoReleasePoolPage对象,依次类推,然后当autoreleasepool销毁时就会去AutoReleasePoolPage对象里找到这些对象的地址将它们的引用计数都做一次减1操作。


四、其它一些注意


注意代理不要出现循环引用,block不要出现循环引用,KVO和通知要在dealloc的时候释放等。

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