OC底层探索18 - 类的加载(下)
2021-06-24 本文已影响0人
Henry________
在上一篇OC底层探索17 - 类的加载(上)中对类的名称、data、方法、属性、协议的注入完成了分析。还留下了一个问题就是类中分类的加载
本文调试源码
objc4-818.2
,所以结论也仅限于该版本。
二、 分类的加载
书接上文,在methodizeClass
中发现了attachToClass
这个方法中对分类方法进行了处理。
前提:
@implementation HRTest
@property(nonatomic, copy)NSString *name;
-(void)sayHappy;
+(void)load{ }
@end
@implementation HRTest (cate1)
-(void)sayHappy;
-(void)sayHappyCate1;
+(void)load{ }
@end
- 类和分类中都实现了
+load
方法,后续会用到。
1、分类的加载时机
static void methodizeClass(Class cls, Class previously)
{
...
objc::unattachedCategories.attachToClass(cls, cls,
isMeta ? ATTACH_METACLASS : ATTACH_CLASS);
}
void attachToClass(Class cls, Class previously, int flags)
{
auto &map = get();
auto it = map.find(previously);
if (it != map.end()) {
category_list &list = it->second;
if (flags & ATTACH_CLASS_AND_METACLASS) {
int otherFlags = flags & ~ATTACH_CLASS_AND_METACLASS;
attachCategories(cls, list.array(), list.count(), otherFlags | ATTACH_CLASS);
//类别中类方法添加到元类中去
attachCategories(cls->ISA(), list.array(), list.count(), otherFlags | ATTACH_METACLASS);
} else {
attachCategories(cls, list.array(), list.count(), flags);
}
map.erase(it);
}
}
-
根据断点调试发现,就发现该方法没有被调用过。
可是根据观察这个方法attachCategories
就是完成分类的加载的,所以在attachCategories
增加断点查看。
- 通过堆栈可以看到执行流程:
load_images
-loadAllCategories
-load_categories_nolock
-attachCategories
。
所有分类的加载是在map_images
之后的load_images
里被调起的,真的是这样吗?记得在文章的开始有提过一个前提
,是类、分类中都实现了+load
方法,如果没有实现这个方法呢?
1.1 类、分类都不实现+load
我们知道如果类中不实现load方法
,则该类是一个懒加载类
,类的加载时机推迟到第一次消息调用。那个分类的加载时机是什么时候呢?
- 断点设置在
methodizeClass
,因为attachCategories
不会被调用; - 堆栈信息看到起点是在
类第一次消息发送时
; - 在类从mach-o中读出
ro
时,类、分类的方法都已经保存在ro里了
;
1.2 类、分类的4类情况
类 | 分类 | 类加载情况 | 分类加载情况 |
---|---|---|---|
load | load | 类在map_image加载 | 分类在load_image加载 |
load | 类在map_image加载 | 分类方法已经通过mach-o读取到ro里 | |
load | 类被标记为非懒加载类,在map_image加载 | 分类方法已经通过mach-o读取到ro里 | |
类在第一次消息转发时加载 | 分类方法已经通过mach-o读取到ro里 |
2、分类的加载
只有在类、分类都实现+load方法才会执行
,其他情况都是直接从mach-o中读取出来的。
static void load_categories_nolock(header_info *hi) {
size_t count;
//所有分类进行循环
auto processCatlist = [&](category_t * const *catlist) {
for (unsigned i = 0; i < count; i++) {
if (cls->isRealized()) {
attachCategories(cls, &lc, 1, ATTACH_EXISTING);
}
}
...
}
static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count...) {
// 脏内存是wwdc2020提出的一种类的内存优化
// extAllocIfNeeded 调用该方法之后才生成rwe。
// 分类、addmethod、addprotocol、addproperty四种情况下才会产生rwe脏内存
auto rwe = cls->data()->extAllocIfNeeded();
//根据调试cats_count = 1,该循环只执行一次,该类的其他分类是通过
for (uint32_t i = 0; i < cats_count; i++) {
auto& entry = cats_list[i];
//拿出分类中的方法
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
//把分类放到最后一位64号位置,猜测方便查询
mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
fromBundle |= entry.hi->isBundle();
}
// 属性、协议的方法注入就省略了
....
}
if (mcount > 0) {
//依旧进行排序
prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount,
NO, fromBundle, __func__);
//插入方法列表
rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
}
}
- 通过该方法完成分类方法、属性、协议的
获取、排序、插入
; - 分类中属性是不自动生成
set
、get
方法;
3、分类方法的插入
在OC底层探索17 - 类的加载(上)已经提到过该方法的一种情况,事实上该方法有3种情况.
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (hasArray()) {
// many lists -> many lists
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount;
//数组进行扩容
array_t *newArray = (array_t *)malloc(array_t::byteSize(newCount));
newArray->count = newCount;
array()->count = newCount;
//旧数组元素从后往前插
for (int i = oldCount - 1; i >= 0; i--)
newArray->lists[i + addedCount] = array()->lists[i];
//新数组元素从前往后插
for (unsigned i = 0; i < addedCount; i++)
newArray->lists[i] = addedLists[i];
free(array());
setArray(newArray);
}
else if (!list && addedCount == 1) {
// 0 lists -> 1 list
list = addedLists[0];
}
else {
// 1 list -> many lists
Ptr<List> oldList = list;
uint32_t oldCount = oldList ? 1 : 0;
uint32_t newCount = oldCount + addedCount;
//数组进行扩容
setArray((array_t *)malloc(array_t::byteSize(newCount)));
array()->count = newCount;
// 把旧数组当做一个元素放到lists最后一位
if (oldList) array()->lists[addedCount] = oldList;
// 把新数组从头依次放入
for (unsigned i = 0; i < addedCount; i++)
array()->lists[i] = addedLists[i];
}
}
- 当分类方法首次注入时会走到
1 list -> many lists
这里,这就是导致类中methodsList有时会是一个二维数组的原因
。 - 会把分类的新方法放入到新数组的最开头,
所有重写类中的方法并没有被替换
,而是插入到了最前方。这就是为什么在方法查找(lookupImp)时从后往前进行查询的
。 - 当第二个分类方法进行注入时,将
数组进行扩容
,然后把新的方法从头依次插入
。
三、 load_images(非懒加载类)
map_images完成后,还记得在_objc_init - _dyld_objc_notify_register(&map_images, load_images, unmap_image);
中的load_images
吗?
void
load_images(const char *path __unused, const struct mach_header *mh)
{
if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
didInitialAttachCategories = true;
//非懒加载类的分类中实现load方法后,通过该方法完成分类的加载.
loadAllCategories();
}
// Discover load methods
{
//准备所有load方法
prepare_load_methods((const headerType *)mh);
}
// Call +load methods (without runtimeLock - re-entrant)
//调用所有load方法
call_load_methods();
}
1、prepare_load_methods 准备所有load方法
void prepare_load_methods(const headerType *mhdr)
{
size_t count, i;
classref_t const *classlist =
_getObjc2NonlazyClassList(mhdr, &count);
for (i = 0; i < count; i++) {
schedule_class_load(remapClass(classlist[i]));
}
//获取所有实现load方法的分类
category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
//所有分类的load方法都会被加载
for (i = 0; i < count; i++) {
category_t *cat = categorylist[i];
Class cls = remapClass(cat->cls);
//非懒加载分类迫使主类完成加载
realizeClassWithoutSwift(cls, nil);
add_category_to_loadable_list(cat);
}
}
- 完成了
类的load获取
,同时也完成了分类load方法的获取
; - 即使类是一个懒加载类,在获取
非懒加载分类
的load方法时迫使主类完成加载
; - 多个分类的
load
方法都会被添加
1.1 add_class_to_loadable_list(类)
static void schedule_class_load(Class cls)
{
// Ensure superclass-first ordering
schedule_class_load(cls->getSuperclass());
}
void add_class_to_loadable_list(Class cls)
{
IMP method;
method = cls->getLoadMethod();
//在数组到达上限时,完成数组的扩容
if (loadable_classes_used == loadable_classes_allocated) {
loadable_classes_allocated = loadable_classes_allocated*2 + 16;
loadable_classes = (struct loadable_class *)
//数组的扩容
realloc(loadable_classes,
loadable_classes_allocated *
sizeof(struct loadable_class));
}
//将load方法和对应的类放入loadable_classes
loadable_classes[loadable_classes_used].cls = cls;
loadable_classes[loadable_classes_used].method = method;
loadable_classes_used++;
}
- 将
类
和load方法
添加到数组loadable_classes
中; - 在数组达到上限后再进行扩容操作,尽可能的节省内存;
1.2 add_category_to_loadable_list(分类)
void add_category_to_loadable_list(Category cat)
{
IMP method;
method = _category_getLoadMethod(cat);
//扩容
if (loadable_categories_used == loadable_categories_allocated) {
loadable_categories_allocated = loadable_categories_allocated*2 + 16;
loadable_categories = (struct loadable_category *)
realloc(loadable_categories,
loadable_categories_allocated *
sizeof(struct loadable_category));
}
//将分类load方法加入loadable_categories
loadable_categories[loadable_categories_used].cat = cat;
loadable_categories[loadable_categories_used].method = method;
loadable_categories_used++;
}
- 方法原理都是一样的;
- 将分类的
load
方法加入loadable_categories
2、prepare_load_methods 调用所有load方法
void call_load_methods(void)
{
bool more_categories;
//当前循环值执行一次
do {
// 1. Repeatedly call class +loads until there aren't any more
//通过完成类所有load方法的调用
//当前循环值执行一次
while (loadable_classes_used > 0) {
call_class_loads();
}
// 2. Call category +loads ONCE
//完成所有分类所有load方法的调用
more_categories = call_category_loads();
// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);
}
-
类
的load方法和分类
的load方法都会被调用
,而且是类的load方法先被调用
。
2.1 call_class_loads
static void call_class_loads(void)
{
int i;
// Detach current loadable list.
struct loadable_class *classes = loadable_classes;
int used = loadable_classes_used;
loadable_classes = nil;
loadable_classes_allocated = 0;
//直接设置为0,外层就不会继续循环
loadable_classes_used = 0;
// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Class cls = classes[i].cls;
load_method_t load_method = (load_method_t)classes[i].method;
if (!cls) continue;
//通过函数指针调用
(*load_method)(cls, @selector(load));
}
//释放class
if (classes) free(classes);
}
-
call_category_loads
是类似的就是不赘述了。 通过函数指针完成load方法的调用
面试题
题:如果类和分类有同名方法,调用会调用哪个方法?
答:两种情况:
- 如果是
普通方法
,则会调用分类中的重名方法 - 如果是
+load方法
,则先调用类中
的+load,在依次调用分类
的load.
总结
类的加载-分类的加载-load方法调用后,加载一个类所有的工作都已经完成了,等待后续使用。