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
目标内存开释的首要逻辑会集在 rootDealloc
与 objc_destructInstance
办法里。其间,taggedPointer 目标无需开释(其在栈上存储)、若一起满足以下条件则直接 free
,不然进入 objc_destructInstance
。
dealloc
在履行最终开释操作(release)的那个线程中被履行,而不是主线程;
在 dealloc
也不要运用 __weak __typeof(self)weak_self = self
这样的代码,这是由于在 weak 注册时会判别其是否处于 deallocating
状态,会发生溃散。
-
isa.nonpointer
为 1,即存在ISA_BITFIELD
位域数据 - 此目标不是其他目标的弱引证目标
- 此目标没有相关目标
- 没有 C++ 的析构函数
- 不存在 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结构进程