转载

iOS 与OS X多线程和内存管理笔记:MRC与引用计数

iOS 与OS X多线程和内存管理笔记:MRC与引用计数

前言

时间已经过去一年多了,每一次在地铁上读这本书都有新的体会和心得.所以在这做一下深层次的分享,让大家对iOS内存管理这块有更加深入的了解.

NSObject类解析

NSObject是Objective-C所有类的基类.这里我们就深入了解一下NSObject的内存相关知识内容.我们都知道NSObject是通过引用计数来决定对象是否需要被释放的,在<>这本书中是通过GNUstep来阐述说明NSObject的alloc方法的内部实现的,我们都知道每一个OC对象中都有一个retainCount属性来记录引用计数.我们看一下简化的NSObject的内部实现.

struct obj_layout{
  
    NSUInteger retainded;
};

+(id)alloc{
    
    int size = sizeof(struct obj_layout) + 对象所占内存大小;
    struct obj_layout *p = (struct obj_layout *)calloc(1, size);
    return (id)(p+1);

}

在上面的代码中我们可以看到alloc内部总共做了两部分的工作,一个是先计算出头部obj_layout以及自身所占有多少空间,然后在内存之中通过calloc函数开辟一个大小为size的连续空间.alloc返回的id值为对象本身的指针(非obj_layout的指针).整体如下图所示.

iOS 与OS X多线程和内存管理笔记:MRC与引用计数

对于retainCount引用计数这一属性,在GNUstep是做了如下的实现的.

-(NSUInteger)retainCount{    
    return NSExtraRefCount(self)+1;
}

inline NSUInteger NSExtraRefCount(id anObject){    
    return ((struct obj_layout *)anObject)[-1].retainded;    
}

在图1-8中我们知道alloc返回的指针是指向对象的头部的,并不是指向struct obj_layout这个结构体的,所以我们想要通过对象本身的指针减去struct obj_layout结构体的大小的地址就是指向struct obj_layout的指针,如下图所示.

iOS 与OS X多线程和内存管理笔记:MRC与引用计数

接下来我们分别看一下GNUstep中的retain、release、delloc的实现由是怎么样的.

retain的实现

-(instancetype)retain{
    NSIncrementExtraRefCount(self);
    return self;
}

inline void NSIncrementExtraRefCount(id anObject){
    
    if (((struct obj_layout *)anObject)[-1].retainded == UINT_MAX - 1) {
        [NSException raise:NSInternalInconsistencyException format:@"NSIncrementExtraRefCount() asked to increment too far"];
    }
  
    ((struct obj_layout *)anObject)[-1].retainded++;
}

其中NSIncrementExtraRefCount()函数保证了retainded变量不会超出最大值,当超出的时候就会发生异常,实际过程中很少会发生这种异常,通常我们只是执行retainded计数加1的操作.同样的release实现过程比较类似.

release的实现

-(void)release{
  
    if (NSDecrementExtraRefCountWasZero(self)) {
        [self dealloc];
    }
}

BOOL NSDecrementExtraRefCountWasZero(id anObject){
    
    if (((struct obj_layout *)anObject)[-1].retainded == 0) {
        return YES;
    }else{
       
        ((struct obj_layout *)anObject)[-1].retainded--;
        return NO;
    }
}

在NSDecrementExtraRefCountWasZero()函数中判断struct obj_layout 结构体中的retainded变量的值是否为0,如果是0,那么在release方法中就会执行对象的dealloc方法,释放对象.

dealloc的实现

- (void)dealloc{
    NSDeallocateObject(self);
}

inline void NSDeallocateObject(id anObject){
    
    struct obj_layout *o = &((struct obj_layout *)anObject)[-1];
    free(o);
}

dealloc的实现就比较简单了,通过对象指针找到有alloc分配的内存块.然后释放.

苹果实现

上面都是GNUstep中对NSObject类的内存管理的实现,那么苹果的实现和上述的实现是否一致呢?其实思路是一致的 ,但是苹果的实现是通过散列表来管理引用计数的.如下图所示.

iOS 与OS X多线程和内存管理笔记:MRC与引用计数

我们先看一下简化的代码实现.

//核心方法
int __CFDoExternRefOperation(uintptr_t op, id obj) {
   
    CFBasicHashRef table = 取得对象对应的散列表(obj);
    int count;
    
    switch (op) {
        case OPERATION_retainCount;
            count = CFBasicHashGetCountOfKey(table, obj);
            return count;
        case OPERATION_retain:
            CFBasicHashAddValue(table, obj);
            return obj;
        case OPERATION_release:
            count = CFBasicHashRemoveValue(table, obj);
            return 0 == count;
    }
}
//调用方法
- (NSUInteger)retainCount {  
     return (NSUInteger) __CFDoExternRefOperation(OPERATION_retainCount, self);  
}  
 
- (id)retain {  
     return (id)__CFDoExternRefOperation(OPERATION_retain, self);  
}  
 
- (void)release {  
     return __CFDoExternRefOperation(OPERATION_release, self);  
}

那么使用散列表和把引用计数保存在对象占用的内存头部到底有什么优势呢?

通过内存块头部管理引用计数的好处:

  • 少量代码即可实现.

  • 能够统一管理引用计数用内存块与对象用内存块.

通过引用计数表管理引用计数的好处:

  • 对象用内存块的分配无需考虑内存块头部.

  • 引用计数表格记录中存有内存块地址,可从各个记录追溯到各对象的内存块.

我们发现上面的说的好像也没有什么优势,其实不然,假定对象的内存块损坏,我们仍然可以通过散列表来确定各内存块的位置,但是通过内存块头部管理引用计数的方式却不行.

iOS 与OS X多线程和内存管理笔记:MRC与引用计数

循环引用

循环引用问题算是老生常谈的问题,但是我们只是知道两个对象相互持有会产生循环引用,自身持有自己会产生循环引用,却不明白其中的逻辑关系,下面我们就梳理一下是如何造成的循环引用的.


首先我们定义一个Test对象.

#import
@interface Test : NSObject
{
    id __strong obj_;
}
-(void)setObject:(id __strong)obj;
@end
#import "Test.h"
@implementation Test
-(instancetype)init{
    self = [super init];
    return self;
}
-(void)setObject:(id)obj{
    
    obj_ = obj;
}
@end

然后我们自己创造一个循环引用的例子.

{
    id test0 = [[Test alloc]init];
    id test1 = [[Test alloc]init];
    
    [test0 addObject:test1];
    [test1 addObject:test0];
}

然后我们具体分析一下上面是如何造成循环引用的.

iOS 与OS X多线程和内存管理笔记:MRC与引用计数

{
    id test0 = [[Test alloc]init];/*对象A*/
    /*指针test0持有Test对象A的强引用*/

    id test1 = [[Test alloc]init];/*对象B*/
    /*指针test1持有Test对象B的强引用*/

    [test0 addObject:test1];
    /*指针test0的obj_成员变量持有持有Test对象B的强引用.
     *此时,持有对象B的强引用为Test对象A的obj_和test1;
     */

    [test1 addObject:test0];
    /*指针test1的obj_成员变量持有持有Test对象A的强引用.
     *此时,持有对象A的强引用为Test对象B的obj_和test0;
     */
}

    /*
     *  test0变量超出其作用域,强引用失效,所以自动释放Test对象A.
     *
     *  test1变量超出其作用域,强引用失效,所以自动释放Test对象B.
     *
     *  此时,持有Test对象A的强引用的变量为Test对象B的obj_;
     *
     *  此时,持有Test对象B的强引用的变量为Test对象A的obj_;
     *     
     *  发生内存泄漏.
     */

上面是两个对象之间的循环引用,相对的自身引用自身造成的循环引用是一样的.比如下面的例子.

iOS 与OS X多线程和内存管理笔记:MRC与引用计数

{
    id test0 = [[Test alloc]init];
    [test0 addObject:test0];
}

解决循环引用的修饰符 :__weak 与__unsafe_unretained

上一个模块我们了解到什么情况会造成循环引用从而进一步的造成内存泄漏,接下来我们看如何解决上面的循环引用问题,我们知道有强引用必然有弱引用,强引用表示持有某个对象,那么我们只要不持有某个对象就可以了(持有对象的本质是引用计数的增加,__weak修饰符不会引起引用计数的变化).这个时候我们就需要__weak修饰符了,比如上面的例子我们可以做如下修改就可以解决循环引用的问题.

#import
@interface Test : NSObject
{
    id __weak obj_;
}
-(void)setObject:(id __strong)obj;
@end

__weak修饰符是在iOS5以上才能使用,在此之前iOS4以及以前我们使用的__unsafe_unretained修饰符,那么这两者有什么区别呢?下面我们就举例说明.

    id __weak obj1 = nil;
    
    @autoreleasepool{
        id __strong obj0 = [[NSObject alloc]init];
        obj1 = obj0;
        NSLog(@"A: %@",obj1);
    }
    
    NSLog(@"B: %@",obj1);

打印结果如下所示.

iOS 与OS X多线程和内存管理笔记:MRC与引用计数

我们再换成__unsafe_unretained修饰符来进行一下比对.

    id __unsafe_unretained obj1 = nil;
    
    @autoreleasepool{
        id __strong obj0 = [[NSObject alloc]init];
        obj1 = obj0;
        NSLog(@"A: %@",obj1);
    }
    
    NSLog(@"B: %@",obj1);

这时候,在第二个NSLog 程序已经崩掉了.

iOS 与OS X多线程和内存管理笔记:MRC与引用计数

那么,都是可以解决循环引用的两个修饰符,是什么造成这种差异呢?这是因为__weak修饰符有个优点:

通过__weak修饰符持有对象的弱引用是,若改对象被废弃,则此弱引用将会自动失效且处于nil被赋值的状态(空弱引用),但是__unsafe_unretained修饰符却没有这样的功能,所以造成了悬垂指针,也就是我们常说的野指针(指针指向已经被释放的内存地址).

ARC中__weak修饰符的实现

我们知道通过<>这本书的67页的讲解,我们了解到__weak修饰符运行机制如下所示.

例如,我们做一下的代码操作.

{
   id __weak obj1 = obj;
}

通过模拟器的,我们可以得到下述的模拟代码.

        id obj1;
        objc_initWeak(&obj1,obj);
        objc_destroyWeak(&obj1);

其中objc_initWeak()函数和 objc_destroyWeak()函数共同调用了objc_storeWeak()这个函数,objc_storeWeak()函数一共有两个参数,函数把第二个参数的复制对象的地址作为键值,将第一参数的附有__weak修饰符的变量的指针注册到weak表中.如果第二个参数为0,则吧变量的地址从weak表中删除.所以上面的代码可以如下表示.

        id obj1;
        obj1 = 0;
        objc_storeWeak(&obj1,obj);
        objc_storeWeak(&obj1,0);

那么在释放对象的时候,释放谁都不持有的对象的同事,程序的动作是怎么样的呢?对象是通过objc_release函数来释放的.

  • objc_release函数的调用

  • 由于引用计数为0所以执行delloc

  • _objc_rootDealloc

  • object_dispose

  • objc_destructInstance

  • objc_clear_deallocating

其实对象在被废弃时最后调用的objc_clear_deallocating函数会对__weak修饰的相关变量进行清除操作,步骤如下所示.

  • 从weak表中获取废弃对象的地址为键值的记录.

  • 将包含在记录中的所有附有__weak修饰符变量的地址(指针),赋值为nil.

  • 从weak表中删除该记录.

  • 从引用计数表中删除废弃对象的地址为键值的记录.

通过上面步骤,我们就可以知道__weak修饰符变量为什么会在所引用的对象被废弃时变为nil,可是由于__weak修饰符修饰的变量的废弃需要对weak表进行操作.所以如果大量使用附有__weak修饰符的变量,那么会增加对CPU的压力.

结束

本篇的博客并非是iOS 与OS X多线程和内存管理的第一章的全部内容,我只是挑选几个日常容易碰到的知识点做了一下分享,比如__autoreleasing修饰符我这里都没有说到.当然了,现在的ARC环境越来越好,所以有些知识点我们都可能用不到,大家在这里做一下了解即可.如有需求可以去传送门下载PDF版.

iOS 与OS X多线程和内存管理的PDF版传送门

作者:神经骚栋

链接:https://www.jianshu.com/p/cad993271e42

正文到此结束
Loading...