preface

OC uses the alloc method to generate objects. What is the underlying principle of alloc? Today we’re going to explore the underlying alloc process.

Output the contents of the object, the address of the object, and the address of the object pointer. The code and print results are as follows:

    XJPerson *p1 = [XJPerson alloc];
    XJPerson *p2 = [p1 init];
    XJPerson *p3 = [p1 init];
    
    XJPerson *p4 = [XJPerson alloc];
    
    NSLog(@"%@-%p-%p", p1, p1, &p1);
    NSLog(@"%@-%p-%p", p2, p2, &p2);
    NSLog(@"%@-%p-%p", p3, p3, &p3);
    
    NSLog(@"%@-%p-%p", p4, p4, &p4); * * * * * * * * * * * * * * * * * * * * * * operation result * * * * * * * * * * * * * * * * * * * * * * * *2021- 06- 06 15:13:30.196346+0800Alloc&init exploration [1950:46211] <XJPerson: 0x60000295a5a0>-0x60000295a5a0-0x7ffee3355068
    2021- 06- 06 15:13:30.196493+0800Alloc&init exploration [1950:46211] <XJPerson: 0x60000295a5a0>-0x60000295a5a0-0x7ffee3355060
    2021- 06- 06 15:13:30.196607+0800Alloc&init exploration [1950:46211] <XJPerson: 0x60000295a5a0>-0x60000295a5a0-0x7ffee3355058
    2021- 06- 06 15:13:30.196699+0800Alloc&init exploration [1950:46211] <XJPerson: 0x60000295a620>-0x60000295a620-0x7ffee3355050
    
Copy the code

According to the print analysis:

  • P1, P2, p3The contents of the objects and the addresses of the objects are the same, but the addresses of the Pointers are different.
  • p4P1, P2, p3Print object content, object address, pointer address are different.

Why is that? The reasons are as followsIt follows that:

  1. allocMethod has the ability to open up memory, andinitMethod does not have the ability to open up memory.
  2. The stack allocates memory continuously from high to low, and the heap allocates memory from low to high.

So let’s actually explore what the alloc method does when the object is initialized.

Preparations:

  1. Download the source objC-818.2.
  2. Objc4-750 source code compilation, for reference only

Three ways to explore the bottom:

You want to explore the process of alloc, but Xcode cannot see the implementation of alloc directly. Here are three ways to explore the underlying process.

1. Symbolic breakpoint

Set the break point in the alloc method and hold when the break point breakscontrol + step intoJump into assembly code to see the underlying order of method calls (new projects have more assembly code, compiled projects have less), and then add the notation breakpoints known through assembly to explore. The process is as follows:

2. Assembly

Xcode -> Debug -> Debug Workflow -> Always Show Disassembly Explore with control + step into the process or directly by adding symbolic breakpoints in assembly code.

Remark:Build Setting -> Optimization LevelYou can change the compilation optimization level. Different compilation optimization levels display different amounts of assembly code. Higher compilation optimization levels display less assembly code.

3. Symbolic breakpoints Add symbolic breakpoints to methods that need to be explored, such as hereallocMethod is added directlyallocBut be careful not to activate it until it is needed, otherwise it will be called in many places and interfere with our exploration.

The ultimate method: source debugging

The three methods above give us a way to explore the underlying level, but they are tedious and not clear enough, now that we know from assemblyobjc_allocIt is to belong tolibobjc.A.dylib, then go to the Apple open source website to downloadobjcSource code compiled into a project run to explore, so deep and cool.After you know the source code library through assembly debugging, you can go directly to the Apple open source websiteopensource.apple.com/tarballs/Download the relevant source code and run debugging.

Alloc source exploration

Explore the alloc method through the source code project and find the underlying call process as shown in the figure belowThe exploration process is as follows:

After breaking the alloc method, hold control + Command + step into to enter the alloc method.

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

Continue to _objc_rootAlloc

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

So let’s go ahead and go to callAlloc

static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__ // Check whether it is Objc2.0
    // slowPath (x):x is likely to be false, with a small probability of being true
    // FastPath (x):x is likely true
    // Remove slowPath and fastPath from the list to tell the compiler to optimize the code
    if(slowpath(checkNil && ! cls))return nil;
    // Determine if the class implements a custom +allocWithZone. If it does not, enter the if conditional
    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

Continue in the order of invocation into _objc_rootAllocWithZone

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

Continue the above operation to enter_class_createInstanceFromZoneThis is the real core code.

Through actual debugging, there are three core methods that have a great impact on OBJ:

  • cls->instanceSize(extraBytes): Calculate the required memory size. Set extraBytes to 0
  • (id)calloc(1, size): Applies to the system to open up memory and returns the address pointer
  • obj->initInstanceIsa(cls, hasCxxDtor)Through:isaTo the corresponding class

We will focus on these three methods:

instanceSize: Calculates the required memory space

Continue to enter instanceSize

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

Continue to cache.FastInstancesize

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
        returnalign16(size + extra - FAST_CACHE_ALLOC_DELTA16); }}Copy the code

Continue to align16 (16 bytes aligned)

static inline size_t align16(size_t x) {
    return (x + size_t(15)) & ~size_t(15);
}
Copy the code

Explore the concrete realization of align16 method, taking align16(12) as an example

x = 12;
(x + size_t(15)) & ~ size_t(15) // ~ indicates the inverse
12 + 15 = 27  0001 1011
15            0000 1111
~15           1111 0000
27 & ~15      0001 1011 & 1111 0000Results:0001 0000  16
Copy the code

Summary: The algorithm align16 is actually an integer multiple of 16. I think it’s rounded down, and the reason I’m doing it from a purely algorithmic point of view is that (x + 15) is several times 16, and the excess is erased. For example, (20 + 15) = 35 = 16 * 2 + 3, which is 32. This algorithm is the same as >> 4 << 4, and the result is a multiple of 16, less than 16 erased.

Why is 16-byte alignment needed?

  1. Data is stored in byte alignment,cpuWhen reading the data, it is ok to read the data in a fixed length of bytes, instead of frequently changing the length of the bytes read, this is a kind of space for time practice.
  2. More secure Since the ISA pointer is 8 bytes in an object, if not aligned, objects will be next to each other, causing access confusion. 16 bytes aligned, will reserve some space, more secure access

calloc: Applies to the system to open up memory and returns the address pointer

First of all byinstanceSizeMethod to calculate the required memory size, and then apply to the systemsizeSize of the memory space returned toobj, soobjIt’s a pointer to a memory address, so let’s verify that by printing

performcallocThe addresses printed later have been changed, indicating that the system allocated memory, but with the common object printed<XJPerson: 0x0000000100616e60>No, why not?

  • objThere is no andclsFor the association binding.
  • It also verifies thatcallocI just opened up the memory.

initInstanceIsaThrough:isaTo the corresponding class

Continue to objC_Object ::initInstanceIsa

inline void 
objc_object::initInstanceIsa(Class cls, boolhasCxxDtor) { ASSERT(! cls->instancesRequireRawIsa()); ASSERT(hasCxxDtor == cls->hasCxxDtor()); initIsa(cls,true, hasCxxDtor);
}
Copy the code

Continue to objc_object::initIsa

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

Memory is associated with the class after newisa.setClass(CLS, this) is discovered through the step over process.

Note: For a detailed exploration of isa structure and source code, see the nature of objects in OC’s underlying principles (iii) Alloc Exploration

Print obj after obj->initInstanceIsa(CLS, hasCxxDtor)

Based on the printed results, conclude that the pointer and class have been associated. So that’s the end of the alloc search.

Summary: The core function of alloc is to allocate memory and associate it with classes via ISA Pointers.