本文将对category的源码进行比较全面的整理分析,最后结合一些面试题进行总结,但愿对读者有所裨益。
GitHub Repo:iOSDeepAnalyse
Follow: MisterBooo · GitHub
Source: Category:从底层原理研究到面试题分析
公众号:五分钟学算法html
本节代码基于如下的代码进行编译研究:git
@interface Person : NSObject
- (void)instanceRun;
+ (void)methodRun;
@property(nonatomic, copy) NSString *name;
@end
复制代码
@interface Person (Eat)
@property(nonatomic, assign) int age;
- (void)instanceEat;
+ (void)methodEat;
@end
复制代码
@interface Person (Drink)
- (void)instanceEat;
@property(nonatomic, copy) NSString *waters;
@end
复制代码
经过objc4中的源码进行分析,能够在objc-runtime-new.h
中找到Category
的结构以下github
struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods;
struct method_list_t *classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};
复制代码
不难发如今这个结构体重存储着对象方法、类方法、协议和属性。接下来咱们来验证一下咱们刚刚本身编写的Person+Eat.m
这个分类在编译时是不是这种结构。面试
经过算法
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person+Eat.m
数组
命令将Person+Eat.m
文件编译成cpp
文件,如下的源码分析基于Person+Eat.cpp
里面的代码。下面让咱们开始窥探Category的底层结构吧~bash
将Person+Eat.cpp
的代码滑到底部部分,能够看见一个名为_category_t
的结构体,这就是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;// 属性列表
};
复制代码
Person+Eat.m
这个分类的结构也是符合_category_t
这种形式的app
static struct _category_t _OBJC_$_CATEGORY_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"Person",
0, // &OBJC_CLASS_$_Person,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Eat, // 对象方法列表
(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Eat,// 类方法列表
(const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_Person_$_Eat, // 协议列表
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Person_$_Eat, // 属性列表
};
复制代码
咱们开始来分析上面这个结构体的内部成员,其中Person
表示类名iphone
_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Eat
是对象方法列表,在Person+Eat.cpp
文件中能够找到_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Eat
具体描述
_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"instanceEat", "v16@0:8", (void *)_I_Person_Eat_instanceEat}}
};
复制代码
instanceEat
就是咱们上述实现的Person+Eat
分类里面的实例方法。
_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Eat
是类方法列表,在Person+Eat.cpp
中具体描述以下
_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"classEat", "v16@0:8", (void *)_C_Person_Eat_classEat}}
};
复制代码
_OBJC_CATEGORY_PROTOCOLS_$_Person_$_Eat
是协议列表,在Person+Eat.cpp
中具体描述以下
_OBJC_CATEGORY_PROTOCOLS_$_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
2,
&_OBJC_PROTOCOL_NSCopying,
&_OBJC_PROTOCOL_NSCoding
};
复制代码
_OBJC_$_PROP_LIST_Person_$_Eat
是属性列表,在Person+Eat.cpp
中具体描述以下
_OBJC_$_PROP_LIST_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_prop_t),
2,
{{"weight","Ti,N"},
{"height","Ti,N"}}
};
复制代码
经过上面的分析,咱们验证了编写一个分类的时候,在编译期间,这个分类内部的确会有category_t
这种数据结构,那么这种数据结构是如何做用到这个类的呢?分类的方法和类的方法调用的逻辑是怎么样的呢?咱们接下来回到objc4源码中进一步分析Category
的加载处理过程来揭晓Category
的神秘面纱。
咱们按照以下函数的调用顺序,一步一步的研究Category
的加载处理过程
void _objc_init(void);
└── void map_images(...);
└── void map_images_nolock(...);
└── void _read_images(...);
└── void _read_images(...);
└── static void remethodizeClass(Class cls);
└──attachCategories(Class cls, category_list *cats, bool flush_caches);
复制代码
文件名 | 方法 |
---|---|
objc-os.mm | _objc_init |
objc-os.mm | map_images |
objc-os.mm | map_images_nolock |
objc-runtime-new.mm | _read_images |
objc-runtime-new.mm | remethodizeClass |
objc-runtime-new.mm | attachCategories |
objc-runtime-new.mm | attachLists |
在iOS 程序 main 函数以前发生了什么 中提到,_objc_init
这个函数是runtime的初始化函数,那咱们从_objc_init
这个函数开始进行分析。
/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
**********************************************************************/
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);
}
复制代码
接着咱们来到 &map_images
读取资源(images这里表明资源模块),来到map_images_nolock
函数中找到_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) {
catlist[i] = nil;
if (PrintConnecting) {
_objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
"missing weak-linked target class",
cat->name, cat);
}
continue;
}
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);
}
}
}
}
复制代码
在上面的代码中,主要作了如下的事情
category
列表list
category list
中的每个category
category
的cls
,若是没有cls
,则跳过(continue
)这个继续获取下一个cat
有实例方法、协议、属性,则调用addUnattachedCategoryForClass
,同时若是cls
有实现的话,就进一步调用remethodizeClass
方法cat
有类方法、协议,则调用addUnattachedCategoryForClass
,同时若是cls
的元类有实现的话,就进一步调用remethodizeClass
方法其中4
,5
两步的区别主要是cls
是类对象仍是元类对象的区别,咱们接下来主要是看在第4
步中的addUnattachedCategoryForClass
和remethodizeClass
方法。
static void addUnattachedCategoryForClass(category_t *cat, Class cls,
header_info *catHeader)
{
runtimeLock.assertWriting();
// DO NOT use cat->cls! cls may be cat->cls->isa instead
NXMapTable *cats = unattachedCategories();
category_list *list;
list = (category_list *)NXMapGet(cats, cls);
if (!list) {
list = (category_list *)
calloc(sizeof(*list) + sizeof(list->list[0]), 1);
} else {
list = (category_list *)
realloc(list, sizeof(*list) + sizeof(list->list[0]) * (list->count + 1));
}
list->list[list->count++] = (locstamped_category_t){cat, catHeader};
NXMapInsert(cats, cls, list);
}
static NXMapTable *unattachedCategories(void)
{
runtimeLock.assertWriting();
//全局对象
static NXMapTable *category_map = nil;
if (category_map) return category_map;
// fixme initial map size
category_map = NXCreateMapTable(NXPtrValueMapPrototype, 16);
return category_map;
}
复制代码
对上面的代码进行解读:
unattachedCategories()
函数生成一个全局对象cats
cls
,获取一个category_list
*list
列表list
指针。那么咱们就生成一个category_list
空间。list
指针,那么就在该指针的基础上再分配出category_list
大小的空间cat
和catHeader
写入。cats
中,key
是cls
, 值是list
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);
}
}
复制代码
在remethodizeClass
函数中将经过attachCategories
函数咱们的分类信息附加到该类中。
//cls = [Person class]
//cats = [category_t(Eat),category_t(Drink)]
static void
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);
bool isMeta = cls->isMetaClass();
// 从新分配内存
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);
}
复制代码
对上面的代码进行解读(假设cls
是类对象,元类对象分析同理):
1.根据方法列表、属性列表、协议列表分配内存
2.cats
是这种数据结构:[category_t(Eat),category_t(Drink),。。。]
,遍历cats
,而后
mlist
数组中,而后再将mlist
数组添加到二维数组mlists
中proplist
数组中,而后再将proplist
数组添加到二维数组proplists
中protolist
数组中,而后再将protolist
数组添加到二维数组protolists
中3.获取cls
的的bits
指针 class_rw_t
,经过attachLists
方法,将mlists
附加到类对象方法列表中,将proplists
附加到类对象的属性列表中,将protolists
附加到类对象的协议列表中
其中mlists
的数据结构以下,proplists
与protolists
同理:
[
[method_t,method_t],
[method_t,method_t]
]
复制代码
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;
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]));
}
}
复制代码
在attachLists
方法主要关注两个变量array()->lists
和addedLists
上面代码的做用就是经过memmove
将原来的类找那个的方法、属性、协议列表分别进行后移,而后经过memcpy
将传入的方法、属性、协议列表填充到开始的位置。
咱们来总结一下这个过程:
经过Runtime加载某个类的全部Category数据
把全部Category的方法、属性、协议数据,合并到一个大数组中,后面参与编译的Category数据,会在数组的前面
将合并后的分类数据(方法、属性、协议),插入到类原来数据的前面
咱们能够用以下的动画来表示一下这个过程(更多动画相关可在公众号内获取)
咱们经过代码来验证一下上面两个注意点是否正确
load方法与initialize方法的调用与通常普通方法的调用有所区别,所以笔者将其放在这一节一并分析进行想对比
一样的,咱们按照以下函数的调用顺序,一步一步的研究load
的加载处理过程
void _objc_init(void);
└── void load_images(...);
└── void call_load_methods(...);
└── void call_class_loads(...);
复制代码
咱们直接从load_images
方法进行分析
void
load_images(const char *path __unused, const struct mach_header *mh)
{
// 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
{
rwlock_writer_t lock2(runtimeLock);
prepare_load_methods((const headerType *)mh);
}
// Call +load methods (without runtimeLock - re-entrant)
call_load_methods();
}
复制代码
在load_images
方法中主要关注prepare_load_methods
方法与call_load_methods
方法
prepare_load_methods
void prepare_load_methods(header_info *hi)
{
size_t count, i;
rwlock_assert_writing(&runtimeLock);
classref_t *classlist =
_getObjc2NonlazyClassList(hi, &count);
for (i = 0; i < count; i++) {
schedule_class_load(remapClass(classlist[i]));
}
category_t **categorylist = _getObjc2NonlazyCategoryList(hi, &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);
}
}
复制代码
static void schedule_class_load(Class cls)
{
if (!cls) return;
assert(cls->isRealized()); // _read_images should realize
if (cls->data()->flags & RW_LOADED) return;
// 确保父类优先的顺序
schedule_class_load(cls->superclass);
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}
复制代码
顾名思义,这个函数的做用就是提早准备好知足 +load 方法调用条件的类和分类,以供接下来的调用。 而后在这个类中调用了schedule_class_load(Class cls)
方法,而且在入参时对父类递归的调用了,确保父类优先的顺序。
call_load_methods
通过prepare_load_methods
的准备,接下来call_load_methods
就开始大显身手了。
void call_load_methods(void)
{
static BOOL loading = NO;
BOOL more_categories;
recursive_mutex_assert_locked(&loadMethodLock);
// 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; } 复制代码
在call_load_methods
中咱们看do
循环这个方法,它调用上一步准备好的类和分类中的 +load 方法,而且确保类优先于分类的顺序。
call_class_loads
call_class_loads
是load
方法调用的核心方法
static void call_class_loads(void)
{
int i;
// Detach current loadable list.
struct loadable_class *classes = loadable_classes;
int used = loadable_classes_used;
loadable_classes = nil;
loadable_classes_allocated = 0;
loadable_classes_used = 0;
// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Class cls = classes[i].cls;
load_method_t load_method = (load_method_t)classes[i].method;
if (!cls) continue;
if (PrintLoading) {
_objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
}
(*load_method)(cls, SEL_load);
}
// Destroy the detached list.
if (classes) _free_internal(classes);
}
复制代码
这个函数的做用就是真正负责调用类的 +load
方法了。它从全局变量 loadable_classes
中取出全部可供调用的类,并进行清零操做。
loadable_classes = nil;
loadable_classes_allocated = 0;
loadable_classes_used = 0;
复制代码
其中 loadable_classes
指向用于保存类信息的内存的首地址,loadable_classes_allocated
标识已分配的内存空间大小,loadable_classes_used
则标识已使用的内存空间大小。
而后,循环调用全部类的 +load
方法。注意,这里是(调用分类的 +load
方法也是如此)直接使用函数内存地址的方式 (*load_method)(cls, SEL_load)
; 对 +load
方法进行调用的,而不是使用发送消息 objc_msgSend
的方式。
可是若是咱们写[Student load]
时,这是使用发送消息 objc_msgSend
的方式。
举个🌰:
@interface Person : NSObject
@end
@implementation Person
+ (void)load{
NSLog(@"%s",__func__);
}
@end
复制代码
@interface Student : Person
@end
@implementation Student
//+ (void)load{
// NSLog(@"%s",__func__);
//}
@end
复制代码
int main(int argc, const char * argv[]) {
@autoreleasepool {
[Student load];
}
return 0;
}
复制代码
输出以下:
第一句走的是load
的加载方式,而第二句走的是objc_msgSend
中消息发送机制,isa
指针经过superclass
在父类中找到类方法。
小总结:
+load
方法会在runtime加载类、分类时调用1.先调用类的+load
按照编译前后顺序调用(先编译,先调用)
调用子类的+load以前会先调用父类的+load
2.再调用分类的+load
按照编译前后顺序调用(先编译,先调用)
一样的,咱们按照以下函数的调用顺序,一步一步的研究initialize
的加载处理过程
Method class_getInstanceMethod(Class cls, SEL sel);
└── IMP lookUpImpOrNil(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver);
└── IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver);
└── void _class_initialize(Class cls);
└── void callInitialize(Class cls);
复制代码
咱们直接打开objc-runtime-new.mm
文件来研究lookUpImpOrForward
这个方法
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
...
rwlock_unlock_write(&runtimeLock);
}
if (initialize && !cls->isInitialized()) {
_class_initialize (_class_getNonMetaClass(cls, inst));
// If sel == initialize, _class_initialize will send +initialize and
// then the messenger will send +initialize again after this
// procedure finishes. Of course, if this is not being called
// from the messenger then it won't happen. 2778172 } // The lock is held to make method-lookup + cache-fill atomic // with respect to method addition. Otherwise, a category could ... } 复制代码
initialize && !cls->isInitialized()
判断代码代表当一个类须要初始化却没有初始化时,会调用_class_initialize
进行初始化。
void _class_initialize(Class cls)
{
...
Class supercls;
BOOL reallyInitialize = NO;
// Make sure super is done initializing BEFORE beginning to initialize cls.
// See note about deadlock above.
supercls = cls->superclass;
if (supercls && !supercls->isInitialized()) {
_class_initialize(supercls);
}
...
callInitialize(cls);
...
}
复制代码
一样的supercls && !supercls->isInitialized()
代表对入参的父类进行了递归调用,以确保父类优先于子类初始化。
void callInitialize(Class cls)
{
((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
asm("");
}
复制代码
最后在callInitialize
中经过发送消息 objc_msgSend
的方式对 +initialize
方法进行调用,也就是说+ initialize
与通常普通方法的调用处理是同样的。 举个🌰:
@interface Person : NSObject
@end
@implementation Person
+ (void)initialize{
NSLog(@"%s",__func__);
}
@end
@implementation Person (Eat)
+ (void)initialize{
NSLog(@"%s",__func__);
}
@end
复制代码
@interface Student : Person
@end
@implementation Student
+ (void)initialize{
NSLog(@"%s",__func__);
}
@end
复制代码
@interface Teacher : Person
@end
@implementation Teacher
@end
复制代码
int main(int argc, const char * argv[]) {
@autoreleasepool {
[Student alloc];
[Student initialize];
[Person alloc];
[Person alloc];
[Person alloc];
[Person alloc];
[Person alloc];
[Person alloc];
NSLog(@"****分割线***");
[Teacher alloc];
[Teacher initialize];
}
return 0;
}
复制代码
输出以下:
小总结:
+initialize
方法会在类第一次接收到消息时调用
- 先调用父类的+initialize,再调用子类的+initialize
- 先初始化父类,再初始化子类,每一个类只会初始化1次
条件 | +load | +initialize |
---|---|---|
关键方法 | (*load_method)(cls, SEL_load) |
objc_msgSend |
调用时机 | 被添加到 runtime 时 | 收到第一条消息前,可能永远不调用 |
调用顺序 | 父类->子类->分类 | 父类->子类 |
调用次数 | 1次 | 屡次 |
是否须要显式调用父类实现 | 否 | 否 |
是否沿用父类的实现 | 否 | 是 |
分类中的实现 | 类和分类都执行 | 覆盖类中的方法,只执行分类的实现 |
^_^
)struct category_t
,里面存储着分类的对象方法、类方法、属性、协议信息-_-||
))不会覆盖!全部分类的方法会在运行时将它们的方法都合并到一个大数组中,后面参与编译的Category数据,会在数组的前面,而后再将该数组合并到类信息中,调用时顺着方法列表的顺序查找。
见load
与initialize
对比章节的表格
不能直接给Category添加成员变量,可是能够经过关联对象或者全局字典等方式间接实现Category有成员变量的效果