历史
苹果在 2011 年的时候,在 WWDC 大会上提出了自动的引用计数(ARC)。ARC 背后的原理是依赖编译器的静态分析能力,通过在编译时找出合理的插入引用计数管理代码.
2014 年的 WWDC 大会上,苹果推出了 Swift 语言,而该语言仍然使用 ARC 技术,作为其内存管理方式。
1.引用计数
1.1什么是引用计数
引用计数(Reference Count)是一个简单而有效的管理对象生命周期的方式。当我们创建一个新对象的时候,它的引用计数为 1,当有一个新的指针指向这个对象时,我们将其引用计数加 1,当某个指针不再指向这个对象是,我们将其引用计数减 1,当对象的引用计数变为 0 时,说明这个对象不再被任何指针指向了,这个时候我们就可以将对象销毁,回收内存。
引用计数
2.ARC的内存管理
ARC 能够解决 iOS 开发中 90% 的内存管理问题,但是另外还有 10% 内存管理,是需要开发者自己处理的,这主要就是与底层 Core Foundation 对象交互的那部分,底层的 Core Foundation 对象由于不在 ARC 的管理下,所以需要自己维护这些对象的引用计数。
内存管理问题
1.循环引用(block,代理)
2.Core Foundation 对象需要手动管理计数器
2.1 循环引用
引用计数这种管理内存的方式虽然使开发变得简便,但是也有瑕疵,那就是不能很好解决循环引用.
循环引用
上图所示:对象A和对象B 相互引用成为成员变量,,只有当对象A销毁时,才会对对象A中所以成员变量引用计数-1.因为对象A的销毁依赖于对象B的销毁,对象B的销毁依赖于对象A的销毁.那么就造成了循环引用。即使这两个对象在在其他地方已经没有任何指针调用它们,它们依旧不会释放.
上述问题不单只是在两个对象之间出现,只有是多个对象中出现了相互持有引用的情况下,都会出现循环引用
解决方法:
-
主动断开循环引用
因为是相互引用造成的循环引用,那么只要在 不再需要使用这个对象时,将其主动断开引用
主动断开
代码示例
@property (nonatomic,copy) void(^testBlock)(void);
- (void)viewDidLoad {
[super viewDidLoad];
self.testBlock = ^{
//当你在快中主动调用self时 Xcode会提示你 出现循环引用 这里只是为了测试
NSLog(@"%@",self);
};
self.testBlock();
}
- (void)viewWillDisappear:(BOOL)animated{
[super viewWillDisappear:animated];
self.testBlock = nil;
NSLog(@"viewWillDisappear");
}
- (void)dealloc {
NSLog(@"TestViewController -- dealloc");
}
上述代码中 在 viewDidLoad
中 self引用 testBlock 作为成员变量 , testBlock 值中调用self, 那么就造成了循环引用. 所以我们假设在 viewWillDisappear 中不再需要使用 testBlock 所以将值设为nil, 那么testBlock 中就不再调用self了, 所以最后self走了dealloc方法
主动断开循环引用这种操作依赖开发中手动操作,这样感觉像回到了 MRC年代 谁创建谁释放 的年代 ,依赖于开发者自己知道什么时候不再需要,主动断开. 所以这种方法不常用.
-
使用弱引用
弱引用虽然持有对象,但是并不增加引用计数,这样就避免了循环引用的产生。在 iOS 开发中,弱引用通常在 delegate 模式中使用。
弱引用
声明属性时将其修饰为弱引用
@property (nonatomic,weak) id<TestDelegate> delegate;
如果block中调用到相互持有的对象 那么将其弱引用 就不会造成循环引用
__weak typeof(self) weakSelf = self;
self.testBlock = ^{
NSLog(@"%@",weakSelf);
};
self.testBlock();
弱引用的实现原理
弱引用的实现原理是这样,系统对于每一个有弱引用的对象,都维护一个表来记录它所有的弱引用的指针地址。这样,当一个对象的引用计数为 0 时,系统就通过这张表,找到所有的弱引用指针,继而把它们都置成 nil。
从这个原理中,我们可以看出,弱引用的使用是有额外的开销的。虽然这个开销很小,但是如果一个地方我们肯定它不需要弱引用的特性,就不应该盲目使用弱引用。举个例子,有人喜欢在手写界面的时候,将所有界面元素都设置成 weak 的,这某种程度上与 Xcode 通过 Storyboard 拖拽生成的新变量是一致的。但是我个人认为这样做并不太合适。因为:
- 我们在创建这个对象时,需要注意临时使用一个强引用持有它,否则因为 weak 变量并不持有对象,就会造成一个对象刚被创建就销毁掉。
- 大部分 ViewController 的视图对象的生命周期与 ViewController 本身是一致的,没有必要额外做这个事情。
- 早先苹果这么设计,是有历史原因的。在早年,当时系统收到 Memory Warning 的时候,ViewController 的 View 会被 unLoad 掉。这个时候,使用 weak 的视图变量是有用的,可以保持这些内存被回收。但是这个设计已经被废弃了,替代方案是将相关视图的 CALayer 对应的 CABackingStore 类型的内存区会被标记成 volatile 类型,详见《再见,viewDidUnload方法》。
2.2 Core Foundation 对象需要手动管理计数器
一般来说,以CF开头的系统API都是 CoreFoundation框架的
手动管理,说白了,就是谁创建,谁销毁
创建两个CF对象
// 创建一个 CFStringRef 对象
CFStringRef strRef= CFStringCreateWithCString(kCFAllocatorDefault, “hello world", kCFStringEncodingUTF8);
// 创建一个 CTFontRef 对象
CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);
创建完后 两个对象引用计数器 = 1
当这两个对象不需要再使用时,那么就需要调用 CFRelease 来使计数器-1
CFRelease(strRef);
CFRelease(fontRef);
手动管理计数器
所以对于底层 Core Foundation 对象,我们只需要延续以前手工管理引用计数的办法即可。
其实Core 开头的框架 很多有会 retain 和 release 方法, 所以我们一般在使用结束后,将创建出来的core对象release一次.
除此之外,还有另外一个问题需要解决。在 ARC 下,我们有时需要将一个 Core Foundation 对象转换成一个 Objective-C 对象,这个时候我们需要告诉编译器,转换过程中的引用计数需要做如何的调整。这就引入了bridge相关的关键字,以下是这些关键字的说明:
- __bridge: 只做类型转换,不修改相关对象的引用计数,原来的 Core Foundation 对象在不用-时,需要调用 CFRelease 方法。
- __bridge_retained:类型转换后,将相关对象的引用计数加 1,原来的 Core Foundation 对象在不用时,需要调用 CFRelease 方法。
- __bridge_transfer:类型转换后,将该对象的引用计数交给 ARC 管理,Core Foundation 对象在不用时,不再需要调用 CFRelease 方法。
我们根据具体的业务逻辑,合理使用上面的 3 种转换关键字,就可以解决 Core Foundation 对象与 Objective-C 对象相对转换的问题了。
使用 Xcode 检测循环引用
在Xcode的的菜单栏中选择 :Product -> Profile,然后选择 “Leaks”,再点击右下角的”Choose” 按钮开始检测。如下图
Leaks
这个时候 iOS 模拟器会运行起来,我们在模拟器里进行一些界面的切换操作。稍等几秒钟,就可以看到 Instruments 检测到了我们的这次循环引用。Instruments 中会用一条红色的条来表示一次内存泄漏的产生。
切换到 Leaks 这栏,点击”Cycles & Roots”,就可以看到以图形方式显示出来的循环引用。这样我们就可以非常方便地找到循环引用的对象了
代码截图
如果出现下图的情况 那么说明Profile中的Build Configuration 选的不是Debug
Profile错误 修改 Profile中的Build Configuration
然后重启Xcode,重新 Product -> Profile 就可以了,还不行就重启电脑.
总结
在ARC的环境下,iOS开发在内存管理方面的工作大部分都不用手动来实现了,但是我认为,我们还是需要去理解引用计数这种内存管理方式,注意循环引用的问题.
学会使用 Instruments 工具来调试项目
最后,愿大家共同进步.
网友评论