preface

As an iOS developer, one of the things I’m most familiar with is how objects are created and initializedallocinit. But one recent question made me feel like they were the most familiar strangers trying to find out.

According to my guess, p1, P2, P3 variable address is different, pointing to the address is also different. But I was surprised by the results.

P1, P2, and P3 print the same object and pointer address. The only difference is the address of the pointer variable. So why would that be? So let’s guess, actually alloc is what’s going on, init doesn’t affect memory. Let’s explore and test this conjecture.

Alloc and init exploration

1.1 Locating source code

Debug –> Debug overflow –> Always show Disassembly; set breakpoints on alloc and init (i.e., lines 13 and 14); We find that both methods refer to the same library, LibobJC.

Although iOS is not opensource, apple also provides some source code for developers to learn and study. We can find and download the source code through apple’s opensource website opensource. This time we explore the objc 818 source code under macOS 11.2. You can download and configure it as follows:

Source address: Source address

Source configuration can refer to the great article: source debugging configuration

1.2 Verification of conjecture

In BPPerson, we didn’t define the alloc and init methods, so we can assume that these methods are in the superclass, and we can also point to the superclass NSObject from the assembly debugging results, so we can search for these methods in the source code and see their source implementation.

Open the source project and search globally for alloc and init. We can find the definitions of these two methods in nsobject. mm as follows:

+ (id)alloc {
    return _objc_rootAlloc(self);
}

id
_objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}

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

- (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

As you can see from the source code, the init method eventually executes the _objc_rootInit function and returns the object itself, while the alloc method eventually executes the callAlloc method, where the object is created. Both the execution results and the source code implementation can verify our conjecture.

The init method returns itself, so why implement and call it? The reason for this implementation is that it allows subclasses to override the method and do what they want in the method, while also making it easier to extend the method. So the init method has to be implemented and called.

2. Alloc process

Now that we’ve validated the alloc and init implementation, we can explore the alloc implementation, and see what we’re doing in the object creation process.

2.1 Symbolic binding of alloc

2.1.1 Assembly verification of symbolic bindings

In the above exploration, when we call [BPPerson alloc], our experience is that we get into NSObject’s + (ID)alloc method, because it’s just a simple call to a class method. But in actual debugging, when the breakpoint executes to [BPPerson Alloc], you step through the assembly (CTRL + console step button) and find that the next step goes to the following place

We find that instead of going directly to the + (id)alloc method, the code goes to objc_alloc and reads and assembles the address to jump to 0x0000000100003eb6. You can see here that dyld’s dyLD_stub_binder function is finally called, which means that there’s probably a symbol binding going on.

2.1.2 Viewing mach-o files

By looking at the lazy-symbol and symbol table of the Mach-o file, we can see that after compilation, the symbol for alloc is not generated, but only for _objc_alloc.

New additions: When calling [BPPerson alloc], which should have generated the alloc symbol in mach-o, there was only _objc_alloc, which is probably a low-level C++ function, instead of calling NSObject’s alloc method, What is the reason for this? We’ll explore more later.

2.1.3 Symbolic binding source verification

Through assembly and mach-O exploration, we can get two results:

  • 1. After calling alloc, you do not immediately enter the Alloc method of NSObject, which is verified by assembly
  • 2. Our Mach-o is generated after compilation, which means there is no alloc symbol generated during compilation, only one _objc_alloc symbol

So from these two results, we can guess if the system is going to do something special with alloc, because everything about alloc points to objc_alloc, so let’s just search the source code and see, what is objc_alloc? After a global search for objc_alloc, we can find a functional implementation of objc_alloc in nsobject. mm, which we’ll look at in the next section; In objc-run-time new.mm, we found a fixupMessageRef function that transformed the IMP of alloc. The source code is as follows:

/*********************************************************************** * fixupMessageRef * Repairs an old vtable dispatch call site. * vtable dispatch itself is not supported. **********************************************************************/ static void fixupMessageRef(message_ref_t *msg) { msg->sel = sel_registerName((const char *)msg->sel); if (msg->imp == &objc_msgSend_fixup) { if (msg->sel == @selector(alloc)) { msg->imp = (IMP)&objc_alloc; } else if (MSG ->sel == @selector(allocWithZone:)) {MSG -> IMP = (IMP)&objc_allocWithZone; } else if (msg->sel == @selector(retain)) { msg->imp = (IMP)&objc_retain; } else if (msg->sel == @selector(release)) { msg->imp = (IMP)&objc_release; } else if (msg->sel == @selector(autorelease)) { msg->imp = (IMP)&objc_autorelease; } else { msg->imp = &objc_msgSend_fixedup; } } else if (msg->imp == &objc_msgSendSuper2_fixup) { msg->imp = &objc_msgSendSuper2_fixedup; } else if (msg->imp == &objc_msgSend_stret_fixup) { msg->imp = &objc_msgSend_stret_fixedup; } else if (msg->imp == &objc_msgSendSuper2_stret_fixup) { msg->imp = &objc_msgSendSuper2_stret_fixedup; } #if defined(__i386__) || defined(__x86_64__) else if (msg->imp == &objc_msgSend_fpret_fixup) { msg->imp = &objc_msgSend_fpret_fixedup; } #endif #if defined(__x86_64__) else if (msg->imp == &objc_msgSend_fp2ret_fixup) { msg->imp = &objc_msgSend_fp2ret_fixedup; } #endif }Copy the code

In this code, we can see that when we call the alloc method, MSG ->sel == @selector(alloc), we’re actually assigning IMP to objc_alloc, so when we call alloc we’re going to call objc_alloc first.

2.1.4 VERIFYING the LLVM source code

In the fixupMessageRef function in the previous section, we found that there are many other system methods besides alloc, such as allocWithZone:, retain, and so on. So what does this function do? We continue to search for fixupMessageRef globally in the objc source code, and finally find that fixupMessageRef is only called in the _read_images function. Due to the long code of the method, only the part about fixupMessageRef is shown here:

/*********************************************************************** * _read_images * Perform initial processing of the headers in the linked * list beginning with headerList. * * Called by: map_images_nolock * * Locking: runtimeLock acquired by map_images **********************************************************************/ void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses) { header_info *hi; uint32_t hIndex; size_t count; size_t i; Class *resolvedFutureClasses = nil; size_t resolvedFutureClassCount = 0; static bool doneOnce; bool launchTime = NO; TimeLogger ts(PrintImageTimes); / * *... */ # SUPPORT_FIXUP // Fix up old objc_msgSend_fixup call sites for (EACH_HEADER) {message_ref_t *refs = _getObjc2MessageRefs(hi, &count); if (count == 0) continue; if (PrintVtables) { _objc_inform("VTABLES: repairing %zu unsupported vtable dispatch " "call sites in %s", count, hi->fname()); } for (i = 0; i < count; i++) { fixupMessageRef(refs+i); } } ts.log("IMAGE TIMES: fix up objc_msgSend_fixup"); #endif /** ... Other code */}Copy the code

The _read_images is executed after dyld loads the process, while the Mach-o is generated before dyld dynamically links, so the _objc_alloc symbol may have been generated at compile time. Open in VSCode (not XCode because it’s slow), search for objc_alloc, and find the following code:

/// Allocate the given objc object.
///   call i8* \@objc_alloc(i8* %value)
llvm::Value *CodeGenFunction::EmitObjCAlloc(llvm::Value *value,
                                            llvm::Type *resultType) {
  return emitObjCValueOperation(*this, value, resultType,
                                CGM.getObjCEntrypoints().objc_alloc,
                                "objc_alloc");
}
Copy the code

Search for EmitObjCAlloc call, will find tryGenerateSpecializedMessageSend

static Optional<llvm::Value *> tryGenerateSpecializedMessageSend(CodeGenFunction &CGF, QualType ResultType, llvm::Value *Receiver, const CallArgList& Args, Selector Sel, const ObjCMethodDecl *method, bool isClassMessage) { auto &CGM = CGF.CGM; if (! CGM.getCodeGenOpts().ObjCConvertMessagesToRuntimeCalls) return None; auto &Runtime = CGM.getLangOpts().ObjCRuntime; switch (Sel.getMethodFamily()) { case OMF_alloc: if (isClassMessage && Runtime.shouldUseRuntimeFunctionsForAlloc() && ResultType->isObjCObjectPointerType()) { // [Foo alloc] -> objc_alloc(Foo) or // [self alloc] -> objc_alloc(self) if (Sel.isUnarySelector() && Sel.getNameForSlot(0) == "alloc") return CGF.EmitObjCAlloc(Receiver, CGF.ConvertType(ResultType)); // [Foo allocWithZone:nil] -> objc_allocWithZone(Foo) or // [self allocWithZone:nil] -> objc_allocWithZone(self) if (Sel.isKeywordSelector() && Sel.getNumArgs() == 1 && Args.size() == 1 && Args.front().getType()->isPointerType() && Sel.getNameForSlot(0) == "allocWithZone") { const llvm::Value* arg = Args.front().getKnownRValue().getScalarVal(); if (isa<llvm::ConstantPointerNull>(arg)) return CGF.EmitObjCAllocWithZone(Receiver, CGF.ConvertType(ResultType)); return None; } } break; */ Return None; }Copy the code

Continue to search tryGenerateSpecializedMessageSend GeneratePossiblySpecializedMessageSend can be found, this function is obviously method about message is sent, the source code is as follows:

CodeGen::RValue CGObjCRuntime::GeneratePossiblySpecializedMessageSend(
    CodeGenFunction &CGF, ReturnValueSlot Return, QualType ResultType,
    Selector Sel, llvm::Value *Receiver, const CallArgList &Args,
    const ObjCInterfaceDecl *OID, const ObjCMethodDecl *Method,
    bool isClassMessage) {
  if (Optional<llvm::Value *> SpecializedResult =
          tryGenerateSpecializedMessageSend(CGF, ResultType, Receiver, Args,
                                            Sel, Method, isClassMessage)) {
    return RValue::get(SpecializedResult.getValue());
  }
  return GenerateMessageSend(CGF, Return, ResultType, Sel, Receiver, Args, OID,
                             Method);
}
Copy the code

Under the simple analysis GeneratePossiblySpecializedMessageSend function:

  • Call tryGenerateSpecializedMessageSend function in the condition judgment, according to the function name can be speculated that the effect of some special messages for processing, into function can see these special function refers to alloc and allocWithZone, etc.

  • The guess is that Apple has made a stake in these system methods, perhaps to monitor them, but I have no way to verify this, it’s just a guess, and I welcome corrections if not.

  • If the conditions are not met, normal messages are sent. This condition includes (1) not a systematic approach; 2. Systematic method, but the pile insertion operation has been carried out.

Here to return to normal process analysis, invoked in tryGenerateSpecializedMessageSend EmitObjCAlloc, changed the alloc call process. These two steps also mean that when you call alloc, you call objc_alloc first. So let’s explore objc_alloc a little bit more.

2.2 Alloc process analysis

2.2.1 Locating key functions

Since we’re going to call objc_alloc first, let’s look at what the objc_alloc function does. Search for objc_alloc in the source code, and you’ll find the following code

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

Comparing the source code of alloc, we can see that both functions will eventually call callAlloc. Therefore, let’s go to the key function callAlloc. The source code is as follows:

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

We put a break point on alloc, on objc_alloc, on callAlloc, By debugging the source through breakpoints we find a call order objc_alloc -> callAlloc -> alloc -> _objc_rootAlloc -> callAlloc.

  • CallAlloc is called twice in this process, first after objc_alloc is called, and then after objc_msgSend, + alloc is called
  • After calling + alloc, going to callAlloc will take you to the _objc_rootAllocWithZone function.

Next we can go to _objc_rootAllocWithZone and see the process of object creation.

Tips: SlowPath () and fastPath () are compiler optimizations that indicate a low and high probability of execution, respectively. Therefore, when the compiler is loading the instruction, it can not load the slowPath until the condition is triggered. Thus, the instruction loading time is optimized and the efficiency is improved.

2.2.2 _objc_rootAllocWithZoneFunction analysis

The _objc_rootAllocWithZone function only calls the _class_createInstanceFromZone function, which is the function that creates the object

/*********************************************************************** * class_createInstance * fixme * Locking: none * * Note: this function has been carefully written so that the fastpath * takes no branch. **********************************************************************/ 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(); bool hasCxxDtor = cls->hasCxxDtor(); bool fast = cls->canAllocNonpointer(); size_t size; size = cls->instanceSize(extraBytes); If (outAllocatedSize) *outAllocatedSize = size; // Create object 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; } // isa related 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

There are a lot of criteria in this function, but the main process can be analyzed in terms of getting the object size, creating the object, initializing ISA, and returning the object. Creating objects and isa initialization related methods, such as Calloc, initIsa, etc., will not be explored in this article, but will look at obtaining memory size and memory alignment.

2.2.3 Memory Alignment

Size = CLS ->instanceSize(extraBytes); This code, if you follow it, you’ll see

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;
}
Copy the code

If ignedInstancesize is cached, go to fastInstanceSize, otherwise call alignedInstanceSize, and make sure the object size is not less than 16.

When you get the size, memory alignment is done, and when you go into these two methods, you find calls for word_align and align16.

static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}
static inline size_t word_align(size_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}
static inline size_t align16(size_t x) {
    return (x + size_t(15)) & ~size_t(15);
}

#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

As you can see from the source code, memory alignment is 8 bytes aligned on 64-bit machines and 4 bytes aligned on 32-bit machines.

Q1: Why memory alignment?

  • Suppose that an object contains int, double, and pointer member variables, respectively. When reading memory, the CPU reads the variables according to the size of their respective types, and then recalculates how much space to read each variable, which will inevitably affect the efficiency
  • If a fixed length of space is read each time, no further calculation is required. That is, space for time, can improve efficiency.
  • Although 8-byte alignment wastes a certain amount of space, the system also optimizes such that if an int and a char add up to less than 8 bytes, they can be stored in the same 8-byte space if appropriate.

Q2: Why 8-byte alignment?

  • In a basic type, the maximum is 8 bytes. If it is a structure, it can store 8 bytes. When that is not enough, another 8 bytes can be created.

Third, summary

1. Summary of alloc flowchart

This paper mainly summarizes the general flow of alloc and memory alignment knowledge, and finally summarizes the flow chart of ALLOc as follows:

There are some things not covered in this article, and object creation and ISA initialization will be summarized in a later article. For the article is not correct place also welcome everyone to correct.

2. Article supplement and modification

In the call part of objc_alloc, the previous understanding is not correct, in this 2.1.3 to make modifications and additions, add section 2.1.4 on the LLVM source code part of the inquiry, but because of the LLVM source code understanding is not enough, part of the process is not clear, if wrong, still welcome to correct.

3. Article reference

IOS low-level -OC Object creation process

The reason why alloc goes into objc_alloc in the first study of alloc