前言
我们在上一篇文章类的加载原理-上已经解析了map_images下_read_images中readClass的分析,遗留了一些问题,因为至始至终我们没有看到类中rw、ro相关信息的读取处理,所以今天我们接着讲解后续类的加载问题。
_read_images(接加载原理上)
上篇文章中,我们对_read_images的解析只讲解了一部分,着重对readClass这个方法进行了分析,但_read_images真正对类详细的加载相关的方法我们还没触及,我们在需要调试的地方加上普通类测试:
这里调试我们观察到一个重要的方法realizeClassWithoutSwift(cls,nil)--类的实现,我们下面着重分析这个方法。
realizeClass
这里我们接着可断点调试进入realizeClassWithoutSwitf,这里还有重要的一点,我们此处分析的是non-lazy(非懒加载)类的实现,如果是lazy(懒加载)类你会发现断点调试不会进入我们自定义的调试代码中,至于non-lazy和lazy类究竟有什么区别,后面我们会分析。这里调试已做过相关处理,调试的non-lazy class的实现:
我们这里断点卡在取出ro(cleanMemory)后,lldb调试打印下:
经过调试验证,发现这里无法正常获取到ro中baseMethodList,按正常情况,应该是可以获取到的它的方法列表的,这里只能说明此时方法还没有被加载。我们看下realizeClassWithoutSwitf方法整体实现:
这个方法中,我们可以确认几个重点,不是元类的时候,ro(cleanMemory)被复制一份到rw中,说明此时rw已被赋值;还有继承链和isa的指向这里也得到了相关的验证;若是metaClass或者设置了环境变量DisableNonPointerIsa( or app sdk version),isa这时候就是一个pointer isa即只会对其中的shiftcls赋值,此时isa中只有类的内存地址,没有其他相关信息。关于non pointer isa中存储的相关字段在类原理分析上中有相关介绍。这里我们继续在realizeClassWithoutSwitf断点调试输出下ro中baseMethodlist,我们要排除掉元类,因为元类的名字跟类名一样:
此时已经经过ro和rw的赋值,但是还会无法取出ro中baseMethodlist。所以我们基本可以确定相关方法的处理是在methodizeClass进行了处理,下面我们看methodizeClass方法实现:
我们看到该方法中,有个处理methodlist的地方ro->baseMethods(),获取methodlist之后,调用prepareMethodLists()处理,我们进入prepareMethodLists:
我们接着看fixupMethodList(),看里面如何进行selectors修复的:
那fixupMethodList里主要是遍历methodlist,取出SEL,把对应的SEL和地址设置到结构体meth中,然后对方法进行排序,这里我们可以调试验证下:
我们可以看到排序后,方法按地址从小到大排过序,那我们这时候再回到methodizeClass方法,看看ro中baseMethodlist:
我们看到此时还是无法输出,而且此时类的信息也不完整,相对应的propertylist和protocollist还没有处理。还有一个问题我们看到prepareMethodLists之后下面有涉及rwe(rw extension)的处理,那rwe是在什么时候会赋值呢,因为我们之前的调试代码都没有进入这里,是直接跳过去执行的,所以这里后面我们也会分析。我们接下来先补充一下懒加载类和非懒加载类的一些问题。
懒加载类(lazy) & 非懒加载类(non-lazy)
非懒加载类其实就是实现了load方法,而懒加载类则没有实现load方法。还记得我们之前在_read_images里加的调试代码,CTPerson里我们是实现了load方法的:
若CTPerson类没有实现load方法,则开篇我们那里写的的调试代码就不会进入:
因为此时cls->nonlazyMangledName()获取到的只有non-lazy类名称。苹果使用lazy类主要是为了节约内存,提高加载效率,因为我们的应用程序一次加载就有成千上百的类,但其实我们没有必要一次性就把它们全部加载出来,大部分的类我们只需要在使用的时候再去加载它,那么lazy类就是这种类,我们平常写的大多数类都是lazy类。
我们之前调试的地方是non-lazy类执行的地方,它大概的执行流程是_read_images——>readClass——>realizeClassWithoutSwitf——>methodizeClass——>prepareMethodLists,那若没有是实现load方法的,做为lazy类在哪里加载的呢,我们在realizeClassWithoutSwitf加上调试代码,无论是lazy还是non-lazy类都会进入该方法中:
这里我们可很清晰看到它的调用流程:main ——> objc_alloc ——> callAlloc ——> _objc_msgSend_uncahced(缓存没有找到) ——> lookUpImpOrForward(慢速查找) ——> realizeAndInitializeIfNeeded_locked ——> realizeClassMaybeSwiftAndLeaveLocked ——> realizeClassMaybeSwiftMaybeRelock ——> realizeClassWithoutSwift,这样我们就非常清楚的知道,这里是通过调用lookUpImpOrForward,走慢速查找那套流程来到了realizeClassWithoutSwift。所以它不是一开始就加载的,而是alloc方法触发了此方法的调用,我们看main方法里,此时代码也停留在调用alloc方法处:
所以我们基本可以确定,lazy(懒加载)类的加载是在第一次消息发送的时候。这里我们画图标注下lazy(懒加载)和non-lazy(非懒加载)类的加载的简洁流程:
分类的本质探索
我们平常开发的时候,应该会经常用到分类,那么我们也有想过对象是如何找到分类里的方法并调用他们呢,这里我们首先创建个CTPerson的分类CT,然后clang编译看下它的cpp文件:
clang后的.cpp文件:
上图是从main.cpp文件截取的图,可以看到CTPerson的分类CT,分类经过clang编译变成了结构体_category_t:
这里我们可以看到,分类的对象方法和类方法都在这里,这是因为分类没有元类。我们其实进入objc源码中可以看到相关处理,最终对象方法会被放到类的方法列表中,而分类中的类方法则添加到元类的方法列表中,后面这里我们还会进行详细的分析:
我们在看下
_category_t各个成员在这里的的赋值:
我们发现_category_t中的name此时是CTPerson,这是因为此时是静态编译,程序并没有运行起来,运行时还没有执行,所以此时的cls也是0。我们再看下method_list_t:
我们可以看到,我们声明的几个方法都在,但是没有几个属性的set和get方法,分类的set和get方法,只能通过关联对象的方式来添加。
分类加载的引入
这里接着分析上面遗留的问题,rwe是在什么时候会赋值呢,我们看下它赋值的地方:
我们可以在methodizeClass方法中看到,获取rwe是通过rw调用ext()方法,我们进入ext(),这里我们看到一个关键的方法extAllocIfNeeded():
接着我们全局搜索下extAllocIfNeeded方法,看看它被调用的地方,我们看到第一个调用的地方attachCategories——添加分类的时候,rwe有被赋值:
第二处调用的地方addMethods_finish:
还有class_addProtocol、_class_addProperty、objc_duplicateClass、class_setVersion等处,我们发现都是在运行时执行的时候涉及,这也就验证了我们之前wwdc2020视频中的提及的runtime运行时执行时才涉及对rwe的处理。这里呢,我们重点关注下attachCategories,看看哪些地方调用attachCategories:
我们看到主要两处调用attachCategories,分别是在attachToClass、load_categories_nolock两处,那么我们后面就可以针对这两个方法反向推导到我们之前流程熟悉的地方,后续篇章我们会针对这里进行继续推导分析。