OC知识梳理:内存管理

1,384 阅读12分钟

内存布局

1. 内核区

  • 内核占用的内存区域。

2. 栈区(Stack)

  • 存放函数的参数值、局部变量的值、对象的指针地址;
  • 编译器自动分配释放;
  • 快速高效,但操作方式不够灵活(类似数据结构栈FIFO);
  • 栈区地址分配方式:由高到低。

优化技巧

  • 不要在一个函数内大量使用局部变量(例如循环创建),可以通过使用AutoReleasePool自动释放池及时释放来进行优化。
  • 注意:局部变量的回收释放是会使用GC(垃圾回收)的,会消耗性能。尽量封装方法,使用方法嵌套,加快函数的读取效率,用空间换时间。

栈区地址分配由高到低,堆区地址分配由低到高,一旦碰头,会造成堆栈溢出。

3. 堆区(heap)

  • iOS中创建的对象都是存储在堆区(alloc);
  • 开发者管理(若开发者不回收,在程序结束时,由系统回收);
  • 速度相对较慢,操作方式灵活(类似链表),会造成内存碎片化;
  • 堆区地址分配方式:由低到高。

优化技巧

  • 对象的创建和回收会消耗大量内存和性能,避免创建过多的对象,超出临界值会造成堆栈溢出。
  • 方法和函数还是有细微区别的,两者之间尽量使用函数。

4. 全局(静态)区(.bbs/data)

  • 存储全局变量或者静态变量(static修饰);
  • 程序结束后由系统进行释放;
  • static int a;未初始化的全局静态变量存放在全局区的.bbs段;
  • static int a = 10;已初始化的全局静态变量存放在全局区的data段。

优化技巧

  • 注意静态变量的作用域(.h和.m的作用域不同,避免在头文件.h中定义全局变量)。
  • 避免对已经定义的静态变量进行重新赋值。

5. 常量区

  • 存储已使用的字符串常量(例如:const、extern修饰的字符串);
  • 程序结束后由系统释放;
  • 相同字符串地址一致(C语言的编译优化)。

6. 代码区(.text)

  • 存储编译后的代码的区域;
  • 包括操作码和要操作的对象或对象的地址引用。

7. 保留段

  • 保留的一块内存区域。

数据结构

1. TaggedPointer

  • Tagged Pointer是专⻔⽤来存储⼩的对象,例如NSNumber,NSDate等。
  • Tagged Pointer指针的值不再是地址了,⽽是真正的值。所以,实际上它不再是⼀个对象了,它只是⼀个披着对象⽪的普通变量⽽已。所以,它的内存并不存储在堆中,也不需要malloc和free。
  • 当指针不够存储数据时,就会使用动态分配内存的方式来存储数据。

2. NONPOINTER_ISA

  • indexed(0):标记是否是纯的ISA指针,还是非指针型的NOPOINTER_ISA指针(0:纯isa指针;1:NOPOINTER_ISA指针);
  • has_assoc(1):标记是否有关联对象(0没有,1存在);
  • has_cxx_dtor(2):该对象是否有C++或者Objc的析构器,如果有析构函数,则需要做析构逻辑,如果没有,则可以更快的释放对象;
  • shiftcls(3~35):标记当前对象的类对象的指针地址,占用33位;
  • magic(36~41):用于调试器判断当前对象是真的对象还是没有初始化的空间;
  • weakly_referenced(42):标记对象是否有弱引用指针,没有弱引用的对象可以更快释放;
  • deallocating(43):标记对象是否正在进行dealloc操作;
  • has_sidetable_rc(44):标记是否有sidetable结构用于存储引用计数;
  • extra_rc(45~63):标记对象的引用计数(首先会存储在该字段中,当到达上限后,再存入对应的引用计数表中)。

3. SideTables

SideTables是一个64个元素长度的hash数组,里面存储了SideTable。SideTables的hash键值就是一个对象obj的address。 因此可以说,一个obj,对应了一个SideTable。但是一个SideTable,会对应多个obj。因为SideTable的数量只有64个,所以会有很多obj共用同一个SideTable。

SideTable

  • spinlock_t slock:锁,用于上锁/解锁SideTable(自旋锁已被废弃,这里名字为spinlock,底层实现其实是os_unfair_lock);
  • RefcountMap refcnts:以DisguisedPtr<objc_object>为key的hash表,用来存储OC对象的引用计数(仅在未开启isa优化,或在isa优化情况下,isa_t的引用计数溢出时才会用到);
  • weak_table_t weak_table:存储对象弱引用指针的hash表。是OC中weak功能实现的核心数据结构。

RefcountMap 引用计数表中存储的值是size_t,实际上是一个无符号的长整型,前两个比特位分别为weakly_referenced和deallocating,用来表示是否有弱引用指针和是否正在进行dealloc,剩下的才是该对象的引用计数值,所以要计算某个对象具体的引用计数值时,要向右偏移两位。

weak_table_t 弱引用表中的Key为对象指针,Value为weak_entry_t,weak_entry_t是一个结构体数组,数组中的元素实际上就是OC对象的弱引用信息。

何谓自旋锁? 它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。

哈希查找

  • 散列表(Hash table,也叫哈希表),是根据键(Key)而直接访问在内存存储位置的数据结构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做散列函数,存放记录的数组称做散列表。
  • 散列函数的规则是:通过某种转换关系,使关键字适度的分散到指定大小的的顺序结构中,越分散,则以后查找的时间复杂度越小,空间复杂度越高。
  • Hash是一种典型以空间换时间的算法,比如原来一个长度为100的数组,对其查找,只需要遍历且匹配相应记录即可,从空间复杂度上来看,假如数组存储的是byte类型数据,那么该数组占用100byte空间。现在我们采用Hash算法,我们前面说的Hash必须有一个规则,约束键与存储位置的关系,那么就需要一个固定长度的hash表,此时,仍然是100byte的数组,假设我们需要的100byte用来记录键与位置的关系,那么总的空间为200byte,而且用于记录规则的表大小会根据规则,大小可能是不定的。

引用计数

1. ARC

在Objective-C中采用ARC机制,让编译器来进行内存管理。在新一代Apple LLVM 编译器中设置ARC为有效状态,就无需再次键入retain或者release代码,这在降低程序崩溃、内存泄漏等风险的同时,很大程度上减小了开发程序的工作量。编译器完全清楚目标对象,并能立刻释放那些不再被使用的对象。如此一来,应用程序将具有可预测性,且能流畅运行,速度也将大幅度提升。

  • ARC是编译器和Runtime协作的结果。
  • ARC中禁止手动调用retain/release/retainCount/dealloc
  • ARC中新增了weakstrong属性关键字。

例如非指针型isa中的nonpointerweakly_referencedhas_sidetable_rcextra_rc都和ARC有直接的关系。

2. alloc

  • 经过一系列调用,最终调用了C函数calloc。
  • 此时并没有设置引用计数为1。

alloc后通过retainCount获取引用计数为1。

3. retain

  1. SideTable& table = SideTables()[this];通过当前对象的指针,到SideTables中,获取该对象所在的SideTable。
  2. size_t& refcntStorage = table.refcnts[this];通过当前对象的指针,在SideTable的引用计数表中,获取该对象的引用计数。
  3. refcntStorage += SIDE_TABLE_RC_ONE;因为size_t前两个比特位存储了特殊的值,所以这里的SIDE_TABLE_RC_ONE考虑到了两位二进制的偏移量,实际上加了4,反应在引用计数上还是加了1。

4. release

  1. SideTable& table = SideTables()[this];
  2. RefcountMap::iterator it = table.refcnts.find(this);根据当前对象指针访问table中的引用计数表。
  3. it->second -= SIDE_TABLE_RC_ONE;将对应的值减去SIDE_TABLE_RC_ONE。

5. retainCount

  1. SideTable& table = SideTables()[this];
  2. size_t refcnt_result = 1;声明一个局部变量,指定它的值为1。
  3. RefcountMap::iterator it = table.refcnts.find(this);通过当前对象,到引用计数表中查找。
  4. refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;将查找的结果向右偏移,与之前声明的局部变量相加,最后返回给调用方。

新alloc的对象,在引用计数表中其实没有相关联的key/value映射,但在获取retainCount时,会获取到值为1。

6. dealloc

// Replaced by NSZombies
- (void)dealloc {
    _objc_rootDealloc(self);
}

_objc_rootDealloc实现:

void
_objc_rootDealloc(id obj)
{
    ASSERT(obj); // 断言 验证对象

    obj->rootDealloc();
}

rootDealloc()实现:

inline void
objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;  // fixme necessary? // 是否是标记指针

    if (fastpath(isa.nonpointer                     && // 是否进行了isa优化
                 !isa.weakly_referenced             && // 是否存在弱引用指向
                 !isa.has_assoc                     && // 是否设置了关联对象
#if ISA_HAS_CXX_DTOR_BIT
                 !isa.has_cxx_dtor                  && // 是否有C++的析构函数
#else
                 !isa.getClass(false)->hasCxxDtor() &&
#endif
                 !isa.has_sidetable_rc)) // 引用计数是否过大无法存储在isa中
    {
        assert(!sidetable_present()); // 满足条件直接释放
        free(this);
    } 
    else {
        object_dispose((id)this); // 不满足条件执行object_dispose()
    }
}

object_dispose()实现:

id 
object_dispose(id obj)
{
    if (!obj) return nil;

    objc_destructInstance(obj); // 销毁实例
    free(obj); // 释放对象

    return nil;
}

objc_destructInstance()实现:

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;
}

clearDeallocating()调用流程

inline void 
objc_object::clearDeallocating()
{
    if (slowpath(!isa.nonpointer)) {
        // Slow path for raw pointer isa.
        sidetable_clearDeallocating();
    }
    else if (slowpath(isa.weakly_referenced  ||  isa.has_sidetable_rc)) {
        // Slow path for non-pointer isa with weak refs and/or side table data.
        clearDeallocating_slow();
    }

    assert(!sidetable_present());
}

clearDeallocating_slow()实现:

NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
    ASSERT(isa.nonpointer  &&  (isa.weakly_referenced || isa.has_sidetable_rc));

    SideTable& table = SideTables()[this];
    table.lock();
    if (isa.weakly_referenced) {
        weak_clear_no_lock(&table.weak_table, (id)this); // 将指向该对象的弱引用指针置为nil。
    }
    if (isa.has_sidetable_rc) {
        table.refcnts.erase(this); // 从引用计数表中擦除该对象引用计数
    }
    table.unlock();
}

弱引用

1. weak实现原理

一个被声明为__weak的对象指针,经过编译器编译,会调用objc_initWeak()方法,然后经过一系列的函数调用栈,最终在weak_register_no_lock()函数中进行弱引用变量的添加。具体添加的位置是通过一个哈希算法来计算的:

  • 如果查找的位置已经有了当前对象对应的弱引用数组weak_entry_t,我们就将新的弱引用变量添加到该数组中;
  • 如果没有查找到,就创建一个新的弱引用数组,并将该指针添加到第0位,其他位置初始化为nil。

2. 当weak指向的对象被释放时,是如何让weak指针置为nil的?

当一个对象被释放时,在dealloc的内部实现中,会判断该对象是否存在弱引用指向,若存在则最终会调用弱引用清除的相关函数weak_clear_no_lock(),在该函数中会根据当前对象指针查找弱引用表,然后遍历当前对象所有的弱引用指针并置为nil。

自动释放池

1. 数据结构

  • 是以栈为节点通过双向链表的形式组合而成。
  • 是和线程一一对应的。
// class AutoreleasePoolPage;
__unsafe_unretained id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;

AutoreleasePoolPage::pop()

  • 根据传入的哨兵对象找到对应位置。
  • 给上次push操作之后添加的对象依次发送release消息。
  • 回退next指针到正确位置。

2. 自动释放池何时释放?

在当次runloop将要结束的时候调用AutoreleasePoolPage::pop()。同时会push一个新的AutoreleasePool。

3. 自动释放池为何可以嵌套使用?

多层嵌套就是多次插入哨兵对象。

4. 自动释放池的使用场景。

在for循环中alloc图片数据等内存消耗较大的场景手动插入autoreleasePool。

循环引用

1. 什么情况下需要注意循环引用?

  • 代理
  • Block
  • NSTimer
  • 大环引用

2. 如何破除循环引用?

  • 避免产生循环引用
  • 在合适的时机手动断环

3. 具体的解决方案都有哪些?

  • __weak
  • __block
  • __unsafe_unretained

__block

  • MRC下,__block修饰对象不会增加其引用计数,避免了循环引用。
  • ARC下,__block修饰对象会被强引用,无法避免循环引用,需手动解环。

__unsafe_unretained

  • 修饰对象不会增加其引用计数,避免了循环引用。
  • 如果被修饰对象在某一时机被释放,会产生悬垂指针,导致内存泄漏。

4. 如何解决NSTimer造成的循环引用?

由于NSTimer被分派后,会被当前线程的Runloop强引用,所以无法通过对象弱引用NSTimer的方法解决循环引用问题。

通过创建一个中间对象,令中间对象持有两个弱引用变量,非别为原对象和NSTimer;在中间对象中实现NSTimer的回调,并在回调前增加一道判断,检查中间对象持有的原对象是否为nil;若原对象存在则执行回调,若原对象已经被释放则将NSTimer设为无效状态。

// NSTimer+WeakTimer.h

#import <Foundation/Foundation.h>

@interface NSTimer (WeakTimer)

+ (NSTimer *)scheduledWeakTimerWithTimeInterval:(NSTimeInterval)ti
                                         target:(id)aTarget
                                       selector:(SEL)aSelector
                                       userInfo:(id)userInfo
                                        repeats:(BOOL)yesOrNo;

@end
// NSTimer+WeakTimer.m

#import "NSTimer+WeakTimer.h"

@interface TimerWeakObject : NSObject

@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL selector;
@property (nonatomic, weak) NSTimer *timer;

- (void)fire:(NSTimer *)timer;

@end

@implementation TimerWeakObject

- (void)fire:(NSTimer *)timer {
    if (self.target) {
        if ([self.target respondsToSelector:self.selector]) {
            [self.target performSelector:self.selector withObject:timer.userInfo];
        }
    } else {
        [self.timer invalidate];
    }
}

@end

@implementation NSTimer (WeakTimer)

+ (NSTimer *)scheduledWeakTimerWithTimeInterval:(NSTimeInterval)ti
                                         target:(id)aTarget
                                       selector:(SEL)aSelector
                                       userInfo:(id)userInfo
                                        repeats:(BOOL)yesOrNo {
    TimerWeakObject *object = [[TimerWeakObject alloc] init];
    object.target = aTarget;
    object.selector = aSelector;
    object.timer = [NSTimer scheduledTimerWithTimeInterval:ti target:object selector:@selector(fire:) userInfo:userInfo repeats:yesOrNo];
    return object.timer;
}

@end