1、原理
分类的实例方法最终会合并存在类对象中。
分类的类方法最终会合并在元类中
- 底层实现
合并方法列表的时间是在运行时。通过runtime动态将分类合并入对应的地方
通过 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc 方法可以看到源码,分类
#import "HXHPerson.h"
NS_ASSUME_NONNULL_BEGIN
@interface HXHPerson (Test)
- (void)test;
+ (void)test2;
@end
NS_ASSUME_NONNULL_END
的结构如下
///可以看到里边可以放属性,协议,实例方法,类方法。
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;
};
下边是怎么赋值
static struct _category_t _OBJC_$_CATEGORY_HXHPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"HXHPerson",
0, // &OBJC_CLASS_$_HXHPerson,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_HXHPerson_$_Test,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_HXHPerson_$_Test,
0,
0,
};
///这是类方法列表的赋值
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_CLASS_METHODS_HXHPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"test2", "v16@0:8", (void *)_C_HXHPerson_Test_test2}}
};
这里可看到,它把值赋给了结构体对应的元素(这里边我没有写属性和协议,所以下边是两个0)
把分类合并入原来类的核心实现为(代码可在官网的runtime找到)
/***********************************************************************
* remethodizeClass
* Attach outstanding categories to an existing class.
* Fixes up cls's method list, protocol list, and property list.
* Updates method caches for cls and its subclasses.
* Locking: runtimeLock must be held by the caller
**********************************************************************/
static void remethodizeClass(Class cls)
{
category_list *cats;
bool isMeta;
runtimeLock.assertWriting();
isMeta = cls->isMetaClass();
// Re-methodizing: check for more categories
if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
if (PrintConnecting) {
_objc_inform("CLASS: attaching categories to class '%s' %s",
cls->nameForLogging(), isMeta ? "(meta)" : "");
}
attachCategories(cls, cats, true /*flush caches*/);
free(cats);
}
}
上述方法的调用时机为调用 _read_images 读取镜像时。
合并所有分类之后
最终方法列表存储为了二元数组(下边源码为对应实现)
///addedLists:分类列表
///addedCount:分类数量
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
if (hasArray()) {
// many lists -> many lists
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
//首先扩容
///array()->lists原来的方法列表
///将原来的数据,往后挪动addedCount这么多位。
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
///拷贝新的二维数组到原来列表指向的地址
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
else if (!list && addedCount == 1) {
// 0 lists -> 1 list
list = addedLists[0];
}
else {
// 1 list -> many lists
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;
if (oldList) array()->lists[addedCount] = oldList;
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
}
总结:加载过程/实现原理
pre、首先,编译时会再入内存(这个时候load方法已经执行)。
1、通过runtime加载某个类的所有category数据;
2、把所有category的方法、属性、协议数据,合并到一个大数组中;(后面参与编译的category数据回在数组的前面)
3、将合并后的分类数据(方法、属性、协议)插入到类原来数据的前面。
要关注的runtime源码方法:
类:objc-os.mm
- _objc_init ///类载入的入口方法
- map_images
- map_images_nolock
类:objc-runtime-new.mm
- _read_images
- remethodizeClass
- attachCategories
- attachLists
源码写的很清楚,是怎么把分类数组添加到本来的类对象中。
需要思考的是为什么要这么做?
下边是一些思考题
1、为什么要用二维数组存这些方法列表?
2、经过这么操作之后,分类中的方法会优先调用,因为他的地址更靠前。那如果两个分类都有呢,谁更靠前?
这个取决去编译顺序。最后面参与编译的,会被放在前边。
3、分类和类扩展(有人会叫他匿名分类)的区别?
category分类编译之后的底层结构是struct category_t, 里面存储着分类的对象方法,类方法,属性,协议信息,在程序运行的时候,runtime会将category数据合并到类信息中。
class extension类拓展在编译的时候它的数据就已经包含在类信息中。
4、category能否添加成员变量?
5、category中有load方法嘛?load方法是什么时候调用的?load方法能继承么?
有。
runtime加载类和分类时调用。不管你用不用,载入内存的时候就已经调用。
可以继承。但一般不会去主动调用load,如果调用的话,机制就变成了消息发送机制(可以去复习消息发送机制的调用顺序)。不再是load_images载入镜像时那种调用所有load。
源码如下
/***********************************************************************
* load_images
* Process +load in the given images which are being mapped in by dyld.
* Calls ABI-agnostic code after taking ABI-specific locks.
*
* Locking: write-locks runtimeLock and loadMethodLock
**********************************************************************/
const char *
load_images(enum dyld_image_states state, uint32_t infoCount,
const struct dyld_image_info infoList[])
{
bool found;
// Return without taking locks if there are no +load methods here.
found = false;
for (uint32_t i = 0; i < infoCount; i++) {
if (hasLoadMethods((const headerType *)infoList[i].imageLoadAddress)) {
found = true;
break;
}
}
if (!found) return nil;
recursive_mutex_locker_t lock(loadMethodLock);
// Discover load methods
{
rwlock_writer_t lock2(runtimeLock);
found = load_images_nolock(state, infoCount, infoList);
}
// Call +load methods (without runtimeLock - re-entrant)
if (found) {
call_load_methods();
}
return nil;
}
此方法为载入镜像,它是objc_init 类初始化其中的一步,load_images最后一步为调用load方法 call_load_methods() ,found是判断是否已经载入过方法列表。
2、load方法
- +load方法会在runtime加载类,分类时调用
- 每个类,分类的+load方法,在程序运行过程中只调用一次
- 调用顺序
1、先调用类的load
按照编译先后顺序调用(先编译,先调用)
调用子类的load之前会先调用父类的+load
2、再调用分类的load
按照编译先后顺序调用。
///载入镜像时调用,准备方法列表
void prepare_load_methods(const headerType *mhdr)
{
size_t count, i;
runtimeLock.assertWriting();
classref_t *classlist =
_getObjc2NonlazyClassList(mhdr, &count);
for (i = 0; i < count; i++) {
schedule_class_load(remapClass(classlist[i]));
}
category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
for (i = 0; i < count; i++) {
category_t *cat = categorylist[i];
Class cls = remapClass(cat->cls);
if (!cls) continue; // category for ignored weak-linked class
realizeClass(cls);
assert(cls->ISA()->isRealized());
add_category_to_loadable_list(cat);
}
}
///递归方法会先去调用父类的方法存到数组,所以load执行顺序为先调用父类的load
static void schedule_class_load(Class cls)
{
if (!cls) return;
assert(cls->isRealized()); // _read_images should realize
if (cls->data()->flags & RW_LOADED) return;
// Ensure superclass-first ordering
schedule_class_load(cls->superclass);
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}
问题1:为何还能正常调用原来的方法?别的普通方法都是覆盖
void call_load_methods(void)
{
static bool loading = NO;
bool more_categories;
loadMethodLock.assertLocked();
// Re-entrant calls do nothing; the outermost call will finish the job.
if (loading) return;
loading = YES;
void *pool = objc_autoreleasePoolPush();
do {
// 1. Repeatedly call class +loads until there aren't any more
while (loadable_classes_used > 0) {
call_class_loads();
}
// 2. Call category +loads ONCE
more_categories = call_category_loads();
// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);
objc_autoreleasePoolPop(pool);
loading = NO;
}
可以看到。load方法的调用不是通过寻找类对象中的方法列表,而是直接for循环分类的所有load方法的来调用。
普通方法是消息机制调用。
3、initialize
3.1、如果分类实现了initialize,就会覆盖分类的initialize;
3.2、如果子类没有实现initialize,就会调用父类的initialize;
问题1、initialize和load的区别?
方向1:调用方式和时机
load首次调用是遍历所有类和分类的load方法地址,直接调用。
initialize是类第一次接收到消息时调用 走的是消息机制,会先调用父类initialize,再调用子类initialize。
方向2:次数 初始化的时候,每一个类只会initialize一次,父类的initialize方法可能调用多次。
方向3:调用顺序
load是先调用类的load(按编译顺序),再去调用分类的load
调用子类load之前会先调用父类的load
initialize
1> 先初始化父类
2> 再初始化子类(可能最终调用的是父类的initialize)