美文网首页
Category与Extension

Category与Extension

作者: alvin_wang | 来源:发表于2018-04-02 17:03 被阅读12次

    在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

    相关文章

      网友评论

          本文标题:Category与Extension

          本文链接:https://www.haomeiwen.com/subject/qdyxhftx.html