iOS分类(category)为什么不能直接添加属性?

从定义分析

先看一下分类的定义:

//Category表示一个结构体指针的类型
typedef struct objc_category *Category;

struct objc_category {
    char * _Nonnull category_name                            OBJC2_UNAVAILABLE;
    char * _Nonnull class_name                               OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable instance_methods     OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable class_methods        OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
}

再看下 Class 的定义:

//Class也表示一个结构体指针的类型
typedef struct objc_class *Class;

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

对比可以发现 category 中少了 struct objc_ivar_list * _Nullable ivars,也就是说没有 ivars 数组,也就没有地方来保存属性的值。

源码实测

先定义一个类 Person:

@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) int age;
@end

@implementation Person
@end

然后打印这个类中的所有的属性、ivar、方法列表:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    // 获取属性
    unsigned int propertyCount;
    objc_property_t *properties = class_copyPropertyList([Person class], &propertyCount);
    for (int i = 0; i < propertyCount; i++) {
        objc_property_t property = properties[i];
        NSLog(@"属性:%d-----%s", i, property_getName(property));
    }
    free(properties); //注意释放c指针,以免内存泄露

    // 获取ivar
    unsigned int ivarCount;
    Ivar *ivars = class_copyIvarList([Person class], &ivarCount);
    for (int i = 0; i < ivarCount; i++) {
        Ivar ivar = ivars[i];
        NSLog(@"ivar:%d-----%s", i, ivar_getName(ivar));
    }
    free(ivars);
    
    // 获取方法列表
    unsigned int methodCount;
    Method *methods = class_copyMethodList([Person class], &methodCount);
    for (int i = 0; i < methodCount; i ++) {
        Method m = methods[i];
        NSLog(@"SEL:%d-----%s", i, sel_getName(method_getName(m)));
    }
    free(methods);
}

打印结果如下:

2019-11-15 09:58:22.117 MusicPlayer[25393:2629911] 属性:0-----name
2019-11-15 09:58:22.118 MusicPlayer[25393:2629911] 属性:1-----age
2019-11-15 09:58:22.118 MusicPlayer[25393:2629911] ivar:0-----_age
2019-11-15 09:58:22.118 MusicPlayer[25393:2629911] ivar:1-----_name
2019-11-15 09:58:22.118 MusicPlayer[25393:2629911] SEL:0-----.cxx_destruct
2019-11-15 09:58:22.118 MusicPlayer[25393:2629911] SEL:1-----setName:
2019-11-15 09:58:22.118 MusicPlayer[25393:2629911] SEL:2-----name
2019-11-15 09:58:22.118 MusicPlayer[25393:2629911] SEL:3-----setAge:
2019-11-15 09:58:22.120 MusicPlayer[25393:2629911] SEL:4-----age

接着添加分类:

@interface Person (Height)
@property (nonatomic, assign) float height;
@end

@implementation Person (Height)
@end

此时 XCode 报警告,没有实现 setter 和 getter。必须实现:

- (float)height {
    return [objc_getAssociatedObject(self, _cmd) floatValue];
}

- (void)setHeight:(float)height {
    objc_setAssociatedObject(self, @selector(height), @(height), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

此时,输出变为:

2019-11-15 10:20:12.838 MusicPlayer[25451:2634649] 属性:0-----height
2019-11-15 10:20:12.839 MusicPlayer[25451:2634649] 属性:1-----name
2019-11-15 10:20:12.839 MusicPlayer[25451:2634649] 属性:2-----age
2019-11-15 10:20:12.840 MusicPlayer[25451:2634649] ivar:0-----_age
2019-11-15 10:20:12.840 MusicPlayer[25451:2634649] ivar:1-----_name
2019-11-15 10:20:12.840 MusicPlayer[25451:2634649] SEL:0-----.cxx_destruct
2019-11-15 10:20:12.840 MusicPlayer[25451:2634649] SEL:1-----setName:
2019-11-15 10:20:12.841 MusicPlayer[25451:2634649] SEL:2-----name
2019-11-15 10:20:12.844 MusicPlayer[25451:2634649] SEL:3-----height
2019-11-15 10:20:12.845 MusicPlayer[25451:2634649] SEL:4-----setHeight:
2019-11-15 10:20:12.845 MusicPlayer[25451:2634649] SEL:5-----setAge:
2019-11-15 10:20:12.846 MusicPlayer[25451:2634649] SEL:6-----age

可以看到实现了 setter 和 getter 之后,仍然没有 ivar: _height,系统没有实现,我们也没有添加。所谓的关联是我们通过 const char 的 key (指针)来访问关联的对象的。所以关联之后我们只能通过 getter 和 setter 方法去操作,不能直接用 ivar _height 来访问!

关联对象的释放

关联对象会在 object_dispose() 方法中释放,不需要用户去释放。

NSObject 调用 -dealloc 时,只调用了 object_dispose()
object_dispose() 则做了以下事情:

  • 为 C++ 的实例变量们(iVars)调用 destructors
  • 为 ARC 状态下的 实例变量们(iVars) 调用 -release
  • 解除所有使用 runtime Associate 方法关联的对象
  • 解除所有 __weak 引用
  • 调用 free()

总结

分类并不会改变原有类的内存分布的情况,它是在运行期间决定的,此时内存的分布已经确定,若此时再添加实例会改变内存的分布情况,这对编译性语言是灾难,是不允许的。反观扩展(extension),作用是为一个已知的类添加一些私有的信息,必须有这个类的源码,才能扩展,它是在编译时生效的,所以能直接为类添加属性或者实例变量。

另外,个人认为:分类中添加 setter 和 getter,其实可以交给编译器来处理,这样会显得语言的设计更加简洁。在类中,声明属性是可以不写 setter 和 getter 的,编译器会帮我们实现,而分类为什么不呢?毕竟分类的属性声明确定之后,setter 和 getter 的实现其实也已经确定了。

由上面的分析联想到 protocol 中添加属性,其实由 protocol 的用途也能猜测出:protocol 是一系列的协议,要求代理去实现,自己并没有实现。方法或属性都是这样,只是做了声明要求代理去实现。所以添加的属性也只是声明其代理有实现这个属性,自身并没有实现其 getter、setter 以及 ivar。

  • 深入理解Objective-C:Category

你可能感兴趣的:(iOS分类(category)为什么不能直接添加属性?)