×

(三) runtime 实际应用场景之动态给分类添加属性

96
意一yiyi
2017.08.16 16:55* 字数 2472

我们知道分类一般是用来给系统的类扩展方法用的, 但是它不能用来给系统的类扩展属性, 其实 准确的说分类是不能给系统的类添加成员变量, 但可以添加属性, 只不过添加的属性没有自动生成与之对应的成员变量, 也没有自动生成 setter 和 getter 方法, 然而我们可以通过 runtime 的关联对象来动态地给帮它完成后续的步骤. 先截个图把问题撂这儿 :

比如我们为一个 Person 的类添加一个叫 Gender 的分类, 并在其中添加了一个叫 gender 的属性, 会发现编译并没有报错, 而仅仅是警告我们没有实现 gender 的setter 和 getter 方法而已. 如下 :


给分类添加属性

给分类添加属性警告

但是如果给分类添加成员变量, 编译会直接报错, 说是你不应该在分类里添加成员变量吧. 如下 :


给分类添加成员变量报错

好了, 那在用 runtime 关联对象完成动态给分类添加属性之前, 我想先弄明白为什么分类不能直接添加成员变量, 但却可以添加属性? 当然在这个问题之前我还想先弄明白成员变量和属性到底是什么区别?


目录

一. 成员变量与属性的区别

二. 为什么不能直接给分类添加成员变量, 但是可以添加属性

三. 动态给分类添加属性

1. 非得用 runtime 关联对象才行吗?
2. 如何使用 runtime 关联对象为分类动态添加属性?

一. 成员变量与属性的区别

要想弄清成员变量和属性的区别, 我们可以通过 iOS5 之前 创建一个成员变量, 以及为其生成 setter 和 getter 方法的方式来理解一下.

  • 假设我们现在有一个 Person 类, 我们为其添加了两个成员变量, 如下 :
@interface Person : NSObject {

    @public
    NSInteger _age;
    NSString *_nickname;
}

@end


@implementation Person

@end
  • 那么如果我们现在想要访问这两个成员变量, 那我们就只能通过箭头的方式来访问, 如下 :
Person *person = [[Person alloc] init];
person->_nickname = @"懿一";
NSLog(@"%@", person->_nickname);

但是, 这种的访问方式是有局限性的, 比如说我们想在访问成员变量的时候添加一些我们想要的额外功能(比如我们想要在访问成员变量的时候就指定成员变量的内存管理方式等), 那我们就需要手动地为成员变量添加一对访问方法了, 只不过大家约定俗成习惯叫这对方法为 setter 和 getter, 这也就是 为啥非要给某个属性添加 setter 和 getter 方法 的原因. 如下 :

- (void)setNickname:(NSString *)nickname;
- (NSString *)nickname;

- (void)setNickname:(NSString *)nickname {
    
    if (_nickname != nickname) {
        
        [_nickname release];
        _nickname = [nickname copy];
    }
}

- (NSString *)nickname {
    
    return _nickname;
}

然后我们就可以通过 点语法 使用具备了额外功能的 setter 和 getter 方法来 间接地访问 成员变量(之所以说是间接地访问, 是因为对成员变量真正的赋值和读取操作其实是在 setter 和 getter 方法内部, setter 和 getter 方法相当于说是一个访问成员变量的 Api).

那么问题来了, 开发中有那么多的成员变量, 我们要是一个一个为它们手动添加 setter 和 getter 方法的话, 那多费劲啊, 因此苹果才引入了 属性 @property@synthesize 这两个概念, 其目的就是 根据我们指定的内存管理策略, 是否原子性访问以及读写权限等设置自动为属性生成一堆 setter 和 getter 方法, 并将这对 setter 和 getter 方法关联到指定的成员变量上. 例如, 我们声明了一个 @property (nonatomic, copy) NSString *nickname; 的属性, 它的作用就是 声明该属性的内存管理策略, 是否原子性访问以及读写权限等设置, 为自动生成 setter 和 getter 方法做准备, 而真正完成 setter 和 getter 方法生成的其实不是 @property, 而是 @synthesize, 正是 @synthesize 依据 @property 的那几个设置才 为属性(注意生成的 setter 和 getter 方法是属于属性的, 而不是直接属于成员变量的)生成了 setter 和 getter 方法, 同时由 @synthesize nickname = _nickname; 这句话的指针赋值操作, 我们可以知道的这句话含义就是表明 为 nickname 生成的存取方法将被用来作用于成员变量 _nickname, 因此上面说了 点语法才会触发 setter 和 getter 方法来间接访问成员变量, 如果直接通过下划线访问成员变量是不会触发 setter 和 getter 方法的.

那么通过上面的一些阐述, 我们就可以对 iOS5 之前创建一个成员变量的步骤做一个总结 :

  • 第一步, 声明一个成员变量;
  • 第二步, 声明一个属性, 其内存管理策略, 是否原子性操作及读写权限等设置用来为 @synthesize 做依据;
  • 第三步, @ synthesize 依据属性的一些设置最终生成 setter 和 getter 方法, 并将 setter 和 getter 的作用目标由属性转为成员变量.

好了, 知道了 iOS5 之前的创建一个成员变量的步骤, 我们就可以得出最终的结论, 针对目前的 iOS 版本, 成员变量与属性的区别是 :

  • 成员变量仅仅是声明了一个成员变量;
  • 而属性则是把 iOS5 之前生成一个成员变量的三步并做了一步, 通过声明一个属性, 会自动该属性生成一个对应的成员变量, 并声明该成员变量的内存管理策略, 是否原子性操作及读写权限等设置, 同时属性还完成了之前 @synthesize 的工作, 为属性自动生成了 setter 和 getter 方法, 并将存取方法的目标由属性转为成员变量.
一个额外知识点 : 
其实和 @synthesize 对应的有一个 @dynamic, 它用来表明不为属性自动生成 setter 和 getter 方法, 而是在运行时动态创建, 一会的动态添加属性其实就可以写一下子.

二. 为什么不能直接给分类添加属性

很简单了, 我们直接看一下 runtime 库对分类(category) 的定义就知道了, 定义如下 :

struct objc_category {
    
    // 分类名
    char *category_name                          OBJC2_UNAVAILABLE;
    // 分类所属的类名
    char *class_name                             OBJC2_UNAVAILABLE;
    // 实例方法列表
    struct objc_method_list *instance_methods    OBJC2_UNAVAILABLE;
    // 类方法列表
    struct objc_method_list *class_methods       OBJC2_UNAVAILABLE;
    // 分类所实现的协议列表
    struct objc_protocol_list *protocols         OBJC2_UNAVAILABLE;
}

可见分类只是依托于其所属的类, 其内部只有用来存储扩展方法和协议的成员变量, 并没有用来存储成员变量的成员变量, 所以分类不能添加成员变量, 但是 objc_category 里面也没看到有用来存储属性的成员变量啊, 那为什么给分类添加属性编译不会报错, 而仅仅是警告呢?

打开 OC runtime 库 --> Project Headers --> objc-runtime-new.h, 搜索 category_t, 我们可以找见 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);
};

我们可以发现 分类里面其实是有用来存储属性的成员变量的, 只不过对于外界来说它是个隐藏成员变量, 它们隐藏就是不想让我们通过分类添加属性, 因为添加了属性之后并不会根据这个属性生成对应的成员变量, 也不会生成相应的 setter 和 getter 方法, 如果不能合理地处理这种现象的话, 程序是要崩掉的, 而这个合理地解决办法就是 runtime 关联对象.

三. 动态给分类添加属性

1. 非得用 runtime 关联对象才行吗?

同样地, 我们假设有一个 Person 类, 定义如下 :

@interface Person : NSObject {
    
    @public
    NSInteger _age;
    NSString *_nickname;
}

- (void)smile;

@end


@implementation Person

- (void)smile {
    
    NSLog(@"要保持微笑哦");
}

@end

然后我们用如下方法打印 Person 类的成员变量, 属性还有方法. 如下 :

// 获取 Person 类所有的成员变量
unsigned int ivarCount = 0;
Ivar *ivars = class_copyIvarList([Person class], &ivarCount);
for (int i = 0; i < ivarCount; i ++) {
    
    Ivar ivar = ivars[i];
    NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
    
    NSLog(@"Person 类的成员变量 : %@", ivarName);
}
free(ivars);

// 获取 Person 类所有的属性
unsigned int propertyCount = 0;
objc_property_t *properties = class_copyPropertyList([Person class], &propertyCount);
for (int i = 0; i < propertyCount; i ++) {
    
    objc_property_t property = properties[i];
    NSString *propertyName = [NSString stringWithUTF8String:property_getName(property)];
    
    NSLog(@"Person 类的属性 : %@", propertyName);
}
free(properties);

// 获取 Person 类所有的实例方法
unsigned int methodCount = 0;
Method *methods = class_copyMethodList([Person class], &methodCount);
for (int i = 0; i < methodCount; i ++) {
    
    Method method = methods[i];
    NSString *methodName = NSStringFromSelector(method_getName(method));
    
    NSLog(@"Person 类的实例方法 : %@", methodName);
}
free(methods);

打印结果如下 :

2017-08-14 22:44:42.707 Runtime[6345:750498] Person 类的成员变量 : _age
2017-08-14 22:44:42.707 Runtime[6345:750498] Person 类的成员变量 : _nickname
2017-08-14 22:44:42.707 Runtime[6345:750498] Person 类的实例方法 : smile

然后我们为 Person 类创建了一个 Gender 的分类, 并添加了一个 gender 的属性, 如下 :

@interface Person (Gender) 

@property (nonatomic, assign) BOOL gender;

@end


@implementation Person (Gender)

@end

再次打印 Person 类的成员变量, 属性还有方法, 如下 :

2017-08-14 22:53:46.333 Runtime[6412:758515] Person 类的成员变量 : _age
2017-08-14 22:53:46.333 Runtime[6412:758515] Person 类的成员变量 : _nickname
2017-08-14 22:53:46.333 Runtime[6412:758515] Person 类的属性 : gender
2017-08-14 22:53:46.333 Runtime[6412:758515] Person 类的实例方法 : smile

可见分类真得可以添加属性, 但是添加的属性确确实实没有自动生成相应的成员变量和 setter, getter 方法.
那我们手动给属性添加 setter 和 getter 方法呗, 如下 :

@implementation Person (Gender)

- (void)setGender:(BOOL)gender {
    
    
}

- (BOOL)gender {
    
    
}

@end

但是方法里怎么实现啊? 我们知道 ** setter 和 getter 方法仅仅是访问成员变量的一对 Api**, 可是现在连成员变量都没有, 那怎么操作啊? 看来好像没办法了, 还好有 runtime 关联对象, 用它来代替成员变量的作用.

2. 如何使用 runtime 关联对象为分类动态添加属性?

(1) 什么是关联对象?
我们知道一个类中的属性会生成一个成员变量, 生成 setter 和 getter 方法并自动将对属性的存取操作转换为对成员变量的存取操作, 换句话说就是成员变量关联到了属性身上.

而成员变量就是存在于 class 结构体 ivars 中的一个对象而已, 用来存值; 而关联对象也仅仅是存在于一个单独哈希表中的对象(听别人说的), 就一动态生成的对象, 没那么神秘, 当然可以用来存值, 因此我们可以用它来冒充成员变量.

因此 runtime 的 关联对象 完全可以看做是 类似 于成员变量的那么一个 对象, 只不过这个对象不是在编译时生成的, 而是在调用 setter 方法时才动态生成的, 不是存在于 class 结构体内部而是存在于一个单独的哈希表中, 但是这并不影响 class 通过 setter 和 getter 方法访问它啊.

(2) 那为什么利用 runtime 关联对象可以完成给分类添加属性这样的操作?
因为我们缺一个存取东西的对象(成员变量), 关联对象可以代替, 我们可以暂时认为关联对象是一个存储在单独哈希表中的一个成员变量, 这样就好理解了;
还缺一对 setter 和 getter 方法, 我们可以自己手写.

(3) 具体的关联操作, 如下 :

- (void)setGender:(BOOL)gender {
    
    // object : 想要把关联对象关联到哪个对象的属性上
    // key : 想要把关联对象关联到哪个属性上, 此处就是 @"gender" 属性
    // value : 关联对象, 调用 setter 方法时才动态生成的一个对象, 此处为 @(gender)
    // policy : 关联对象的内存管理策略
    objc_setAssociatedObject(self, @"gender", @(gender), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (BOOL)gender {
    
    // object, key : 想要获取哪个对象的哪个属性的关联对象
    return [objc_getAssociatedObject(self, @"gender") boolValue];
}
iOS 开发:runtime
Web note ad 1