IOS Low-level exploration series

  • IOS low-level exploration – alloc & init
  • IOS underlayer exploration – Calloc and Isa
  • IOS Low-level exploration – classes
  • IOS Low-level exploration – cache_t
  • IOS Low-level exploration – Methods
  • IOS Low-level exploration – message lookup
  • IOS Low-level exploration – Message forwarding
  • IOS Low-level exploration – app loading
  • IOS low-level exploration – class loading
  • IOS Low-level exploration – classification loading
  • IOS low-level exploration – class extension and associated objects
  • IOS Low-level exploration – KVC
  • IOS Basics – KVO

IOS leak check and fill series

  • IOS leak fix – PerfromSelector
  • IOS bug fix – Threads
  • – RunLoop for iOS
  • IOS – LLVM & Clang

Alloc & init explore

As iOS developers, we deal with objects most of the time. From an object-oriented design point of view, object creation and initialization is the most basic. So today we’re going to explore the underlying implementation of alloc and init, the most commonly used methods in iOS.

1. How to conduct low-level exploration

For third-party open source frameworks, there are certain methods and routines we can master to analyze internal principles and details. For the iOS layer, especially the OC layer, we may need to use some methods that are not commonly used in development.

The main purpose of this series is to explore the bottom line, so we as iOS developers need to focus on the entire life cycle from app launch to app kill. We might as well start with the main function we are most familiar with. In general, if we break a breakpoint in the main.m file, the call stack view on the left should look something like this:

There are two caveats to getting such a call stack:

  • Need to be closedXcodeOn the left side of theDebugThe bottom of the regionshow only stack frames with debug symbols and between libraries

  • Need to add one_objc_initSymbolic endpoint of

It is not difficult to derive a simple and crude structure of the loading process from the above call stack information

We now have such a simple process structure in mind that we will go back to tease out the entire startup process when we analyze the underlying layer later.

Next, let’s begin the actual exploration process.

We’ll just open Up Xcode and create a new Single View App project, and then we’ll call the alloc method in viewController.m.

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

Instead of going into the alloc implementation the usual way we explore the source code, holding down Command + Control, we end up with a header file that only declares the alloc method, with no corresponding implementation. At this point, we fall into deep doubt, which can be solved simply by remembering the following three common ways of exploring:

1.1 Direct code breakpoints

The specific operation mode is Control + IN

in
Step into instruction

Libobjc.a.dylib is the dynamic link library we want to find.

1.2 Enabling disassembly display

To enable the Always Show Disassembly command under Debug Workflow on the Debug menu

Next we will set the breakpoint again, and step by step debugging will also come to the following image:

1.3 Symbol breakpoint

We first select Symbolic Breakpoint and then enter objc_Alloc as shown below:

So far, we have the alloc implementation in the libObjc dynamic library, and it happens that Apple has open source this part of the code, so we can download the latest version of Apple open source website 10.14.5. The latest libObc is 756.

Second, the discoverylibObjcThe source code

After we downloaded the source code of libObjc to our computer, it cannot run directly. We need to carry out certain configuration to realize the source tracking process. This piece of content is not within the scope of this article, readers can refer to iOS_OBJC4-756.2 latest source code compilation and debugging.

Once libObjc is configured, we create a new command line project and run the following code:

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

2.1 objc_alloc

And then we go directly to the symbol breakpoint objc_alloc, and then we debug step by step, starting with objc_Alloc

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

2.2 First callAlloc

And then we go to the callAlloc method, and notice that the third argument is passed false

static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
    // Check whether the passed checkNil is nulled
    if(slowpath(checkNil && ! cls))return nil;

    // If the current compilation environment is OC 2.0
#if __OBJC2__
    // The current class has no custom allocWithZone
    if(fastpath(! cls->ISA()->hasCustomAWZ())) {// No alloc/allocWithZone implementation. Go straight to the allocator.
        // Without implementing either alloc or allocWithZone, you'll get there.
        // fixme store hasCustomAWZ in the non-meta class and 
        // add it to canAllocFast's summary
        // Fix classes that have no metaclass, that is, do not inherit from NSObject
        // Determine if the current class can quickly open up memory, note that this is never called, because canAllocFast is inside
        // Returns false
        if (fastpath(cls->canAllocFast())) {
            // No ctors, raw isa, etc. Go straight to the metal.
            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 {
            // Has ctor or raw isa or something. Use the slower path.
            id obj = class_createInstance(cls, 0);
            if(slowpath(! obj))return callBadAllocHandler(cls);
            returnobj; }}#endif

    // No shortcuts available.
    if (allocWithZone) return [cls allocWithZone:nil];
    return [cls alloc];
}
Copy the code

2.3 _objc_rootAlloc

Because the third parameter we passed in objc_init, allocWithZone, is true, and our CLS is NSObject, that means we’re going to come right here to return [CLS alloc]. We’ll move on to the alloc method:

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

Then we go inside the _objc_rootAlloc method:

// Base class implementation of +alloc. cls is not nil.
// Calls [cls allocWithZone:nil].
id
_objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/.true/*allocWithZone*/);
}
Copy the code

2.4 Second callAlloc

Doesn’t it look a little bit similar? Yeah, the first thing we’re going into is objc_init is also calling callAlloc, but there are two different arguments here. The second argument checkNil is nullated and passed false. I already nulled it the first time I called callAlloc, so there’s no need to null it again. The third parameter, allocWithZone, passes true. For this method, I refer to the Apple developer documentation, which explains it as follows:

Do not override allocWithZone: to include any initialization code. Instead, class-specific versions of init… methods. This method exists for historical reasons; memory zones are no longer used by Objective-C. Do not reload the allocWithZone and populate it with any initialization code. Instead, init it… In which to initialize the class. This method exists for historical reasons; memory zones are no longer used in Objective-C.

According to apple’s developer documentation, allocWithZone is essentially the same as alloc, but in objective-C’s ancient days, programmers needed to use things like allocWithZone to optimize the memory structure of objects, whereas today, You actually write alloc and allocWithZone all the same at the bottom.

Okay, that’s a little bit further, but let’s go back to callAlloc again, and the second time we go to callAlloc, in! CLS ->ISA()->hasCustomAWZ() The judgment here is essentially between the flags of class_rw_t in the CLS (object_class) structure and the last macro RW_HAS_DEFAULT_AWZ. In our test, when we first enter the callAlloc method, the flags value is 1, then the flags value is 0 and the previous 1<<16, which is false, and then the hasCustomAWZ value is inverted. It returns true, and then if it’s negative, it skips the logic in if; The second time you enter the callAlloc method, the flags value is a large integer, and the result is not zero after 1<<16, so hasDefaultAWZ will return true, hasCustomAWZ will return false, So when we go back to callAlloc we’re going to go into the if logic.

Here’s an aside: In the structure of our OC class, we have a structure called class_rw_t and a structure called class_ro_t. Class_rw_t can extend classes at runtime, including attributes, methods, protocols, etc. Class_ro_t stores member variables, attributes, methods, etc., but these are determined at compile time and cannot be modified at run time.

 bool hasCustomAWZ() {
    return ! bits.hasDefaultAWZ();
 }   

 bool hasDefaultAWZ() {
 	return data()->flags & RW_HAS_DEFAULT_AWZ;
 }
Copy the code

And then we’re going to come to the canAllocFast judgment, and we’re going to go inside the method

if (fastpath(cls->canAllocFast()))    
Copy the code
    boolcanAllocFast() { assert(! isFuture());return bits.canAllocFast();
    }

    bool canAllocFast() {
        return false;
    }
Copy the code

It turns out that canAllocFast always returns false, which means it goes straight to the following logic

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

Let’s go inside the class_createInstance method again

id 
class_createInstance(Class cls, size_t extraBytes)
{
    return _class_createInstanceFromZone(cls, extraBytes, nil);
}

static __attribute__((always_inline)) 
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, 
                              bool cxxConstruct = true, 
                              size_t *outAllocatedSize = nil)
{
    // Nulls the CLS
    if(! cls)return nil;
	// Assert whether CLS is implemented
    assert(cls->isRealized());

    // Read class's info bits all at once for performance
    // does CLS have C++ initializer
    bool hasCxxCtor = cls->hasCxxCtor();
    // does CLS have a C++ destructor
    bool hasCxxDtor = cls->hasCxxDtor();
    // Can CLS allocate Nonpointer? If so, memory optimization is enabled
    bool fast = cls->canAllocNonpointer();
		
    // extraBytes 0, then get the CLS instance memory size
    size_t size = cls->instanceSize(extraBytes);
    // outAllocatedSize is nil by default, skipped
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    // Where zone is passed nil, fast is passed true, so it enters the logic here
    if(! zone && fast) {// Allocate memory according to size
        obj = (id)calloc(1, size);
        // If pioneer fails, return nil
        if(! obj)return nil;
        // pass CLS and C++ destructor to initInstanceIsa to instantiate isa
        obj->initInstanceIsa(cls, hasCxxDtor);
    } 
    else {
        // If the zone is not empty, in my tests, generally the alloc call does not come here, just allocWithZone
        // Or copyWithZone will come to the following logic
        if (zone) {
            // Allocate memory according to the given zone and size
            obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
        } else {
            // Allocate memory according to size
            obj = (id)calloc(1, size);
        }
        // If pioneer fails, return nil
        if(! obj)return nil;

        // Use raw pointer isa on the assumption that they might be 
        // doing something weird with the zone or RR.
        // Initialize isa
        obj->initIsa(cls);
    }

    // if C++ initializes the constructor and destructor, optimize to speed up the whole process
    if (cxxConstruct && hasCxxCtor) {
        obj = _objc_constructOrFree(obj, cls);
    }

    // Return the final result
    return obj;
}
Copy the code

At this point, we have explored the alloc process, but we still have some questions, such as how to determine the size of the object’s memory, how to initialize isa, it does not matter, we will explore next. Here, an ALLOC flow chart drawn by the author is given first. Due to the author’s limited level, readers are welcome to point out any mistakes:

2.5 Init Brief Analysis

Having analyzed the flow of alloc, let’s move on to the flow of init. Compared to alloc, the internal implementation of init is very simple, it goes to _objc_rootInit, and then it just returns obj. So this is kind of an abstract factory design pattern, and NSObject init doesn’t really do anything, but if you inherit from NSObject, then you can override initialization methods like initWithXXX to do some initialization.

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

Third, summary

Xun Zi’s exhortation in the pre-Qin Dynasty said:

Short step, no thousands of miles; Not small streams, beyond into rivers and seas.

When we explore the underlying principles of iOS, we should also hold such a learning attitude, pay attention to the accumulation of little things, start small, a little makes a mica mica. In the next article, the author will answer the two questions left by this paper:

  • How is object initialization memory allocated?
  • How is ISA initialized?