iOS RunTime之五:Category

简介

CategoryObjective-C 2.0 之后添加的特性,一般我们使用 Category 的场景主要可以动态地为已经存在的类扩展新的属性和方法。这样做的好处就是:

  • 可以减少臃肿的代码。
  • 可以把不同的功能拆开,方便以后的维护。

以下源码来自于opensource.apple.com的objc4-750.tar.gz

runtime.h中查看定义中:

#if __OBJC2__
typedef struct method_t *Method;
typedef struct ivar_t *Ivar;
typedef struct category_t *Category;
typedef struct property_t *objc_property_t;
#else
typedef struct old_method *Method;
typedef struct old_ivar *Ivar;
typedef struct old_category *Category;
typedef struct old_property *objc_property_t;
#endif

打开 objc 源代码,在objc-runtime-new.h中我们可以发现:

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;

    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }

    property_list_t *propertiesForMeta(bool isMeta) {
        if (isMeta) return nil; // classProperties;
        else return instanceProperties;
    }
};

注意:

  • name:是指 class_name 而不是 category_name
  • cls:要扩展的类对象,编译期间是不会定义的,而是在运行时通过 * name 对应到对应的类对象。
  • instanceMethods:category 中所有给类添加的实例方法的列表。
  • classMethods:category 中所有添加的类方法的列表。
  • protocols:category 实现的所有协议的列表。
  • instanceProperties:category 中添加的所有属性。

从 category 的定义也可以看出 category 可以添加实例方法,类方法,甚至可以实现协议,添加属性,无法添加实例变量。

For example

Paste_Image.png

使用 clang 的命令去看看 category 到底会变成什么:

clang -rewrite-objc Person+Student.m

注意:

Paste_Image.png

解决方法
在终端中输入下面的命令:

sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
Paste_Image.png

然后打开文件目录,会发现多了一个 10w 多行的 .cpp 文件。

Paste_Image.png

上图中可以发现:

  • 编译器生成了实例方法列表 _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Student
    和属性列表 _OBJC_$_PROP_LIST_Person_$_Student,并且两者的命名格式都遵循了公共前缀+扩展列表+类名+category名字的命名方式。
  • 在实例方法列表里面找到了在 Student 这个 category 里面扩展的 study 方法,属性列表里面找到了在 Student 里添加的 age 属性。
  • category 的名称用 static 来修饰,目的是用来区分可以在不同分类中扩展各种列表以及后面的 category 结构体本身命名,所以在同一个编译单元里我们的 category 名不能重复,否则会出现编译错误。

Category的原理

Category 是依赖于 dyld 动态加载:

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();
    lock_init();
    exception_init();
       
    // Register for unmap first, in case some +load unmaps something
    _dyld_register_func_for_remove_image(&unmap_image);
    dyld_register_image_state_change_handler(dyld_image_state_bound,
                                             1/*batch*/, &map_images);
    dyld_register_image_state_change_handler(dyld_image_state_dependents_initialized, 0/*not batch*/, &load_images);
}

Category 被附加到类上面是在 map_images 的时候发生的,在 new-ABI 的标准下,_objc_init 里面的调用的 map_images 最终会调用 objc-runtime-new.mm 里面的 _read_images 方法,而在 _read_images 方法的结尾,有以下的代码片段:

/***********************************************************************
* _read_images
* Perform initial processing of the headers in the linked 
* list beginning with headerList. 
*
* Called by: map_images_nolock
*
* Locking: runtimeLock acquired by map_images
**********************************************************************/
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
        if (DisableTaggedPointers) {
            disableTaggedPointers();
        }
        
        initializeTaggedPointerObfuscator();

        if (PrintConnecting) {
            _objc_inform("CLASS: found %d classes during launch", totalClasses);
        }

        // namedClasses
        // Preoptimized classes don't go in this table.
        // 4/3 is NXMapTable's load factor
        int namedClassesSize = 
            (isPreoptimized() ? unoptimizedTotalClasses : totalClasses) * 4 / 3;
        gdb_objc_realized_classes =
            NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);
        
        allocatedClasses = NXCreateHashTable(NXPtrPrototype, 0, nil);
        
        ts.log("IMAGE TIMES: first time tasks");
    }

    // 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);
                }
            }
        }
    }
}

从代码中看出,
通过 _getObjc2ClassList 方法获取所有的 Category 然后添加到 catlist 数组中;把 Category 的实例方法、协议以及属性添加到类上;把 Category 的类方法和协议添加到类的 metaclass 上。addUnattachedCategoryForClass 方法只是把类和 Category 做一个关联映射。

static void remethodizeClass(Class cls)
{
    category_list *cats;
    bool isMeta;

    runtimeLock.assertLocked();

    isMeta = cls->isMetaClass();

    // Re-methodizing: check for more categories
    if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
        if (PrintConnecting) {
            _objc_inform("CLASS: attaching categories to class '%s' %s", 
                         cls->nameForLogging(), isMeta ? "(meta)" : "");
        }
        
        attachCategories(cls, cats, true /*flush caches*/);        
        free(cats);
    }
}

remethodizeClass 方法将 Category 合并到类中,然后更新类的方法列表、协议列表和属性列表,更新类以及子类的方法缓存。

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 合并到类中。

首先根据方法列表,属性列表,协议列表,malloc 分配内存,根据多少个分类以及每一块方法需要多少内存来分配相应的内存地址。

通过 while 循环,遍历所有的 Category,得到它的方法列表 mlistproplistprotolist 并存入 mlistsproplistsprotolists 中。换句话说,它的主要作用就是将 Category 中的方法、属性和协议拼接到类(主类或元类)中,更新类的数据字段 data()mlistproplistprotolist 的值。

之后通过类对象的 data() 方法,调用方法列表、属性列表、协议列表的 attachList 函数,将所有的分类的方法、属性、协议列表数组传进去,将分类和本类相应的对象方法,属性,和协议进行了合并。

image.png

attachList 函数中 memmove 内存移动和 memcpy 内存拷贝。

// memmove :内存移动。
/*  __dst : 移动内存的目的地
*   __src : 被移动的内存首地址
*   __len : 被移动的内存长度
*   将__src的内存移动__len块内存到__dst中
*/
void    *memmove(void *__dst, const void *__src, size_t __len);

// memcpy :内存拷贝。
/*  __dst : 拷贝内存的拷贝目的地
*   __src : 被拷贝的内存首地址
*   __n : 被移动的内存长度
*   将__src的内存移动__n块内存到__dst中
*/
void    *memcpy(void *__dst, const void *__src, size_t __n);

经过 memmovememcpy 方法之后,分类的方法,属性,协议列表被放在了类对象中原本存储的方法,属性,协议列表前面。为了保证分类方法优先调用,我们知道当分类重写本类的方法时,会覆盖本类的方法。其实经过上面的分析我们知道本质上并不是覆盖,而是优先调用,本类的方法依然在内存中的。

通过以上可以看出:

  • Category 的方法没有“完全替换掉”原来类已经有的方法,也就是说如果 Category 和原来类都有 methodA,那么 category 附加完成之后,类的方法列表里会有两个 methodA
  • Category 的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,这也就是我们平常所说的 Category 的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会罢休,殊不知后面可能还有一样名字的方法。

你可能感兴趣的:(iOS RunTime之五:Category)