包罗万象的runtime(二):变量&属性&关联

2018-06-03  本文已影响4人  ElaineYin

成员变量用于类内部,无需与外界接触的变量。根据成员变量的私有性,为了方便访问,所以就有了属性变量。属性变量的好处就是允许让其他对象访问到该变量(因为属性创建过程中自动产生了set 和get方法)。当然,你可以设置只读或者可写等,设置方法也可自定义。所以,属性变量是用于与其他对象交互的变量。

简单来说,就是属性自动生成set、get方法,方面外部访问。
接触iOS的人都知道,@property声明的属性默认会生成一个_类型的成员变量,同时也会生成setter/getter方法。(苹果将默认编译器从GCC转换为LLVM(low level virtual machine)之后)
但是如果同时重写get、set方法,编译还会报错

image.png
原因是:当你复写了get和set方法之后@property默认生成的@synthesize就不会起作用了,这也就意味着你的类不会自动生成出来实例变量了,你就必须要自己声明实例变量
@implementation Person
{
    NSString *_name;
}

或者实现@synthesize name = _name;也可以

1. runtime中的变量&属性

1.1 成员变量Ivar

成员变量的实质是什么呢,在runtime中,它是一个objc_ivar类型的指针

struct objc_ivar {
    char * _Nullable ivar_name                               OBJC2_UNAVAILABLE;
    char * _Nullable ivar_type                               OBJC2_UNAVAILABLE;
    int ivar_offset                                          OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
}       

Ivar的相关操作

//获取Ivar的名称
  const char *ivar_getName(Ivar v);
  //获取Ivar的类型编码,
  const char *ivar_getTypeEncoding(Ivar v)
  //通过变量名称获取类中的实例成员变量
  Ivar class_getInstanceVariable(Class cls, const char *name)
  //通过变量名称获取类中的类成员变量
  Ivar class_getClassVariable(Class cls, const char *name)
  //获取指定类的Ivar列表及Ivar个数
  Ivar *class_copyIvarList(Class cls, unsigned int *outCount)
  //获取实例对象中Ivar的值
  id object_getIvar(id obj, Ivar ivar) 
  //设置实例对象中Ivar的值
  void object_setIvar(id obj, Ivar ivar, id value)

code for test

////定义一个Person类
@interface Person : NSObject
{
    NSString * _address;
    NSString * _idNo;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *sex;
@property (atomic, assign) int age;
- (instancetype)initWithID:(NSString *)idNo address:(NSString *)address;
@end
@implementation Person

- (instancetype)initWithID:(NSString *)idNo address:(NSString *)address {
    if (self = [super init]) {
        _address = address;
        _idNo = idNo;
    }
    return self;
}
@end

/// 调用测试一下
- (void)test {
    Person *person = [[Person alloc] initWithID:@"3715251993098767567" address:@"山东聊城冠县"];
    person.name = @"Elaine";
    person.sex = @"F";
    [self ivarOperation:person];
}
///成员变量
- (void)ivarOperation:(id)obj {
    NSLog(@"%s的实例变量操作:",object_getClassName(obj));
    Class cls = object_getClass(obj);
    unsigned int outCount = 0;
    /// 获取成员变量列表
    Ivar *ivars = class_copyIvarList(cls, &outCount);
    for (int i = 0; i < outCount; i++) {
        Ivar ivar = ivars[i];
        /// 获取变量名
        const char *name = ivar_getName(ivar);
        /// 获取变量值,每次迭代到非objective-c对象的时候,如基本数据类型,BOOL、int、float就会报错
        const char *type = ivar_getTypeEncoding(ivar);
        NSString *stringType =  [NSString stringWithCString:type encoding:NSUTF8StringEncoding];
        if (![stringType hasPrefix:@"@"]) {
            continue;
        }
        id value = object_getIvar(obj, ivar);
        NSLog(@"%s = %@",name, value);
        /// 修改
        NSString *key = [NSString stringWithUTF8String:name];
        if ([key isEqualToString:@"_address"]) {
            object_setIvar(obj, ivar, @"山东济南");
        }
    }
    free(ivars);
}
/// 输出
2018-06-02 13:40:20.905438+0800 RuntimeDemo[4666:839773] Person的实例变量操作:
2018-06-02 13:40:20.905609+0800 RuntimeDemo[4666:839773] _address = 山东聊城冠县
2018-06-02 13:40:20.905752+0800 RuntimeDemo[4666:839773] 修改之后_address = 山东济南
2018-06-02 13:40:20.905865+0800 RuntimeDemo[4666:839773] _idNo = 3715251993098767567
2018-06-02 13:40:20.905964+0800 RuntimeDemo[4666:839773] _name = Elaine
2018-06-02 13:40:20.906053+0800 RuntimeDemo[4666:839773] _sex = F

代码运行之后发现:runtime成员变量列表里面包含属性字段,这是因为@property会自动生成实例变量,没什么疑惑了吧,咱们继续!!

1.2 property相关操作
//替换类中的属性
  void class_replaceProperty(Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount)
  //获取类中的属性
  objc_property_t class_getProperty(Class cls, const char *name)
  //拷贝类中的属性列表
  objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
  //获取属性名称
  const char *property_getName(objc_property_t property)
  //获取属性的特性
  const char *property_getAttributes(objc_property_t property) 
  //拷贝属性的特性列表
  objc_property_attribute_t *property_copyAttributeList(objc_property_t property, unsigned int *outCount)
  //拷贝属性的特性的值
  char *property_copyAttributeValue(objc_property_t property, const char *attributeName)

同样用代码试试

- (void)test {
    Person *person = [[Person alloc] initWithID:@"3715251993098767567" address:@"山东聊城冠县"];
    person.name = @"Elaine";
    person.sex = @"F";
    [self propertyOperation:person];
}
///属性操作
- (void)propertyOperation:(id)obj {
    NSLog(@"%s的属性操作:",object_getClassName(obj));
    Class cls = object_getClass(obj);
    unsigned int outCount = 0;
    ///获取属性列表
    objc_property_t * properties = class_copyPropertyList(cls, &outCount);
    for (int i = 0; i < outCount; i++) {
        objc_property_t property = properties[i];
        /// 通过property_getName函数获得属性的名字
        const char* name = property_getName(property);
        /// 获取属性的特性
        const char* attribute = property_getAttributes(property);
        NSLog(@"%s的attribute = %s",name, attribute);
    }
    free(properties);
}
/// 输出
2018-06-02 16:03:51.565179+0800 RuntimeDemo[6210:1173876] Person的属性操作:
2018-06-02 16:03:51.565328+0800 RuntimeDemo[6210:1173876] name的attribute = T@"NSString",C,N,V_name
2018-06-02 16:03:51.565429+0800 RuntimeDemo[6210:1173876] sex的attribute = T@"NSString",C,N,V_sex
2018-06-02 16:03:51.565548+0800 RuntimeDemo[6210:1173876] age的attribute = Ti,V_age

上面输出我们可以看出,name 自然是这个属性的名称了,但是 attribute: T@"NSString",C,N,V_sex 这串字符串又是什么呢?
还是官方文档来的靠谱:Objective-C Runtime Programming Guide Property Type and Functions

属性类型  name值:T  value:变化  //T 后面是放的是该属性的数据类型
编码类型  name值:C(copy) &(strong) W(weak) 空(assign) 等 value:无
非/原子性 name值:空(atomic) N(Nonatomic)  value:无
变量名称  name值:V  value:变化  // V 后面放的是该属性的变量名称(@property 提供了 getter 和 setter 方法,并创建一个以下划线开头的变量)

2. 关联对象

参考链接:
ios动态添加属性的几种方法
Objective-C Associated Objects 的实现原理

We all know that : Category不能添加成员变量,可以通过关联对象添加属性

2.1 Category为什么不能添加成员变量?

在Runtime中,objc_class结构体大小是固定的,不可能往这个结构体中添加数据,只能修改。所以ivars指向的是一个固定区域,只能修改成员变量值,不能增加成员变量个数。methodList是一个二维数组,所以可以修改*methodLists的值来增加成员方法,虽没办法扩展methodLists指向的内存区域,却可以改变这个内存区域的值(存储的是指针)。因此,可以动态添加方法,不能添加成员变量。

2.2 Category为什么可以添加属性?

我们都知道runtime里面可以通过关联对象添加属性,比如我们可以给UIView添加一个属性name:

@interface UIView (Runtime)
@property (nonatomic, copy)NSString *name;
@end

@implementation UIView (Runtime)
- (void)setName:(NSString *)name
{
    objc_setAssociatedObject(self, "NAME", name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name
{
    return objc_getAssociatedObject(self, "NAME");
}
@end

/// 使用
UIView *view1 = [[UIView alloc] init];
view1.name = @"topView";
[self.view addSubview:view1];

为什么不能添加变量,而是可以添加属性呢?
我的理解:property=ivar+get+set
Category添加的属性,不会自动生成实例变量,这里添加的属性其实是添加的getter与setter方法。
category是运行时决定的,类实例在编译器已经决定了它的内存结构,所以运行时不能改变内存结构,因此,category不能添加实例变量,那么通过关联对象添加属性不应该破坏实例的内存结构,也就是说添加的属性所占的内存跟类的实例应该是没关系的。
到底是不是这样呢?看下源代码

/// set关联对象
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
    // retain the new value (if any) outside the lock.
    ObjcAssociation old_association(0, nil);
    /// 根据传入的value获取new_valu
    id new_value = value ? acquireValue(value, policy) : nil;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        disguised_ptr_t disguised_object = DISGUISE(object);
        if (new_value) {
            // 如果new_value存在,根据传入的对象object获取对应的ObjectAssociationMap对象
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i != associations.end()) {
                // 如果ObjectAssociationMap存在,根据传入的key获取对应的关联对象ObjectAssociationMap
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    // 如果旧的关联对象存在,存入新的关联对象,释放旧的关联对象
                    old_association = j->second;
                    j->second = ObjcAssociation(policy, new_value);
                } else {
                    //旧的关联对象不存在,直接存入新的关联对象
                    (*refs)[key] = ObjcAssociation(policy, new_value);
                }
            } else {
                // 如果ObjectAssociationMap不存在,为该对象创建一个新的ObjectAssociationMap
                ObjectAssociationMap *refs = new ObjectAssociationMap;
                associations[disguised_object] = refs;
                (*refs)[key] = ObjcAssociation(policy, new_value);
                object->setHasAssociatedObjects();
            }
        } else {
            // setting the association to nil breaks the association.
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i !=  associations.end()) {
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    refs->erase(j);
                }
            }
        }
    }
    // release the old value (outside of the lock).
    if (old_association.hasValue()) ReleaseValue()(old_association);
}
image.png
/// 获取关联对象
id _object_get_associative_reference(id object, void *key) {
    id value = nil;
    uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        disguised_ptr_t disguised_object = DISGUISE(object);
        AssociationsHashMap::iterator i = associations.find(disguised_object);
        if (i != associations.end()) {
            ObjectAssociationMap *refs = i->second;
            ObjectAssociationMap::iterator j = refs->find(key);
            if (j != refs->end()) {
                ObjcAssociation &entry = j->second;
                value = entry.value();
                policy = entry.policy();
                if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) {
                    objc_retain(value);
                }
            }
        }
    }
    if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
        objc_autorelease(value);
    }
    return value;
}

get_associative_reference先根据对象地址在 AssociationsHashMap 中查找其对应的 ObjectAssociationMap 对象,如果能找到则进一步根据 key 在 ObjectAssociationMap 对象中查找这个 key 所对应的关联结构 ObjcAssociation ,如果能找到则返回 ObjcAssociation 对象的 value 值,否则返回 nil

看完源码,发现跟我们想的是一样的,新添加的属性并没有改变类对象的内存结构,它是通过关联对象存储在哈希表里面,类实例通过关键字在哈希表查找关联对象。

看到这里,再思考一下:Category为什么可以添加方法?
下一篇会讲一下runtime中的method,应该能从里面找到答案

参考资料:
https://blog.csdn.net/u012946824/article/details/51788565
https://www.cnblogs.com/LeeGof/p/6674949.html
https://www.jianshu.com/p/ead476cdb828
https://www.jianshu.com/p/cefa1da5e775
https://blog.csdn.net/shengyumojian/article/details/44919695
https://www.jianshu.com/p/6ebda3cd8052
http://www.cocoachina.com/ios/20170502/19163.html

上一篇下一篇

猜你喜欢

热点阅读