(原文出处:Objective-C Internals | Always Processing)

Objective-C 内存经过引证计数计划进行办理,它从一个相对简略的API开展成为一个杂乱的、高度优化的完结,一起保持源代码和ABI兼容性。

背景 OS X 10.7 和 iOS 5 引入了主动引证计数(ARC),经过消除样板代码并减少引证计数错误(泄漏和超开释)的表面积,进步了Objective-C程序员的出产功率

在ARC之前,-[NSObject retain]、-[NSObject release] [1]和-[NSObject autorelease] [2]办法是办理方针引证计数的仅有接口。直到OS X 10.8和iOS 6之前,NSObject的完结是Foundation的一部分,而不是Objective-C运转时的一部分。

ARC的设计者确认了一个关键要求,以进步该功用成功的或许性,从Apple试图向Objective-C添加废物收回的失利测验中吸取教训:主动引证计数有必要在同一进程中与手动引证计数透明地互操作,而不需求重新编译现有代码(例如,第三方二进制库)。

在macOS的早期版别中,一些方针重写了引证计数办法[3],以运用自己的完结,通常是出于性能原因。ARC有必要支撑与这些自定义引证计数完结的透明互操作,以满意上述要求。

进口点

引证计数操作有两个接口:长期存在的NSObject API和由ARC运用的编译器私有API,两者都调用一个中心完结。以下两个末节将分别查看每个接口的保存完结,下一节将讨论中心完结。

NSObject -[NSObject retain]的完结[4]是微乎其微的,它只是调用_objc_rootRetain来保存self。

// runtime/NSObject.mm 第 2502-2504 行
(id)retain {
    return _objc_rootRetain(self);
}
  • 术语”root”表明方针类层次结构中的根类收到了保存音讯。因而,类没有掩盖-retain或掩盖调用了超类办法,所以保存操作确保运用运转时的完结。(正如咱们将在下面的末节中看到的,不是一切的进口点都有这个确保。)

接下来,_objc_rootRetain函数,这也是微乎其微的,调用objc_object::rootRetain()。

// runtime/NSObject.mm 第 1875-1881 行
id _objc_rootRetain(id obj) {
    ASSERT(obj);
    return obj->rootRetain();
}

这个函数的存在是一个前史留传物。在第一个ARC完结中,这个函数是保存完结,可是在随后的版别中的各种重构中,它仍然存在。这个函数的仅有其他调用者是留传的Object类,它是用Objective-C++完结的,因而能够直接调用objc_object::rootRetain()。

最终,objc_object::rootRetain()调用rootRetain的一个重载版别。

// runtime/objc-object.h 第 607-611 行
id objc_object::rootRetain() {
    return rootRetain(false, RRVariant::Fast);
}

tryRetain 启用对加载弱引证的支撑[5]。该参数为false,由于弱引证无法履行此代码途径。 (运转时有必要首要从弱引证加载方针,然后方针才干接纳音讯,而经过加载操作取得的方针引证是强引证。)

variant 供给了有关调用途径的上下文,使中心完结能够省略不必要的作业。经过NSObject履行的保存操作运用RRVariant::Fast,以越过查看类是否具有自定义引证计数完结的过程,由于经过根类履行该操作在定义上不是自定义的。

主动引证计数

启用ARC时,编译器经过一个专用于ARC的编译器私有API履行引证计数操作,作为性能优化。该API答应引证计数操作直接调用Objective-C运转时,越过发送音讯的开销。

// runtime/NSObject.mm 第 1772-1777 行
id objc_retain(id obj) {
    if (_objc_isTaggedPointerOrNil(obj)) return obj;
    return obj->retain();
}

这个函数首要查看方针指针的值,并且假如它不引证堆上的方针,则当即回来。这或许发生在两种状况下:

指针为nil。向nil发送音讯是合法的,因而有必要支撑对-[NSObject retain]的这种优化也支撑nil指针。

指针是符号指针。符号指针是Objective-C运转时的完结细节,编译器无法看到,因而编译器无法消除保存操作。符号指针不参与引证计数(没有盯梢堆分配),所以不需求持续履行。

假如方针指针的值引证堆上的方针,则函数调用objc_object::retain()履行保存操作。

// runtime/objc-object.h 第 589-596 行
inline id objc_object::retain() {
  ASSERT(!isTaggedPointer());
  return rootRetain(false, RRVariant::FastOrMsgSend);
}

这个函数调用中心完结(虽然在这一点上rootRetain中的root是一个误称):

tryRetain 设置为false,原因与上面在NSObject进口点中讨论的相同。

RRVariant::FastOrMsgSend 作为variant。请注意,这个函数的称号不包含术语root,由于尚未进行introspection,无论是直接的(参见下面的rootRetain)仍是直接的(经过音讯发送,参见上面的NSObject),所以尚不知道方针的类是否掩盖了任何引证计数办法。称号中的MsgSend部分指示中心完结在需求时履行必要的introspection,以经过音讯发送履行操作。

rootRetain

objc_object::rootRetain(bool, RRVariant)函数有一些规划较大,因而咱们将逐一部分进行分析。

// runtime/objc-object.h 第 622 行
if (slowpath(isTaggedPointer())) return (id)this;

虽然ARC进口点查看符号指针,NSObject进口点却不会。我暂时无法当即理解为什么NSObject的完结不履行此查看,但有必要在某个地方履行,而在这个运转时版别中,它在这里履行。

接下来,运转时加载方针的isa值。

// runtime/objc-object.h 第 624-630 行
bool sideTableLocked = false;
bool transcribeToSideTable = false;
isa_t oldisa = LoadExclusive(&isa().bits);
isa_t newisa;

isa存储了方针的引证计数在一切现代Apple平台上。Objective-C运转时运用ARM的独占监督同步原语来办理arm64架构上的并发性,这就是LoadExclusive函数的称号。在包含arm64e在内的一切其他体系结构上,Objective-C运转时运用C11原子操作。(我不确认这里是arm64仍是arm64e是异常状况,或许为什么。)

假如编译器私有API是保存操作的进口点,运转时有必要查看类是否掩盖了任何引证计数办法。

// runtime/objc-object.h 第 632-642 行
if (variant == RRVariant::FastOrMsgSend) {
  // These checks are only meaningful for objc_retain()
  // They are here so that we avoid a re-load of the isa.
  if (slowpath(oldisa.getDecodedClass(false)->hasCustomRR())) {
    ClearExclusive(&isa().bits);
    if (oldisa.getDecodedClass(false)->canCallSwiftRR()) {
      return swiftRetain.load(memory_order_relaxed)((id)this);
    }
    return ((id(*)(objc_object *, SEL))objc_msgSend)(this, @selector(retain));
  }
}

自定义引证计数完结很少见,所以运转时运用其slowpath()宏来提示CPU的分支猜测单元,这条途径不太或许运转。getDecodedClass()回来方针的Class方针,它有一个标志,指示类是否掩盖了任何引证计数办法。这个快速查看为ARC进口点供给了支撑自定义引证计数完结的必要类introspection,开销最小。

getDecodedClass()中的”decoded”一词或许是指从非指针isa中提取类方针指针。这个函数的具体细节取决于方针体系结构:

arm64_32:Apple Watch ABI运用32位指针,因而其非指针isa存储在一个表中的索引中,该表存有类方针(没有足够的位数能够运用指针值存储额定数据)。

假如isa不是指针,则函数调用classForIndex()来从表中获取类方针。

不然,假如isa是指针,则它是一个指向类方针的指针,因而函数将isa位按原样回来。

在一切其他方针体系结构上,这个函数是getClass()的别名,它经过从isa值中掩码出非指针位来回来类方针。

arm64e:假如启用了isa指针认证,则函数运用编译器核算的掩码提取类方针指针值。可是,函数会越过认证,由于调用者为authenticate传递false。不进行认证的文档原因是认证是作为objc_msgSend的一部分进行的,因而不需求额定的认证。

不然,函数运用静态掩码定义提取类方针指针值。

假如类具有自定义引证计数完结,运转时会发送一个-retain音讯给方针,以完结ARC启动的保存操作。请注意,方针随后或许会调用-[NSObject retain],可是此代码块不会再次履行,由于variant将是RRVariant::Fast。

纯Swift类(即,不是承继自NSObject的类)为了支撑将纯Swift方针桥接到Objective-C,派生自SwiftObject类(仅适用于Apple平台)。由于Swift运用自己的引证计数体系,所以SwiftObject完结了引证计数办法,以支撑将纯Swift方针桥接到Objective-C。为了优化这种状况,Objective-C运转时直接调用Swift运转时的swift_retain()函数[6](而不是经过发送音讯保存方针)。

接着持续下一个代码块。

// runtime/objc-object.h 第 644-651 行
if (slowpath(!oldisa.nonpointer)) {
  // a Class is a Class forever, so we can perform this check once
  // outside of the CAS loop
  if (oldisa.getDecodedClass(false)->isMetaClass()) {
    ClearExclusive(&isa().bits);
    return (id)this;
  }
}

类方针永久不会被开释,因而不需求引证计数。因而,假如方针是类方针,则函数回来方针本身,而不履行任何其他操作。

比较和交流循环

比较和交流循环是保存完结的中心部分。它从(重新)初始化循环的开端状况开端。

// runtime/objc-object.h 第 654-655 行
do {
  transcribeToSideTable = false;
  newisa = oldisa;

它将newisa设置为当时的isa值(即oldisa),循环将更新它以反映增加的保存计数。接下来的末节将查看transcribeToSideTable的运用。

// runtime/objc-object.h 第 656-660 行
if (slowpath(!newisa.nonpointer)) {
    ClearExclusive(&isa().bits);
    if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
    else return sidetable_retain(sideTableLocked);
}

首要,循环查看方针实例是否具有非指针isa。假如没有,则将保存计数记录在侧表中[7]。这个查看在循环中履行,由于假如此线程在比较和交流之前丢失了比较和交流,这或许是由于另一个线程以一种办法突变了方针,以使其不再运用非指针isa。

接下来,循环查看是否丢失了另一个竞赛。

// runtime/objc-object.h 第 661-673 行
// 不要查看newisa.fast_rr;咱们现已调用了任何RR重写
if (slowpath(newisa.isDeallocating())) {
    ClearExclusive(&isa().bits);
    if (sideTableLocked) {
        ASSERT(variant == RRVariant::Full);
        sidetable_unlock();
    }
    if (slowpath(tryRetain)) {
        return nil;
    } else {
        return (id)this;
    }
}

在线程测验在(至少)以下三种状况下保存方针的一起,方针或许正在进行毁掉:

tryRetain为true,并且此线程在开端开释方针之前失去了加载弱方针的竞赛。函数回来nil,表明无法取得强引证。在这种状况下,调用者objc_loadWeakRetained()持有弱引证侧表的锁,防止方针被开释,因而从方针指针读取isa是定义行为。

另一个线程开释了方针,导致它被毁掉,通常是由于在进程一起从强非原子属性读取和写入时发生的竞赛条件。这种状况下的一切都是未定义行为。函数回来self以满意-retain合同,但它将成为一个悬挂指针,简直必定会在线程的不久将来导致溃散。在竞赛条件中触发此代码途径是“走运”的。实践上,经过悬挂指针读取的isa位或许会将这个函数引导到恣意数量的方向,导致不可猜测的效果。

在-dealloc中的逻辑导致进行保存操作(例如,-dealloc完结将self传递给整理例程,ARC编译器会宣布retain/release对)。请注意,这种状况不是与上述两种状况相同的竞赛条件,由于保存是在履行开释的同一线程上测验的。但是,假如履行保存操作的函数需求方针实例在其调用范围内存活(例如,在另一个方针的强属性中存储self),这种状况或许导致未定义行为,由于当毁掉完结时,指针将变为悬挂指针。

最终,咱们来到实践的递增部分。

// runtime/objc-object.h的674-675行
uintptr_t carry;
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++

回想一下,非指针isa是一个位字段,有三种变体。RC_ONE的值是当将位字段视为整数时表明保存计数为1的位。保存计数存储在isa的最高位中,因而假如一切保存计数位都在运用中(在下一末节中讨论),则会发生溢出或进位。假如没有溢出发生,newisa包含递增的保存计数,假如溢出发生,则预备将溢出保存计数的一半发送到侧表。

// runtime/objc-object.h的691行
} while (slowpath(!StoreExclusive(&isa().bits, &oldisa.bits, newisa.bits)));

假如&isa()的值与&oldisa中的值匹配,则比较和交流操作成功,并将newisa的值写入&isa(),循环结束。

不然,&isa()的值自从这个线程将其加载到oldisa中以来发生了改变。比较和交流操作失利,并将&isa()中的新值写入&oldisa。循环持续,直到线程赢得比较和交流操作,或许另一个线程将方针状况更改为激活上面的一个回来途径。

完好变体

假如保存计数溢出了非指针isa中的位,运转时将运用侧表来存储保存计数的一部分。

// runtime/objc-object.h的677-690行
if (slowpath(carry)) {
    // newisa.extra_rc++溢出
    if (variant != RRVariant::Full) {
    ClearExclusive(&isa().bits);
    return rootRetain_overflow(tryRetain);
}
// 保存计数的一半内联,预备将另一半复制到侧表中。
if (!tryRetain && !sideTableLocked) sidetable_lock();
sideTableLocked = true;
transcribeToSideTable = true;
newisa.extra_rc = RC_HALF;
newisa.has_sidetable_rc = true;
}

假如此函数调用运用Fast或FastOrMsgSend变体,则它中止保存操作的测验,并将责任转移到rootRetain_overflow()。

// runtime/objc-object.h的1372-1376行
NEVER_INLINE id objc_object::rootRetain_overflow(bool tryRetain) {
    return rootRetain(tryRetain, RRVariant::Full);
}

我猜测此函数的目的是为了在堆栈盯梢中供给一个结构,以协助Apple工程师扫除运转时中的保存溃散问题,由于侧表锁定(运用非可重入自旋锁)的相互作用或许很难推理

假如在Full变体中保存计数溢出,则完结会将保存计数的一半发送到侧表,并将另一半保存在非指针isa中。将保存计数分成一半是为了最小化侧表拜访的数量(需求较少的CPU指令和较少的锁获取)。假如完结只发送了溢出位到侧表,溢出鸿沟值处的引证计数操作或许会对体系发生性能影响。

// runtime/objc-object.h的693-700行
    if (variant == RRVariant::Full) {
    if (slowpath(transcribeToSideTable)) {
    // 将保存计数的另一半复制到侧表中。
    sidetable_addExtraRC_nolock(RC_HALF);
    }
    if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
    }

在比较和交流成功之后,假如有必要,更新侧表:

回来self

在比较和交流成功之后,假如有必要,更新侧表,然后回来self。

// runtime/objc-object.h的702-703行
if (slowpath(variant == RRVariant::Full && !tryRetain)) {
    sidetable_unlock();
}
return (id)this;
rootRetain_overflow()

rootRetain_overflow()完结十分简略。假如传入的tryRetain参数为true,它会回来nil,由于不能成功获取强引证。

// runtime/objc-object.h的1378-1379行
id objc_object::rootRetain_overflow(bool tryRetain __unused) {
    return nil;
}

不然,它将履行与rootRetain()的完好变体相同的逻辑,但无需履行比较和交流操作。这意味着它会在方针引证计数的一半增加到侧表之前回来。

// runtime/objc-object.h的1381-1385行
    return sidetable_retain(tryRetain, RC_ONE, 0, false);
}

rootRetain的完好变体将处理此调用,然后持续履行完好的逻辑。

结论

Objective-C内存办理的中心是引证计数。此文章深入探讨了引证计数操作的内部机制,涵盖了从NSObject的保存办法开端,一直到在方针的isa值上履行比较和交流操作,以及在溢出状况下将引证计数的一半复制到侧表中的状况。

理解Objective-C内存办理的内部机制能够协助开发人员编写更健壮的代码,并更好地利用ARC等主动引证计数工具。这关于构建高性能和稳定的iOS和macOS应用程序至关重要。