美文网首页
iOS - Little Category

iOS - Little Category

作者: kwdx | 来源:发表于2018-05-31 21:47 被阅读0次

    category是Objective-C里面最常用到的功能之一。category可以为已有的类增添实例方法或类方法,而不需要修改原有类。

    Category 结构体

    在苹果开源的objc项目的objc-runtime-new.h头文件中可以找到Category的结构体

    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;      
    
        method_list_t *methodsForMeta(bool isMeta) {
            if (isMeta) return classMethods;
            else return instanceMethods;
        }
    
        property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
    };
    

    name:   Category所有附加的类的类名
    cls:   Category索要附加的类对象
    instanceMethods:   Category中定义的实例方法
    classMethods:   Category中定义的类方法
    protocols:   Category中声明实现的协议列表
    instanceProperties:   Category中声明的实例属性
    _classProperties:   Category中声明的类属性,但不会出现在内存中

    当然,这些字段所代表的函数都只是我们的猜测而已,现在就需要一步一步的去验证是不是真的是这样子。我们可以先简单的创建一个Person的分类Test1来分析category_t结构中是不是真的存储这些值

    // Person+Test1.h
    @interface Person (Test1) <NSCopying, NSMutableCopying>
    @property (nonatomic, strong) NSString *smallName;
    - (void)run;
    - (void)eat;
    + (void)battle;
    @end
    
    // Person+Test1.m
    @implementation Person (Test1)
    - (void)run {
        NSLog(@"categroy1 - run");
    }
    - (void)eat {
        NSLog(@"categroy1 - eat");
    }
    + (void)battle {
        NSLog(@"categroy1 - battle");
    }
    @end
    

    通过clang编译器反编译之后能够得到C/C++代码,下面截取Person(Test1)结构体的相关代码分析一下
    xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person+Test1.m

    static struct _category_t _OBJC_$_CATEGORY_Person_$_Test1 __attribute__ ((used, section ("__DATA,__objc_const"))) = 
    {
        "Person",
        0, // &OBJC_CLASS_$_Person,
        (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test1,
        (const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Test1,
        (const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_Person_$_Test1,
        (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Person_$_Test1,
    };
    
    • 第一个字段namePerson
    • 第二个字段cls是0,但是注释为Person的类对象地址,这个待会再看
    • 第三个字段instanceMethods是一个_method_list_t的结构体指针
    • 第四个字段classMethods也是一个_method_list_t的结构体指针
    • 第五个字段protocols则是一个_protocol_list_t的结构体指针
    • 第六个字段instanceProperties是一个_prop_list_t的结构体指针
    • 第七个字段_classProperties,没有这个字段了,在category_t结构体的定义中也说明了这个字段不会出现在存储中

    我们一个一个的查看这些结构体指针里面是否真的存储的是我们的Person(Test1)分类中的定义的内容

    1. instanceMethods
    static void _I_Person_Test1_run(Person * self, SEL _cmd) {
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_pw_9kxn77w15h15s3l_24f9vtv80000gn_T_Person_Test1_28c269_mi_0);
    }
    static void _I_Person_Test1_eat(Person * self, SEL _cmd) {
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_pw_9kxn77w15h15s3l_24f9vtv80000gn_T_Person_Test1_28c269_mi_1);
    }
    static struct /*_method_list_t*/ {
        unsigned int entsize;  // sizeof(struct _objc_method)
        unsigned int method_count;
        struct _objc_method method_list[2];
    } _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test1 __attribute__ ((used, section ("__DATA,__objc_const"))) = {
        sizeof(_objc_method),
        4,
        {{(struct objc_selector *)"run", "v16@0:8", (void *)_I_Person_Test1_run},
        {(struct objc_selector *)"eat", "v16@0:8", (void *)_I_Person_Test1_eat}},
        {(struct objc_selector *)"copyWithZone:", "@24@0:8^{_NSZone=}16", (void *)_I_Person_Test1_copyWithZone_},
        {(struct objc_selector *)"mutableCopyWithZone:", "@24@0:8^{_NSZone=}16", (void *)_I_Person_Test1_mutableCopyWithZone_}}
    };
    

    OBJC$CATEGORY_INSTANCE_METHODS_Person$_Test1里面存储了四个_objc_method结构体,通过objc_selector可以看出这四个函数刚好是我们实现的runeatcopyWithZone:mutableCopyWithZone:四个实例方法,而函数调用地址也是我们在.m文件中实现的方法。

    2. classMethods
    static struct /*_method_list_t*/ {
        unsigned int entsize;  // sizeof(struct _objc_method)
        unsigned int method_count;
        struct _objc_method method_list[1];
    } _OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Test1 __attribute__ ((used, section ("__DATA,__objc_const"))) = {
        sizeof(_objc_method),
        1,
        {{(struct objc_selector *)"battle", "v16@0:8", (void *)_C_Person_Test1_battle}}
    };
    

    反观OBJC$CATEGORY_CLASS_METHODS_Person$_Test1里面只有一个方法,并且是我们定义并实现的battle类方法。

    3. protocols
    static struct /*_protocol_list_t*/ {
        long protocol_count;  // Note, this is 32/64 bit
        struct _protocol_t *super_protocols[2];
    } _OBJC_CATEGORY_PROTOCOLS_$_Person_$_Test1 __attribute__ ((used, section ("__DATA,__objc_const"))) = {
        2,
        &_OBJC_PROTOCOL_NSCopying,
        &_OBJC_PROTOCOL_NSMutableCopying
    };
    

    我们在分类中实现的分类有2个,分别为NSCopyNSMutableCopying;而在OBJC_CATEGORY_PROTOCOLS$Person$_Test1中的2个协议就是我们所实现的2个协议。

    4. instanceProperties
    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_$_Test1 __attribute__ ((used, section ("__DATA,__objc_const"))) = {
        sizeof(_prop_t),
        1,
        {{"smallName","T@\"NSString\",&,N"}}
    };
    

    我们在分类的声明中声明定义了一个名为smallName的属性,在OBJC$PROP_LIST_Person$_Test1结构体里面就是我们所声明的实例属性。

    可能有同学有疑问了,如果通过@property声明的属性,编译器应该是会自动为我们生成一个_smallName的成员变量,settergetter方法才对的,为什么这个分类里面既没有_smallName成员变量,也没有settergetter方法呢?
    那是因为成员变量也是在编译的时候确定的,类结构体也是在编译的时候就确定的。而Category分类是在运行加载进内存的时候才附加到类中的,所以Category分类中声明的属性编译器是不会生成对应的成员变量的,既然没有成员变量,settergetter方法也就失去了存在的意义,因此编译器也不会自动生成这两个实例方法。

    Category 加载

    在objc-runtime-new.mm文件中我们找到一段代码,通过方法名猜测是用来加载Category分类的

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

    这段代码理解起来不是很难,3个函数参数分类为类对象clscategory_list结构体指针cats和是否清除缓存flush_caches
    首先定义一个method_list_t结构体指针数组mlists,通过malloc方式分配一块内存,内存大小由category_list里面的category_t结构体个数和结构体指针所占内存共同决定;property_list_t结构体指针数组proplistsprotocol_list_t结构体指针protolists也同理。

    从后往前遍历cats中的list数组,并将每个元素中的cat成员(category_t分类结构体)中的方法数组指针保存到指针数组mlists中,属性数组指针则保存到指针数组proplists中,协议数组指针保存到指针数组protolists中。

    取出cls对象中的class_rw_t结构体赋值为rw,调用attachLists(List* const * addedLists, uint32_t addedCount)将指针数组mlistsproplistsprotolists分别附加到rwmethodspropertiesprotocols上。

    struct class_rw_t {
        // Be warned that Symbolication knows the layout of this structure.
        uint32_t flags;
        uint32_t version;
    
        const class_ro_t *ro;
    
        method_array_t methods;
        property_array_t properties;
        protocol_array_t protocols;
    
        Class firstSubclass;
        Class nextSiblingClass;
    
        char *demangledName;
    
    #if SUPPORT_INDEXED_ISA
        uint32_t index;
    #endif
        ...
    };
    

    class_rw_t主要存放OC类对象(元类对象)的方法数组,属性数组,协议数组,因此将mlistsproplistsprotolists这三个指针数组都附加上去。给原来的类对象(元类对象)附加额外的方法,属性,协议。method_array_tproperty_array_tprotocol_array_t都实现了一套模板template <typename Element, typename List>,这三个成员主体都是指针数组,每个指针所指向的内存又是一个数组,大概的内存如下图所示,如果我理解错了,请联系我

    image.png

    接下来进去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]));
            }
        }
    

    在这个函数内分成了3种情况去处理:

    • 原指针数组有多个值
    1. 计算添加新的指针数组后总的个数,并根据总个数调用realloc函数重新分配内存,*如果新分配的内存大小大于原来的大小,原数据不会丢失
    2. 将原有的指针数组移动到新内存的最后,前面空出addedCount个指针内存
    3. addedLists指针数组复制到新内存的前面
    • 原指针数组为空,且新添加的指针数组中只有一个指针
    1. 直接将** addedLists的唯一指针赋值给list**
    • 其他情况
    1. 重新分配内存
    2. 判断原指针数组是否有值,有值的话,先把值放到数组的最后
    3. addedLists指针数组复制到新内存的前面

    为什么在将原来的指针数组放到数组的最后这一步要用memmove而不是memcpy呢?请看下图,

    image.png

    在调用realloc函数的时候,如果原内存段后有足够的内存空间,则会扩展这段内存段,否则就重新分配一段新的内存空间,并将原内存段中的数据复制到新的内存段中,释放原内存段内存。
    假如原数据有3个指针,新扩展了2个指针大小的空间,这时如果调用memcpy函数将原来的3个指针复制到这块内存段的最后,因为memcpy函数是一个一个元素的复制,就会出现中间那个指针在还没复制前就被覆盖丢失了。
    如果调用的是memmove函数,函数内部会先判断源内存段和目标内存段是否存在重叠部分,如果存在,会根据重叠地方判断从头到尾移动数据还是从尾到头来移动数据,上图中源内存段的尾部和目标内存段的头部重叠了,所以memmove会先移动源内存段的尾部数据,这样可以保证数据在迁移前不被覆盖丢失。

    为什么赋值新的指针数据的时候要用memcpy而不继续用memmove呢?猜测是memcpy效率更高吧。

    分类的方法是在原方法的前面,当调用方法的时候,从前往后遍历,先找到分类中的方法就直接调用,但是原方法还在方法列表中的。
    还可以通过运行时来验证原方法是否还存在

    unsigned int count;
    Method *methods = class_copyMethodList([Person class], &count);
    for (int i=0; i<count; i++) {
        Method method = methods[i];
        NSLog(@"%@", NSStringFromSelector(method_getName(method)));
    }
    free(methods);
    

    打印结果为:

    打印结果
    我定义了2个Person的分类,都实现了- (void)run;实例方法,因此打印出来得有3个- (void)run;方法。

    最后附上本文的代码:GitHub传送门

    相关文章

      网友评论

          本文标题:iOS - Little Category

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