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

作者: QiShare | 来源:发表于2018-09-05 21:21 被阅读158次

    级别: ★★☆☆☆
    标签:「iOS」「内存管理」「Objective-C」
    作者: MrLiuQ
    审校: QiShare团队

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

    文章目录如下:
    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》第五章。

    关注我们的途径有:
    QiShare(简书)
    QiShare(掘金)
    QiShare(知乎)
    QiShare(GitHub)
    QiShare(CocoaChina)
    QiShare(StackOverflow)
    QiShare(微信公众号)

    推荐文章:
    iOS与JS交互之WKWebView-WKUIDelegate协议
    如果360推出辣椒水,各位女士会买吗?
    从撒狗粮带你了解WoT连接场景

    相关文章

      网友评论

      • 哈哈哈士奇XHB:C++怎么捕获OC的异常,有源码不?
      • Corbin___:@autoreleasepool不是出了{}会被释放吗
        Lucky_Man:@陈泽槟_Corbin 你的这个回复使我对autoreleasepool有了新的理解。
        我写了一个小示例。https://www.jianshu.com/p/2f675b4d553f
        主要代码为:

        __weak NSObject *weakObj;
        @autoreleasepool{
        NSObject *obj2 = [NSObject new];
        weakObj = obj2;
        NSLog(@"%@--%@", obj2, weakObj);
        }
        NSLog(@"%@", weakObj);

        另外一种有有引用关系的情况,可以考虑嵌套一个autoreleasepool
        @autoreleasepool{
        NSMutableArray *arrM = [NSMutableArray array];
        @autoreleasepool{
        NSObject *obj2 = [NSObject new];
        [arrM addObject:obj2];
        weakObj = obj2;
        NSLog(@"%@--%@", obj2, weakObj);
        }
        }
        NSLog(@"%@", weakObj);

        输出结果均为:
        2018-09-11 22:20:07.323471+0800 QiFirstTasteOfAnimation[2285:483390] <NSObject: 0x28388b130>--<NSObject: 0x28388b130>
        2018-09-11 22:20:07.323488+0800 QiFirstTasteOfAnimation[2285:483390] (null)
        Corbin___:那如果有引用关系的话,正如你的代码,这种情况,自动释放池有起作用吗,我记得以前有做过一次test,自动释放池不起作用了,就是出了},没有马上释放,那时候我以为是这个机制修改了
        Lucky_Man:我的理解是这样的,如果下边的代码中,如果没有添加member到数组的话,那么member会在我们自己写的这个autoreleasepool的右括号的时候释放掉,不过因为我们添加到了数组,形成了引用关系。所以释放的时机会延后。
        如果我们不写这个autoreleasepool的话,那么默认的创建的对象会放在main.m 的那个大的autoreleasepool里边。
        两者比较看来,我们自己写的autoreleasepool可以使得临时对象提早回收。

        NSArray *qiShare = /*一个很大的数组*/
        NSMutableArray *qiShareMembersArray = [NSMutableArray new];

        for (NSStirng *name in qiShare) {
        @autoreleasepool {
        QiShareMember *member = [QiShareMember alloc] initWithName:name];
        [qiShareMembersArray addObject:member];
        }
        }

      本文标题:iOS 编写高质量Objective-C代码(五)

      本文链接:https://www.haomeiwen.com/subject/swpzwftx.html