This is the first day of my participation in Gwen Challenge

The introduction

We deal with different OC objects every day. Although we know that objects are just data stored in a region of memory, we don’t know exactly what happens when we create objects by alloc.

Let’s start with the following code

    NSObject *person = [GCPerson alloc];
    NSObject *person1 = [person init];
    NSObject *person2 = [person init];
    NSLog(@" Object address %@ %@ %@",person, person1,person2);
    NSLog(%p %p %p",person, person1,person2);
    NSLog(@" Address of pointer itself %p %p %p",&person, &person1,&person2);
Copy the code

The printed result is

Object address <GCPerson:0x600003d64060>  <GCPerson: 0x600003d64060>   <GCPerson: 0x600003d64060> The address to which the pointer points0x600003d64060  0x600003d64060   0x600003d64060The address of the pointer itself0x7ffeed1d5568  0x7ffeed1d5560   0x7ffeed1d5558
Copy the code
  1. The first line is printedPerson, person1, person2Memory addresses of three objects.

  2. The address to which the pointer points in the second linePerson, person1, person2The memory address to which the three pointer objects point;

  3. The third line is printed&person, & Person1, &person2Addresses of the three pointer objects themselves; Due to thePerson, person1, person2Objects are three objects on the stack frame opened by ViewDidLoad, so their addresses are three consecutive addresses in the stack space from high to low;

  4. The first two lines print an area of memory that is stored on the heap and holds the passageallocObject created, but whyallocAfter the object andinitThe address of the object is the same, right? Our guess is probably because the object’s memory address allocation isallocTo perform,initDoes not manipulate the object’s memory address;

As is shown in

With that in mind, we tried to find information about the alloc method call

Low-level exploration: assembly and source code with analysis to find the corresponding method

1. Direct breakpoint to find symbols;

  1. After entering breakpoint, matchcontrol + step intoViewing call Information

  1. Find the corresponding symbol information

  1. Set a symbolic breakpoint to view information

  1. Finally find the corresponding method

2. Find the corresponding symbol by viewing the assembly code to view the call;

  1. At some point in our development, we want to enter the assembly mode to see how the current code is executing at the assembly level. We can enter the assembly debugging mode by setting the following:Xcode -> Debug -> Debug Workflow -> Always Show Disassembly

  1. Enter the assembly mode to view the execution information

3. Since we are using an emulator and seeing X86 assembly, we will see different assembly instructions when we use arm architecture for assembly debugging; For example,X86The assemblycallqInstruction inarmAssembly might change toblInstruction, but does not affect the actual call results do not care too much;

3. Add alloc symbol breakpoint directly;

  1. We already know that we are looking for the alloc method, so we can directly add the symbol breakpoint to the alloc method, but it is important to note that the alloc method is called too often, in order to avoid other interference debugging, we need to make the breakpoint in theNSObject *person = [GCPerson alloc];And then it takes effect on this row;

Explore objC source code

Although the above three methods can find relevant call information for debugging, they are too complex and inefficient. Since objC is open source, why not explore the source code directly? Let’s explore it together!

The preparatory work

  1. Apple Open Source and Source Browser for objC Source download;
  2. Common computer basics: big end and small end mode
  3. Memory printing of common LLDB debugging commands

Debugging process

Through tracing, we see that alloc calls the following functions in sequence

+ (id)alloc {
    return _objc_rootAlloc(self);
}
Copy the code
id
_objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/.true/*allocWithZone*/);
}
Copy the code
  1. Core method
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
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
  1. The current method is the method of alloc to create an object, the core code has several places:

size = cls->instanceSize(extraBytes); // Calculate the member variable class_ro_t determined at compile time;

obj = (id)calloc(1, size); // Open up memory space to create the core method of the object

obj->initInstanceIsa(cls, hasCxxDtor); // The isa of the object can be determined only after the binding and thus the class type can be determined

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);// Calculate the member variable class_ro_t determined at compile time;
    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);// Open up memory space to create the core method of the object
    }
    if(slowpath(! obj)) {if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
            return _objc_callBadAllocHandler(cls);
        }
        return nil;
    }

    if(! zone && fast) { obj->initInstanceIsa(cls, hasCxxDtor);// The isa of the object can be determined after the binding
    } 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
  1. The current method calculates the core method of allocating memory addresses for alloc
inline size_t instanceSize(size_t extraBytes) const {
        if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
            return cache.fastInstanceSize(extraBytes);// Alignment rule 16-byte alignment with cache
        }

        size_t size = alignedInstanceSize() + extraBytes;// No cache alignment rule 8 byte alignment
        // CF requires all objects be at least 16 bytes.
        if (size < 16) size = 16;  
        return size;
    }
Copy the code
  1. Binding object ISA
inline void 
objc_object::initInstanceIsa(Class cls, boolhasCxxDtor) { ASSERT(! cls->instancesRequireRawIsa()); ASSERT(hasCxxDtor == cls->hasCxxDtor()); initIsa(cls,true, hasCxxDtor);
}

inline void 
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT boolhasCxxDtor) { ASSERT(! isTaggedPointer()); isa_t newisa(0);

    if(! nonpointer) { newisa.setClass(cls,this);
    } else{ ASSERT(! DisableNonpointerIsa); ASSERT(! cls->instancesRequireRawIsa());#if SUPPORT_INDEXED_ISA
        ASSERT(cls->classArrayIndex() > 0);
        newisa.bits = ISA_INDEX_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
        newisa.bits = ISA_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
#   if ISA_HAS_CXX_DTOR_BIT
        newisa.has_cxx_dtor = hasCxxDtor;
#   endif
        newisa.setClass(cls, this);
#endif
        newisa.extra_rc = 1;
    }

    // This write must be performed in a single store in some cases
    // (for example when realizing a class because other threads
    // may simultaneously try to use the class).
    // fixme use atomics here to guarantee single-store and to
    // guarantee memory order w.r.t. the class index table
    / /... but not too atomic because we don't want to hurt instantiation
    isa = newisa;
}
Copy the code

The process to summarize

So far, the whole alloc process has been analyzed, which mainly includes several core logic such as calculating size, applying for memory, and binding ISA. As the overall flow chart is as follows

What is difficult to understand in the whole process is the calculation of size.

InstanceSize inquiry

InstanceSize summary
  1. HasFastInstanceSize determines whether the size calculation is memory aligned or byte aligned;
  2. Memory alignment is between objects. The principle of memory alignment is 16-byte alignment. When calculating size, if there is a cache, memory alignment will be performed directly.
  3. Byte alignment is between internal attributes of objects. Byte alignment is 8-byte alignment. When calculating size, byte alignment is computed if there is no cache.
  4. Byte alignment and memory alignment are rounded up;
Alignment algorithm
  1. Computes an algorithm for rounding up& ~ mask (x + mask)Among themmaskIt can be 2 to any power minus 1, for byte alignmentmaskIs 8-1, memory alignedmaskFor 16-1;
  2. (x+mask) >> bits << bitsThis algorithm and the first effect is the same, the specific approach first leftTwo to the powerOne’s place, to the rightTwo to the powerBits.

other

  1. Compilation optimization. In the process of this exploration, some “strange” situations occasionally appear, which are actually the result of compilation optimization. The specific reasons are as follows: compilation optimization;

conclusion

The above is part of the preliminary investigation into the execution process of alloc, and we will supplement more contents after the follow-up investigation.