废话不多说,直接进入主题。。。
下面我们由几个问题展开:
1、什么是Category?
2、Category有什么特点?
3、Category中能添加哪些内容?
什么是Category?
Category(分类)是在Objective-C 2.0之后出现的,主要用于给现有类添加方法,以便对原有类的一个补充和完善,因为我们任何类都不是天生完美的。我们看下苹果的官方文档是如何解释的:
You use categories to define additional methods of an existing class—even one whose source code is unavailable to you—without subclassing. You typically use a category to add methods to an existing class, such as one defined in the Cocoa frameworks. The added methods are inherited by subclasses and are indistinguishable at runtime from the original methods of the class. You can also use categories of your own classes to:
Distribute the implementation of your own classes into separate source files—for example, you could group the methods of a large class into several categories and put each category in a different file.
Declare private methods.
总结一下就是:
1、分解体积庞大的自定义类文件
2、声明私有方法
Category有什么特点?
我们先看这句解释
The added methods are inherited by subclasses and are indistinguishable at runtime from the original methods of the class.
可知:
1、分类是在运行时期决议的。
2、分类中添加的方法子类是可继承的。
那么除此之外分类还可以添加什么呢?
Category中可以添加哪些内容?
我们先看下苹果源码的实现:
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;
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta) {
if (isMeta) return nil; // classProperties;
else return instanceProperties;
}
};
在category_t的结构体中我们可以看到如下:
name:类名
cls:类
instanceMethods:实例方法列表
classMethods:类方法列表
protocols:协议列表
instanceProperties:属性列表
现在就很明确了:
分类中可以添加:实例方法、类方法、协议、属性
所以也由此可知:category是不能添加成员变量的,从源代码层面上解释就是category_t的结构体里面没有ivars成员变量列表。
那么,问题又来了。。。。。有人该会问啦,上面不是说可以添加属性么,按照正常情况来说,我在类文件里面使用@property声明一个属性,编译器就会自动生成一个带下划线的成员变量呀,这样不就是能添加成员变量嘛,NONONO,在category里面这样的想法是不对的,下面我解释一下原因:
第一、在分类里使用@property声明属性,只是将该属性添加到该类的属性列表(instanceProperties),声明的getter、setter方法添加到实例方法列表,但是不会生成相应的成员变量,也没有实现setter、getter方法。
第二、前面有讲到category是在运行时期决议的,因为在运行期也就是编译完成之后,对象的内存布局已经确定,如果添加实例变量就需要重新内存对齐,就会破坏类的内部布局,这对编译型语言来说是不可取的。
这里说明一点,其实有很多开发者包括我自己以前都会误认为category是不能添加属性的,其实这种理解是错误的,属性是可以添加的,在category中@property声明一个属性编译器并不会报错,但是在运行时期调用setter、getter方法的时候就会报错,错误原因是找不到该方法,所以也证明了category能添加属性,但是不能添加成员变量。
但是我们在开发过程中需要往分类中添加成员变量怎么办呢?
这个问题其实大家都知道改如何解决,那就是runtime的关联对象,说到这,可能有人又会有疑问啦,既然说category中添加成员变量会遇到内存对齐问题,为什么通过runtime就可以添加呢,所以请带着疑问继续往下继续看:
先看一下关联对象的实现源码
void objc_setAssociatedObject_non_gc(id object, const void *key, id value, objc_AssociationPolicy policy) {
_object_set_associative_reference(object, (void *)key, value, policy);
}
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
// retain the new value (if any) outside the lock.
ObjcAssociation old_association(0, nil);
id new_value = value ? acquireValue(value, policy) : nil;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
disguised_ptr_t disguised_object = DISGUISE(object);
if (new_value) {
// break any existing association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// secondary table exists
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
j->second = ObjcAssociation(policy, new_value);
} else {
(*refs)[key] = ObjcAssociation(policy, new_value);
}
} else {
// create the new association (first time).
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
object->setHasAssociatedObjects();
}
} else {
// setting the association to nil breaks the association.
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);
}
class AssociationsManager {
static spinlock_t _lock;
static AssociationsHashMap *_map; // associative references: object pointer -> PtrPtrHashMap.
public:
AssociationsManager() { _lock.lock(); }
~AssociationsManager() { _lock.unlock(); }
AssociationsHashMap &associations() {
if (_map == NULL)
_map = new AssociationsHashMap();
return *_map;
}
};
通过源代码我们可以看见,关联对象是由AssociationsManager进行管理,AssociationsManager结构体里面是由一个静态AssociationsHashMap来存储所有的关联对象的,
disguised_ptr_t disguised_object = DISGUISE(object);这里是获取要关联属性的对象的指针地址,作为map的key,value对应的是ObjectAssociationMap,看下ObjectAssociationMap里面都有什么
class ObjectAssociationMap : public std::map<void *, ObjcAssociation, ObjectPointerLess, ObjectAssociationMapAllocator>
其中void * 就是我们在objc_setAssociatedObject_non_gc(id object, const void *key, id value, objc_AssociationPolicy policy) 中传入的key,
然后看下ObjcAssociation
class ObjcAssociation {
uintptr_t _policy;
id _value;
};
这里面的value就是我们关联的对象成员变量的值。
到这里,关联对象的解释基本就结束了,所以看得出来,通过关联对象给category添加成员变量由于是存储在一个全局的AssociationsManager里面,所以并不会对现有类的内存造成影响。
objc_getAssociatedObject_gc就不做详细介绍了,大家可以静下心来阅读苹果的源代码。
注: 本文章解释的比较浅显,也易懂,如有解释不当的地方欢迎留言交流,谢谢!!!
致谢:多谢曦哥对我的问题的解答和帮助!
作者:张叔叔