iOS underlying principle 32: memory management

Keywords: iOS Interview

This paper mainly analyzes the memory management scheme in memory management and the underlying source code analysis of retain, retain count, release and dealloc

ARC & MRC

Memory management schemes in iOS can be roughly divided into two categories: MRC (manual memory management) and ARC (automatic memory management)

MRC

  • In the MRC era, the system determines whether an object is destroyed by counting its references. There are the following rules

    • The reference count is 1 when the object is created

    • When an object is referenced by other pointers, you need to manually call [objc retain] to make the reference count of the object + 1

    • When the pointer variable no longer uses the object, you need to manually call [objc release] to release the object so that the reference count of the object is - 1

    • When the reference count of an object is 0, the system will destroy the object

  • Therefore, in MRC mode, it must be followed: who creates, who releases, who references and who manages
    ARC

  • ARC mode is an automatic management mechanism introduced in WWDC2011 and iOS5, that is, automatic reference counting. Is a feature of the compiler. The rules are the same as MRC, except that manual retain, release and autorelease are not required in ARC mode. The compiler inserts release and autorelease in place.

Memory layout

We introduced the five memory regions in iOS underlying principle 24: five memory regions. In fact, in addition to the memory area, there are also the kernel area and reserved area. Take the 4GB mobile phone as an example, as shown below, the system gives 3GB to the five areas + reserved area, and the remaining 1GB to the kernel area

Memory layout

  • Kernel area: the area used by the system for kernel processing operations

  • Five regions: no more explanation here. Please refer to the link above for details

  • Reserved area: reserved for system processing nil, etc

Here is a question: why does the last memory address of the five regions start from 0x00400000. The main reason is that 0x00000000 represents nil and cannot directly represent a segment with nil, so a separate segment of memory is given to deal with nil and other situations

Memory layout related interview questions

Interview question 1: is there any difference between global variables and local variables in memory? If so, what is the difference?

  • There are differences

  • Global variables are stored in the global storage area of memory (i.e. bss+data segment) and occupy static storage units

  • Local variables are stored in the stack, and storage units are dynamically allocated to variables only when the function is called

Interview question 2: can I modify global variables, global static variables, local static variables and local variables in Block?

  • You can modify global variables and global static variables, because global variables and static global variables are global and have a wide scope

  • Local static variables can be modified, but local variables cannot be modified

    • Local static variables (static modified) and local variables are captured from the outside by block and become  __ main_block_impl_0 is a member variable of this structure

    • Local variables are passed to the block constructor in the form of values, and only the variables used in the block will be captured. Since only the value of the variable is captured, not the memory address, the value of the local variable cannot be changed inside the block

    • A local static variable is captured by a block in the form of a pointer. Since a pointer is captured, the value of a local static variable can be modified

  • In ARC environment, once used__ Modify the block and modify it in the block to trigger copy, and the block will copy from the stack area to the heap area. At this time, the block is the heap area block

  • In ARC mode, block refers to data of id type, whether there is one or not__ Block modification will retain, but not for basic data types__ Block cannot modify the variable value; If so__ Block modification is also modified at the bottom__ Block_byref_a_0 structure, pointing its internal forwarding pointer to the address after copy to modify the value

Memory management scheme

In addition to MRC and ARC mentioned above, there are three memory management schemes

  • Tagged Pointer: specially used to handle small objects, such as NSNumber, NSDate, NSString, etc

  • Nonpointer_isa: non pointer type isa is mainly used to optimize 64 bit addresses. This has been introduced in iOS underlying principle 07: the principle of isa and class association

  • SideTables: hash table. There are two main tables in the hash table: reference count table and weak reference table

Here we mainly focus on the Tagged Pointer   And SideTables, we introduce Tagged Pointer through an interview question

Interview questions

What's wrong with the following code?

//*********Code 1*********
- (void)taggedPointerDemo {
  self.queue = dispatch_queue_create("com.cjl.cn", DISPATCH_QUEUE_CONCURRENT);
    
    for (int i = 0; i<10000; i++) {
        dispatch_async(self.queue, ^{
            self.nameStr = [NSString stringWithFormat:@"CJL"];  // alloc heap iOS optimization - taggedpointer
             NSLog(@"%@",self.nameStr);
        });
    }
}

//*********Code 2*********
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"coming");
    for (int i = 0; i<10000; i++) {
        dispatch_async(self.queue, ^{
            self.nameStr = [NSString stringWithFormat:@"CJL_The harder you work, the luckier you are!!!"];
            NSLog(@"%@",self.nameStr);
        });
    }
}

Run the above code and find that there is no problem with taggedPointerDemo running alone. After the touchesBegan method is triggered. The program will crash because multiple threads release an object at the same time, resulting in   Transition release so crash. The root cause is the inconsistency of nameStr's underlying types, which can be seen through debugging

Debug NSString

  • The nameStr type in the taggedpoinerdemo method is   Nstagedpointerstring, stored in the constant area. Because nameStr is in the heap area during alloc allocation, it is small, so it has been optimized by iOS in xcode to become the type of NSTaggedPointerString and stored in the constant area

  • The nameStr type in the touchesBegan method is   NSCFString type, stored on the heap

Memory management of NSString

We can test the memory management of NSString through two ways of NSString initialization

  • adopt   WithString + @ "" initialization

  • adopt   WithFormat mode initialization

#define KLog(_c) NSLog(@"%@ -- %p -- %@",_c,_c,[_c class]);

- (void)testNSString{
    //Initialization method 1: through WithString + @ ""
    NSString *s1 = @"1";
    NSString *s2 = [[NSString alloc] initWithString:@"222"];
    NSString *s3 = [NSString stringWithString:@"33"];
    
    KLog(s1);
    KLog(s2);
    KLog(s3);
    
    //Initialization method 2: through WithFormat
    //String length is within 9
    NSString *s4 = [NSString stringWithFormat:@"123456789"];
    NSString *s5 = [[NSString alloc] initWithFormat:@"123456789"];
    
    //String length greater than 9
    NSString *s6 = [NSString stringWithFormat:@"1234567890"];
    NSString *s7 = [[NSString alloc] initWithFormat:@"1234567890"];
    
    KLog(s4);
    KLog(s5);
    KLog(s6);
    KLog(s7);
}

Here are the results of the run

Operation results

Therefore, it can be concluded from the above that the memory management of NSString is mainly divided into three types

  • __ NSCFConstantString: a string constant. It is a compile time constant. The value of retainCount is very large. Its operation will not change the reference count. It is stored in the string constant area

  • __ NSCFString: it is a subclass of NSString created at runtime. After creation, the reference count will be increased by 1 and stored on the heap

  • NSTaggedPointerString: tag pointer, which is the optimization of NSString, NSNumber and other objects made by apple in 64 bit environment. For NSString objects

    • When the string is composed of numbers and English letters and the length is less than or equal to 9, it will automatically become the NSTaggedPointerString type and stored in the constant area

    • When there are Chinese or other special symbols, they will directly become__ NSCFString type, stored in heap

Tagged Pointer small object

A NSString interview question leads to the Tagged Pointer. In order to explore the reference counting processing of small objects, we need to go to the objc source code to check the references in the retain and release source codes   Handling of Tagged Pointer small objects

Analysis of reference counting processing of small objects

  • View setproperty - > reallysetproperty source code, where is the new value retain and the old value release

  • Enter objc_retain,objc_ The release source code determines whether it is a small object. If it is a small object, it will not be retained or released, but will be returned directly. Therefore, we can draw a conclusion: if the object is a small object, retain and release will not be performed

//****************objc_retain****************
__attribute__((aligned(16), flatten, noinline))
id 
objc_retain(id obj)
{
    if (!obj) return obj;
    //Judge whether it is a small object. If so, the object will be returned directly
    if (obj->isTaggedPointer()) return obj;
    //If it is not a small object, retain
    return obj->retain();
}

//****************objc_release****************
__attribute__((aligned(16), flatten, noinline))
void 
objc_release(id obj)
{
    if (!obj) return;
    //If it is a small object, it returns directly
    if (obj->isTaggedPointer()) return;
    //If it is not a small object, release
    return obj->release();
}

Address analysis of small objects

Continue to take NSString as an example. For NSString

  • General NSString object pointers are string value + pointer address, which are separated

  • For Tagged Pointer, its pointer + value can be reflected in small objects. So Tagged Pointer   Contains both pointers and values

In the previous article on class loading, the_ read_ The images source code has a method to process small objects, that is, the initializetaggedpointerobjfuscator method

  • Enter_ read_ Images - > initializetargedpointerobfuscator source code implementation

static void
initializeTaggedPointerObfuscator(void)
{
    
    if (sdkIsOlderThan(10_14, 12_0, 12_0, 5_0, 3_0) ||
        // Set the obfuscator to zero for apps linked against older SDKs,
        // in case they're relying on the tagged pointer representation.
        DisableTaggedPointerObfuscation) {
        objc_debug_taggedpointer_obfuscator = 0;
    }
    //After iOS14, small objects are confused by and operation+_ OBJC_TAG_MASK confusion
    else {
        // Pull random data into the variable, then shift away all non-payload bits.
        arc4random_buf(&objc_debug_taggedpointer_obfuscator,
                       sizeof(objc_debug_taggedpointer_obfuscator));
        objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
    }
}

In the implementation, we can see that after iOS14, the Tagged Pointer adopts confusion processing, as shown below

  • We can use objc in the source code_ debug_ taggedPointer_ Obfuscator looks for the encoding and decoding of the tagged pointer to see how the underlying confusion is handled

//code
static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
    return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr);
}
//code
static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
    return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;

Through the implementation, we can know that in the encoding and decoding part, there are two layers of XOR, and the purpose is to get the small object itself, such as   1010 0001 as an example, assume that the mask is   0101 1000

    1010 0001 
   ^0101 1000 mask((code)
    1111 1001
   ^0101 1000 mask(Decoding)
    1010 0001
  • Therefore, in the outside world, in order to obtain the real address of the small object, we can copy the decoded source code to the outside and decode the NSString confusion part, as shown below


    Observe the decoded small object address, where 62 represents the ASCII code of b. take NSNumber as an example, we can also see that 1 is our actual value

Here, we have verified that the value is indeed stored in the pointer address of the small object. What are the meanings of 0xa and 0xb in the high order of the small object address?

//NSString
0xa000000000000621

//NSNumber
0xb000000000000012
0xb000000000000025
  • You need to check in the source code_ objc_isTaggedPointer source code, mainly by retaining the highest value (i.e. 64 bit value) to judge whether it is equal to_ OBJC_TAG_MASK (i.e. 2 ^ 63) to judge whether it is a small object

static inline bool 
_objc_isTaggedPointer(const void * _Nullable ptr)
{
    //It is equivalent to PTR & 1 shifting 63 to the left, that is, 2 ^ 63. It is equivalent to that all bits except 64 bits are 0, that is, only the highest value is retained
    return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

Therefore, 0xa and 0xb are mainly used to judge whether it is a small object taggedpointer, that is, the judgment condition, and whether it is 1 in bit 64 (taggedpointer pointer address means pointer address and value)

  • 0xa   Convert to binary   1 010 (64 is 1, and the last three bits of 63 ~ 61 indicate tagType - 2), indicating NSString type

  • 0xb   Convert to binary   1 011 (64 is 1, and the last three digits of 63 ~ 61 represent tagType type - 3), indicating NSNumber type. It should be noted here that if the value of NSNumber is - 1, the value in its address is represented by complement

You can pass here_ objc_ Parameter tag type objc of maketaggedpointer method_ tag_ index_ T enters its enumeration, where   2 means NSString and 3 means NSNumber

  • Similarly, we can define an NSDate object to verify whether its tagType is 6. According to the printing result, the high bit of the address is 0xe, which is converted to binary 1 110, excluding 64 bit 1, and the remaining 3 bits are exactly converted to decimal 6, which is in line with the above enumerated value

    image

Tagged Pointer summary

  • Tagged Pointer small object type (used to store NSNumber, NSDate and small NSString). The small object pointer is no longer a simple address, but an address + value, that is, a real value. Therefore, in fact, it is no longer an object. It is just an ordinary variable covered with an object. So it can be read directly. The advantage is that it takes up less space and saves memory

  • Tagged Pointer small object   Instead of entering retain and release, it returns directly, which means that ARC management is not required, so it can be released and recycled directly by the system

  • The memory of Tagged Pointer is not stored in the heap, but in the constant area. malloc and free are not required, so it can be read directly. Compared with the data stored in the heap, the efficiency is about three times faster. The creation efficiency is nearly 100 times faster than that of the heap

  • Therefore, generally speaking, the taggedPointer's memory management scheme is much faster than the conventional memory management

  • In the 64 bit address of Tagged Pointer, the first four bits represent the type, the last four bits are mainly used for some processing by the system, and the middle 56 bits are used to store the value

  • Memory optimization suggestions: for NSString, when the string is small, it is recommended to initialize directly through @ "", because it is stored in the constant area and can be read directly. It will be faster than WithFormat initialization

SideTables hash table

When the reference count is stored to a certain value, it will not be stored in the nonpointer_ Extra of isa bit field_ RC, but will be stored in SideTables   In hash table

Let's continue to explore the underlying implementation of reference count retain

retain source code analysis

  • Enter objc_ The source code implementation of retain - > retain - > rootretain mainly includes the following logic:

    • 1. If it wasn't Nonpointer_isa, the SideTables hash table is directly operated. At this time, there are not only one hash table, but many hash tables (why multiple hash tables are needed will be analyzed later)

    • 2. Judge whether it is releasing. If it is, execute dealloc process

    • 3. Execute extra_rc+1, that is, the reference count + 1 operation, and give a reference count status ID carry, which is used to represent extra_ Is it full

    • 4. If the status of carray indicates extra_ When the reference count of RC is full, you need to operate the hash table, that is, take out half of the full state and save it to extra_rc, the other half has RC of hash table_ half. The reason for this is that if they are all stored in the hash table, each hash table operation needs to be unlocked. The operation is time-consuming and consumes a lot of performance. The purpose of this half split operation is to improve performance

    • [step 1] judge whether it is a Nonpointer_isa

    • [step 2] operation reference count

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

    bool sideTableLocked = false;
    bool transcribeToSideTable = false;
    //Why is there isa? Because the reference count needs to be + 1, that is, retain+1, and the reference count is stored in the bits of ISA, it is necessary to replace the old isa with the new Isa
    isa_t oldisa;
    isa_t newisa;
    //a key
    do {
        transcribeToSideTable = false;
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
        //Judge whether it is nonpointer isa
        if (slowpath(!newisa.nonpointer)) {
            //If it is not nonpointer isa, directly operate the hash sidetable
            ClearExclusive(&isa.bits);
            if (rawISA()->isMetaClass()) return (id)this;
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
            else return sidetable_retain();
        }
        // don't check newisa.fast_rr; we already called any RR overrides
        //dealloc source code
        if (slowpath(tryRetain && newisa.deallocating)) {
            ClearExclusive(&isa.bits);
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            return nil;
        }
        
        
        uintptr_t carry;
        //Execute the reference count + 1 operation, that is, 1ull < < 45 (arm64) in bits, that is, extra_rc, used to store reference count values for this object
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++
        //Judge extra_ Whether RC is full, and carry is the identifier
        if (slowpath(carry)) {
            // newisa.extra_rc++ overflowed
            if (!handleOverflow) {
                ClearExclusive(&isa.bits);
                return rootRetain_overflow(tryRetain);
            }
            // Leave half of the retain counts inline and 
            // prepare to copy the other half to the side table.
            if (!tryRetain && !sideTableLocked) sidetable_lock();
            sideTableLocked = true;
            transcribeToSideTable = true;
            //If extra_ When RC is full, you can directly take out half of the full state and save it to extra_rc
            newisa.extra_rc = RC_HALF;
            //Give an identifier of YES, indicating that it needs to be stored in the hash table
            newisa.has_sidetable_rc = true;
        }
    } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));

    if (slowpath(transcribeToSideTable)) {
        // Copy the other half of the retain counts to the side table.
        //Put the other half in the RC of the hash table_ In half, that is, in the full state, it is 8 bits, half is 1, shift 7 bits to the left, that is, divide by 2
        //The purpose of this operation is to improve performance, because if all hash tables exist, you need to access the Hash list when release-1 is required. You need to unlock each time to compare the performance consumption. extra_ If you store half of RC, you can directly operate extra_rc is enough, and there is no need to operate the hash table. Performance will improve a lot
        sidetable_addExtraRC_nolock(RC_HALF);
    }

    if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
    return (id)this;
}

Question 1: Why are there multiple hash tables in memory? How many can I get at most?

  • If the hash table has only one table, it means that all global objects will be stored in one table and will be unlocked (the lock is to lock the reading and writing of the whole table). When unlocking, because all data are in one table, it means that the data is not safe

  • If you open a table for each object, it will cost performance, so you can't have countless tables

  • The type of hash table is SideTable, which is defined as follows

struct SideTable {
    spinlock_t slock;//On / off
    RefcountMap refcnts;//Reference count table
    weak_table_t weak_table;//Weak reference table
    
    ....
}
  • By viewing sidetable_ The unlock method locates SideTables, which is obtained internally through the get method of SideTablesMap. SideTablesMap is defined through stripedmap < sidetable >

void 
objc_object::sidetable_unlock()
{
    //SideTables hash tables are not just one, but many, similar to associated object tables
    SideTable& table = SideTables()[this];
    table.unlock();
}
👇
static StripedMap<SideTable>& SideTables() {
    return SideTablesMap.get();
}
👇
static objc::ExplicitInit<StripedMap<SideTable>> SideTablesMap;

Thus, you can enter the definition of StripedMap. From here, you can see that there can only be 8 hash tables in the real machine at the same time

Question 2: Why are hash tables used instead of arrays and linked lists?

  • Array: it is characterized by convenient query (i.e. access through subscript) and troublesome addition and deletion (similar to the previously mentioned methodList, adding and deleting through memcopy and memmove is very troublesome). Therefore, the characteristic of data is fast reading and inconvenient storage

  • Linked list: it is characterized by convenient addition and deletion and slow query (you need to traverse the query from the first node), so the characteristics of linked list are fast storage and slow reading

  • The essence of a hash table is a hash table. The hash table combines the advantages of arrays and linked lists. It is more convenient to add, delete, modify and query. For example, the zipper hash table (in the previous article, the storage structure of tls is zipper), which is the most commonly used, as shown below


    You can verify from sideTables - > stripedmap - > indexforpointer that the hash index is calculated through the hash function   And why [] can be used for sideTables

    Therefore, to sum up, the underlying process of retain is as follows

Summary: retain complete answer

  • retain will first judge whether it is Nonpointer isa at the bottom. If not, it will directly operate the hash table for + 1 operation

  • If it is Nonpointer isa, you also need to judge whether it is releasing. If it is releasing, execute dealloc process to release weak reference table and reference technology table, and finally free object memory

  • If it is not being released, the normal reference count of Nonpointer isa is + 1. It should be noted that extra_rc has only 8 bits on the real machine to store the value of reference count. When the storage is full, it needs to be stored with the help of hash table. You need to add a full extra_rc is split in half, and half (i.e. 2 ^ 7) is stored in the hash table. The other half is still stored in extra_ In RC, the + 1 or - 1 operation for general reference counting, and then return

release source code analysis

After analyzing the underlying implementation of retain, let's analyze the underlying implementation of release

  • Through setproperty - > reallysetproperty - > objc_ In the order of release - > release - > rootRelease - > rootRelease, enter the rootRelease source code, and the operation is the opposite of retain

    • Determine whether half of the reference count is stored in the hash table

    • If yes, take half of the stored reference count from the hash table, perform the - 1 operation, and then store it in extra_rc medium

    • If extra_ If RC has no value and the hash table is empty, the destruct is performed directly, that is, dealloc operation, which is automatically triggered

    • Judge whether it is Nonpointer isa. If not, directly perform - 1 operation on the hash table

    • For Nonpointer isa, extra_ The reference count value in RC performs the - 1 operation and stores the extra at this time_ RC status to carry

    • If the status of carray is 0, go to the underflow process

    • The underflow process has the following steps:

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;
        //Judge whether it is Nonpointer isa
        if (slowpath(!newisa.nonpointer)) {
            //If not, the hash table - 1 is operated directly
            ClearExclusive(&isa.bits);
            if (rawISA()->isMetaClass()) return false;
            if (sideTableLocked) sidetable_unlock();
            return sidetable_release(performDealloc);
        }
        // don't check newisa.fast_rr; we already called any RR overrides
        uintptr_t carry;
        //Perform a reference count - 1 operation, that is, extra_rc-1
        newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc--
        //If extra_ If the value of RC is 0, go to underflow
        if (slowpath(carry)) {
            // don't ClearExclusive()
            goto underflow;
        }
    } while (slowpath(!StoreReleaseExclusive(&isa.bits, 
                                             oldisa.bits, newisa.bits)));

    if (slowpath(sideTableLocked)) sidetable_unlock();
    return false;

 underflow:
    // newisa.extra_rc-- underflowed: borrow from side table or deallocate

    // abandon newisa to undo the decrement
    newisa = oldisa;
    //Determine whether half of the reference count is stored in the hash table
    if (slowpath(newisa.has_sidetable_rc)) {
        if (!handleUnderflow) {
            ClearExclusive(&isa.bits);
            return rootRelease_underflow(performDealloc);
        }

        // Transfer retain count from side table to inline storage.

        if (!sideTableLocked) {
            ClearExclusive(&isa.bits);
            sidetable_lock();
            sideTableLocked = true;
            // Need to start over to avoid a race against 
            // the nonpointer -> raw pointer transition.
            goto retry;
        }

        // Try to remove some retain counts from the side table.
        //Fetches the stored half reference count from the hash table
        size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);

        // To avoid races, has_sidetable_rc must remain set 
        // even if the side table count is now zero.

        if (borrowed > 0) {
            // Side table retain count decreased.
            // Try to add them to the inline count.
            //Perform the - 1 operation and store it in extra_rc medium
            newisa.extra_rc = borrowed - 1;  // redo the original decrement too
            bool stored = StoreReleaseExclusive(&isa.bits, 
                                                oldisa.bits, newisa.bits);
            if (!stored) {
                // Inline update failed. 
                // Try it again right now. This prevents livelock on LL/SC 
                // architectures where the side table access itself may have 
                // dropped the reservation.
                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 (!stored) {
                // Inline update failed.
                // Put the retains back in the side table.
                sidetable_addExtraRC_nolock(borrowed);
                goto retry;
            }

            // Decrement successful after borrowing from side table.
            // This decrement cannot be the deallocating decrement - the side 
            // table lock and has_sidetable_rc bit ensure that if everyone 
            // else tried to -release while we worked, the last one would block.
            sidetable_unlock();
            return false;
        }
        else {
            // Side table is empty after all. Fall-through to the dealloc path.
        }
    }
    //At this point, extra_ If the RC median value is 0 and the hash table is empty, the destruct is performed directly, that is, the dealloc process is automatically triggered
    // Really deallocate.
    //Time to trigger dealloc
    if (slowpath(newisa.deallocating)) {
        ClearExclusive(&isa.bits);
        if (sideTableLocked) sidetable_unlock();
        return overrelease_error();
        // does not actually return
    }
    newisa.deallocating = true;
    if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;

    if (slowpath(sideTableLocked)) sidetable_unlock();

    __c11_atomic_thread_fence(__ATOMIC_ACQUIRE);

    if (performDealloc) {
        //Send a dealloc message
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
    }
    return true;
}

Therefore, to sum up, the underlying process of release is shown in the figure below

dealloc source code analysis

Dealloc destructors are mentioned in the underlying implementation of retain and release. Let's analyze the underlying implementation of dealloc

  • Enter dealloc - >_ objc_ Rootdealloc - > rootdealloc source code implementation, there are two main things:

    • Judge whether there are isa, cxx, associated objects, weak reference table and reference count table according to the conditions. If not, free the memory directly

    • If yes, enter object_dispose method

inline void
objc_object::rootDealloc()
{
    //What needs to be done to release objects?
    //1. isa - cxx - associated object - weak reference table - reference count table
    //2,free
    if (isTaggedPointer()) return;  // fixme necessary?

    //If none of these are available, free directly
    if (fastpath(isa.nonpointer  &&  
                 !isa.weakly_referenced  &&  
                 !isa.has_assoc  &&  
                 !isa.has_cxx_dtor  &&  
                 !isa.has_sidetable_rc))
    {
        assert(!sidetable_present());
        free(this);
    } 
    else {
        //If so
        object_dispose((id)this);
    }
}
  • Enter object_ The purpose of dispose source code is as follows

    • Call c + + destructor

    • Delete associated reference

    • Release hash table

    • Empty weak reference table

    • To destroy an instance, you can:

    • Free free memory

id 
object_dispose(id obj)
{
    if (!obj) return nil;
    //Destroy the instance without freeing memory
    objc_destructInstance(obj);
    //Free memory
    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.
        //Call C + + destructor
        if (cxx) object_cxxDestruct(obj);
        //Delete associated reference
        if (assoc) _object_remove_assocations(obj);
        //release
        obj->clearDeallocating();
    }

    return obj;
}
👇
inline void 
objc_object::clearDeallocating()
{
    //Judge whether it is nonpointer isa
    if (slowpath(!isa.nonpointer)) {
        // Slow path for raw pointer isa.
        //If not, the hash table is released directly
        sidetable_clearDeallocating();
    }
    //If yes, clear the weak reference table + hash table
    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());
}
👇
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) {
        //Empty weak reference table
        weak_clear_no_lock(&table.weak_table, (id)this);
    }
    if (isa.has_sidetable_rc) {
        //Clear reference count
        table.refcnts.erase(this);
    }
    table.unlock();
}

Therefore, to sum up, the flow chart of dealloc bottom layer is shown in the figure

Therefore, so far, from the initial alloc underlying analysis (see iOS underlying principle 02: alloc & init & new source code analysis) - >   retain  ->  release  ->  dealloc is all connected in series

retainCount source code analysis

The analysis of citation counting is illustrated by an interview question

Interview question: what is the reference count of the object created by alloc?

  • Define the following code and print its reference count

NSObject *objc = [NSObject alloc];
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)objc));

The printing results are as follows

  • Enter retaincount - >_ objc_ Rootretaincount - > rootretaincount source code, which is implemented as follows

- (NSUInteger)retainCount {
    return _objc_rootRetainCount(self);
}
👇
uintptr_t
_objc_rootRetainCount(id obj)
{
    ASSERT(obj);

    return obj->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 it is nonpointer isa, there is the lower level processing of reference counting
    if (bits.nonpointer) {
        //The object reference count created by alloc is 0, including sideTable, so for alloc, it is 0 + 1 = 1, which is why the reference count obtained through retain count is 1
        uintptr_t rc = 1 + bits.extra_rc;
        if (bits.has_sidetable_rc) {
            rc += sidetable_getExtraRC_nolock();
        }
        sidetable_unlock();
        return rc;
    }
    //If not, return to normal
    sidetable_unlock();
    return sidetable_retainCount();
}

Here, we can view extra at this time through source breakpoint debugging_ RC, the result is as follows

Answer: To sum up, the actual reference count of the object created by alloc is 0, and the reference count print result is 1, because in the underlying rootRetainCount method, the reference count is + 1 by default, but there is only read operation on the reference count, and there is no write operation. In short, to prevent the object created by alloc from being released (the reference count will be released if it is 0) Therefore, in the compilation phase, the bottom layer of the program performs the + 1 operation by default. In fact, in extra_ The reference count in RC is still 0

summary

  • The object created by alloc has no retain and release

  • The reference count of alloc creation object is 0. During compilation, the program will add 1 by default, so it is 1 when reading the reference count

Posted by Chas267 on Sun, 26 Sep 2021 16:12:54 -0700