iOS 选用什么内存办理方式

在 iOS 中,选用自动引证计数(ARC,Automatic Reference Counting)机制来进行内存办理,让编译器来协助内存办理,无需程序员手动键入 retain、release 等代码进行内存办理,取而代之的是由编译器来刺进相关内存办理的代码。这一点的好处在于能够降低程序溃散、内存走漏等危险的一起,很大程度上也能够削减程序员的工作量。

与引证计数相对应的,是垃圾收回(GC,Garbage Collection)机制,JavaScript、Java、Golong 等语言都选用这种机制进行内存办理,它将一切的目标当作一个集合,然后在 GC 循环中定时监测活动目标和非活动目标,及时将这些用不到的非活动目标开释以避免内存走漏。

相对于 GC 来说,引证计数是部分的,在运行时无需额定开支,一起其内存收回是平稳、时机清晰的,没有被持有的目标会被立即开释,但一起也引入了循环引证导致的内存走漏这种新的内存办理问题。

内存办理的相关操作

当生成新的目标时,其引证计数为 1,当有其他指针持有这个目标时,其引证计数加 1,当其他指针开释这个目标时,其引证计数减 1,当这个目标的引证计数变为 0 时,目标会被抛弃。上文中出现的“生成”、“持有”、“开释”、“抛弃”对应的 Objective-C 的办法如下表:

目标操作 Objective-C 办法
生成并持有目标 alloc / new / copy / mutableCopy 办法
持有目标 retain 办法
开释目标 release 办法
抛弃目标 dealloc 办法

在 MRC 机制下,由于需求程序员手动刺进 retain、release 代码,无需考虑引证计数,按如下考虑方式进行代码编写就能够办理好内存:

  • 自己生成的目标,自己所持有。
  • 非自己生成的目标,自己也能持有。
  • 不再需求自己持有的目标时开释。
  • 非自己持有的目标无法开释。

但 ARC 中,由于交给了编译器进行内存办理,每个目标都是相当于强引证,但这会发生循环引证的问题,由于引证计数不能达到 0,导致目标无法被开释,因而引入了一切权修饰符来处理这个问题:

  • __strong:目标的默认一切权修饰符,它表明对目标的“强引证”,在超出其作用域时,强引证失效。

  • __weak:运用 __weak 修饰的目标不会持有目标,因而不会使目标的引证计数加 1,一起弱引证指向的目标被抛弃时,弱引证会指向 nil,利用这一点能够来处理循环引证的场景。

  • __unsafe_unretained:与 __weak 类似,__unsafe_unretained 修饰的目标不会持有目标,但在指向的目标被抛弃时,不会指向 nil,会变成野指针。运用 __unsafe_unretained 时需求清晰清楚它的生命周期小于或者等于被指向目标的生命周期,它与 Swift 中的 unowned 类似,一起它的功率也比 __weak 高。

  • __autoreleasing:运用 __autoreleaseing 修饰的目标会被注册到 AutoReleasePool 中,会延迟到 AutoReleasePool 被毁掉时才会调用目标的 release 办法,这会延伸目标的生命周期。但由于编译器优化的原因,实际用到的当地是很少的。

循环引证:目标间的相互强引证发生有向环,导致有向环中的每一个节点都无法被开释(没有目标的引证计数为 0),进而会导致内存走漏。例如:

// test 持有自身导致的循环引证
id test = [[Test alloc] init];
[test setObject: test];

alloc & retain & release & dealloc 源码探求

alloc

目标内存分配的首要逻辑会集在 callAlloc_class_createInstanceFromZone 办法里。为了提升功率,其在 callAlloc 里判别了 hasCustomAWZ(自定义的 allocWithZone 办法),没有履行 _objc_rootAllocWithZone,有则进入音讯派发 allocWithZone。NSZone 已被体系忽略,由于前史留传原因才得以保存,因而虽然有许多跳转流程,但最终都会指向 _class_createInstanceFromZone 办法。

static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
    if (slowpath(checkNil && !cls)) return nil;
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        return _objc_rootAllocWithZone(cls, nil);
    }
#endif
    if (allocWithZone) {
        return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
    }
    return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}

_class_createInstanceFromZone 首要做了三件事,获取目标内存占用巨细并分配、设置 isa 指针、以及履行 C++ 结构办法。

获取内存占用并分配:cls->instanceSize(extraBytes); 经过在 cache 或者 ro()->instanceSize (编译时确认)获取占用内存,并进行内存对齐,最终调用 calloc 办法进行内存分配。

设置 isa 指针:设置如 has_cxx_dtor(是否有 C++ 析构函数)、shiftcls(类目标或者元类目标的地址)、extra_rc(引证计数) 等信息。

履行 C++ 结构办法:从基类开始向下递归履行 C++ 的结构函数。

// 简化后代码
static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
                              int construct_flags = OBJECT_CONSTRUCT_NONE,
                              bool cxxConstruct = true,
                              size_t *outAllocatedSize = nil)
{
    bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
    bool hasCxxDtor = cls->hasCxxDtor();
    bool fast = cls->canAllocNonpointer();
    size_t size;
    // 获取实例变量内存占用巨细
    size = cls->instanceSize(extraBytes);
    id obj;
    obj = (id)calloc(1, size);
    // 设置 isa 指针
    obj->initInstanceIsa(cls, hasCxxDtor);
    if (fastpath(!hasCxxCtor)) {
        return obj;
    }
    construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
    // 履行 C++ 结构函数
    return object_cxxConstructFromClass(obj, cls, construct_flags);
}

NSZone:它是为了防止内存碎片化而引入的结构,对内存分配的区域本身进行多重化办理,根据运用目标的意图、目标的巨细分配内存,然后提高了内存办理的功率。但目前运行时体系中的内存办理本身已极具功率,运用 NSZone 来办理反而会引起内存运用功率低下以及源代码复杂化等问题,因而运行时只是简略地忽略了 NSZone 的概念。

retain

retain 会将目标的引证计数 + 1,其首要逻辑首要会集在 rootRetain 办法中,引证计数一般会存储在两个当地,首先是 isa 指针的 extra_rc 域中,若有溢出则会将一半的引证计数值存储到 SideTable 中。

// 简化后代码
ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant)
{
    bool sideTableLocked = false;
    bool transcribeToSideTable = false;
    isa_t oldisa;
    isa_t newisa;
    oldisa = LoadExclusive(&isa.bits); // 加载 isa 指针
    do {
        newisa = oldisa;
        uintptr_t carry;
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++
        // 若 newisa.extra_rc++ 溢出,再调用一次,将 variant 设置为 RRVariant::Full
        if (slowpath(carry)) {
            if (variant != RRVariant::Full) {
                ClearExclusive(&isa.bits);
                return rootRetain_overflow(tryRetain);
            }
            // 留下一半的引证计数值,并将另一半拷贝到 SideTable中
            if (!tryRetain && !sideTableLocked) sidetable_lock();
            sideTableLocked = true;
            transcribeToSideTable = true;
            newisa.extra_rc = RC_HALF;
            newisa.has_sidetable_rc = true;
        }
    } while (slowpath(!StoreExclusive(&isa.bits, &oldisa.bits, newisa.bits)));
    if (variant == RRVariant::Full) {
        if (slowpath(transcribeToSideTable)) {
            // 拷贝一半的引证计数值到 SideTable 中
            sidetable_addExtraRC_nolock(RC_HALF);
        }
        if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
    }
    return (id)this;
}

关于 SideTable,其本身是大局的 SideTables() 的 value 元素,key 则是经过目标指针地址的偏移映射,找到归于目标的 SideTable,再经过目标的地址,取得归于目标的引证计数表。当 SideTable 中目标的引证计数溢出时,会将标志位(SIDE_TABLE_RC_PINNED)置为 1。

// 经过目标的地址偏移与 StripeCount 巨细映射到归于目标的 SideTable 的下标
static unsigned int indexForPointer(const void *p) {
    uintptr_t addr = reinterpret_cast<uintptr_t>(p);
    return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
}
// 参加额定的引证计数到 SideTable 中
bool 
objc_object::sidetable_addExtraRC_nolock(size_t delta_rc)
{
    SideTable& table = SideTables()[this];
    size_t& refcntStorage = table.refcnts[this];
    size_t oldRefcnt = refcntStorage;
    if (oldRefcnt & SIDE_TABLE_RC_PINNED) return true;
    uintptr_t carry;
    size_t newRefcnt = 
        addc(oldRefcnt, delta_rc << SIDE_TABLE_RC_SHIFT, 0, &carry);
    if (carry) {
        refcntStorage =
            SIDE_TABLE_RC_PINNED | (oldRefcnt & SIDE_TABLE_FLAG_MASK);
        return true;
    }
    else {
        refcntStorage = newRefcnt;
        return false;
    }
}

release

与 retain 对应,release 办法的首要逻辑会集在 rootRelease 中,它会将目标的引证计数 – 1,假如发生下溢(underflow),则会从 SideTable 中借取一半引证计数值,若引证计数为 0 则毁掉目标。

// 简化后代码
ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, objc_object::RRVariant variant)
{
    if (slowpath(isTaggedPointer())) return false;
    bool sideTableLocked = false;
    isa_t newisa, oldisa;
    oldisa = LoadExclusive(&isa.bits);
retry:
    do {
        newisa = oldisa;
        uintptr_t carry;
        newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc--
        if (slowpath(carry)) { // 发生下溢
            goto underflow;
        }
    } while (slowpath(!StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits)));
    // 毁掉目标
    if (slowpath(newisa.isDeallocating()))
        goto deallocate;
    return false;
 underflow:
    newisa = oldisa;
    if (slowpath(newisa.has_sidetable_rc)) {
        if (variant != RRVariant::Full) {
            ClearExclusive(&isa.bits);
            return rootRelease_underflow(performDealloc);
        }
        auto borrow = sidetable_subExtraRC_nolock(RC_HALF);
				bool emptySideTable = borrow.remaining == 0;
        if (borrow.borrowed > 0) {
            newisa.extra_rc = borrow.borrowed - 1;
            newisa.has_sidetable_rc = !emptySideTable;
        }
        if (emptySideTable)
                sidetable_clearExtraRC_nolock();
    }
deallocate:
    if (performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
    }
    return true;
}

dealloc

目标内存开释的首要逻辑会集在 rootDeallocobjc_destructInstance 办法里。其间,taggedPointer 目标无需开释(其在栈上存储)、若一起满足以下条件则直接 free,不然进入 objc_destructInstance

dealloc 在履行最终开释操作(release)的那个线程中被履行,而不是主线程;

dealloc 也不要运用 __weak __typeof(self)weak_self = self 这样的代码,这是由于在 weak 注册时会判别其是否处于 deallocating 状态,会发生溃散。

  1. isa.nonpointer 为 1,即存在 ISA_BITFIELD 位域数据
  2. 此目标不是其他目标的弱引证目标
  3. 此目标没有相关目标
  4. 没有 C++ 的析构函数
  5. 不存在 SideTable 记载引证计数
inline void
objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;  // fixme necessary?
    if (fastpath(isa.nonpointer                     &&
                 !isa.weakly_referenced             &&
                 !isa.has_assoc                     &&
#if ISA_HAS_CXX_DTOR_BIT
                 !isa.has_cxx_dtor                  &&
#else
                 !isa.getClass(false)->hasCxxDtor() &&
#endif
                 !isa.has_sidetable_rc))
    {
        assert(!sidetable_present());
        free(this);
    } 
    else {
        object_dispose((id)this);
    }
}

objc_destructInstance 则是整理目标相关的资源,C++ 的析构函数、相关目标、SideTable 中的弱引证指针和引证计数表,之后再 free。在 C++ 析构函数中,会遍历其一切的实例变量,形如 objc_storeStrong(&ivar, null) 调用,则会对一切的实例变量进行 release,并将其置为 nil。一起经由编译器刺进类似 [super dealloc],则会实现了由子类开始遍历到基类的 dealloc

void *objc_destructInstance(id obj)
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();
        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj, /*deallocating*/true);
        obj->clearDeallocating();
    }
    return obj;
}

不要在 init 和 dealloc 中调用 accessor 办法

init、和 dealloc 中,这个阶段处于未彻底初始化成功或者正在抛弃阶段,一起由于继承、多态特性,本来意图到调用父类的办法调用到了子类,就或许会出现错误,例如在 init 中:

@interface BaseClass : NSObject
@property(nonatomic) NSString* info;
@end
@implementation BaseClass
- (instancetype)init {  
    if ([super init]) {
        self.info = @"baseInfo"; 
    } 
    return self;
}
@end
@interface SubClass : BaseClass
@end
@interface SubClass ()
@property (nonatomic) NSString* subInfo;
@end
@implementation SubClass
- (instancetype)init {
     if (self = [super init]) {
         self.subInfo = @"subInfo"; 
    } 
    return self;
}
- (void)setInfo:(NSString *)info {
    [super setInfo:info]; 
    NSString* copyString = [NSString stringWithString:self.subInfo]; NSLog(@"%@",copyString);
}
@end

这时候创建一个 SubClass 实例变量,由于继承、多态特性会调用到子类的 setInfo,子类的 accessor 实现的代码彻底以子类已彻底初始化的前提编写的,此刻的 subInfo 还并未彻底初始化,进而会形成溃散。

这一点与 Swift 中,结构器在第一阶段结构完结之前,不能调用任何实例办法,不能读取任何实例特点的值,不能引证 self 作为一个值类似。

第一阶段:类中的每个存储型特点赋一个初始值。

第二阶段:给每个类一次机会,在新实例预备运用之前进一步自定义它们的存储型特点。

两段式结构进程的运用让结构进程更安全,一起在整个类层级结构中给予了每个类彻底的灵活性。两段式结构进程能够防止特点值在初始化之前被拜访,也能够防止特点被另外一个结构器意外地赋予不同的值。

在《Effective Objective-C 2.0 编写高质量iOS与OS X代码的有效52个有效办法》中也指出:

在dealloc里不要调用特点的存取办法,由于有人或许会覆写这些办法,并于其间做一些无法再收回阶段安全履行的操作。此外,特点或许正处于“键值调查”(Key-Value Observation,KVO)机制的监控之下,该特点的调查者(Observer)或许会在特点值改变时“保存”或运用这个行将收回的目标。这种做法会令运行期体系的状态彻底失调,然后导致一些不可思议的错误。

[1] Objective-C 高档编程 iOS与OS X多线程和内存办理

[2] ARC下dealloc进程及.cxxdestruct的探求

[3] 黑箱中的 retain 和 release

[4] 为什么不能在init和dealloc函数中运用accessor办法

[5] Swift结构进程