內存管理

內存布局

內存布局.png
  • stack(棧區): 方法調用
  • heap(堆區):通過alloc等分配的對象
  • bss:未初始化的全局變量或靜態變量等。
  • data:已初始化的全局變量等。
  • text:程序的代碼段

內存管理方案

iOS是如何對內存進行管理的?

  • TaggedPointer:對一些小對象如NSNumber

  • NONPOINTER_ISA: 對于64位架構下的應用程序

    在64位架構下,isa指針占用64位bit,實際有32位或者40位就夠用了,剩余的實際上是浪費的,蘋果為了提高內存利用率,在這些剩余的bit位當中,存儲了一些關于內存管理的相關數據內容,所以稱為非指針型的isa

  • 散列表

    散列表是一個復雜的數據結構,其中包含了應用計數表和弱引用計數表。

NONPOINTER_ISA結構

arm64架構

nonpointer_isa01.png
nonpointer_isa02.png
  • 第0號位是indexed的標志位,如果這個位置是0,代表的是我們使用的isa指針只是一個純的isa指針,它里面的內容就直接代表了當前對象的類對象的地址;如果這個位置是1,就代表這個isa指針里面存儲的不僅是他的類對象的地址,而且還有一些內存管理方面的數據。

  • 第1號位has_assoc是表示當前對象是否有關聯對象,0沒有,1有。

  • 第2位has_cxx_dtor,表示的是當前對象是否有使用到C++相關的一些代碼,或者C++語言方面的一些內容。在ARC中也可以通過這個標志位,來表示有些對象是通過ARC來進行內存管理的。

  • 后面的3-35位shiftcls,表示當前對象的類對象指針地址。

  • 后面的6位是一個magic字段

  • 后面是一位weakly_referenced,標識這個對象是否有相應的弱引用指針。

  • deallocating,表示的是當前對象是否在進行dealloc操作

  • has_sidetable_rc,表示的是當前這個isa指針當中,如果所存儲的引用計數已經達到了上限的話,需要外掛一個sidetable數據結構,去存儲相關的引用計數內容(也就是散列表)

  • extra_rc額外的引用計數,當我們引用計數在一個很小的值得范圍之內就會存到isa指針當中,而不是由單獨的引用計數表去存他的引用計數。

散列表方式

SideTables()源碼
static StripedMap<SideTable>& SideTables() {
    return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}
SideTables()結構
sideTable.png

side tables實際上是一個hash表,通過一個對象指針,找到他對應的引用計數表,或弱引用表。

Side Table

SideTable源碼
struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;

    SideTable() {
        memset(&weak_table, 0, sizeof(weak_table));
    }

    ~SideTable() {
        _objc_fatal("Do not delete SideTable.");
    }

    void lock() { slock.lock(); }
    void unlock() { slock.unlock(); }
    void forceReset() { slock.forceReset(); }

    // Address-ordered lock discipline for a pair of side tables.

    template<HaveOld, HaveNew>
    static void lockTwo(SideTable *lock1, SideTable *lock2);
    template<HaveOld, HaveNew>
    static void unlockTwo(SideTable *lock1, SideTable *lock2);
};

SideTable結構

sideTable02.png

為什么不是一個side table?

one_side_table03.png

假如說只有一張side table,相當于我們在內存當中分配的所有對象的引用計數或者說弱引用存儲都放在一張大表當中,這個時候如果我們要操作某一個對象的引用計數值進行修改,比如說進行加1或減1的操作的話,由于所有的對象可能是在不同的線程當中去分配創建的,包括調用他們的release,retain等方法,也可能是在不同的線程當中進行操作;這個時候對一種表進行操作的時候,需要進行加鎖處理,才能保證對于數據的訪問安全,在這個過程中就存在了一個效率問題。比如說用戶的內存空間一共有4GB,那么可能分配出成千上百萬個內存對象,如果說每一個對象在對他進行內存引用計數的改變的時候,都操作這張表很顯然就會有效率的問題。如果說已經又一個對象在操作這張表,下一個對象就要等他操作完,把鎖釋放之后再進行操作,這效率就會太低了。

one_side_table04.png

系統為了解決效率問題,引入了分離鎖的技術方案。我們可以把內存對象所對應的引用計數表,可以分拆成多個部分。比如說分拆成8個,需要對8個表分別加鎖。當A和B同時進行引用計數操作的話可以進行并發操作,如果是一張表他們需要進行順序操作。很明顯分離鎖可以提高訪問效率。

怎樣實現快速分流?

快速分流指通過一個對象的指針如何快速定位到它屬于那張side table 表當中。

hash表.png

side tables的本質是一張Hash表。這張hash表當中,可能有64張具體的side table 存儲不同對象的引用計數表和弱引用表。

自旋鎖 Spinlock_t

  • Spinlock_t是"忙等"的鎖。

    如果當前鎖已被其他線程獲取,那么當前線程會不斷的探測這個鎖是否有被釋放,如果釋放掉,自己第一時間去獲取這個鎖。所以說自旋鎖是一種忙等的鎖。獲取不到鎖的時候,他會他自己的線程阻塞休眠,然后等到其他線程釋放這個鎖的時候來喚醒當前線程。

  • 適用于輕量訪問。

引用計數表RefcountMap

引用計數表實際上是一個hash表,我們可以通過指針來找到對應對象的引用天計數,這一過程實際上也是hash查找(使用hash查找是為了提高查找效率)。

refcount_map.png

插入和獲取是通過同一個hash函數完成,避免了遞歸查找和for循環遍歷

size_t內存分配

size_t.png
  • 第一個二進制位表示的是weakly_referenced,對象是否有弱引用,0沒有,1有。
  • 第二位deallocating表示當前對象是否處于dealloc
  • 后面(RC)存儲的是對象的實際引用計數值,在實際計算這個引用計數值,需要向右偏移兩位,因為后面兩位需要去掉。

弱引用表weak_table_t

weak_table_t實際上也是一個hash表.

weak_table_t.png

weak_entry_t實際上是一個結構體數組。結構體數組存儲的是每一個的弱引用指針,也就是代碼當中定義的__weak id obj,obj內存地址即指針就存儲在weak_entry_t

MRC & ARC

MRC 手動引用計數

  • alloc: 用來分配一個對象的內存空間。
  • retain:對一個對象的引用計數加1;
  • release:對一個對象的引用計數減1;
  • retainCount:獲取當前對象的引用計數值
  • autorelease:如果調用了一個對象的autorelease方法,當前這個對象會在autoreleasepool結束的時候,調用他的release操作進行引用計數減1.
  • dealloc:在MRC當中調用dealloc方法需要顯式調用[super dealloc]來釋放或廢棄父類的相關成員變量。

ARC 自動引用計數

  • ARC是LLVM和Runtime協作來進行自動引用計數管理;
  • ARC中禁止手動調用retain,release,retainCount,dealloc,并且在ARC中可以重寫某個對象的dealloc方法,但是不能再dealloc方法當中,顯示調用[super dealloc]
  • ARC中新增了weak,strong屬性關鍵字。

ARC實際是由編譯期自動為我們插入retainrelease操作之外,還需要runtime的功能進行支持,然后由編譯器和Runtime共同協作才能組成ARC的全部功能。

引用計數管理

實現原理分析

  • alloc
  • retain
  • release
  • retainCount
  • dealloc

alloc實現

  • 經過一系列調用,最終調用了C函數calloc
  • 此時并沒有設置引用計數為1

retain實現

id
objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
    assert(!isa.nonpointer);
#endif
  //hash查找SideTable
    SideTable& table = SideTables()[this];
    
  //SideTable加鎖
    table.lock();
  //hash查找引用計數值
    size_t& refcntStorage = table.refcnts[this];
    if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
      //引用計數加1
      //#define SIDE_TABLE_RC_ONE            (1UL<<2)
        refcntStorage += SIDE_TABLE_RC_ONE;
    }
    table.unlock();

    return (id)this;
}
  1. 通過當前對象的指針this,經過hash函數的計算,可以快速的SideTables當中找到(hash查找)它對應的SideTable
  2. 然后在SideTable當中獲取應用計數map這個成員變量,通過對象的指針this,在SideTable的引用計數表中獲取(hash查找)當前當前對象的引用計數值。
  3. 經過一定的條件判斷之后,引用計數加1。

引用計數加1,實際是加上了偏移量對應的操作,這個偏移量是4,反應出來的結果是加1,因為size_t64位,前兩位不是存儲引用計數,所以需要向左偏移兩位操作1UL<<2

release實現

uintptr_t
objc_object::sidetable_release(bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
    assert(!isa.nonpointer);
#endif
  //hash查找SideTable
    SideTable& table = SideTables()[this];

    bool do_dealloc = false;

  //加鎖
    table.lock();
  //根據當前對象指針,訪問table的應用計數表
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it == table.refcnts.end()) {
        do_dealloc = true;
        table.refcnts[this] = SIDE_TABLE_DEALLOCATING;
    } else if (it->second < SIDE_TABLE_DEALLOCATING) {
        // SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it.
        do_dealloc = true;
        it->second |= SIDE_TABLE_DEALLOCATING;
    } else if (! (it->second & SIDE_TABLE_RC_PINNED)) {
      //引用計數減1操作
        it->second -= SIDE_TABLE_RC_ONE;
    }
    table.unlock();
    if (do_dealloc  &&  performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
    }
    return do_dealloc;
}
  1. 通過當前對象的指針this,經過hash函數的計算,可以快速的SideTables當中找到(hash查找)它對應的SideTable
  2. 根據當前對象指針,訪問table的應用計數表
  3. 找到對應的值進行引用計數減1操作

retainCount實現

uintptr_t
objc_object::sidetable_retainCount()
{
  //hash查找SideTable
    SideTable& table = SideTables()[this];
  //聲明局部變量賦值為1
    size_t refcnt_result = 1;
    
  //加鎖
    table.lock();
  //根據當前對象指針,訪問table的應用計數表
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it != table.refcnts.end()) {
        // this is valid for SIDE_TABLE_RC_PINNED too
      //查找結果向右位移2位,再加上局部變量的值
        refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;
    }
    table.unlock();
    return refcnt_result;
}
  1. 通過當前對象的指針this,經過hash函數的計算,可以快速的SideTables當中找到(hash查找)它對應的SideTable

  2. 根據當前對象指針,訪問table的應用計數表

  3. 找到對應的值向右位移2位,再加上局部變量的值1

    這就是alloc操作之后,引用計數沒有變化,但retainCount獲取的值是1的原因

dealloc實現源碼

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

void
_objc_rootDealloc(id obj)
{
    assert(obj);

    obj->rootDealloc();
}

inline void
objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;  // fixme necessary?

    if (fastpath(isa.nonpointer  &&  
                 !isa.weakly_referenced  &&  
                 !isa.has_assoc  &&  
                 !isa.has_cxx_dtor  &&  
                 !isa.has_sidetable_rc))
    {
        assert(!sidetable_present());
        free(this);
    } 
    else {
        object_dispose((id)this);
    }
}

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

    objc_destructInstance(obj);    
    free(obj);

    return nil;
}

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);
        obj->clearDeallocating();
    }

    return obj;
}

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

void 
objc_object::sidetable_clearDeallocating()
{
    SideTable& table = SideTables()[this];

    // clear any weak table items
    // clear extra retain count and deallocating bit
    // (fixme warn or abort if extra retain count == 0 ?)
    table.lock();
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it != table.refcnts.end()) {
        if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
            weak_clear_no_lock(&table.weak_table, (id)this);
        }
        table.refcnts.erase(it);
    }
    table.unlock();
}

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);
    }
    if (isa.has_sidetable_rc) {
        table.refcnts.erase(this);
    }
    table.unlock();
}

void 
objc_object::sidetable_clearDeallocating()
{
    SideTable& table = SideTables()[this];

    // clear any weak table items
    // clear extra retain count and deallocating bit
    // (fixme warn or abort if extra retain count == 0 ?)
    table.lock();
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it != table.refcnts.end()) {
        if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
            weak_clear_no_lock(&table.weak_table, (id)this);
        }
        table.refcnts.erase(it);
    }
    table.unlock();
}

dealloc實現流程圖
dealloc.png
object_dispose()實現
object_dispose()實現.png
objc_destructInstance()實現
objc_destructInstance()實現.png
clearDeallocating()實現
clearDeallocating實現.png

弱引用管理

弱引用管理.png
id
objc_initWeak(id *location, id newObj)
{
    if (!newObj) {
        *location = nil;
        return nil;
    }

    return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
        (location, (objc_object*)newObj);
}

添加weak變量的弱引用實現

weak調用棧.png

當一個對象被釋放或者廢棄之后,weak變量怎樣處理的?

/** 
 * Called by dealloc; nils out all weak pointers that point to the 
 * provided object so that they can no longer be used.
 * 
 * @param weak_table 
 * @param referent The object being deallocated. 
 */
void 
weak_clear_no_lock(weak_table_t *weak_table, id referent_id) 
{
    objc_object *referent = (objc_object *)referent_id;

    weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
    if (entry == nil) {
        /// XXX shouldn't happen, but does with mismatched CF/objc
        //printf("XXX no entry for clear deallocating %p\n", referent);
        return;
    }

    // zero out references
    weak_referrer_t *referrers;
    size_t count;
   
    if (entry->out_of_line()) {
        referrers = entry->referrers;
        count = TABLE_SIZE(entry);
    } 
    else {
        referrers = entry->inline_referrers;
        count = WEAK_INLINE_COUNT;
    }
    
    for (size_t i = 0; i < count; ++i) {
        //referrers取到弱引用指針的所有對應的數組列表
        objc_object **referrer = referrers[I];
        if (referrer) {//如果referrer即弱引用指針存在
            if (*referrer == referent) { //如果弱引用指針對應的是被廢棄的對象的話,就將指針置為nil
                *referrer = nil;
            }
            else if (*referrer) {
                _objc_inform("__weak variable at %p holds %p instead of %p. "
                             "This is probably incorrect use of "
                             "objc_storeWeak() and objc_loadWeak(). "
                             "Break on objc_weak_error to debug.\n", 
                             referrer, (void*)*referrer, (void*)referent);
                objc_weak_error();
            }
        }
    }
    
    weak_entry_remove(weak_table, entry);
}

當一個對象被dealloc之后,內部實現當中會去調用weak_clear_no_lock()函數,函數實現內部會根據弱引用指針查找弱引用表把當前對象相對應的弱引用拿出來,然后遍歷數組的所有弱引用指針,分別置為nil

自動釋放池

編譯期會將代碼塊@autoreleasepool{}改寫為:

  1. void *ctx = objc_autoreleasePoolPush();

  2. {}中的代碼

  3. objc_autoreleasePoolPop(ctx);

    一次pop實際上相當于一次批量的pop操作

void *
objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::push();
}

void
objc_autoreleasePoolPop(void *ctxt)
{
    AutoreleasePoolPage::pop(ctxt);
}

自動釋放池的數據結構

  • 是以為節點通過雙向鏈表的形式組合而成。(什么是自動釋放池/自動釋放池的數據結構是怎樣的?)
  • 是和線程一一對應的

雙向鏈表

雙向鏈表.png

棧結構.png

AutoreleasePoolPage類源碼

class AutoreleasePoolPage 
{
    // EMPTY_POOL_PLACEHOLDER is stored in TLS when exactly one pool is 
    // pushed and it has never contained any objects. This saves memory 
    // when the top level (i.e. libdispatch) pushes and pops pools but 
    // never uses them.
#   define EMPTY_POOL_PLACEHOLDER ((id*)1)

#   define POOL_BOUNDARY nil
    static pthread_key_t const key = AUTORELEASE_POOL_KEY;
    static uint8_t const SCRIBBLE = 0xA3;  // 0xA3A3A3A3 after releasing
    static size_t const SIZE = 
#if PROTECT_AUTORELEASEPOOL
        PAGE_MAX_SIZE;  // must be multiple of vm page size
#else
        PAGE_MAX_SIZE;  // size and alignment, power of 2
#endif
    static size_t const COUNT = SIZE / sizeof(id);

    magic_t const magic;
    id *next;//指向當前棧中下一個可填充的位置
    pthread_t const thread; //線程
    AutoreleasePoolPage * const parent;//雙向鏈表的父指針
    AutoreleasePoolPage *child;//雙向鏈表的子指針
    uint32_t const depth;
    uint32_t hiwat;

    // SIZE-sizeof(*this) bytes of contents follow

    static void * operator new(size_t size) {
        return malloc_zone_memalign(malloc_default_zone(), SIZE, SIZE);
    }
    static void operator delete(void * p) {
        return free(p);
    }
  
  .....
}

AutoreleasePoolPage結構

autoreleasePoolPage.png

AutoreleasePoolPage::push

autoreleasePoolPage_push.png

[obj autorelease]

obj_autorelease.png

AutoreleasePoolPage::pop

  • 根據傳入的哨兵對象找到對應位置。
  • 給上次push操作之后添加的對象依次發送release消息。
  • 回退next指針到正確位置。
autoreleasePoolPage_pop01.png

autoreleasePoolPage_pop02.png

總結

  • 當每次runloop將要結束的時候調用AutoreleasePoolPage::pop()
  • 多層嵌套就是多次插入哨兵對象。
  • 在for循環中alloc圖片數據等內存消耗較大的場景手動插入autoreleasePool
- (void)viewDidLoad
{
  [super viewDidLoad];
  NSMutableArray *array = [NSMutableArray array];
  NSLog(@"%@",array);
}

上面array的內存在什么時候釋放的?

當每次runloop將要結束的時候,都會對前一次創建的AutoreleasePool調用AutoreleasePoolPage::pop()操作,同時會push進來一個AutoreleasePool。所以array對象會在當前runloop就要結束的時候調用AutoreleasePoolPage::pop()方法,把對應的array對象,調用其release函數對其進行釋放

AutoreleasePool的實現原理是怎樣的?

是以為節點通過雙向鏈表的形式組合而成的數據結構

AutoreleasePool為何可以嵌套使用?

多層嵌套就是多次插入哨兵對象。在我們每次創建代碼塊@autoreleasepool{},系統就會為我們進行哨兵對象的插入,完成新的AutoreleasePool的創建,實際上也是創建了一個AutoreleasePoolPage,假如當前AutoreleasePoolPage沒有滿的話,就不用創建AutoreleasePoolPage。所以新創建的AutoreleasePool底層就是插入一個哨兵對象,所以可以多層嵌套。

循環引用

三種循環引用

  • 自循環引用
  • 相互循環引用
  • 多循環引用

自循環引用

自循環引用.png

相互循環引用

相互循環引用.png

多循環引用

多循環引用.png

循環引用考點

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

如何破除循環引用?

  • 避免產生循環引用
  • 在合適的時機手動斷環

破除循環引用具體的解決方案都有哪些?

  • __weak破解
  • __block破解
  • __unsafe_unretained破解

__weak破解

weak破解.png

__block破解

  • MRC下,__block修飾對象不會增加其引用計數避免了循環引用。
  • ARC下,__block修飾對象會被強引用,無法避免循環引用,需手動解環

__unsafe_unretained破解

  • 修飾對象不會增加其引用計數,避免了循環引用。
  • 如果被修飾對象在某一時機被釋放,會產生懸垂指針!

循環引用示例

  • Block的使用示例。(參看Block的講解)

  • NSTimer使用示例。

nstimer方案.png
#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)interval
                                         target:(id)aTarget
                                       selector:(SEL)aSelector
                                       userInfo:(id)userInfo
                                        repeats:(BOOL)repeats
{
    TimerWeakObject *object = [[TimerWeakObject alloc] init];
    object.target = aTarget;
    object.selector = aSelector;
    object.timer = [NSTimer scheduledTimerWithTimeInterval:interval target:object selector:@selector(fire:) userInfo:userInfo repeats:repeats];
    
    return object.timer;
}

@end

內存管理面試總結

  • 什么是ARC?
  • 為什么weak指針指向的對象在廢棄之后會被自動置為nil?
  • 蘋果是如何實現AutoreleasePool的?
  • 什么是循環引用?你遇到過哪些循環引用,是怎樣解決的?
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 1、內存布局 stack:方法調用 heap:通過alloc等分配對象 bss:未初始化的全局變量等。 data:...
    AKyS佐毅閱讀 1,614評論 0 19
  • 文章目錄 一.內存管理準則 二.屬性內存管理修飾符全解析 三.block中的weak和strong 四.weak是...
    YouKnowZrx閱讀 1,066評論 5 10
  • 前言 從我開始學習iOS的時候,身邊的朋友、網上的博客都告訴我iOS的內存管理是依靠引用計數的,然后說引用計數大于...
    蓋世英雄_ix4n04閱讀 574評論 0 1
  • 一.面試問題 使用CADisplayLink、NSTimer有什么注意點?循環引用、NSTimer定時器不準 介紹...
    蔚尼閱讀 879評論 0 1
  • 29.理解引用計數 Objective-C語言使用引用計數來管理內存,也就是說,每個對象都有個可以遞增或遞減的計數...
    Code_Ninja閱讀 1,523評論 1 3