ARC is a reference count management technology in iOS, which helps iOS to implement automatic garbage collection. The principle of the implementation is managed by the compiler, and the runtime library assists the compiler to complete. It mainly involves Clang (LLVM compiler) and objC4 runtime libraries.

The main content of this paper is extended by modifiers __strong, __weak and __autoRelease, which respectively extend the implementation principles of reference counting, weak reference table and automatic release pool. Here are some questions to ask before you read:

  • How do I store reference counts under ARC?

  • Such as[NSDictionary dictionary]Method to create objects that are different in ARC.

  • Weak reference table data structure.

  • Explain Hot and Cold pages in the auto-release pool.

If you already know the above, this article may be of limited help to you, but if you still have questions about these questions, I believe this article will answer your questions.

A, Clang

In Objective-C, the reference relationship of an object is determined by reference modifiers such as __strong, __weak, __autorelease, and so on, and the compiler generates code with different logic to manage memory based on these modifiers.

To see where Clang comes in, we can convert Objective-C code into LLVM intermediate code at the command line:

// Switch to your file pathcdPath // Use main.m to generate the intermediate code file main.ll clang-s - fobjc-arc-emit - LLVM main.m -o main.llCopy the code

I added the defaultFunction method to the main.m file and converted it to intermediate code using the command line command:

void defaultFunction() {
    id obj = [NSObject new];
}
Copy the code

After you type the command on the command line, you’ll find main.ll under the folder, which will look like this:

define void @defaultFunction() # 0 {
  %1 = alloca i8*, align 8
  %2 = load %struct._class_t*, %struct._class_t** @"OBJC_CLASSLIST_REFERENCES_The $_", align 8 %3 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_, align 8, ! invariant.load ! 8 %4 = bitcast %struct._class_t* %2 to i8* %5 = call i8* bitcast (i8* (i8*, i8*, ...) * @objc_msgSend to i8* (i8*, i8*)*)(i8* %4, i8* %3) %6 = bitcast i8* %5 to %0* %7 = bitcast %0* %6 to i8* store i8* %7, i8** %1, align 8 call void @objc_storeStrong(i8** %1, i8* null)# 4
  ret void
}
Copy the code

Although the content is a bit much, but careful analysis down roughly the following content:

void defaultFunction() {
	id obj = obj_msgSend(NSObject, @selector(new));
	objc_storeStrong(obj, null);
}
Copy the code

Obj_msgSend (NSObject, @selector(new)) is a new object, and objc_storeStrong is a method in objC4 library. The logic is as follows:

void objc_storeStrong(id *location, id obj)
{
    id prev = *location;
    if (obj == prev) {
        return;
    }
    objc_retain(obj);
    *location = obj;
    objc_release(prev);
}
Copy the code

The above code does the following four things in order:

  1. Check that the obj address entered is the same as the address pointed to by the pointer.
  2. Hold object, reference count + 1.
  3. Pointer to obj.
  4. The reference count of the original object is -1.

Objc_retain and objc_release are also methods in the OBJC4 library, which will be discussed later in this article.

Second, the isa

Before you can analyze the ARC source code, you need to be familiar with ISA, which stores some very important information. Here is how ISA is structured:

union isa_t { Class cls; uintptr_t bits; struct { uintptr_t nonpointer : 1; Uintptr_t has_assoc: 1; Uintptr_t has_cxx_dtor: 1; Uintptr_t shiftcls: 33; // MACH_VM_MAX_ADDRESS 0x1000000000 -> Uintptr_t Weakly_referenced: 1; Uintptr_t dealLocating: 1; Uintptr_t has_sidetable_rc: 1; //1 uintptr_t extra_rc: 19; //1 uintptr_t extra_rc: 19 //-> store reference count}; };Copy the code

Nonpointer, Weakly_referenced, has_sidetable_rc and extra_rc are all member variables that are directly related to ARC, and most other variables are also involved.

struct objc_object {
    isa_t isa;
};
Copy the code

As you can see from the following code, objc_object isa wrapper on top of isa.

struct objc_class : objc_object { isa_t isa; // Inherit from objc_object Class superclass; cache_t cache; // Implement caching and vtable class_datA_bits_t bits; // class_rw_t * plus custom rr/alloc flags };Copy the code

Objc_class inherits objc_Object with the following structure:

  • isaObjc_object refers to the class, objc_class refers to the metaclass.
  • superclass: points to the parent class.
  • cache: Stores the method cache and Vtable optimized for user message forwarding.
  • bits: class_rw_t and class_ro_t, which hold lists of methods, protocols, attributes, and other flags.

The __strong modifier

In the MRC era, the Retain modifier increases the reference count of the referenced object by + 1. In ARC, the __strong modifier replaces the Retain modifier. We can use Clang to convert Objective-C code into LLVM to see how this works.

3.1 The middle code of the __strong modifier

Moving on to converting objective-C code into LLVM intermediate code, this time we’ll try the __strong modifier:

void strongFunction() {
    id obj = [NSObject new];
    __strong id obj1 = obj;
}
Copy the code

Intermediate code (the following intermediate code is removed for easier understanding) :

void defaultFunction() {
	id obj = obj_msgSend(NSObject, @selector(new));
	id obj1 = objc_retain(obj)
	objc_storeStrong(obj, null);
	objc_storeStrong(obj1, null);
}
Copy the code

Create an object, reference count + 1, release the object separately, and embed the logic inside objc_storeStrong:

void defaultFunction() {
	id obj = obj_msgSend(NSObject, @selector(new));
	id obj1 = objc_retain(obj)
	objc_release(obj);
	objc_release(obj1);
}
Copy the code

3.2 objc_retain

Next, let’s look at the internal logic of objC_retain and objc_release by analyzing the objC4 library source code. Take a look at the objc_retain implementation:

id objc_retain(id obj) {
    if(! obj)return obj;
    if (obj->isTaggedPointer()) return obj;
    return obj->retain();
}
Copy the code

Continue to see the final location to the objc_Object ::rootRetain method:

ALWAYS_INLINE ID objC_object ::rootRetain(bool tryRetain, bool handleOverflow) {// If TaggedPointer is returnedif (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)) {// Unoptimized ISA part ClearExclusive(& ISa.bits);if(! tryRetain && sideTableLocked) sidetable_unlock(); //if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
            else returnsidetable_retain(); } // Donot check newisa.fast_rr; we already called any RR overridesif (slowpath(tryRetain && newisa.deallocating)) {
            ClearExclusive(&isa.bits);
            if(! tryRetain && sideTableLocked) sidetable_unlock();returnnil; } // the extra_rc does not overflow reference count ++ uintptr_t carry; newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); Extra_rc ++ // extra_rc overflowif (slowpath(carry)) {
            // newisa.extra_rc++ overflowed
            if(! handleOverflow) { ClearExclusive(&isa.bits); // Call this function again with argument handleOverflow astrue
                returnrootRetain_overflow(tryRetain); } // Keep half the reference count // ready to copy the other half to side table.if(! tryRetain && ! sideTableLocked) sidetable_lock(); sideTableLocked =true;
            transcribeToSideTable = true;
            newisa.extra_rc = RC_HALF;
            newisa.has_sidetable_rc = true; } // Update isa value}while(slowpath(! StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));if(slowPath (transcribeToSideTable)) {// copy the other half to sidetable side table.sideTable_addextrarc_nolock (RC_HALF); }if(slowpath(! tryRetain && sideTableLocked)) sidetable_unlock();return (id)this;
}
Copy the code

The above code is divided into three small branches:

  • TaggedPointer: A value stored in a pointer is returned directly.
  • ! Newisa. nonpointer: unoptimized ISA, usedsidetable_retain().
  • Newisa.nonpointer: Optimized ISA, extra_RC overflowed and unoverflowed.
    • When no overflow occurs,isa.extra_rcPlus 1 is done.
    • Overflow when willisa.extra_rcThe median value is transferred tosidetableIn, and then willisa.has_sidetable_rcSet totrueIs usedsidetableTo count the number of references.

3.3 objc_release

Continue with the objc_Release implementation and finally locate the objc_Object ::rootRelease method:

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)) {// Unoptimized ISA ClearExclusive(& ISa.bits);if(sideTableLocked) sidetable_unlock(); // Whether to execute the Dealloc function, if yestrueExecute SEL_deallocreturn sidetable_release(performDealloc);
        }

        // extra_rc --
        newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc--
        if(slowpath(carry)) { // donot ClearExclusive() goto underflow; } // Update isa value}while(slowpath(! StoreReleaseExclusive(&isa.bits, oldisa.bits, newisa.bits)));if (slowpath(sideTableLocked)) sidetable_unlock();
    return false; Underflow: // Handle underflow, borrow from side table or release newisa = oldisa; // if sidetable_rc is usedif (slowpath(newisa.has_sidetable_rc)) {
        if(! HandleUnderflow) {// Call this function to handle overflow ClearExclusive(& ISa.bits);returnrootRelease_underflow(performDealloc); } // Borrow a reference count from sideTable to extra_rc size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);ifNewisa. extra_rc = digital-1; newISA. extra_rc = digital-1; newisA. extra_rc = digital-1; // redo the original decrement too bool stored = StoreReleaseExclusive(&isa.bits, oldisa.bits, newisa.bits); // Save failed. Restore the site and try againif(! stored) { isa_t oldisa2 = LoadExclusive(&isa.bits); isa_t newisa2 = oldisa2;if (newisa2.nonpointer) {
                    uintptr_t overflow;
                    newisa2.bits = 
                        addc(newisa2.bits, RC_ONE * (borrowed-1), 0, &overflow);
                    if(! overflow) { stored = StoreReleaseExclusive(&isa.bits, oldisa2.bits, newisa2.bits); }}} // If the save fails, return to side tableif(! stored) { sidetable_addExtraRC_nolock(borrowed); goto retry; } sidetable_unlock();return false;
        }
        else{// Side table is empty after all. fall-through to the dealloc path.}} // Sidetable_rc is not used, Or sidetable_rc count == 0 will be released directly // If it is already released, throw an overrelease errorif (slowpath(newisa.deallocating)) {
        ClearExclusive(&isa.bits);
        if (sideTableLocked) sidetable_unlock();
        return overrelease_error();
        // does not actually return} // Update the ISA state newISa.dealLocating =true;
    if(! StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;if(slowpath(sideTableLocked)) sidetable_unlock(); // Execute the SEL_dealloc event __sync_synchronize();if (performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
    }
    return true;
}
Copy the code

This is a long list of code that looks like rootRetain’s logic when broken down:

  • TaggedPointer: Returns false directly.
  • ! Nonpointer: Non-optimized ISA executes sidetable_release.
  • Nonpointer: An optimized ISA can be overflowed or not overflowed.
    • Unspilled: extra_rc–.
    • Underflow: Borrows from sidetable to extra_RC half full, failing which means the reference count has gone to zero and needs to be freed. Where borrowing may fail to save and retry continuously.

The reference count is stored in ISa.extra_rc and sidetable, respectively. When ISA.EXTRA_RC overflows, half of the count is moved to sidetable, and when it overflows, the count is rolled back. When both are empty, the release process is performed.

3.4 rootRetainCount

The objc_Object ::rootRetainCount method is used to calculate the reference count. As you can see from the previous rootRetain and rootRelease source code analysis, reference counts exist in Isa. extra_RC and sidetable, respectively. This is also reflected in the rootRetainCount method.

inline uintptr_t objc_object::rootRetainCount() {// TaggedPointer returnsif (isTaggedPointer()) return(uintptr_t)this; sidetable_lock(); Isa ISA_t bits = LoadExclusive(&ISa.bits); ClearExclusive(&isa.bits); // Optimized ISA requires sidetable + bits.extra_rc + 1if (bits.nonpointer) {
        uintptr_t rc = 1 + bits.extra_rc;
        if (bits.has_sidetable_rc) {
            rc += sidetable_getExtraRC_nolock();
        }
        sidetable_unlock();
        returnrc; } // Return sidetable_retainCount sidetable_unlock();return sidetable_retainCount();
}
Copy the code

3.5 objc_autoreleaseReturnValue and objc_retainAutoreleasedReturnValue

In the era of MRC, there is a phrase called “create who release”. This means that objects created by developers using methods such as alloc, new, copy, and mutableCopy need to be released manually. Objects created and returned by other methods do not need to be released after being returned to the user. Such objects as arrays created by the [NSMutableArray Array] method are managed by automatic release pools by default. In the ARC era, the compiler also does some special processing for returned objects, which is explained below.

First, convert the code below to intermediate code:

id strongArrayInitFunction() {
    return [[NSMutableArray alloc] init];
}

void strongArrayFunction() {
    __strong id obj = [NSMutableArray array];
}
Copy the code

After the intermediate code is refined, the following code is obtained:

strongArrayInitFunction() {
	id obj = objc_msgSend(objc_msgSend(NSMutableArray, @selector(alloc)), @selector(init));
	objc_autoreleaseReturnValue(obj);
	return obj;
}

strongArrayFunction() {
	id obj = objc_msgSend(NSMutableArray, @selector(array));
	objc_retainAutoreleasedReturnValue(obj)
	objc_release(obj);
}
Copy the code

Relative to the user to create objects, [NSMutableArray array] method to create a more the object returned after converting objc_retainAutoreleasedReturnValue method. This involves an optimization process:

  1. To save an object register toautoreleasePoolThe operation is being executedobjc_autoreleaseReturnValueAccording to check whether the list of methods to be called later containsobjc_retainAutoreleasedReturnValueMethod to determine whether to optimize the flow.
  2. In the implementationobjc_autoreleaseReturnValue, the optimization process stores a flag bit in TLS (Thread Local Storage) and returns the object directly.
  3. Execute subsequent methodsobjc_retainAutoreleasedReturnValueCheck the flag bit of TLS to determine whether it is in the optimization process. If it is in the optimization process, the object is directly returned and the TLS state is restored.

The objc_autoreleaseReturnValue implementation is as follows:

Id objc_autoreleaseReturnValue(id obj) {// Return the object directly if the optimizer is usedif (prepareOptimizedReturn(ReturnAtPlus1)) returnobj; // Otherwise, go to the automatic release poolreturnobjc_autorelease(obj); } static ALWAYS_INLINE bool prepareOptimizedReturn(ReturnDisposition disposition) { assert(getReturnDisposition() == ReturnAtPlus0); / / check to use the method of the function or the caller calls list, if then perform objc_retainAutoreleasedReturnValue, will not be registered in the autoreleasePoolif (callerAcceptsOptimizedReturn(__builtin_return_address(0)) {// Set the flag ReturnAtPlus1if (disposition) setReturnDisposition(disposition);
        return true;
    }

    return false; } // Store ReturnAtPlus1 or ReturnAtPlus0 to TLS static ALWAYS_INLINE voidsetReturnDisposition(ReturnDisposition disposition) { tls_set_direct(RETURN_DISPOSITION_KEY, (void*)(uintptr_t)disposition); } // Fetch the tag static ALWAYS_INLINE ReturnDispositiongetReturnDisposition() {
    return (ReturnDisposition)(uintptr_t)tls_get_direct(RETURN_DISPOSITION_KEY);
}
Copy the code

The objc_autoreleaseReturnValue code logic is roughly divided into:

  1. Check whether a call is immediately followed in the caller’s methodobjc_retainAutoreleasedReturnValue.
  2. Save ReturnAtPlus1 to TLS.
  3. Returns the object if optimization is used, otherwise it is added to the automatic release pool.

Below is the objc_retainAutoreleasedReturnValue source code analysis:

Obj id objc_retainAutoreleasedReturnValue (id) {/ / if the TLS in tag using the optimization program, is returned directlyif (acceptOptimizedReturn() == ReturnAtPlus1) return obj;

    return objc_retain(obj);
}

static ALWAYS_INLINE ReturnDisposition acceptOptimizedReturn() {// Return ReturnDisposition disposition = getReturnDisposition(); // Restore to an unoptimized statesetReturnDisposition(ReturnAtPlus0);  // reset to the unoptimized state
    return disposition;
}
Copy the code

Objc_retainAutoreleasedReturnValue code logic roughly divided into:

  1. Retrieve the TLS tag.
  2. Reset TLS markup to ReturnAtPlus0.
  3. Return object if optimization is used, otherwise reference count + 1.

Through the analysis of the source code can be learned that the following code optimization process and not optimization process is quite a big difference:

strongArrayFunction() {
	id obj = objc_msgSend(NSMutableArray, @selector(array));
	objc_retainAutoreleasedReturnValue(obj)
	objc_release(obj);
}
Copy the code

The final optimization process is equivalent to:

id obj = objc_msgSend(objc_msgSend(NSMutableArray, @selector(alloc)), @selector(init));
objc_release(obj);
Copy the code

The unoptimized process is equivalent to:

id obj = objc_msgSend(objc_msgSend(NSMutableArray, @selector(alloc)), @selector(init));
objc_autorelease(obj);
objc_retain(obj);
objc_release(obj);
Copy the code

The __weak modifier

As we all know, weak indicates a weak reference and the reference count does not increase. After the original object is released, the weak reference variable will also be removed.

4.1 Middle code for the __weak modifier

First convert the following code to intermediate code:

void weakFunction() {
    __weak id obj = [NSObject new];
}

void weak1Function() {
    id obj = [NSObject new];
    __weak id obj1 = obj;
}

void weak2Function() {
    id obj = [NSObject new];
    __weak id obj1 = obj;
    NSLog(@"% @",obj1);
}
Copy the code

Below is the intermediate code after transformation and refining:

weakFunction() {
	id temp = objc_msgSend(NSObject, @selector(new));
	objc_initWeak(&obj, temp);
	objc_release(temp);
	objc_destroyWeak(obj);
}

weak1Function() {
	id obj = objc_msgSend(NSObject, @selector(new));
	objc_initWeak(&obj1, obj);
	objc_destroyWeak(obj1);
	objc_storeStrong(obj, null);
}

weak2Function() {
	id obj = objc_msgSend(NSObject, @selector(new));
	objc_initWeak(obj1, obj);
	id temp = objc_loadWeakRetained(obj1);
	NSLog(@"% @",temp);
	objc_release(temp);
	objc_destroyWeak(obj1);
	objc_storeStrong(obj, null);
}	
Copy the code
  • WeakFunction: After declaring __weak object in this method, it is not used, so after objc_initWeak, it immediately releases and calls objc_Release and objc_destroyWeak methods.

  • Weak1Function: in this method, OBj is a strong reference, obj1 is a weak reference, objc_initWeak and objc_destroyWeak are called in pairs successively, corresponding to the initialization and release methods of weak reference variables.

  • Weak2Function: Different from weak1Function is that weak-reference variable obj1 is used. Before weak-reference variable is used, the compiler creates a temporary strong-reference object, which is released immediately after it is used up.

4.2 objc_initWeak and objc_destroyWeak

2 objc_initWeak and objc_destroyWeak

Here are the objc_initWeak and objc_destroyWeak code implementations:

id objc_initWeak(id *location, id newObj) {
    if(! newObj) { *location = nil;returnnil; } // This address has no value and is being assigned a new valuereturnstoreWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating> (location, (objc_object*)newObj); } void objc_destroyWeak(id *location) {// This address has a value, no new value, Crash (void)storeWeak<DoHaveOld, DontHaveNew, DontCrashIfDeallocating> (location, nil); }Copy the code

In the end, storeWeak is used to implement the respective logic. Before we look at storeWeak, we should first understand the meaning of its template parameters:

storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating> (location, (objc_object*)newObj);
Copy the code

DontHaveOld, DoHaveNew, and DoCrashIfDeallocating are template parameters, which are as follows:

enum HaveOld { DontHaveOld = false, DoHaveOld = true}; // No value enum HaveNew {DontHaveNew =false, DoHaveNew = true}; // New value enum CrashIfDeallocating {DontCrashIfDeallocating =false, DoCrashIfDeallocating = true}; // Whether the object being released crashesCopy the code

4.2.2 storeWeak

Continue with the implementation of storeWeak:

template <HaveOld haveOld, HaveNew haveNew,
          CrashIfDeallocating crashIfDeallocating>
static id storeWeak(id *location, objc_object *newObj)
{
    assert(haveOld  ||  haveNew);
    if(! haveNew) assert(newObj == nil); Class previouslyInitializedClass = nil; id oldObj; SideTable *oldTable; SideTable *newTable; Retry: // Remove the SideTable from SideTables that stores weak-referenced tables (weak_table_t)if (haveOld) {
        oldObj = *location;
        oldTable = &SideTables()[oldObj];
    } else {
        oldTable = nil;
    }
    if (haveNew) {
        newTable = &SideTables()[newObj];
    } else{ newTable = nil; } SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable); // If the value pointed to by location changes, the oldObj is retrieved againif(haveOld && *location ! = oldObj) { SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable); goto retry; } // If there is a new valueifClass CLS = newObj->getIsa();if(cls ! = previouslyInitializedClass && ! ((objc_class *) CLS)->isInitialized()) {// Create a non-metaclass and initialize it, SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable); _class_initialize(_class_getNonMetaClass(cls, (id)newObj)); previouslyInitializedClass = cls; goto retry; }} // If there are old values, clear the weak reference table corresponding to the old valuesif(haveOld) { weak_unregister_no_lock(&oldTable->weak_table, oldObj, location); } // If a new value is assigned, register the weak reference table corresponding to the new valueif(haveNew) { newObj = (objc_object *) weak_register_no_lock(&newTable->weak_table, (id)newObj, location, crashIfDeallocating); // Set the ISA flag bitweakly_referenced totrue
        if(newObj && ! newObj->isTaggedPointer()) { newObj->setWeaklyReferenced_nolock();
        }

        // Do not set *location anywhere else. That would introduce a race.
        *location = (id)newObj;
    }
    else {
        // No new value. The storage is not changed.
    }
    SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
    return (id)newObj;
}
Copy the code

This code does a few things:

  1. From the global hash tableSideTables, using the address of the object itself to carry out bit operation to get the corresponding subscript, obtain the weak reference table of the object.SideTablesIs a hash table of 64 elements, and when a collision occurs, maybe oneSideTableThere are multiple objects sharing a weak reference table in.
  2. If a new value is allocated, the class corresponding to the new value is checked to see if it has been initialized. If not, the class is initialized in place.
  3. If location points to another old value, the weak reference table corresponding to the old value is unregistered.
  4. If a new value is assigned, the new value is registered in the corresponding weak reference table. willisa.weakly_referencedSet totrue, indicating that the object has weak reference variables and the weak reference table should be cleared when the object is released.

Holdings weak_register_no_lock and weak_unregister_no_lock

Weak_register_no_lock and weak_unregister_NO_lock are used in the above code to register and unregister weak reference tables. Continue to check the implementation of these two methods:

id weak_register_no_lock(weak_table_t *weak_table, id referent_id, id *referrer_id, bool crashIfDeallocating) { objc_object *referent = (objc_object *)referent_id; // Referrer = (objc_object **)referrer_id; // weak reference variableif(! referent || referent->isTaggedPointer())returnreferent_id; // Check that the object is not locating; bool dealLocating;if(! referent->ISA()->hasCustomRR()) { deallocating = referent->rootIsDeallocating(); }else {
        BOOL (*allowsWeakReference)(objc_object *, SEL) =
        (BOOL(*)(objc_object *, SEL))
        object_getMethodImplementation((id)referent,
                                       SEL_allowsWeakReference);
        if ((IMP)allowsWeakReference == _objc_msgForward) {
            returnnil; } deallocating = ! (*allowsWeakReference)(referent, SEL_allowsWeakReference); } // If releasing, determine whether to trigger crash based on crashIfDeallocatingif (deallocating) {
        if (crashIfDeallocating) {
            _objc_fatal("Cannot form weak reference to instance (%p) of "
                        "class %s. It is possible that this object was "
                        "over-released, or is in the process of deallocation.",
                        (void*)referent, object_getClassName((id)referent));
        } else {
            returnnil; } } weak_entry_t *entry; // Add the weak reference record to the weak table if the weak reference record exists in the current tableif ((entry = weak_entry_for_referent(weak_table, referent))) {
        append_referrer(entry, referrer);
    }
    elseWeak_entry_t new_entry(referent, referrer); weak_entry_t new_entry(referent, referrer); Weak_grow_maybe (weak_table); weak_grow_maybe(weak_table); Weak_entry_insert (weak_table, &new_entry); weak_entry_insert(weak_table, &new_entry); }return referent_id;
}
Copy the code

The main logic of this code is:

  1. Check whether it is being released, and if so, according tocrashIfDeallocatingCheck whether a crash is triggered.
  2. checkweak_tableIs there a corresponding to the referenced object inentryIf so, add the address of the weak reference variable pointer directly to itentryIn the.
  3. ifweak_tableI didn’t find a matchentry, create a new oneentryAnd add the weak reference variable pointer addressentryIn the. At the same time checkweak_tableWhether to expand the capacity.

Weak_unregister_no_lock code is implemented below:

void weak_unregister_no_lock(weak_table_t *weak_table, id referent_id, id *referrer_id) { objc_object *referent = (objc_object *)referent_id; // Referrer = (objc_object **)referrer_id; // Weak_entry_t *entry;if(! referent)return;

    if((entry = weak_entry_for_referent(weak_table, referent)) {weak_entry_for_referent(weak_table, referent)) { Remove the reference from the record remove_referrer(entry, referrer); // Check if the reference record is empty bool empty =true;
        if(entry->out_of_line() && entry->num_refs ! = 0) { empty =false;
        }
        else {
            for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
                if (entry->inline_referrers[i]) {
                    empty = false; 
                    break; }}} // Remove the record if the current record is emptyif(empty) { weak_entry_remove(weak_table, entry); }}}Copy the code

The main logic of this code is:

  1. fromweak_tableAccording to find the corresponding of the referenced objectentry, and then will weakly reference the variable pointerreferrerfromentryRemoved.
  2. Removes a weakly referenced variable pointerreferrerAfter that, checkentryWhether it is empty, and if it is, remove it fromweak_tableRemoved.

4.2.4 weak_table

The above code uses weak_table to save the entry of the referenced object, and the following continues to analyze the specific implementation of the addition and deletion function of Weak_table:

static weak_entry_t * weak_entry_for_referent(weak_table_t *weak_table, objc_object *referent)
{
    assert(referent);
    weak_entry_t *weak_entries = weak_table->weak_entries;
    if(! weak_entries)returnnil; Size_t begin = hash_pointer(referent) & Weak_table ->mask; size_t index = begin; size_t hash_displacement = 0; // Linear probe, if the subscript stores other objects, move down until the correct subscript is found.while(weak_table->weak_entries[index].referent ! = referent) { index = (index+1) & weak_table->mask; // Cannot exceed the maximum length limit of weak_table // Return to the initial subscript, abnormal error is reportedif(index == begin) bad_weak_table(weak_table->weak_entries); // Every time the conflict moves down hash_displacement+ 1, the current displacement does not exceed the maximum recorded displacement hash_displacement++;if (hash_displacement > weak_table->max_hash_displacement) {
            returnnil; }}return &weak_table->weak_entries[index];
}
Copy the code

The above code is the process of weak_table searching for entry, and also the hash table addressing process, using the method of linear detection to solve the problem of hash conflict:

  1. Obtain the hash table subscript by calculating the address of the referenced object.
  2. Check if the corresponding subscript is stored and we want to find the address, if so return that address.
  3. If not, keep looking until you find it. In the process of moving down, the subscript cannot be exceededweak_tableMaximum length, at the same timehash_displacementCan’t exceed the recordmax_hash_displacementMaximum hash displacement.max_hash_displacementIs the maximum hash displacement recorded for all insert operations, and if it exceeds that, there must be an error.

The following is the code implementation of weak_table inserting entry:

static void weak_entry_insert(weak_table_t *weak_table, weak_entry_t *new_entry) { weak_entry_t *weak_entries = weak_table->weak_entries; assert(weak_entries ! = nil); Size_t begin = hash_pointer(new_entry->referent) & (weak_table->mask); // Use the hash algorithm to get the subscript size_t begin = hash_pointer(new_entry->referent) & (weak_table->mask); size_t index = begin; size_t hash_displacement = 0; // Determine whether the current subscript is empty, if not continue to address spacewhile(weak_entries[index].referent ! = nil) { index = (index+1) & weak_table->mask;if(index == begin) bad_weak_table(weak_entries); hash_displacement++; } // Store weak_entries[index] = *new_entry; weak_table->num_entries++; // Update the maximum hash shift valueif(hash_displacement > weak_table->max_hash_displacement) { weak_table->max_hash_displacement = hash_displacement; }}Copy the code

Similar to the checking process, the steps for weak_table to insert entry are as follows:

  1. Obtain the hash table subscript by calculating the address of the referenced object.
  2. Check whether the corresponding subscript is empty. If not, continue searching until a space is found.
  3. The weak reference variable pointer is put into the void and updated at the same timeweak_tableThe current number of members ofnum_entriesAnd the maximum hash displacementmax_hash_displacement.

The following is the code implementation of weak_table to remove entry:

Static void weak_entry_remove(weak_table_t *weak_table, weak_entry_t *entry) {// Release all weak references in the entryif(entry->out_of_line()) free(entry->referrers); // void pointer bzero(entry, sizeof(*entry)); Weak_table ->num_entries--; // Update the number of weak_table objects and check whether the table size can be reduced. weak_compact_maybe(weak_table); }Copy the code

The steps for removing entry from weak_table are as follows:

  1. The release ofentryAnd weak reference variables in it.
  2. Update the number of Weak_table objects and check whether the table size can be reduced

4.2.5 entry and referrer

In the weak reference table entry corresponds to the object being referenced, and the referrer represents the weak reference variable. Each time it is weakly referenced, the weak reference variable pointer referrer is added to the entry, and when the original object is released, the entry is emptied and removed.

Let’s look at the implementation of adding referrer to entry:

static void append_referrer(weak_entry_t *entry, objc_object **new_referrer)
{
    if(! Entry ->out_of_line()) {// Inline_referrers are added directly to the inline_referrers when inline_referrers are not exceededfor (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
            if (entry->inline_referrers[i] == nil) {
                entry->inline_referrers[i] = new_referrer;
                return; } // If inline_referrers exceeds WEAK_INLINE_COUNT, Weak_referrer_t *new_referrers = (Weak_referrer_t *) calloc(WEAK_INLINE_COUNT, sizeof(Weak_referrer_t)); // Transfer the inline_referrers reference to new_referrers onlyfor(size_t i = 0; i < WEAK_INLINE_COUNT; i++) { new_referrers[i] = entry->inline_referrers[i]; } // referrers = new_referrers; entry->num_refs = WEAK_INLINE_COUNT; entry->out_of_line_ness = REFERRERS_OUT_OF_LINE; entry->mask = WEAK_INLINE_COUNT-1; entry->max_hash_displacement = 0; } assert(entry->out_of_line()); // Perform capacity expansion when the load factor is too highif (entry->num_refs >= TABLE_SIZE(entry) * 3/4) {
        returngrow_refs_and_insert(entry, new_referrer); } // Calculate the subscript size_t begin = w_hash_pointer(new_referrer) & (entry->mask); size_t index = begin; size_t hash_displacement = 0; // The following table position is not empty, occurshashThe collision,while(entry->referrers[index] ! = nil) {// move hash_displacement++; index = (index+1) & entry->mask;if(index == begin) bad_weak_table(entry); } // Record the maximum displacementif(hash_displacement > entry->max_hash_displacement) { entry->max_hash_displacement = hash_displacement; Weak_referrer_t &ref = entry-> Referrers [index]; weak_referrer_t &ref = entry-> Referrers [index]; ref = new_referrer; entry->num_refs++; }Copy the code

The structure of Entry is similar to that of Weak_table, which uses hash table and linear detection method to find corresponding positions. On this basis there is a bit of a difference:

  1. entryThere’s a flag bitout_of_lineInitially, the flag bit isfalse.entryI’m using an ordered arrayinline_referrersThe storage structure of.
  2. wheninline_referrersThe number of members exceedsWEAK_INLINE_COUNT.out_of_lineFlag bit becomestrueTo start using hash table storage structures. Capacity expansion occurs whenever the hash table load exceeds 3/4.

Moving on to the concrete implementation of removing the referrer from Entry:

static void remove_referrer(weak_entry_t *entry, objc_object **old_referrer)
{
    if(! Entry ->out_of_line()) {// If inline_referrers is not exceeded, the corresponding position will be cleared directlyfor (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
            if (entry->inline_referrers[i] == old_referrer) {
                entry->inline_referrers[i] = nil;
                return;
            }
        }
        _objc_inform("Attempted to unregister unknown __weak variable "
                     "at %p. This is probably incorrect use of "
                     "objc_storeWeak() and objc_loadWeak(). "
                     "Break on objc_weak_error to debug.\n", 
                     old_referrer);
        objc_weak_error();
        return; Size_t begin = w_hash_pointer(old_referrer) & (entry->mask); size_t index = begin; size_t hash_displacement = 0; // A hash conflict occurswhile(entry->referrers[index] ! = old_referrer) { index = (index+1) & entry->mask;if (index == begin) bad_weak_table(entry);
        hash_displacement++;
        if (hash_displacement > entry->max_hash_displacement) {
            _objc_inform("Attempted to unregister unknown __weak variable "
                         "at %p. This is probably incorrect use of "
                         "objc_storeWeak() and objc_loadWeak(). "
                         "Break on objc_weak_error to debug.\n", 
                         old_referrer);
            objc_weak_error();
            return; } // referrers[index] = nil; entry->num_refs--; }Copy the code

Steps to remove the referrer from Entry:

  1. out_of_lineforfalseWhen from an ordered arrayinline_referrersTo find and remove.
  2. out_of_linefortrueFrom the hash table and remove.

4.2.6 dealloc

When the referenced object is released, the ISA.Weakly_referenced flag bit is checked, and each weakly_referenced flag bit is true.

- (void)dealloc {
    _objc_rootDealloc(self);
}
Copy the code

Follow the dealloc method logic down to clearDeallocating_slow:

NEVER_INLINE void objc_object::clearDeallocating_slow() { assert(isa.nonpointer && (isa.weakly_referenced || isa.has_sidetable_rc)); Weak_table SideTable& table = SideTables()[this]; table.lock(); // Determine if there is weak reference to clear the corresponding entry of the objectif(isa.weakly_referenced) { weak_clear_no_lock(&table.weak_table, (id)this); } // Clear the reference count for this object stored in sideTableif (isa.has_sidetable_rc) {
        table.refcnts.erase(this);
    }
    table.unlock();
}
Copy the code

As can be seen from the above code, when the object release executes the dealloc function, it checks the ISA.WeakLY_referenced flag bit and then decides whether to clean the entry in the Weak_table.

4.3 objc_loadWeakRetained

As you can see from the intermediate code analysis above, before using weak reference variables, the compiler creates a temporary strong reference object to ensure that it will not cause errors when using weak reference variables. It will be released immediately after use.

Let’s see how we can strongly reference a weak reference pointer to an object:

id objc_loadWeakRetained(id *location) { id obj; id result; Class cls; SideTable *table; Retry: // Get a weak reference pointer to the object obj = *location;if(! obj)return nil;
    if (obj->isTaggedPointer()) returnobj; Weak_table table = &SideTables()[obj]; // Retry table->lock() if the referenced object changes in the meantime;if(*location ! = obj) { table->unlock(); goto retry; } result = obj; cls = obj->ISA();if(! CLS ->hasCustomRR()) {// Classes and superclasses are not customized Retain/release/autorelease/retainCount / _tryRetain / _isDeallocating/retainWeakReference/allowsWeakReference method assert(cls->isInitialized()); / / try to retainif(! obj->rootTryRetain()) { result = nil; }}else {
        if(CLS - > isInitialized () | | _thisThreadIsInitializingClass (CLS)) {/ / to get custom BOOL SEL_retainWeakReference method (*tryRetain)(id, SEL) = (BOOL(*)(id, SEL)) class_getMethodImplementation(cls, SEL_retainWeakReference);if((IMP)tryRetain == _objc_msgForward) { result = nil; } // Call a custom functionelse if(! (*tryRetain)(obj, SEL_retainWeakReference)) { result = nil; }}else{// class is not initialized, then retry table->unlock(); _class_initialize(cls); goto retry; } } table->unlock();return result;
}
Copy the code

The main logic of the above code is:

  1. The weak reference table is obtained from the object to which the weak reference points and locked to prevent it from being purged in the meantime.
  2. Check whether custom is includedretainMethod, if not, use the defaultrootTryRetainMethod to make the reference count + 1.
  3. If custom is usedretainMethod, the custom method will be called. Before calling, it will determine whether the class of the object has been initialized. If not, it will be initialized first and then called.

The __autorelease modifier

In an ARC environment, the __autoRelease modifier adds objects to the autorelease pool, which manages the release.

5.1 Intermediate code for the __autorelease modifier

Convert the following code to intermediate code to see what the compiler does:

void autoReleasingFunction() { @autoreleasepool { __autoreleasing id obj = [NSObject new]; }}Copy the code

Intermediate code after conversion:

void autoReleasingFunction() {
	id token = objc_autoreleasePoolPush();
	id obj = objc_msgSend(NSObject, @selector(new));
	objc_autorelease(obj);
	objc_autoreleasePoolPop(token);	
}
Copy the code

The above code can be analyzed as follows:

  • @autoreleasepool{}The keyword is converted by the compiler toobjc_autoreleasePoolPushandobjc_autoreleasePoolPopThis pair of methods.
  • The __autoreleasing modifier is converted toobjc_autoreleaseThat will beobjAdd to automatic release pool.

As can be seen from the above analysis, the compiler’s processing logic for automatic release pool can be roughly divided into:

  1. byobjc_autoreleasePoolPushAs the first function to automatically release the pool scope.
  2. useobjc_autoreleaseAdds objects to the automatic release pool.
  3. byobjc_autoreleasePoolPopAs the last function to automatically release the pool scope.

5.2 Automatic release pool preparation

Before we go any further, we need to know a few things about auto-release pools:

Each autoreleasepoolpool consists of one or more AutoreleasepoolPages, 4096 bytes in SIZE, that form a bidirectional linked list using parent and child Pointers.

  • HotPage: the current page in use. Operations are performed on the hotPage, usually at the end of the list or the second to last. Stored in TLS, it can be understood as a linked list of auto-release pools shared by each thread.

  • ColdPage: The page at the head of the list, possibly also hotPage.

  • POOL_BOUNDARY: nil macro definition that replaces the previous sentinel object POOL_SENTINEL to be pushed into the auto-release pool in objc_autoreleasePoolPush when the auto-release pool is created. When calling objc_autoreleasePoolPop, objects in the pool are released sequentially until the last POOL_BOUNDARY is reached.

  • EMPTY_POOL_PLACEHOLDER: Pushing a POOL_BOUNDARY when no object has been pushed into the autofree pool stores EMPTY_POOL_PLACEHOLDER in TLS as the identifier and POOL_BOUNDARY is not pushed into the pool at this time. The next time an object is pushed into the automatic release pool, check to retrieve the identifier in TLS and then push POOL_BOUNDARY.

  • Next: pointer to AutoreleasePoolPage A pointer to a slot at the top of the stack that moves up each time a new element is added.

5.3 objc_autoreleasePoolPush

Objc_autoreleasePoolPush method code implementation:

static inline void *push() 
{
    id *dest;
    if(DebugPoolAllocation) {// In test state, a new page is created for each automatic release pool. Dest = autoreleaseNewPage(POOL_BOUNDARY); }else{// push a POOL_BOUNDARY onto the stack, dest = autoreleaseFast(POOL_BOUNDARY); } assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);return dest;
}
Copy the code

The objc_autoreleasePoolPush method essentially pushes a POOL_BOUNDARY into the automatic releasepool as the starting point for the AutoReleasepool. The specific logic of the autoreleaseFast method will be examined later when the AutoRelease method is examined.

5.4 autorelease

Here’s how to add the autorelease method to the pool:

static inline id autorelease(id obj) {
        id *dest __unused = autoreleaseFast(obj);
        return obj;
}
Copy the code

Read on:

static inline id *autoreleaseFast(id obj) {
    AutoreleasePoolPage *page = hotPage();
    
    if(page && ! Page ->full()) {// Add a hotPage to the stackreturn page->add(obj);
    } else if(page) {// hotPage is full. Create a page and add it to a new pagereturn autoreleaseFullPage(obj, page);
    } else{// Create a new Page without hotPage and add it to the PagereturnautoreleaseNoPage(obj); }}Copy the code

The above code logic:

  • ifhotPageIf it exists and is not full, push it directlyhotPage.
  • ifhotPageExists and is full, callautoreleaseFullPage.
  • ifhotPageNot present, callautoreleaseNoPage.

The implementation of autoreleaseFullPage continues below:

id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) { assert(page == hotPage()); assert(page->full() || DebugPoolAllocation); // If no page is found, create a new page and set to hotPagedo { 
        if (page->child) page = page->child;
        else page = new AutoreleasePoolPage(page);
    } while (page->full());
    setHotPage(page);
    return page->add(obj);
}
Copy the code

This method is executed when the hotPage is full. The logic is as follows:

  1. To viewhotPageIs there a successor node? If so, use the successor node directly.
  2. If there is no successor node, create oneAutoreleasePoolPage.
  3. Adds an object to the obtainedpageAnd set it tohotPage, is stored in TLS to share.

Let’s start with the autoreleaseNoPage method code implementation:

Id *autoreleaseNoPage(id obj) {// No page indicates that there is No release pool yet, or that there is an empty placeholder pool, but the object assert(! hotPage()); bool pushExtraBoundary =false;
    if(haveEmptyPoolPlaceholder()) {// If it is an empty placeholder pool, add a free pool boundary to pushExtraBoundary =true;
    }
    else if(obj ! = POOL_BOUNDARY && DebugMissingPools) { // We are pushing an object with no poolin place, 
        // and no-pool debugging was requested by environment.
        _objc_inform("MISSING POOLS: (%p) Object %p of class %s "
                     "autoreleased with no pool in place - "
                     "just leaking - break on "
                     "objc_autoreleaseNoPool() to debug", 
                     pthread_self(), (void*)obj, object_getClassName(obj));
        objc_autoreleaseNoPool(obj);
        returnnil; } // If POOL_BOUNDARY is passed, the empty pool placeholder is setelse if(obj == POOL_BOUNDARY && ! DebugPoolAllocation) {return setEmptyPoolPlaceholder(); } // We are pushing an object or a non-placeholder D pool. Set hotPage AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);setHotPage(page); // Insert the release pool boundaryif(pushExtraBoundary) { page->add(POOL_BOUNDARY); } // Objects are added to the release poolreturn page->add(obj);
}
Copy the code

AutoreleaseNoPage is only called when the autoreleaseNoPage has no page. The main logic is:

  1. If the current auto release pool is pushed into a sentryPOOL_BOUNDARYWhen willEmptyPoolPlaceholderStore in TLS.
  2. If TLS is storedEmptyPoolPlaceholderWhen creating wellpageAfter that, one will be pushed in firstPOOL_BOUNDARY, and then push in the objects that join the auto-release pool.

5.5 objc_autoreleasePoolPop

When the scope of the automatic release pool ends, objc_autoreleasePoolPop is called torelease the objects in the automatic release pool.

static inline void pop(void *token) { AutoreleasePoolPage *page; id *stop; // If it is an empty pool placeholder, empty the entire autofree poolif (token == (void*)EMPTY_POOL_PLACEHOLDER) {
        ifPop pop(coldPage()->begin()); }else{// Unused release pool, empty hotPage stored in TLSsetHotPage(nil);
        }
        return;
    }
    page = pageForPointer(token);
    stop = (id *)token;
    if(*stop ! = POOL_BOUNDARY) {coldPage()->begin();if(stop == page->begin() && ! page->parent) { }else{// if POOL_BOUNDARY is not coldPage()->begin(returnbadPop(token); }}if (PrintPoolHiwat) printHiwat(); // release all objects after stop page->releaseUntil(stop); // Clear subsequent node pagesif(page->child) {// If the current page is not half full, all subsequent pages are killedif (page->lessThanHalfFull()) {
            page->child->kill(a); } // If the current page reaches more than half full, keep the next pageelse if (page->child->child) {
            page->child->child->kill(a); }}}Copy the code

The above code logic:

  1. Check if the input parameter is an empty pool placeholderEMPTY_POOL_PLACEHOLDERIf yes, continue to determine whetherhotPageExists, ifhotPageExistence changes the endpoint of release tocoldPage()->begin()If thehotPageIf the TLS storage does not exist, the TLS storage is emptyhotPage.
  2. checkstopneitherPOOL_BOUNDARYIs notcoldPage()->begin()The case will report an error.
  3. Clear the automatic release poolstopAll objects after that.
  4. Determine the currentpageIf it is not half full, all subsequent pages are killed, if it is more than half full, only the next page is keptpage.

5.6 the add and releaseUntil

The entire AutoreleasePoolPage is a stack, and the AutoreleasePoolPage method adds objects to the automatic release pool:

id *add(id obj) { assert(! full()); unprotect(); id *ret = next; // copy pointer *next++ = obj; // Next moves back to the next empty address protect() after storing obj to the memory address pointed to by next;return ret;
}
Copy the code

The logic is very simple: the add method stores the newly added object to the address pointed to by the pointer next at the top of the stack, and then points to the next location.

AutoreleasePoolPage unstack implementation

Void releaseUntil(id *stop) {// If next and stop do not point to the same memory address, the execution continueswhile(this->next ! = stop) { AutoreleasePoolPage *page = hotPage(); // If hotPage is empty, set the previous page to hotPagewhile (page->empty()) {
            page = page->parent;
            setHotPage(page); } page->unprotect(); id obj = *--page->next; Memset ((void*)page->next, SCRIBBLE, sizeof(*page->next)); // Clear page->protect();if(obj ! = POOL_BOUNDARY) { objc_release(obj); // top element reference count - 1}}setHotPage(this);
}
Copy the code

The code looks like this:

  1. Determine the top of the stack pointernextandstopIf it is not to the same memory address, continue to unstack.
  2. Determine the currentpageIf it is cleared, it proceeds to clean up the previous listpage.
  3. When out of the stack, the top pointer moves down to clear the top memory.
  4. If the current stack is notPOOL_BOUNDARYThe callobjc_releaseReference count – 1.

experience

By reading the objC4 source code, I was able to connect my previous ARC knowledge with a better understanding of how the details work.

If you think this article is good, you can go to the original article “Understanding ARC implementation principles” to ✨.

reference

  • Objective-c advanced programming