iOS 底层探索篇 ——类的加载原理(下)

  • 1. 为什么要有ro,rw,rwe
  • 2. cls->data()
  • 3. extAllocIfNeeded & attachCategories
  • 4.分类的懒加载
  • 类和分类都实现load
  • 类实现load和分类不实现load
  • 类不实现load和分类实现load
  • 类和分类都不实现load
  • 5. 4种不同情况下的分类加载
  • 类和分类都实现load
  • 类不实现load,分类实现load
  • 类实现load,分类不实现load
  • 类和分类都不实现load
  • 6.多个分类的加载情况
  • 分类全都实现load
  • 分类不全都实现load


1. 为什么要有ro,rw,rwe

  • ro属于cleanmemory,在编译即确定的内存空间,只读,加载后不会改变内容的空间
  • rw属于dirtymemory,rw是运行时结构,可读可写,可以向类中添加属性、方法等,在运行时会改变的内存;
  • rwe相当于类的额外信息,因为在实际使用过程中,只有很少的类会真正的改变他们的内容,所以为避免资源的消耗就有了rwe;

ro被复制一份到rw里面,这是因为在运行时分类可以添加方法,而程序员也可以动态添加方法或者属性到类里面,而ro是只读的,所以需要在rw里面来追踪这些东西。
不是每一个类都会动态添加,所以如果这片内存写在rw里面,那么就会对脏内存有影响,所以把这些东西放在rwe里面。

2. cls->data()

realizeClassWithoutSwift中的ro时怎么来的呢?查找到赋值的地方。

iOS开发 内存暴涨 ios内存释放底层原理_swift


iOS开发 内存暴涨 ios内存释放底层原理_xcode_02


发现返回的是一个地址指针,而指针可以进行取值和强转。这里是从macho里面获取地址指针,然后对class_ro_t的数据进行赋值。

3. extAllocIfNeeded & attachCategories

回到methodizeClass里面查看,看到rwe则是等于rw->ext()

iOS开发 内存暴涨 ios内存释放底层原理_xcode_03


iOS开发 内存暴涨 ios内存释放底层原理_xcode_04


点进去查看实现,发现下面一个方法extAllocIfNeeded,如果有这个方法就会有rwe

iOS开发 内存暴涨 ios内存释放底层原理_iOS开发 内存暴涨_05


在源码中查找extAllocIfNeeded,发现在attachCategories里面有调用。也就是在添加分类的时候,rwe是必然有值的。同时在class_setVersionaddMethods_finishclass_addProtocol_class_addPropertyobjc_duplicateClass的时候也是有调用的。这说明在运行时进行处理的时候,才会进行rwe的创建。

iOS开发 内存暴涨 ios内存释放底层原理_xcode_06

iOS开发 内存暴涨 ios内存释放底层原理_xcode_07


iOS开发 内存暴涨 ios内存释放底层原理_xcode_08


iOS开发 内存暴涨 ios内存释放底层原理_objective-c_09


现在来看attachCategories,因为需要研究分类时如何写到类里面的。在源码中搜索attachCategories,发现在attachToClassload_categories_nolock里面有调用。

iOS开发 内存暴涨 ios内存释放底层原理_swift_10


iOS开发 内存暴涨 ios内存释放底层原理_iOS开发 内存暴涨_11


load_categories_nolock比较陌生,而attachToClass在methodizeClass里面有看到过,所以来看一下attachToClass。

看到这里有一个地方判断previously来看是否进入调用attachToClass。

iOS开发 内存暴涨 ios内存释放底层原理_xcode_12

看一下previously是哪里来的。发现是methodizeClass的第二个参数。

iOS开发 内存暴涨 ios内存释放底层原理_iOS开发 内存暴涨_13


接下来搜索methodizeClass的调用。发现在realizeClassWithoutSwift里面。

iOS开发 内存暴涨 ios内存释放底层原理_xcode_14


又看到previously是realizeClassWithoutSwift的第二个参数。

iOS开发 内存暴涨 ios内存释放底层原理_ios_15


接下来寻找realizeClassWithoutSwift的调用。发现传的第二个参数全是nil,那么就代表previously是一个备用参数,为了方便内部进行相关的调节。

既然previously是nil,那么上面点函数就不会进去,也就是只会调用下面点attachToClass。

iOS开发 内存暴涨 ios内存释放底层原理_ios_16

接下来看attachCategories里面的attachLists。先看中间最短的地方。这里表示如果没有list而且attachLists里面就一个元素,那么list就是一个只有这个元素的一位数组。

iOS开发 内存暴涨 ios内存释放底层原理_xcode_17


再来看下面的else 的情况,这里做的是重新创建一个count为新list.count + 1 的数组(有oldlist的情况下,否则为0),将之前的一整个数组放到新数组的最后一位,然后将要添加的数组的元素从新数组的头部开始依次添加进去。

iOS开发 内存暴涨 ios内存释放底层原理_xcode_18


iOS开发 内存暴涨 ios内存释放底层原理_objective-c_19


再来看最后一个部分,这里做的就是重新创建一个count为oldList的count个数加addedList的count个数的数组,先将oldList的元素依次从i+addedCount位添加进去,然后将addedList的依次从头开始添加进去。

iOS开发 内存暴涨 ios内存释放底层原理_swift_20


iOS开发 内存暴涨 ios内存释放底层原理_objective-c_21

4.分类的懒加载

之前说到类有懒加载并且由load方法来控制,那么分类是否也有懒加载呢?并且分类的懒加载和类的懒加载是否有关系呢?来探索一下。

类和分类都实现load

先来看类和分类都实现load的情况,在attachCategories打下断点并运行。

iOS开发 内存暴涨 ios内存释放底层原理_swift_22


运行后发现会来到_read_images的非懒加载区域

iOS开发 内存暴涨 ios内存释放底层原理_ios_23


然后来到realizeClassWithoutSwift。

iOS开发 内存暴涨 ios内存释放底层原理_iOS开发 内存暴涨_24


然后来到methodizeClass里面调用attachToClass。

iOS开发 内存暴涨 ios内存释放底层原理_xcode_25


在往下就来到了attachCategories

iOS开发 内存暴涨 ios内存释放底层原理_iOS开发 内存暴涨_26


所以调用流程如下:

map_images -> map_images_nolock -> _read_images -> readClass -> realizeClassWithoutSwift -> methodizeClass -> attachToClass -> load_images -> loadAllCategories -> load_categories_nolock -> load_categories_nolock -> attachCategories -> attachLists

类实现load和分类不实现load

把分类的load方法去掉后重新运行,发现不会实现attachCategories方法了。

iOS开发 内存暴涨 ios内存释放底层原理_swift_27

类不实现load和分类实现load

把分类的load方法去掉后重新运行,发现和上面的情况一样不会实现attachCategories方法了。

iOS开发 内存暴涨 ios内存释放底层原理_ios_28

类和分类都不实现load

运行后发现直接到main函数里面了。

5. 4种不同情况下的分类加载

类和分类都实现load

实现类和分类的load方法后运行,进入realizeClassWithoutSwift后输入ro查看分类数据是否被加载进去,如果有的话那么就说明在realizeClassWithoutSwift分类就已经被加载进去了,否则就在realizeClassWithoutSwift之后。

输出所有的baseMehtods之后发现,没有分类的方法,那么说明分类还没有被加载进去。

iOS开发 内存暴涨 ios内存释放底层原理_objective-c_29

iOS开发 内存暴涨 ios内存释放底层原理_ios_30


iOS开发 内存暴涨 ios内存释放底层原理_objective-c_31


接下来看load_categories_nolock里面,输出cat看一下,确实是我们创建的分类。

iOS开发 内存暴涨 ios内存释放底层原理_swift_32

在输出cls 看一下发现确实是LGPerson。

iOS开发 内存暴涨 ios内存释放底层原理_ios_33


接下来往下走发现调用attachCategories的地方,这里因为cls已经在read_images时候加载过了所以cls->isRealized()为true。

iOS开发 内存暴涨 ios内存释放底层原理_iOS开发 内存暴涨_34


看到attachCategories里面,看到mlist的数据结构,且里面count为2,也就是分类里面的2个方法。

iOS开发 内存暴涨 ios内存释放底层原理_objective-c_35


在往下走,发现把mlist放到了mlists里的最后一位。

iOS开发 内存暴涨 ios内存释放底层原理_swift_36


iOS开发 内存暴涨 ios内存释放底层原理_objective-c_37


iOS开发 内存暴涨 ios内存释放底层原理_objective-c_38

iOS开发 内存暴涨 ios内存释放底层原理_xcode_39


往下走,跳过中间的属性和协议,就来到了这里。看到这里调用prepareMethodLists进行了排序,然后往rwe的methods调用attachLists添加了分类方法。

iOS开发 内存暴涨 ios内存释放底层原理_xcode_40


进去看attachLists。这里看到addedLists是个二级指针。

iOS开发 内存暴涨 ios内存释放底层原理_iOS开发 内存暴涨_41


然后往下走,看到进入了else。

iOS开发 内存暴涨 ios内存释放底层原理_ios_42


打印输出发现存入的都是指针。

iOS开发 内存暴涨 ios内存释放底层原理_swift_43


输出看一下发现这里的addedList[0]存的是分类

iOS开发 内存暴涨 ios内存释放底层原理_objective-c_44

类不实现load,分类实现load

没有进入attachCategories,也就是没有加载分类,但是加载类。

iOS开发 内存暴涨 ios内存释放底层原理_objective-c_45


这里类被迫营业了。分类是针对主类实现的,所以分类要实现,就要先实现类的。这里在realizeClassWithoutSwift里面输出ro的baseMethods,发现分类的方法也都加载了进去了,说明类和分类已经合到一起了,编译器已经将分类的数据放在 cls->data()。

iOS开发 内存暴涨 ios内存释放底层原理_xcode_46

iOS开发 内存暴涨 ios内存释放底层原理_ios_47


iOS开发 内存暴涨 ios内存释放底层原理_xcode_48


iOS开发 内存暴涨 ios内存释放底层原理_objective-c_49

类实现load,分类不实现load

没有进入attachCategories,也就是没有加载分类,但是加载类。

iOS开发 内存暴涨 ios内存释放底层原理_ios_50


这里在realizeClassWithoutSwift里面输出ro的baseMethods,发现分类的方法也都加载了进去了,说明类和分类已经合到一起了,编译器已经将分类的数据放在 cls->data(),

iOS开发 内存暴涨 ios内存释放底层原理_swift_51

类和分类都不实现load

运行后发现直接到main函数里面了,两者都懒加载。

iOS开发 内存暴涨 ios内存释放底层原理_xcode_52


这里在realizeClassWithoutSwift里面输出ro的baseMethods,发现分类的方法也都加载了进去了,说明类和分类已经合到一起了,编译器已经将分类的数据放在 cls->data(),

iOS开发 内存暴涨 ios内存释放底层原理_ios_53


也就是说,当类和分类都不实现load的时候,两者都懒加载,在类第一次收到消息的时候初始化。但是类和分类加载都已经在data里面了。

6.多个分类的加载情况

分类全都实现load

iOS开发 内存暴涨 ios内存释放底层原理_swift_54

添加一个分类,然后运行。看到两个分类都加载了进去。

iOS开发 内存暴涨 ios内存释放底层原理_objective-c_55


iOS开发 内存暴涨 ios内存释放底层原理_swift_56


接下来的流程和单个分类的加载情况一样,但是attachLists却不一样了。这里就加载了分类。

iOS开发 内存暴涨 ios内存释放底层原理_xcode_57

分类不全都实现load

运行后还是全都加载了。

iOS开发 内存暴涨 ios内存释放底层原理_swift_58


processCatlist是block,在下面调用。

iOS开发 内存暴涨 ios内存释放底层原理_swift_59


load_categories_nolock是由loadAllCategories调用,loadAllCategories则是由load_images里面调用,并且由didInitialAttachCategories和didCallDyldNotifyRegister决定调不调用,didInitialAttachCategories默认为false,而当loadAllCategories调用后,didInitialAttachCategories就为true,所以就只调用一次。

iOS开发 内存暴涨 ios内存释放底层原理_swift_60


iOS开发 内存暴涨 ios内存释放底层原理_iOS开发 内存暴涨_61


所以能加载到分类的原因是通过catList从macho里面读取到的。

一般来说,mach会加载整个数据结构,当调用load的时候,就会打乱算法来进行计算,所以说load耗时。正常来说,类和分类都会在macho里面加载好的了。