在项目开发中,都会遇到扩展已有类的情况,在iOS中,普遍使用继承,但是在Objective-C 2.0中,提供了category这个语言特性,可以动态的为已有的类添加新功能。下面我面就探究一下它的原理
Category简介
Category是Objective-C 2.0之后添加的语言特性,category的主要作用是为已经存在的类添加方法。除此之外,apple还推荐了category的另外两个使用场景1
- 可以把类的实现分开在几个不同的文件里面。这样做有几个显而易见的好处,a)可以减少单个文件的体积 b)可以把不同的功能组织到不同的category里 c)可以由多个开发者共同完成一个类 d)可以按需加载想要的category 等等。
- 声明私有方法
不过除了apple推荐的使用场景,广大开发者脑洞大开,还衍生出了category的其他几个使用场景:
- 模拟多继承
- 把framework的私有方法公开
Category和Extension比较
Extension看起来很像一个匿名的Category,但是Extension和有名字的Category几乎完全是两个东西,Extension编译期决议,它就是类的一部分,在编译期和头文件里的@interface以及和实现文件里的@implement一起形成一个完整的类,它随着类的产生而产生,亦随之一起消亡,Extension一般用来隐藏类的私有信息,你必须有一个类的源码才能为一个类添加Extension,所以你无法为系统的类添加Extension(详见2)
但是Category实在运行期决议的。
就category和extension的区别来看,我们可以推导出一个明显的事实,extension可以添加实例变量,而category是无法添加实例变量的(因为在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的)。
Category实现
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;
};
创建一个类的分类,然后用clang编译器编译源文件,得到一个.cpp文件,就可以在里面看到。
image.png
在iOS中,所有的OC类和对象,在runtime层都是用struct表示的,Category也是一样,它使用_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_t结构体中可以给分类添加实例方法、类方法、协议和属性,但是不能添加实例变量。
image.png
image.png
首先先定义一个Person类和一个Person+Run的分类,使用clang -rewrite-objc Person+Run.m之后,我们得到一个大约3.5M的.cpp文件,在文件的最后
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;
};
extern "C" __declspec(dllimport) struct objc_cache _objc_empty_cache;
#pragma warning(disable:4273)
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Run __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"run", "v16@0:8", (void *)_I_Person_Run_run}}
};
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_Person_$_Run __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"run", "v16@0:8", (void *)_C_Person_Run_run}}
};
static const char *_OBJC_PROTOCOL_METHOD_TYPES_NSCopying [] __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"@24@0:8^{_NSZone=}16"
};
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_PROTOCOL_INSTANCE_METHODS_NSCopying __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"copyWithZone:", "@24@0:8^{_NSZone=}16", 0}}
};
struct _protocol_t _OBJC_PROTOCOL_NSCopying __attribute__ ((used)) = {
0,
"NSCopying",
0,
(const struct method_list_t *)&_OBJC_PROTOCOL_INSTANCE_METHODS_NSCopying,
0,
0,
0,
0,
sizeof(_protocol_t),
0,
(const char **)&_OBJC_PROTOCOL_METHOD_TYPES_NSCopying
};
struct _protocol_t *_OBJC_LABEL_PROTOCOL_$_NSCopying = &_OBJC_PROTOCOL_NSCopying;
static struct /*_protocol_list_t*/ {
long protocol_count; // Note, this is 32/64 bit
struct _protocol_t *super_protocols[1];
} _OBJC_CATEGORY_PROTOCOLS_$_Person_$_Run __attribute__ ((used, section ("__DATA,__objc_const"))) = {
1,
&_OBJC_PROTOCOL_NSCopying
};
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_Person_$_Run __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_prop_t),
2,
{{"name","T@\"NSString\",C,N"},
{"height","Td,N"}}
};
extern "C" __declspec(dllimport) struct _class_t OBJC_CLASS_$_Person;
static struct _category_t _OBJC_$_CATEGORY_Person_$_Run __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"Person",
0, // &OBJC_CLASS_$_Person,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Run,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Run,
(const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_Person_$_Run,
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Person_$_Run,
};
static void OBJC_CATEGORY_SETUP_$_Person_$_Run(void ) {
_OBJC_$_CATEGORY_Person_$_Run.cls = &OBJC_CLASS_$_Person;
}
#pragma section(".objc_inithooks$B", long, read, write)
__declspec(allocate(".objc_inithooks$B")) static void *OBJC_CATEGORY_SETUP[] = {
(void *)&OBJC_CATEGORY_SETUP_$_Person_$_Run,
};
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
&_OBJC_$_CATEGORY_Person_$_Run,
};
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
- 首先编译器生成了实例方法列表OBJC$CATEGORY_INSTANCE_METHODS_Person$_Run 和类方法列表OBJC$CATEGORY_CLASS_METHODS_Person$_Run,而且这两个方法列表中都有自己实现的方法“run”
_OBJC_PROTOCOL_INSTANCE_METHODS_NSCopying中添加了遵循的协议
OBJC$PROP_LIST_Person$_Run 中添加了属性 - 其次,编译器商城了category本身OBJC$CATEGORY_Person$_Run,并且初始化category本身
- 最后,编译器在DATA段下的__objc_catlist,regular,no_dead_strip中保存了一个大小为1的category_t数组L_OBJC_LABEL_CATEGORY$,如果是多个就会增加数组的长度,用于运行期category的加载,到此为止,编译器的工作就已经完成。
Category的加载
iOS的运行时是依赖OC的runtime来实现的,OS X和iOS通过dyld动态加载。
在runtime源码中的objc-os.mm文件中,找到OC的入口方法
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
environ_init();
tls_init();
static_init();
lock_init();
exception_init();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
category呗附加到类上面是在map_iamges的时候发生的,在new-ABI标准下,_objc_init里面调用的map_images最终会调用objc-runtime-new.mm里面的_read_images方法,而在_read_images方法的结尾,有以下的代码片段:
// Discover categories.
for (EACH_HEADER) {
category_t **catlist =
_getObjc2CategoryList(hi, &count);
bool hasClassProperties = hi->info()->hasCategoryClassProperties();
for (i = 0; i < count; i++) {
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);
if (!cls) {
// Category's target class is missing (probably weak-linked).
// Disavow any knowledge of this category.
catlist[i] = nil;
if (PrintConnecting) {
_objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
"missing weak-linked target class",
cat->name, cat);
}
continue;
}
// Process this category.
// First, register the category with its target class.
// Then, rebuild the class's method lists (etc) if
// the class is realized.
bool classExists = NO;
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
addUnattachedCategoryForClass(cat, cls, hi);
if (cls->isRealized()) {
remethodizeClass(cls);
classExists = YES;
}
if (PrintConnecting) {
_objc_inform("CLASS: found category -%s(%s) %s",
cls->nameForLogging(), cat->name,
classExists ? "on existing class" : "");
}
}
if (cat->classMethods || cat->protocols
|| (hasClassProperties && cat->_classProperties))
{
addUnattachedCategoryForClass(cat, cls->ISA(), hi);
if (cls->ISA()->isRealized()) {
remethodizeClass(cls->ISA());
}
if (PrintConnecting) {
_objc_inform("CLASS: found category +%s(%s)",
cls->nameForLogging(), cat->name);
}
}
}
}
首先,我们拿到的catlist就是上节中讲到的编译器为我们准备的category_t数组,把category的实例方法、协议以及属性添加到类上,把category的类方法和协议添加到类的metaclass上。
值得注意的是,在代码中有一小段注释 / || cat->classProperties /,看来苹果有过给类添加属性的计划啊。
ok,我们接着往里看,category的各种列表是怎么最终添加到类上的,就拿实例方法列表来说吧:
在上述的代码片段里,addUnattachedCategoryForClass只是把类和category做一个关联映射,而remethodizeClass才是真正去处理添加事宜的功臣。
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);
}
}
而对于添加类的实例方法而言,又会去调用attachCategoryMethods这个方法,我们去看下attachCategories
// oldest categories first.
static void
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);
bool isMeta = cls->isMetaClass();
// fixme rearrange to remove these intermediate allocations
method_list_t **mlists = (method_list_t **)
malloc(cats->count * sizeof(*mlists));
property_list_t **proplists = (property_list_t **)
malloc(cats->count * sizeof(*proplists));
protocol_list_t **protolists = (protocol_list_t **)
malloc(cats->count * sizeof(*protolists));
// Count backwards through cats to get newest categories first
int mcount = 0;
int propcount = 0;
int protocount = 0;
int i = cats->count;
bool fromBundle = NO;
while (i--) {
auto& entry = cats->list[i];
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
mlists[mcount++] = mlist;
fromBundle |= entry.hi->isBundle();
}
property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
proplists[propcount++] = proplist;
}
protocol_list_t *protolist = entry.cat->protocols;
if (protolist) {
protolists[protocount++] = protolist;
}
}
auto rw = cls->data();
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
rw->methods.attachLists(mlists, mcount);
free(mlists);
if (flush_caches && mcount > 0) flushCaches(cls);
rw->properties.attachLists(proplists, propcount);
free(proplists);
rw->protocols.attachLists(protolists, protocount);
free(protolists);
}
attachCategories做的工作相对比较简单,它只是把所有category的实例方法列表拼成了一个大的实例方法列表,然后转交给了prepareMethodLists方法
static void
prepareMethodLists(Class cls, method_list_t **addedLists, int addedCount,
bool baseMethods, bool methodsFromBundle)
{
runtimeLock.assertWriting();
if (addedCount == 0) return;
// Don't scan redundantly
bool scanForCustomRR = !cls->hasCustomRR();
bool scanForCustomAWZ = !cls->hasCustomAWZ();
// There exist RR/AWZ special cases for some class's base methods.
// But this code should never need to scan base methods for RR/AWZ:
// default RR/AWZ cannot be set before setInitialized().
// Therefore we need not handle any special cases here.
if (baseMethods) {
assert(!scanForCustomRR && !scanForCustomAWZ);
}
// Add method lists to array.
// Reallocate un-fixed method lists.
// The new methods are PREPENDED to the method list array.
for (int i = 0; i < addedCount; i++) {
method_list_t *mlist = addedLists[i];
assert(mlist);
// Fixup selectors if necessary
if (!mlist->isFixedUp()) {
fixupMethodList(mlist, methodsFromBundle, true/*sort*/);
}
// Scan for method implementations tracked by the class's flags
if (scanForCustomRR && methodListImplementsRR(mlist)) {
cls->setHasCustomRR();
scanForCustomRR = false;
}
if (scanForCustomAWZ && methodListImplementsAWZ(mlist)) {
cls->setHasCustomAWZ();
scanForCustomAWZ = false;
}
}
}
需要注意的有两点:
1)、category的方法没有“完全替换掉”原来类已经有的方法,也就是说如果category和原来类都有methodA,那么category附加完成之后,类的方法列表里会有两个methodA
2)、category的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,这也就是我们平常所说的category的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会罢休_,殊不知后面可能还有一样名字的方法。
Category+load方法
我们知道,在类和category中都可以有+load方法,那么有两个问题:
1)、在类的+load方法调用的时候,我们可以调用category中声明的方法么?
2)、这么些个+load方法,调用顺序是咋样的呢?
首先先做一个实验,我们定义两个category并且在里面都定义并且实现了一个相同的方法run
image.png
image.png
image.png
我们在main函数中引用文件,并且创建对象执行方法
image.png
运行结果后
image.png
当我们注释掉Person+Test文件后
image.png
奇迹出现了,运行结果仍然是Person+Test文件中的run方法
image.png
我们在两个category中都实现+(void)load方法,并且在Xcode中点击Edit Scheme,添加如下两个环境变量OBJC_PRINT_LOAD_METHODS,OBJC_PRINT_REPLACE_METHODS(可以在执行load方法以及加载category的时候打印log信息,更多的环境变量选项可参见objc-private.h):
image.png
运行后得到的信息
objc[10123]: LOAD: class 'NSObject' scheduled for +load
objc[10123]: LOAD: class 'OS_dispatch_queue' scheduled for +load
objc[10123]: LOAD: class 'OS_dispatch_source' scheduled for +load
objc[10123]: LOAD: class 'OS_dispatch_mach' scheduled for +load
objc[10123]: LOAD: class 'OS_dispatch_queue_runloop' scheduled for +load
objc[10123]: LOAD: class 'OS_dispatch_semaphore' scheduled for +load
objc[10123]: LOAD: class 'OS_dispatch_group' scheduled for +load
objc[10123]: LOAD: class 'OS_dispatch_queue_serial' scheduled for +load
objc[10123]: LOAD: class 'OS_dispatch_queue_concurrent' scheduled for +load
objc[10123]: LOAD: class 'OS_dispatch_queue_main' scheduled for +load
objc[10123]: LOAD: class 'OS_dispatch_queue_root' scheduled for +load
objc[10123]: LOAD: class 'OS_dispatch_queue_network_event' scheduled for +load
objc[10123]: LOAD: class 'OS_dispatch_queue_mgr' scheduled for +load
objc[10123]: LOAD: class 'OS_dispatch_queue_specific_queue' scheduled for +load
objc[10123]: LOAD: class 'OS_dispatch_queue_attr' scheduled for +load
objc[10123]: LOAD: class 'OS_dispatch_mach_msg' scheduled for +load
objc[10123]: LOAD: class 'OS_dispatch_io' scheduled for +load
objc[10123]: LOAD: class 'OS_dispatch_operation' scheduled for +load
objc[10123]: LOAD: class 'OS_dispatch_disk' scheduled for +load
objc[10123]: LOAD: class 'OS_voucher' scheduled for +load
objc[10123]: LOAD: class 'OS_dispatch_data_empty' scheduled for +load
objc[10123]: LOAD: +[NSObject load]
objc[10123]: LOAD: +[OS_dispatch_queue load]
objc[10123]: LOAD: +[OS_dispatch_source load]
objc[10123]: LOAD: +[OS_dispatch_mach load]
objc[10123]: LOAD: +[OS_dispatch_queue_runloop load]
objc[10123]: LOAD: +[OS_dispatch_semaphore load]
objc[10123]: LOAD: +[OS_dispatch_group load]
objc[10123]: LOAD: +[OS_dispatch_queue_serial load]
objc[10123]: LOAD: +[OS_dispatch_queue_concurrent load]
objc[10123]: LOAD: +[OS_dispatch_queue_main load]
objc[10123]: LOAD: +[OS_dispatch_queue_root load]
objc[10123]: LOAD: +[OS_dispatch_queue_network_event load]
objc[10123]: LOAD: +[OS_dispatch_queue_mgr load]
objc[10123]: LOAD: +[OS_dispatch_queue_specific_queue load]
objc[10123]: LOAD: +[OS_dispatch_queue_attr load]
objc[10123]: LOAD: +[OS_dispatch_mach_msg load]
objc[10123]: LOAD: +[OS_dispatch_io load]
objc[10123]: LOAD: +[OS_dispatch_operation load]
objc[10123]: LOAD: +[OS_dispatch_disk load]
objc[10123]: LOAD: +[OS_voucher load]
objc[10123]: LOAD: +[OS_dispatch_data_empty load]
objc[10123]: LOAD: class 'OS_os_log' scheduled for +load
objc[10123]: LOAD: class 'OS_os_activity' scheduled for +load
objc[10123]: LOAD: +[OS_os_log load]
objc[10123]: LOAD: +[OS_os_activity load]
objc[10123]: LOAD: class 'OS_xpc_connection' scheduled for +load
objc[10123]: LOAD: class 'OS_xpc_service' scheduled for +load
objc[10123]: LOAD: class 'OS_xpc_null' scheduled for +load
objc[10123]: LOAD: class 'OS_xpc_bool' scheduled for +load
objc[10123]: LOAD: class 'OS_xpc_int64' scheduled for +load
objc[10123]: LOAD: class 'OS_xpc_uint64' scheduled for +load
objc[10123]: LOAD: class 'OS_xpc_double' scheduled for +load
objc[10123]: LOAD: class 'OS_xpc_pointer' scheduled for +load
objc[10123]: LOAD: class 'OS_xpc_date' scheduled for +load
objc[10123]: LOAD: class 'OS_xpc_data' scheduled for +load
objc[10123]: LOAD: class 'OS_xpc_string' scheduled for +load
objc[10123]: LOAD: class 'OS_xpc_uuid' scheduled for +load
objc[10123]: LOAD: class 'OS_xpc_fd' scheduled for +load
objc[10123]: LOAD: class 'OS_xpc_shmem' scheduled for +load
objc[10123]: LOAD: class 'OS_xpc_mach_send' scheduled for +load
objc[10123]: LOAD: class 'OS_xpc_array' scheduled for +load
objc[10123]: LOAD: class 'OS_xpc_dictionary' scheduled for +load
objc[10123]: LOAD: class 'OS_xpc_error' scheduled for +load
objc[10123]: LOAD: class 'OS_xpc_endpoint' scheduled for +load
objc[10123]: LOAD: class 'OS_xpc_serializer' scheduled for +load
objc[10123]: LOAD: class 'OS_xpc_pipe' scheduled for +load
objc[10123]: LOAD: class 'OS_xpc_mach_recv' scheduled for +load
objc[10123]: LOAD: class 'OS_xpc_bundle' scheduled for +load
objc[10123]: LOAD: class 'OS_xpc_service_instance' scheduled for +load
objc[10123]: LOAD: class 'OS_xpc_activity' scheduled for +load
objc[10123]: LOAD: class 'OS_xpc_file_transfer' scheduled for +load
objc[10123]: LOAD: +[OS_xpc_connection load]
objc[10123]: LOAD: +[OS_xpc_service load]
objc[10123]: LOAD: +[OS_xpc_null load]
objc[10123]: LOAD: +[OS_xpc_bool load]
objc[10123]: LOAD: +[OS_xpc_int64 load]
objc[10123]: LOAD: +[OS_xpc_uint64 load]
objc[10123]: LOAD: +[OS_xpc_double load]
objc[10123]: LOAD: +[OS_xpc_pointer load]
objc[10123]: LOAD: +[OS_xpc_date load]
objc[10123]: LOAD: +[OS_xpc_data load]
objc[10123]: LOAD: +[OS_xpc_string load]
objc[10123]: LOAD: +[OS_xpc_uuid load]
objc[10123]: LOAD: +[OS_xpc_fd load]
objc[10123]: LOAD: +[OS_xpc_shmem load]
objc[10123]: LOAD: +[OS_xpc_mach_send load]
objc[10123]: LOAD: +[OS_xpc_array load]
objc[10123]: LOAD: +[OS_xpc_dictionary load]
objc[10123]: LOAD: +[OS_xpc_error load]
objc[10123]: LOAD: +[OS_xpc_endpoint load]
objc[10123]: LOAD: +[OS_xpc_serializer load]
objc[10123]: LOAD: +[OS_xpc_pipe load]
objc[10123]: LOAD: +[OS_xpc_mach_recv load]
objc[10123]: LOAD: +[OS_xpc_bundle load]
objc[10123]: LOAD: +[OS_xpc_service_instance load]
objc[10123]: LOAD: +[OS_xpc_activity load]
objc[10123]: LOAD: +[OS_xpc_file_transfer load]
objc[10123]: LOAD: class '__IncompleteProtocol' scheduled for +load
objc[10123]: LOAD: class 'Protocol' scheduled for +load
objc[10123]: LOAD: class '__NSUnrecognizedTaggedPointer' scheduled for +load
objc[10123]: LOAD: +[__IncompleteProtocol load]
objc[10123]: LOAD: +[Protocol load]
objc[10123]: LOAD: +[__NSUnrecognizedTaggedPointer load]
objc[10123]: LOAD: category 'NSObject(NSObject)' scheduled for +load
objc[10123]: LOAD: +[NSObject(NSObject) load]
objc[10123]: LOAD: class 'NSMergePolicy' scheduled for +load
objc[10123]: LOAD: +[NSMergePolicy load]
objc[10123]: LOAD: category 'NSObject(NSObject)' scheduled for +load
objc[10123]: LOAD: +[NSObject(NSObject) load]
objc[10123]: LOAD: class 'NSApplication' scheduled for +load
objc[10123]: LOAD: class 'NSBinder' scheduled for +load
objc[10123]: LOAD: class 'NSColorSpaceColor' scheduled for +load
objc[10123]: LOAD: class 'NSNextStepFrame' scheduled for +load
objc[10123]: LOAD: +[NSApplication load]
objc[10123]: LOAD: +[NSBinder load]
objc[10123]: LOAD: +[NSColorSpaceColor load]
objc[10123]: LOAD: +[NSNextStepFrame load]
objc[10123]: LOAD: class '_MediaServicesOSLog' scheduled for +load
objc[10123]: LOAD: +[_MediaServicesOSLog load]
objc[10123]: LOAD: class 'Person' scheduled for +load
objc[10123]: LOAD: category 'Person(Run)' scheduled for +load
objc[10123]: LOAD: category 'Person(Test)' scheduled for +load
objc[10123]: LOAD: +[Person load]
objc[10123]: LOAD: +[Person(Run) load]
objc[10123]: LOAD: +[Person(Test) load]
所以,对于上面两个问题,答案是很明显的:
1)、可以调用,因为附加category到类的工作会先于+load方法的执行
2)、+load的执行顺序是先类,后category,而category的+load执行顺序是根据编译顺序决定的。
目前的编译顺序是这样的:
image.png
调整两个category的编译顺序之后
image.png
控制台的输出为
image.png
虽然对于+load的执行顺序是这样,但是对于“覆盖”掉的方法,则会先找到最后一个编译的category里的对应方法。
怎么调用到原来类中被category覆盖掉的方法?
对于这个问题,我们已经知道category其实并不是完全替换掉原来类的同名方法,只是category在方法列表的前面而已,所以我们只要顺着方法列表找到最后一个对应名字的方法,就可以调用原来类的方法。
Category和关联对象
我们知道在category里面是无法为category添加实例变量的。但是我们很多时候需要在category中添加和对象关联的值,这个时候可以求助关联对象来实现
image.png
image.png
但是关联对象又是存在什么地方呢? 如何存储? 对象销毁时候如何处理关联对象呢?
我们去翻一下runtime的源码,在objc-references.mm文件中有个方法_object_set_associative_reference:
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
// retain the new value (if any) outside the lock.
ObjcAssociation old_association(0, nil);
id new_value = value ? acquireValue(value, policy) : nil;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
disguised_ptr_t disguised_object = DISGUISE(object);
if (new_value) {
// break any existing association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// secondary table exists
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
j->second = ObjcAssociation(policy, new_value);
} else {
(*refs)[key] = ObjcAssociation(policy, new_value);
}
} else {
// create the new association (first time).
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
object->setHasAssociatedObjects();
}
} else {
// setting the association to nil breaks the association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
refs->erase(j);
}
}
}
}
// release the old value (outside of the lock).
if (old_association.hasValue()) ReleaseValue()(old_association);
}
我们可以看到所有的关联对象都由AssociationsManager管理,而AssociationsManager定义如下:
class AssociationsManager {
// associative references: object pointer -> PtrPtrHashMap.
static AssociationsHashMap *_map;
public:
AssociationsManager() { AssociationsManagerLock.lock(); }
~AssociationsManager() { AssociationsManagerLock.unlock(); }
AssociationsHashMap &associations() {
if (_map == NULL)
_map = new AssociationsHashMap();
return *_map;
}
};
AssociationsManager里面是由一个静态AssociationsHashMap来存储所有的关联对象的。这相当于把所有对象的关联对象都存在一个全局map里面。而map的的key是这个对象的指针地址(任意两个不同对象的指针地址一定是不同的),而这个map的value又是另外一个AssociationsHashMap,里面保存了关联对象的kv对。
而在对象的销毁逻辑里面,见objc-runtime-new.mm:
void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();
// This order is important.
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj);
obj->clearDeallocating();
}
return obj;
}
嗯,runtime的销毁对象函数objc_destructInstance里面会判断这个对象有没有关联对象,如果有,会调用_object_remove_assocations做关联对象的清理工作。
ps:由于本有能力有限,有关runtime源码解析的部分是查找资料得来的。
网友评论