preface

To understand different blocks of knowledge individually cannot help us to understand and memorize, nor can we connect the knowledge points in series to achieve mastery.

Choose a main line from the actual scene, to understand and learn the knowledge points encountered by the main line, in order to better connect the different blocks of the series, deepen the understanding.

From object creation, explore the nature of the object, the process of creation, encounter ISA, object -> class -> metaclass, cache_t, memory alignment, classification, taggedPoint, method caching, method lookup, message forwarding, Memory management and so on.

By doing so, we can not only master these concepts, but also understand them, and get to the root of why Apple designs the way it does.

This article starts from the object creation, combs the object creation process, explores each knowledge point encountered.

Documents:

  • Objc source code.

  • Objc4-756.2 latest source code compiler debugging.

OC object creation exploration

How objects are created, most commonly alloc init, or new.

New project preparation code:

NSObject * obj = [NSObject alloc];
Copy the code

Add a breakpoint and run the project by clicking Step into.

objc
objc_alloc

Actually run the case on objC 756.2, add breakpoints to alloc and objc_alloc respectively, and you’ll see that objc_alloc goes first.

1, objc_alloc and alloc

But looking at the source code, we see that NSObject has an alloc class method. So why doesn’t our external [NSObject alloc] call the alloc class method instead of going into objc_alloc?

This part of the author through part of the source code combined with MachO file to view the speculation is as follows:

  • Xcode 10Will go right intoalloc , Xcode 11Will go right intoobjc_allocBecause inXcode11The compiledallocThe corresponding symbol will be set toobjc_alloc .
  • whileXcode 10Not really. We can use itMachOViewLook at the projects compiled in these two environments separatelyMach-OIn the__DATASection,__la_symbol_ptrSection.

The following are the author’s test results.

In addition, some codes can be found in objC source code as follows:

static void 
fixupMessageRef(message_ref_t *msg)
{    
    msg->sel = sel_registerName((const char *)msg->sel);

    if (msg->imp == &objc_msgSend_fixup) { 
        if (msg->sel == SEL_alloc) {
            msg->imp = (IMP)&objc_alloc; // This is what happens when the symbol is bound.
        } else if (msg->sel == SEL_allocWithZone) {
            msg->imp = (IMP)&objc_allocWithZone;
        } else if (msg->sel == SEL_retain) {
            msg->imp = (IMP)&objc_retain;
        } else if (msg->sel == SEL_release) {
            msg->imp = (IMP)&objc_release;
        } else if (msg->sel == SEL_autorelease) {
            msg->imp = (IMP)&objc_autorelease;
        } else{ msg->imp = &objc_msgSend_fixedup; }}/ *... * /
}
Copy the code

(Read the Hook/fishHook principle and symbol table and the dyLD loading process from scratch for more information about symbol binding.)

◈ alloc class method source is as follows:

+ (id)alloc {
    return _objc_rootAlloc(self);
}
Copy the code
id _objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/.true/*allocWithZone*/);
}
Copy the code

The ◈ objc_alloc function is as follows:

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

We can see that both alloc and objc_alloc go into callAlloc, but the last two arguments are passed differently. So let’s move on.

◈ –> Tips:

Xcode 11 calls [NSObject alloc] to objc_alloc, and internally calls [CLS alloc] directly to alloc. I haven’t found the exact information to confirm this. Guess if there’s any fully open source code for symbol binding and fixup. If you know, welcome to exchange.

2、 callAlloc

static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
    if(slowpath(checkNil && ! cls))return nil;
#if __OBJC2__
    if(fastpath(! cls->ISA()->hasCustomAWZ())) {if (fastpath(cls->canAllocFast())) {
            bool dtor = cls->hasCxxDtor();
            id obj = (id)calloc(1, cls->bits.fastInstanceSize());
            if(slowpath(! obj))return callBadAllocHandler(cls);
            obj->initInstanceIsa(cls, dtor);
            return obj;
        }
        else {
            id obj = class_createInstance(cls, 0);
            if(slowpath(! obj))return callBadAllocHandler(cls);
            returnobj; }}#endif
    if (allocWithZone) return [cls allocWithZone:nil];
    return [cls alloc];
}
Copy the code

First we notice two macro-defined functions: fastPath and slowPath.

// x is probably not 0, and the compiler is expected to optimize it
#define fastpath(x) (__builtin_expect(bool(x), 1))
// x is likely to be 0 and the compiler is expected to optimize
#define slowpath(x) (__builtin_expect(bool(x), 0))
Copy the code

So let’s just mention that in passing.

2.1. Fastpath and SlowPath

Removing fastPath and slowPath does not affect any functionality at all. The reason fastPath and slowpath are included in the if statement is to tell the compiler:

The condition in if is fastpath or slowpath event

This allows the compiler to optimize the code.

So how do you tell the compiler, or how does the compiler target processing and optimization?

For example 🌰 :

if (x)
    return 2;
else 
    return 1;
Copy the code

Reading:

  • 1️ : Since the computer does not read one instruction at a time, but multiple instructions, return 2 will be read in when it reads an IF statement. If x is 0, return 1 is re-read, and reread instructions are relatively time-consuming.

  • 2️ discount: If x has a very high probability of being 0, the return 2 instruction will inevitably be read every time and has almost no chance to execute, resulting in unnecessary command re-stress.

  • 3️ discount: Thus, of the two macros defined by Apple, FastPath (x) still returns x, only telling the compiler that the value of x is generally not 0 so that compilation can be optimized. Similarly, slowpath(x) means that the value of x is likely to be 0 and the compiler is expected to optimize.

The explanation of this example comes from bestsswifter’s in-depth understanding OF GCD, you can have a look.

So in callAlloc, step one

if(slowpath(checkNil && ! cls))return nil;
Copy the code

It’s basically telling the compiler that CLS is most likely to have a value, and the compiler should handle it accordingly.

CLS ->ISA()->hasCustomAWZ().

2.2, hasCustomAWZ

AllocWithZone = AllocWithZone = AllocWithZone; This is determined by the hasCustomAWZ method in the objC_class structure of the class.

bool hasCustomAWZ(a) {
    return ! bits.hasDefaultAWZ();
}
Copy the code

HasDefaultAWZ is implemented as follows:

bool hasDefaultAWZ(a) {
    return data()->flags & RW_HAS_DEFAULT_AWZ;
}
void setHasDefaultAWZ(a) {
    data()->setFlags(RW_HAS_DEFAULT_AWZ);
}
void setHasCustomAWZ(a) {
    data()->clearFlags(RW_HAS_DEFAULT_AWZ);
}
Copy the code

It’s actually a tag in the RW that indicates whether the user has implemented allocWithZone.

Since classes are lazily loaded, when the first message is sent to the class, the class is not loaded. Therefore, when the class first receives alloc and enters hasCustomAWZ, there is no DefaultAWZ. So hasCustomAWZ is true and therefore goes directly to [CLS alloc];

We can test it as follows:

LBPerson *objc = [[LBPerson alloc] init];
LBPerson *objc1 = [[LBPerson alloc] init];
Copy the code

When objc enters callAlloc, it enters [CLS alloc] below, and when objC1 enters, it directly enters if (fastPath (! CLS ->ISA()->hasCustomAWZ())) {internal.

Tip:

  • 1️ retail: Familiar to usinitializeWhen the class receives the first messageobjc_msgSendThe process is invoked by the trigger.
  • 2️ discount: The above result isXcode 11Environment,Xcode 10Environmental direct entryallocThat isobjc_msgSendSo it goes straight inifSet up process.
  • 3 ️ ⃣ : aboutallocWithZoneAll we need to know for the moment is that it’s another method that the object opens up, and if you override it, when you alloc, it’s going to go into user definedallocWithZoneFlow. This is also what we deal with when we write singletonsallocWithZoneThe reason why.

The handling done to Initialize in lookUpImpOrForward.

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO;

    runtimeLock.assertUnlocked();
    / *... * /
    if(initialize && ! cls->isInitialized()) { runtimeLock.unlock(); _class_initialize (_class_getNonMetaClass(cls, inst)); runtimeLock.lock(); }/ *... * /
}
Copy the code

As for the structure of the class and the specific content of ISA, I will write two articles exclusively about it because there are many contents. First, I will put a picture for the convenience of having a general understanding.

When first entered [CLS alloc]; , let’s look at the source code implementation:

+ (id)alloc {
    return _objc_rootAlloc(self);
}
Copy the code
id _objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/.true/*allocWithZone*/);
}
Copy the code

Again in callAlloc, because [CLS alloc]; The message sending mechanism is triggered, DefaultAWZ is true, hasCustomAWZ is false, and so on to the next process.

2.3, canAllocFast

The source code is as follows:

bool canAllocFast(a) { assert(! isFuture());return bits.canAllocFast();
}
#if! __LP64__
/ * * /
#elif 1
#else
#define FAST_ALLOC              (1UL<<2)

#if FAST_ALLOC
#else
    bool canAllocFast(a) {
        return false;
    }
#endif
Copy the code

You can clearly see that the return is false. So callAlloc comes in

id obj = class_createInstance(cls, 0);
if(slowpath(! obj))return callBadAllocHandler(cls);
return obj;
Copy the code

The reason for doing this is that under 32-bit systems, there are additional processes that 64-bit systems no longer use, so macro definitions are used to handle compatibility.

2.4, class_createInstance

id class_createInstance(Class cls, size_t extraBytes)
{
    return _class_createInstanceFromZone(cls, extraBytes, nil);
}
Copy the code
static __attribute__((always_inline)) 
id _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, 
                              bool cxxConstruct = true.size_t *outAllocatedSize = nil)
{
    if(! cls)return nil;
    assert(cls->isRealized());

    bool hasCxxCtor = cls->hasCxxCtor();
    bool hasCxxDtor = cls->hasCxxDtor();
    bool fast = cls->canAllocNonpointer();

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

    id obj;
    if(! zone && fast) { obj = (id)calloc(1, size);
        if(! obj)return nil;
        obj->initInstanceIsa(cls, hasCxxDtor);
    } 
    else {
        if (zone) {
            obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
        } else {
            obj = (id)calloc(1, size);
        }
        if(! obj)return nil;
        obj->initIsa(cls);
    }

    if (cxxConstruct && hasCxxCtor) {
        obj = _objc_constructOrFree(obj, cls);
    }
    return obj;
}
Copy the code

By looking at the function name and return value, we know that we have reached the point where we are creating objects and allocating memory.

The first is hasCxxCtor and hasCxxDtor.

Since the reference:

  • -fobjc-call-cxx-cdtors
  • IOS: “. Cxx_destruct “-a hidden selector in my class

2.4.1 hasCxxCtor and hasCxxDtor

Let’s take a look at the object release process.

  • 1️ one: on the subjectdealloc, will judge whether can be released, based on five main criteria:
    NONPointer_ISA // whether isa isa non-pointer type
    weakly_reference // Whether there is a reference
    has_assoc // Whether there is an associated object
    has_cxx_dtor // is there any c++ related content
    has_sidetable_rc // Whether to use sidetable
    Copy the code
  • 2️ one: if not before5In any of these cases, the release operation can be performed,CFunction of thefree()Otherwise, the system entersobject_dispose
  • 3 ️ ⃣ :object_dispose
    • Direct callobjc_destructInstance() .
    • After the callCFunction of thefree() .
  • 4 ️ ⃣ :objc_destructInstance
    • To determinehasCxxDtor, if you havec++Related content to callobject_cxxDestruct()Destroy c++ related content.
    • To determinehasAssociatedObjects, if there is an associated objectobject_remove_associations()To destroy the associated object.
    • And then callclearDeallocating() .
    • The execution is complete.
  • 5 ️ ⃣ :clearDeallocating()Calling process
    • To perform firstsideTable_clearDeallocating() .
    • To performwaek_clear_no_lock, sets the weak reference pointer to the object tonil .
    • Next to executetable.refcnts.eraser()To erase the object’s reference count from the reference count table.
    • To this end,deallocThe execution process is complete.

Both of these were originally used in objc++ to handle construction and destruct of c++ member variables, and later.cxx_destruct was used to handle memory freed under ARC.

  • When using MRC, the developer must manually write dealloc to ensure that all references to all objects it reserves are released. This is manual and error prone.

  • When ARC was introduced, the code to perform tasks equivalent to these manual distributions had to be implemented in every object with all but simple attributes. Relying on developers to implement dealloc routines manually will not solve this problem.

  • Thus a pre-existing mechanism of Objective-C ++ is used, namely a selector called a hidden selector, which (.cxx_destruct) is automatically called by the Objective C runtime before objects are released, and they are automatically generated by the compiler.

So hasCxxCtor and hasCxxDtor, just to flag the presence of these two selectors.

As some of you may have noticed, there is.cxx_destruct when we get the list of class methods.

Testing:

void testObjc_copyMethodList(Class pClass){
    unsigned int count = 0;
    Method *methods = class_copyMethodList(pClass, &count);
    for (unsigned int i=0; i < count; i++) {
        Method const method = methods[i];
        // Get the method name
        NSString *key = NSStringFromSelector(method_getName(method));
        
        NSLog(@"Method, name: %@", key);
    }
    free(methods);
}
Copy the code

Print the following:

For this reason,.cxx_destruct is also often called a hidden selector.

Going back to class_createInstance, the next step is canAllocNonpointer, which is covered in more detail in ISA. Size_t size = CLS ->instanceSize(extraBytes);

2.4.2, instanceSize

This is where we begin to calculate the amount of memory required, which involves the oft-mentioned memory alignment.

The principle of OC objects occupying memory is also described in detail in this article.

First look at the source code:

size_t instanceSize(size_t extraBytes) {
    size_t size = alignedInstanceSize() + extraBytes;
    // CF requires a minimum of 16 bytes for all objects.
    if (size < 16) size = 16;
    return size;
}

// Class's ivar size rounded to a pointer-size boundary
uint32_t alignedInstanceSize() {
    return word_align(unalignedInstanceSize());
}
Copy the code
static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}

#ifdef __LP64__
#   define WORD_SHIFT 3UL
#   define WORD_MASK 7UL
#   define WORD_BITS 64
#else
#   define WORD_SHIFT 2UL
#   define WORD_MASK 3UL
#   define WORD_BITS 32
#endif
Copy the code

InstanceSize passes the parameter extraBytes as 0, and the attributes are aligned 8 bytes in 64 bits and 4 bytes in 32 bits.

Use (x + WORD_MASK) & ~WORD_MASK; It’s the same effect as moving the bits three places to the left and three places to the right. Data ()-> RO ->instanceSize (unalignedInstanceSize)

Minimum 16 bytes (if (size < 16) size = 16).

Then, since the zone passed in is NULL and supports Nonpointer ISA. So we go to the if satisfy statement.

id obj;
if(! zone && fast) { obj = (id)calloc(1, size);if(! obj)return nil;
    obj->initInstanceIsa(cls, hasCxxDtor);
} 
Copy the code

2.4.3, calloc

Click to find calloc source code in malloc.

void * calloc(size_t num_items, size_t size)
{
    void *retval;
    retval = malloc_zone_calloc(default_zone, num_items, size);
    if (retval == NULL) {
	errno = ENOMEM;
    }
    return retval;
}
Copy the code

Tip: if you can’t follow the source code, you can follow the following methods

And finally here:

static void *
_nano_malloc_check_clear(nanozone_t *nanozone, size_t size, boolean_t cleared_requested)
{
	MALLOC_TRACE(TRACE_nano_malloc, (uintptr_t)nanozone, size, cleared_requested, 0);

	void *ptr;
	size_t slot_key;
	size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key); // Note slot_key is set here
	mag_index_t mag_index = nano_mag_index(nanozone);

	nano_meta_admin_t pMeta = &(nanozone->meta_data[mag_index][slot_key]);

	ptr = OSAtomicDequeue(&(pMeta->slot_LIFO), offsetof(struct chained_block_s, next));
	if (ptr) {
	    / * * * / has been eliminated
	} else {
	    ptr = segregated_next_block(nanozone, pMeta, slot_bytes, mag_index);
	}

	if (cleared_requested && ptr) {
	    memset(ptr, 0, slot_bytes); // TODO: Needs a memory barrier after memset to ensure zeroes land first?
	}
	return ptr;
}
Copy the code

Segregated_size_to_fit is as follows:

static MALLOC_INLINE size_t
segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
{
    size_t k, slot_bytes;

    if (0 == size) {
        size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
    }
    k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
    slot_bytes = k << SHIFT_NANO_QUANTUM;// multiply by power of two quanta size
    *pKey = k - 1;// Zero-based!

    return slot_bytes;
}

#define SHIFT_NANO_QUANTUM		4
#define NANO_REGIME_QUANTA_SIZE	(1 << SHIFT_NANO_QUANTUM)	/ / 16
Copy the code

Slot_bytes corresponds to (size + (16-1)) >> 4 << 4, which is 16-byte alignment, so calloc() allocates object memory according to 16-byte alignment criteria.

So calloc opens up memory and returns a pointer to that memory address. Back to libobjc, _class_createInstanceFromZone next.

obj->initInstanceIsa(cls, hasCxxDtor);
Copy the code

2.4.4, initInstanceIsa

inline void 
objc_object::initInstanceIsa(Class cls, boolhasCxxDtor) { assert(! cls->instancesRequireRawIsa()); assert(hasCxxDtor == cls->hasCxxDtor()); initIsa(cls,true, hasCxxDtor);
}
Copy the code
inline void 
objc_object::initIsa(Class cls, bool nonpointer, boolhasCxxDtor) { assert(! isTaggedPointer());if(! nonpointer) { isa.cls = cls; }else{ assert(! DisableNonpointerIsa); assert(! cls->instancesRequireRawIsa());isa_t newisa(0);

#if SUPPORT_INDEXED_ISA
        /*arm64 does not go here */
#else
        newisa.bits = ISA_MAGIC_VALUE;
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.shiftcls = (uintptr_t)cls >> 3;
#endifisa = newisa; }}Copy the code

Shiftcls = (uintptr_t) CLS >> 3; More on this in a follow-up ISA article.

At this point, object creation has been explored. The release process has been covered a little bit.

3, the init

Look at the init

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

id _objc_rootInit(id obj)
{
    return obj;
}
Copy the code

As you can see, init returns the method caller by default. This is designed for engineering purposes, so that you can do some initialization or assignment when you initialize an object.

4, new

+ (id)new {
    return [callAlloc(self.false/*checkNil*/) init];
}
Copy the code

New is equivalent to alloc + init. But using new doesn’t call the various init factory methods we’ve overridden.

There is a rumor that it is for Java and other language developers to join the habit problem, listen to it, not true.

share

Finally, Sunnyxx gave 4 questions in an offline sharing session. You can check it out and talk about it, say your answer, and share an analysis article if necessary.