iOS 编写高质量Objective-C代码(五)
《编写高质量OC代码》已经顺利完成一二三四五篇!
附上链接:
iOS 编写高质量Objective-C代码(一)
iOS 编写高质量Objective-C代码(二)
iOS 编写高质量Objective-C代码(三)
iOS 编写高质量Objective-C代码(四)
iOS 编写高质量Objective-C代码(五)
本篇的主题是iOS中的 “内存管理机制”。
说到iOS内存管理,逃不过iOS的两种内存管理机制:MRC & ARC。
先简单介绍一下:
MRC(manual reference counting): “手动引用计数” ,由开发者管理内存。
ARC(automatic reference counting):“自动引用计数”,从iOS 5
开始支持, ,由编译器帮忙管理内存。
苹果引入ARC机制的原因猜测:
iOS 4
之前,所有iOS开发者必须要手动管理内存,即手动管理对象的内存分配和释放。首先,不断插入retain
、release
等内存管理语句,大大加大了工作量和代码量。其次,在面对一些多线程并发操作时,开发者手动管理内存并不简单,还可能会带来很多无法预知的问题。
所以,苹果从iOS 5
开始引入ARC机制,由编译器帮忙管理内存。在编译期,编译器会自动加上内存管理语句。这样,开发者可以更加关注业务逻辑。
下面进入正题:编写高质量Objective-C代码(五)——内存管理篇。
一、理解引用计数
- 引用计数工作原理:
这里引入《Objective-C 高级编程 iOS与OSX多线程和内存管理》这本书的例子:
很经典的图解:
解释:
1.开灯:引申为:“ 创建对象 ”。
2.关灯:引申为:“ 销毁对象 ”。
解释:
1.有人来上班打卡了:开灯。——(创建对象,计数为1)
2.又有人来了:保持开灯。——(保持对象,计数为2)
3.又有人来了:保持开灯。——(保持对象,计数为3)
4.有人下班打卡了:保持开灯。——(保持对象,计数为2)
5.又有人下班了:保持开灯。——(保持对象,计数为1)
6.所有员工全下班了:关灯。——(销毁对象,计数为0)
场景 | 对应OC的动作 | 对应OC的方法 |
---|---|---|
上班开灯 | 生成对象 | alloc/new/copy/mutableCopy等 |
需要照明 | 持有对象 | retain |
不需要照明 | 解除持有 | release |
下班关灯 | 销毁对象 | dealloc |
如果觉得本书中的例子说的有点抽象难懂,没关系,请看下面图解示例:
~提示:实箭头为强引用,虚箭头为弱引用。~
- 属性存取方法中的内存管理:
这里有个set方法的例子:
- (void)setObject:(id)object {[object retain];// Added by ARC[_object release];// Added by ARC_object = object;
}
解释:set方法将保留新值,释放旧值,然后更新实例变量。这三个语句的顺序很重要。
如果先release
再retain
。那么该对象可能已经被回收,此时retain
操作无效,因为对象已释放。这时实例变量就变成了悬挂指针。~悬挂指针:指针指nil的指针。~
- 自动释放池:
细心的同学会发现,在我们写iOS程序时,main
函数里就有一个autoreleasepool
(自动释放池)。
int main(int argc, char * argv[]) {@autoreleasepool {return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));}
}
autorelease
能延长对象的生命周期,在对象跨越“方法调用边界”后(就是}
后)依然可以存活一段时间。
- 循环引用:
循环引用(retain cycle
)又称为“保留环”。
形成循环引用的原因:是对象之间互相通过强指针指向对方(或者说互相强持有对方)。
在开发中,我们不希望出现循环引用,因为会造成内存泄漏。
解决方案:有一方使用弱引用(weak reference
),解开循环引用,让多个对象都可以释放。
PS:关于如何检验项目中有无内存泄漏:参考这篇博客。
二、以ARC简化引用计数
,在ARC环境下,禁止调用:retain
、release
、autorelease
、dealloc
方法。
- 使用ARC时必须遵循的方法命名规则:
若方法名以alloc
、new
、copy
、mutableCopy
开头,则规定返回的对象归调用者。 - 变量的内存管理语义:
对比一下MRC和ARC在代码上的区别
MRC环境下:
- (void)setObject:(id)object {[_object release];_object = [object retain];
}
这样会出现一种边界情况,如果新值和旧值是同一个对象,那么会先释放掉,object就变成悬挂指针。
ARC环境下:
- (void)setObject:(id)object {_object = object;
}
ARC会用一种更安全的方式解决边界问题:先保留新值,再释放旧值,最后更新实例变量。
同时,ARC可以通过修饰符来改变局部变量和实例变量的语义:
修饰符 | 语义 |
---|---|
__strong | 默认,强持有,保留此值。 |
__weak | 不保留此值,安全。对象释放后,指针置nil。 |
__unsafe_unretained | 不保留此值,不安全。对象释放后,指针依然指向原地址(即不置nil)。 |
__autoreleasing | 此值在方法返回时自动释放。 |
- ARC如何清理实例变量:
MRC中,开发者需要在dealloc
中动插入必要的清理代码(cleanup code)。
而ARC会借用Objective-C++
的一项特性来完成清理任务,回收OC++对象时,会调用C++的析构函数:底层走.cxx_destruct
方法。而当释放OC对象时,ARC在.cxx_destruct
底层方法中添加所需要的清理代码(这个方法底层的某个时机会调用dealloc
方法)。
不过如果有非OC的对象,还是要重写dealloc
方法。比如CoreFoundation
中的对象或是malloc()
分配在堆中的内存依然需要清理。这时要适时调用CFRetain
/CFRelease
。
- (void)dealloc {CFRelease(_coreFoundationObject);free(_heapAllocatedMemoryBlob);
}
三、dealloc方法中只释放引用并解除监听
调用dealloc
方法时,对象已经处于回收状态了。这时不能调用其他方法,尤其是异步执行某些任务又要回调的方法。如果异步执行完回调的时候对象已经摧毁,会直接crash。
dealloc
方法里要做些释放相关的事情,比如:
- 释放指向其他对象的引用。
- 取消订阅KVO。
- 取消NSNotificationCenter通知。
举个例子:
- KVO:
- (void)viewDidLoad {//....[webView addObserver:self forKeyPath:@"canGoBack" options:NSKeyValueObservingOptionNew context:nil];[webView addObserver:self forKeyPath:@"canGoForward" options:NSKeyValueObservingOptionNew context:nil];[webView addObserver:self forKeyPath:@"title" options:NSKeyValueObservingOptionNew context:nil];[webView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew context:nil];
}#pragma mark - KVO- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{self.backItem.enabled = self.webView.canGoBack;self.forwardItem.enabled = self.webView.canGoForward;self.title = self.webView.title;self.progressView.progress = self.webView.estimatedProgress;self.progressView.hidden = self.webView.estimatedProgress>=1;
}- (void)dealloc {[self.webView removeObserver:self forKeyPath:@"canGoBack"];//< 移除KVO[self.webView removeObserver:self forKeyPath:@"canGoForward"];[self.webView removeObserver:self forKeyPath:@"title"];[self.webView removeObserver:self forKeyPath:@"estimatedProgress"];
}
- NSNotificationCenter:
- (void)viewDidLoad {//......// 添加响应通知[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(tabBarBtnRepeatClick) name:BQTabBarButtonDidRepeatClickNotification object:nil];[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(titleBtnRepeatClick) name:BQTitleButtonDidRepeatClickNotification object:nil];
}// 移除通知
- (void)dealloc {// [[NSNotificationCenter defaultCenter] removeObserver:self name:BQTabBarButtonDidRepeatClickNotification object:nil];
// [[NSNotificationCenter defaultCenter] removeObserver:self name:BQTitleButtonDidRepeatClickNotification object:nil];// 或者使用一个语句全部移除[[NSNotificationCenter defaultCenter] removeObserver:self];
}
四、编写“ 异常安全代码 ”时留意内存管理问题
异常只应在发生严重错误后抛出。
用的不好会造成内存泄漏:在try
块中,如果先保留了某个对象,然后在释放它之前又抛出了异常,那么除非catch块能解决问题,否则对象所占内存就会泄漏。
原因:C++
的析构函数由Objective-C
的异常处理例程来运行。由于抛出异常会缩短生命期,所以发生异常时必须析构,不然就内存泄漏,而这时如果文件句柄(file handle)等系统资源没有正确清理,就会发生内存泄漏。
- 捕获异常时,一定要将
try
块内所创立的对象清理干净。 - ARC下,编译器默认不生成安全处理异常所需的清理代码。如要开启,请手动打开:
-fobjc-arc-exceptions
标志。但很影响性能。所以建议最好还是不要用。但有种情况是可以使用的:Objective-C++
模式。
PS:在运行期系统,C++
与Objective-C
的异常互相兼容。也就是说其中任一语言抛出的异常,能用另一语言所编的“异常处理程序”捕获。而在编写Objective-C++
代码时,C++处理异常所用的代码与ARC实现的附加代码类似,编译器自动打开-fobjc-arc-exceptions
标志,其性能损失不大。
最后,还是建议:
- 异常只用于处理严重的错误(fatal error,致命错误)
对于一些不那么严重的错误(nonfatal error,非致命错误),有两种解决方案:
- 让对象返回
nil
或者0
(例如:初始化的参数不合法,方法返回nil或0) - 使用
NSError
- 让对象返回
五、以弱引用避免循环引用(避免内存泄漏)
这条比较简单,内容主旨就是标题:以弱引用避免循环引用(Retain Cycle)
- 为了避免因循环引用而造成内存泄漏。这时,某些引用需要设置为弱引用(
weak
)。 - 使用弱引用
weak
,ARC下,对象释放时,指针会置nil
。
六、以 “自动释放池块” 降低内存峰值
- 默认情况下:自动释放池需要等待线程执行下一次事件循环时才清空,通常for循环会不断创建新对象加入自动释放池里,循环结束才释放。因此,可能会占用大量内存。
- 手动加入自动释放池块(
@autoreleasepool
):每次for循环都会直接释放内存,从而降低了内存的峰值。
尤其,在遍历处理一些大数组或者大字典的时候,可以使用自动释放池来降低内存峰值,例如:
NSArray *qiShare = /*一个很大的数组*/
NSMutableArray *qiShareMembersArray = [NSMutableArray new];
for (NSStirng *name in qiShare) {@autoreleasepool {QiShareMember *member = [QiShareMember alloc] initWithName:name];[qiShareMembersArray addObject:member];}
}
PS:自动释放池的原理:排布在“栈”中,对象执行autorelease消息后,系统将其放入最顶端的池里(进栈),而清空自动释放池就是把对象销毁(出栈)。而调用出栈的时机:就是当前线程执行下一次事件循环时。
七、用 “僵尸对象” 调试内存管理问题
如上图,勾选这里可以开启僵尸对象设置。开启之后,系统在回收对象时,不将其真正的回收,而是把它的isa指针
指向特殊的僵尸类(zombie class),变成僵尸对象。僵尸类能够响应所有的选择子,响应方式为:打印一条包含消息内容以及其接收者的消息,然后终止应用程序。
僵尸对象简单原理:在Objective-C的运行期程序库、Foundation框架以及CoreFoundation框架的底层加入了实现代码。在系统即将回收对象时,通过一个环境变量
NSZombieEnabled
识别是僵尸对象——不彻底回收,isa
指针指向僵尸类并且响应所有选择子。
八、不要使用retainCount
在苹果引入ARC之后retainCount已经正式废弃,任何时候都没法调用这个retainCount方法来查看引用计数了,因为这个值实际上已经没有准确性了(而且在ARC环境下也调用不了)。但是在MRC下还是可以正常使用的。
最后,特别致谢:《Effective Objective-C 2.0》第五章。
iOS 编写高质量Objective-C代码(五)相关推荐
- iOS 编写高质量Objective-C代码(六)
级别: ★★☆☆☆ 标签:「iOS」「Block」「Objective-C」 作者: MrLiuQ 审校: QiShare团队 前言: 这几篇文章是小编在钻研<Effective Objecti ...
- iOS 编写高质量Objective-C代码(八)
前言: 这几篇文章是小编在钻研<Effective Objective-C 2.0>的知识产出,其中包含作者和小编的观点,以及小编整理的一些demo.希望能帮助大家以简洁的文字快速领悟原作 ...
- HTML Inspector – 帮助你编写高质量的 HTML 代码
HTML Inspector 是一款代码质量检测工具,帮助你编写更优秀的 HTML 代码.HTML Inspector 使用 JavaScript 编写,运行在浏览器中,是最好的 HTML 代码检测工 ...
- 如何编写高质量的C#代码(一)
如何编写高质量的C#代码(一) 从"整洁代码"谈起 一千个读者,就有一千个哈姆雷特,代码质量也同样如此. 想必每一个对于代码有追求的开发者,对于"高质量"这个词 ...
- 如何编写高质量的C#代码(一)
从"整洁代码"谈起 一千个读者,就有一千个哈姆雷特,代码质量也同样如此. 想必每一个对于代码有追求的开发者,对于"高质量"这个词,或多或少都有自己的一丝理解.当 ...
- 怎样编写高质量的Java代码
代码质量概述 怎样辨别一个项目代码写得好还是坏?优秀的代码和腐化的代码区别在哪里?怎么让自己写的代码既漂亮又有生命力?接下来将对代码质量的问题进行一些粗略的介绍.也请有过代码质量相关经验的朋友提出宝贵 ...
- iOS应用开发最佳实践:编写高质量的Objective-C代码
点标记语法 属性和幂等方法(多次调用和一次调用返回的结果相同)使用点标记语法访问,其他的情况使用方括号标记语法. 良好的风格: view.backgroundColor = [UIColor oran ...
- 编写高质量的Objective-C代码
点标记语法 属性和幂等方法(多次调用和一次调用返回的结果相同)使用点标记语法访问,其他的情况使用方括号标记语法. 良好的风格: view.backgroundColor = [UIColor oran ...
- 如何编写高质量的Java代码
Java代码安全规范 本文目的 故障分析 资源分析 安全条目 QPS CPU Memory 磁盘I/O Network 代码工程 Web安全 附录 Redis网卡打满参照表 参考 本文目的 向Nasa ...
- [精华]如何编写高质量的VB代码
2003-01-01· ·冯睿··yesky简介: 本文描述了如何通过一些技术手段来提高VB代码的执行效率.这些手段可以分为两个大的部分:编码技术和编译优化技术.在编码技术中介绍了如何通过使用高效的数 ...
最新文章
- jstack可以定位到线程堆栈
- 使用python中的socket实现服务器和客户端,并完成图片的传输
- 五十八、如何对一个数进行分解质因数
- leetcode435. 无重叠区间
- 安装多实例造成***S故障
- gohu恒温花洒使用教程_使用家庭助理构建更好的恒温器
- java多线程信息共享 多线程管理
- 微信小程序的bindtap和catchtap实际场景 对话框中按钮点击和对话框背景点击处理笔记...
- 美团外卖与饿了么竞品分析
- 帝国php获取栏目id,帝国CMS如何获取子栏目
- Mac升级系统后,Android Studio 不能用问题
- 通过python使用多种方法改变图片尺寸
- IDEA Eval Reset 使用方法
- Android系统分区备份与还原
- Code jock 8.7 源代码编译
- PCLINT(2):MVG NEST LOC (圈复杂度 嵌套深度 代码行数)
- 一文读懂MACD技术指标
- 计算机课程设计答辩评语,【课程设计教师评语】_课程设计指导教师评语模板...
- ECMALL模板解析机制
- Pointofix下载、安装和使用快捷键