《编写高质量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开发者必须要手动管理内存,即手动管理对象的内存分配和释放。首先,不断插入retainrelease等内存管理语句,大大加大了工作量和代码量。其次,在面对一些多线程并发操作时,开发者手动管理内存并不简单,还可能会带来很多无法预知的问题。
所以,苹果从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方法将保留新值,释放旧值,然后更新实例变量。这三个语句的顺序很重要。
如果先releaseretain。那么该对象可能已经被回收,此时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环境下,禁止调用:retainreleaseautoreleasedealloc方法。

  • 使用ARC时必须遵循的方法命名规则:
    若方法名以allocnewcopymutableCopy开头,则规定返回的对象归调用者。
  • 变量的内存管理语义:

对比一下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标志,其性能损失不大。

最后,还是建议:

  1. 异常只用于处理严重的错误(fatal error,致命错误)
  2. 对于一些不那么严重的错误(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代码(五)相关推荐

  1. iOS 编写高质量Objective-C代码(六)

    级别: ★★☆☆☆ 标签:「iOS」「Block」「Objective-C」 作者: MrLiuQ 审校: QiShare团队 前言: 这几篇文章是小编在钻研<Effective Objecti ...

  2. iOS 编写高质量Objective-C代码(八)

    前言: 这几篇文章是小编在钻研<Effective Objective-C 2.0>的知识产出,其中包含作者和小编的观点,以及小编整理的一些demo.希望能帮助大家以简洁的文字快速领悟原作 ...

  3. HTML Inspector – 帮助你编写高质量的 HTML 代码

    HTML Inspector 是一款代码质量检测工具,帮助你编写更优秀的 HTML 代码.HTML Inspector 使用 JavaScript 编写,运行在浏览器中,是最好的 HTML 代码检测工 ...

  4. ​如何编写高质量的C#代码(一)

    如何编写高质量的C#代码(一) 从"整洁代码"谈起 一千个读者,就有一千个哈姆雷特,代码质量也同样如此. 想必每一个对于代码有追求的开发者,对于"高质量"这个词 ...

  5. 如何编写高质量的C#代码(一)

    从"整洁代码"谈起 一千个读者,就有一千个哈姆雷特,代码质量也同样如此. 想必每一个对于代码有追求的开发者,对于"高质量"这个词,或多或少都有自己的一丝理解.当 ...

  6. 怎样编写高质量的Java代码

    代码质量概述 怎样辨别一个项目代码写得好还是坏?优秀的代码和腐化的代码区别在哪里?怎么让自己写的代码既漂亮又有生命力?接下来将对代码质量的问题进行一些粗略的介绍.也请有过代码质量相关经验的朋友提出宝贵 ...

  7. iOS应用开发最佳实践:编写高质量的Objective-C代码

    点标记语法 属性和幂等方法(多次调用和一次调用返回的结果相同)使用点标记语法访问,其他的情况使用方括号标记语法. 良好的风格: view.backgroundColor = [UIColor oran ...

  8. 编写高质量的Objective-C代码

    点标记语法 属性和幂等方法(多次调用和一次调用返回的结果相同)使用点标记语法访问,其他的情况使用方括号标记语法. 良好的风格: view.backgroundColor = [UIColor oran ...

  9. 如何编写高质量的Java代码

    Java代码安全规范 本文目的 故障分析 资源分析 安全条目 QPS CPU Memory 磁盘I/O Network 代码工程 Web安全 附录 Redis网卡打满参照表 参考 本文目的 向Nasa ...

  10. [精华]如何编写高质量的VB代码

    2003-01-01· ·冯睿··yesky简介: 本文描述了如何通过一些技术手段来提高VB代码的执行效率.这些手段可以分为两个大的部分:编码技术和编译优化技术.在编码技术中介绍了如何通过使用高效的数 ...

最新文章

  1. jstack可以定位到线程堆栈
  2. 使用python中的socket实现服务器和客户端,并完成图片的传输
  3. 五十八、如何对一个数进行分解质因数
  4. leetcode435. 无重叠区间
  5. 安装多实例造成***S故障
  6. gohu恒温花洒使用教程_使用家庭助理构建更好的恒温器
  7. java多线程信息共享 多线程管理
  8. 微信小程序的bindtap和catchtap实际场景 对话框中按钮点击和对话框背景点击处理笔记...
  9. 美团外卖与饿了么竞品分析
  10. 帝国php获取栏目id,帝国CMS如何获取子栏目
  11. Mac升级系统后,Android Studio 不能用问题
  12. 通过python使用多种方法改变图片尺寸
  13. IDEA Eval Reset 使用方法
  14. Android系统分区备份与还原
  15. Code jock 8.7 源代码编译
  16. PCLINT(2):MVG NEST LOC (圈复杂度 嵌套深度 代码行数)
  17. 一文读懂MACD技术指标
  18. 计算机课程设计答辩评语,【课程设计教师评语】_课程设计指导教师评语模板...
  19. ECMALL模板解析机制
  20. Pointofix下载、安装和使用快捷键

热门文章

  1. lintcode-111-爬楼梯
  2. MySQL 显示版本、端口、状态
  3. MapGIS二调数据裁剪工具
  4. 可视化技巧:显示带colorbar的热图(matplotlib)
  5. KORG Software TRITON for mac(虚拟合成器软件)
  6. 急!Mac无法修复磁盘,应该怎么办? 看 这 里!
  7. Mac电脑上如何备份Instagram帐户?一款软件轻松帮你搞定
  8. OC可变数组的常用操作
  9. 李佳琦618直播清单都在这张思维导图上了
  10. 音乐制作:用FL Studio做电子音乐