Series of articles: Alloc, retain, isKindOfClass, and other AWZ, RR, and CORE methods do not follow their own IMP iOS low-level exploration, iOS malloc memory allocation principles .

⭐ : Core logical content

🐈 stands for branch knowledge expansion

 stands for “Apple official documentation or source code”

OC Object initialization process

This article is mainly divided into three parts: 1, OC object alloc flow chart 2, focus on the core function _class_createInstanceFromZone three main functions: Calculate object size, allocate memory and initialize ISA, 3, init after alloc and the underlying analysis of new method.

Resources:

 objc4 – the source code

Compilable objC4 source code (Cooci)

Flow chart of alloc

⭐ An NSObject subclass calls alloc as follows

First set up the debug code:

The breakpoint is set after the first [MGObject alloc] break. Open the disassembly Debug page (Xcode toolbar “Debug” -> “Debug Workflow” -> “Allways Show Disassembly”).

As you can see, the assembly instruction callq calls the function objc_alloc, which is the first step in the flowchart, and then look at the nobject.mm file

⭐ objc_alloc function source:

// Calls [cls alloc].
id
objc_alloc(Class cls)
{
    return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}
Copy the code

⭐ callAlloc  :

// Call [cls alloc] or [cls allocWithZone:nil], with appropriate // shortcutting optimizations. static ALWAYS_INLINE id callAlloc(Class cls, bool checkNil, bool allocWithZone=false) { #if __OBJC2__ if (slowpath(checkNil && ! cls)) return nil; if (fastpath(! cls->ISA()->hasCustomAWZ())) { return _objc_rootAllocWithZone(cls, nil); } #endif // No shortcuts available. if (allocWithZone) { return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil); } return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc)); }Copy the code

Objc_alloc calls callAlloc directly with CLS, true/*checkNil*/, false/*allocWithZone*/. CLS ->ISA()->hasCustomAWZ() ->hasCustomAWZ(

🐈 hasCustomAWZ() 

#if FAST_CACHE_HAS_DEFAULT_AWZ bool hasCustomAWZ() const { return ! cache.getBit(FAST_CACHE_HAS_DEFAULT_AWZ); } void setHasDefaultAWZ() { cache.setBit(FAST_CACHE_HAS_DEFAULT_AWZ); } void setHasCustomAWZ() { cache.clearBit(FAST_CACHE_HAS_DEFAULT_AWZ); } #else bool hasCustomAWZ() const { return ! (bits.data()->flags & RW_HAS_DEFAULT_AWZ); } void setHasDefaultAWZ() { bits.data()->setFlags(RW_HAS_DEFAULT_AWZ); } void setHasCustomAWZ() { bits.data()->clearFlags(RW_HAS_DEFAULT_AWZ); } #endifCopy the code

HasCustomAWZ () is the reverse of HAS_DEFAULT_AWZ. HAS_DEFAULT_AWZ does not exist in two cases: CLS is not initialized yet

If we override MGObect +(void)initialize, we can see that the code is leaving +initialize (+initialize is triggered when the class first calls objc_msgSend).

! CLS – > ISA () – > hasCustomAWZ () will return YES (HAS_DEFAULT_AWZ is YES, the not get hasCustomAWZ () value is NO overall to invert the if judging the value of the YES, good, good, round round _ (: з “<) _)

/*allocWithZone*/ will trigger objc_msgSend)(CLS, At sign selector alloc, that’s the IMP that actually calls the NSObject alloc method

⭐ + (id)alloc function  :

+ (id)alloc {
    return _objc_rootAlloc(self);
}
Copy the code

🐈 About why Apple calls alloc why does it call objc_alloc first and not the IMP of alloc itself? Alloc, retain, isKindOfClass and other AWZ, RR, CORE methods do not go their own IMP.

The purpose of this process, I think, is to ensure that CLS is initialized and not nil before actually allocating memory

⭐ _objc_rootAlloc source 

// Base class implementation of +alloc. cls is not nil.
// Calls [cls allocWithZone:nil].
id
_objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
Copy the code

CLS, false/*checkNil*/, true/*allocWithZone*/, no non-null check required, And allocWithZone is required, which means that the pre-processing is done, and it’s time to actually open up memory initialization.

The CLS is initialized and the state code is initialized! CLS ->ISA()->hasCustomAWZ() : _objc_rootAllocWithZone(CLS, nil)

⭐ _objc_rootAllocWithZone source 

NEVER_INLINE
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
    // allocWithZone under __OBJC2__ ignores the zone parameter
    return _class_createInstanceFromZone(cls, 0, nil,
                                         OBJECT_CONSTRUCT_CALL_BADALLOC);
}
Copy the code

_objc_rootAllocWithZone calls _class_createInstanceFromZone directly. _class_createInstanceFromZone is the function that actually does the work. This function calculates the size of the object, allocates memory, initializes ISA, and returns the initialized OBj object, which is discussed in the next section

Calculate object size, apply for memory, and initialize ISA

Calculate object size, allocate memory, initialize ISA, This is done using _class_createInstanceFromZone. Note that _objc_rootAllocWithZone calls _class_createInstanceFromZone Size_t passes 0,void *zone passes nil

⭐ _class_createInstanceFromZone source 

static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
                              int construct_flags = OBJECT_CONSTRUCT_NONE,
                              bool cxxConstruct = true.size_t *outAllocatedSize = nil)
{
    ASSERT(cls->isRealized());

    // Read class's info bits all at once for performance
    bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor(a);bool hasCxxDtor = cls->hasCxxDtor(a);bool fast = cls->canAllocNonpointer(a);size_t size;

    size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if (zone) {
        obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
    } else {
        obj = (id)calloc(1, size);
    }
    if (slowpath(! obj)) {if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
            return _objc_callBadAllocHandler(cls);
        }
        return nil;
    }

    if(! zone && fast) { obj->initInstanceIsa(cls, hasCxxDtor);
    } else {
        // Use raw pointer isa on the assumption that they might be
        // doing something weird with the zone or RR.
        obj->initIsa(cls);
    }

    if (fastpath(! hasCxxCtor)) {return obj;
    }

    construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
    return object_cxxConstructFromClass(obj, cls, construct_flags);
}
Copy the code

Calculate object size

Size = CLS ->instanceSize(extraBytes); extraBytes passed 0 in the previous _objc_rootAllocWithZone call

⭐ setInstanceSize() 

    inline size_t instanceSize(size_t extraBytes) const {
        if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
            return cache.fastInstanceSize(extraBytes);
        }

        size_t size = alignedInstanceSize() + extraBytes;
        // CF requires all objects be at least 16 bytes.
        if (size < 16) size = 16;  
        return size;
    }
    
    // Class's ivar size rounded up to a pointer-size boundary.
    uint32_t alignedInstanceSize() const {
        return word_align(unalignedInstanceSize());
    }
    
    // May be unaligned depending on class's ivars.
    uint32_t unalignedInstanceSize() const {
        ASSERT(isRealized());
        return data()->ro()->instanceSize;
    }
    
    static inline uint32_t word_align(uint32_t x) {
        return (x + WORD_MASK) & ~WORD_MASK;
    }
Copy the code

Fastpath Path source 

size_t fastInstanceSize(size_t extra) const { ASSERT(hasFastInstanceSize(extra)); if (__builtin_constant_p(extra) && extra == 0) { return _flags & FAST_CACHE_ALLOC_MASK16; } else { size_t size = _flags & FAST_CACHE_ALLOC_MASK; // remove the FAST_CACHE_ALLOC_DELTA16 that was added // by setFastInstanceSize return align16(size + extra - FAST_CACHE_ALLOC_DELTA16); } } void setFastInstanceSize(size_t newSize) { // Set during realization or construction only. No locking needed. uint16_t newBits = _flags & ~FAST_CACHE_ALLOC_MASK; uint16_t sizeBits; // Adding FAST_CACHE_ALLOC_DELTA16 allows for FAST_CACHE_ALLOC_MASK16 // to yield the proper 16byte aligned allocation size with a single mask sizeBits = word_align(newSize) + FAST_CACHE_ALLOC_DELTA16; sizeBits &= FAST_CACHE_ALLOC_MASK; if (newSize <= sizeBits) { newBits |= sizeBits; } _flags = newBits; } void setInstanceSize(uint32_t newSize) { ASSERT(isRealized()); ASSERT(data()->flags & RW_REALIZING); auto ro = data()->ro(); if (newSize ! = ro->instanceSize) { ASSERT(data()->flags & RW_COPIED_RO); *const_cast<uint32_t *>(&ro->instanceSize) = newSize; } cache.setFastInstanceSize(newSize); }Copy the code

🐈 inline tells the compiler that this function is recommended to be inline when compiled. It does not need to create a new function stack. This is equivalent to putting the code implementation in another function body, which is efficient for small functions. Some compilers optimise tail-recursion, and disassemble it as an iterated function instead of resetting the callq to create a new stack of functions. But jeQ and other iterative condition judgment instructions, if there is a recursion of the scene is best written in tail recursion will not open up the function call stack.

⭐ above source code analysis, fastPath first determine whether there is a cache. FastInstanceSize if there is a direct use of the value, In cache.fastInstanceSize, setInstanceSize –> setFastInstanceSize There is a second property alignment in setFastInstanceSize word_align(newSize) and another 16-byte alignment before finally calling the fastInstanceSize return.

If FastPath doesn’t do the normal calculation, call the alignedInstanceSize method, Word_align (unalignedInstanceSize()); unalignedInstanceSize(); This value takes instanceSize from ro ro()->instanceSize

Both paths use word_align for the data size, depending on WORD_MASK, which depends on the compiler definition of __LP64__ meaning long, pointer and 64-bit 8 bytes

    #ifdef __LP64__
    #   define WORD_MASK 7UL
    #else
    #   define WORD_MASK 3UL
    #endif
Copy the code

The ⭐ alignment code is rounded up by bit operations and is 8-byte aligned under compilation conditions for __LP64__

(x + WORD_MASK) & ~WORD_MASK
Copy the code

Add +7(111) = 8(1000) 23 if the fourth digit of the binary is greater than the integer multiple of 8, then carry the fourth digit by 1, then &~ 7(~0000 0111 = 1111 1000) is equal to the last three digits of the &, all the other digits are 1. And you end up with an integer multiple of 8.

Application memory

With size_t calculated in the previous step, the system will then apply memory. Since zone is passed nil, it will pass obj = (id)calloc(1, size). Calloc will be explained in more detail in a future blog post.

So now that I’ve got the memory address that OBj is pointing to, but it’s not being used yet, the first address is pointing to a piece of dirty memory, so printing OBj would be a mess

Initialize the isa

In the previous step, obj has requested memory from the operating system, but the first variable isa in the Class structure has not been initialized. The first address refers to a piece of dirty memory. After isa is initialized, obj’s first address ISA is initialized and associated with MGObject. More details about ISA and its initialization will be added in a future blog post.

    if(! zone && fast) { obj->initInstanceIsa(cls, hasCxxDtor);
    } else {
        // Use raw pointer isa on the assumption that they might be
        // doing something weird with the zone or RR.
        obj->initIsa(cls);
    }
Copy the code

If there is no c++ constructor, return obj directly. If there is c++ constructor, return obj after performing related logic. At this point, the alloc process ends.

Init new method bottom layer

⭐ -init  :

- (id)init {
    return _objc_rootInit(self);
}

id
_objc_rootInit(id obj)
{
    // In practice, it will be hard to rely on this function.
    // Many classes do not properly chain -init calls.
    return obj;
}
Copy the code

-init source code is very few, directly return the object itself, a bit like c++ virtual function, the real actual function function needs to subclass according to their own needs to implement, here do layer interface, factory design mode.

But when you actually run it, what you end up calling is objc_alloc_init which is optimized by the compiler, and I’ll explain that in the next section

⭐ -new related source code 

+ (id)new {
    return [callAlloc(self, false/*checkNil*/) init];
}

static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{... }Copy the code

-new is also very simple source code implementation, directly call C++ function callAlloc return obj,obj call OC method -init, note that if some class constructor is not -init using -new may cause problems.

When init is actually run, the compiler optimizes objc_opt_new, which will be explained in the next article

🐈 callAlloc is an optional c++ function with default values. The last parameter, allocWithZone, is false by default. It can be called without this parameter. The goal is to avoid function call ambiguity.

Write in the last

The code environment of this article is based on Apple objC4-818.2 source code and Xcode12. The content is structured and refined as far as possible in order to save readers’ reading time and cost. If there is any wrong writing or supplement, please timely communicate with us ~~

Finally, thank you for reading everything. Have a nice codding