IOS底层原理之Category窥探

一、category简介

category是Objective-C 2.0之后添加的语言特性,主要作用是为已经存在的类添加方法,苹果官方推荐两个使用场景。

  1. 将类的实现分割成多个文件。这样便可以将一个庞大的类按照功能的不同包装成多个文件实现,大大提高的代码的可读性;多个开发者可以同时完成一个类。
  2. 声明私有方法。

在日常的开发中,我们也不仅仅是基于这两个使用场景,我们还可以通过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,
};

从这样的一段代码中我们得出如下信息:

  1. 编译器生成了方法列表OBJC_$CATEGORY_INSTANCE_METHODS_Person$_DS,列表中填充的正是我们在DS这个category中定义的方法sayCategory。
  2. 编译器生成了属性列表OBJC_$PROP_LIST_Person$_DS,列表中填充的是我们在DS这个category中定义的属性name。
  3. 编译器生成了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的结构信息:

  1. 名称-name
  2. 类-cls
  3. category为类添加的对象方法列表-instanceMethods
  4. category为类添加的类方法列表-classMethods
  5. category为类添加的协议列表-protocols
  6. category为类添加的实例属性列表-instanceProperties
  7. 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加载到类的过程中做了如下几个操作

  1. 通过_getObjc2CategoryList拿到category的列表。
  2. 将category中的对象方法,协议以及属性添加到类中。
  3. 将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

三、总结

  1. category的底层结构是category_t,是一个结构体。
  2. category中可以添加对象方法、类方法、协议实现、属性,但是不能添加实例变量。
  3. category在运行期间加载到类中。
  4. category中的方法不会真正覆盖类中的同名方法,而是将category中的方法加载在方法列表的前面,方法调用的按顺序查找方法,所以在方法调用的时候调用的是category中的方法。
  5. category中的属性并无实质性的意义,可以利用objc_setAssociatedObjectobjc_getAssociatedObject和对象关联。
  6. 所有对象的关联对象存储在一个全局的AssociationsHashMap中。

你可能感兴趣的:(IOS底层原理之Category窥探)