iOS 类加载流程分析(下)

489 阅读16分钟

前言

  在上一篇文章iOS类加载流程分析(上)中我们已经探讨了ObjC源码中read_images函数一半的代码流程,所以本篇文章将对其下半部分的代码流程进行探究与分析。

学习重点

非懒加载类的加载流程

类的数据是如何加载的

非懒加载类以及懒加载类

1. _read_images函数下半部分代码解析

1.1 读取协议

  当我们协议里面有协议的时候,会进行读取协议的操作,其代码如下所示:

 // 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");

1.2 修复没有被加载的协议

  其代码如下所示:

// 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");

1.3 分类处理

  其代码如下所示:

我的电脑  14:23:17
// Discover categories. Only do this after the initial category
    // attachment has been done. For 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");

1.4 类的加载处理

  其代码如下所示:

// 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");

1.5 优化被侵犯的类

  没有被处理的类,优化那些被侵犯的类,其代码如下所示:

// 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();
    }

2 类的加载流程探究

  其实探究到这里,我们可以发现类的加载处理以及优化被侵犯的类的代码中调用了之前我们在慢速消息转发流程中所探究过的realizeClassWithoutSwift函数,而realizeClassWithoutSwift这个函数中就对类进行了加载,那么我们此时可能会觉得恍然大悟,觉得一定会调用realizeClassWithoutSwift这个函数,但实际情况真的是这样吗?因此我们在一些地方编写代码打上断点,排除系统类的影响,只探究我们自定义的Person类是如何加载的,如下图所示:

image.png

image.png

  编译运行代码,看程序是否能卡在断点,结果却发现没有卡在断点,控制台也没有打印输出任何信息,此时此刻的你,是不是觉得到现在对于_read_images函数的探究流程已经结束了呢?其实不然,现在我们在Person类中实现load类方法,再次运行程序,就会来到如下图所示的断点中:

image.png

  此时此刻,我们发现程序才会调用realizeClassWithoutSwift函数,这个函数中代码如下图所示:

image.png

2.1 获取类ro中的数据

  当函数执行到第一个断点时,在控制台打印输出ro的信息,如下所示:

image.png

  可以看到此时是可以打印出Person类中的方法列表中的数据的,但是代码执行到这里你可能会有如下的疑问,那就是为何cls调用data()函数就可以获取其方法列表数据呢?还有就是data()函数的返回值类型实际上是class_rw_t *,为何能够强转成class_ro_t *这种类型呢?

  • 问题一: 为何cls调用data()函数就可以获取其方法列表数据呢?data()函数是如何加载到类数据的呢?

  首先来看看cls所调用的data()这个函数中的代码,如下所示:

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

  而在这个data函数中实际上是调用了bits(class_data_bits_t 类型)的data()函数,其代码如下所示:

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

  在这个函数中打上断点,看看Person这个类调用data函数时,bits的值是多少?,首先打印Person这个类的内存地址是多少,如下图所示:

image.png

  当断点执行到(class_data_bits_t)这个结构体类型中的data()函数时,打印bits的值,如下图所示:

image.png

  原来是通过bits & FAST_DATA_MASK(0x00007ffffffffff8UL)获取到class_rw_t结构体变量的地址的,然后获取到方法列表的,FAST_DATA_MASK的值是写死的,所以关键就在于bits的值是如何获取得到的呢?我们猜测这些数据可能存储在Mach-o中,但首先我们需要知道此时此刻Mach-o加载到内存中的ALSR的值,因此通过image list命令查看应用程序加载到内存的首地址,如下图所示:

image.png

  可以发现主工程Mach-o加载到内存中的首地址为0x100000000,ALSR的值就为这个首地址减去虚拟内存地址首地址(0x100000000-4G),为0,而通过计算可得,Person类在Mach-o中的值应该为0x100008428(运行时刻在内存中的地址 - ALSR),让我们看看Mach-o中是不是这样,如下图所示:

image.png

  那么Person类类对象的数据就应该是存储在Mach-o中偏移量为8428(0x100008428 - 0x100000000(虚拟内存首地址))的位置,我们来看看这个地方存储的Person类的数据,如下图所示:

image.png

  可以发现Mach-o中所存储的bits的值为Ox00000001000080a0,bits的值加载到内存就为Ox00000001000080a0(Ox00000001000080a0 + ALSR),正好与我们在data()函数中打印的bits的值一样,那么这是不是个偶然呢?如果这个值是对的,那么表示此时此刻内存中Person类类对象的isa的值一定是0x0000000100008400(0x0000000100008400 + ALSR),那么如果在工程中打印这个值,它一定表示Person的metaclass对象的地址,而且如果打印这个元类的isa,它一定就是NSObject的元类对象的地址,验证结果如下:

image.png

image.png

  此时此刻根元类的地址为0x000000010036c0f0,与我们根据Mach-o中Person类数据中的isa的值所推导出来的NSObject根元类的地址一致,根据以上的结果我们可以得到以下的结论:自定义类的类数据是存储在Mach-o中的,并且其isa以及bits的值在主程序的Mach-o文件被lldb加载链接到内存之前就已经有了,也就是说一个类的isa的值以及bits字段的值是在编译期就确定了的,而在lldb加载链接时只要读取它们的值就可以了。

  • 问题二:data()函数的返回值类型实际上是class_rw_t *,为何能够强转成class_ro_t *这种类型呢?

在ObjC源码中class_ro_t与class_rw_t实际上都是结构体类型,而class_ro_t *与class_rw_t都是指向结构体的指针,大小都为8字节大小,因此是可以相互赋值的,但是读取数据的时候可能有问题,例如,在程序中编写如下代码:

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;
}

编译运行程序,程序并不会报错崩溃,并且输出的信息如下图所示:

image.png

当将Test_1结构体指针t1强转成Test_2结构体指针t2时,输出t2的成员变量,发现数据就会出现问题,但是当将t2又强转回Test_1结构体指针t5时,输出t5的各个成员变量,数据就恢复正常了,因此我们可以确信,此时通过data()函数取出来的数据结构类型实际上就是class_ro_t *,那么就表示当时在这个指针地址所存储的是就class_ro_t结构体的数据,那么这个当时是什么时候呢?我们可以肯定的是必然不在运行时刻进行存储的,因为这些数据(protocols、methods、properties)现在是在lldb加载链接应用程序时读取出来的(lldb加载链接应用程序是在其运行时之前的,此时还没有执行主程序的main函数),那么这些数据一定是在编译时存储进去的,很有可能就是llvm做的这些存储操作,因此我们查看llvm的源码,搜索class_ro_t就可以找到其定义,如下图所示:

image.png

可以发现llvm中的class_ro_t的数据结构类型与ObjC源码中class_ro_t的数据结构类型是一致的,再来看看这个结构体中read函数,其代码如下所示:

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;
}

可以看到,在这个函数中process以及传入进来的addr来读取内存数据的,然后创建了一个DataExtractor类型的变量extractor,通过extractor变量对class_ro_t中各个成员变量赋值,让我们再来看看llvm是在哪些地方读取class_ro_t结构体类型的数据的,全局搜索Read函数,找到如下图所示代码:

image.png

也就是在Read_class_row这个函数中读取了ro结构体的值。

2.2 将类ro中的数据拷贝到rw,cls保存rw

  继续执行程序,来到下一个断点,输出ro的值,由于Person类中rw data还未被分配内存,所以执行了下图红框中的代码:

image.png

  这段代码首先开辟了一段class_rw_t结构体类型的内存空间,然后将函数开始时所创建的class_rw_t *类型指针rw指向这块内存空间的首地址,调用class_rw_t体中的set_ro函数,传入ro为参数,我们跳到这个函数中查看其代码并打上断点,并打印参数ro的值,发现执行了下图中红框部分所示的分支代码:

image.png

  然后再来看看set_ro_or_rwe函数中的代码,如下图所示:

image.png

  由以上代码可知,ro_or_rw_ext_t实际上是objc::PointerUnion<const class_ro_t, class_rw_ext_t, PTRAUTH_STR("class_ro_t"), PTRAUTH_STR("class_rw_ext_t")>这种类型的别名,而在set_ro_or_rwe这个函数中,调用了PointerUnion的初始化函数,传入了roro_or_rw_ext的地址作为参数,我们来看看这个初始化函数做了什么,如下图所示:

image.png

  这个初始化函数中是将ro的值赋值给了_value,然后我们再来看看之后调用的storeAt函数(将ro_or_rw_ext作为参数1传入),如下图所示:

image.png

  经过分析我们知道了,这段代码的意思就是将ro指针的值拷贝了一份赋值给了class_rw_t中的局部变量ro_or_rw_extclass PointerUnion类型)中的成员变量_value,我们再来验证一下,如下图红框中打印结果所示:

image.png

  然后紧接着设置rwflags值,并调用cls中的函数setData保存rw的值,这个函数代码如下所示:

image.png

image.png

  经过这个setData()这个函数的调用,会将rw的地址通过一些操作保存到cls成员变量bitsstruct class_data_bits_t 类型)的唯一成员变量bitsuintptr_t 类型)中,而之后再调用clsdata()函数获取到的就是class_rw_t *类型的指针变量了。

2.3 递归实现cls的superclass以及metaclass

  代码如下图所示:

image.png

  setSuperclass函数代码如下所示:

image.png

  initClassIsa函数代码如下所示:

image.png

2.4 设置InstanceSize以及超类子类链接

  代码如下图所示:

image.png

  addSubclass函数中部分代码如下图所示:

image.png

  addRootClass函数中代码如下图所示:

image.png

image.png

2.5 条理化类

  调用methodizeClass函数条理化类cls,函数第一个参数为cls,第二个参数不管cls是分类还是父类传入的值都为nil,如下图所示:

image.png

  为了方便我们探究自定的类,因此在methodizeClass函数中编写验证代码,如下图所示:

image.png

3. 条理化类流程

  首先,我们在Person类中添加多个方法以及属性,代码如下所示:

@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

  编译运行代码,代码运行到我们编写的if语句中,打开所有断点。

3.1 方法列表

  在methodizeClass函数中对方法列表的处理代码如下图所示:

image.png

  这个方法中调用了prepareMethodLists函数,其代码如下图所示:

image.png

  这个函数中的关键代码其实是fixupMethodList这个函数的调用,其代码如下图所示:

image.png

  阅读其注释以及代码我们可以知道这个方法中对clsmethods进行了排序,并且是根据Methodsel的地址来进行排序的,此时你可能觉得有些许熟悉,因为我们之前在方法慢速查找流程中接触过这一块的内容,当时是使用二分查找算法从方法列表中查找方法的,如果要查找的sel的地址在类的classMethod_list存在,就会返回Method_list最前面的与sel所对应的impMethod,然后跳转执行imp中的代码,而我们都知道能使用二分查找算法的前提是这个列表中的Method是排好序的,因此我们在fixupMethodList函数调用前后打印一下方法列表中的sel名字以及其地址,看看它是如何排序的,因此在fixupMethodList前后编写如下图所示代码:

image.png

  运行程序,Person类方法列表排序前后打印顺序如下图所示:

image.png

  首先我们来看看排序前的方法顺序,可以发现每个sel的地址都是以从小到大的顺序排好了的(而且是按照类implementation中实现顺序排序好的,与类interface暴露给外界调用的顺序无关,然后每个属性从上到下的get方法以及set方法依次排在后面),那么此时你也许会想,那这还需要进行排序吗?这不是多此一举吗?那让我们再来看看,排好序之后的输出打印,可以发现sel的顺序发生了变化,并且某些sel的地址也变了(例如b方法本来排在首个位置,现在排在了第10,它的地址本来是0x100003e57却突然变成了0x7fff7bd88deb),这是怎么回事呢?这些地址是在排序的过程中改变的呢?还是在排序前改变的呢?这就需要我们进一步的来验证了,因此我们在fixupMethodList函数中开头以及stable_sort这个排序函数调用前后都按顺序打印一下方法列表的方法,代码如下所示:

image.png

  运行编译,进入断点,先清空之前的输出信息,然后打印输出结果,如下所示:

image.png

image.png

image.png

  通过打印信息结果我们可以知道,原来在排序之前Method_list中一些sel的地址就已经改变了,而改变sel的地址的代码如下图所示:

image.png

image.png

image.png

  在__sel_registerName函数中打上断点,会发现方法列表中凡是sel被改变了地址值的都是因为调用search_builtins函数获取到的result不为空导致的,也就是红框1中的代码,而没有改变sel地址的都执行了红框2中的代码,所以为了之后能使用二分查找算法来快速查找方法,还需要对其进行一次排序,而至于为何调用search_builtins函数的原因,本人目前也不清楚。

3.2 条理化分类

  方法排序完毕之后,紧接着会判断rwe是否为空,如果不为空就会获取rwe中的methods并且调用attachLists,附着刚刚排序完毕的方法列表,打印rwe的值,会发现rwe此时为nil,如下图所示:

image.png

  也就是说,函数中关于rwe的代码分支都不会执行,那么在什么情况获取到的rwe才不为nil呢?attachLists函数中执行的代码逻辑又是什么呢?这个问题将会在下篇博客分类的加载流程中进行探讨。

  然后我们再来看看接下来的代码,如下图所示:

image.png

  根据对previously的打印我们知道程序不会执行红框1中的代码,而是直接执行了红框2中的代码,并且此时isMetafalse,那么什么情况下程序会执行红框1中的代码呢?这个问题也会在下篇博客分类的加载流程中进行探讨。

4 类加载流程总结

4.1 懒加载类以及非懒加载类

  我想你应该还记得在read_images函数中之所以能够调用realizeClassWithoutSwift函数加载Person类,是因为我们实现了Person类中的类方法load,这种类的加载方式起始称为非懒加载类(map_images函数调用时加载类数据),但如果我们不实现Person类的类方法load,程序会在消息的慢速转发流程中的lookupImpOrForward函数中调用realizeClassWithoutSwift函数加载Person类,这种类的加载方式则称之为懒加载类(类数据加载推迟到第一次消息的时候),俩种加载方式对比如下图所示:

image.png

  在我们的应用程序应该尽量少使用非懒加载类,因为非懒加载类是在程序启动之前进行数据加载的,而每个类的数据加载都是耗时操作,如果程序中非懒加载类特别多,就需要在应用启动之前优先加载所有的非懒加载类的数据,但是这些类的数据也不是立马就使用的,因此会占用内存空间,对内存来说也是有消耗的,因此要尽可能的使用懒加载类,少实现类中的load类方法。

4.2 非懒加载类加载流程图

未命名文件-4.png