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的底层原理

iOS经典面试题之深入解析分类Category的本质以及如何被加载

iOS之深入解析单例的实现和销毁的底层原理

猿创征文|iOS经典面试题之深入解析分类Category的本质以及如何被加载

iOS之深入解析YYModel的底层原理