引言
上篇文章讲到了dyldobjc的连接,在_objc_init函数中,通过_dyld_objc_notify_register注册三个回调函数:map_imagesload_imagesunmap_image,如图所示。我们在011-iOS底层原理-_objc_init中已经探索了load_imagesunmap_image的作用与流程,本文将探索map_images
012-iOS底层原理-类的加载 - 图20

工程:LGProject

map_images
对以 headerList开头的链表中的 headers 进行初始处理
011-iOS底层原理-_objc_init中已经探索了map_images函数内部返回的是map_images_nolock()的结果,进入map_images_nolock找到了_read_images()这个函数。而此函数是本文所探索的入口。

map_images管理文件中和动态库中所有的符号:class,protocal,selector,category

1、map_images_nolock
012-iOS底层原理-类的加载 - 图21
#####2、_read_images
_read_images的源码共有360行(行行出状元?)
由我们之前探索dyld加载流程的思路:掌握主线。将if else等分支代码全部折叠起来,可以看到,共有的特性:ts.log()打印没段代码的作用,如图所示:
012-iOS底层原理-类的加载 - 图22
因此,我们得到如下过程,我们将逐步探索这10个过程:
012-iOS底层原理-类的加载 - 图23
######2.1 、doneOnce条件控制执行一次的加载
doneOnce的定义是static bool doneOnce;,静态变量,在if (!doneOnce) {内设置为doneOnce = YES;因此只走一次。
1)disableTaggedPointers()为禁用所有TaggedPointers,其内部实现为:
  1. static void disableTaggedPointers()
  2. {
  3. objc_debug_taggedpointer_mask = 0;
  4. objc_debug_taggedpointer_slot_shift = 0;
  5. objc_debug_taggedpointer_slot_mask = 0;
  6. objc_debug_taggedpointer_payload_lshift = 0;
  7. objc_debug_taggedpointer_payload_rshift = 0;
  8. objc_debug_taggedpointer_ext_mask = 0;
  9. objc_debug_taggedpointer_ext_slot_shift = 0;
  10. objc_debug_taggedpointer_ext_slot_mask = 0;
  11. objc_debug_taggedpointer_ext_payload_lshift = 0;
  12. objc_debug_taggedpointer_ext_payload_rshift = 0;
  13. }

2)initializeTaggedPointerObfuscator()随机初始化 objc_debug_taggedpointer_obfuscator。标记指针混淆器旨在使攻击者更难将特定对象构造为标记指针,在存在缓冲区溢出或其他写入控制的情况下记忆。混淆器在设置时与标记指针异或或检索有效载荷值。他们首先充满了随机性采用。
总而言之,这个函数就是为了小对象类型的一些处理,初始化小对象类型(NSNumber、NSString都是有小对象组成的对象,存放在常量区,并且占用空间非常的小。),主要对**小对象通过mask做一些混淆**
参考文章
3)gdb_objc_realized_classes实际上是NXMapTable类型的哈希表,包含了不在 dyld 共享缓存中的被命名的类,这些类不管是否被实现。此表不包括 必须使用 getClass查找的 被懒加载命名的类。
换句话说,gdb_objc_realized_classes相当于一个总表。而在_objc_init函数中,runtime_init里初始化的allocatedClasses表,是一张已经初始化好的类和元类的表。
也就是说:**gdb_objc_realized_classes**包含**allocatedClasses**
这张总表所开辟的内存大小,是在总类数量的4/3倍4/3NXMapTable的加载因子。这是为了配合前面cache_t扩容的3/4负载因子。
######2.2、修复预编译阶段的@selector混乱问题
我们知道SEL是由名字+地址组成的,因此匹配两个SEL,需要对比名字+地址。否则可判定为不相等。
源码如下:

  1. // Fix up @selector references
  2. static size_t UnfixedSelectors;
  3. {
  4. mutex_locker_t lock(selLock);
  5. for (EACH_HEADER) {
  6. if (hi->hasPreoptimizedSelectors()) continue;
  7. bool isBundle = hi->isBundle();
  8. SEL *sels = _getObjc2SelectorRefs(hi, &count);
  9. UnfixedSelectors += count;
  10. for (i = 0; i < count; i++) {
  11. const char *name = sel_cname(sels[i]);
  12. SEL sel = sel_registerNameNoLock(name, isBundle);
  13. if (sels[i] != sel) {
  14. sels[i] = sel;
  15. }
  16. }
  17. }
  18. }
  19. ts.log("IMAGE TIMES: fix up selector references");

我们在objc工程中UnfixedSelectors代码块打上几个断点,如图所示。运行后用lldb调试,结果如下:
012-iOS底层原理-类的加载 - 图24
1、sel来自于sel_registerNameNoLock() -> __sel_registerName() ->search_builtins() -> _dyld_get_objc_selector()。换句话说就是sel来自于dyld加载出来的。
2、sels来自于Mach-O文件里的__objc_selrefs,即:_getObjc2SelectorRefs -> __objc_selrefs
两个sel来源不同,会导致同名不同地址的情况。因此,需要对这些selectors进行fix up。
######2.3、错误混乱的类处理
1、从MachO文件中字段__objc_classlist获取所有类列表,然后 通过readClass得到相应的类。
2、走完for循环,发现if (newCls != cls && newCls) {}并未进入。原因是:如果readClass的结果newClas与列表中的cls不同,则进行修复操作,但这一般不会出现,只有类被移动并且没有被删除才会出现。
3、lldb调试012-iOS底层原理-类的加载 - 图25由图可知,从MachO中获取的类,未通过readClass时,只有一个地址,并未关联到相应的类名。通过readClass之后,关联上了相应的类名。并且得到的newCls与原始的cls名字+地址都一致。
######2.4、修复重映射一些没有被镜像文件加载进来的类
将未映射的类和父类重映射,其中被重映射的类都是非懒加载的类。此代码块一般情况下是不会被执行。
012-iOS底层原理-类的加载 - 图26
######2.5、修复一些消息
通过读取MachO文件的__objc_msgrefs字段,通过fixupMessageRef函数进行修复,如如alloc -> objc_alloc、allocWithZone -> objc_allocWithZone 等,内部如下:
012-iOS底层原理-类的加载 - 图27
__sel_registerName注册方法名,内部源码如下:

  1. static SEL __sel_registerName(const char *name, bool shouldLock, bool copy)
  2. {
  3. SEL result = 0;
  4. if (shouldLock) selLock.assertUnlocked();
  5. else selLock.assertLocked();
  6. if (!name) return (SEL)0;
  7. // 从dyld里查找,有该name就返回
  8. result = search_builtins(name);
  9. if (result) return result;
  10. conditional_mutex_locker_t lock(selLock, shouldLock);
  11. // 将name插入方法表namedSelectors
  12. auto it = namedSelectors.get().insert(name);
  13. if (it.second) {
  14. // No match. Insert.
  15. *it.first = (const char *)sel_alloc(name, copy);
  16. }
  17. return (SEL)*it.first;
  18. }
2.6、修复protocol引用,并 readProtocol
通过读取MachO__objc_protolist字段,将得到的protolist存入到protocol_map哈希表中。
如果这是来自共享缓存的image镜像,则跳过读取协议。请注意,启动后我们确实需要遍历协议,因为共享缓存中的协议用 isCanonical()标记,如果选择某些非共享缓存二进制文件作为规范定义,则可能不是这样。
012-iOS底层原理-类的加载 - 图28
readProtocol()源码如下:
  1. static void
  2. readProtocol(protocol_t *newproto, Class protocol_class,
  3. NXMapTable *protocol_map,
  4. bool headerIsPreoptimized, bool headerIsBundle)
  5. {
  6. // This is not enough to make protocols in unloaded bundles safe,
  7. // but it does prevent crashes when looking up unrelated protocols.
  8. auto insertFn = headerIsBundle ? NXMapKeyCopyingInsert : NXMapInsert;
  9. protocol_t *oldproto = (protocol_t *)getProtocol(newproto->mangledName);
  10. if (oldproto) {
  11. if (oldproto != newproto) {
  12. 如果我们是一个共享缓存二进制文件,那么我们就有了这个协议的定义,但是如果选择了另一个,那么我们需要清除我们的 isCanonical 位,以便没有人信任它。
  13. 如果 getProtocol 返回共享缓存协议,则规范定义已经在共享缓存中,我们不需要做任何事情。
  14. if (headerIsPreoptimized && !oldproto->isCanonical()) {
  15. // Note newproto is an entry in our __objc_protolist section which
  16. // for shared cache binaries points to the original protocol in
  17. // that binary, not the shared cache uniqued one.
  18. auto cacheproto = (protocol_t *)
  19. getSharedCachePreoptimizedProtocol(newproto->mangledName);
  20. if (cacheproto && cacheproto->isCanonical())
  21. cacheproto->clearIsCanonical();// 清除isCanonical 位
  22. }
  23. }
  24. }
  25. else if (headerIsPreoptimized) {
  26. 共享缓存初始化了协议对象本身,但为了允许缓存外替换,需要将其添加到协议表中。
  27. protocol_t *cacheproto = (protocol_t *)
  28. getPreoptimizedProtocol(newproto->mangledName);
  29. protocol_t *installedproto;
  30. if (cacheproto && cacheproto != newproto) {
  31. // Another definition in the shared cache wins (because
  32. // everything in the cache was fixed up to point to it).
  33. installedproto = cacheproto;
  34. }
  35. else {
  36. // This definition wins.
  37. installedproto = newproto;
  38. }
  39. ......省略代码......
  40. insertFn(protocol_map, installedproto->mangledName,
  41. installedproto);
  42. }
  43. else {
  44. 未预优化镜像的新协议。将其固定到位。修复可卸载包中的重复协议
  45. newproto->initIsa(protocol_class); // fixme pinned
  46. insertFn(protocol_map, newproto->mangledName, newproto);
  47. }
  48. }
2.7、修复没有被加载的协议
如图所示:remapProtocolRef()未执行
012-iOS底层原理-类的加载 - 图29
remapProtocolRef()函数如下,通过remapProtocol()函数,重新映射得到新的newproto,再与protoref比较,将newproto赋值给*protoref
  1. static void remapProtocolRef(protocol_t **protoref)
  2. {
  3. runtimeLock.assertLocked();
  4. protocol_t *newproto = remapProtocol((protocol_ref_t)*protoref);
  5. if (*protoref != newproto) {
  6. *protoref = newproto;
  7. UnfixedProtocolReferences++;
  8. }
  9. }
2.8、分类处理
仅在完成初始化分类后才执行此操作。对于启动时出现的分类,被推迟到_dyld_objc_notify_register 调用完成后的第一个load_images 调用。即loadAllCategories();
源码如下:
  1. if (didInitialAttachCategories) {
  2. for (EACH_HEADER) {
  3. load_categories_nolock(hi);
  4. }
  5. }
2.9、类的加载处理 (重点)
主要是实现类的加载处理,加载非懒加载类。流程如下:
1、通过nlclslist()函数从MachO文件中的__objc_nlclslist字段获取classlist类表。
即:nlclslist()—>_getObjc2NonlazyClassList()—>MachO的__objc_nlclslist
  1. classref_t const *classlist = hi->nlclslist(&count);

2、遍历classlist将class重新映射,得到的新class和metaClass插入类表中。

  1. addClassTableEntry(cls);

012-iOS底层原理-类的加载 - 图30

3、通过realizeClassWithoutSwift(cls, nil);实现类。
cls 执行第一次初始化,包括分配其读写(r w)数据,因为前面的readClass只读取了类的名字和地址,并未读取r w数据,因此在此读取。不执行任何 Swift 端初始化,最终返回类的真实类的结构。
######2.10 、没有被处理的类 优化那些被侵犯的类
实现新解析的未来类,以防 CF 操作这些类。
在2.3中,resolvedFutureClasses被赋值,但我们通过调试,可知前面的赋值并未执行。因此,此处的resolvedFutureClasses为空。只有第2.3步的resolvedFutureClasses执行赋值操作后,此处才会在这步处理这些未来类。012-iOS底层原理-类的加载 - 图31
####3、(核心重点分析) readClass
在2.3步骤中,从Macho读取__objc_classlist字段的类表后,遍历此classlist,通过readClass()读取类并加入到类表、内存中。其中readClass得到的是类的名称和地址,类的内容在此时并没有配置。
进入readClass内部,源码如下:
012-iOS底层原理-类的加载 - 图32由上图的红色字体和方框注释,将readClass简化后的代码如下:012-iOS底层原理-类的加载 - 图33
1、从ro中读取到类名;
2、addNamedClass()类名插入到哈希表中(gdb_objc_realized_classes,前面提到的,该表存放所有类);
3、addClassTableEntry()类和元类插入到哈希表中(allocatedClasses,前面提到的,该表在_objc_init中的runtime_init创建的表中,该表存放已经创建的类)。
由于readClass是在for循环中调用的,即从MachO中读取到的classlist遍历操作readClass,因此除了我们自定义的类之外,还会有很多系统的类。我们将其打印出来。源码以及打印结果如下:

  1. Class readClass(Class cls, bool headerIsBundle, bool headerIsPreoptimized)
  2. {
  3. const char *mangledName = cls->nonlazyMangledName();
  4. printf("---- %s----%s\n",__func__,mangledName);
  5. ---------省略-后面代码--------
  6. }

012-iOS底层原理-类的加载 - 图34
由上图打印结果可以看到,我们自定义的类名出现在了打印的最后。我们只需要知道类的加载过程,系统类太复杂,不利于我们添加断点停下,因此并非我们的首选。我们的思路是通过我们自定义的类的加载来探索,因此,我们只需要判断mangledNameQLPerson相等的时候,停下来。即可查看变量的值以及lldb调试。代码设计如下:加入了strcmp函数,将断点添加进来,并在每一个if处打上断点。

  1. Class readClass(Class cls, bool headerIsBundle, bool headerIsPreoptimized)
  2. {
  3. const char *mangledName = cls->nonlazyMangledName();
  4. const char *customClsName = "QLPerson";
  5. int cmpResult = strcmp(mangledName, customClsName);
  6. if (cmpResult == 0) {
  7. printf("---- %s----%s\n",__func__,mangledName);
  8. }
  9. ---------省略-后面代码--------
  10. }

断点停下后,Xcode点击Step over,再一次验证了不在此处设置类的rw 、ro。
1、断点来到addNamedClass(未执行),此时的Class只有一个地址012-iOS底层原理-类的加载 - 图35
2、断点执行addNamedClass(执行完毕)。012-iOS底层原理-类的加载 - 图36
3、断点执行到addClassTableEntry,将cls和元类插入表中。012-iOS底层原理-类的加载 - 图37
####4、(核心重点分析) realizeClassWithoutSwift
上面第3步read_class加载的是类名+地址。realizeClassWithoutSwift则是加载类的data,配置ro,rw等内容。我们将通过断点调试,来探索这其中的流程。
#####【4.1】、加载本类data,设置ro,rw
由于我们只需要探索我们自定义的类,因此在realizeClassWithoutSwift()函数内,我们加入了判断mangledName = QLPerson,让断点停在此处。进一步lldb调试ro,rw,等内容。我们所要探索的类的内容,请参考006—iOS底层 - 类的结构(属性、成员变量、方法的探索)。包括属性,成员变量,方法,cache等。
012-iOS底层原理-类的加载 - 图38
调试结果如下:
1)属性/成员变量:012-iOS底层原理-类的加载 - 图39
2)方法:012-iOS底层原理-类的加载 - 图40
打印方法发现打印不出来。继续往下走。
#####【4.2】递归实现父类,元类完善继承链和isa走向
如果父类和元类还没有被实现,则递归调用realizeClassWithoutSwift()去实现父类和元类。

  1. supercls = realizeClassWithoutSwift(remapClass(cls->getSuperclass()), nil);
  2. metacls = realizeClassWithoutSwift(remapClass(cls->ISA()), nil);

实现了父类和元类后,并设置是否支持Non-pointer isa ,将他们保存。

  1. // Update superclass and metaclass in case of remapping
  2. cls->setSuperclass(supercls);
  3. cls->initClassIsa(metacls);
  4. ....省略代码......
  5. 此处要用递归的视角去看待,将继承链完善。
  6. if (supercls) {
  7. addSubclass(supercls, cls);
  8. } else {
  9. addRootClass(cls);
  10. }
【4.3】配置类的方法:methodizeClass
在上面的4.1步骤中,我们未能打印method,methodizeClass函数即为配置类的方法。
012-iOS底层原理-类的加载 - 图41
######【4.3.1】预处理方法列表:prepareMethodLists
prepareMethodLists源码中,最主要的是对方法列表的修复,遍历addedLists,调用fixupMethodList函数
012-iOS底层原理-类的加载 - 图42
######【4.3.2】修复方法列表:fixupMethodList
此函数是遍历方法列表,把方法名设置后,对方法进行排序:
a)meth.setName(sel_registerNameNoLock(name, bundleCopy));实际上是调用了__sel_registerName(),也就是我们前面的_read_images第2.5步,修复objc_msgSend重定向的时候提到的地方。
012-iOS底层原理-类的加载 - 图43
调试结果如下:012-iOS底层原理-类的加载 - 图44
由此可见,方法的排序,并非以名字排序,而是以地址排序。

5、总结
【5.1】类的加载(本类)流程图如下:012-iOS底层原理-类的加载 - 图45
【5.2】分类(category)的加载将在下一篇讲解
【5.3】此流程为非懒加载类的流程,即在测试类QLPerson中实现了+load方法,在map_images中加载所有类的数据。
若是未实现+load方法,则在实现类的函数realizeClassWithoutSwift的流程如下:lookUpImpOrForward->realizeClassMaybeSwiftMaybeRelock->realizeClassWithoutSwift->methodizeClass
两者之间的差异,如图所示:012-iOS底层原理-类的加载 - 图46