preface

We explored half of the read_images function code flow in the ObjC source code in the previous article iOS Class Loading Process Analysis (Part 1), so this article will explore the second half of the code flow.

Learn the key

The loading process of a non-lazily loaded class

How is the data for the class loaded

Non-lazily loaded classes and lazily loaded classes

1. The second half of the _read_images function code parsing

1.1 Read Protocol

When we have a protocol in the protocol, we will read the operation of the protocol, and its code is as follows:

 // Discover protocols. Fix up protocol refs.
    for (EACH_HEADER) {
        extern objc_class OBJC_CLASS_$_Protocol;
        Class cls = (Class)&OBJC_CLASS_$_Protocol;
        ASSERT(cls);
        NXMapTable *protocol_map = protocols();
        bool isPreoptimized = hi->hasPreoptimizedProtocols();

        // Skip reading protocols if this is an image from the shared cache
        // and we support roots
        // Note, after launch we do need to walk the protocol as the protocol
        // in the shared cache is marked with isCanonical() and that may not
        // be true if some non-shared cache binary was chosen as the canonical
        // definition
        if (launchTime && isPreoptimized) {
            if (PrintProtocols) {
                _objc_inform("PROTOCOLS: Skipping reading protocols in image: %s",
                             hi->fname());
            }
            continue;
        }

        bool isBundle = hi->isBundle();

        protocol_t * const *protolist = _getObjc2ProtocolList(hi, &count);
        for (i = 0; i < count; i++) {
            readProtocol(protolist[i], cls, protocol_map, 
                         isPreoptimized, isBundle);
        }
    }

    ts.log("IMAGE TIMES: discover protocols");
Copy the code

1.2 Fix the protocol not loaded

The code looks like this:

// Fix up @protocol references
    // Preoptimized images may have the right 
    // answer already but we don't know for sure.
    for (EACH_HEADER) {
        // At launch time, we know preoptimized image refs are pointing at the
        // shared cache definition of a protocol.  We can skip the check on
        // launch, but have to visit @protocol refs for shared cache images
        // loaded later.
        if (launchTime && hi->isPreoptimized())
            continue;
        protocol_t **protolist = _getObjc2ProtocolRefs(hi, &count);
        for (i = 0; i < count; i++) {
            remapProtocolRef(&protolist[i]);
        }
    }

    ts.log("IMAGE TIMES: fix up @protocol references");
Copy the code

1.3 Classification and processing

The code looks like this:

My computer 14:23:17 // Discover categories. Only this after the initial category // Attachment has been done categories present at startup, // discovery is deferred until the first load_images call after // the call to _dyld_objc_notify_register completes. rdar://problem/53119145 if (didInitialAttachCategories) { for (EACH_HEADER) { load_categories_nolock(hi); } } ts.log("IMAGE TIMES: discover categories");Copy the code

1.4 Class loading processing

The code looks like this:

// Category discovery MUST BE Late to avoid potential races // when other threads call the new category code before // this thread finishes its fixups. // +load handled by prepare_load_methods() // Realize non-lazy classes (for +load methods and static instances) for (EACH_HEADER) { classref_t const *classlist = hi->nlclslist(&count); for (i = 0; i < count; i++) { Class cls = remapClass(classlist[i]); if (! cls) continue; addClassTableEntry(cls); if (cls->isSwiftStable()) { if (cls->swiftMetadataInitializer()) { _objc_fatal("Swift class %s with a metadata initializer " "is not allowed to be non-lazy", cls->nameForLogging()); } // fixme also disallow relocatable classes // We can't disallow all Swift classes because of // classes like Swift.__EmptyArrayStorage } realizeClassWithoutSwift(cls, nil); } } ts.log("IMAGE TIMES: realize non-lazy classes");Copy the code

1.5 Optimize the violated class

For classes that have not been handled, optimize those that have been violated, and the code looks like this:

// Realize newly-resolved future classes, in case CF manipulates them
    if (resolvedFutureClasses) {
        for (i = 0; i < resolvedFutureClassCount; i++) {
            Class cls = resolvedFutureClasses[i];
            if (cls->isSwiftStable()) {
                _objc_fatal("Swift class is not allowed to be future");
            }
            realizeClassWithoutSwift(cls, nil);
            cls->setInstancesRequireRawIsaRecursively(false/*inherited*/);
        }
        free(resolvedFutureClasses);
    }

    ts.log("IMAGE TIMES: realize future classes");

    if (DebugNonFragileIvars) {
        realizeAllClasses();
    }
Copy the code

2 kinds of load process exploration

In fact, we can find that the realizeClassWithoutSwift function we explored in the slow message forwarding process is called in the class loading process and the code to optimize the violated class. RealizeClassWithoutSwift is a function that loads the class, so we might think we’re going to call realizeClassWithoutSwift, but is that really the case? So we wrote code somewhere to break the system class and just explore how our custom Person class loads, as shown below:

When you compile and run your code to see if it sticks at the breakpoint, only to find that it doesn’t and that the console doesn’t print anything, doesn’t it seem like the end of the process for you to explore the _read_images function? Actually, if we implement the Load method in the Person class and run the program again, we end up at the breakpoint shown below:

At this point, the realizeClassWithoutSwift function is called only when we find it. The code in this function looks like this:

2.1 Obtaining data in ro

When the function reaches the first breakpoint, the console prints ro as follows:

You can see that at this point you can print out the data in the list of methods in the Person class, but at this point in the code you might be wondering why CLS calls data() to get the list of methods. Also, the data() function’s return type is actually class_rw_t *. Why is it strong enough to be class_ro_t *?

  • Question 1: Whyclscalldata()Function can get its method list data? How does the data() function load the class data?

First, look at the code in the data() function called by CLS, as follows:

class_rw_t *data() const {
        return bits.data();
}
Copy the code

The data() function of bits (class_data_bits_t) is called in the data() function. The code looks like this:

class_rw_t* data() const {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
}
Copy the code

Put a breakpoint in this function and see what is the value of bits when the Person class calls the data function? First print the memory address of the Person class, as shown in the figure below:

When the breakpoint executes the data() function in the structure type (class_data_bits_t), the bits value is printed, as shown in the figure below:

We used bits & FAST_DATA_MASK (0x00007FFFFFFff8ul) to get the address of the class_rw_t structure variable, and then we got the list of methods, and the FAST_DATA_MASK value was written dead. So the key is how do bits get their values? We suspect that this data may be stored in Mach-o, but first we need to know the value of the ALSR loaded into memory by Mach-O at this moment, so we use the image list command to check the first address of the application loaded into memory, as shown below:

It can be found that the initial address of the master project Mach-o loaded into memory is 0x100000000, and the value of ALSR is 0 minus the initial address of the virtual memory address (0x100000000-4G). The Person class in Mach-o should have a value of 0x100008428 (address in memory at runtime -alsr). Let’s see if this is true in Mach-O, as shown below:

The data for the Person class object should be stored in Mach-o at offset 8428 (0x100008428-0x100000000 (virtual memory first address)). Let’s take a look at the location where the data for the Person class is stored, as shown below:

It can be found that the value of bits stored in Mach-o is Ox00000001000080a0, and the value of bits is Ox00000001000080a0 (Ox00000001000080a0 + ALSR) when loaded into memory. Exactly the same value as the bits we print in the data() function, so is this a coincidence? If this value is correct, then the isa value for the Person class object in memory at this point must be 0x0000000100008400 (0x0000000100008400 + ALSR), so if you print this value in the project, It must represent the address of the Metaclass object of Person, and if you print isa for this metaclass, it must be the address of the metaclass object of NSObject.

At this point in time, the address of the root metaclass is 0x000000010036C0f0, which is consistent with the address of the root metaclass of NSObject derived from the isa value of the Person class in Mach-o. The class data of a custom class is stored in Mach-o, and the isa and bits values are already there before the mach-o file of the main program is loaded and linked to memory by LLDB. This means that the isa and bits field values of a class are determined at compile time. The LLDB simply reads the values of the links when they are loaded.

  • Problem two:data()The return value type of the function is actuallyclass_rw_t *, why can strong intoclass_ro_t *What about this type?

In the ObjC source code, class_ro_t and class_rw_t are actually struct types. Class_ro_t * and class_rw_t are Pointers to structs. They are both 8 bytes in size, so they can be assigned to each other. Write the following code in the program:

struct Test_1 { int age; double height; char name[10]; }; struct Test_2 { double height; int age; }; Int main(int argc, const char * argv[]) {struct Test_1 *t1 = new Test_1{20, 183.0, "hh"}; Struct Test_2 *t2 = new Test_2{175.0, 18}; struct Test_2 *t3 = (Test_2 *)t1; struct Test_1 *t4 = (Test_1 *)t2; struct Test_1 *t5 = (Test_1 *)t3; Printf ("t1: age = %d, height = %f, name = %s\n", t1->age, t1->height, t1->name); Printf ("t2: age = %d, height = %f\n", t2->age, t2->height); Printf ("t3: age = %d, height = %f\n", t3->age, t3->height); Printf ("t4: age = %d, height = %f, name = %s\n", t4->age, t4->height, t4->name); Printf ("t5: age = %d, height = %f, name = %s\n", t5->age, t5->height, t5->name); return 0; }Copy the code

Compile and run the program, the program will not crash, and the output information is as shown in the following figure:

When the Test_1 structure pointer t1 is strongly converted to the Test_2 structure pointer T2, the data will appear, but when t2 is strongly converted back to the Test_1 structure pointer T5, the data will return to normal, so we can be sure that, The data structure type is class_ro_t *. The data structure type is class_ro_t *. The data structure type is class_ro_t *. What we can be sure of is that it is not stored at runtime because the data (protocols, methods, properties) is now read by LLDB when the linked application is loaded (before it is run time). The data must have been stored at compile time. It is likely that LLVM did the storage operation. Therefore, we can look at the source code of LLVM and search for class_ro_t to find its definition, as shown in the figure below:

We can see that the data structure type of class_ro_t in LLVM is the same as the data structure type of class_ro_t in ObjC source code. Look at the read function in this structure. The code looks like this:

bool ClassDescriptorV2::class_ro_t::Read(Process *process, lldb::addr_t addr) {
  size_t ptr_size = process->GetAddressByteSize();

  size_t size = sizeof(uint32_t)   // uint32_t flags;
                + sizeof(uint32_t) // uint32_t instanceStart;
                + sizeof(uint32_t) // uint32_t instanceSize;
                + (ptr_size == 8 ? sizeof(uint32_t)
                                 : 0) // uint32_t reserved; // __LP64__ only
                + ptr_size            // const uint8_t *ivarLayout;
                + ptr_size            // const char *name;
                + ptr_size            // const method_list_t *baseMethods;
                + ptr_size            // const protocol_list_t *baseProtocols;
                + ptr_size            // const ivar_list_t *ivars;
                + ptr_size            // const uint8_t *weakIvarLayout;
                + ptr_size;           // const property_list_t *baseProperties;

  DataBufferHeap buffer(size, '\0');
  Status error;

  process->ReadMemory(addr, buffer.GetBytes(), size, error);
  if (error.Fail()) {
    return false;
  }

  DataExtractor extractor(buffer.GetBytes(), size, process->GetByteOrder(),
                          process->GetAddressByteSize());

  lldb::offset_t cursor = 0;

  m_flags = extractor.GetU32_unchecked(&cursor);
  m_instanceStart = extractor.GetU32_unchecked(&cursor);
  m_instanceSize = extractor.GetU32_unchecked(&cursor);
  if (ptr_size == 8)
    m_reserved = extractor.GetU32_unchecked(&cursor);
  else
    m_reserved = 0;
  m_ivarLayout_ptr = extractor.GetAddress_unchecked(&cursor);
  m_name_ptr = extractor.GetAddress_unchecked(&cursor);
  m_baseMethods_ptr = extractor.GetAddress_unchecked(&cursor);
  m_baseProtocols_ptr = extractor.GetAddress_unchecked(&cursor);
  m_ivars_ptr = extractor.GetAddress_unchecked(&cursor);
  m_weakIvarLayout_ptr = extractor.GetAddress_unchecked(&cursor);
  m_baseProperties_ptr = extractor.GetAddress_unchecked(&cursor);

  DataBufferHeap name_buf(1024, '\0');

  process->ReadCStringFromMemory(m_name_ptr, (char *)name_buf.GetBytes(),
                                 name_buf.GetByteSize(), error);

  if (error.Fail()) {
    return false;
  }

  m_name.assign((char *)name_buf.GetBytes());

  return true;
}
Copy the code

As you can see, in this function, process and addr are passed in to read the data from memory, and then a variable of type DataExtractor is created, extractor, which is used to assign values to each member variable in class_ro_t. Let’s look at where LLVM reads the class_ro_t structure type, and search the Read function globally to find the code shown in the figure below:

That is, we read the ro structure in the Read_class_row function.

2.2 Copy data in class RO to RW, and CLS saves RW

The Person class rW data has not yet been allocated memory, so the code in the red box below executes:

Class_rw_t * (rw, rw, rw, rw, rw, rw, rw, rw); We jump to the function and look at its code, break it, print the value of the parameter ro, and see that the branch code shown in the red box below is executed:

Then look at the code in the set_ro_or_rwe function, which looks like this:

From the above code, Objc ::PointerUnion

, and in set_ro_or_rwe, we call PointerUnion, passing in the addresses of ro and ro_or_rw_ext. Let’s take a look at what this initialization function does, as shown below:

This initializer assigns the value of ro to _value, and then we call storeAt (passing ro_or_rw_ext as argument 1), as shown in the figure below:

We have made a copy of the value of the ro pointer and assigned it to the member variable _value of the ro_or_rw_ext (class PointerUnion type) in class_rw_t. As shown in the red box below:

Next, set the rW flags and call the CLS function setData to save the rW value. This function code looks like this:

(struct class_data_bits_t); (struct class_data_bits_t); A pointer of type class_rw_t * is obtained by calling CLS data().

2.3 Recursively implement CLS superclass and Metaclass

The code is shown in the figure below:

The setSuperclass function code looks like this:

The initClassIsa function code is as follows:

2.4 Set links for InstanceSize and superclass subclasses

The code is shown in the figure below:

Part of the code in the addSubclass function is shown in the figure below:

The code in the addRootClass function is shown in the figure below:

2.5 Organization

Call methodizeClass to class CLS, first argument CLS, second argument nil whether CLS is a class or a parent, as shown below:

To facilitate our exploration of custom classes, we write validation code in the methodizeClass function, as shown in the figure below:

3. Organizational processes

First, we add methods and properties to the Person class as follows:

@interface Person : NSObject

@property (nonatomic, copy) NSString *name;

@property (nonatomic, assign) int age;

@property (nonatomic, assign) float height;

@property (nonatomic, copy) NSString *nickName;

- (void)b;

- (void)say;

- (void)c;

- (void)hi;

- (void)drink;

- (void)a;

+ (void)class_Method_1;

+ (void)class_Method_z;

+ (void)class_Method_r;

+ (void)class_Method_h;

@end

@implementation Person

+ (void)load {
    
}

- (void)b {
    
}

- (void)say {
    
}

- (void)hi {
    
}

- (void)c {
    
}

- (void)drink {
    
}

- (void)a {
    
}

+ (void)class_Method_1 {
    
}

+ (void)class_Method_z {
    
}

+ (void)class_Method_r {
    
}

+ (void)class_Method_h {
    
}

@end
Copy the code

Compile the run code, which runs into the if statement we wrote, opening all breakpoints.

3.1 Method List

The code for handling the list of methods in the methodizeClass function is shown below:

This method calls the prepareMethodLists function, and the code looks like this:

The key code in this function is actually a call to the fixupMethodList function, which looks like this:

Reading the comments and the code, we can see that this Method sorts the METHODS of CLS by the address of sel in the Method, which may sound a little familiar to you because we touched on this part of the Method slow lookup process before. The binary search algorithm was used to find a Method from a list of methods. If the sel address was in the Method_list of the class, we would return the imp Method that corresponds to the first Method in the Method_list, and then jump through the IMP code. And we know that binary search is possible only if the list of methods is sorted, so let’s print sel names and their addresses before and after the fixupMethodList call to see how it’s sorted. So write the following code before and after fixupMethodList:

Running the program, the Person class method list is printed in the following order:

First of all, let’s look at the method order before sorting, and we can see that the address of each SEL is sorted in order from smallest to largest (and in the implementation order). Ok, regardless of the order in which the class interface is exposed to calls, Then the get and set methods for each property are listed from top to bottom), so you might be wondering, does this need to be sorted? Isn’t that unnecessary? Let’s take a look at the sorted output. We can find that the order of SEL has changed, and the address of some SEL has also changed (for example, method B used to be in the first place, but now it is in the 10th place, and its address used to be 0x100003e57 but suddenly changed to 0x7fff7bd88deb). What’s going on? Student: Do these addresses change during the sorting process? Or did it change before sorting? This requires further verification, so we print a list of methods at the beginning of the fixupMethodList function and before and after the stable_sort call. The code looks like this:

Run the compile, enter the breakpoint, clear the previous output, and print the output as follows:

We can see from the print results that some SEL addresses in Method_list have changed before sorting, and the code to change sel addresses is shown in the figure below:

If you break the __sel_registerName function, you will find that the result of calling the search_builtins function is not empty, and that the result of calling the search_builtins function is not empty. Those that did not change the SEL address executed the code in red box 2, so in order to use binary search algorithm to quickly find the method later, they need to be sorted once. As for the reason why the search_builtins function was called, I am not clear at present.

3.2 Organizational classification

After the methods are sorted, it will determine if RWE is empty. If not, it will take the methods in RWE and call attachLists, attach the methods that have just been sorted, print out the value of RWE, and it will find that RWE is nil, as shown in the figure below:

In other words, the rWE branch of the function will not be executed, so under what circumstances can an RWE be retrieved that is not nil? What’s the code logic that’s executing in the attachLists function? This question will be discussed in the next blog category loading process.

Then let’s look at the following code, as shown in the figure below:

We know from printing the Previously applied code that the program does not execute the code in red box 1, but directly executes the code in red box 2, and isMeta is false, so when does the program execute the code in red box 1? This issue will also be discussed in the next blog category loading process.

4. Summary of class loading process

4.1 Lazily Loaded and non-lazily loaded Classes

I think you’ll remember that the reason we were able to call realizeClassWithoutSwift in the read_images function to load the Person class is because we implemented the class method load in the Person class, The loading method of this class is initially called a non-lazy loading class (the class data is loaded when the map_images function is called), but if we don’t implement the class method load of the Person class, The program calls realizeClassWithoutSwift to load the Person class in the slow-moving lookupImpOrForward function. This loading method is called lazy loading (loading the class data until the first message). The two loading modes are shown in the following figure:

In our application should use less as far as possible not lazy loading class, because of lazy loading is to load the data before the program is started, and each kind of load data is time-consuming operation, if the program of china-africa lazy loading class much more special, you need to in the application before starting to load all the lazy loading priority class of the data, but these classes of data are not immediately used, This takes up memory space and is also a drain on memory, so use lazy-loaded classes as much as possible and implement less load methods in classes.

4.2 Non-lazy class loading flowchart