The core idea of Objective-C memory management is to control the life cycle of memory objects by reference counting objects. To put it bluntly, a call to retain is incremented by 1, a call to release is decayed by 1, the reference count is cleared, or a call to dealloc is destroyed.

Reference counting

The reference count is the number of times an object is held. Is the core of memory management. Let’s look at an example of reference counting:

- (void)testRefCount {
    NSObject *obj = [NSObject alloc];
    NSLog(@"==refCount==%ld==", (long)CFGetRetainCount((__bridge CFTypeRef)(obj)));
}
Copy the code

Run the program and the result is 1. There is no reference counting operation in the alloc process, so why is the print 1? RetainCount:

- (NSUInteger)retainCount {
    return ((id)self)->rootRetainCount();
}

inline uintptr_t 
objc_object::rootRetainCount()
{
    if (isTaggedPointer()) return (uintptr_t)this;

    sidetable_lock();
    isa_t bits = LoadExclusive(&isa.bits);
    ClearExclusive(&isa.bits);
    if (bits.nonpointer) {
        uintptr_t rc = 1 + bits.extra_rc;
        if (bits.has_sidetable_rc) {
            rc += sidetable_getExtraRC_nolock();
        }
        sidetable_unlock();
        return rc;
    }

    sidetable_unlock();
    return sidetable_retainCount();
}
Copy the code

As you can see, the total value of the reference count is the extra_rc value in the ISA plus the reference count table in the hash table plus 1, and for our new alloc object, the reference count is printed as 1 because of this 1, but the new alloc object has a reference count of 0.

So where are reference counts stored, and how are retain and release handled? Let’s take a look at retain:

retain

id objc_retain(id obj) { if (! obj) return obj; if (obj->isTaggedPointer()) return obj; return obj->retain(); } inline id objc_object::retain() { assert(! isTaggedPointer()); if (fastpath(! ISA()->hasCustomRR())) { return rootRetain(); } return ((id(*)(objc_object *, SEL))objc_msgSend)(this, SEL_retain); }Copy the code

As you can see, calling objc_retain will first determine if it is isTaggedPointer and return it if it is. RootRetain is then called if the object does not have a custom retain/release method, otherwise a SEL_retain message is sent via objc_msgSend.

id objc_object::rootRetain() { return rootRetain(false, false); } 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; Isa oldisa = LoadExclusive(&ISa.bits); newisa = oldisa; if (slowpath(! Newisa.nonpointer)) {// Not nonpointer ISA hash table handles ClearExclusive(& ISa.bits); if (! tryRetain && sideTableLocked) sidetable_unlock(); if (tryRetain) return sidetable_tryRetain() ? (id)this : nil; else return sidetable_retain(); } // If deallocating is not being done, slowPath (tryRetain && newisa.deallocating) {ClearExclusive(&isa.bits); if (! tryRetain && sideTableLocked) sidetable_unlock(); return nil; } // is nonpointer ISA extra_rc++ uintptr_t carry; newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++ if (slowpath(carry)) { // newisa.extra_rc++ overflowed if (! handleOverflow) { ClearExclusive(&isa.bits); return rootRetain_overflow(tryRetain); } // Store half of it in the hash table, half in the extra_rc and process the EXTRA_RC flag bits of ISA and has_sideTABLE_rc 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 (slowpath(transcribeToSideTable)) { sidetable_addExtraRC_nolock(RC_HALF); } if (slowpath(! tryRetain && sideTableLocked)) sidetable_unlock(); return (id)this; }Copy the code

Enter rootRetain and do the following:

    1. Determine whether or notisTaggedPointer.TaggedPointerDoes not need to maintain reference counting, returns directly.
    1. notTaggedPointerGets the object’sisa, judge whether or notnonpointer isa
    1. notnonpointer isa, to hash table processing, reference counting++Operation, and then return
    1. To determine whether or notdeallocatingIf yes, return directly
    1. isnonpointer isaforisaThe mark of aextra_rcperform++operation
    1. If the count exceedsextra_rcAs many as you can store, you store half of themextra_rcAnd put thehas_sidetable_rcFlag position is 1. Then copy the other half and save it in the hash table.

Hash tables are multiple tables, multiple tables rather than one for performance and security reasons, but multiple tables are not just one table per object. Hash tables store reference counts as follows:

bool objc_object::sidetable_tryRetain() { SideTable& table = SideTables()[this]; bool result = true; RefcountMap::iterator it = table.refcnts.find(this); if (it == table.refcnts.end()) { table.refcnts[this] = SIDE_TABLE_RC_ONE; } else if (it->second & SIDE_TABLE_DEALLOCATING) { result = false; } else if (! (it->second & SIDE_TABLE_RC_PINNED)) { it->second += SIDE_TABLE_RC_ONE; } return result; } id objc_object::sidetable_retain() { #if SUPPORT_NONPOINTER_ISA assert(! isa.nonpointer); #endif SideTable& table = SideTables()[this]; table.lock(); size_t& refcntStorage = table.refcnts[this]; if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) { refcntStorage += SIDE_TABLE_RC_ONE; } table.unlock(); return (id)this; }Copy the code

release

Retain is reference count +1 and release is reference count -1, and the flow is complementary. Calling release will still get you into the objc_Release method.

void objc_release(id obj) { if (! obj) return; if (obj->isTaggedPointer()) return; return obj->release(); } inline void objc_object::release() { assert(! isTaggedPointer()); if (fastpath(! ISA()->hasCustomRR())) { rootRelease(); return; } ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_release); }Copy the code

As you can see, calling objc_Release also checks if it is isTaggedPointer and returns it if it is. RootRelease is then called if the object does not have a custom retain/release method, otherwise the SEL_release message is sent via objc_msgSend.

bool objc_object::rootRelease() { return rootRelease(true, false); } 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)) {// Not nonpointer_ISA handles the reference count in the hash table ClearExclusive(& ISa.bits); if (sideTableLocked) sidetable_unlock(); return sidetable_release(performDealloc); } // nonpointer_isa uses the uintptr_t carry in the EXTRA_rc of ISA; newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); If (slowpath(carry)) {// If (slowpath(carry)) {// If (slowpath(carry)) {// If (slowpath(carry)); } } while (slowpath(! StoreReleaseExclusive(&isa.bits, oldisa.bits, newisa.bits))); if (slowpath(sideTableLocked)) sidetable_unlock(); return false; underflow: newisa = oldisa; If (slowPath (newisa.has_sidetable_rc)) {if (! handleUnderflow) { ClearExclusive(&isa.bits); RootRelease_underflow (performDealloc); rootRelease_underflow(performDealloc); } if (! sideTableLocked) { ClearExclusive(&isa.bits); sidetable_lock(); sideTableLocked = true; // The hash table is not unlocked, and the lock is recursive goto retry; Size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF); If (borrowed > 0) {// If (borrowed > 0) {// Do the -- operation on the reference count of the hashtable and store it in the EXTRA_rc of ISA. bool stored = StoreReleaseExclusive(&isa.bits, oldisa.bits, newisa.bits); if (! Stored) {// If you did not successfully store into ISA's extra_rc then store again at...... } if (! Stored) {sidetable_addExtraRC_nolock(borrowed); // Stored) {sidetable_addExtraRC_nolock(borrowed); goto retry; } // Borrow from the hash table -- success sidetable_unlock(); return false; } else {// The reference count for the hash is empty}} // ISA didn't throw an exception in deallocating if (slowPath (newisa.deallocating)) { ClearExclusive(&isa.bits); if (sideTableLocked) sidetable_unlock(); return overrelease_error(); } // Mark ISA as deallocating, and recurse newisa.deallocating = true; if (! StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry; if (slowpath(sideTableLocked)) sidetable_unlock(); __sync_synchronize(); If (performDealloc) {// Send a SEL_dealloc message ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc); } return true; }Copy the code

To comb through the process, enter rootRelease as follows:

    1. Determine whether or notisTaggedPointer.TaggedPointerDoes not need to maintain reference counting, returns directly.
    1. notTaggedPointerGets the object’sisa, judge whether or notnonpointer isa
    1. notnonpointer isa, to hash table processing, reference counting--If the reference count of the hash table is cleared, the object needs to be sentSEL_deallocInformation, executiondeallocOperation, and then return.
    1. If it isnonpointer_isaonisatheextra_rcfor--Operation, whenextra_rcWhen the count is 0 and not enough subtraction is required, the subtraction is borrowed from the hash table reference counter.
    1. judgeisathehas_sidetable_rcIf there is a value, go to step 6, if there is no value, go to step 8
    1. Gets the reference count for the hash table, and if the count is zero, sends it to the objectSEL_deallocInformation, executiondeallocOperation.
    1. If the reference count obtained from the hash table is greater than zero, subtract one from the count and storeisatheextra_rcAnd return. If there is no success, the recursive store is done twice, and if there is still no success, the count is stored in the hash table and one more recursive operation is performed.
    1. judgeisaAre you in the middle ofdeallocatingIf not, throw an exception.
    1. Send to the objectSEL_deallocInformation, executiondeallocOperation.

Hash table reference count table release reference count

uintptr_t
objc_object::sidetable_release(bool performDealloc)
{
    SideTable& table = SideTables()[this];

    bool do_dealloc = false;

    table.lock();
    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) {
        do_dealloc = true;
        it->second |= SIDE_TABLE_DEALLOCATING;
    } else if (! (it->second & SIDE_TABLE_RC_PINNED)) {
        it->second -= SIDE_TABLE_RC_ONE;
    }
    table.unlock();
    if (do_dealloc  &&  performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
    }
    return do_dealloc;
}
Copy the code

dealloc

The dealloc method is used when a page or object is destroyed.

- (void)dealloc { _objc_rootDealloc(self); } void _objc_rootDealloc(id obj) { assert(obj); obj->rootDealloc(); } inline void objc_object::rootDealloc() {if (isTaggedPointer()) return; // fixme necessary? // no weak reference table, no associated object, no c++ destructor, no hash table reference count 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(); Object_cxxDestruct (obj); // If (CXX) object_cxxDestruct(obj); If (assoc) _object_remove_assocations(obj); obj->clearDeallocating(); } return obj; } inline void objc_object::clearDeallocating() { if (slowpath(! Isa.nonpointer)) {// Non-pointer isa sidetable_clearDeallocating(); } else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) { // non-pointer isa clearDeallocating_slow(); } assert(! sidetable_present()); } void objc_object::sidetable_clearDeallocating() { SideTable& table = SideTables()[this]; table.lock(); RefcountMap::iterator it = table.refcnts.find(this); if (it ! = table.refcnt.end ()) {if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {// Clear the weak reference table weak_clear_no_lock(&table.weak_table, (id)this); Tab.refcnt.erase (it);} // Remove the reference count table from the hash table. } table.unlock(); } void objc_object::clearDeallocating_slow() { assert(isa.nonpointer && (isa.weakly_referenced || isa.has_sidetable_rc)); SideTable& table = SideTables()[this]; table.lock(); Weakly_referenced) {weak_clear_no_lock(&table. Weak_table, (id)this); // Clear weakly_referenced if (ISa.weakly_referenced) {weak_clear_no_lock(&table. Weak_table, (id)this); } if (isa.has_sidetable_rc) {table.refcnt.erase (this); } table.unlock(); }Copy the code

To summarize, the process of calling the dealloc method is as follows:

    1. If it isTaggedPointer, the reference count is not processed and returns
    1. If it isnonpointer_isa, and there is no weak reference table, no associated object, no c++ destructor, and no hash table reference countobject_dispose()
    1. callobjc_destructInstance(obj)And then in the callfree(obj)
    1. objc_destructInstance()Method, determine if there iscxxThe destructor needs to call the destructor method and delete the associated object if it exists.
    1. Clear information about weak reference tables and reference count tables from hash tables.

The release of weak reference tables is described in the weak principle, which is not described here.

conclusion

retainprocess

releaseprocess

deallocprocess