Different versions of the operating system have different limits on the memory occupied by the App. When the limit is exceeded, the App will be forcibly killed. Therefore, the requirements for memory become higher and higher, so this chapter explores the memory management scheme in iOS

The memory management technology of mobile terminal mainly includes the tag clearing algorithm of Garbage Collection (GC) and the reference counting method used by Apple.

Compared with GC tag clearing algorithm, reference counting method can recycle objects with reference count of 0 in time and reduce search times. However, reference counting can cause problems with circular references, such as when an external variable strongly references a Block and the Block also strongly references an external variable. We need to solve the problem of circular references by weak references.

In addition, prior to ARC (Automatic reference Counting), memory was always managed through MRC (Manual reference Counting), which was a way of writing large amounts of memory management code by hand. Therefore, Apple developed ARC technology to let compilers do this code management work. However, ARC still needs to be aware of circular references. When ARC’s memory management code is automatically added by the compiler, it can be less efficient than manual memory management in some cases, so MRC will be used to manage and optimize memory usage in some scenarios with high memory requirements.

Memory layout

First, let’s review the memory layout of OS/iOS

In the iOS program memory, from the bottom address, to the high address is divided into: program area, data area, heap area, stack area. The program area is mainly code segment, and the data area includes data segment and BSS segment. Let’s take a look at what each region represents

  • Stack: Automatically allocated by the compiler, managed by the system, and cleared when no longer needed. Local variables and function parameters are stored here. The memory address of the stack area is usually starting with 0x7, and the memory space is allocated from the highest address to the bottom
  • Heap: those defined bynew.alloc,block,copyThe objects created are stored here, managed by the developer, and need to tell the system when to free memory. Under ARC, the compiler automatically frees memory when appropriate, whereas under MRC, the developer needs to manually free the memory. The memory address of the heap is usually starting with 0x6, and the memory space is allocated from the bottom address to the highest address
  • _DATA area: mainly divided into BSS(static area) and data section (constant area)
    • BSS(static zone) : The BSS segment is also called the static zone. The global variables that are not initialized are stored here. Once initialized, it is reclaimed and the data is transferred to the data segment.
    • Data segment (Constant area) : Data segment, also known as constant area, is used to store constants and is not recycled until the end of the program.
  • Code snippet: An area of program code used to hold code that is compiled into binary and stored in memory while a program is running. At the end of the program, the system will automatically reclaim the data stored in the code segment.

Minor problem: static Scope of static variables

First, look at the following code. What is the printed result of age?

static int age = 10;

@interface Person : NSObject- (void)add; + (void)reduce;
@end

@implementation Person

- (void)add {
    age++;
    NSLog(% @ "Person internal: % @ - p - % d".self, &age, age);
}

+ (void)reduce {
    age--;
    NSLog(% @ "Person internal: % @ - p - % d".self, &age, age);
}
@end


@implementation Person (WY)

- (void)wy_add {
    age++;
    NSLog(@"Person (wy) inside :%@-%p--%d".self, &age, age);
}

@end

- (void)viewDidLoad {
    [super viewDidLoad];

    NSLog(@"vc:%p--%d", &age, age);
    age = 40;
    NSLog(@"vc:%p--%d", &age, age);
    [[Person new] add];
    NSLog(@"vc:%p--%d", &age, age);
    [Person reduce];
    NSLog(@"vc:%p--%d", &age, age);
    [[Person new] wy_add];
}
Copy the code

By printing the results, we can draw the following conclusion:

The scope of static variables is independent of objects, classes, and categories, only files.

Memory optimization scheme

In addition to using ARC for automatic reference counting, iOS has some other memory optimization schemes, including Tagged Ponter,NONPOINTER_ISA, and SideTable3

Tagged Ponter

An overview of the

In September 2013, apple launched iPhone5s, which was equipped with A7 dual-core processor with 64-bit architecture for the first time. In order to save memory and improve execution efficiency, apple proposed the concept of Tagged Pointer.

Before using Tagged Pointer, if you declare an NSNumber *number = @10; Variable, you need an 8-byte pointer variable number, and a 16-byte NSNumber object, which points to the address of the NSNumber object. This takes 24 bytes of memory.

After using Tagged Pointer, the Data stored in the NSNumber Pointer becomes Tag + Data, which means the Data is stored directly in the Pointer. The data 10 is stored directly in the pointer variable number, which takes only 8 bytes.

But when Pointers are insufficient to store data, dynamic memory allocation is used to store data.

The printed results of the following interview questions can be well reflected

Dispatch_queue_t queue = dispatch_get_global_queue(0, 0);for (int i = 0; i < 1000; i++) {
        dispatch_async(queue, ^{
            self.name = [NSString stringWithFormat:@"asdasdefafdfa"];
        });
    }
    NSLog(@"end");
Copy the code
Dispatch_queue_t queue = dispatch_get_global_queue(0, 0);for (int i = 0; i < 1000; i++) {
        dispatch_async(queue, ^{
            self.name = [NSString stringWithFormat:@"abc"];
        });
    }
    NSLog(@"end");
Copy the code

The printed result of the above two pieces of code is that a crash occurs in paragraph 1, but not in paragraph 2.

Logically, when multiple threads are created to operate on name, the value of name will be retained and released continuously, which will cause resource competition and crash. However, the second segment will not crash, indicating that under Tagged Pointer, the set and GET methods will not be called if the value is small. Because its value is stored directly in a pointer variable, it can be modified directly.

Through the source code, we can also intuitively see that the Tagged Pointer type object is returned directly.

To summarize the benefits of using Tagged Pointer

  • Tagged PointerIs specifically used to store small objects, such asNSNumber.NSDateAnd so on.
  • Tagged PointerThe value of a pointer is no longer an address, but a real value. So, it’s not really an object anymore, it’s just a normal variable in an object’s skin. So, its memory is not stored in the heap and is not neededmallocandfree.
  • When Pointers are insufficient to store data, dynamically allocated memory is used to store data.
  • Three times more efficient at memory reads and 106 times faster at creation.

The source code

static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
    return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr);
}

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

static inline void * _Nonnull
_objc_makeTaggedPointer(objc_tag_index_t tag, uintptr_t value)
{
    if (tag <= OBJC_TAG_Last60BitPayload) {
        uintptr_t result =
            (_OBJC_TAG_MASK | 
             ((uintptr_t)tag << _OBJC_TAG_INDEX_SHIFT) | 
             ((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT));
        return _objc_encodeTaggedPointer(result);
    } else {
        uintptr_t result =
            (_OBJC_TAG_EXT_MASK |
             ((uintptr_t)(tag - OBJC_TAG_First52BitPayload) << _OBJC_TAG_EXT_INDEX_SHIFT) |
             ((value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_LSHIFT));
        return_objc_encodeTaggedPointer(result); }}Copy the code

_objc_decodeTaggedPointer and _objc_taggedPointersEnabled encode and unencode pointer to taggedPointer. Both methods perform xOR operations on the pointer address and objC_debug_TaggedPOinter_obfuscator

We all know that xor operation of A and B to c and xor operation of A can regain the value of A, usually can be used to achieve the exchange of two values without intermediate variables. Tagged Pointer uses this principle. Through this decoding method, we can get the real pointer address of the object

The following are the Tagged Pointer flags defined by the system.

enum objc_tag_index_t : uint16_t
{
    // 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, 

    // 52-bit payloads
    OBJC_TAG_Photos_1          = 8,
    OBJC_TAG_Photos_2          = 9,
    OBJC_TAG_Photos_3          = 10,
    OBJC_TAG_Photos_4          = 11,
    OBJC_TAG_XPC_1             = 12,
    OBJC_TAG_XPC_2             = 13,
    OBJC_TAG_XPC_3             = 14,
    OBJC_TAG_XPC_4             = 15,

    OBJC_TAG_First60BitPayload = 0, 
    OBJC_TAG_Last60BitPayload  = 6, 
    OBJC_TAG_First52BitPayload = 8, 
    OBJC_TAG_Last52BitPayload  = 263, 

    OBJC_TAG_RESERVED_264      = 264
};

Copy the code

NONPOINTER_ISA

We talked about NONPOINTER_ISA in a previous blog post, which is also a memory optimization solution for Apple. Storing a memory address in 64 bits is obviously a waste, since few devices have that much memory. You can then optimize your storage plan to use some of the extra space to store other content. If the first bit of the ISA pointer is 1, the optimized ISA pointer is used. ☞iOS underlying learning – OC Object past life

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
      uintptr_t nonpointer        : 1;                                         
      uintptr_t has_assoc         : 1;                                         
      uintptr_t has_cxx_dtor      : 1;                                         
      uintptr_t shiftcls          : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ 
      uintptr_t magic             : 6;                                         
      uintptr_t weakly_referenced : 1;                                         
      uintptr_t deallocating      : 1;                                         
      uintptr_t has_sidetable_rc  : 1;                                         
      uintptr_t extra_rc          : 8
    };
#endif
};

Copy the code

SideTable

In NONPOINTER_ISA there are two member variables has_SIDETABLE_rc and EXTRA_rc. The value of has_SIDETABLE_rc changes to 1 when extra_RC’s 19-bit memory is insufficient to store the reference count, and the reference count is stored in SideTable.

SideTables can be understood as a global hash array that stores data of type SideTable. It has a length of 64, meaning 64 SideTables.

struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;
}
Copy the code

The SideTable member variables are as follows:

  • spinlock_t: spin lock, used to lock/unlock SideTable.
  • RefcountMap: Hash table used to store reference counts for OC objects (only used when ISA optimization is disabled or isa optimization overflows isa_t reference counts).
  • weak_table_t: Hash table that stores weak reference Pointers to objects. Is the core data structure of the weak function in OC.

For weak reference table weak_table_t, read ☞iOS low-level learning – Memory management weak principle exploration

// RefcountMap disguises its pointers because we 
// do not want the table to act as a root for `leaks`.
typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,RefcountMapValuePurgeable> RefcountMap;
Copy the code

This is essentially the template type objc::DenseMap. The three type parameters of the template DisguisedPtr< objC_object >, size_t, and true respectively indicate the key type of DenseMap, value type, and whether the hash node of the response needs to be automatically released when value == 0, which is true here.

MRC and ARC

MRC and ARC concepts

MRC

In the MRC era, programmers need to manage memory manually. When creating an object, they need to add the code to release the object inside the set method and get method. And add the release code to the object’s dealloc.

@property (nonatomic, strong) Person *person;

- (void)setPerson:(Person *)person {
    if(_person ! = person) { [_person release]; _person = [person retain]; } } - (Person *) person {return _person;
}
Copy the code

ARC

In the ARC environment, we don’t manage memory manually as before, the system helps us do things like release or autorelease. ARC is the result of a collaboration between the LLVM compiler and RunTime. The LLVM compiler automatically generates release, reatin, and autoRelease codes. Weak references are released by the RunTime.

Reference counting

Reference counting is a memory management technique that refers to the process of storing the number of references to a resource (object, memory, disk space, etc.) and releasing it when the number of references reaches zero. Automatic resource management can be achieved using reference counting techniques. Reference counting can also refer to garbage collection algorithms that use reference counting techniques to reclaim unused resources.

In iOS, the reference count of an object is +1 after an operation such as reatin, and -1 after an operation such as release

Reference counting rule

The rules for reference counting can be summarized as follows:

  1. Self generated objects, own. (alloc, new, copy, mutableCopy, etc.)
  2. Objects that are not generated by themselves can be held by themselves. (retain, etc.)
  3. Release when you no longer need to hold objects yourself. (Release, dealloc, etc.)
  4. Objects that are not owned by you cannot be released.

The principle of exploring

Alloc and retainCount principle

For the process of alloc, read ☞iOS low-level learnings – OC object life and history to view, so as not to do too much superfluous description.

There is one detail to note, however, that alloc itself only allocates memory space and does not increase the reference count. Extra_rc is 0 in ISA.

Uintptr_t rc = 1 + bits.extra_rc; It is preceded by a constant 1 to mark the reference count of the generated object.

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

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

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

Reatin principle

Through the following source code, we can know:

  • The TaggedPointer object does not participate in reference counting
  • Small probability if anyhasCustomRR, will go message sending
inline id objc_object::retain() { assert(! isTaggedPointer());if(fastpath(! ISA()->hasCustomRR())) {return rootRetain();
    }

    return ((id(*)(objc_object *, SEL))objc_msgSend)(this, SEL_retain);
}

Copy the code

Next we look at the rootRetain method, which does the following:

  1. Checks whether the current object is oneTaggedPointerIf so, return.
  2. Determines whether ISA is optimized for NONPOINTER_ISA, and if not, stores the reference count in the SideTable. 64-bit devices will not enter this branch.
  3. Determines whether the current device is being destructed.
  4. Place the bits of ISA inextra_rcAdd one.
  5. If theextra_rcIs already fullsidetable_addExtraRC_nolockThe SideTable method moves half the reference count to the SideTable.
ALWAYS_INLINE ID objC_object ::rootRetain(bool tryRetain, bool handleOverflow) {✅// Return if TaggedPointerif (isTaggedPointer()) return (id)this;

    bool sideTableLocked = false;
    bool transcribeToSideTable = false;

    isa_t oldisa;
    isa_t newisa;

    do {
        transcribeToSideTable =false; oldisa = LoadExclusive(&isa.bits); newisa = oldisa; ✅// If ISA is not optimized by NONPOINTER_ISAif(slowpath(! newisa.nonpointer)) { ClearExclusive(&isa.bits);if(! tryRetain && sideTableLocked) sidetable_unlock();if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
            else returnsidetable_retain(); // The reference count is stored in SideTable} // donot check newisa.fast_rr; We already called any RR overrides ✅// Check object is destructingif (slowpath(tryRetain && newisa.deallocating)) {
            ClearExclusive(&isa.bits);
            if(! tryRetain && sideTableLocked) sidetable_unlock();returnnil; } uintptr_t carry; ✅// the extra_rc in bits of ISA adds 1 newISa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++ ✅// If bits' EXTRA_rc is full, store half of it in sideTableif (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; newisa.extra_rc = RC_HALF; // the value of extra_rc is half-empty 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. ✅// Store the other half of the reference count in sideTable sidetable_addExtraRC_nolock(RC_HALF); }if(slowpath(! tryRetain && sideTableLocked)) sidetable_unlock();return (id)this;
}
Copy the code

other

The principle of release is the opposite of the principle of retain. You can explore it for yourself.

The principle of dealloc is also explained in the iOS low-level learning – Memory management weak principle exploration

conclusion

  • Memory layout:
    • Memory addresses are classified from highest to lowestThe stack area(automatic memory processing),The heap area(Developer management, ARC will automatically release),Static area(global variable, static variable)Data segment(Constant),Code segment(Area of binary code written)
    • Static The scope of a static variable is related to an object, class, or category.
  • Memory optimization scheme:
    • Tagged Pointer
      • Tagged Pointer is used to store small objects, such as NSNumber and NSDate.
      • The Tagged Pointer value is no longer an address, but a real value. So, it’s not really an object anymore, it’s just a normal variable in an object’s skin. Therefore, its memory is not stored in the heap and does not require malloc and free.
      • When Pointers are insufficient to store data, dynamically allocated memory is used to store data.
      • Three times more efficient at memory reads and 106 times faster at creation.
    • NONPOINTER_ISA
      • throughnonpointerFlag indicates whether pointer optimization is enabled
      • throughextra_rcTo store reference counts, which vary in length depending on the system architecture
      • throughhas_sidetable_rcTo judge beyondextra_rcAfter, whether there is a globalSideTableStore reference count
    • SideTable
      • SideTablesIt’s a global hash array that storesSideTableType data
      • SideTablebyspinlock_t(Spin lock, used to lock/unlock),RefcountMap(storageextra_rcOverflow or un-optimized reference counting),weak_table_t(Store weak reference table)
  • Reference counting rules:
    • Self generated objects, own. (alloc, new, copy, mutableCopy, etc.)
    • Objects that are not generated by themselves can be held by themselves. (retain, etc.)
    • Release when you no longer need to hold objects yourself. (Release, dealloc, etc.)
    • Objects that are not owned by you cannot be released.
  • Alloc itself only allocates memory space and does not increase the reference count. Extra_rc is 0 in ISA. Just callrootRetainCountIt is preceded by a constant 1 to mark the reference count of the generated object.
  • Reatin principle:
    • Checks whether the current object is oneTaggedPointerIf so, return.
    • Determine whether ISA has passedNONPOINTER_ISAOptimize, if not, store the reference count inSideTableIn the. 64-bit devices will not enter this branch.
    • Determines whether the current device is being destructed.
    • Add 1 to extra_rc in bits of ISA.
    • If theextra_rcIs already fullsidetable_addExtraRC_nolockThe SideTable method moves half the reference count to the SideTable.

reference

Memory Management (1)

IOS Memory Management 1: Tagged Pointer& Reference count