iOS 编写高质量Objective-C代码(五)

栏目: Objective-C · 发布时间: 6年前

内容简介:级别: ★★☆☆☆标签:「iOS」「内存管理」「Objective-C」作者:MrLiuQ

级别: ★★☆☆☆

标签:「iOS」「内存管理」「Objective-C」

作者:MrLiuQ

审校:QiShare团队

前言: 这几篇文章是小编在钻研《Effective Objective-C 2.0》的知识产出,其中包含作者和小编的观点,以及小编整理的一些demo。希望能帮助大家以简洁的文字快速领悟原作者的精华。 在这里,QiShare团队向原作者Matt Galloway表达诚挚的敬意。

本篇的主题是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多线程和内存管理》这本书的例子: 很经典的图解:

iOS 编写高质量Objective-C代码(五)

解释:

1.开灯:引申为: “ 创建对象 ”

2.关灯:引申为: “ 销毁对象 ”

iOS 编写高质量Objective-C代码(五)

解释:

1.有人来上班打卡了:开灯。——(创建对象,计数为1)

2.又有人来了:保持开灯。——(保持对象,计数为2)

3.又有人来了:保持开灯。——(保持对象,计数为3)

4.有人下班打卡了:保持开灯。——(保持对象,计数为2)

5.又有人下班了:保持开灯。——(保持对象,计数为1)

6.所有员工全下班了:关灯。——(销毁对象,计数为0)

场景 对应OC的动作 对应OC的方法
上班开灯 生成对象 alloc/new/copy/mutableCopy等
需要照明 持有对象 retain
不需要照明 解除持有 release
下班关灯 销毁对象 dealloc

如果觉得本书中的例子说的有点抽象难懂,没关系,请看下面图解示例:

提示:实箭头为强引用,虚箭头为弱引用。

iOS 编写高质量Objective-C代码(五)
  • 属性存取方法中的内存管理:

这里有个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环境下,禁止:no_entry_sign:调用: 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

六、以 “自动释放池块” 降低内存峰值

@autoreleasepool

尤其,在遍历处理一些 大数组 或者 大字典 的时候,可以使用自动释放池来降低内存峰值,例如:

NSArray *qiShare = /*一个很大的数组*/
NSMutableArray *qiShareMembersArray = [NSMutableArray new];
for (NSStirng *name in qiShare) {
    @autoreleasepool {
        QiShareMember *member = [QiShareMember alloc] initWithName:name];
        [qiShareMembersArray addObject:member];
    }
}
复制代码

PS:自动释放池的原理:排布在“栈”中,对象执行autorelease消息后,系统将其放入最顶端的池里(进栈),而清空自动释放池就是把对象销毁(出栈)。而调用出栈的时机:就是当前线程执行下一次事件循环时。

七、用 “僵尸对象” 调试内存管理问题

iOS 编写高质量Objective-C代码(五)

如上图,勾选这里可以开启僵尸对象设置。开启之后,系统在回收对象时,不将其真正的回收,而是把它的 isa指针 指向特殊的僵尸类(zombie class),变成僵尸对象。僵尸类能够响应所有的选择子,响应方式为:打印一条包含消息内容以及其接收者的消息,然后终止应用程序。

僵尸对象简单原理:在Objective-C的运行期程序库、Foundation框架以及CoreFoundation框架的底层加入了实现代码。在系统即将回收对象时,通过一个环境变量 NSZombieEnabled 识别是僵尸对象——不彻底回收, isa 指针指向僵尸类并且响应所有选择子。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

产品经理手册(原书第4版)(白金版)

产品经理手册(原书第4版)(白金版)

[美] 琳达·哥乔斯(Linda Gorchels) / 祝亚雄、冯华丽、金骆彬 / 机械工业出版社 / 2017-8 / 65.00

产品经理的职责起点是新产品开发,贯穿产品生命周期的全过程。本书按上下游产品管理进行组织。 在上游的新产品开发流程中,作者阐述了如何从市场、产品、行业、公司的角度规划企划方案,并获得老板、销售部、运营部的资源支持,推进新产品的项目流程,实现所有目标,制定和实施新产品发布。 下游产品的管理核心在于生命周期的管理,营销更是生命周期管理的重中之重。产品经理如何让产品满足客户需求,让客户获得对产......一起来看看 《产品经理手册(原书第4版)(白金版)》 这本书的介绍吧!

URL 编码/解码
URL 编码/解码

URL 编码/解码

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换