IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    所谓,引用计数

    forkong发表于 2016-06-14 13:56:13
    love 0

    博文链接: http://ifujun.com/suo-wei-yin-yong-ji-shu/

    简介

    在大部分关于Objective-C的书中,一般对于引用计数的讲解基本类似于下面(以 Objective-C基础教程 为例):

    Cocoa采用了一种称为引用计数的技术。每个对象有一个与之相关联的整数,称作它的引用计数器。当某段代码需要访问一个对象时,该代码将该对象的引用计数器值加1。当该代码结束访问时,将该对象的引用计数器值减1。当引用计数器值为0时,表示不再有代码访问该对象,因此对象将被销毁,其占用的内存被系统回收以便重用。

    概括一下就是,每个对象都会有个引用计数器,当且仅当引用计数器的值大于0时,该对象才可能是存活的。

    引用计数的内存回收是分布于整个运行期的,基本类似于下图。图中红色表示引用计数的活动。(图片来自于https://github.com/kenfox/gc-viz)

    从图中我们可以很直接的看出一些优点,比如:

    • 不需要等到内存不够才回收。

    • 不需要挂起应用程序才回收,回收分布于整个运行期。

    当然,引用计数也有一些缺点:

    • 无法完全解决循环引用导致的内存泄露问题。

    • 即使只读操作,也会引起内存写操作(引用计数的修改)。

    • 引用计数读写操作要原子化。

    retain release

    在苹果开源的 runtime 中,在objc-object.h中有部分关于retain和release的实现代码,具体如下:

    Retain

    objc_object::rootRetain(bool tryRetain, bool handleOverflow)
    {
        assert(!UseGC);
        if (isTaggedPointer()) return (id)this;
        ...
        do {
            transcribeToSideTable = false;
            oldisa = LoadExclusive(&isa.bits);
            newisa = oldisa;
            if (!newisa.indexed) goto unindexed;
            if (tryRetain && newisa.deallocating) goto tryfail;
            uintptr_t carry;
            newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);
            ... 
        } while (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits));
        ...
    }
    

    Release

    ALWAYS_INLINE bool 
    objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
    {
        assert(!UseGC);
        if (isTaggedPointer()) return false;
        ...
        do {
            oldisa = LoadExclusive(&isa.bits);
            newisa = oldisa;
            if (!newisa.indexed) goto unindexed;
            uintptr_t carry;
            newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);
            ...
        } while (!StoreReleaseExclusive(&isa.bits, oldisa.bits, newisa.bits));
        ...
    }
    

    在 draveness 的黑箱中的 retain 和 release中,draveness 对此进行了比较详细的讲解,我在此也不再赘述了,只补充几点:

    Tagged Pointer

    对 Tagged Pointer 类型的对象进行retain和release是没有意义的,从 rootRetain 的 if (isTaggedPointer()) return (id)this;可以看出。

    原子化

    上面说到,引用计数有个缺点是读写的原子化,在源码中,不管是retain、release、retainCount操作都是加锁的。

    这里加解锁的方法是sidetable_lock()和sidetable_unlock()。在
    NSObject.mm中,sidetable_lock()的具体结构是:

    void 
    objc_object::sidetable_lock()
    {
        SideTable& table = SideTables()[this];
        table.lock();
    }
    

    SideTable中使用的锁是spinlock_t。

    struct SideTable {
        spinlock_t slock;
        ...
    };
    

    这是类似于 Linux 上的自旋锁,和OSSpinLock有一些不同,应该不存在OSSpinLock的优先级反转问题,因为,苹果很多地方依然在使用,比如苹果的atomic使用的也是spinlock_t。(参考objc-accessors.mm)

    ARC

    我们知道,ARC是苹果的一项编译器功能,ARC会在编译期自动添加代码,但是,除此之外,还需要 Objective-C 运行时的协助。

    ARC让我们不需要再手写一些类似于retain、release、autorelease的代码。这看上去有点像GC了,但是,它依然解决不了循环引用等问题,所以,只能说ARC是一种处于GC和手动管理内存中间的一个状态。

    那 Objective-C 有过GC吗,有,以前有过,用的是类似于标记-清除的GC算法,后来在iOS上就完全使用手动管理内存了,再后来就是ARC了。(我们上面的rootRetain代码中就有这么一行:assert(!UseGC);)

    ARC大家都很熟了,它的一些规则什么的,我们就不重复了,就讲讲一些需要注意的点吧。

    桥接

    ARC只能作用于 Objective-C 类型,CoreFoundation 等类型的依然需要手动管理。Objective-C 对象的指针和 CoreFoundation 类型的指针是不一样的。

    我们一般有三种类型__bridge、__bridge_transfer、__bridge_retained。

    如果 CoreFoundation 对象和 Objective-C 对象转换只涉及类型,不涉及所有权的话,可以使用__bridge,比如这样:

    id obj = (__bridge id)CFDictionaryGetValue(cfDict, key);
    

    这时候ARC就可以接管这个对象并自动管理。

    但是,如果所有权被变更了,那么,再使用__bridge的话,就会发生内存泄露。

    NSString *value = (__bridge NSString *)CFPreferencesCopyAppValue(CFSTR("someKey"), CFSTR("com.company.someapp"));
    [self useValue: value];
    

    其实,上面这段就等同于:

    CFStringRef valueCF = CFPreferencesCopyAppValue(CFSTR("someKey"), CFSTR("com.company.someapp"));
    NSString *value = (__bridge NSString *)valueCF;
    //CFRelease(valueCF);
    [self useValue: value];
    

    其实这时候是需要加一行CFRelease(valueCF)的,如果没有的话,valueCF是会内存泄露的。

    当然,上面的写法也是可以的,只是这个临时变量存在的意义不大,写法也比较啰嗦,可以使用__bridge_transfer去解决这个问题。

    NSString *value = (__bridge_transfer NSString *)CFPreferencesCopyAppValue(CFSTR("someKey"), CFSTR("com.company.someapp"));
    [self useValue: value];
    

    和__bridge 不一样,__bridge_transfer会将值和所有权都移交出去,ARC接管到所有权之后,ARC在这个对象用完之后会进行释放。

    __bridge_retained和__bridge_transfer类似,只是__bridge_retained用于将 Objective-C 对象转化为 CoreFoundation 对象,而__bridge_transfer用于将 CoreFoundation 对象转化为 Objective-C 对象。

    举个例子,假设[self someString]这个方法会返回一个NSString类型的值,现在要将NSString类型的值转化为CFStringRef类型,使用__bridge_retained的话,相当于告诉ARC,对于这个对象,你的所有权已经没有了,我要自己来管理了。所以,我们要手动在后面加上CFRelease()方法。

    CFStringRef value = (__bridge_retained CFStringRef)[self someString];
    UseCFStringValue(value);
    CFRelease(value);
    

    上面的例子来自于Mikeash。

    总结一下就是:

    • __bridge会将非Objective-C对象和Objective-C对象进行转换,但并不会移交所有权。

    • __bridge_transfer会将非Objective-C对象转化为Objective-C对象,同时会移交所有权,ARC会帮你释放这个对象。

    • __bridge_retained会将Objective-C对象转化为非Objective-C对象,同时会移交所有权,你需要手动管理这个对象。

    防御式编程

    一般来说,我们很少使用try...catch,我们一般抛Error而不是Exception,但是,总有一些特殊的情况,try...catch的存在依然是有意义的。

    如果我们在try中进行一些对象创建的操作的话,可能会造成内存泄露,比如:

    @try {
        SomeObject *obj = [[SomeObject alloc] init];
        [obj doSomething];
    } @catch (NSException *exception) {
        NSLog(@"%@", exception);
    }
    

    如果try代码段中发成错误,obj将不会得到释放。如果现在是MRC,那你可以在finally中添加[obj release],但是在ARC下,你无法添加,ARC也不会帮你添加。

    所以,不要在try中进行对象的创建操作,要移出来。

    performSelector

    在Effective Objective-C 2.0一书中,作者说到:

    编译器并不知道将要调用的选择子是什么,因此,也就不了解其方法签名及返回值,甚至连是否有返回值都不清楚。而且,由于编译器不知道方法名,所以就没办法运用ARC的内存管理规则来判定返回的值是不是应该释放。鉴于此,ARC采用了比较谨慎的做法,就是不添加释放操作。然而,这么做会导致内存泄露。

    我在iOS 常用Timer 盘点一文中进行了试验,原文如下:

    我们试验一下,这里printDescriptionA和printDescriptionB方法各会返回一个不同类型的View(此View是新建的对象),printDescriptionC会返回Void。

    NSArray *array = @[@"printDescriptionA",
                       @"printDescriptionB",
                       @"printDescriptionC"];
    
    NSString *selString = array[arc4random()%3];
    NSLog(@"sel = %@", selString);
    SEL tempSel = NSSelectorFromString(selString);
    if ([self respondsToSelector:tempSel])
    {
        [self performSelector:tempSel withObject:nil afterDelay:3.0f];
    }
    

    几次尝试之后,我发现,这是可以正常释放的。

    如果我的试验正确的话,那么,ARC肯定不只是在编译期的优化,在运行时也是有优化的。这也印证了我上面所说的,ARC会在编译期自动添加代码,但是,除此之外,还需要 Objective-C 运行时的协助。

    而不是苹果文档中说的:

    ARC works by adding code at compile time to ensure that objects live as long as necessary, but no longer.

    当然,也可能是我的试验不正确,如果你知道如何触发这种内存泄露,请告诉我。

    实现简单引用计数

    我们来实现一个简单引用计数的代码,我们需要实现以下方法:

    • retain

      • addReference

    • release

      • deleteReference

    • retainCount

    依据我们上面提到的引用计数读写操作要原子化,我们需要添加锁的操作,并且,我们这里简单理解为当引用计数为0时,进行dealloc方法的调用。

    为了方便,我们用pthread_mutex来代替spinlock_t(pthread_mutex是一种互斥锁,性能也挺高)。

    基本代码类似于下面:

    #import "FKObject.h"
    #import <objc/runtime.h>
    #include <pthread.h>
    
    @interface FKObject ()
    {
        pthread_mutex_t fk_lock;
    }
    
    @property (readwrite, nonatomic) NSUInteger fk_retainCount;
    @end
    
    @implementation FKObject
    
    -(instancetype)init
    {
        if (self = [super init])
        {
            pthread_mutex_init(&fk_lock, NULL);
            _fk_retainCount = 1;
        }
        return self;
    }
    -(void)fk_retain
    {
        [self addReference];
    }
    -(void)fk_release
    {
        NSUInteger count = [self deleteReference];
        if (count == 0)
        {
            [self fk_dealloc];
        }
    }
    -(void)fk_dealloc
    {
        //因为ARC下不能主动调用dealloc方法,所以这里伪造一个fk_dealloc来模拟
        NSLog(@"%@ dealloc", self);
    }
    -(void)addReference
    {
        pthread_mutex_lock(&fk_lock);
        NSUInteger count = [self fk_retainCount];
        [self setFk_retainCount:++count];
        pthread_mutex_unlock(&fk_lock);
    }
    -(NSUInteger)deleteReference
    {
        pthread_mutex_lock(&fk_lock);
        NSUInteger count = [self fk_retainCount];
        [self setFk_retainCount:--count];
        pthread_mutex_unlock(&fk_lock);
        return count;
    }
    @end
    
    

    我们来测试一下:

    FKObject *object = [[FKObject alloc] init];
    NSLog(@"%ld", object.fk_retainCount);
    [object fk_retain];
    NSLog(@"%ld", object.fk_retainCount);
    [object fk_release];
    NSLog(@"%ld", object.fk_retainCount);
    [object fk_release];
    

    代码

    https://github.com/Forkong/ReferenceCountingTest

    参考文档

    • https://book.douban.com/subject/26740958/

    • https://spin.atomicobject.com/2014/09/03/visualizing-garbage-collection-algorithms/

    • http://clang.llvm.org/docs/AutomaticReferenceCounting.html

    • https://mikeash.com/pyblog/friday-qa-2011-09-30-automatic-reference-counting.html



沪ICP备19023445号-2号
友情链接