所谓,引用计数

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

简介

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

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

归纳一下就是,每一个对象都会有个引用计数器,当且仅当引用计数器的值大于0时,该对象才多是存活的。github

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

从图中咱们能够很直接的看出一些优势,好比:编程

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

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

固然,引用计数也有一些缺点dom

  • 没法彻底解决循环引用致使的内存泄露问题。ide

  • 即便只读操做,也会引发内存写操做(引用计数的修改)。

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

retain release

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

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 类型的对象进行retainrelease是没有意义的,从 rootRetainif (isTaggedPointer()) return (id)this;能够看出。

原子化

上面说到,引用计数有个缺点是读写的原子化,在源码中,不论是retainreleaseretainCount操做都是加锁的。

这里加解锁的方法是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让咱们不须要再手写一些相似于retainreleaseautorelease的代码。这看上去有点像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 盘点一文中进行了试验,原文以下

咱们试验一下,这里printDescriptionAprintDescriptionB方法各会返回一个不一样类型的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_tpthread_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

参考文档

相关文章
相关标签/搜索