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

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

内容简介:级别: ★★☆☆☆标签:「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 指针指向僵尸类并且响应所有选择子。


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

查看所有标签

猜你喜欢:

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

Fluent Python

Fluent Python

Luciano Ramalho / O'Reilly Media / 2015-8-20 / USD 39.99

Learn how to write idiomatic, effective Python code by leveraging its best features. Python's simplicity quickly lets you become productive with it, but this often means you aren’t using everything th......一起来看看 《Fluent Python》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

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

URL 编码/解码

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

UNIX 时间戳转换