一、category简介
category是Objective-C 2.0之后添加的语言特性,主要作用是为已经存在的类添加方法,苹果官方推荐两个使用场景。
- 将类的实现分割成多个文件。这样便可以将一个庞大的类按照功能的不同包装成多个文件实现,大大提高的代码的可读性;多个开发者可以同时完成一个类。
- 声明私有方法。
在日常的开发中,我们也不仅仅是基于这两个使用场景,我们还可以通过category来模拟多继承、公开Framework库的私有方法、拓展类的功能等。
二、category源码初探
1、利用clang命令揭开category的面纱
我们先去定义一个category。
@interface Person (DS)
@property (nonatomic, copy) NSArray *name;
-(void)sayCategory;
@end
@implementation Person (DS)
- (void)sayCategory{
NSLog(@"%s",__func__);
}
@end
在Person+DS.m文件目录下,在命令行输入如下命令:
clang -rewrite-objc Person+DS.m -o Person.cpp
在同级目录下会生成一个cpp文件。在Person.cpp文件中找到了如下的代码:
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_Person_$_DS __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"sayCategory", "v16@0:8", (void *)_I_Person_DS_sayCategory}}
};
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_Person_$_DS __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_prop_t),
1,
{{"name","T@\"NSArray\",C,N"}}
};
extern "C" __declspec(dllimport) struct _class_t OBJC_CLASS_$_Person;
static struct _category_t _OBJC_$_CATEGORY_Person_$_DS __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"Person",
0, // &OBJC_CLASS_$_Person,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_DS,
0,
0,
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Person_$_DS,
};
从这样的一段代码中我们得出如下信息:
- 编译器生成了方法列表OBJC_$CATEGORY_INSTANCE_METHODS_Person$_DS,列表中填充的正是我们在DS这个category中定义的方法sayCategory。
- 编译器生成了属性列表OBJC_$PROP_LIST_Person$_DS,列表中填充的是我们在DS这个category中定义的属性name。
- 编译器生成了category本身OBJC_$CATEGORY_Person$_DS,是一个static修饰的结构体_category_t,由此可见在同一个编译单元category的名称不能重复。
2、category的底层源码
在上一个小节中,我们知道了category的本质是一个category_t的结构体,那么我们就看看在源码中category是怎么样的一个结构。
苹果官方开源的objc源码在这里可以下载到。下载下来的源码无法直接通过编译,需要自己修改下配置。
如下是在Objc源码中找到的category_t的结构源码:
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;
};
这段代码明确告诉了我们category的结构信息:
- 名称-name
- 类-cls
- category为类添加的对象方法列表-instanceMethods
- category为类添加的类方法列表-classMethods
- category为类添加的协议列表-protocols
- category为类添加的实例属性列表-instanceProperties
- category为类添加的类属性列表-_classProperties
由此我们可以确信在category中可以添加对象方法、类方法、协议实现、属性,而不能添加成员变量。
我们虽然知道了category的底层结构,但是到底category的加载过程是怎样的呢?下面我们来探索下在运行期category的加载过程。
3、category的加载
我们都知道Objective-C的运行依赖于runtime,在OC运行时,runtime的入口如下。
// Runtime初始化方法
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();
// _objc_init->map_images->map_images_nolock->_read_images->realizeClass
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
这段OC的入口代码中,_dyld_objc_notify_register(&map_images, load_images, unmap_image);
方法的调用才是OC初始化的开始。
category加载到类中是在map_images
的时候发生的,而map_images
最终会调用到objc-runtime-new.mm中的_read_images
方法。_read_images
方法做了大量的工作,这里我们只关心category的相关操作。
// 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);
}
}
}
}
从这段代码我们不难看出来,category加载到类的过程中做了如下几个操作
- 通过
_getObjc2CategoryList
拿到category的列表。- 将category中的对象方法,协议以及属性添加到类中。
- 将category中的类方法、协议以及类属性添加到类的元类中。
将category的信息加载到类中调用的是方法remethodizeClass
方法,而在remethodizeClass
方法中最终会调用attachCategories
方法将category中的信息贴到类中。如下是attachCategories
方法的代码。
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);
}
在这段代码里,我们看到了attachCategories
方法将category中的对象方法、属性、协议添加到了类中。最后会将这些信息添加到类的data中,在前面的深入OC底层探索NSObject的结构这篇文章中我们已然分析了类中信息的存储结构。
4、所谓方法覆盖
如果在category定义并实现了和类中一样的方法,这时候在方法调用的时候调用的是category中的方法而不是类中的方法,这样便出现了方法覆盖的现象,但是事实是否是这样的呢?
从上面的代码中我们有看到category中方法的添加是通过attachLists
方法来实现的,我们来看下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]));
}
}
这段代码是在原有的方法列表基础上做的操作,只是将原来的方法列表位置往后移动了category中方法数量,并把category中的方法添加到了列表的前面,由此我们得出一个结论:
category中的方法覆盖是一种假象,只是在category中的方法加载到类的时候,将方法添加到了类的方法列表前面,并未替换原有的方法。而方法调用的查找是顺序查找的,所以调用的是调用的是category中的方法。
我们利用lldb指令来看下类中的方法列表的实际情况。在Person类和Person的category中定义并实现同一个方法。
从上面的截图中我们发现在方法列表中,在类中的定义的方法和类的category中的定义方法是一并存在的,而且category中的方法在类中的方法的前面,从这一层面也证实了我们前面得出的结论。
5、关联对象
如上分析我知道了category无法添加实例变量,虽然可以在category中添加属性,但是并不会在category中生成属性的getter/setter方法,也就是说我们无法正常使用category中定义的属性,但是在开发中不可避免的需要在category中添加对象关联的值,这个时候就可以通过关联对象来实现getter/setter方法。
- (void)setName:(NSString *)name
{
objc_setAssociatedObject(self,
"name",
name,
OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString*)name
{
NSString *nameObject = objc_getAssociatedObject(self, "name");
return nameObject;
}
如上代码所示就是category中属性关联对象,下面我们来看下关联对象的存储、销毁和调用原理。
5.1 设置关联对象-objc_setAssociatedObject
设置关联对象调用objc_setAssociatedObject
方法,而在objc_setAssociatedObject
中会调用_object_set_associative_reference
方法。如下是_object_set_associative_reference
的源码。
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
// retain the new value (if any) outside the lock.
// 初始化空的ObjcAssociation(关联对象)
ObjcAssociation old_association(0, nil);
id new_value = value ? acquireValue(value, policy) : nil;
{
// 初始化一个manager
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
// 获取对象的DISGUISE值,作为AssociationsHashMap的key
disguised_ptr_t disguised_object = DISGUISE(object);
if (new_value) {
// value有值,不为nil
// break any existing association.
// AssociationsHashMap::iterator 类型的迭代器
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// secondary table exists
// 获取到ObjectAssociationMap(key是外部传来的key,value是关联对象类ObjcAssociation)
ObjectAssociationMap *refs = i->second;
// ObjectAssociationMap::iterator 类型的迭代器
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
// 原来该key对应的有关联对象
// 将原关联对象的值存起来,并且赋新值
old_association = j->second;
j->second = ObjcAssociation(policy, new_value);
} else {
// 无该key对应的关联对象,直接赋值即可
// ObjcAssociation(policy, new_value)提供了这样的构造函数
(*refs)[key] = ObjcAssociation(policy, new_value);
}
} else {
// create the new association (first time).
// 执行到这里,说明该对象是第一次添加关联对象
// 初始化ObjectAssociationMap
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
// 赋值
(*refs)[key] = ObjcAssociation(policy, new_value);
// 设置该对象的有关联对象,调用的是setHasAssociatedObjects()方法
object->setHasAssociatedObjects();
}
} else {
// setting the association to nil breaks the association.
// value无值,也就是释放一个key对应的关联对象
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;
// 调用erase()方法删除对应的关联对象
refs->erase(j);
}
}
}
}
// release the old value (outside of the lock).
// 释放旧的关联对象
if (old_association.hasValue()) ReleaseValue()(old_association);
}
从上面的代码中我们可以得知,关联对象其实是交给了AssociationsManager
进行了管理,AssociationsManager的代码如下:
class AssociationsManager {
// associative references: object pointer -> PtrPtrHashMap.
// AssociationsManager中只有一个变量AssociationsHashMap
static AssociationsHashMap *_map;
public:
// 构造函数中加锁
AssociationsManager() { AssociationsManagerLock.lock(); }
// 析构函数中释放锁
~AssociationsManager() { AssociationsManagerLock.unlock(); }
// 构造函数、析构函数中加锁、释放锁的操作,保证了AssociationsManager是线程安全的
AssociationsHashMap &associations() {
// AssociationsHashMap 的实现可以理解成单例对象
if (_map == NULL)
_map = new AssociationsHashMap();
return *_map;
}
};
在AssociationsManager中有一个静态(static)的AssociationsHashMap,关联对象就存储在AssociationsHashMap中,这就相当于把所有对象的关联对象都存储在一个全局的AssociationsHashMap 类型的map中,map的key是对象的指针地址,value是一个AssociationsHashMap,里面存储了关联对象的key和value。
5.2 获取关联对象的值-objc_getAssociatedObject
获取关联对象调用objc_getAssociatedObject
方法,而在objc_getAssociatedObject
中会调用_object_get_associative_reference
方法。如下是_object_get_associative_reference
的源码。
// 获取关联对象
id _object_get_associative_reference(id object, void *key) {
id value = nil;
uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;
{
AssociationsManager manager;
// 获取到manager中的AssociationsHashMap
AssociationsHashMap &associations(manager.associations());
// 获取对象的DISGUISE值
disguised_ptr_t disguised_object = DISGUISE(object);
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// 获取ObjectAssociationMap
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
// 获取到关联对象ObjcAssociation
ObjcAssociation &entry = j->second;
// 获取到value
value = entry.value();
policy = entry.policy();
if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) {
objc_retain(value);
}
}
}
}
if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
objc_autorelease(value);
}
// 返回关联对像的值
return value;
}
获取关联对象就是通过key从全局的AssociationsHashMap中拿到存储的value值。
5.3 销毁关联对象
对象销毁时调用objc_removeAssociatedObjects
方法,而在objc_removeAssociatedObjects
中会调用_object_remove_assocations
方法。如下是_object_remove_assocations
的源码。
// 移除对象object的所有关联对象
void _object_remove_assocations(id object) {
// 声明了一个vector
vector< ObjcAssociation,ObjcAllocator > elements;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
// 如果map size为空,直接返回
if (associations.size() == 0) return;
// 获取对象的DISGUISE值
disguised_ptr_t disguised_object = DISGUISE(object);
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// copy all of the associations that need to be removed.
ObjectAssociationMap *refs = i->second;
for (ObjectAssociationMap::iterator j = refs->begin(), end = refs->end(); j != end; ++j) {
elements.push_back(j->second);
}
// remove the secondary table.
delete refs;
associations.erase(i);
}
}
// the calls to releaseValue() happen outside of the lock.
for_each(elements.begin(), elements.end(), ReleaseValue());
}
对象销毁的时候会判断当前的对象是否有关联对象,如果有的话会从全局的AssociationsHashMap中找到当前对象的关联对象,并从中移除。
5.4 关联对象的属性修饰符
如上代码所示,在设置关联对象的时候有这样的OBJC_ASSOCIATION_COPY_NONATOMIC
的一个东西,这是关联对象的属性修饰符。
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, /**< Specifies a weak reference to the associated object. */
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object.
* The association is not made atomically. */
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, /**< Specifies that the associated object is copied.
* The association is not made atomically. */
OBJC_ASSOCIATION_RETAIN = 01401, /**< Specifies a strong reference to the associated object.
* The association is made atomically. */
OBJC_ASSOCIATION_COPY = 01403 /**< Specifies that the associated object is copied.
* The association is made atomically. */
};
objc_AssociationPolicy | description |
---|---|
OBJC_ASSOCIATION_ASSIGN | assign |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | nonatomic, strong |
OBJC_ASSOCIATION_COPY_NONATOMIC | nonatomic, copy |
OBJC_ASSOCIATION_RETAIN | atomic, strong |
OBJC_ASSOCIATION_COPY | atomic, copy |
三、总结
- category的底层结构是category_t,是一个结构体。
- category中可以添加对象方法、类方法、协议实现、属性,但是不能添加实例变量。
- category在运行期间加载到类中。
- category中的方法不会真正覆盖类中的同名方法,而是将category中的方法加载在方法列表的前面,方法调用的按顺序查找方法,所以在方法调用的时候调用的是category中的方法。
- category中的属性并无实质性的意义,可以利用
objc_setAssociatedObject
和objc_getAssociatedObject
和对象关联。 - 所有对象的关联对象存储在一个全局的
AssociationsHashMap
中。