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


前言

<<iOS 与OS X多线程和内存管理>>笔记:MRC与引用计数中我写的都是比较简单易懂的知识点. 时间已经过去一年多了,每一次在地铁上读这本书都有新的体会和心得.所以在这做一下深层次的分享,让大家对iOS内存管理这块有更加深入的了解.


NSObject类解析

NSObject是Objective-C所有类的基类.这里我们就深入了解一下NSObject的内存相关知识内容.我们都知道NSObject是通过引用计数来决定对象是否需要被释放的,在<<iOS 与OS X多线程和内存管理>>这本书中是通过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的指针).整体如下图所示.



对于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的指针,如下图所示.



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

  • 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类的内存管理的实现,那么苹果的实现和上述的实现是否一致呢?其实思路是一致的 ,但是苹果的实现是通过散列表来管理引用计数的.如下图所示.

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

//核心方法
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);  
}  

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

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

  • 少量代码即可实现.
  • 能够统一管理引用计数用内存块与对象用内存块.

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

  • 对象用内存块的分配无需考虑内存块头部.
  • 引用计数表格记录中存有内存块地址,可从各个记录追溯到各对象的内存块.

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



循环引用

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

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

#import <Foundation/Foundation.h>

@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];
}

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


{
    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_;
     *     
     *  发生内存泄漏.
     */

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


{
    id test0 = [[Test alloc]init];

    [test0 addObject:test0];
}


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

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

#import <Foundation/Foundation.h>

@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);

打印结果如下所示.


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

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

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


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

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


ARC中__weak修饰符的实现

我们知道通过<<iOS 与OS X多线程和内存管理>>这本书的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版传送门🚪



©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容