在Objective-C中,要扩展一个类的方法,首先想到的应该是继承,这是面向对象语言的一个特性。继承可以很方便的增加方法,属性等,同时还可以覆写父类的方法。但是,对于大型而复杂的类,继承会导致维护困难。这时Category就可以发挥作用了。
什么是Category
Category是Objective-C 2.0之后添加的语言特性,其主要作用是为已经存在的类添加方法。通过它,你可以
- 把代码分散到多个类中-比如把类中的不同模块的方法放入几个不同的category中。
- 申明私有方法。
- 模拟多继承。
- 公开framework的私有方法。
Category的使用注意
因为谁都可以扩展一个类,所以在使用Category时有几点需要注意的地方。
比如你的应用扩展了NSString类,同时你链接的第三方库也扩展了NSString类。刚好两者扩展了一个相同的方法名。由于Category是在runtime时实现的,这是加载哪个实现是不确定的,从而会导致不确定的结果。
又比如你扩展了NSSortDescriptor
类,增加了一个sortDescriptorWithKey:ascending:
方法。在低版本的iOS中这个方法是不存在的,但是高版本的iOS中,这个方法是默认被实现的。这时就会有一个命名冲突。
因此为了避免上述的情况,建议是在扩展类的方法名前加入前缀。比如:
@interface NSSortDescriptor (XYZAdditions)
+ (id)xyz_sortDescriptorWithKey:(NSString *)key ascending:(BOOL)ascending;
@end
Category与Extension的区别
Extension像一个匿名的Category,但是两者差别很大。Extension只能附加在源码的类上面,它是编译时决定的,从而可以添加实例变量。而Category是在运行时决定的,内存布局已经确定,从而不可以添加实例变量。Extension常用来隐藏实现细节,比如不想对外公开的方法和实例变量等。
Category源码实现
通过查看runtime源码,发现category实际上是一个叫做category_t的结构体
typedef 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;
}
从中我们可以发现,它可以添加实例方法、类方法、协议、实例属性。
接下去我们看一下category是如何加载的。
void _objc_init(void) {
...
dyld_register_image_state_change_handler(dyld_image_state_bound,
1/*batch*/, &map_2_images);
...
}
首先通过runtime的入口函数_objc_init
方法中加载map_2_images
。
const char * map_2_images(enum dyld_image_states state, uint32_t infoCount,
const struct dyld_image_info infoList[])
{
rwlock_writer_t lock(runtimeLock);
return map_images_nolock(state, infoCount, infoList);
}
然后map_2_images
通过加锁访问map_images_nolock
。
const char *map_images_nolock(enum dyld_image_states state, uint32_t infoCount,
const struct dyld_image_info infoList[])
{
...
_read_images(hList, hCount);
...
}
在这里通过_read_images
去读取image。
void _read_images(header_info **hList, uint32_t hCount)
{
...
// Discover categories.
for (EACH_HEADER) {
category_t **catlist =
_getObjc2CategoryList(hi, &count);
for (i = 0; i < count; i++) {
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);
...
// 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 (cat->classMethods || cat->protocols
/* || cat->classProperties */)
{
addUnattachedCategoryForClass(cat, cls->ISA(), hi);
if (cls->ISA()->isRealized()) {
remethodizeClass(cls->ISA());
}
...
}
}
}
...
}
上述代码执行的操作就是找到相应的category,分别把category的实例方法、协议、属性加入到类上,类方法、协议添加到元类上面。首先注册category到目标类上去,然后如果类或则元类已经实现,则重构它的方法列表。
具体的执行是addUnattachedCategoryForClass
和remethodizeClass
方法。
static void addUnattachedCategoryForClass(category_t *cat, Class cls,
header_info *catHeader)
{
runtimeLock.assertWriting();
// DO NOT use cat->cls! cls may be cat->cls->isa instead
NXMapTable *cats = unattachedCategories();
category_list *list;
list = (category_list *)NXMapGet(cats, cls);
if (!list) {
list = (category_list *)
calloc(sizeof(*list) + sizeof(list->list[0]), 1);
} else {
list = (category_list *)
realloc(list, sizeof(*list) + sizeof(list->list[0]) * (list->count + 1));
}
list->list[list->count++] = (locstamped_category_t){cat, catHeader};
NXMapInsert(cats, cls, list);
}
addUnattachedCategoryForClass
方法把类和category做了一个关联映射。
static void remethodizeClass(Class cls)
{
...
attachCategories(cls, cats, true /*flush caches*/);
...
}
而remethodizeClass
方法内部其实调用了attachCategories
方法。attachCategories
方法是真正把category里面的东西加入到类中去。
static void
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
...
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);
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);
}
从中我们可以看到,这里把category中的方法、属性、协议添加到原有的类上面。
这里说明一下,category中的方法并不会覆盖原有的方法,如果存在两个相同的方法。但是由于category中的方法是放在前面的,所以在消息转发查找方法时会先找到category的方法,从而形成了覆盖原有方法的错觉。
Category与+load()
runtime在加载类和分类时,是通过调用各自的指针分开加载的,因此既会执行类的+load()
方法,也会执行分类的+load()
方法。但是当我们手动调用+load()
方法时,则分类的+load()
方法会先于类的+load()
方法,并造成覆盖的错觉。测试代码如下:
#import "Person.h"
@implementation Person
+ (void)load {
NSLog(@"main load");
}
@end
@implementation Person (Fly)
+ (void)load {
NSLog(@"category load");
}
- (void)fly {
NSLog(@"I can fly");
}
- (void)jump {
NSLog(@"I can jump");
}
@end
#import "ViewController.h"
#import "Person.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
[Person load];
}
@end
结果如下:
2018-01-30 17:02:44.442283+0800 CategoryDemo[23354:4396068] main load
2018-01-30 17:02:44.442758+0800 CategoryDemo[23354:4396068] category load
2018-01-30 17:02:44.599671+0800 CategoryDemo[23354:4396068] category load
从结果中可知先调用了类的+load()
方法,再调用了分类的+load()
方法。最后主动调用时,调用了分类的+load()
方法。
Category与实例变量
从源码中可知,category是无法添加实例变量的。但是有时往往需要实例变量,这时可以通过runtime关联对象做一个假的实例变量。
-------------------------------
@interface Person (Fly)
@property(nonatomic,copy) NSString *name;
@end
-------------------------------
static NSString *associateKey = @"name";
@implementation Person (Fly)
- (void)setName:(NSString *)name {
objc_setAssociatedObject(self, &associateKey, name, OBJC_ASSOCIATION_COPY);
}
- (NSString *)name {
return objc_getAssociatedObject(self, &associateKey);
}
@end
然后我们分析一下objc_setAssociatedObject
的源码。
void objc_setAssociatedObject(id object, const void *key, id value,
objc_AssociationPolicy policy)
{
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) {
...
AssociationsHashMap::iterator i = associations.find(disguised_object);
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
(*refs)[key] = ObjcAssociation(policy, new_value);
...
}
...
}
...
}
从中可以发现,这个associateObject
是由AssociationsManager
管理的,AssociationsManager
里面有一个AssociationsHashMap
的哈希表,用来存储所有的object,其key值为这个objcet的地址。
参考
1.Category
2.Customizing Existing Classes
3.Objective-C Category 的实现原理
4.深入理解Objective-C:Category