探索之前

用MachOView分析Build后的文件

之前提到过,类的创建是在编译期,也就是build之后的文件中就已经创建了这个类,说白了就是这个类有地址了。项目结构如下,然后将build后的可执行文件拖到MachOView软件中查看。
截屏2020-05-06 14.54.23.png截屏2020-05-06 15.01.45.png
在MachOView中能看到这三个类分别的起始地址。接下来在XCode中通过LLDB打印来验证一下,这三个类的地址和MachOView上是否一致。
截屏2020-05-06 15.32.17.png
那么,问题来了。鄙人夜观objc_class的结构:

  1. struct objc_class : objc_object {
  2. // Class ISA;
  3. Class superclass;
  4. cache_t cache; // formerly cache pointer and vtable
  5. class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
  6. // 省略部分代码
  7. }

ISA:8字节;superClass:8字节;cache:16字节;bits:8字节。总计40字节。(之前文章提到过,点击属性的类型可以看出来)
回过头来再看上边的地址(16进制表示):
Person 地址是0x100003708
Student 地址是0x100003730 (和Person的地址刚好差40字节)
Teacher地址是0x1000037a8 (和Student的地址相差120字节)
思考:难道Teacher和Student之间穿插了别的类了吗?
手动计算一下内存偏移40个字节应该是0x100003758,再偏移40字节为0x100003780,之后再偏移40字节就是0x1000037a8了,所以这里需要看的就是0x100003758和0x100003780究竟是什么东西?用LLDB打印看一下:
截屏2020-05-06 16.01.14.png
惊天地泣鬼神了么?居然Student之后还是Student。Teacher之前还有个Teacher。什么鬼?迷茫之中,脑海中有一个灵石传音告诉我,元类。用Student类通过isa指针来验证一下我们的猜想吧:
截屏2020-05-06 16.11.12.png

0x00007ffffffffff8为isa的mask掩码,在之前isa相关博客多次提到过

验证通过,打印的元类地址0x100003758,和上边通过内存偏移得出来的一模一样。元类也是在编译时创建的,属性也是需要40字节地址的。根据lldb打印猜想:元类和类在地址内存中是没有先后顺序的但是应该是挨着的。在Person地址0x100003708向前做40个字节的偏移0x1000036d0,如果猜想准确,它应该指向的是Person的元类:lldb打印验证一下:
截屏2020-05-06 16.06.58.png
写这么多,就一个目的,确定类在编译的时候就已经创建了,有了地址。空有一些地址和内存空间,它这里边的属性还没有赋值,比如ISA,superClass,而类的加载要将这些属性,这些空间给着实添上数据。

从何处开始研究

APP启动到main函数之间的那些事儿博客中提到过,在main函数之前,dyld库之后,会回调到objc中的_objc_init函数,开始进行各种初始化的操作,如下图。其中我们这篇文章开始研究的就是这个方法及其参数(回调函数)_dyld_objc_notify_register(&map_images, load_images, unmap_image);上边的几个函数的作用参考后边的备注。接下来开始分析map_images和load_images。(unmap_image,就是从内存中移除已经映射的文件研究意义不大)
截屏2020-04-26 11.12.06.png

map_images

map_images:简单来说就是镜像文件的映射,将数据从编译出来的Mach-O文件中映射到内存段中。这里边的代码操作就是将数据读取出俩,然后插入到一个个表中。源码:

  1. void
  2. map_images(unsigned count, const char * const paths[],
  3. const struct mach_header * const mhdrs[])
  4. {
  5. mutex_locker_t lock(runtimeLock);
  6. return map_images_nolock(count, paths, mhdrs);
  7. }
  8. void
  9. map_images_nolock(unsigned mhCount, const char * const mhPaths[],
  10. const struct mach_header * const mhdrs[])
  11. {
  12. // 省略部分代码
  13. // Find all images with Objective-C metadata.
  14. hCount = 0;
  15. // 省略部分代码
  16. if (hCount > 0) {
  17. _read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
  18. }
  19. }

map_images源码最终走到的根本点就是_read_images_read_images 就是在Mach-O文件中,拿到数据(比如所有的Class、SEL等),映射到对应的表中。下面分析_read_images。

_read_images

1、创建两个表
  1. if (!doneOnce) {
  2. doneOnce = YES;
  3. // namedClasses
  4. // Preoptimized classes don't go in this table.
  5. // 4/3 is NXMapTable's load factor
  6. int namedClassesSize =
  7. (isPreoptimized() ? unoptimizedTotalClasses : totalClasses) * 4 / 3;
  8. gdb_objc_realized_classes =
  9. NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);
  10. allocatedClasses = NXCreateHashTable(NXPtrPrototype, 0, nil);
  11. ts.log("IMAGE TIMES: first time tasks");
  12. }

通过doneOnce来控制这个模块的代码只执行一次。保证gdb_objc_realized_classesallocatedClasses表只创建一次。看一下这两个表官方给的解释:

  1. // This is a misnomer: gdb_objc_realized_classes is actually a list of
  2. // named classes not in the dyld shared cache, whether realized or not.
  3. NXMapTable *gdb_objc_realized_classes; // exported for debuggers in objc-gdb.h

gdb_objc_realized_classes:无论是否实现,只要不在dyld共享缓存中并且已经命名的类的列表。

  1. /***********************************************************************
  2. * allocatedClasses
  3. * A table of all classes (and metaclasses) which have been allocated
  4. * with objc_allocateClassPair.
  5. **********************************************************************/
  6. static NXHashTable *allocatedClasses = nil;

allocatedClasses:通过objc_allocateClassPair已经分配的所有类(包括元类)的列表。
开局先创建两个关于类的表,可以看成一个大的集合,一个小的集合。之后在Mach-O文件中关于类的数据就要根据条件填充到这两个表中。

2、将所有类映射到内存
  1. for (EACH_HEADER) {
  2. classref_t *classlist = _getObjc2ClassList(hi, &count);
  3. for (i = 0; i < count; i++) {
  4. Class cls = (Class)classlist[i];
  5. Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized);
  6. if (newCls != cls && newCls) {
  7. // Class was moved but not deleted. Currently this occurs
  8. // only when the new class resolved a future class.
  9. // Non-lazily realize the class below.
  10. resolvedFutureClasses = (Class *)
  11. realloc(resolvedFutureClasses,
  12. (resolvedFutureClassCount+1) * sizeof(Class));
  13. resolvedFutureClasses[resolvedFutureClassCount++] = newCls;
  14. }
  15. }
  16. }
  17. ts.log("IMAGE TIMES: discover classes");

_getObjc2ClassList 拿到每个头文件下所有的类,这里有一点需要注意,断点在Class cls = (Class)classlist[i];时,cls还是是一个地址,并不会显示出类的名字。在执行完Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized);newCls就有了类的名字。readClass源码:

  1. Class readClass(Class cls, bool headerIsBundle, bool headerIsPreoptimized)
  2. {
  3. const char *mangledName = cls->mangledName();
  4. Class replacing = nil;
  5. if (Class newCls = popFutureNamedClass(mangledName)) {
  6. class_rw_t *rw = newCls->data();
  7. const class_ro_t *old_ro = rw->ro;
  8. memcpy(newCls, cls, sizeof(objc_class));
  9. rw->ro = (class_ro_t *)newCls->data();
  10. newCls->setData(rw);
  11. freeIfMutable((char *)old_ro->name);
  12. free((void *)old_ro);
  13. addRemappedClass(cls, newCls);
  14. replacing = cls;
  15. cls = newCls;
  16. }
  17. if (headerIsPreoptimized && !replacing) {
  18. assert(getClass(mangledName));
  19. } else {
  20. addNamedClass(cls, mangledName, replacing);
  21. addClassTableEntry(cls);
  22. }
  23. return cls;
  24. }

先解释上边提到的关于类的名字的问题,const char *mangledName = cls->mangledName();拿到了类的名字,然后在addNamedClass(cls, mangledName, replacing);时候为cls赋予了名字。
然后说一个这个地方面试的坑点,if (Class newCls = popFutureNamedClass(mangledName))判断句里边的内容,看似像是只要进入这个条件,就会初始化rw、为rw做一些赋值的操作。但是注意,这个条件正常情况下不满足,进不来,直接将断点打在if里边,发现无论系统类还是自己创建的类都进不来,所以rw并不是在这个时候创建和赋值的,这是个坑点。至于提到的Future Class我也不清楚到底是什么,平时用到的类和系统类可以说都是Normal Class,这里注意一下就行。
所以,这里返回的cls还是传进来的cls,然后,readClass里边的操作就是将cls命名,并添加到对应的表中。
addNamedClass(cls, mangledName, replacing);将已命名的类添加到gdb_objc_realized_classes表中;

  1. static void addNamedClass(Class cls, const char *name, Class replacing = nil)
  2. {
  3. Class old;
  4. if ((old = getClass(name)) && old != replacing) {
  5. inform_duplicate(name, old, cls);
  6. addNonMetaClass(cls);
  7. } else {
  8. // 插入到gdb_objc_realized_classes表中
  9. NXMapInsert(gdb_objc_realized_classes, name, cls);
  10. }
  11. }

addClassTableEntry(cls);将已经分配完成的类添加到allocatedClasses表中。

  1. static void addClassTableEntry(Class cls, bool addMeta = true) {
  2. if (!isKnownClass(cls))
  3. // 插入到allocatedClasses表中
  4. NXHashInsert(allocatedClasses, cls);
  5. if (addMeta)
  6. addClassTableEntry(cls->ISA(), false);
  7. }

至此,所有的类都实实在在的已经映射到我们内存表中,当然只是把类写进去,类里边的属性还没有赋值,还不到时候。继续阅读_read_images源码。

3、将所有SEL都注册到另外一张哈希表
  1. for (EACH_HEADER) {
  2. SEL *sels = _getObjc2SelectorRefs(hi, &count);
  3. UnfixedSelectors += count;
  4. for (i = 0; i < count; i++) {
  5. const char *name = sel_cname(sels[i]);
  6. // 注册SEL的操作
  7. sels[i] = sel_registerNameNoLock(name, isBundle);
  8. }
  9. }

4、修复函数指针

修复旧的函数指针调用遗留,一般进不来,if后直接continue。

  1. for (EACH_HEADER) {
  2. message_ref_t *refs = _getObjc2MessageRefs(hi, &count);
  3. if (count == 0) continue;
  4. for (i = 0; i < count; i++) {
  5. fixupMessageRef(refs+i);
  6. }
  7. }
  8. ts.log("IMAGE TIMES: fix up objc_msgSend_fixup");

5、将协议列表加载到Protocol的哈希表中
  1. for (EACH_HEADER) {
  2. extern objc_class OBJC_CLASS_$_Protocol;
  3. Class cls = (Class)&OBJC_CLASS_$_Protocol;
  4. assert(cls);
  5. NXMapTable *protocol_map = protocols();
  6. bool isPreoptimized = hi->isPreoptimized();
  7. bool isBundle = hi->isBundle();
  8. protocol_t **protolist = _getObjc2ProtocolList(hi, &count);
  9. for (i = 0; i < count; i++) {
  10. readProtocol(protolist[i], cls, protocol_map,
  11. isPreoptimized, isBundle);
  12. }
  13. }

6、对所有Protocol重映射
  1. for (EACH_HEADER) {
  2. // 需要注意到是,下面的函数是_getObjc2ProtocolRefs,和上面的_getObjc2ProtocolList不一样
  3. protocol_t **protolist = _getObjc2ProtocolRefs(hi, &count);
  4. for (i = 0; i < count; i++) {
  5. remapProtocolRef(&protolist[i]);
  6. }
  7. }

7、实现非懒加载类

至于什么是懒加载类和非懒加载类后面再说。这里拿到非懒加载类之后,就对类信息进行实现。

  1. for (EACH_HEADER) {
  2. classref_t *classlist =
  3. _getObjc2NonlazyClassList(hi, &count);
  4. for (i = 0; i < count; i++) {
  5. Class cls = remapClass(classlist[i]);// 重新映射?
  6. if (!cls) continue;
  7. // 添加到allocatedClasses表中
  8. addClassTableEntry(cls);
  9. // 实现非懒加载的类(实例化类对象的一些信息,例如rw)
  10. realizeClass(cls);
  11. }
  12. }
  13. ts.log("IMAGE TIMES: realize non-lazy classes");

8、对未来的类进行初始化

这里上边解释过 Future Class正常遇不到,所以这个if 也进不来,不需要考虑。

  1. if (resolvedFutureClasses) {
  2. for (i = 0; i < resolvedFutureClassCount; i++) {
  3. realizeClass(resolvedFutureClasses[i]);
  4. resolvedFutureClasses[i]->setInstancesRequireRawIsa(false/*inherited*/);
  5. }
  6. free(resolvedFutureClasses);
  7. }
  8. ts.log("IMAGE TIMES: realize future classes");

9、处理分类Category
  1. // Discover categories.
  2. for (EACH_HEADER) {
  3. category_t **catlist =
  4. _getObjc2CategoryList(hi, &count);
  5. bool hasClassProperties = hi->info()->hasCategoryClassProperties();
  6. for (i = 0; i < count; i++) {
  7. // 循环遍历当前类的所有Category
  8. category_t *cat = catlist[i];
  9. Class cls = remapClass(cat->cls);
  10. // Process this category.
  11. // First, register the category with its target class.
  12. // Then, rebuild the class's method lists (etc) if
  13. // the class is realized.
  14. // 首先,通过其所属的类注册Category。如果这个类已经被实现,则重新构造类的方法列表。
  15. bool classExists = NO;
  16. if (cat->instanceMethods || cat->protocols
  17. || cat->instanceProperties)
  18. {
  19. // 将Category添加到对应Class的value中,value是Class对应的所有category数组
  20. addUnattachedCategoryForClass(cat, cls, hi);
  21. // 如果已经实现,将Category的method、protocol、property添加到Class
  22. if (cls->isRealized()) {
  23. remethodizeClass(cls);
  24. classExists = YES;
  25. }
  26. }
  27. // 这块和上面逻辑一样,区别在于这块是对Meta Class做操作,而上面则是对Class做操作
  28. // 根据下面的逻辑,从代码的角度来说,是可以对原类添加Category的
  29. if (cat->classMethods || cat->protocols
  30. || (hasClassProperties && cat->_classProperties))
  31. {
  32. addUnattachedCategoryForClass(cat, cls->ISA(), hi);
  33. if (cls->ISA()->isRealized()) {
  34. remethodizeClass(cls->ISA());
  35. }
  36. }
  37. }
  38. }
  39. ts.log("IMAGE TIMES: discover categories");

以上,map_images、_read_images中主要功能就是将类及其分类等信息从Mach-O文件中映射到内存中,处理分类并且如果类已实现的话,将分类的方法、属性、协议添加到类中。另外重点注意的就是非懒加载类,在这个时候就将类进行初始化了——realizeClass(cls)

类的加载

类的加载实际上就是对类结构体的ISA、superClass、cache(这个例外,对象调用方法时候填充)、bits属性进行初始化赋值。

realizeClass类的初始化

在_read_images中对非懒加载类进行映射的时候,就已经调用了realizeClass(cls)函数。这里先分析这个函数中的赋值操作,之后再分析懒加载类和非懒加载类的加载相关。源码主要有以下操作:

1、读取data(),初始化rw并拷贝ro到rw->ro
  1. ro = (const class_ro_t *)cls->data();
  2. // 这个就是之前提到的future class,一般不会进来
  3. if (ro->flags & RO_FUTURE) {
  4. // This was a future class. rw data is already allocated.
  5. rw = cls->data();
  6. ro = cls->data()->ro;
  7. cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
  8. } else {// 走normal class
  9. // Normal class. Allocate writeable class data.
  10. rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
  11. rw->ro = ro;
  12. rw->flags = RW_REALIZED|RW_REALIZING;
  13. cls->setData(rw);
  14. }

直接编译相关的数据在data()中,直接强转为class_ro_t类型。条件判断走进Normal class中创建rw,然后对rw->ro进行赋值,注意最后的cls->setData(rw);这也是为什么类加载之后我们读取的data()会强转为class_rw_t而不再是class_ro_t了。这里仅仅对rw中的ro进行赋值,查看rw的结构:

  1. struct class_rw_t {
  2. // Be warned that Symbolication knows the layout of this structure.
  3. uint32_t flags;
  4. uint32_t version;
  5. const class_ro_t *ro;
  6. method_array_t methods;
  7. property_array_t properties;
  8. protocol_array_t protocols;
  9. Class firstSubclass;
  10. Class nextSiblingClass;
  11. char *demangledName;
  12. };

可以看到除了ro以外还有其他很多属性没有赋值,这里注意一下就行,具体哪里赋值,后边讲。

2、递归映射并实现父类和元类
  1. supercls = realizeClass(remapClass(cls->superclass));
  2. metacls = realizeClass(remapClass(cls->ISA()));

类结构中有ISA和superClass属性需要赋值。这里递归实现应该没什么问题,问题是这个递归的终止在哪儿?realizeClass(cls)函数入口处有判断if (!cls) return nil;if (cls->isRealized()) return cls;父类向上最终都会找到nil,而元类向上最终一直是NSObject元类,已经实现就返回cls。这就是递归终止点。

  1. // Update superclass and metaclass in case of remapping
  2. cls->superclass = supercls;
  3. cls->initClassIsa(metacls);

为当前类赋值父类和元类(也就是赋值ISA)。

3、将此类连接到其超类的子类列表
  1. // Connect this class to its superclass's subclass lists
  2. if (supercls) {
  3. addSubclass(supercls, cls);
  4. } else {
  5. addRootClass(cls);
  6. }

其实就是把这个父类和类互相关联起来。

4、将ro的数据写入到rw中

最后的最后,调用了methodizeClass(cls);方法。之前创建rw的时候,只对rw的ro进行赋值了,其他的属性,就是在这个方法中赋值的。展开分析。

methodizeClass

  1. static void methodizeClass(Class cls)
  2. {
  3. bool isMeta = cls->isMetaClass();
  4. auto rw = cls->data();
  5. auto ro = rw->ro;
  6. // Install methods and properties that the class implements itself.
  7. method_list_t *list = ro->baseMethods();
  8. if (list) {
  9. prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
  10. rw->methods.attachLists(&list, 1);
  11. }
  12. property_list_t *proplist = ro->baseProperties;
  13. if (proplist) {
  14. rw->properties.attachLists(&proplist, 1);
  15. }
  16. protocol_list_t *protolist = ro->baseProtocols;
  17. if (protolist) {
  18. rw->protocols.attachLists(&protolist, 1);
  19. }
  20. // Root classes get bonus method implementations if they don't have
  21. // them already. These apply before category replacements.
  22. if (cls->isRootMetaclass()) {
  23. // root metaclass
  24. addMethod(cls, SEL_initialize, (IMP)&objc_noop_imp, "", NO);
  25. }
  26. // Attach categories.
  27. category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
  28. attachCategories(cls, cats, false /*don't flush caches*/);
  29. }

在这里分别拿到ro中的baseMethods()basePropertiesbaseProtocols对rw中的method_array_t methods;property_array_t properties;protocol_array_t protocols;进行赋值。之后还进行attachCategories处理分类的方法、属性、协议列表,这个点进去看下,先记住这个,后边分析分类的时候会提到。这里重点看attachLists这个方法,看看是如何将ro中的各个列表加进去的。

attachLists

  1. void attachLists(List* const * addedLists, uint32_t addedCount) {
  2. if (addedCount == 0) return;
  3. if (hasArray()) {
  4. // many lists -> many lists
  5. uint32_t oldCount = array()->count;
  6. uint32_t newCount = oldCount + addedCount;
  7. setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
  8. array()->count = newCount;
  9. memmove(array()->lists + addedCount, array()->lists,
  10. oldCount * sizeof(array()->lists[0]));
  11. memcpy(array()->lists, addedLists,
  12. addedCount * sizeof(array()->lists[0]));
  13. }
  14. else if (!list && addedCount == 1) {
  15. // 0 lists -> 1 list
  16. list = addedLists[0];
  17. }
  18. else {
  19. // 1 list -> many lists
  20. List* oldList = list;
  21. uint32_t oldCount = oldList ? 1 : 0;
  22. uint32_t newCount = oldCount + addedCount;
  23. setArray((array_t *)malloc(array_t::byteSize(newCount)));
  24. array()->count = newCount;
  25. if (oldList) array()->lists[addedCount] = oldList;
  26. memcpy(array()->lists, addedLists,
  27. addedCount * sizeof(array()->lists[0]));
  28. }
  29. }

源码中分了多对多0对一一对多三种情况,rw中的methods、properties、protocols都是二维数组的形式,新添加进来的列表其实都是插在前边,上边的算法就是关于字节移动和拷贝的逻辑。

思考:都什么时候会用到attachLists这个方法? 1、类的加载,如上methodizeClass方法中; 2、添加方法:addMethods; 3、添加属性:_class_addProperty; 4、添加协议:class_addProtocol; 5、分类的加载:attachCategories;

至此,大致知道了类的初始化都做了哪些处理。下边具体分析一下类的家在时机。首先需要了解的什么是懒加载类和非懒加载类。

懒加载类和非懒加载类

在_read_images方法中,非懒加载类操作_getObjc2NonlazyClassList的上边有一句注释:// Realize non-lazy classes (for +load methods and static instances)意思就是实现了+load方法的就被认为是非懒加载类。
反之,未实现+load方法的类就是懒加载类。接下来分析加载时机,此时不考虑分类的情况(分类同样也会有懒加载分类和非懒加载分类)。

加载时机

非懒加载类

先说非懒加载类,在分析_read_images方法的时候,在非懒加载类循环中就调用了realizeClass``(cls);即对这个类进行了实现。(realizeClass后边做展开分析)。所以,非懒加载类的加载时机在_read_images时已经实现。

懒加载类

那么懒加载类是什么时候实现的。还记得之前消息查找的方法吗?

  1. IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
  2. bool initialize, bool cache, bool resolver)
  3. {
  4. // 省略代码 ...
  5. if (!cls->isRealized()) {
  6. realizeClass(cls);
  7. }
  8. // 省略代码 ...
  9. }

类在第一次使用(消息发送的时候)就会判断类是否已经实现,如果没实现,就先实现,然后再去查找方法。懒加载类的加载时机在消息发送时。可以在if (!cls->isRealized())打个断点,自己创建一个类尝试实现和不实现+load方法来调试看是都在发送消息的时候走进去realizeClass(cls)

分类的加载

类有懒加载和非懒加载,那么分类也有懒加载分类和非懒加载分类。那么这和类来组合,就出现如下四种,这里我根据debug调试总结出四种情况的加载流程,具体的debug自行总结。

懒加载分类

和类一样,未实现+load方法的可以视为懒加载分类。首先明确一点,懒加载分类里边的数据是直接编译进类的ro中。这个点做一步验证:假设场景,懒加载类和懒加载分类(均未实现+load方法),懒加载类在发送消息的时候会初始化。Teacher类及其分类方法:
截屏2020-05-08 10.53.26.png截屏2020-05-08 10.53.34.png
分享一个调试小技巧,因为进入realizeClass函数可能会有很多系统类,在芸芸众生之中找到我的类:

  1. class_ro_t *lgro = (const class_ro_t *)cls->data();
  2. const char *lgname = lgro->name;
  3. const char *lgnameTest = "Teacher";
  4. if (strcmp(lgname, lgnameTest) == 0) {
  5. printf("是我们的人");
  6. }

好,继续我们的验证,一张图解释一切:
截屏2020-05-08 11.18.17.png

1、懒加载类+懒加载分类

懒加载类的加载是在消息发送的时候,而懒加载分类的数据直接被编译到ro中,所以在将ro中的数据attachLists到rw的时候就已经处理了,这里可以直接总结出流程即:
lookUpImpOrForward -> realizeClass(cls) -> methodizeClass(cls) -> attachLists

2、非懒加载类+懒加载分类

非懒加载类实现了+load方法:
_read_images -> _getObjc2NonlazyClassList -> realizeClass(cls) -> methodizeClass(cls)-> attachLists

非懒加载分类

实现了+load方法的分类视为非懒加载分类。非懒加载分类的数据不会直接编译到类的ro中,需要在类初始化的时候添加到rw中。

3、懒加载类+非懒加载分类

有种类似子类实现了,连带父类会一起实现的感觉。既然分类实现了+load方法,分类提前了,那么类虽然是懒加载类,按理说是要消息发送的时候实现,但是由于类提前了,类也就提前初始化了。
_read_images -> _getObjc2CategoryList -> addUnattachedCategoryForClass ->由于类没有实现,至此``_read_images 即 ``map_images执行完毕。在load_images中prepare_load_methods -> _getObjc2NonlazyCategoryList -> realizeClass(cls) -> methodizeClass(cls)-> attachLists

4、非懒加载类+非懒加载分类

_read_images -> _getObjc2NonlazyClassList -> realizeClass(cls)-> 至此,非懒加载类已经实现 -> _getObjc2CategoryList -> addUnattachedCategoryForClass -> 因为非懒加载类已经实现 -> remethodizeClass -> attachCategories -> attachLists

load_images

在上边懒加载类+非懒加载分类模块已经初步提到了load_images。再或者说,map_images做的主要就是一些表映射,还有非懒加载类的初始化等工作,那么多次提到的+load方法是什么时候调用的呢?看源码:

  1. void
  2. load_images(const char *path __unused, const struct mach_header *mh)
  3. {
  4. // Return without taking locks if there are no +load methods here.
  5. if (!hasLoadMethods((const headerType *)mh)) return;
  6. recursive_mutex_locker_t lock(loadMethodLock);
  7. // Discover load methods
  8. {
  9. mutex_locker_t lock2(runtimeLock);
  10. prepare_load_methods((const headerType *)mh);
  11. }
  12. // Call +load methods (without runtimeLock - re-entrant)
  13. call_load_methods();
  14. }

先看注释,直接就看到了// Call +load methods。字面意思,回调+load方法。回调之前,需要先准备一下,或者说需要统计一下都有哪些类和分类实现了+load方法。往上看// Discover load methods发现load方法,逻辑很完美。下面看看在上边懒加载类+非懒加载分类提到的prepare_load_methods

  1. void prepare_load_methods(const headerType *mhdr)
  2. {
  3. size_t count, i;
  4. runtimeLock.assertLocked();
  5. classref_t *classlist =
  6. _getObjc2NonlazyClassList(mhdr, &count);
  7. for (i = 0; i < count; i++) {
  8. schedule_class_load(remapClass(classlist[i]));
  9. }
  10. category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
  11. for (i = 0; i < count; i++) {
  12. category_t *cat = categorylist[i];
  13. Class cls = remapClass(cat->cls);
  14. if (!cls) continue; // category for ignored weak-linked class
  15. realizeClass(cls);
  16. assert(cls->ISA()->isRealized());
  17. add_category_to_loadable_list(cat);
  18. }
  19. }

找到所有的非懒加载类(实现+load方法)

schedule_class_load -> add_class_to_loadable_list 最根本的就是记录下实现+load方法的cls和method:

  1. loadable_classes[loadable_classes_used].cls = cls;
  2. loadable_classes[loadable_classes_used].method = method;

找到所有的非懒加载分类

这里边发现一个很熟悉的方法realizeClass(cls);。之前提过,非懒加载分类会带动懒加载的类提前初始化(而不是等到消息发送的时候才初始化)就是在这个地方。add_category_to_loadable_list 这个最根本的也是记录下分类cat和method:

  1. loadable_categories[loadable_categories_used].cat = cat;
  2. loadable_categories[loadable_categories_used].method = method;

找到了cls、cat也找到了method,准备工作完成,调用call_load_methods();完成+load方法回调。

类和分类中相同方法的调用顺序

load方法的调用顺序

如果类和分类都实现了+load方法,那么系统回调的顺序是什么样子?打印看一下:
截屏2020-05-08 14.00.12.png
打印结果:load方法的回调,先回调类再回调分类;

除+load方法外相同方法,对象会调用哪个?

这里在Teacher类和分类中都写上-(void)saySomething;实例化一个对象调用这个方法,打印开一下:
截屏2020-05-08 14.03.50.png
打印结果证明:其余相同方法会调用分类的。为什么?
还记得上边提到的attachLists吗?这个的插入是前插,也就是分类的方法列表会插入到类原有的列表的前边。方法查找的时候必然优先找到分类的imp,然后直接响应了。

类拓展

什么是类拓展?这个其实在开发中经常用到,比如我们在Person类想定义一些属性或者方法,不让外界直接调用,一般就会在Person.m上边这样写:

  1. @interface Person ()
  2. @property (nonatomic, strong) NSString *personName;
  3. -(void)person;
  4. @end

这个其实就是类拓展,也可以称之为匿名分类Person ()括号中没有名字,分类的括号中还有名字。那么类拓展的数据是什么时候加载的?Person没有实现+load方法是懒加载类,在消息发送的时候调用,我们直接在消息发送断点,查看ro:
截屏2020-05-08 14.26.14.png

总结

至此,本文大致分析了从编译期类的创建,到映射到内存,然后分为四种情况简单总结了类和分类的加载整个流程,最后提到了+load方法是在什么时候回调的。大致就这些,有新的理解随时补充。