selector

iOS底层之分类的加载原理

2020-10-17  本文已影响0人  大橘猪猪侠

在上一篇文章中探索过iOS类的加载,也探索了部分分类的加载;那么本篇文章将继续接着继续探索分类的加载。

在上一篇文章中,我们在attachCategories方法中探究到了在分类进行添加之后,才会对rwe进行赋值和初始化的,那么我们就从rwe开始赋值的操作继续探索。

在探索过程中,我们需要带着问题去探索,第一个问题就是分类时如何加载到类中去的?

首先看下图,是attachCategories的一部分代码,在rwe初始化之后,rwe就有值了,那么在attachLists是怎么把方法添加到里面去的呢?

下图是attachLists方法,它分三种情况:
第一种情况是第二方框,当addedLists来了之后,将addedLists的第一个元素赋值给list,也就是说,当list不存在时,就会从0-1;
第二种情况是第三个方框的代码,当有list之后,加入很多个list,总的意思就是将新加入的放在前面,旧的数据放在后面。它这样的实现是一种算法思维,LRU算法,也就是说当分类和主类中的方法存在一样时,就加载主类的方法。
第三种情况是加载多个分类方法,它的功能是先扩容,然后将后加入的分类放在前面。

iShot2020-10-17 13.53.52.png

下面我们去通过代码去验证一下:
首先我们根据rwe的初始化代码去研究,在代码中添加我们对自己的类的判断:
attachCategories方法中的auto rwe = cls->data()->extAllocIfNeeded();行代码去查看extAllocIfNeeded()方法中的extAlloc

iShot2020-10-17 14.22.32.png

当程序进入到这个判断中,就证明是我们自己添加的类;下面我们来看一下list的变化:

在刚开始的时候,list是空的,没有任何值;

(lldb) p rwe
(class_rw_ext_t *) $0 = 0x0000000101130a10
(lldb) p *$0
(class_rw_ext_t) $1 = {
  ro = 0x0000000000000000
  methods = {
    list_array_tt<method_t, method_list_t> = {
       = {
        list = 0x0000000000000000
        arrayAndFlag = 0
      }
    }
  }
  properties = {
    list_array_tt<property_t, property_list_t> = {
       = {
        list = 0x0000000000000000
        arrayAndFlag = 0
      }
    }
  }
  protocols = {
    list_array_tt<unsigned long, protocol_list_t> = {
       = {
        list = 0x0000000000000000
        arrayAndFlag = 0
      }
    }
  }
  demangledName = 0x0000000000000000
  version = 0
}
iShot2020-10-17 14.32.56.png

而当我们执行完method_list_t *list = ro->baseMethods();行代码后,再去获取list的值:

(lldb) p list
(method_list_t *) $2 = 0x00000001000081c8
(lldb) p *$2
(method_list_t) $3 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 8
    first = {
      name = "kc_instanceMethod1"
      types = 0x0000000100003f73 "v16@0:8"
      imp = 0x0000000100003aa0 (KCObjc`-[Person kc_instanceMethod1])
    }
  }
}
(lldb) 

可以看到它现在加载的是本类的rw;
下面继续进入到attachLists中去看看是如何进行加载的:

iShot2020-10-17 14.36.38.png

当程序进入attachLists方法,它首先执行第一步的方法,将list赋值为addedLists的第1个元素;
下面我们来看看他的值是什么:

(lldb) p addedLists
(method_list_t *const *)【指针地址】 $4 = 0x00007ffeefbf63f8
(lldb) p addedLists[1]
(method_list_t *const) $5 = 0x00000001003419dd
(lldb) p 0x00000001003419dd
(long) $6 = 4298381789
(lldb) p *$5
(method_list_t) $7 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 1936876880
    count = 620785263
    first = {
      name = <no value available>
      types = 0xa0e781a6e89188e6 ""
      imp = 0x20849ae7b6a9e794 (0x20849ae7b6a9e794)
    }
  }
}
(lldb) p addedLists[0]
(method_list_t *const) $8 = 0x00000001000081c8
(lldb) p *$8
(method_list_t) $9 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 8
    first = {
      name = "kc_instanceMethod1"
      types = 0x0000000100003f73 "v16@0:8"
      imp = 0x0000000100003aa0 (KCObjc`-[Person kc_instanceMethod1])
    }
  }
}

addedLists的第0个,第1个都打印了一下,都有值,因为地址是连续的,第一个可能是别人的内容。那么本类的方法就加载完毕,现在去看看分类的加载:

继续执行方法,回道attachCategories方法,在程序断点执行到下图时:

iShot2020-10-17 14.51.08.png

我们去看一下mlist的值(这个如果执行的方法不是这个,可以清除缓存重新试一下),可以看到是分类的方法:

(lldb) p mlist
(method_list_t *) $0 = 0x0000000100008038
(lldb) p *$0
(method_list_t) $1 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 24
    count = 4
    first = {
      name = "kc_instanceMethod1"
      types = 0x0000000100003f73 "v16@0:8"
      imp = 0x0000000100003970 (KCObjc`-[Person(LGA) kc_instanceMethod1])
    }
  }
}
(lldb) 

继续执行程序,看下图,首先经过排序,然后指针内存平移,获得最后一个元素:


iShot2020-10-17 14.55.52.png
(lldb) p mlists
(method_list_t *[64]) $2 = {
  [0] = 0xffffffffffffffff
  [1] = 0x0000000000000000
  [2] = 0x00007ffeefbf6a20
  [3] = 0x00007fff68c52ad3
  [4] = 0x00007fff86a22be0
  [5] = 0x00007ffeefbf6b10
  [6] = 0x00007ffeefbf69e0
  [7] = 0x000000010030f66e
  [8] = 0x000000010034ce08
  [9] = 0x000000010034ce08
  [10] = 0x00007fff86a22be0
  [11] = 0x00007ffeefbf6a8f
  [12] = 0x00007ffeefbf69b0
  [13] = 0x0000000100310a70
  [14] = 0x00007ffeefbf6af8
  [15] = 0x00007ffeefbf6a8f
  [16] = 0x00007ffeefbf6a90
  [17] = 0x00007ffeefbf6af8
  [18] = 0x00007ffeefbf69e0
  [19] = 0x0000000100310a25
  [20] = 0x01007ffeefbf6a00
  [21] = 0x00007ffeefbf6a8f
  [22] = 0x00007ffeefbf6a90
  [23] = 0x00007ffeefbf6af8
  [24] = 0x0000000000000000
  [25] = 0x00007fff8f2bce28
  [26] = 0x0000000000000000
  [27] = 0x00000001003432a2
  [28] = 0x00007ffeefbf6a20
  [29] = 0x00007fff68c22ad7
  [30] = 0x00007fff8f2bce28
  [31] = 0x0000000000000044
  [32] = 0x00007ffeefbf6a50
  [33] = 0x00007fff68c2b2bd
  [34] = 0x00000001000b0e68
  [35] = 0x00019ca683cec01a
  [36] = 0x00000001000b0d28
  [37] = 0x00000001002d77b0
  [38] = 0x00007ffeefbf6b30
  [39] = 0x00007fff68c2941e
  [40] = 0x00000001000dedc0
  [41] = 0x00000001003432db
  [42] = 0x00000001003419dd
  [43] = 0x0000000000000000
  [44] = 0x0000000000000000
  [45] = 0x0000000000000002
  [46] = 0x00007ffeefbf6ab0
  [47] = 0x00000001005b12d5
  [48] = 0x00000001005af8e0
  [49] = 0x00000001005af8e0
  [50] = 0x00007ffeefbf6be0
  [51] = 0x00000001000038f0
  [52] = 0x00007ffeefbf6ae0
  [53] = 0x0000000100008410
  [54] = 0x00007ffeefbf6b00
  [55] = 0x0000000100008430
  [56] = 0x00007ffeefbf6b00
  [57] = 0x00000001002e42ae
  [58] = 0x00000001000083e8
  [59] = 0x0000000100008410
  [60] = 0x00007ffeefbf6b30
  [61] = 0x00000001002e4412
  [62] = 0x0000003000000018
  [63] = 0x0000000100008038
}
(lldb) p mlists + ATTACH_BUFSIZ - mcount
(method_list_t **) $3 = 0x00007ffeefbf6b18
(lldb) p *$3
(method_list_t *) $4 = 0x0000000100008038
(lldb) 

当继续执行,又开始跑回attachLists方法,然后执行第二部操作,其中的hasArray原本是私有属性,经过更改,变为public:

iShot2020-10-17 15.00.49.png

而当我们继续执行,还是会重复第二次的操作,接下来就进入了第三种方式的加载,开始是LGA分类的加载,然后是LGB分类的加载:
list_array_tt的结构

iShot2020-10-17 15.07.19.png

执行第三部分代码:


iShot2020-10-17 15.07.53.png
(lldb) p array()
(list_array_tt<method_t, method_list_t>::array_t *) $14 = 0x0000000100640730
(lldb) p $14.lists[0]
(method_list_t *) $15 = 0x0000000100008300
  Fix-it applied, fixed expression was: 
    $14->lists[0]
(lldb) p *$15
(method_list_t) $16 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 4
    first = {
      name = "kc_instanceMethod1"
      types = 0x0000000100003f73 "v16@0:8"
      imp = 0x0000000100003be0 (KCObjc`-[Person(LGB) kc_instanceMethod1])
    }
  }
}
(lldb) 

打印得到的是LGB的分类方法,最后完整执行程序,最终打印的方法也是我们在上面验证的方法:

iShot2020-10-17 15.09.56.png

那么关于如何加载分类,已经得到了答案,那么是在什么时机加载的呢?

下面带着第二个问题去探索:

看看完整的执行整个过程(中间加了很多自己写的判断类的代码,有很多打印输出),其中主类和两个分类都添加了load方法:

readClass: 这个是我要研究的 Person 
_getObjc2NonlazyClassList: 这个是我要研究的 Person 
realizeClassWithoutSwift: 这个是我要研究的 Person 
methodizeClass: 这个是我要研究的 Person 
prepareMethodLists: 这个是我要研究的 Person 
attachToClass: 这个是我要研究的 Person 
load_categories_nolock:operator(): 这个是我要研究的 Person 
extAlloc:这是我要研究的 Person 
attachCategories: 这个是我要研究的 Person 
prepareMethodLists: 这个是我要研究的 Person 
extAlloc:这是我要研究的 Person 
load_categories_nolock:operator(): 这个是我要研究的 Person 
attachCategories: 这个是我要研究的 Person 
prepareMethodLists: 这个是我要研究的 Person 
prepare_load_methods: 这个是我要研究的 Person 
realizeClassWithoutSwift: 这个是我要研究的 Person 
prepare_load_methods: 这个是我要研究的 Person 
realizeClassWithoutSwift: 这个是我要研究的 Person 
2020-10-17 15:13:22.830386+0800 KCObjc[40272:4663341] -[Person(LGB) kc_instanceMethod1]
2020-10-17 15:13:22.831749+0800 KCObjc[40272:4663341] 0x10115be80

从上面的所有方法的打印过程中,有很多方法都是探索过的,通过上帝视角,得到第二个问题的探索入口在load_categories_nolock方法当中;
而经过搜索load_categories_nolock后,只有两处调用了此方法,一个在_read_images中,另一个在loadAllCategories方法中,而经过验证,_read_images方法中的load_categories_nolock并不会执行,那么真正执行load_categories_nolock的方法在loadAllCategories中。

那么在之前的探索,可以得知非懒加载类在Load的情况下才存在,那么如果主类中加入Load方法,分类只弄一个有Load方法,去验证一下分类是否会加载;

在经过测试后,得到一个结论:只要有一个分类是非懒加载类,那么所有分类方法都会是非懒加载类,运行时会将非懒加载类添加进去,它的数据全部由load_image加载到数据。

那么就有一个很有意思的问题,当主类没Load方法,分类有或分类没Load,主类有,那又会有哪些情况呢?
下面去探索一下:
首先在分类中将Load方法注释,主类保留:
先完整执行程序,看打印输出的结果

readClass: 这个是我要研究的 Person 
_getObjc2NonlazyClassList: 这个是我要研究的 Person 
realizeClassWithoutSwift: 这个是我要研究的 Person 
methodizeClass: 这个是我要研究的 Person 
prepareMethodLists: 这个是我要研究的 Person 
attachToClass: 这个是我要研究的 Person 
2020-10-17 16:03:15.569478+0800 KCObjc[40649:4689679] -[Person(LGB) kc_instanceMethod1]
2020-10-17 16:03:15.570557+0800 KCObjc[40649:4689679] 0x10072a1f0
Program ended with exit code: 0

可以看到程序会执行realizeClassWithoutSwift方法,我们以此方法为研究对象
realizeClassWithoutSwift方法中打上断点,去看看ro的值:

iShot2020-10-17 16.01.43.png
(lldb) p kc_ro
(const class_ro_t *) $0 = 0x0000000100008240
(lldb) p *$0
(const class_ro_t) $1 = {
  flags = 388
  instanceStart = 8
  instanceSize = 24
  reserved = 0
  ivarLayout = 0x0000000100003f6d "\x11"
  name = 0x0000000100003f66 "Person"
  baseMethodList = 0x0000000100008038
  baseProtocols = 0x0000000000000000
  ivars = 0x0000000100008288
  weakIvarLayout = 0x0000000000000000
  baseProperties = 0x00000001000082d0
  _swiftMetadataInitializer_NEVER_USE = {}
}
(lldb) p $1->baseMethodList
(method_list_t *const) $2 = 0x0000000100008038
  Fix-it applied, fixed expression was: 
    $1.baseMethodList
(lldb) p $2
(method_list_t *const) $2 = 0x0000000100008038
(lldb) p $2.get(0)
(method_t) $3 = {
  name = "kc_instanceMethod1"
  types = 0x0000000100003f73 "v16@0:8"
  imp = 0x0000000100003be0 (KCObjc`-[Person(LGB) kc_instanceMethod1])
}
  Fix-it applied, fixed expression was: 
    $2->get(0)
(lldb) p $2.get(1)
(method_t) $4 = {
  name = "cateB_2"
  types = 0x0000000100003f73 "v16@0:8"
  imp = 0x0000000100003c10 (KCObjc`-[Person(LGB) cateB_2])
}
  Fix-it applied, fixed expression was: 
    $2->get(1)
(lldb) p $2.get(2)
(method_t) $5 = {
  name = "cateB_1"
  types = 0x0000000100003f73 "v16@0:8"
  imp = 0x0000000100003c40 (KCObjc`-[Person(LGB) cateB_1])
}
  Fix-it applied, fixed expression was: 
    $2->get(2)
(lldb) p $2.get(3)
(method_t) $6 = {
  name = "cateB_3"
  types = 0x0000000100003f73 "v16@0:8"
  imp = 0x0000000100003c70 (KCObjc`-[Person(LGB) cateB_3])
}
  Fix-it applied, fixed expression was: 
    $2->get(3)
(lldb) p $2.get(4)
(method_t) $7 = {
  name = "kc_instanceMethod1"
  types = 0x0000000100003f73 "v16@0:8"
  imp = 0x0000000100003980 (KCObjc`-[Person(LGA) kc_instanceMethod1])
}
  Fix-it applied, fixed expression was: 
    $2->get(4)
(lldb) p $2.get(5)
(method_t) $8 = {
  name = "cateA_2"
  types = 0x0000000100003f73 "v16@0:8"
  imp = 0x00000001000039b0 (KCObjc`-[Person(LGA) cateA_2])
}
  Fix-it applied, fixed expression was: 
    $2->get(5)
(lldb) p $2.get(6)
(method_t) $9 = {
  name = "cateA_1"
  types = 0x0000000100003f73 "v16@0:8"
  imp = 0x00000001000039e0 (KCObjc`-[Person(LGA) cateA_1])
}
  Fix-it applied, fixed expression was: 
    $2->get(6)
(lldb) p $2.get(7)
(method_t) $10 = {
  name = "cateA_3"
  types = 0x0000000100003f73 "v16@0:8"
  imp = 0x0000000100003a10 (KCObjc`-[Person(LGA) cateA_3])
}
  Fix-it applied, fixed expression was: 
    $2->get(7)
(lldb) p $2.get(8)
(method_t) $11 = {
  name = "kc_instanceMethod3"
  types = 0x0000000100003f73 "v16@0:8"
  imp = 0x0000000100003a80 (KCObjc`-[Person kc_instanceMethod3])
}
  Fix-it applied, fixed expression was: 
    $2->get(8)
(lldb) 

可以看到ro中的方法列表有PersonLGALGB的所有方法,而且加载没有顺序,那么意味着当前的数据在这个过程中,只要没有进行非懒加载,在cls读取Mach-o数据的时候,他就已经编译进来了,不需要运行时再添加分类方法进去了。

然后我们去看看排序之后的方法列表是怎样的:

(lldb) p $2.get(0)
(method_t) $12 = {
  name = "kc_instanceMethod1"
  types = 0x0000000100003f73 "v16@0:8"
  imp = 0x0000000100003be0 (KCObjc`-[Person(LGB) kc_instanceMethod1])
}
  Fix-it applied, fixed expression was: 
    $2->get(0)
(lldb) p $2.get(1)
(method_t) $13 = {
  name = "kc_instanceMethod1"
  types = 0x0000000100003f73 "v16@0:8"
  imp = 0x0000000100003980 (KCObjc`-[Person(LGA) kc_instanceMethod1])
}
  Fix-it applied, fixed expression was: 
    $2->get(1)
(lldb) p $2.get(2)
(method_t) $14 = {
  name = "kc_instanceMethod1"
  types = 0x0000000100003f73 "v16@0:8"
  imp = 0x0000000100003ab0 (KCObjc`-[Person kc_instanceMethod1])
}
  Fix-it applied, fixed expression was: 
    $2->get(2)
(lldb) p $2.get(3)
(method_t) $15 = {
  name = "cateA_2"
  types = 0x0000000100003f73 "v16@0:8"
  imp = 0x00000001000039b0 (KCObjc`-[Person(LGA) cateA_2])
}
  Fix-it applied, fixed expression was: 
    $2->get(3)
(lldb) p $2.get(4)
(method_t) $16 = {
  name = "cateA_1"
  types = 0x0000000100003f73 "v16@0:8"
  imp = 0x00000001000039e0 (KCObjc`-[Person(LGA) cateA_1])
}
  Fix-it applied, fixed expression was: 
    $2->get(4)
(lldb) p $2.get(5)
(method_t) $17 = {
  name = "cateA_3"
  types = 0x0000000100003f73 "v16@0:8"
  imp = 0x0000000100003a10 (KCObjc`-[Person(LGA) cateA_3])
}
  Fix-it applied, fixed expression was: 
    $2->get(5)
(lldb) 

这加载的顺序并没有排序,其实他是进行排序了的;至于它是如何实现排序的,就不写过程了,系统首先会根据主类的方法进行排序,在分类的方法中,则会根据方法地址从小到大进行排序。

那么就得到了第二个结论:当主类有Load,分类没有,那么数据来自于data(),只需要对同名方法进行处理和修复一下。

那么主类没有Load,分类也没有Load,先打印一下整个输出:

readClass: 这个是我要研究的 Person 
realizeClassMaybeSwiftMaybeRelock: 这个是我要研究的 Person 
realizeClassMaybeSwiftMaybeRelock: 这个是我要研究的 Person 
realizeClassWithoutSwift: 这个是我要研究的 Person 
methodizeClass: 这个是我要研究的 Person 
prepareMethodLists: 这个是我要研究的 Person 
attachToClass: 这个是我要研究的 Person 
2020-10-17 16:29:33.924814+0800 KCObjc[40771:4699799] -[Person(LGB) kc_instanceMethod1]
2020-10-17 16:29:33.925950+0800 KCObjc[40771:4699799] 0x10114c8d0
Program ended with exit code: 0

从打印的结果得知,整个数据的加载,都推迟到了第一次调用消息的时候,那么它是从哪加载数据的呢?那么就需要以readClass方法入手,经过探索,它同样是从data()中加载的数据。

最后一种情况,主类没有Load,分类有Load

同样的先打印流程:

readClass: 这个是我要研究的 Person 
operator(): 这个是我要研究的 Person 
operator(): 这个是我要研究的 Person 
prepare_load_methods: 这个是我要研究的 Person 
realizeClassWithoutSwift: 这个是我要研究的 Person 
realizeClassWithoutSwift: 这个是我要研究的 Person 
methodizeClass: 这个是我要研究的 Person 
prepareMethodLists: 这个是我要研究的 Person 
attachToClass: 这个是我要研究的 Person 
extAlloc:这是我要研究的 Person 
attachCategories: 这个是我要研究的 Person 
prepareMethodLists: 这个是我要研究的 Person 
methodizeClass: 这个是我要研究的 Person 
prepareMethodLists: 这个是我要研究的 Person 
attachToClass: 这个是我要研究的 Person 
extAlloc:这是我要研究的 Person 
attachCategories: 这个是我要研究的 Person 
prepareMethodLists: 这个是我要研究的 Person 
prepare_load_methods: 这个是我要研究的 Person 
realizeClassWithoutSwift: 这个是我要研究的 Person 
2020-10-17 16:37:55.247587+0800 KCObjc[40892:4706600] -[Person(LGB) kc_instanceMethod1]
2020-10-17 16:37:55.249469+0800 KCObjc[40892:4706600] 0x101a38cb0
Program ended with exit code: 0

在所有打印方法中,没有load_categories_nolock方法的打印,那么先在这个方法处打上断点,然后同样的我们先以readClass入手:
看下图的count只有8个,只有主类的方法,没有分类的方法。

iShot2020-10-17 16.57.54.png

继续执行,他就会进入load_categories_nolock方法,通过bt打印堆栈信息:

iShot2020-10-17 17.02.33.png

可以看到,它就经过loadAllCategories方法,而调用此方法的函数是load_images方法。这跟第一种方试差不太多;按原理来说,主类没有Load方法,那么它会在第一次消息的时候加载,而实际上是主类没有Load,分类有,会迫使主类提前加载。

那么最后得到结论

iShot2020-10-17 17.14.37.png
上一篇下一篇

猜你喜欢

热点阅读