Category的本质
写一段简单的代码,之后的分析都基于这段代码
// MJPerson.h
#import
@interface MJPerson : NSObject
- (void)run;
@end
//MJPerson.m
#import "MJPerson.h"
// class extension (匿名分类\类扩展)
@interface MJPerson()
{
int _abc;
}
@property (nonatomic, assign) int age;
- (void)abc;
@end
@implementation MJPerson
- (void)abc
{
}
- (void)run
{
NSLog(@"MJPerson - run");
}
+ (void)run2
{
}
@end
//MJPerson+Test.h
#import "MJPerson.h"
@interface MJPerson (Test)
- (void)test;
@end
//MJPerson+Test.m
#import "MJPerson+Test.h"
@implementation MJPerson (Test)
- (void)run
{
NSLog(@"MJPerson (Test) - run");
}
- (void)test
{
NSLog(@"test");
}
+ (void)test2
{
}
//MJPerson+Eat.h
#import "MJPerson.h"
@interface MJPerson (Eat)
- (void)eat;
@property (assign, nonatomic) int weight;
@property (assign, nonatomic) double height;
@end
//MJPerson+Eat.m
#import "MJPerson+Eat.h"
@implementation MJPerson (Eat)
- (void)run
{
NSLog(@"MJPerson (Eat) - run");
}
- (void)eat
{
NSLog(@"eat");
}
- (void)eat1
{
NSLog(@"eat1");
}
+ (void)eat2
{
}
+ (void)eat3
{
}
@end
我们知道实例对象的isa指针指向类对象,类对象的isa指针指向元类对象,当MJPerson实例对象调用run方法时,通过实例对象的isa指针找到类对象,然后在类对象中查找对象方法,如果没有找到,就通过类对象的superclass指针找到父类对象,接着去寻找run方法。
那么当调用分类的方法时,步骤是否和调用对象方法一样呢?
分类中的对象方法依然是存储在类对象中的,同本类对象方法在同一个地方,调用步骤也同调用对象方法一样。如果是类方法的话,也同样是存储在元类对象中。
那么分类方法是如何存储在类对象中的,我们来通过源码看一下分类的底层结构。
Category的底层结构
定义在objc源码的objc-runtime-new.h中
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);
};
从Category的底层结构的源码可看到,对象方法,类方法,协议和属性都有对应的存储方式,但不存在成员变量,因此分类中是不允许添加成员变量的。分类中添加的属性并不会帮助我们自动生成成员变量,只会生成get set方法的声明,需要我们自己去实现。
那么分类中的方法,协议,属性是如何存储在类对象中的呢?
通过命令行将MJPerson+Test.m文件转化为c++文件,查看其中的编译过程。
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc MJPerson+Test.m
在分类转化为c++文件中可以看出_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; //属性列表
};
紧接着,我们可以看到_method_list_t类型的结构体,如下图所示
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_MJPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"test", "v16@0:8", (void *)_I_MJPerson_Test_test}}
};
从 _OBJC_$_CATEGORY_INSTANCE_METHODS_Preson_$_Test
可以看出是 INSTANCE_METHODS 对象方法,并且一一对应为上面结构体内赋值。还可以发现结构体中存储了方法占用的内存,方法数量,以及方法列表。从上图中找到分类中我们实现对应的对象方法test
接下来我们发现同样的_method_list_t类型的类方法结构体
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_MJPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"test2", "v16@0:8", (void *)_C_MJPerson_Test_test2}}
};
从 _OBJC_$_CATEGORY_CLASS_METHODS_MJPerson_$_Test
可以看出是类方法列表结构体 ,结构体中存储了方法占用的内存,方法数量,以及方法列表。从上图中找到分类中我们实现对应的类方法test2
接下来是协议方法列表
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_$_MJPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
1,
&_OBJC_PROTOCOL_NSCopying
};
通过源码可以看到先将协议方法通过_method_list_t结构体存储,之后通过_protocol_t结构体存储在_OBJC_CATEGORY_PROTOCOLS_$_MJPerson_$_Test
中同_protocol_list_t结构体一一对应,分别为protocol_count 协议数量以及存储了协议方法的_protocol_t结构体。
接着可以看到属性列表
static struct /*_prop_list_t*/ {
unsigned int entsize; // sizeof(struct _prop_t)
unsigned int count_of_properties;
struct _prop_t prop_list[1];
} _OBJC_$_PROP_LIST_MJPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_prop_t),
1,
{{"age","Ti,N"}}
};
属性列表结构体_OBJC_$_PROP_LIST_MJPerson_$_Test
同_prop_list_t结构体对应,存储属性的占用空间,属性数量,以及属性列表,从上图中可以看到我们自己写的age属性。
最后可以看到定义了_OBJC_$_CATEGORY_MJPerson_$_Test
结构体,并且将我们上面着重分析的结构体一一赋值,对照一下。
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);
};
extern "C" __declspec(dllimport) struct _class_t OBJC_CLASS_$_MJPerson;
static struct _category_t _OBJC_$_CATEGORY_MJPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"MJPerson",
0, // &OBJC_CLASS_$_MJPerson,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_MJPerson_$_Test,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_MJPerson_$_Test,
(const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_MJPerson_$_Test,
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_MJPerson_$_Test,
};
static void OBJC_CATEGORY_SETUP_$_MJPerson_$_Test(void ) {
_OBJC_$_CATEGORY_MJPerson_$_Test.cls = &OBJC_CLASS_$_MJPerson;
}
上下一一对应,并且我们看到定义class_t类型的OBJC_CLASSCATEGORY_Preson_Preson结构体地址。我们这里可以看出,cls指针指向的应该是分类的主类对象的地址。
通过以上分析发现,分类源码中确实是将我们定义的对象方法,类方法,属性等都存放在catagory_t结构体中。接下来再回到runtime源码查看catagory_t存储的方法,属性,协议等是如何存储在类对象中的。
首先来到 objc-os.mm 中的 _objc_init (runtime初始化函数)
/***********************************************************************
* _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
* Process the given images which are being mapped in by dyld.
* Calls ABI-agnostic code after taking ABI-specific locks.
*
* Locking: write-locks runtimeLock
**********************************************************************/
void
map_images(unsigned count, const char * const paths[],
const struct mach_header * const mhdrs[])
{
rwlock_writer_t lock(runtimeLock);
return map_images_nolock(count, paths, mhdrs);
}
接着在 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) {
// 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);
}
}
}
}
从上述代码中我们可以知道这段代码是用来查找有没有分类的。通过 _getObjc2CategoryList
函数获取到分类列表之后,进行遍历,获取其中的方法,协议,属性等。可以看到最终都调用了 remethodizeClass(cls);
函数。我们来到 remethodizeClass(cls);
函数内部查看。
/***********************************************************************
* 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);
}
}
通过上述代码我们发现attachCategories函数接收了类对象cls和分类数组cats,如我们一开始写的代码所示,一个类可以有多个分类。之前我们说到分类信息存储在category_t结构体中,那么多个分类则保存在category_list中。
来到attachCategories函数内部
// Attach method lists and properties and protocols from categories to a class.
// Assumes the categories in cats are all loaded and sorted by load order,
// 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);
}
从源码中可以看出,首先根据方法列表,属性列表,协议列表,malloc分配内存,根据多少个分类以及每一块方法需要多少内存来分配相应的内存地址。之后从分类数组里面往三个数组里面存放分类数组里面存放的分类方法,属性以及协议放入对应mlist、proplists、protolosts数组中,这三个数组放着所有分类的方法,属性和协议。之后通过类对象的data()方法,拿到类对象的class_rw_t结构体rw,在class结构中我们介绍过,class_rw_t中存放着类对象的方法,属性和协议等数据,rw结构体通过类对象的data方法获取,所以rw里面存放这类对象里面的数据。之后分别通过rw调用方法列表、属性列表、协议列表的attachList函数,将所有的分类的方法、属性、协议列表数组传进去。
我们来看一下attachLists函数内部
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]));
}
}
上述源代码中有两个重要的数组
array()->lists: 类对象原来的方法列表,属性列表,协议列表。
addedLists:传入所有分类的方法列表,属性列表,协议列表。
attachLists函数中最重要的两个方法为memmove内存移动和memcpy内存拷贝。我们先来分别看一下这两个函数
// memmove :内存移动。
/* __dst : 移动内存的目的地
* __src : 被移动的内存首地址
* __len : 被移动的内存长度
* 将__src的内存移动__len块内存到__dst中
*/
void *memmove(void *__dst, const void *__src, size_t __len);
// memcpy :内存拷贝。
/* __dst : 拷贝内存的拷贝目的地
* __src : 被拷贝的内存首地址
* __n : 被移动的内存长度
* 将__src的内存移动__n块内存到__dst中
*/
void *memcpy(void *__dst, const void *__src, size_t __n);
下面我们图示经过memmove和memcpy方法过后的内存变化。
经过memmove方法之后,本类的方法,属性,协议列表会分别后移,但是本类的对应数组的指针依然指向原始位置,经过memcpy方法之后,分类的方法,属性,协议列表被放在了类对象中原本存储的方法,属性,协议列表前面。
将分类方法的列表追加到本来的对象方法前面的目的是为了保证分类方法优先调用,我们知道当分类重写本类的方法时,会覆盖本类的方法。经过上面的分析我们知道本质上并不是覆盖,而是优先调用。本类的方法依然在内存中的。我们可以通过打印所有类的所有方法名来查看
void printMethodNamesOfClass(Class cls)
{
unsigned int count;
// 获得方法数组
Method *methodList = class_copyMethodList(cls, &count);
// 存储方法名
NSMutableString *methodNames = [NSMutableString string];
// 遍历所有的方法
for (int i = 0; i < count; i++) {
// 获得方法
Method method = methodList[I];
// 获得方法名
NSString *methodName = NSStringFromSelector(method_getName(method));
// 拼接方法名
[methodNames appendString:methodName];
[methodNames appendString:@", "];
}
// 释放
free(methodList);
// 打印方法名
NSLog(@"%@ - %@", cls, methodNames);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
MJPerson *person = [[MJPerson alloc] init];
[person run];
printMethodNamesOfClass([MJPerson class]);
}
return 0;
}
从打印内容可以发现,调用的是Test中的run方法,并且MJPerson类中存储着三个run方法。
+load方法
load方法会在程序启动就会调用,当装载类信息的时候就会调用。
调用顺序看一下源代码。
首先来到 objc-os.mm 中的 _objc_init (runtime初始化函数)
/***********************************************************************
* _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);
}
接着进入 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 函数内点击进入 call_load_methods 函数
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(); //先调用类的load 方法
}
// 2. Call category +loads ONCE
more_categories = call_category_loads(); //之后调用分类的load方法
// 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方法,之后调用分类的load方法。
代码验证一下:
// MJPerson.h
#import
@interface MJPerson : NSObject
@end
// MJPerson.m
#import "MJPerson.h"
@implementation MJPerson
+ (void)load
{
NSLog(@"MJPerson +load");
}
@end
// MJPerson+Test1.h
#import "MJPerson.h"
@interface MJPerson (Test1)
@end
// MJPerson+Test1.m
#import "MJPerson+Test1.h"
@implementation MJPerson (Test1)
+ (void)load
{
NSLog(@"MJPerson (Test1) +load");
}
@end
//MJStudent.h
#import "MJPerson.h"
@interface MJStudent : MJPerson
@end
// MJStudent.m
#import "MJStudent.h"
@implementation MJStudent
+ (void)load
{
NSLog(@"MJStudent +load");
}
@end
// MJStudent+Test1.h
#import "MJStudent.h"
@interface MJStudent (Test1)
@end
// MJStudent+Test1.m
#import "MJStudent+Test1.h"
@implementation MJStudent (Test1)
+ (void)load
{
NSLog(@"MJStudent (Test1) +load");
}
@end
从 call_load_methods 分别点击进入 call_class_loads 函数 和 call_category_loads 函数,可以发现 load 方法的调用代码为
(*load_method)(cls, SEL_load);
由此可知 +load方法是根据方法地址直接调用,并不是经过objc_msgSend函数调用
+load 方法总结
- +load方法会在runtime加载类、分类时调用
- 每个类、分类的+load,在程序运行过程中只调用一次
- 调用顺序
- 先调用类的+load,按照编译先后顺序调用(先编译,先调用)
- 调用子类的+load之前会先调用父类的+load
- 所有类的 +load 方法调用完了,再调用分类的+load,按照编译先后顺序调用(先编译,先调用)
+initialize方法
首先我们来看一下initialize的源码(objc-initialize.mm 中)
void callInitialize(Class cls)
{
((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
asm("");
}
由上图可发现,initialize是通过消息发送机制调用的,消息发送机制通过isa指针找到对应的方法与实现,所以分类如果重写了initialize 方法,会优先调用分类方法中的实现。
+initialize 方法总结
- +initialize方法会在类第一次接收到消息时调用
- 调用顺序
- 先调用父类的+initialize,再调用子类的+initialize (先初始化父类,再初始化子类,每个类只会初始化1次)
- +initialize和+load的很大区别是,+initialize是通过objc_msgSend进行调用的,所以有以下特点:
- 如果子类没有实现+initialize,会调用父类的+initialize(所以父类的+initialize可能会被调用多次)
- 如果分类实现了+initialize,就覆盖类本身的+initialize调用