美文网首页
包罗万象的runtime(二):变量&属性&关联

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

作者: ElaineYin | 来源:发表于2018-06-03 03:34 被阅读4次

    成员变量用于类内部,无需与外界接触的变量。根据成员变量的私有性,为了方便访问,所以就有了属性变量。属性变量的好处就是允许让其他对象访问到该变量(因为属性创建过程中自动产生了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

    相关文章

      网友评论

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

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