iOS 底层探索系列swift
- iOS 底层探索 - alloc & init
- iOS 底层探索 - calloc 和 isa
- iOS 底层探索 - 类
- iOS 底层探索 - cache_t
- iOS 底层探索 - 方法
- iOS 底层探索 - 消息查找
- iOS 底层探索 - 消息转发
- iOS 底层探索 - 应用加载
- iOS 底层探索 - 类的加载
- iOS 底层探索 - 分类的加载
- iOS 底层探索 - 类拓展和关联对象
- iOS 底层探索 - KVC
- iOS 底层探索 - KVO
iOS 查漏补缺系列缓存
上一章咱们探索了 iOS
中类的加载,让咱们简单回顾一下大概的流程。bash
libObjc
向 dyld
注册了回调 _dyld_objc_notify_register
,当 dyld
把 App
以及 App
所依赖的一系列 Mach-O
镜像加载到当前 App
被分配的内存空间以后,dyld
会经过 _dyld_objc_notify_mapped
也就是 map_images
来通知 libObjc
来完成具体的加载工做,map_images
被调用以后会来到 _read_images
_read_images
gdb_objc_realized_classes
哈希表中(插入方式为 类名为 key
,类对象为value
, 不包括经过 共享缓存 里面的类),同时还会把类插入到 allocatedClasses
这个集合里面,注意,allocatedClasses
的类型为 NXHashTable
,能够类比为 NSSet
,而 gdb_objc_realized_classes
的类型为 NXMapTable
,能够类比为 NSDictionary
SEL
插入到 namedSelectors
哈希表中(插入方式为:SEL
名称为 key
,SEL
为value
)Protocol
插入到 readProtocol
哈希表中(插入方式为:Protocol
名称为 key
,Protocol
为 value
)Protocol
作重映射rw
和 ro
的初始化操做咱们大体明白了类的加载流程,接下来,让咱们在 _read_images
源码中打印一下类加载以后的结果验证一下是否加载了咱们本身建立的类。markdown
如上图所示,咱们增长一行代码:app
printf("_getObjc2NonlazyClassList Class:%s\n",cls->mangledName()); 复制代码
接着咱们观察打印结果:函数
忘了提一句,咱们这一个有三个类: LGPerson
、 LGStudent
、 LGTeacher
oop
可是打印出来的结果没有 LGPerson
,这是为何呢?答案看这里,咱们实际上是在 LGStudent
和 LGTeacher
内部实现了 +load
方法。而 LGPerson
则是没有实现 +load
方法。post
咱们这个时候观察 _read_images
源码这部分的注释:学习
Realize non-lazy classes (for +load methods and static instances)this
实现非懒加载类(实现了
+load
方法和静态实例)
什么意思呢,咱们这里其实打印的都是所谓的非懒加载类,这里除了咱们本身实现了 +load
方法的两个类以外,其余的内容都是系统内置的类,包括咱们十分熟悉的 NSObject
类。经过这里其实反过来推论,咱们没有实现 +load
方法就是所谓的**懒加载类,这种类并不会在 ****_read_images**
环节被加载,那么应该是在哪里加载呢?咱们稍微思考一下,咱们通常第一次操做一个类是否是在初始化这个类的时候,而初始化类不就是发送 alloc
消息吗,而根据咱们前面探索消息查找的知识,在第一次发送某个消息的时候,是没有缓存的,因此会来到一个很是重要的方法叫 lookUpImpOrForward
,咱们在 main.m
中 LGPerson
类初始化的地方和 lookUpImpOrForward
入口处打上断点:
Tips: 这里有个小技巧,咱们先打开
main.m
文件中的断点,等断点来到了咱们想要探索的LGPerson
初始化的位置的时候,咱们再打开lookUpImpOrForward
处的断点,这样才能确保当前执行lookUpImpOrForward
的是咱们的研究对象LGPerson
由于咱们断点的位置是 LGPerson
类发送 alloc
消息,而显然 alloc
做为类方法是存储在元类上的,也就是说 lookUpImpOrForward
的 cls
实际上是 LGPerson
元类。那么 inst
就应该是真正的对象,可实际以下图所示:
此时的 inst
只是一个地址,说明尚未初始化。咱们让程序接着下面走,会来到这样一行代码:
这里的 if
判断经过方法名咱们不难看出是只有当 cls
未实现的时候才会走里面的 realizeClassMaybeSwiftAndLeaveLocked
方法,那也就是说 LGPerson
元类没有被实现,也就是 LGPerson
类没有实现或者说没有被加载。
咱们就顺着 realizeClassMaybeSwiftAndLeaveLocked
方法往下面走走看,看究竟是在哪把咱们这个懒加载类给加载出来的:
static Class realizeClassMaybeSwiftMaybeRelock(Class cls, mutex_t& lock, bool leaveLocked) { lock.assertLocked(); if (!cls->isSwiftStable_ButAllowLegacyForNow()) { // Non-Swift class. Realize it now with the lock still held. // fixme wrong in the future for objc subclasses of swift classes realizeClassWithoutSwift(cls); if (!leaveLocked) lock.unlock(); } else { // Swift class. We need to drop locks and call the Swift // runtime to initialize it. lock.unlock(); cls = realizeSwiftClass(cls); assert(cls->isRealized()); // callback must have provoked realization if (leaveLocked) lock.lock(); } return cls; } 复制代码
咱们一路跟随断点来到了 realizeClassMaybeSwiftMaybeRelock
方法,而后咱们看到了咱们熟悉的一个方法 realizeClassWithoutSwift
,这个方法内部会进行 ro/rw
的赋值操做以及 category
的 attatch
,关于这个方法更多内容能够查看上一篇文章。
接着咱们返回到 lookUpImpOrForward
方法中来,而后进行一下 LLDB
打印,看一下当前这个 inst
也就是 LGPerson
对象是否已经被加载了。
经过上面的打印,咱们能够看到 rw
已经有值了,也就是说 LGPerson
类被加载了。
咱们总结一下,若是类没有实现 load
方法,那么这个类就是懒加载类,其调用堆栈以下图所示:
反之、这个类若是实现了 load
方法,那么这个类就是非懒加载类,其调用堆栈以下图所示:
关于非懒加载类的加载流程咱们已经很熟悉了,咱们总结下懒加载类的流程:
_class_lookupMethodAndLoadCache3
,关于这个方法咱们在前面的消息查找章节已经介绍过了,不熟悉的同窗能够去查阅一下。_class_lookupMethodAndLoadCache3
会调用 lookUpImpOrForward
,这个方法的重要性在咱们学习 Runtime
的过程当中不言而喻lookUpImpOrForward
内部会进行一下判断,若是 cls
没有被实现,会调用 realizeClassMaybeSwiftAndLeaveLocked
方法realizeClassMaybeSwiftAndLeaveLocked
方法又会调用 realizeClassMaybeSwiftMaybeRelock
方法realizeClassMaybeSwiftMaybeRelock
方法内部会进行一下是不是 Swift
的判断,若是不是 Swift
环境的话,就会来到 realizeClassWithoutSwift
,也就是最终的类的加载的地方分类做为 Objective-C
中常见的特性,相信你们都不会陌生,不过在底层它是怎么实现的呢?
为了探究分类的底层实现,咱们只须要用 clang
的重写命令
clang -rewrite-objc LGTeacher+test.m -o category.cpp
复制代码
咱们查看 category.cpp
这个文件,来到文件尾部能够看到:
static struct _category_t _OBJC_$_CATEGORY_LGTeacher_$_test __attribute__ ((used, section ("__DATA,__objc_const"))) = { "LGTeacher", 0, // &OBJC_CLASS_$_LGTeacher, (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_LGTeacher_$_test, (const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_LGTeacher_$_test, 0, (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_LGTeacher_$_test, }; 复制代码
咱们能够看到 LGTeacher+test
分类在底层的实现是一个结构体,其名字为 _OBJC_$_CATEGORY_LGTeacher_$_test
,很明显这是一个按规则生成的符号,中间的 LGTeacher
是类名,后面的 test
是分类的名字。
咱们的分类如上图所示,定义了属性、实例方法和类方法,恰好在底层对应了
_OBJC_$_PROP_LIST_LGTeacher_$_test
_OBJC_$_CATEGORY_INSTANCE_METHODS_LGTeacher_$_test
_OBJC_$_CATEGORY_CLASS_METHODS_LGTeacher_$_test
同时,咱们在后面能够看到以下的代码:
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= { &_OBJC_$_CATEGORY_LGTeacher_$_test, }; 复制代码
这代表分类是存储在 __DATA
段的 __objc_catlist
section 里面的。
咱们根据 _category_t
来到 libObjc
源码中进行查找,不过咱们须要去掉一下 _category_t
的下划线,而后不难找到分类真正的定义所在:
struct category_t { const char *name; classref_t cls; struct method_list_t *instanceMethods; struct method_list_t *classMethods; struct protocol_list_t *protocols; struct property_list_t *instanceProperties; // Fields below this point are not always present on disk. struct property_list_t *_classProperties; method_list_t *methodsForMeta(bool isMeta) { if (isMeta) return classMethods; else return instanceMethods; } property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi); }; 复制代码
根据刚才 clang
重写以后的内容,咱们不难看出
name
: 是分类所关联的类,也就是类的名字,而不是分类的名字cls
: 咱们在前面能够看到 clang
重写后这个值为 0,可是后面有注释为 &OBJC_CLASS_$_LGTeacher
,也就是咱们的类对象的定义,因此这里其实就是咱们要扩展的类对象,只是在编译期这个值并不存在instanceMethods
: 分类上存储的实例方法classMethods
:分类上存储的类方法protocols
:分类所实现的协议instanceProperties
:分类所定义的实例属性,不过咱们通常在分类中添加属性都是经过关联对象来实现的_classProperties
:分类所定义的类属性。这里有一行注释:Fields below this point are not always present on disk. 下面的内容并非一直在磁盘上保存
也就是说 _classProperties
实际上是一个私有属性,但并非一直都存在的。
咱们如今知道了类分为了 懒加载类
和 非懒加载类
,它们的加载时机是不同的,那么分类的加载又是怎么样的呢?咱们仍是一样的先分析没有实现 load
方法的分类的状况:
可是咱们在分析前,还要搞清楚一点,分类必须依附于类而存在,若是只有分类,没有类,那么从逻辑上是说不通的,就算实现了,编译器也会忽略掉。而关于类是懒加载仍是非懒加载的,因此这里咱们还要再细分一次。
咱们先分析第一种状况,也就是类和分类都不实现 load
方法的状况。
首先,非懒加载类的流程上面咱们已经探索过了,在向类第一次发送消息的时候,非懒加载类才会开始加载,而根据咱们上一章类的加载探索内容,在 realizeClassWithoutSwift
方法的最后有一个 methodizeClass
方法,在这个方法里面会有一个 Attach categories
的地方:
可是咱们断点以后发现这个时候经过 unattachedCategoriesForClass
方法并无取到分类,咱们此时不妨经过 LLDB
打印一下当前类里面是否已经把分类的内容附加上了。
前面的流程你们都很熟悉了,咱们直接看 cls
的 rw
中的 methods
是否有内容:
此时 LGTeacher
类里面是没有方法的,这里读取 rw
却有一个结果,咱们不难看出这是位于 LGTeacher+test
分类中的一个 initialize
方法,这个方法是我手动加个这个分类的。这样进一步证实了,若是是懒加载类,而且分类也是懒加载,那么分类的加载并不会来到 unattachedCategoriesForClass
,而是直接在编译时加载到了类的 ro
里面,而后在运行时被拷贝到了类的 rw
里面。这一点能够经过下面的 LLDB
打印来证实。
若是细心的读者可能会发现,不是在 _read_images
的最后那块有一个 Discover categories
吗,万一懒加载分类是在这里加载的呢?咱们一试便知:
这里在 Discover categories
内部作了一下判断,若是是 LGTeacher
类进来了,就打印一下,结果发现并无打印,说明分类也不是在这里被加载的。
一样的道理,当类为非懒加载类的时候,走的是 _read_images
里面的流程,这个时候咱们的懒加载分类是在哪加载的呢?
咱们直接在 methodizeClass
方法中打上断点,并作了一下简单的判断:
const char *cname = ro->name; const char *oname = "LGTeacher"; if (strcmp(cname, oname) == 0) { printf("methodizeClass :%s \n",cname); } 复制代码
结果能够看到:
分类仍是不在这,同时经过 LLDB
打印,发现分类的方法已经在类的 ro
里面了,因此说分类的加载其实跟类的懒加载与否并无关系,也就是说懒加载的分类都是在编译时期被加载的。
咱们再接着分下下面两种状况:
其实懒加载和非懒加载的最大区别就是加载是否提早,而实现了 +load
方法的分类,面对的是懒加载的类,
而懒加载的类咱们前面已经知道了,是在第一次发送消息的时候才会被加载的,那咱们直接在lookupImpOrForward
=> realizeClassMaybeSwiftAndLeaveLocked
=> realizeClassMaybeSwiftMaybeRelock
=> realizeClassWithoutSwift
=> methodizeClass
流程中的 methodizeClass
打上断点,看下在这里分类会不会被加载:
这一次经过 unattachedCategoriesForClass
取出来值了,而且在这以前 cls
的 ro
中并无分类的 initialize
方法:
可是咱们注意观察此时的调用堆栈:
为何走的不是发送消息的流程,而走的是 load_images
里面的 prepare_load_methods
方法呢?咱们来到 prepare_load_methods
方法处:
能够看到,实际上是在这里调用了 realizeClassWithoutSwift
方法来加载类的。而上面的 _getObjc2NonlazyCategoryList
方法显示就是获取的全部的非懒加载分类,而后遍历这些非懒加载分类,而后去加载这些分类所依赖的类。这个逻辑很好理解,非懒加载分类让咱们的懒加载类实现提早了,因此说懒加载类并不必定只会在第一次消息发送的时候加载,还要取决于有没有非懒加载的分类,若是有非懒加载的分类,那么就走的是 load_images
里面的 prepare_load_methods
的 realizeClassWithoutSwift
。
非懒加载类的流程咱们也十分熟悉了,在 _read_images
里面进行加载,而此时,分类也是非懒加载。咱们仍是在 methodizeClass
里面进行断点:
结果如上图所示,此次从 unattachedCategoriesForClass
方法取出来的是 NULL
值,显然分类不是在这个地方被加载的,咱们回到 _read_images
方法,还记得那个 Discover categories
流程吗,咱们打开里面的断点:
由于当前类已经在前面的非懒加载类加载流程中被加载完成,因此这里会来到 remethodizeClass
方法,咱们进入其内部实现:
static void remethodizeClass(Class cls) { category_list *cats; bool isMeta; runtimeLock.assertLocked(); isMeta = cls->isMetaClass(); // Re-methodizing: check for more categories if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) { if (PrintConnecting) { _objc_inform("CLASS: attaching categories to class '%s' %s", cls->nameForLogging(), isMeta ? "(meta)" : ""); } attachCategories(cls, cats, true /*flush caches*/); free(cats); } } 复制代码
能够看到有一个 attachCategories
方法,断点也确实来到了这个地方, attachCategories
方法有一段注释:
// Attach method lists and properties and protocols from categories to a class. // Assumes the categories in cats are all loaded and sorted by load order, // oldest categories first.
将分类的方法、属性和协议添加到类上 假设传入的分类列表都是按加载顺序加载完毕了 先加载的分类排在前面
其实 attachCategories
这个方法只会在实现了 load
方法的分类下才会被调用,而来到 attachCategories
以前又取决于类是否为懒加载,若是是懒加载,那么就在 load_images
里面去处理,若是是非懒加载,那么就在 map_images
里面去处理。
咱们今天探索的内容可能会有点绕,不过其实探索下来,咱们只须要保持研究重点就很简单。分类的加载其实能够笼统的分为实现 load
方法和没有实现 load
方法:
load
方法的分类由编译时肯定load
方法的分类由运行时去肯定这也说明分类的加载和类的加载是不同的,而结合着类的懒加载与否,咱们有如下的结论:
类的加载在第一次消息发送的时候,而分类的加载则在编译时
类的加载在
_read_images
处,分类的加载仍是在编译时
类的加载在
load_images
内部,分类的加载在类加载以后的methodizeClass
![]()
类的加载在
_read_images
处,分类的加载在类加载以后的reMethodizeClass
![]()
分类的加载探索完了,咱们下一章将探索类拓展和关联对象,敬请期待~