Category
可以动态地为已有类添加新行为。Apple还推荐了category的另外两个使用场景
- 可以把类的实现分开在几个不同的文件里面。这样做有几个显而易见的好处,
- a)可以减少单个文件的体积
- b)可以把不同的功能组织到不同的category里
- c)可以由多个开发者共同完成一个类
- d)可以按需加载想要的category 等等。
- 声明私有方法
Extension && Category
1.Category的特点
- 运行时决议
- 通过
runtime
动态将分类的方法合并到类对象、元类对象中 - 实例方法合并到类对象中,类方法合并到元类对象中
- 通过
- 可以为系统类添加分类
2.分类中可以添加哪些内容
- 实例方法
- 类方法
- 协议
- 属性
Class Extension(扩展)
- 声明私有属性
- 声明私有方法
- 声明私有成员变量
- 编译时决议,Category 运行时决议
- 不能为系统类添加扩展
- 只能以声明的形式存在,多数情况下,寄生于宿主类的.m文件中
extension在编译期决议,它就是类的一部分,在编译期和头文件里的@interface以及实现文件里的@implement一起形成一个完整的类,它伴随类的产生而产生,亦随之一起消亡。extension一般用来隐藏类的私有信息,你必须有一个类的源码才能为一个类添加extension,所以你无法为系统的类比如NSString添加extension。
但是category则完全不一样,它是在运行期决议的。 就category和extension的区别来看,我们可以推导出一个明显的事实,extension可以添加实例变量,而category是无法添加实例变量的(因为在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的)。
Category Runtime 结构
使用 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc MNPerson+Test.m
函数,生产一个cpp文件,窥探其底层结构(编译状态)
struct _category_t {
//宿主类名称 - 这里的MNPerson
const char *name;
//宿主类对象,里面有isa
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;
};
//_class_t 结构
struct _class_t {
struct _class_t *isa;
struct _class_t *superclass;
void *cache;
void *vtable;
struct _class_ro_t *ro;
};
- 每个分类都是独立的
- 每个分类的结构都一致,都是
category_t
-category如何加载
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();
runtime_init();
exception_init();
cache_init();
_imp_implementationWithBlock_init();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
忽略掉一堆 init ,重点来看
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
这个方法会注册3个事件并给出回调。 重点来看一下map_images和load_images; 从这俩个回调方法里看,你会发现Category在map_images会加载完毕,而load_images会调用+load方法。
类的load方法中,能调用分类的方法。
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
/* 二维数组( **mlists => 两颗星星,一个)
[
[method_t,],
[method_t,method_t],
[method_t,method_t,method_t],
]
*/
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);
}
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;
//realloc - 重新分配内存 - 扩容了
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
//memmove,内存挪动
//array()->lists 原来的方法列表
memmove(array()->lists + addedCount,
array()->lists,
oldCount * sizeof(array()->lists[0]));
//memcpy - 将分类的方法列表 copy 到原来的方法列表中
memcpy(array()->lists,
addedLists,
addedCount * sizeof(array()->lists[0]));
}
...
}
画图分析
category被附加到类上面是在map_images的时候发生的
要注意的有两点:
1)、category的方法没有“完全替换掉”原来类已经有的方法,也就是说如果category和原来类都有methodA,那么category附加完成之后,类的方法列表里会有两个methodA
2)、category的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,这也就是我们平常所说的category的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会罢休,殊不知后面可能还有一样名字的方法。
Category的加载处理流程
分类的加载处理流程主要有下面三步:
1.通过Runtime加载某个类的所有Category数据 2.把所有Category的方法、属性、协议数据,合并到一个大数组中 后面参与编译的Category数据,会在数组的前面
3.将合并后的分类数据(方法、属性、协议),插入到类原来数据的前面
旁枝末叶-category和+load方法
load实现原理
- 类第一次加载进内存的时候,会调用
+ load
方法,无需导入,无需使用- 每个类、分类的
+ load
在程序运行过程中只会执行一次+ load
走的不是消息发送的objc_msgSend
调用,而是找到+ load
函数的地址,直接调用
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) {
//先加载宿主类的load方法(按照编译顺序,调用load方法)
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;
}
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
// 递归调用,先将父类添加到load方法列表中,再将自己加进去
schedule_class_load(cls->superclass);
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}
调用顺序
先调用宿主类的 + load
函数
- 按照编译先后顺序调用(先编译,先调用)
- 调用子类的+load之前会先调用父类的+load
再调用分类的的 + load
函数
- 按照编译先后顺序调用(先编译,先调用)
实验证明:宿主类先调用,分类再调用
2019-02-27 17:28:00.519862+0800 load-Initialize-Demo[91107:2281575] MNPerson + load
2019-02-27 17:28:00.520032+0800 load-Initialize-Demo[91107:2281575] MNPerson (Play) + load
2019-02-27 17:28:00.520047+0800 load-Initialize-Demo[91107:2281575] MNPerson (Eat) + load
2019-02-27 17:39:10.354050+0800 load-Initialize-Demo[91308:2303030] MNDog + load (宿主类1)
2019-02-27 17:39:10.354237+0800 load-Initialize-Demo[91308:2303030] MNPerson + load (宿主类2)
2019-02-27 17:39:10.354252+0800 load-Initialize-Demo[91308:2303030] MNDog (Rua) + load (分类1)
2019-02-27 17:39:10.354263+0800 load-Initialize-Demo[91308:2303030] MNPerson (Play) + load(分类2)
2019-02-27 17:39:10.354274+0800 load-Initialize-Demo[91308:2303030] MNPerson (Eat) + load(分类3)
2019-02-27 17:39:10.354285+0800 load-Initialize-Demo[91308:2303030] MNDog (Run) + load(分类4)
父类和本类的调用:父类的方法优先于子类的方法。一个类的+load方法不用写明[super load],父类就会收到调用。
Initialize实现原理
- 类第一次接收到消息的时候,会调用该方法,需导入,并使用
+ Initialize
走的是消息发送的objc_msgSend
调用
initialize 与 load 的区别
- load 是类第一次加载的时候调用,initialize 是类第一次接收到消息的时候调用,每个类只会initialize一次(父类的initialize方法可能被调用多次)
- load 和 initialize,加载or调用的时候,都会先调用父类对应的
load
orinitialize
方法,再调用自己本身的; - load 和 initialize 都是系统自动调用的话,都只会调用一次
- 调用方式也不一样,load 是根据函数地址直接调用,initialize 是通过
objc_msgSend
- 调用时刻,load是runtime加载类、分类的时候调用(只会调用一次)
- 调用顺序:
- load:
- 先调用类的load
- 先编译的类,优先调用load
- 调用子类的load之前,会先调用父类的load
- 在调用分类的load
- 先调用类的load
- initialize:
- 先初始化父列
- 再初始化子类(可能最终调用的是父类的初始化方法)
- load:
我们知道,在类和category中都可以有+load方法,那么有两个问题:
1)、在类的+load方法调用的时候,我们可以调用category中声明的方法么?
2)、这么些个+load方法,调用顺序是咋样的呢?
答:
1)、可以调用,因为附加category到类的工作会先于+load方法的执行
2)、加载顺序是父类先+load,然后子类+load,然后分类+load,+load的执行顺序是先类,后category,而category的+load执行顺序是根据编译顺序决定的。
实际调用时,调用的是后添加的方法,即后添加的方法在方法列表methodLists的这个数组的顶部
后+load的类的方法,后添加到方法列表,而这时的添加方式又是插入顶部添加,即
[methodLists insertObject:category_method atIndex:0];
所以objc_msgSend遍历方法列表查找SEL 对应的IMP时,会先找到分类重写的那个,调用执行。然后添加到缓存列表中,这样主类方法实现永远也不会调到。
(后编译的Category,插入的方法在每个类大方法数组最前面)