Blog link OC Memory management – reference counter

Storage policy for reference counting

  1. Some objects are used if supportedTaggedPointer, Apple returns its pointer value directly as a reference count;
  2. If the current device is64Bit environment and useObjective - 2.0 C“, then “some” object will use itisaA portion of the pointer’s space to store its reference count;
  3. Otherwise,RuntimeA hash table is used to manage the reference count.

Tagged Pointer

Tagged Pointer is used to optimize memory.

  1. Tagged PointerDesigned to store small objects such asNSNumberandNSDateAnd so on;
  2. 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;
  3. Three times more efficient at memory reads and 106 times faster at creation.

The following implementation is used to reflect Tagged Pointer on 64-bit systems:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        NSNumber *number1 = @1;
        NSNumber *number2 = @2;
        NSNumber *number3 = @3;
        NSNumber *number4 = @4;
        
        NSNumber *numberLager = @(MAXFLOAT);
        
        NSLog(@"number1 pointer is %p", number1);
        NSLog(@"number2 pointer is %p", number2);
        NSLog(@"number3 pointer is %p", number3);
        NSLog(@"number4 pointer is %p", number4);
        NSLog(@"numberLager pointer is %p", numberLager);
        
        returnUIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); }} // Print the result: 2018-09-25 15:26:05.788382+0800 NSObjectProject[68029:24580896] Number1 Pointer is 0x9C344c19D780bc93 2018-09-25 15:26:05.789257+0800 NSObjectProject[68029:24580896] Number2 Pointer is 0x9C344c19D780bCA3 2018-09-25 15:26:05.789383+0800 NSObjectProject[68029:24580896] Number3 Pointer is 0x9C344c19D780bCB3 2018-09-25 15:26:05.789489+0800 NSObjectProject[68029:24580896] Number4 Pointer is 0x9C344C19D780bCC3 2018-09-25 15:26:05.789579+0800 NSObjectProject[68029:24580896] numberLager Pointer is 0x600001E60D80Copy the code

As we know, all objects have their corresponding ISA Pointer, so the introduction of Tagged Pointer will affect the ISA Pointer.

Let’s look at the use of Tagged Pointer on an object

inline bool 
objc_object::isTaggedPointer() {
    return _objc_isTaggedPointer(this);
}
Copy the code

How to determine if it is Tagged Pointer’s object:

  1. Look at the object. When it comes to in front of theTagged PointerIt’s designed to store small objects that haveNSDate,NSNumber,NSString;
  2. Set your own. Set in environment variablesOBJC_DISABLE_TAGGED_POINTERSforYESIndicates forcibly disabledTagged Pointer.

Isa pointer

The essence of ISA — ISA_T consortium

The isa pointer is defined in the objc_object structure:

struct objc_object { isa_t isa; } // the definition of isa_t union isa_t {isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;

#if SUPPORT_PACKED_ISA

    // extra_rc must be the MSB-most field (so it matches carry/overflow flags)
    // nonpointer must be the LSB (fixme or get rid of it)
    // shiftcls must occupy the same bits that a real class pointer would
    // bits + RC_ONE is equivalent to extra_rc + 1
    // RC_HALF is the high bit of extra_rc (i.e. half of its range)

    // future expansion:
    // uintptr_t fast_rr : 1;     // no r/r overrides
    // uintptr_t lock : 2;        // lock for atomic property, @synch
    // uintptr_t extraBytes : 1;  // allocated with extra bytes

# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
    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 magic             : 6;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 19;
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
    };

# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
    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;
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
    };

# else
# error unknown architecture for packed isa
# endif

// SUPPORT_PACKED_ISA
#endif


#if SUPPORT_INDEXED_ISA

# if __ARM_ARCH_7K__ >= 2

# define ISA_INDEX_IS_NPI 1
# define ISA_INDEX_MASK 0x0001FFFC
# define ISA_INDEX_SHIFT 2
# define ISA_INDEX_BITS 15
# define ISA_INDEX_COUNT (1 << ISA_INDEX_BITS)
# define ISA_INDEX_MAGIC_MASK 0x001E0001
# define ISA_INDEX_MAGIC_VALUE 0x001C0001
    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t indexcls          : 15;
        uintptr_t magic             : 4;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 7;
# define RC_ONE (1ULL<<25)
# define RC_HALF (1ULL<<6)
    };

# else
# error unknown architecture for indexed isa
# endif

// SUPPORT_INDEXED_ISA
#endif

};
Copy the code

There are a number of environments defined here, but let’s focus on the 64-bit CPU (if __arm64__) definition:

# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
    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 magic             : 6;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 19;
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
    };
Copy the code

nonpointer

This variable takes up 1bit of memory and can have two values: 0 and 1, representing different types of ISA_t:

  1. 0saidisa_tPointer optimization is not enabled and is not usedisa_tA structure defined in. accessobjc_objecttheisaIt’s going to go straight backisa_tThe structure of theclsVariables,clsVariables point to the structure of the class to which the object belongs;
  2. 1saidisa_tPointer optimization is enabled. Direct access is unavailableobjc_objecttheisaThe member variable (isa at this point is oneTagged Pointer),isaContains information about classes and reference counts of objects.

has_assoc

This variable is associated with an associated reference to an object.

has_cxx_dtor

Indicates whether the object has a destructor. If so, the destructor logic needs to be done. If not, the object can be released more quickly.

shiftcls

With pointer optimization enabled, store the value of the class pointer in 33bits. In initIsa() there is newisa.shiftcls = (uintptr_t) CLS >> 3; This code stores the class pointer in isa.

magic

Used by the debugger to determine whether the current object is a real object or has no space to initialize

weakly_referenced

Weak variables that indicate whether an object is or has been referred to an ARC. Objects without weak references can be released faster.

deallocating

Flags whether the object is freeing memory.

extra_rc

extra_rcIt takes up 19 bits. The maximum reference count that can be stored should be(Why do I write this becauseextra_rcThe value -1 is saved and +1 is used to get the reference countSideTables.SideTablesContains aRefcountMapRetrieves the reference count based on the address of the objectsize_t.

Here’s the question: why use both extra_RC and SideTables?

Maybe because of history, the CPU used to be32A,isaThere are only reference counts that can be stored. So inarm64Under, reference counts are usually stored inisaIn the.

More details will be covered during the retain operation.

has_sidetable_rc

When the reference counter is too large, the reference count is stored in a property of a class called SideTable.

ISA_MAGIC_MASK

Obtain the magic value through the mask.

ISA_MASK

Obtain the class pointer value of ISA through the mask.

RC_ONE and RC_HALF

Correlation calculation for reference counting.

The macro in the isa_t union

SUPPORT_PACKED_ISA

Indicates whether the platform supports inserting information other than Class into isa Pointers.

  1. If it does, it willClassInformation intoisa_tStruct, and attach some other information, such as the one abovenonpointerAnd so on;
  2. If not, it will not be usedisa_tDefined withinstruct, at this momentisa_tUse onlycls(Class pointer).

On iOS and MacOSX devices, SUPPORT_PACKED_ISA is defined as 1.

SUPPORT_INDEXED_ISA

SUPPORT_INDEXED_ISA Indicates that the Class information stored in ISA_T is the address of the Class. In initIsa() there are:

#if SUPPORT_INDEXED_ISA
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
Copy the code

On iOS devices, SUPPRT_INDEXED_ISA is 0.

Isa type related macros

SUPPORT_NONPOINTER_ISA

The isa pointer used to mark whether optimization is supported or not is defined as:

#if !SUPPORT_INDEXED_ISA  &&  !SUPPORT_PACKED_ISA
# define SUPPORT_NONPOINTER_ISA 0
#else
# define SUPPORT_NONPOINTER_ISA 1
#endif
Copy the code

How to determine whether to support optimized ISA Pointers?

  1. Known to iOSSUPPORT_PACKED_ISATo 1,SUPPORT_INDEXED_ISAIs 0, as you can see from the above definition, for iOSSUPPORT_NONPOINTER_ISA1;
  2. Set in environment variablesOBJC_DISABLE_NONPOINTER_ISA.

It is important to note that the optimized ISA pointer does not necessarily store reference counts, even in 64-bit environments. After all, it may not be enough to store reference counts in the 19-bit iOS system. The other 19 bits hold the reference count minus one.

SideTable

The SideTable structure is often seen in the source code. Its definition:

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

SideTable has three member variables:

  1. slockSpin-locks to ensure atomic operation;
  2. refcntsFor reference countinghashTable;
  3. weak_tableUsed for weak referenceshashTable.

Here we will focus on the reference count hash table. Typedefs objc::DenseMap

,size_t,true> RefcountMap;

DenseMap (llvm-densemap.h) is the DenseMap class in which the SideTable is used to hold reference counts. DenseMap takes DisguisedPtr< OBJC_object > as key and size_t as value. The DisguisedPtr class is the encapsulation of the OBJC_Object * pointer and some of its operations. Its content can be understood as the object’s memory address. The value is of type __darwin_size_t, which is generally equivalent to an unsigned long in the Darwin kernel. In fact, the value saved here is equal to the reference count minus 1.

Get the reference count

RetainCount provides a reference counter, which is defined as:

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

inline uintptr_t 
objc_object::rootRetainCount() {
    if (isTaggedPointer()) return(uintptr_t)this; sidetable_lock(); Isa.bits = LoadExclusive(&isa.bits); Clrex 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();
    returnsidetable_retainCount(); Uintptr_t objc_object:: uintptr_tsidetable_retainCount() {
    SideTable& table = SideTables()[this];

    size_t refcnt_result = 1;
    
    table.lock();
    RefcountMap::iterator it = table.refcnts.find(this);
    if(it ! = table.refcnts.end()) { // this is validfor SIDE_TABLE_RC_PINNED too
        refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;
    }
    table.unlock();
    return refcnt_result;
}
Copy the code

As you can see from the above code, there are three cases of getting a reference count:

  1. Tagged PointerIf so, return to ISA itself;
  2. nonTagged PointerAnd pointer optimization is enabled. In this case, the reference count is changed fromextra_rc(+1 is used here, so -1 is needed when saving), and then check if there is anySideTableIf there is plus there isSideTableCount in;
  3. nonTagged Pointer, pointer optimization is not turned on, usesidetable_retainCount()Function returns.

Impact of manual operation on reference counting

objc_retain()

#if __OBJC2__
__attribute__((aligned(16)))
id 
objc_retain(id obj) {
    if(! obj)return obj;
    if (obj->isTaggedPointer()) return obj;
    return obj->retain();
}
#else
id objc_retain(id obj) { return [obj retain]; }
Copy the code

If it is Tagged Pointer, return the object itself; otherwise, return it via retain() of the object.

inline id 
objc_object::retain() { assert(! isTaggedPointer()); The hasCustomRR method checks whether a class (including its parent) contains a default methodif(fastpath(! ISA()->hasCustomRR())) {return rootRetain();
    }

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

This function does not want to deal with Tagged Pointer. The hasCustomRR function is then used to check whether the class (including its parent) contains a default method, and if so, to call a custom method. If not, call rootRetain().

ALWAYS_INLINE id 
objc_object::rootRetain() {
    return rootRetain(false.false); } // ALWAYS_INLINE ID objc_object::rootRetain(bool tryRetain, bool handleOverflow) {if (isTaggedPointer()) return(id)this; isa_t oldisa; isa_t newisa; Oldisa = LoadExclusive(&isa.bits); newisa = oldisa;if(newisa.nonpointer = 0) {// newisa.nonpointer = 0 indicates that all digits are address values // Release the lock, using the assembly instruction clrex ClearExclusive(& ISa.bits); // Since all bits are address values, use SideTable directly to store reference countsreturnsidetable_retain(); } // Store the result of extra_rc++ uintptr_t carry; // extra_rc++ newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);if(carry == 0) {// extra_rc++ overflow, carry to side table newisa.extra_rc = RC_HALF; newisa.has_sidetable_rc =true; sidetable_addExtraRC_nolock(RC_HALF); } // write newisa to isa StoreExclusive(& ISa.bits, oldisa.bits, newISa.bits)return (id)this;
}
Copy the code

You can see from the above:

  • Tagged PointerReturns the object itself directly;
  • newisa.nonpointer == 0Pointer optimization is not enabledSideTableTo store reference counts;
  • Turn on pointer optimization using ISAextra_rcSave the reference count and use it when it is exceededSideTableTo store additional reference counts.

objc_release()

#if __OBJC2__
__attribute__((aligned(16)))
void 
objc_release(id obj) {
    if(! obj)return;
    if (obj->isTaggedPointer()) return;
    return obj->release();
}
#else
void objc_release(id obj) { [obj release]; }
#endif//release() source code inline void objc_object::release() { assert(! isTaggedPointer());if(fastpath(! ISA()->hasCustomRR())) { rootRelease();return;
    }

    ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_release);
}
Copy the code

The logic here is the same as that of objc_retain(), so look directly at the rootRelease() function and, as above, the code below is simplified.

ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, bool handleUnderflow) {
    if (isTaggedPointer()) return false;
    
    isa_t oldisa;
    isa_t newisa;
    
retry:
    oldisa = LoadExclusive(&isa.bits);
    newisa = oldisa;
    if (newisa.nonpointer == 0) {
        ClearExclusive(&isa.bits);
        if (sideTableLocked) sidetable_unlock();
        return sidetable_release(performDealloc);
    }
    
    uintptr_t carry;
    // extra_rc--
    newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);
    if(carry == 0) {// Need to borrow from SideTable, or reference count 0 goto underflow; } // Store reference count to ISA StoreReleaseExclusive(& ISa.bits, oldisa.bits, newISa.bits)return false; Underflow: // borrow from SideTable // or call delloc with reference count 0 // omit N more code here // To sum up: change SideTable with extra_rc, // call dealloc when reference count is reduced to 0if (performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
    }
    return true;
}
Copy the code

You can see it from above:

  1. Determine whether or notTagged PointerIs returned directly;
  2. Pointer optimization is not enabledSideTableStored reference count -1;
  3. Turn on pointer optimization using ISAextra_rcSave reference count -1 whencarry==0Indicates the need toSideTableThe saved reference count is also out or the reference count is zero, so perform the last step;
  4. The last calldeallocSo this also answers the previous OC memory management — Object generation and destructiondeallocWhen is the question called, inrootRelease(bool performDealloc, bool handleUnderflow)The function is called if it determines that the reference count is zerodeallocThe function.

conclusion

  1. Where does reference counting exist?

    • Tagged PointerInstead of reference counting, Apple simply returns the object’s pointer value as a reference count;
    • Pointer optimization (nonpointer == 1Object whose reference count exists firstisatheextra_rcIn the more than524288Exist.SideTabletheRefcountMapOr rather,DenseMap;
    • An object without pointer optimization enabled exists directlySideTabletheRefcountMapOr rather,DenseMapIn the.
  2. The essence of retain/release

    • Tagged PointerDon’t participate inretain/release;
    • Find the reference count storage area, then +1/-1 and handle the carry/borrow case depending on whether pointer optimization is turned on or not;
    • Called when the reference count is reduced to 0deallocFunction.
  3. What is the isa

    // ISA() assumes this is NOT a tagged pointer object
    Class ISA();
    
    // getIsa() allows this to be a tagged pointer object
    Class getIsa();
    Copy the code
    • The first thing to know is that the ISA pointer is not necessarily a class pointer anymore, so you need to use itISA()Get a class pointer;
    • Tagged PointerObject does not haveisaPointers, some of themisa_tStructure of;
    • Isa Pointers to other objects are still class Pointers.
  4. What is the value of the object

    • If it isTagged Pointer, the value of an object is a pointer;
    • If theTagged PointerThe value of the object is the value of the memory region to which the pointer points.

Addendum: a multi-threaded security topic

The following code results

@property (nonatomic, strong) NSString *target;
//....

dispatch_queue_t queue = dispatch_queue_create("parallel", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 1000000 ; i++) {
    dispatch_async(queue, ^{
        self.target = [NSString stringWithFormat:@"ksddkjalkjd%d",i];
    });
}
Copy the code

C. Crash D. Crash

Cause of Crash: Excessive release.

Key points:

  1. In the asynchronous execution of global queue and custom parallel queue, the number of threads will be determined according to the task system.
  2. targetusestrongThe Block intercepts the modifier of the object;
  3. Even using_targetThe effect is the same because it is used by defaultstrongAn implicit modification;
  4. strongThe source code is as follows:
objc_storeStrong(id *location, id obj)
{
id prev = *location;
if (obj == prev) {
return;
}
objc_retain(obj);
*location = obj;
objc_release(prev);
}
Copy the code

Suppose that this concurrent queue creates two threads, A and B, which, since they are asynchronous, can execute simultaneously. So there is A scenario where in thread A the code executes to objc_retain(obj), but in thread B it may execute to objc_release(prev), at which point the prev has been released. So when A executes to objc_release(prev), it will overrelease, causing the program to crash.

Solutions:

  1. Add a mutex
  2. If you use a serial queue, if you use a serial queue, it’s actually internally dependentDISPATCH_OBJ_BARRIER_BITSets the blocking flag bit
  3. The use of weak
  4. Using Tagged Pointer