IOS-Category实现原理
分类(Categroy)
你用分类都做了那些事?
- 声明私有方法
- 分解体积庞大的类文件
- 把Framework的私有方法公开化
特点
-
运行时决议
在编译后的Categroy类时,是没有将方法添加到宿主类的,是通过程序在运行时,通过Runtime动态添加到宿主类上面的
-
可以为系统类添加分类
类似UIView获取Left Top等分类
-
分类是用于给原有类添加方法的,因为在分类的结构体指针中,没有属性列表,只有方法列表。
原则上分类只能添加方法,不能添加属性,实际上可以通过'关联对象'给分类添加属性
-
分类中是可以写
@property
,但是不会生成setter/getter
方法,也不会生成实现以及私有的成员变量,会编译通过,但是引用变量就会报错 -
如果分类中和原类中有同名的方法,会优先调用分类中的方法,就是说会忽略原有的类方法(不是替换掉原有的方法,因为分类的方法是后添加到方法列表中的,所有会优先调用)
同名方法调用的优先级:
分类 > 本类 > 父类
-
如果多个分类中都有和原有的类中有同名方法,那么调用该方法的时候,执行谁室友编译器决定的;编译器会执行最后一个参与编译的分类中的方法
分类中都可以添加哪些内容
- 实例方法
- 类方法
- 协议
- 属性(关联对象)
实现原理
- 将category中的方法,属性,协议数据放在
category_t
结构体中,然后通过remethodizeClass
方法重新整理类的数据 - category中的实例方法和属性被整合到主类中
- category中的类方法被整合到元类中
- category中对协议助理的比较特殊,同时被整合到主类和元类中
调用加载栈
调用加载栈实现原理
category源码分析(objc-756.2)
分类结构体
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);
};
remethodizeClass
static void remethodizeClass(Class cls)
{
category_list *cats;
bool isMeta;
runtimeLock.assertLocked();
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)" : "");
}
//将分类cats拼接到cls上
attachCategories(cls, cats, true /*flush caches*/);
free(cats);
}
}
attachCategories
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_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;
}
}
//获取宿主类当中的rw数据,其中包含数组类的方法列表信息
auto rw = cls->data();
// 主要针对 分类中有关内存管理的相关方法,做一些特殊处理
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
/*
rw代表宿主类
methods代表宿主类的方法列表
attachLists方法是 将含有mcount个元素的二维数组拼接到rw的methods上
*/
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);
}
attachLists
/*
addedLists 上面传递过来的二维数组
[[method_t,method_t,...], [method_t], [method_t,method_t,method_t],...]
----------------------- ---------- ---------------------------
分类A的方法列表 B C
addedCount 二维数组长度 3
*/
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
if (hasArray()) {
// 类表中原有的元素总数
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]));
/*
内存拷贝
[
A ---> [addedLists中的第一个元素],
B ---> [addedLists中的第二个元素],
C ---> [addedLists中的第三个元素],
[原有第一个元素],
[原有第二元素]
]
这就是为什么有相同的方法名时,会先调用最后编译的分类方法的原因
*/
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]));
}
}
分类添属性(成员变量)(关联对象)
@interface UIView (KTestCategroy)
@property (nonatomic,copy) NSString *name;
@end
@implementation UIView (KTestCategroy)
-(void)setName:(NSString *)name{
objc_setAssociatedObject(self, "name", name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
-(NSString*)name{
return objc_getAssociatedObject(self, "name");
}
@end
{
"0x847547475":{
"@selector(name)":{
"value":"KsView",
"policy":"copy"
}
}
}
关联对象实现
- 关联对象由
AssociationsManager
管理,并存放在AssociationsHashMap
中 - 所有对象的关联内容都在同一个<font color=FF0000>全局容易中存储</font>中
objc_setAssociatedObject源码分析
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
// This code used to work when nil was passed for object and key. Some code
// probably relies on that to not crash. Check and handle it explicitly.
// rdar://problem/44094390
if (!object && !value) return;
assert(object);
if (object->getIsa()->forbidsAssociatedObjects())
_objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object));
// retain the new value (if any) outside the lock.
ObjcAssociation old_association(0, nil);
id new_value = value ? acquireValue(value, policy) : nil;
{
//关联对象管理类
AssociationsManager manager;
// 获取其维护的一个Hashmap
// 一个全局的容器
AssociationsHashMap &associations(manager.associations());
disguised_ptr_t disguised_object = DISGUISE(object);
if (new_value) {
// 根据对象指针查找对应的一个AssociationsHashMap结构的map
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// secondary table exists
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
//如果找到这个Key,替换新值
if (j != refs->end()) {
old_association = j->second;
j->second = ObjcAssociation(policy, new_value);
} else {
//设置新值
(*refs)[key] = ObjcAssociation(policy, new_value);
}
} else {
// 第一次为这个对象添加关联对象
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
object->setHasAssociatedObjects();
}
} else {
// 传过来的新值是nil,则清空原有的值
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);
}
总结
-
category为什么不能添加属性?
category可以添加属性,但是不会生成实例变量和set/get方法,因为category_t结构体中不存在成员变量,成员变量是存放在实例对象中的,在编译好的那一刻就决定好了。而分类是在运行时才去加载的,那么就无法在程序运行时将分类的成员变量添加到实例对象的结构体重,所以说分类中不可以添加成员变量。实例变量没有set/get方法,也没有自己的isa指针,所以,添加属性时系统没有报错,但是也不能用。
-
category中有load方法吗?load方法调用时机以及load方法能继承吗?
category中有load方法;
category中的load方法在程序装载类信息的时候就会调用;
load方法可以继承,会先调用父类的load方法
调用顺序:先调用类的
+load
方法,再调用分类的+load
,先编译,先调用的原则,调用子类的+load
方法前会先调用父类的+load
-
load、initialize的区别,以及它们在category重写的时候的调用顺序
区别就在于调用方式和调用时刻;
调用方式:load是根据函数地址直接调用;initialize是通过
objc_msgSend
调用调用时刻:load是在runtime加载类、分类的时候调用(只调用一次);initialize是类第一次接收到消息的时候调用,每个类只会initialize一次(父类的initialize方法可能会被调用多次)
-
如何清空一个关联对象?
传nil则清空