1、Category的基本使用
Category的使用场合是什么?
- 将一个类拆成很多模块(其实就是解耦,将相关的功能放到一起)
2、Category的实现原理?
- 通过runtime动态将分类的方法合并到类对象、元类对象中
- Category编译之后的底层结构是 struct_category_t , 里面存储着分类的对象方法、类方法、属性、协议信息
- 在程序运行的时候,runtime会将 Category 的数据,合并到类信息中(类对象、元类对象)
- 转C++代码
$ xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc MJPerson+Test.m
- 分类结构体
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; // 属性列表 (分类里能实现属性)
};
- Test分类
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,
0,
0,
0,
};
- Eat分类
static struct _category_t _OBJC_$_CATEGORY_MJPerson_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"MJPerson",
0, // &OBJC_CLASS_$_MJPerson,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_MJPerson_$_Eat,
0,
0,
0,
};
3、源码分析1
源码定义
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的加载处理过程
- 通过Runtime加载某个类的所有Category数据
- 把所有Category的方法、属性、协议数据,合并到一个大数组中,后面参与编译的Category数据,会在数组的前面
- 将合并后的分类数据(方法、属性、协议),插入到类原来数据的前面
源码解读顺序
objc-os.mm
- _objc_init
- map_images
- map_images_nolock
objc-runtime-new.mm
- _read_images
- remethodizeClass
- attachCategories
- attachLists
- realloc、memmove、memcpy
cls = [MJPerson class]
cats = [category_t(Test), category_t(Eat)]
static void
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);
bool isMeta = cls->isMetaClass();
/*
方法数组
[
[method_t, method_t],
[method_t, method_t]
]
*/
method_list_t **mlists = (method_list_t **)
malloc(cats->count * sizeof(*mlists));
/*
属性数组
[
[property_t, property_t],
[property_t, property_t]
]
*/
property_list_t **proplists = (property_list_t **)
malloc(cats->count * sizeof(*proplists));
/*
协议数组
[
[protocol_t, protocol_t],
[protocol_t, protocol_t]
]
*/
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);
}
5、memmove、memcopy的区别
void *memcpy(void *dst, const void *src, size_t count);
void *memmove(void *dst, const void *src, size_t count);
- 他们的作用是一样的
- 唯一的区别是,当内存发生局部重叠的时候,memmove保证拷贝的结果是正确的,memcpy不保证拷贝的结果的正确
6、load基本使用
Category中有load方法么?load方法什么时候调用的?load方法能继承么?
- 有load方法
- +load方法会在runtime加载类、分类时调用;
- 每个类、分类的+load,在程序运行过程中只调用一次
- 调用顺序
1、先调用类的+load,(按照编译先后顺序,先编译,先调用),调用子类的+load之前会调用父类的+load
2、再调用分类的+load按照编译先后顺序调用(先编译,先调用)
7、load调用原理
-load方法调用
test方法和load方法的本质区别?(+load方法为什么不会被覆盖)
- test方法是通过消息机制调用 objc_msgSend([MJPerson class], @selector(test))
- + load方法调用,直接找到内存中的地址,进行方法调用
8、load调用顺序
- +load方法会在runtime加载类、分类时调用
- 每个类、分类的+load,在程序运行过程中只调用一次
调用顺序
- 1、先调用类的+load
按照编译先后顺序调用(先编译,先调用)
调用子类的+load之前会先调用父类的+load
- 2、再调用分类的+load
按照编译的顺序调用(先编译,先调用)
objc4源码解读过程:objc-os.mm
- _objc_init
- load_images
- prepare_load_methods
schedule_class_methods
add_class_to_loadable_list
add_category_to_loadable_list
- call_load_methods
call_class_loads
call_category_loads
(*load_method)(cls, SEL_load)
+ load方法时根据方法地址直接调用,并不是经过objc_msgSend函数调用
- 先调用类方法,后调用分类方法
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;
}
如果有100个类,会先调用哪个类的方法(按照数组的先后顺序)
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(classes);
}
9、initialize的基本使用
initialize 方法(分类会覆盖类方法,通过消息机制(objc_msgSend)调用)
- initialize方法会在类第一次接收到消息时调用
- 调用顺序:
先调用父类的+initialize,再调用子类的+initialize(如果父类已经调用过了,调用子类的时候,父类就不回再调用了)
为什么load三个方法都会调用?
- 直接通过函数指针调用,不是通过消息机制(objc_msgSend)调用
10、initialize 注意点
initialize和load的很大区别是,
- +initialize是通过objc_msgSend进行调用的
- +load方法是直接找到内存地址进行调用的
所以initialize有以下特点:
- 如果子类没有实现+initialize,会调用父类的+initialize(所以父类的+initialize可能会被调用多次)
- 如果分类实现了+initialize,就覆盖类本身的+initialize调用
// 伪代码(为什么会打印三次的原因,不代表父类初始化了3次)
if (MJStudent没有初始化) {
if (MJPerson没有初始化) {
objc_msgSend([MJPerson class], @selector(initialize))
}
objc_msgSend([MJStudent class], @selector(initialize))
}
if (MJTeacher没有初始化) {
if (MJPerson没有初始化) {
objc_msgSend([MJPerson class], @selector(initialize))
}
objc_msgSend([MJTeacher class], @selector(initialize))
}
11、面试题
1、load、initialize方法的区别是什么?它们在category中的调用顺序?以及出现继承时他们之间的调用过程?
区别:
- 调用方式
1、load是根据函数地址直接调用
2、initialize是荣光objc_msgSend调用
- 调用时刻
1、load是runtime加载类、分类的时候调用(只会调用1次)
2、initialize是类第一次接收消息时调用,每一个类只会initialize一次(父类的initialize方法可能会被调用多次)
load、initialize的调用顺序
- load
1、先调用类的load
先编译的类,优先调用load
调用子类的load之前,会先调用父类的load
2、再调用分类的load
先编译的分类,优先调用load
- initialize
先初始化父类
再初始化子类(可能最终调用的是父类的initialize方法)
2、不同Category中存在同一个方法,会执行哪个方法?如果是连个都执行,执行顺序是什么样的?
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *test = [[NSObject alloc] init];
[test printTest];
}
return 0;
}
@implementation NSObject (Test1)
- (void)printTest {
NSLog(@"-----test1");
}
@end
@implementation NSObject (Test2)
- (void)printTest {
NSLog(@"-----test2");
}
@end
2019-07-04 08:56:36.870570+0800 test[911:13532] -----test2
3、Category和 Class Extension的区别?(分类和 类扩展(就是.m文件中的.h) 的区别)
- Class Extension 在编译的时候,它的数据就已经包含在类信息中
- Category是在运行时,才会将数据合并到类信息中
4、Category中有load方法么?load方法什么时候调用的?load方法能继承么?
- 有load方法
- +load方法会在runtime加载类、分类时调用;
- load方法可以继承,但是一般情况下不回主动去调用load方法,都是让系统自动去调用