preface

Five years of code, a million allocs, but what’s alloc doing in the underlying code that we can’t see? So far I have not known 🤦♀️. So let’s take this opportunity today to see what alloc is doing at the bottom that nobody knows about.

  • Before we get started, let’s take a look at some of the interview questions we’ve encountered before, roughly as follows:
    YSHStudent *baseStu = [YSHStudent alloc];
    YSHStudent *stu1 = [baseStu init];
    YSHStudent *stu2 = [baseStu init];
    YSHStudent *stu3 = [baseStu init];
    YSHStudent *newStu = [YSHStudent alloc];
    
    NSLog(@"%@---------%p---------%p",baseStu,baseStu,&baseStu);
    NSLog(@"%@---------%p---------%p",stu1,stu1,&stu1);
    NSLog(@"%@---------%p---------%p",stu2,stu2,&stu2);
    NSLog(@"%@---------%p---------%p",stu3,stu3,&stu3);
    NSLog(@"%@---------%p---------%p",newStu,newStu,&newStu);
Copy the code

BaseStu, STU1, STU2, STU3, newStu object address, pointer address Let’s just print out the results.

The 2021-06-06 10:57:53. 766217 + 0800 alloc [27048-1427279] < YSHStudent: 0 x600001f68770 > -- -- -- -- -- -- -- -- -- 0 x600001f68770 -- -- -- -- -- -- -- -- -- 0 x7ffeeeb8a500 10:57:53 2021-06-06. 766808 + 0800 alloc (27048-1427279) <YSHStudent: 0 x600001f68770 > -- -- -- -- -- -- -- -- -- 0 x600001f68770 -- -- -- -- -- -- -- -- -- 0 x7ffeeeb8a4f8 10:57:53 2021-06-06. 767075 + 0800 alloc (27048-1427279) <YSHStudent: 0 x600001f68770 > -- -- -- -- -- -- -- -- -- 0 x600001f68770 -- -- -- -- -- -- -- -- -- 0 x7ffeeeb8a4f0 10:57:53 2021-06-06. 767250 + 0800 alloc (27048-1427279) <YSHStudent: 0x600001f68780>---------0x600001f68780---------0x7ffeeeb8a4e8Copy the code

The result can be seen directly by printing:

The object addresses of baseStu and STU1, STU2, and STU3 are identical, but the pointer addresses are different. NewStu and baseStu, STU1, STU2, STU3 are not only different pointer addresses, but also different object addresses. And found a pattern in pointer addresses, 0x7FFEEEB8A4E8 + 8 = 0x7FFEEEB8A4F0, 0x7FFEEEB8A4F8 + 8 = 0x7FFEEEB8a4F8, 0x7FFEEEB8a4F8 + 8 = 0x7FFEEEB8a500, 0x7FFEEeb8A4F8 + 8 = 0x7FFEEEB8a500, Is a sequence of consecutive pointer addresses 8 bytes apart, from which we can infer that stack memory is contiguous and Pointers occupy 8 bytes of memory space (PS: stack memory from high address to low address; Heap area from low address to high address)

Now let’s get down to business and begin to explore the underlying principle of alloc.

The preparatory work

  1. Download the source code, source address (Apple open Source library)
  2. Download down the source code is not directly compiled, at this time to save time can read a cool god blog or directly download github compiler source code address
  3. In fact, normally we do not know which open source library alloc belongs to, how to know which open source library alloc belongs to, I will introduce three methods.

There are three common ways to explore the underlying layer

First, the symbol breakpoint directly with the process

  1. Make a breakpoint where you want to debug, and then press the Control key + Step into to proceed one step down.

2. Step to the objc_alloc method and add the objc_alloc symbol breakpoint.Alloc is in libobjc.a.dylib.

Add a symbolic breakpoint directly to the method you want to explore

For example, add the alloc symbol breakpoint directly. (PS: reactivate the symbol breakpoint where it is used, alloc will be called many times in the project, so it is expected that the keyboard will be discarded by itself.)

Third, view the underlying assembly to see the process

For those of you who know about reverse, this is certainly familiar. Arguably one of the most common methods. The specific method of opening assembly debugging is shown as follows:

When you open it, when you get to the alloc breakpoint, you will see the following page and find that objc_Alloc will be called. You can add the symbol breakpoint and then step through the process.

These are the three methods of low-level exploration, and finally we can find that alloc is in libobjc.a.dylib.

Compile and run the source code project

We’ll run the compiled source code we downloaded, and then we’ll follow the alloc process.

1. Under the breakpoint, enter the alloc source code implementation

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

2. Access the source code of _objc_rootAlloc

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

3. Enter callAlloc source code implementation

static ALWAYS_INLINE id callAlloc(Class cls, bool checkNil, Bool allocWithZone=false) {#if __OBJC2__ compiler optimizations are available // slowPath most likely false // FastPath most likely true // Removing slowPath and fastPath here has no effect on the code logic, it should just tell the compiler to optimize the code to improve compilation efficiency. if (slowpath(checkNil && ! cls)) return nil; // Determine if a class has a custom +allocWithZone implementation. If not, go to the implementation inside 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

4. Enter the source code implementation of _objc_rootAllocWithZone

id _objc_rootAllocWithZone(Class cls, // allocWithZone under __OBJC2__ ignores the hide. Return _class_createInstanceFromZone(CLS, 0, nil, OBJECT_CONSTRUCT_CALL_BADALLOC); }Copy the code

5. Access _class_createInstanceFromZone source code implementation

When you get to this stage, you find that there is more code than the previous steps, so you should be getting to the core. Through source code analysis, there are three core methods in this method that do three important things:

  • cls->instanceSize(extraBytes); ——- Calculate memory space
  • (id)calloc(1, size); ——- open up memory space and return memory address pointer
  • obj->initInstanceIsa(cls, hasCxxDtor); ——- associates isa and classes
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; 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) {// associate CLS class with ISA 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

Analysis of the source code, we can also get the internal implementation flow chart of the method is as follows:

The implementation of these key methods is analyzed below

  • Analysis of instanceSize source code implementation
    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

Enter the fastInstanceSize source code implementation

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 //16 bytes aligned return align16(size + extra -) FAST_CACHE_ALLOC_DELTA16); }}Copy the code

Enter align16 source code implementation

Return (x + size_t(15)) &~ size_t(15); return (x + size_t(15)) &~ size_t(15); return (x + size_t(15)) &~ size_t(15); }Copy the code

We find that this method is a 16-byte alignment algorithm, and before we can parse the algorithm, we need to understand why 16-byte alignment is required.

  • We need to know that the CPU reads data in the unit of byte speed. If the CPU reads unaligned data frequently, it will seriously increase the CPU overhead and reduce efficiency
  • Why is 16-byte alignment not 8-byte alignment? We all know that in an object, the first attribute ISA takes up 8 bytes. If there is only 8 bytes, there is no space left, which can cause the ISA of this object and the ISA of another object to be next to each other, which can easily cause access confusion. Also, an object does not have only one property isa.

Thus, with 16-byte alignment, CPU reads are faster and access is safer. The procedure for the 16-byte alignment algorithm is shown below

X + size_t (15)) & ~ size_t (15) & is and operation ~ to invert operation & (and) rule is: all for 1 to 1, otherwise 0 ~ (against) rule is: 1, 0, 0 change 1 here we take 9 9 + 15 = 24 24 binary as follows: 0001 1000 15 binary bit: 0000 1111 15 Reverse binary bit: 1111 0000 0001 1000 1111 0000 = 0001 0000Copy the code
  • Calloc analysis: request memory, return address pointer

The instanceSize method is used to calculate the size of the memory, and the size of the memory is allocated to obj, so obj is a pointer to the memory address. As is shown in

We noticed that the printed format is a little different from that of <YSHStudent 0x00000001000ebe00>.

1. The obJ address has not been associated with the CLS passed in. 2

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

After calloc, memory has been allocated and classes have been passed in. Therefore, initInstanceIsa is basically to initialize an ISA pointer, point the ISA pointer to the allocated memory address, and associate the ISA pointer with the CLS class. We step down to the breakpoint, and after initInstanceIsa, we print obj and we find that it’s the same format that we printed before, it’s already associated.

The last

According to the analysis of the source code, we can draw the flow chart of alloc implementation at the bottom.

conclusion

  • Through the analysis of alloc source code, it can be known that the main purpose of Alloc is to open up memory and associate ISA and CLS classes.
  • There are three core steps to open up memory: calculate memory size — apply for memory space — associate ISA and CLS classes
id
objc_alloc(Class cls)
{
    return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}

Copy the code