Category深度解析

作者: sonialiu | 来源:发表于2016-06-19 19:21 被阅读1016次

    因为Category在iOS平时的开发中用的比较多,所以本文从各个方面介绍一下Category,很多地方都是自己的理解,欢迎各位看到的同学批评指正,多多交流。

    一、Category作用

    1. 为已存在的类添加方法;对类进行了很好的扩展,应对变化的需求。
    2. 可以把一个类的实现分散到多个文件中,使得每个文件不至于庞大,而且可以聚集同一逻辑的代码在一个文件中。同时,也可以按需加载方法。
    3. 同一个类可以由多个人共同完成。

    二、为什么category不能添加属性或者说是成员变量

    这个问题从runtime的角度进行分析。
    下图是objc_class结构体:


    classOfRuntime.png

    不详细介绍每一个字段的含义了,主要介绍与category相关的部分:
    在上面的objc_class结构体中,ivars是objc_ivar_list(成员变量列表)指针;methodLists是指向objc_method_list指针的指针。在Runtime中,objc_class结构体大小是固定的,不可能往这个结构体中添加数据,只能修改。所以ivars指向的是一个固定区域,只能修改成员变量值,不能增加成员变量个数。methodList是是一个二维数组,所以可以修改*methodLists的值来增加成员方法,虽没办法扩展methodLists指向的内存区域,却可以改变这个内存区域的值(存储的是指针)。因此,可以动态添加方法,不能添加成员变量。类似的是protocol,因为它是编译器处理的,所以可以添加变量。category是在运行时处理的。

    三、怎么样可以实现添加属性或者说添加成员变量

    上面说了category中不可能添加成员变量或属性。因为系统无法合成实现属性所需的实例变量,所以category中也无法添加@property。但有时候确实需要添加属性,那有没有其他办法,可以不改变对象的内存结构,变相的给对象增加成员变量。
    我们可以Runtime的objc_getAssociatedObject和objc_setAssociatedObject方法来模拟属性的get和set方法,用关联对象来模拟实例变量,这样就有了@property的三要素,只是跟@property的实现机制是完全不一样的。
    有两种方式,第一种代码更简洁:

    .h文件:

    @property (nonatomic) NSString Id;
    

    .m文件,要#import <objc/runtime.h>

    - (NSString *)Id
    {
       return objc_getAssociatedObject(self, @selector(Id));
    }
    
    - (void)setId:(NSString *)Id
    {
       objc_setAssociatedObject(self, @selector(Id), Id, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    

    第二种:

    static void *UIViewPoint =@"UIViewPoint";
    @implementation UIView (Point)
    
    @dynamic pointView;
    
    - (void)setPointView:(UIView *)pointView {
        objc_setAssociatedObject(self, UIViewPoint, pointView,OBJC_ASSOCIATION_RETAIN);
    }
    
    - (UIView *)pointView {
       return objc_getAssociatedObject(self, UIViewPoint);
    }
    

    这两个方法的原型如下:

    id objc_getAssociatedObject(id object, const void *key);
    void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
    

    @selector(Id) 是参数中的 key,方法二中使用了静态指针 static void * 类型的参数来代替,第一种方法因为省略了声明参数的代码,并且能很好地保证 key 的唯一性,所以更简洁。

    OBJC_ASSOCIATION_RETAIN_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. */
    };
    

    当属性是基本数据类型的时候,可能只是需要添加get和set方法,不需要实例变量的时候,可以在category中这样添加属性,这种方式,同样没有改变对象的内存结构。

    @property (nonatomic) CGFloat left;
       @property (nonatomic) CGFloat right;
    
    - (CGFloat)left {
        return self.frame.origin.x;
    }
    - (void)setLeft:(CGFloat)x {
        CGRect frame = self.frame;
        frame.origin.x = x;
        self.frame = frame;
    }
    - (CGFloat)right {
        return self.left + self.width;
    }
    
    - (void)setRight:(CGFloat)right {
        if(right == self.right){
            return;
        }
        CGRect frame = self.frame;
        frame.origin.x = right - frame.size.width;
        self.frame = frame;
    }
    

    我查了很多资料,很多人认为category可以添加属性,但是不可以添加成员变量,我觉得这种说法是不严谨的,所以写了这篇博客,来总结一下。如果有理解的不对的地方,希望看到的朋友指正,大家多多交流。

    参考:

    1. http://yulingtianxia.com/blog/2014/11/05/objective-c-runtime/
    2. http://zhangbuhuai.com/2015/04/26/unstanding-the-Objective-C-Runtime-part1/
    3. http://draveness.me/ao/

    相关文章

      网友评论

      • 387644ace1ad:objc_class是已经被编译过的,被编译过的结构体大小不允许改变。
        category 是基于运行时的:
        typedef struct category_t {
        const char *name; //类的名字
        classref_t cls; //类
        struct method_list_t *instanceMethods; //category中所有给类添加的实例方法的列表
        struct method_list_t *classMethods; //category中所有添加的类方法的列表
        struct protocol_list_t *protocols; //category实现的所有协议的列表
        struct property_list_t *instanceProperties; //category中添加的所有属性
        } category_t;

        结构体中并没有成员变量这个list,所以无法在categoty添加成员变量,添加属性是可以的,但是不会生成添加属性的getter和setter方法,所以,尽管添加了属性,也无法使用点语法调用getter和setter方法,但你可以使用运行时实现关联对象并可以引用。
        XIAODAO:另外,文中objc_class采用的是Objective-C 1.0 对象布局模型,现在使用的都是Objective-C 2.0 对象布局模型了

        runtime源码下载:
        https://opensource.apple.com/tarballs/objc4/
        比如680版本就是Objective-C 2.0
        XIAODAO:两位好,我水平很菜,请不吝赐教哈。请问下“objc_class是已经被编译过的,被编译过的结构体大小不允许改变”这句的理解。这个是编译原理里面的内容吗?我想自己琢磨一下,却不知道从哪方面去找资料
      • XIAODAO:你好,请问“在Runtime中,objc_class结构体大小是固定的”,这个是为什么呢?
        387644ace1ad:objc_class是已经被编译过的,被编译过的结构体大小不允许改变。
        category 才是基于运行时的,结构体如下:
        typedef struct category_t {
        const char *name; //类的名字
        classref_t cls; //类
        struct method_list_t *instanceMethods; //category中所有给类添加的实例方法的列表
        struct method_list_t *classMethods; //category中所有添加的类方法的列表
        struct protocol_list_t *protocols; //category实现的所有协议的列表
        struct property_list_t *instanceProperties; //category中添加的所有属性
        } category_t;
      • 那一年的北海:学习了。

      本文标题:Category深度解析

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