iOS之深入解析类加载的底层原理:分类如何加载到类以及分类和类的配合使用
Posted Forever_wj
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS之深入解析类加载的底层原理:分类如何加载到类以及分类和类的配合使用相关的知识,希望对你有一定的参考价值。
一、分类的本质
① Xcode Documentation
- 通过 Xcode 文档搜索,在 Documentation 搜索 Category 关键字:
- 点击 Category ,如下:
② 通过 objc 源码搜索 category_t
③ 通过 clang
- 在 main 中定义 YDWperson 的分类 DW:
@interface YDWPerson (DW)
@property (nonatomic, copy) NSString *cate_name;
@property (nonatomic, assign) int cate_age;
- (void)cate_instanceMethod1;
- (void)cate_instanceMethod2;
- (void)cate_instanceMethod3;
+ (void)cate_sayClassMethod;
@end
@implementation YDWPerson (DW)
- (void)cate_instanceMethod1 {
NSLog(@"%s", __func__);
}
- (void)cate_instanceMethod2 {
NSLog(@"%s", __func__);
}
- (void)cate_instanceMethod3 {
NSLog(@"%s", __func__);
}
+ (void)cate_sayClassMethod {
NSLog(@"%s", __func__);
}
@end
- clang -rewrite-objc main.m -o main.cpp 查看底层编译,即 main.cpp,如下所示:
static struct _category_t _OBJC_$_CATEGORY_YDWPerson_$_DW __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"YDWPerson",
0, // &OBJC_CLASS_$_YDWPerson,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_YDWPerson_$_DW,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_YDWPerson_$_DW,
0,
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_YDWPerson_$_DW,
};
- 在源码中,我们已经得知分类的类型为 catagory_t;可以发现分类的倒数第二个的值为 0,表示没有协议,所以被赋值为 0。
- 搜索 struct _category_t,如下所示:
struct _category_t {
const char *name;
struct _class_t *cls;
// 实例化方法
const struct _method_list_t *instance_methods;
// 类方法
const struct _method_list_t *class_methods;
const struct _protocol_list_t *protocols;
const struct _prop_list_t *properties;
};
- 搜索 CATEGORY_INSTANCE_METHODS_YDWPerson,找到其底层实现:
static struct _category_t _OBJC_$_CATEGORY_YDWPerson_$_DW __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"YDWPerson",
0, // &OBJC_CLASS_$_YDWPerson,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_YDWPerson_$_DW,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_YDWPerson_$_DW,
0,
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_YDWPerson_$_DW,
};
- OBJCKaTeX parse error: Expected group after '_' at position 37: …THODS_YDWPerson_̲_DW 中有 3 个方法,格式为:sel + 签名 + 地址,是 method_t 结构体的属性,即key:
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[3];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_YDWPerson_$_DW __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
3,
{{(struct objc_selector *)"cate_instanceMethod1", "v16@0:8", (void *)_I_YDWPerson_DW_cate_instanceMethod1},
{(struct objc_selector *)"cate_instanceMethod2", "v16@0:8", (void *)_I_YDWPerson_DW_cate_instanceMethod2},
{(struct objc_selector *)"cate_instanceMethod3", "v16@0:8", (void *)_I_YDWPerson_DW_cate_instanceMethod3}}
};
- 在源码中搜索 method_t,其中对应关系如下:
struct method_t {
SEL name; // sel
const char *types; // 方法签名
MethodListIMP imp; // 函数地址
struct SortBySELAddress :
public std::binary_function<const method_t&,
const method_t&, bool>
{
bool operator() (const method_t& lhs,
const method_t& rhs)
{ return lhs.name < rhs.name; }
};
};
- 在这里发现一个问题:查看 _prop_list_t,明明分类中定义了属性,但是在底层编译中并没有看到属性,如下图所示:
static struct /*_prop_list_t*/ {
unsigned int entsize; // sizeof(struct _prop_t)
unsigned int count_of_properties;
struct _prop_t prop_list[2];
} _OBJC_$_PROP_LIST_YDWPerson_$_DW __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_prop_t),
2,
{{"cate_name","T@\\"NSString\\",C,N"},
{"cate_age","Ti,N"}}
};
- 这是因为分类中定义的属性没有相应的 set、get 方法,但是我们可以通过关联对象来设置(下文会具体说明)。
④ 总结
- 分类的本质是一个 _category_t 类型:
- 两个属性:name(类的名称) 和 cls(类对象);
- 两个 method_list_t 类型的方法列表,表示分类中实现的实例方法和类方法;
- 一个 protocol_list_t 类型的协议列表,表示分类中实现的协议;
- 一个 prop_list_t 类型的属性列表,表示分类中定义的属性,一般在分类中添加的属性都是通过关联对象来实现;
struct _category_t {
const char *name; // 类名称
struct _class_t *cls; // 类对象
const struct _method_list_t *instance_methods; // 实例化方法列表
const struct _method_list_t *class_methods; // 类方法列表
const struct _protocol_list_t *protocols; // 协议列表
const struct _prop_list_t *properties; // 属性列表
};
- 分类中的属性是没有 set、get 方法。
二、分类的加载流程
- 首先给 YDWPerson 类添加两个分类 DWA 和 DWB:
- 在iOS之深入解析类加载的底层原理:类如何加载到内存中?中,我们查看 realizeClassWithoutSwift -> methodizeClass -> attachToClass -> load_categories_nolock -> extAlloc ->attachCategories 中提及了 rwe 的加载,并且在 methodizeClass 实现中,发现“类的数据”和“分类的数据”是分开处理的,其主要原因是在编译阶段,就已经确定好了方法的归属位置(即实例方法存储在类中,类方法存储在元类中),而分类是后加进来的,如下:
// Methodizing for the first time
if (PrintConnecting) {
_objc_inform("CLASS: methodizing class '%s' %s",
cls->nameForLogging(), isMeta ? "(meta)" : "");
}
// Install methods and properties that the class implements itself.
method_list_t *list = ro->baseMethods();
if (list) {
prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
if (rwe) rwe->methods.attachLists(&list, 1);
}
property_list_t *proplist = ro->baseProperties;
if (rwe && proplist) {
rwe->properties.attachLists(&proplist, 1);
}
protocol_list_t *protolist = ro->baseProtocols;
if (rwe && protolist) {
rwe->protocols.attachLists(&protolist, 1);
}
// Root classes get bonus method implementations if they don't have
// them already. These apply before category replacements.
if (cls->isRootMetaclass()) {
// root metaclass
addMethod(cls, @selector(initialize), (IMP)&objc_noop_imp, "", NO);
}
// Attach categories.
if (previously) {
if (isMeta) {
objc::unattachedCategories.attachToClass(cls, previously,
ATTACH_METACLASS);
} else {
// When a class relocates, categories with class methods
// may be registered on the class itself rather than on
// the metaclass. Tell attachToClass to look for those.
objc::unattachedCategories.attachToClass(cls, previously,
ATTACH_CLASS_AND_METACLASS);
}
}
objc::unattachedCategories.attachToClass(cls, cls,
isMeta ? ATTACH_METACLASS : ATTACH_CLASS);
- 分类需要通过 attatchToClass 添加到类,然后才能在外界进行使用。分类的加载主要分为三步:
- 分类数据加载时机:根据类和分类是否实现 load 方法来区分不同的时机;
- attachCategories 准备分类数据;
- attachLists 将分类数据添加到主类中。
三、分类的加载时机
① 通过“attachCategories 准备分类数据”反推加载时机
- 在类加载到内存中的时候,当执行到 attachCategories 方法时,必然会有分类数据的加载,可以通过反推法查看在什么时候调用 attachCategories 的,通过查找,有两个方法中调用:
-
- load_categories_nolock 方法中:
// First, register the category with its target class.
// Then, rebuild the class's method lists (etc) if
// the class is realized.
if (cat->instanceMethods ||
cat->protocols ||
cat->instanceProperties) {
if (cls->isRealized()) {
attachCategories(cls, &lc, 1, ATTACH_EXISTING);
} else {
objc::unattachedCategories.addForClass(lc, cls);
}
}
if (cat->classMethods ||
cat->protocols ||
(hasClassProperties && cat->_classProperties)) {
if (cls->ISA()->isRealized()) {
attachCategories(cls->ISA(), &lc, 1, ATTACH_EXISTING | ATTACH_METACLASS);
} else {
objc::unattachedCategories.addForClass(lc, cls->ISA());
}
}
-
- 在 addToClass 方法中,经过调试发现,从来不会进到 if 流程中,除非加载两次,一般的类一般只会加载一次:
void attachToClass(Class cls, Class previously, int flags)
{
runtimeLock.assertLocked();
ASSERT((flags & ATTACH_CLASS) ||
(flags & ATTACH_METACLASS) ||
(flags & ATTACH_CLASS_AND_METACLASS));
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);
}
}
- 直接运行 objc 代码,从打印日志,可以发现 attachToClass 方法的下一步就是 load_categories_nolock 方法,进行加载分类数据。全局搜索 load_categories_nolock 的调用的地方,发现只有两次调用:
-
- loadAllCategories 方法:
static void loadAllCategories() {
mutex_locker_t lock(runtimeLock);
for (auto *hi = FirstHeader; hi != NULL; hi = hi->getNext()) {
load_categories_nolock(hi);
}
}
-
- 在 _read_images 方法,但是经过调试,是不会进入这个 if 判断流程的,而是走的 loadAllCategories:
// Discover categories. Only do this after the initial category
// attachment has been done. For categories present at startup,
// discovery is deferred until the first load_images call after
// the call to _dyld_objc_notify_register completes. rdar://problem/53119145
if (didInitialAttachCategories) {
for (EACH_HEADER) {
load_categories_nolock(hi);
}
}
ts.log("IMAGE TIMES: discover categories");
- 最后,全局搜索 loadAllCategories 看看在哪里地方进行了调用,发现只有 load_images 方法里面进行了调用:
void
load_images(const char *path __unused, const struct mach_header *mh)
{
if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
didInitialAttachCategories = true;
loadAllCategories();
}
// Return without taking locks if there are no +load methods here.
if (!hasLoadMethods((const headerType *)mh)) return;
recursive_mutex_locker_t lock(loadMethodLock);
// Discover load methods
{
mutex_locker_t lock2(runtimeLock);
prepare_load_methods((const headerType *)mh);
}
// Call +load methods (without runtimeLock - re-entrant)
call_load_methods();
}
② 通过堆栈信息分析
- 在 attachCategories 中加自定义逻辑的断点,bt 查看堆栈信息,如下:
- 在上述情况下分类的数据加载时机为 attachCategories -> load_categories_nolock -> loadAllCategories -> load_images,而分类的正常加载流程为 realizeClassWithoutSwift -> methodizeClass -> attachToClass ->attachCategories。
- 再来看看另外一种情况,主类和分类(DWA)实现 load,分类(DWB)则不实现:
- 调试如下:
p entry.cat
(category_t *const) $1 = 0x00000001000082b0
p $1
(category_t) $2 = {
name = 0x0000000100003cbf "DWB"
cls = 0x0000000100008430
instanceMethods = 0x0000000100008248
classMethods = 0x0000000000000000
protocols = 0x0000000000000000
instanceProperties = 0x0000000000000000
_classProperties = 0x0000000000000000
}
- 可以看到,只要有一个分类是非懒加载分类,那么所有的分类都会被标记为非懒加载分类。
四、分类和类的搭配使用
① 非懒加载类 + 非懒加载分类
- 非懒加载类 + 非懒加载分类,即主类实现 +load 方法,分类同样实现 +load 方法,类的数据加载是通过 _getObjc2NonlazyClassList 加载,即 ro、rw 的操作,在 extAlloc 方法中对 rwe 赋值初始化,分类的数据加载是通过 load_images 加载到类中的。
- 执行流程为 map_images -> map_images_nolock -> _read_images -> readClass -> _getObjc2NonlazyClassList -> realizeClassWithoutSwift -> methodizeClass -> attachToClass,此时的 mlists 是一维数组,然后再执行到 load_images 部分。
- load_images --> loadAllCategories -> load_categories_nolock -> load_categories_nolock -> attachCategories -> attachLists,此时的mlists 是二维数组。
② 非懒加载类与懒加载分类
- 非懒加载类与懒加载分类,即主类实现了+load 方法,分类未实现 +load 方法。
- 在 realizeClassWithoutSwift 中的自定义断点,查看 ro:
-
- 调试如下:
p kc_ro->baseMethodList
(method_list_t *const) $0 = 0x0000000100003038
p $0
(method_list_t) $1 = {
entsize_list_tt<method_t, method_list_t,3> = {
entsizeAndFlags = 24
count = 16
first = {
name = "dw_instanceMethod1"
types = 0x0000000100001f6d "v16@0:8"
imp = 0x0000000100001bc0 (DWObjc`-[YDWPerson(DWB) dw_instanceMethod1])
}
}
}
p $1.get(0)
(method_t) $2 = {
name = "dw_instanceMethod1"
types = 0x0000000100001f6d "v16@0:8"
imp = 0x0000000100001bc0 (DWObjc`-[YDWPerson(DWB) dw_instanceMethod1])
}
p $1.get(1)
(method_t) $3 = {
name = "cateB_2"
types = 0x0000000100001f6d "v16@0:8"
imp = 0x0000000100001bf0 (DWObjc`-[YDWPerson(DWB) cateB_2])
}
p $1.get(2)
(method_t) $3 = {
name = "cateB_1"
types = 0x0000000100001f6d "v16@0:8"
imp = 0x0000000100001c20 (DWObjc`-[YDWPerson(DWB) cateB_1])
}
p $1.get(3)
(method_t) $3 = {
name = "cateB_3"
types = 0x0000000100001f6d "v16@0:8"
imp = 0x0000000100001c50 (DWObjc`-[YDWPerson(DWB) cateB_3])
}
p $1.get(4)
(method_t) $3 = {
name = "dw_instanceMethod1"
types = 0x0000000100001f6d "v16@0:8"
imp = 0x0000000100001960 (DWObjc`-[YDWPerson(DWB) dw_instanceMethod1])
以上是关于iOS之深入解析类加载的底层原理:分类如何加载到类以及分类和类的配合使用的主要内容,如果未能解决你的问题,请参考以下文章
iOS之深入解析类方法+load与+initialize的底层原理
iOS经典面试题之深入解析分类Category的本质以及如何被加载