函數(shù)和內(nèi)存管理

OC每一個(gè)方法名是一個(gè)SEL ,根據(jù)SEL可以查找到函數(shù)體存放的內(nèi)存地址IMP。查找的原理請(qǐng)參考iOS OC運(yùn)行時(shí)詳解。

讓我們跟著一個(gè)方法,一步步探究以下問題

  • 函數(shù)執(zhí)行前后做了什么
  • 函數(shù)內(nèi)部成員的內(nèi)存如何管理
@interface TestObject :NSObject
@property (nonatomic , strong) NSArray *array;
@end

@implementation TestObject

- (instancetype)init
{
    if (self = [super init]) {
         dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_global_queue(0, 0), ^{
            self.array = [NSArray array];
            NSLog(@"dispatch_after");

        });
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            self.array = [NSArray array];
            NSLog(@"dispatch_after main");
            
        });
    }
    return self;
}

- (void)dealloc
{
    NSLog(@"dealloc %@",_array);
}

@end

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self testTemporaryVariable];
}

- (void)testTemporaryVariable
{
    TestObject *obj = [[TestObject alloc] init]; // 臨時(shí)變量
    obj.array = [NSArray arrayWithObject:@"test"]; // 引用一次
    // 函數(shù)結(jié)束,obj會(huì)在什么時(shí)候釋放?
}

@end

跟蹤TestObject的dealloc調(diào)用棧發(fā)現(xiàn)objc_object::sidetable_release(bool)回調(diào)了dealloc

TestObject的dealloc調(diào)用棧

接下查看objc源碼來看sidetable_release是做什么用的。

objc_object結(jié)構(gòu)體中可以發(fā)現(xiàn)這個(gè)函數(shù)。說明每個(gè)OC對(duì)象都有
id sidetable_retain();
uintptr_t sidetable_release(bool performDealloc = true);
兩個(gè)函數(shù)

#ifdef __LP64__
#   define WORD_SHIFT 3UL
#   define WORD_MASK 7UL
#   define WORD_BITS 64
#else
#   define WORD_SHIFT 2UL
#   define WORD_MASK 3UL
#   define WORD_BITS 32
#endif

static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}
static inline size_t word_align(size_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}

// The order of these bits is important.
#define SIDE_TABLE_WEAKLY_REFERENCED (1UL<<0)
#define SIDE_TABLE_DEALLOCATING      (1UL<<1)  // MSB-ward of weak bit
#define SIDE_TABLE_RC_ONE            (1UL<<2)  // MSB-ward of deallocating bit
#define SIDE_TABLE_RC_PINNED         (1UL<<(WORD_BITS-1))

#define SIDE_TABLE_RC_SHIFT 2
#define SIDE_TABLE_FLAG_MASK (SIDE_TABLE_RC_ONE-1)
// rdar://20206767
// return uintptr_t instead of bool so that the various raw-isa 
// -release paths all return zero in eax
uintptr_t
objc_object::sidetable_release(bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
    assert(!isa.nonpointer);
#endif
    // static StripedMap<SideTable>& SideTables() {
    //     return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
    // }
    SideTable& table = SideTables()[this];// 找到當(dāng)前對(duì)象的管理表,包含引用計(jì)數(shù)map和當(dāng)前對(duì)象的弱引用表

    bool do_dealloc = false;

    table.lock();
    RefcountMap::iterator it = table.refcnts.find(this);// 引用計(jì)數(shù)表中以當(dāng)前對(duì)象的地址為Key查找到當(dāng)前對(duì)象的引用計(jì)數(shù)表
    if (it == table.refcnts.end()) { // 引用計(jì)數(shù)了
        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)) {
        it->second -= SIDE_TABLE_RC_ONE; // 引用計(jì)數(shù) -= SIDE_TABLE_RC_ONE
    }
    table.unlock();
    if (do_dealloc  &&  performDealloc) {// 在此處調(diào)用dealloc
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
    }
    return do_dealloc;
}

struct SideTable {
    spinlock_t slock;
// refcnt的詳細(xì)解釋
// ZeroValuesArePurgeable=true is used by the refcount table.
// A key/value pair with value==0 is not required to be stored   in the refcount table; it could correctly be erased instead.
// For performance, we do keep zero values in the table when the  true refcount decreases to 1: this makes any future retain faster.
// For memory size, we allow rehashes and table insertions to  remove a zero value as if it were a tombstone.
    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);
};
/**
 * The global weak references table. Stores object ids as keys,
 * and weak_entry_t structs as their values.
 */
struct weak_table_t {
    weak_entry_t *weak_entries;
    size_t    num_entries;
    uintptr_t mask;
    uintptr_t max_hash_displacement;
};

通過上面的分析大致能夠了解sideTable_release(bool)判斷了對(duì)象的引用計(jì)數(shù),達(dá)到釋放閾值并進(jìn)行標(biāo)記。未達(dá)到釋放閾值的計(jì)數(shù)器-1,不調(diào)用dealloc函數(shù)。直到當(dāng)前對(duì)象調(diào)用release,并達(dá)到閾值,才會(huì)被釋放。
這里可以在上面的代碼中得到證實(shí)。延時(shí)函數(shù)內(nèi)部強(qiáng)引用了obj對(duì)象,直到dispatch_after的block執(zhí)行完成后才會(huì)繼續(xù)走dealloc.

根據(jù)現(xiàn)有知識(shí),OC調(diào)用某函數(shù),其實(shí)給這個(gè)函數(shù)地址發(fā)消息。比如:

  • ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);

閱讀objc-msg-arm64.s源碼可以發(fā)現(xiàn), objc_msgSend的匯編實(shí)現(xiàn)。
GetClassFromIsa_p16根據(jù)對(duì)象的isa指針緩存中獲取類,緩存不命中去執(zhí)行常規(guī)查找,找到執(zhí)行完,加入方法緩存。

下面分析執(zhí)行sideTable_release函數(shù)之前干了什么。調(diào)用來源有下面三個(gè)

ALWAYS_INLINE bool 
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
    if (isTaggedPointer()) return false;

    bool sideTableLocked = false;

    isa_t oldisa;
    isa_t newisa;

 retry:
    do {
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            if (sideTableLocked) sidetable_unlock();
            return sidetable_release(performDealloc);
        }
........
     }while (slowpath(!StoreReleaseExclusive(&isa.bits, 
                                             oldisa.bits, newisa.bits)));
........
}

// Base release implementation, ignoring overrides.
// Does not call -dealloc.
// Returns true if the object should now be deallocated.
// This does not check isa.fast_rr; if there is an RR override then 
// it was already called and it chose to call [super release].
inline bool 
objc_object::rootRelease()
{
    if (isTaggedPointer()) return false;
    return sidetable_release(true);
}

inline bool 
objc_object::rootReleaseShouldDealloc()
{
    if (isTaggedPointer()) return false;
    return sidetable_release(false);
}

可以猜想一個(gè)對(duì)象調(diào)用了release操作,引用計(jì)數(shù)達(dá)到了閾值,就會(huì)回調(diào)dealloc。

讓我們繼續(xù)思考。MRC下函數(shù)的局部成員,誰(shuí)分配誰(shuí)釋放。ARC把這個(gè)過程集成到了Clang中。Objective-C Automatic Reference Counting (ARC).

  • 編譯器到底什么時(shí)候把retain和release加入代碼的?

這就涉及到llvm里講的,所有權(quán)歸屬操作。

Methods in the `alloc`, `copy`, `init`, `mutableCopy`, and `new` [families](https://clang.llvm.org/docs/AutomaticReferenceCounting.html#arc-method-families) are implicitly marked `__attribute__((ns_returns_retained))`. This may be suppressed by explicitly marking the method `__attribute__((ns_returns_not_retained))`.

It is undefined behavior if the method to which an Objective-C message send statically resolves has different retain semantics on its result from the method it dynamically resolves to. It is undefined behavior if a block or function call is made through a static type with different retain semantics on its result from the implementation of the called block or function.
// 我們 通過這個(gè)簡(jiǎn)單的函數(shù)看一下app可執(zhí)行文件的方法到底變成了什么
- (id)testTemporaryVariable
{
    TestObject *obj = [[TestObject alloc] init];
    obj.array = [NSArray arrayWithObject:@"test"];
    return obj;
}

工程run 成功后,hopper查看生成的可執(zhí)行文件,找到對(duì)應(yīng)方法如下圖:

image.png

可以看出給obj賦值操作(setArray:)先調(diào)用了objc_retainAutoreleasedReturnValue方法,相當(dāng)于幫我們r(jià)etain一次,因?yàn)閍rc下的返回對(duì)象都是autorelease對(duì)象。該方法其實(shí)是對(duì)arrayWithObject:返回對(duì)象的引用計(jì)數(shù)管理,可以在NSObject.mm源碼找到。如下

// Accept a value returned through a +0 autoreleasing convention for use at +1.
id
objc_retainAutoreleasedReturnValue(id obj)
{
    if (acceptOptimizedReturn() == ReturnAtPlus1) return obj;

    return objc_retain(obj);
}

其實(shí)就是調(diào)用了objc_object::rootRetain(bool tryRetain, bool handleOverflow)方法。

ALWAYS_INLINE id 
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
   if (isTaggedPointer()) return (id)this;

   bool sideTableLocked = false;
   bool transcribeToSideTable = false;

   isa_t oldisa;
   isa_t newisa;

   do {
       transcribeToSideTable = false;
       oldisa = LoadExclusive(&isa.bits);
       newisa = oldisa;
       if (slowpath(!newisa.nonpointer)) {
           ClearExclusive(&isa.bits);
           if (!tryRetain && sideTableLocked) sidetable_unlock();
           if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
           else return sidetable_retain();
       }
.......
   } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));
.......
}

這就走到了與sidetable_release()對(duì)應(yīng)的sidetable_retain()方法。暫時(shí)略過這里。
函數(shù)最后執(zhí)行了objc_storeStrong方法,相當(dāng)于與調(diào)用了一次release。通過查看源碼可以發(fā)現(xiàn)這個(gè)函數(shù)做了哪些操作。

void
objc_storeStrong(id *location, id obj)
{
    id prev = *location; // 取出該地址的對(duì)象
    if (obj == prev) { // 對(duì)象沒有變化return
        return;
    }
    objc_retain(obj); // retain新對(duì)象
    *location = obj; // 新對(duì)象存入該地址
    objc_release(prev);// 釋放老對(duì)象
}

Autoreleasepool

Autorelease pool implementation 定義
A thread's autorelease pool is a stack of pointers.
Each pointer is either an object to release, or POOL_BOUNDARY which is
an autorelease pool boundary.
A pool token is a pointer to the POOL_BOUNDARY for that pool. When
the pool is popped, every object hotter than the sentinel is released.
The stack is divided into a doubly-linked list of pages. Pages are added
and deleted as necessary.
Thread-local storage points to the hot page, where newly autoreleased
objects are stored.

Autoreleasepool 是一個(gè)以棧為節(jié)點(diǎn)的雙向鏈表結(jié)構(gòu)。


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;
// 雙向鏈表結(jié)構(gòu),AutoreleasePoolPage為每個(gè)節(jié)點(diǎn)結(jié)構(gòu)
    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);
    }
......
}

在@autoreleasepool {}結(jié)構(gòu)中alloc的對(duì)象會(huì)被加入自動(dòng)釋放池中

- (void)testAutoreleasePool
{
    @autoreleasepool {
        TestObject *obj = [[TestObject alloc] init];
    }
}

image.png

可見先調(diào)用了objc_autoreleasePoolPush 函數(shù)執(zhí)行完又調(diào)用了objc_autoreleasePoolPop 。

是push入棧了什么?push操作發(fā)生了什么?

static inline id *autoreleaseFast(id obj)
    {
        AutoreleasePoolPage *page = hotPage();
        if (page && !page->full()) {
            return page->add(obj);
        } else if (page) {
            return autoreleaseFullPage(obj, page);
        } else {
            return autoreleaseNoPage(obj);
        }
    }

    id *add(id obj)
    {
        assert(!full());
        unprotect();
        id *ret = next;  // faster than `return next-1` because of aliasing
        *next++ = obj;
        protect();
        return ret;
    }

可見push的返回值是加在棧頭的哨兵nil的地址,在這之后加入page的指針指向的對(duì)象都會(huì)在pop時(shí)釋放掉。

那autoreleasepool和autorelease/runloop什么關(guān)系呢?
在沒有手加Autorelease Pool的情況下,Autorelease對(duì)象是在當(dāng)前的runloop迭代結(jié)束時(shí)釋放的,而它能夠釋放的原因是系統(tǒng)在每個(gè)runloop迭代中都加入了自動(dòng)釋放池Push和Pop。
加上Autorelease Pool后就是出了作用域就釋放。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容