ios底层-类的加载下 类和分类在不同场景下的加载

197 阅读8分钟

Xcode 11.6

libobjc:781

核心方法回顾

类的加载上中罗列了一些和类加载相关的方法以便于这一次的展开分析。主要相关方法如下:

read_images

  • 读取镜像文件,进行一些初始化工作,修复一些异常
  • 加载非懒加载类 ->realizeClassWithoutSwift

realizeClassWithoutSwift

  • 调整类的内部结构,读取ro,创建rw,赋值rw->ro等等
  • 递归实现父类和元类
  • 更新父类以及元类和当前类的关系
  • 拷贝ro中的一些标识到rw
  • 条理化类-> methodizeClass

methodizeClass 修复类中的方法

  • 装载当前cls自己实现的方法和属性,不包含分类 -> prepareMethodLists
  • ro中的方法列表进行排序
  • 为根元类动态添加initialize的实现
  • 加载分类数据

prepareMethodLists

  • 对传入的方法列表进行按照sel地址的排序,确保后续方法查找的数组是有序数组

loadAllCategories

  • 加载所有的macho类型文件中的分类 -> load_categories_nolock

load_categories_nolock

  • 读取macho文件中的分类列表
  • 加载分类到对应的类中 ->attachCategories/unattachedCategories.addForClass

unattachedCategories.addForClass

  • 当分类对应的类没有实现时,将分类暂存,当主类实现后再进行加载

attachCategories

  • 创建class_rwe_ex_t结构体 ->class_rw_t::extAlloc()
  • 遍历分类的列表,优化分类的方法列表(prepareMethodLists),添加分类中的方法、协议、属性列表到rwe中 -> attachLists

class_rw_t::extAlloc()

  • 开辟class_rwe_ex_t,复制ro中的方法、协议以及属性等数据到rwe中。

attachLists

  • 将方法/协议/属性列表作为单个元素加入到二维数组lists中,具体操作为开辟新的内存空间,采用头插法,确保新插入元素位于二维数组的首位。

addForClass

  • 存储未实现类的分类(此时类为懒加载,分类为非懒加载)

分情况讨论类和分类的加载情况

在上述方法中,realizeClassWithoutSwift为实现类的必须方法,attachCategories为添加分类到主类的核心方法,通过在这两个方法中打下断点来观测不同情况下类和分类的加载情况。

懒加载指的是没有实现+load方法,否则为非懒加载

1. 类lazy+分类lazy

...
@implementation ZHYPerson
- (void)method {}
- (void)instanceMethod {}
@end
...
@implementation ZHYPerson (Category1)
- (void)method {}
- (void)categoryMethod{}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        ZHYPerson *person = [ZHYPerson alloc];

观察断点发现我们自定义的懒加载类ZHYPerson的实现是在main函数执行实例化ZHYPerson的时候触发的。那么分类是什么时候加载的呢?我们先来打印一下当前cls的方法列表。

(lldb) p ro->baseMethods()
(method_list_t *) $0 = 0x00000001000042c8
(lldb) p *$0
(method_list_t) $1 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 24
    count = 4
    first = {
      name = "method"
      types = 0x0000000100003e08 "v16@0:8"
      imp = 0x0000000100003190 (objc-debug`-[ZHYPerson(Category1) method] at main.m:91)
    }
  }
}
(lldb) p $1.get(1)
(method_t) $2 = {
  name = "categoryMethod"
  types = 0x0000000100003e08 "v16@0:8"
  imp = 0x00000001000031a0 (objc-debug`-[ZHYPerson(Category1) categoryMethod] at main.m:92)
}
(lldb) p $1.get(2)
(method_t) $3 = {
  name = "method"
  types = 0x0000000100003e08 "v16@0:8"
  imp = 0x0000000100003170 (objc-debug`-[ZHYPerson method] at main.m:84)
}
(lldb) p $1.get(3)
(method_t) $4 = {
  name = "instanceMethod"
  types = 0x0000000100003e08 "v16@0:8"
  imp = 0x0000000100003180 (objc-debug`-[ZHYPerson instanceMethod] at main.m:85)
}

此时ZHYPerson的方法列表中一共有4个方法,他们的顺序为:

1. [ZHYPerson(Category1) method]
2. [ZHYPerson(Category1) categoryMethod]
3. [ZHYPerson method]
4. [ZHYPerson instanceMethod]

此时就会产生三个疑问:

  • realizeClassWithoutSwift的调用时机是在执行我们自己的代码ZHYPerson *person = [ZHYPerson alloc]时,和上一节的分析好像不太一致。
  • 通过代码可以发现ro并不是正常的方法得到的,而是通过auto ro = (const class_ro_t *)cls->data()得到的,为什么会这样呢?
  • 分类是什么时候加载的?attachCategories并没有执行啊? 1. 第一个疑问realizeClassWithoutSwift的调用时机 上图中按照从下往上有对应的方法调用注释。因为分类和主类都是懒加载类,因此只有在需要用到的时候才会实现,并没有遵循非懒加载类的那一套流程。

2. 第二个疑问ro的获取方式

class_rw_t *data() const {
    return bits.data();
}

根据上面的方法实现得知cls->data()方法本来应该得到的是class_rw_t,但是系统将rw强转成了ro。另外一点是此时我们的类还没有实现,因此读取到的数据应该是直接从macho文件中读取到的,就是说是编译期就确定的数据。那么唯一可以解释的就是我们的类在编译时并没有class_rw_t这个结构,而是将class_ro_t这个编译器确定的结构体指针放在了rw指针的位置。

3. 第三个疑问分类是什么时候加载的? 经过第二个疑问的解答得出此时的ro是编译期确定的内容,同时我们打印方法列表的时候发现分类中的方法methodcategoryMethod都已经被添加到了方法列表中。这就是说分类已经在编译期就加载到了我们的主类当中。

此时我们过掉断点,发现attachCategories方法始终没有执行。

综上可以得出如下结论: 懒加载分类在编译期就加载到了主类当中。

2. 类nonLazy+分类nonLazy

主类和分类都实现了load方法。

@interface ZHYPerson:NSObject
@end
@implementation ZHYPerson
+ (void)load {}
- (void)method {}
- (void)instanceMethod {}
@end

@interface ZHYPerson (Category1)
@end
@implementation ZHYPerson (Category1)
+ (void)load {}
- (void)method {}
- (void)categoryMethod{}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        ZHYPerson *person = [ZHYPerson alloc];
	}
    return 0;
}

观察断点发现此时类的实现是在dyld的流程中触发的。此时我们的main函数还没有执行。这个结果符合懒加载类的特征,我们来打印一下ro中的内容。

(lldb) p ro->baseMethods()
(method_list_t *) $0 = 0x0000000100004390
(lldb) p *$0
(method_list_t) $1 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 24
    count = 2
    first = {
      name = "method"
      types = 0x0000000100003e08 "v16@0:8"
      imp = 0x0000000100003160 (objc-debug`-[ZHYPerson method] at main.m:85)
    }
  }
}
(lldb) p $1.get(1)
(method_t) $2 = {
  name = "instanceMethod"
  types = 0x0000000100003e08 "v16@0:8"
  imp = 0x0000000100003170 (objc-debug`-[ZHYPerson instanceMethod] at main.m:86)
}

发现ro中只有两个主类中的方法,说明当前的这种组合情况下编译期只确定了主类的信息,分类并没有加载进来。 过掉断点: attachCategories执行了。此时的流程也是有dyld触发的,处于main函数之前,打印ro的内容:

(lldb) p ro->baseMethods()
(method_list_t *) $0 = 0x0000000100004390
(lldb) p *$0
(method_list_t) $1 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 24
    count = 2
    first = {
      name = "method"
      types = 0x0000000100003e08 "v16@0:8"
      imp = 0x0000000100003160 (objc-debug`-[ZHYPerson method] at main.m:85)
    }
  }
}
(lldb) p $1.get(1)
(method_t) $2 = {
  name = "instanceMethod"
  types = 0x0000000100003e08 "v16@0:8"
  imp = 0x0000000100003170 (objc-debug`-[ZHYPerson instanceMethod] at main.m:86)
}

ro的内容只是顺序发生了变化,这是因为在realizeClassWithoutSwift过程中对方法进行了排序。接下来打印rwe中的methods:

(lldb) p rwe->methods
(method_array_t) $48 = {
  list_array_tt<method_t, method_list_t> = {
     = {
      list = 0x0000000100707f01
      arrayAndFlag = 4302339841
    }
  }
}
(lldb) p $48.beginLists()
// 二维数组
(method_list_t *const *) $49 = 0x0000000100707f08
(lldb) p *$49
// 读取数组的第一个元素method_list_t *指针
(method_list_t *const) $50 = 0x00000001000043c8
(lldb) p *$50
(method_list_t) $51 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 2
    first = {
      name = "categoryMethod"
      types = 0x0000000100003e08 "v16@0:8"
      imp = 0x00000001000031a0 (objc-debug`-[ZHYPerson(Category1) categoryMethod] at main.m:94)
    }
  }
}
(lldb) p $51.get(1)
(method_t) $52 = {
  name = "method"
  types = 0x0000000100003e08 "v16@0:8"
  imp = 0x0000000100003190 (objc-debug`-[ZHYPerson(Category1) method] at main.m:93)
}
(lldb) p *($49+1)
// 读取数组的第一个元素method_list_t *指针
(method_list_t *const) $53 = 0x0000000100004390
(lldb) p *$53
(method_list_t) $54 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 2
    first = {
      name = "instanceMethod"
      types = 0x0000000100003e08 "v16@0:8"
      imp = 0x0000000100003170 (objc-debug`-[ZHYPerson instanceMethod] at main.m:86)
    }
  }
}
(lldb) p $54.get(1)
(method_t) $55 = {
  name = "method"
  types = 0x0000000100003e08 "v16@0:8"
  imp = 0x0000000100003160 (objc-debug`-[ZHYPerson method] at main.m:85)
}

我们知道编译期类的结构中并没有rwe,是在运行时类的结构需要发生变化时才会生成,生成之后会将ro中对应的数据拷贝到rwe中,同时还负责运行时添加的一些其他数据。通过上述打印我们可以验证一下几点:

  • rwe中的methods是一个存储method_list_t指针的数组,对应的是ro中的方法列表或者分类中的方法列表
  • 分类中的方法列表的位置比主类的方法列表位置靠前,因为插入的时候采取的是头插法。

3. 类nonLazy+分类Lazy

主类实现load方法,分类不实现

@interface ZHYPerson:NSObject
@end
@implementation ZHYPerson
+ (void)load {}
- (void)method {}
- (void)instanceMethod {}
@end

@interface ZHYPerson (Category1)
@end
@implementation ZHYPerson (Category1)
- (void)method {}
- (void)categoryMethod{}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        ZHYPerson *person = [ZHYPerson alloc];
	}
    return 0;
}

类的实现是在dyld流程中执行,在main函数之前,打印ro:

(lldb) p ro.baseMethods()
(method_list_t *) $0 = 0x00000001000042d0
  Fix-it applied, fixed expression was: 
    ro->baseMethods()
(lldb) p *$0
(method_list_t) $1 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 24
    count = 4
    first = {
      name = "method"
      types = 0x0000000100003e08 "v16@0:8"
      imp = 0x0000000100003190 (objc-debug`-[ZHYPerson(Category1) method] at main.m:92)
    }
  }
}
(lldb) p $1.get(1)
(method_t) $2 = {
  name = "categoryMethod"
  types = 0x0000000100003e08 "v16@0:8"
  imp = 0x00000001000031a0 (objc-debug`-[ZHYPerson(Category1) categoryMethod] at main.m:93)
}
(lldb) p $1.get(2)
(method_t) $3 = {
  name = "method"
  types = 0x0000000100003e08 "v16@0:8"
  imp = 0x0000000100003170 (objc-debug`-[ZHYPerson method] at main.m:85)
}
(lldb) p $1.get(3)
(method_t) $4 = {
  name = "instanceMethod"
  types = 0x0000000100003e08 "v16@0:8"
  imp = 0x0000000100003180 (objc-debug`-[ZHYPerson instanceMethod] at main.m:86)
}

分类的方法在编译期已经加入到了ro中,由于主类是非懒加载的原因,在main函数之前就实现了。

4. 类lazy+分类nonLazy

主类不实现load方法,分类实现load方法。

@interface ZHYPerson:NSObject
@end
@implementation ZHYPerson
- (void)method {}
- (void)instanceMethod {}
@end@interface ZHYPerson (Category1)
@end
@implementation ZHYPerson (Category1)
+ (void)load {}
- (void)method {}
- (void)categoryMethod{}
@end
​
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        ZHYPerson *person = [ZHYPerson alloc];
    }
    return 0;
}

当主类是懒加载,分类是非懒加载时realizeClassWithoutSwift的调用堆栈如上图所示,此时触发时机延迟到了load_images方法中。我们先来看一下主类中ro的数据:

(lldb) p ro->baseMethods()
(method_list_t *) $0 = 0x0000000100002200
(lldb) p *$0
(method_list_t) $1 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 24
    count = 2
    first = {
      name = "method"
      types = 0x0000000100001ecd "v16@0:8"
      imp = 0x0000000100001650 (objc-debug`-[ZHYPerson method] at main.m:84)
    }
  }
}
(lldb) p $1.get(1)
(method_t) $2 = {
  name = "instanceMethod"
  types = 0x0000000100001ecd "v16@0:8"
  imp = 0x0000000100001660 (objc-debug`-[ZHYPerson instanceMethod] at main.m:85)
}

此时主类的ro的方法列表中只有两个方法:

  • method
  • instanceMethod 说明当前情况下分类的数据在编译期并没有合并到主类中。跳过当前断点来到attachCategories中: 此时分类数据已经加载完毕,我们来看一下rwe中的数据情况:
(lldb) p rwe->methods
(method_array_t) $3 = {
  list_array_tt<method_t, method_list_t> = {
     = {
      list = 0x000000010105c6a1
      arrayAndFlag = 4312123041
    }
  }
}
(lldb) p $3.beginLists()
(method_list_t *const *) $4 = 0x000000010105c6a8
(lldb) p *$4
(method_list_t *const) $5 = 0x0000000100002238
(lldb) p *$5
(method_list_t) $6 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 2
    first = {
      name = "categoryMethod"
      types = 0x0000000100001ecd "v16@0:8"
      imp = 0x0000000100001690 (objc-debug`-[ZHYPerson(Category1) categoryMethod] at main.m:93)
    }
  }
}
(lldb) p $6.get(1)
(method_t) $7 = {
  name = "method"
  types = 0x0000000100001ecd "v16@0:8"
  imp = 0x0000000100001680 (objc-debug`-[ZHYPerson(Category1) method] at main.m:92)
}
(lldb) p *($4+1)
(method_list_t *const) $8 = 0x0000000100002200
(lldb) p *$8
(method_list_t) $9 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 2
    first = {
      name = "instanceMethod"
      types = 0x0000000100001ecd "v16@0:8"
      imp = 0x0000000100001660 (objc-debug`-[ZHYPerson instanceMethod] at main.m:85)
    }
  }
}
(lldb) p $8.get(1)
(method_t) $10 = {
  name = "method"
  types = 0x0000000100001ecd "v16@0:8"
  imp = 0x0000000100001650 (objc-debug`-[ZHYPerson method] at main.m:84)
}

可以看到rwe的方法列表中数组中放着分类的方法列表和主类的方法列表,且分类在前,主类在后,此时的结构和主类分类同为非懒加载类的结构是一样的。 主要原因有两个:

1. 当前cls是懒加载类,因此在map_images中处理非懒加载类时并没有执行cls的实现。

2. 当前cls的分类是非懒加载,因此在load_images中触发了非懒加载分类的加载,由于此时主类还没有实现,分类被暂时保存到了unattachedCategories中,在prepare_load_methods中处理非懒加载分类时,触发了对主类的加载,加载过程中将unattachedCategories中保存的对应分类添加到了主类中

额外补充

动态添加方法时是怎么处理的?

动态添加方法一般使用class_addMethod,具体实现如下:

BOOL 
class_addMethod(Class cls, SEL name, IMP imp, const char *types)
{
    if (!cls) return NO;

    mutex_locker_t lock(runtimeLock);
    return ! addMethod(cls, name, imp, types ?: "", NO);
}

addMethod

static IMP 
addMethod(Class cls, SEL name, IMP imp, const char *types, bool replace)
{
    IMP result = nil;

    runtimeLock.assertLocked();

    checkIsKnownClass(cls);
    
    ASSERT(types);
    ASSERT(cls->isRealized());

    method_t *m;
    // 从当前类中查找SEL对应的方法
    if ((m = getMethodNoSuper_nolock(cls, name))) {
        // already exists
        if (!replace) {
        	// 不能替换的话会返回该方法的IMP
            result = m->imp;
        } else {
        	// 可以替换的话就替换IMP
            result = _method_setImplementation(cls, m, imp);
        }
    } else {
    // 并没有在当前类中找到SEL对应的方法,此时需要查询rwe是否存在,如果没有就创建rwe
        auto rwe = cls->data()->extAllocIfNeeded();

        // fixme optimize
        // 生成一个method_list_t,将方法对应的数据放进去
        method_list_t *newlist;
        newlist = (method_list_t *)calloc(sizeof(*newlist), 1);
        newlist->entsizeAndFlags = 
            (uint32_t)sizeof(method_t) | fixed_up_method_list;
        newlist->count = 1;
        newlist->first.name = name;
        newlist->first.types = strdupIfMutable(types);
        newlist->first.imp = imp;

        prepareMethodLists(cls, &newlist, 1, NO, NO);
        // 添加方法列表到rwe的methods中
        rwe->methods.attachLists(&newlist, 1);
        // 清空缓存
        flushCaches(cls);

        result = nil;
    }

    return result;
}

主要流程如下:

1. 在当前类cls中查找是否存在和sel重名的方法

2. 如果找到直接返回对应的imp(replace参数为NO,说明不能替换,否则直接替换imp)

3. 没有找到sel对应的method

4. 取出rwe或创建rwe

5. 初始化一个method_list_t数组,将需要添加的方法sel、签名、实现等赋值给第一个元素

6. 将数组添加到rwe的methods中

7. 清空缓存,此时方法列表已经发生了变化

备注

类懒加载和分类非懒加载的情况在Xcode12表现有些异样,可能是我写错了,此处存疑。