Category与Extension

在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到目标类上去,然后如果类或则元类已经实现,则重构它的方法列表。
具体的执行是addUnattachedCategoryForClassremethodizeClass方法。

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

Author: MrHook
Link: https://bigjar.github.io/2018/01/30/Category%E4%B8%8EExtension/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.