1. Memory layout

Memory five partition: stack area, heap area, global area, constant area, code area

1. Five memory areas

  1. The stack area (stack)

    • The characteristics of

      • A stack is a system data structure whose corresponding process or thread is unique
      • A stack is a data structure that scales to a lower address
      • A stack is a contiguous area of memory that follows a first-in, last-out (FILOThe principle of)
      • Stack address space iniOSIs in the0X7At the beginning
      • Stack areas are typically allocated at run time
    • Store content

      • The stack area is automatically allocated and released by the compiler to store local variables
      • Arguments to a function, such as hidden arguments to a function (id self.SEL _cmd)
    • The advantages and disadvantages

      • Advantages: Because the stack is automatically allocated and freed by the compiler, there is no memory fragmentation, so it is fast and efficient
      • Disadvantages: limited stack memory size, inflexible data
      • iOSThe main thread stack size is1MBAnd the other main thread is512KB.MAConly8M

    Parameter values passed to the function, local variables declared in the function body, and so on, are automatically allocated and released by the compiler, usually after the function has finished executing. (Note: static variables are not included. Static means the variable is stored in the global/static section.)

    The Threading Programming Guide provides a Guide to memory size, as shown below:

  2. The heap area (heap)

    • The characteristics of

      • A heap is a data structure that scales to a higher address
      • The heap is a discontinuous area of memory, similar to a linked list structure (easy to add and delete, not easy to query), following the first-in, first-out (FIFO)FIFOThe principle of)
      • The address space of the heap isiOSIs in the0x6At first, the allocation of space is always dynamic
      • Heap allocation is generally allocated at run time
    • Store content

      • The heap area is allocated and freed dynamically by the programmer. If the programmer does not free it, it may be reclaimed by the operating system after the program ends
      • OCThe use ofallocOr usenewOpen up space to create objects
      • CLanguage usemalloc,calloc,reallocAllocated space, neededfreeThe release of
    • The advantages and disadvantages

      • Advantages: flexible and convenient, wide range of data adaptation
      • Disadvantages: Manual management is required, which is slow and prone to memory fragmentation

    When accessing memory in the heap, it is generally necessary to read the pointer address of the stack through the object, and then access the heap through the pointer address. Since iOS now uses ARC to manage objects, there is no need to release them manually.

  3. Global region (static region)(BSS segment)

    • A BSS segment is usually an area of memory used to store uninitialized or initialized global variables in a program with an initial value of 0. BSS is short for Block Started by Symbol. The BSS segment is a static memory allocation.

    • Data segment: A data segment is usually an area of memory used to store initialized global variables in a program. A data segment is allocated statically.

    • The global area is the memory space allocated at compile time, which generally starts with 0x1 in iOS. During the program running, the data in this memory is always stored and released by the system after the program ends

      • Uninitialized global and static variables, i.e.,BSSArea (.bss)
      • Initialized global and static variables, i.e.,Data area(.data)
    • A variable decorated static becomes a static variable whose memory is allocated by the global/static section at compile time and only once.

    • Static can modify local or global variables.

  4. Constant area (data segment)

    • The constant area is the memory space allocated at compile timeiOSIn general with0x1At the beginning, released by the system at the end of the program
    • Usually an area of memory used to store initialized global and static variables in a program. Data segment belongs to static memory allocation and can be divided into read-only data segment and read/write data segment. String constants, etc., are stored in read-only data segments that are retrieved at the end of the program.
  5. Code area (code segment)

    • Code areas are allocated at compile time to store code that is compiled into binary and stored in memory while the program is running
    • The code area needs to prevent illegal modification at run time, so only read operations are allowed, not write (modify) operations — it is not writable.
  • In addition to the above memory areas, the system also reserves some memory areas.

2. Verify memory partitions

The following code distinguishes the different memory regions.

  1. The stack area

    The verification code is as follows:

    - (void) testStack {NSLog (@ "* * * * * * * * * * * * stack area * * * * * * * * * * * *"); Int a = 10; int b = 20; NSObject *object = [NSObject new]; NSLog(@"a == \t%p",&a); NSLog(@"b == \t%p",&b); NSLog(@"object == \t%p",&object); NSLog(@"%lu",sizeof(&object)); NSLog(@"%lu",sizeof(a)); }Copy the code

    In the above code, a, B, and object are local variables that are stored on the stack. Running results:

  2. The heap area

    The verification code is as follows:

    - (void) testHeap {NSLog (@ "* * * * * * * * * * * * heap area * * * * * * * * * * * *"); NSObject *object1 = [NSObject new]; NSObject *object2 = [NSObject new]; NSObject *object3 = [NSObject new]; NSLog(@"object1 = %@",object1); NSLog(@"object2 = %@",object2); NSLog(@"object3 = %@",object3); // Access -- through object -- > heap address -- > pointer to existing stack}Copy the code

    The above code creates three variables that are stored on the stack and have Pointers to objects in the heap. The operation structure is shown below:

  3. Global area, constant area

    The case code is as follows:

    int clA; int clB = 10; static int bssA; static NSString *bssStr1; static int bssB = 10; static NSString *bssStr2 = @"hello"; static NSString *name = @"name"; - (void)viewDidLoad { [super viewDidLoad]; NSLog (@ "* * * * * * * * * * * * stack area * * * * * * * * * * * *"); int sa = 10; NSLog(@"bssA == \t%p",&sa); NSLog(@"************ global ************"); NSLog(@"clA == \t%p",&clA); NSLog(@"bssA == \t%p",&bssA); NSLog(@"bssStr1 == \t%p",&bssStr1); NSLog(@"clB == \t%p",&clB); NSLog(@"bssB == \t%p",&bssB); NSLog(@"bssStr2 == \t%p",&bssStr2); NSLog(@"bssStr2 == \t%p",&name); }Copy the code

    In the above example, we print the address of the global area variable and compare it with the stack area variable. The result is shown below:

2. TiggedPointer small objects

What is a small object

We know that an object is at least 8 bytes long, but it’s a bit wasteful for some data, such as NSNumber, NSDate, and NSString. So in 64-bit environments, Tagged Pointer is introduced to store the data in a small object. Take strings as an example, see the following figure:

Str1 is of type NSTaggedPointerString and STR4 is of type __NSCFString. At the same time, through the console output address, it can be found that the address of the rest of the heap is also very different:

2. Case study

We continue to analyze the differences through case studies.

  • Case 1

  • Case 2

  • The results

    What happens when you run the two cases separately?

    • Case 1complains
    • Case 2The normal operation

    Debug open to view assembly,Case 1The error message is shown in the following figure:Analyze the run error log,Bad memory accessWhy?

  • Cause analysis,

    The set method is essentially a retain of the new value and a release of the old value. NameStr is thread-unsafe because it is modified to nonatomic. When multiple threads access at the same time, multiple releases occur, so bad memory access occurs.

  • How to solve it?

    Modify to atomic or lock.

  • Why does Case 2 work?

    inCase 1Set a breakpointnameStrThe data type is__NSCFString, as shown below:

    And in theCase 2,nameStrThe data type isTiggedPointer, as shown below:

    Normal objects are Pointers to addresses in heap memory, so case 1 causes bad memory access due to multi-threaded access, whereas TaggedPointer is stored in the constant area and does not create memory. TiggedPointer is filtered for object release, meaning that TiggedPointer does not handle reference counts. See the source code below:

3.TiggedPointer principle analysis

We already explored the TiggedPointer aspect in the loading _read_images method of the class. See below:

Through initializeTaggedPointerObfuscator method, realize TaggedPointer pointer initialization of confusion, achieve the source code below:

In other words, in the case above, we pass%pprintTaggedPointer objectThe content of the address is the result of the pointer conversion through the confounder.

By searching objC_debug_taggedpointer_obfuscator globally, we can find the pointer encoding and decoding algorithm for TaggedPointer:

Through the above algorithm, it can be found that the coding process is:

uintptr_t value = (objc_debug_taggedpointer_obfuscator ^ ptr);
Copy the code

The decoding process is as follows:

value ^ objc_debug_taggedpointer_obfuscator;
Copy the code

Found the encoding and decoding algorithm, we can decode the small object output address, get his original pointer content. See the following processing flow:

** 0xA000000000000621 ** is the result after decoding. So what does this address mean? This is what we need to explore!!

  • TaggedPointer Pointer type analysis

    In the source code associated with TaggedPointer, the following code is found:

    static inline bool 
    _objc_isTaggedPointer(const void * _Nullable ptr)
    {
        return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
    }
    
    #if OBJC_SPLIT_TAGGED_POINTERS
    # define _OBJC_TAG_MASK (1UL<<63)
    Copy the code

    Check whether an object is of type TaggedPointer by following the object pointer & with _OBJC_TAG_MASK equal to _OBJC_TAG_MASK itself. The mask is a 64-bit value with a high value of 1 and all other values of 0. That is, an object whose high address is 1 is considered a small object.

    The following cases are introduced for analysis:

    Based on the output structure of the above case, it can be determined that the high level 0xA represents NSString, 0xb represents NSNumber, and 0xe represents NSDate. Let’s restore:

    • 0xa -> 1010
    • 0xb -> 1011
    • 0xe -> 1110

    You can see that the high order is all 1, so these are all TaggedPointer, small objects. So if you remove the high 1, the remaining bits should represent tag, i.e. :

    • 0xa -> 1010 -> 010saidNSString
    • 0xb -> 1011 -> 011saidNSNumber
    • 0xe -> 1110 -> 110saidNSDate

    Is that right? Check out the source code below:

    Objc_tag_index_t = tag_index_t; objC_tag_index_t = tag;

    #if __has_feature(objc_fixed_enum)  ||  __cplusplus >= 201103L
    enum objc_tag_index_t : uint16_t
    #else
    typedef uint16_t objc_tag_index_t;
    enum
    #endif
    {
        // 60-bit payloads
        OBJC_TAG_NSAtom            = 0, 
        OBJC_TAG_1                 = 1, 
        OBJC_TAG_NSString          = 2, 
        OBJC_TAG_NSNumber          = 3, 
        OBJC_TAG_NSIndexPath       = 4, 
        OBJC_TAG_NSManagedObjectID = 5, 
        OBJC_TAG_NSDate            = 6,
    
        // 60-bit reserved
        OBJC_TAG_RESERVED_7        = 7, 
    };
    Copy the code
    • NSString = 2 -> 010
    • NSNumber = 3 -> 011
    • NSDate = 6 -> 110

    Exactly what we thought! The small object type address contains the type. So where are the values stored?

  • TaggedPointer value analysis

    Let’s also introduce a case:

    In the example above, we can see that the trailing bit of the pointer indicates the length of the small object. So where are the values stored? WWDC notes that if you want to obtain the internal value, you need to view the binary and obtain the corresponding value by bit. The analysis process is shown in the figure below:

    As can be seen from the above, the pointer to the small object contains the object type, the value of the object, and the length of the object.

  • conclusion

    Through the interpretation of the source code and case analysis, we send small objects in the release operation will be filtered, will not execute the relevant release process, it is stored in the constant area, will not be memory application and release, high efficiency a lot!

3. Reference count

We know that memory management schemes are divided into MRC and ARC, but in both cases, reference counts are handled using methods such as alloc, Dealloc, RealEase, retain, retainCount, AutorealEase, and so on. In MRC environment, we need to call these methods manually; in ARC environment, the system will automatically call them for us. So how do these methods work? We analyze step by step!

First, to review, Nonpointer ISA uses the structural bit domain and provides different bit domain setting rules for arm64 and x86 architectures. Two important fields are included: the has_SIDETABLE_RC reference count table and the EXTRA_RC object reference count.

How to analyze the relationship between the alloc and retain methods! In the previous chapters, we have analyzed the processing flow of alloc and completed the creation of ISA. Retain also operates on the reference count of an object, starting with the retain method.

1. Retain method

Retain method ()

Call rootRetain method, find rootRetain implementation source code:

Through preliminary interpretation, it is found that the red box area is the core code. The following is an in-depth analysis of the content of this part.

At the beginning of the method, we determine whether the current object is TaggedPointer type, that is, small objects. If it is a small object, it will not be processed directly. Therefore, small objects will not be processed in reference counting, nor need to open and release memory, which will be automatically completed by the system.

In the do… In the while loop, operations related to reference counting in ISA are carried out. In the while judgment statement, the StoreExclusive method is called to complete the comparison and replacement of old and new ISAs. After success, the loop is broken.

In the loop, we first determine the reference count of the hash table that the object corresponds to if it is not Nonpointer ISA. See the following code:

if (slowpath(! newisa.nonpointer)) { ClearExclusive(&isa.bits); if (tryRetain) return sidetable_tryRetain() ? (id)this : nil; else return sidetable_retain(sideTableLocked); }Copy the code

Weak hash table weak hash table weak hash table weak hash table weak hash table weak hash table

If you are releasing isDeallocating, which is when THE EXTRA_rc and has_SIDETABLE_rc of ISA are both 0, you don’t need to process the reference count.

    bool isDeallocating() {
        return extra_rc == 0 && has_sidetable_rc == 0;
    }
Copy the code

If none of the above is true, the extra_rc property in ISA is added to the reference count by 1. The extra_RC bit value varies depending on the framework where the EXTRA_RC is located in ISA, so the RC_ONE bit field value varies. See the following code:

    uintptr_t carry;
    newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  //extra_rc++
Copy the code

Carry is used to determine if the EXTRA_RC field is full. If it is, execute the following code:

if (slowpath(carry)) { // newisa.extra_rc++ overflowed if (variant ! = RRVariant::Full) { 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; newisa.extra_rc = RC_HALF; newisa.has_sidetable_rc = true; }Copy the code

In the above process, if EXTRA_RC is full, half of the capacity that extra_RC can store is placed in the hash table corresponding to the object. See the following code:

if (slowpath(transcribeToSideTable)) { // Copy the other half of the retain counts to the side table. sidetable_addExtraRC_nolock(RC_HALF); } bool objc_object::sidetable_addExtraRC_nolock(size_t delta_rc) { ASSERT(isa.nonpointer); SideTable& table = SideTables()[this]; size_t& refcntStorage = table.refcnts[this]; size_t oldRefcnt = refcntStorage; // isa-side bits should not be set here ASSERT((oldRefcnt & SIDE_TABLE_DEALLOCATING) == 0); ASSERT((oldRefcnt & SIDE_TABLE_WEAKLY_REFERENCED) == 0); if (oldRefcnt & SIDE_TABLE_RC_PINNED) return true; uintptr_t carry; size_t newRefcnt = addc(oldRefcnt, delta_rc << SIDE_TABLE_RC_SHIFT, 0, &carry); if (carry) { refcntStorage = SIDE_TABLE_RC_PINNED | (oldRefcnt & SIDE_TABLE_FLAG_MASK); return true; } else { refcntStorage = newRefcnt; return false; }}Copy the code

The extra_rc full state contains half of its full state in the hash table to avoid frequent hash manipulation. The EXTRA_RC full state also doesn’t appear very often with slowPath (carry), so the full half already has quite a bit of storage space!

2. Release method

It’s easy to understand how release works, the reverse of reference counting. Find the implementation source code for Release:

It will also determine if the current object is a small object TaggedPointer. If it is a small object, reference counting is not required. Call the release method if it’s not a small object. Continue tracing the code, and the rootRelease method is eventually called, as shown below:

The extra_rc is subtracted by 1, as shown in the following code:

    uintptr_t carry;
    newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc--

    if (slowpath(carry)) {
        // don't ClearExclusive()
        goto underflow;
    }
Copy the code

The flag bit CARRY is also set to determine whether extra_RC has been emptied. If the extra_RC reference count is 0 at this point, it goes into the underflow process. In underflow, we first determine if the object has a hash table, and if so, we remove some reference counts from the hash table to extra_RC, as shown in the following code:

// Try to remove some retain counts from the side table. auto borrow = sidetable_subExtraRC_nolock(RC_HALF); bool emptySideTable = borrow.remaining == 0; // We'll clear the side table if no refcounts remain there Whether to clear the hash table newisa.extra_rc = borrow. // redo the original decrement too newisa.has_sidetable_rc = ! emptySideTable;Copy the code

In this process, a portion of the reference count in the hash table is assigned to extra_RC, and the hash table is set to empty based on the number of references remaining. If the hash table is set to emptySideTable and empty, the sidetable_clearExtraRC_nolock method is called to remove the SideTable from SideTables:

if (emptySideTable)
                sidetable_clearExtraRC_nolock();

void
objc_object::sidetable_clearExtraRC_nolock()
{
    ASSERT(isa.nonpointer);
    SideTable& table = SideTables()[this];
    RefcountMap::iterator it = table.refcnts.find(this);
    table.refcnts.erase(it);
}
Copy the code

When extra_RC is empty and the hash table is cleared, being in isDeallocating goes to the DealLocate process, which sends a DealLOc message to complete the object’s release.

    if (performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
    }
Copy the code

3.dealloc

Dealloc eventually calls the rootDealloc method, as shown in the following code:

When an object is released, it is not difficult to understand whether it is a small object. Small objects do not need to be handled, because the system will automatically release them for us. At the same time, the object’s ISA is used to determine whether it is nonpointer ISA. If it is used to determine whether it can have weak references, associated objects, destructors, and hash tables. If none exists, the free method is called to free the object. If it does, the object_Dispose method is called. See the following code:

If there are associated objects, use the _object_remove_assocations method to release them. This section has been analyzed in the class extension of the classification and classification (category). The hash table and weak reference table are released by calling the clearDeallocating method, as described in the weak implementation and destruction process.

4.retainCount

The rootretainCount method is called to obtain the reference count.

Check whether it is a small object. Small objects do not perform reference counting. In the case of nonpointer ISA, the extra_rc value is first fetched from the ISA pointer, and the hash table is evaluated, and if so, the value of the hash table is added. If it is not Nonpointer ISA, get the reference count for the object in the SideTable.