美文网首页
iOS底层--runtime应用之动态添加属性/成员变量

iOS底层--runtime应用之动态添加属性/成员变量

作者: Engandend | 来源:发表于2020-04-28 15:02 被阅读0次

    问题

    1、编译完成的类 能否对其添加变量/属性
    2、运行时创建的类,能否对其添加变量/属性

    备用的方法:

    //添加一个nonatomic,copy 修饰符的NSString
    void je_class_addProperty(Class targetClass , const char *propertyName){
        
        objc_property_attribute_t type = { "T", [[NSString stringWithFormat:@"@\"%@\"",NSStringFromClass([NSString class])] UTF8String] }; //type
        objc_property_attribute_t ownership0 = { "C", "" }; // C = copy
        objc_property_attribute_t ownership = { "N", "" }; //N = nonatomic
        objc_property_attribute_t backingivar  = { "V", [NSString stringWithFormat:@"_%@",[NSString stringWithCString:propertyName encoding:NSUTF8StringEncoding]].UTF8String };  //variable name
        objc_property_attribute_t attrs[] = {type, ownership0, ownership,backingivar};
    
        class_addProperty(targetClass, propertyName, attrs, 4); // 4:attrs元素的个数
    
    }
    
    // 打印属性
    void je_printerProperty(Class targetClass){
        unsigned int outCount, i;
        objc_property_t *properties = class_copyPropertyList(targetClass, &outCount);
        for (i = 0; i < outCount; i++) {
            objc_property_t property = properties[i];
            fprintf(stdout, "%s %s\n", property_getName(property), property_getAttributes(property));
        }
    }
    
    //  打印成员变量
    void je_printerIvar(Class targetClass){
        unsigned int count = 0;
        Ivar *ivars = class_copyIvarList(targetClass, &count);
        for (unsigned int i=0; i < count; i++) {
            Ivar const ivar = ivars[i];
            const char*cName = ivar_getName(ivar);
            NSString *ivarName = [NSString stringWithUTF8String:cName];
            NSLog(@"ivarName:%@",ivarName);
        }
        free(ivars);
        printf("ivar count = %u\n",count);
    }
    

    1、编译时

    1.1 编译时:添加成员变量

    不行
    先来看看类的结构,类的成员变量存储在bits-> rw -> ro 中,也就是说,编译完成的类,成员变量是readonly(只读)属性。只能读取,不能更改,
    所以不能动态去添加。
    换个角度来说,类在编译完成之后,其结构是一个结构体,系统已经完成了对其空间大小的分配,你如果这个时候再去添加,根本就没有地方给你了,怎么添加呢?

    我们来验证一下:

    创建一个类,并对其添加一个成员变量,并打印/赋值

    //创建一个JEPerson 类
    @interface JEPerson : NSObject
    @end
    
    //在main中 添加_jeName的成员变量  对jeName赋值并打印
    class_addIvar([JEPerson class], "_jeName", sizeof(NSString *), log2(sizeof(NSString *)), "@");  //添加一个_jeName的成员变量
    
    // 打印成员变量
    je_printerIvar([JEPerson class]);
    
    JEPerson *person = [JEPerson alloc];
    [person setValue:@"这是jeName的value" forKey:@"_jeName"]; //因为是动态添加,所以无法用点语法,用kvc的方式赋值
    NSLog(@"%@",[person valueForKey:@"_jeName"]);            //读取_jeName 并打印出来
    
    // 打印结果
    ivar count = 0
    reason: '[<JEPerson 0x100736f60> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key _jeName.'
    

    从打印结果来看,说明这个 _jeName 成员变量没有添加成功,所以ivar的count = 0 并且对_jeName 进行kvc赋值的时候,报了UndefinedKey 的错误

    1.2 编译时:添加属性

    可以

    // 同样,在对一个已创建的类JEPerson进行操作
    
    // 添加jeName 属性并打印
    je_class_addProperty([JEPerson class], "jeName");  //添加属性
    je_printerProperty([JEPerson class]);        //打印属性
    je_printerIvar([JEPerson class]);      // 打印成员变量
    
    // 打印结果
    jeName T@"NSString",C,N,V_jeName    //属性添加成功
    ivar count = 0            //属性添加成功,但是没有自动添加相应的成员变量
    

    从打印结果看,属性添加成功,只是没有自动添加一个待下划线的成员变量

    通过控制台lldb来打印看看data()里面究竟有什么内容

    (lldb) p [JEPerson class]
    (Class) $0 = JEPerson
    (lldb) x/5gx $0
    0x100003210: 0x0000000100003238 0x0000000100333140
    0x100003220: 0x000000010032d490 0x0000801000000000
    0x100003230: 0x000000010195a784
    
    //强转bits
    (lldb) p (class_data_bits_t *)0x100003230
    (class_data_bits_t *) $1 = 0x0000000100003230
    
    (lldb) p $1->data()
    (class_rw_t *) $2 = 0x000000010195a780
     
    (lldb) p (class_ro_t *)$2->ro
    (class_ro_t *) $3 = 0x00000001000030a0
     
    //查看ro内容
    (lldb) p *$3
    (class_ro_t) $4 = {
      flags = 128
      instanceStart = 8
      instanceSize = 8
      reserved = 0
      ivarLayout = 0x0000000000000000
      name = 0x0000000100001f5b "JEPerson"
      baseMethodList = 0x00000001000030e8
      baseProtocols = 0x0000000000000000
      ivars = 0x0000000000000000
      weakIvarLayout = 0x0000000000000000
      baseProperties = 0x0000000000000000
      _swiftMetadataInitializer_NEVER_USE = {}
    }
    
    // 查看rw内容
    (lldb) p *$2
    (class_rw_t) $6 = {
      flags = 2148007936
      version = 0
      witness = 1
      ro = 0x00000001000030a0
      methods = {
        list_array_tt<method_t, method_list_t> = {
           = {
            list = 0x00000001000030e8
            arrayAndFlag = 4294979816
          }
        }
      }
      properties = {
        list_array_tt<property_t, property_list_t> = {
           = {
            list = 0x000000010195a7c0
            arrayAndFlag = 4321552320
          }
        }
      }
      protocols = {
        list_array_tt<unsigned long, protocol_list_t> = {
           = {
            list = 0x0000000000000000
            arrayAndFlag = 0
          }
        }
      }
      firstSubclass = nil
      nextSiblingClass = NSUUID
      demangledName = 0x0000000000000000
    }
    

    从控制台的打印开看,动态添加属性之后,在rw->properties中有内容,而rw ->ro中的ivars 、baseProperties都没有内容

    动态添加的属性是可行的,存储在rw中

    2、运行时

    2.1 运行时: 添加成员变量

    可行
    我们来验证一下:

    动态创建一个类,并对其添加一个成员变量,并打印/赋值

    Class student = objc_allocateClassPair([NSObject class], "JEStudent", 0);
    // 2: 添加成员变量 1<<aligment
    // ivar - ro - ivarlist
    class_addIvar(student, "jeName", sizeof(NSString *), log2(sizeof(NSString *)), "@");
    // 3: 注册到内存
    objc_registerClassPair(student);
    
    id person = [student alloc];
    [person setValue:@"student value" forKey:@"jeName"];
    NSLog(@"%@",[person valueForKey:@"jeName"]);
    
    //打印结果
    student value
    

    从以上代码来看,可以动态创建类并动态添加成员变量
    ️请注意
    动态添加成员变量必须在类注册到内存之前(objc_registerClassPair 方法之前),原理和编译时不能添加成员变量是一样的

    我们通过lldb打印的方式来看看具体rw、ro的结构

    // ro的内容
    (lldb) p *$3
    (const class_ro_t) $4 = {
      flags = 0
      instanceStart = 8
      instanceSize = 16
      reserved = 0
      ivarLayout = 0x000000010032d5a8 ""
      name = 0x0000000100001f20 "JEStudent"
      baseMethodList = 0x0000000000000000
      baseProtocols = 0x0000000000000000
      ivars = 0x0000000100634fa0
      weakIvarLayout = 0x000000010032d5a8 ""
      baseProperties = 0x0000000000000000
      _swiftMetadataInitializer_NEVER_USE = {}
    }
    
    // ro .ivars 的内容
    (lldb) p $4.ivars
    (const ivar_list_t *const) $5 = 0x0000000100634fa0
    (lldb) p *$5
    (const ivar_list_t) $6 = {
      entsize_list_tt<ivar_t, ivar_list_t, 0> = {
        entsizeAndFlags = 32
        count = 1                                //一个成员变量
        first = {
          offset = 0x000000010062a780
          name = 0x0000000100001f2a "jeName"    //成员变量为jeName
          type = 0x0000000100001f31 "@"
          alignment_raw = 3
          size = 8
        }
      }
    }
    
    //rw的内容
    (lldb) p *$2
    (class_rw_t) $7 = {                
      flags = 2315255808
      version = 0
      witness = 0
      ro = 0x0000000100634f00
      methods = {
        list_array_tt<method_t, method_list_t> = {
           = {
            list = 0x0000000000000000
            arrayAndFlag = 0
          }
        }
      }
      properties = {
        list_array_tt<property_t, property_list_t> = {
           = {
            list = 0x0000000000000000
            arrayAndFlag = 0
          }
        }
      }
      protocols = {
        list_array_tt<unsigned long, protocol_list_t> = {
           = {
            list = 0x0000000000000000
            arrayAndFlag = 0
          }
        }
      }
      firstSubclass = nil
      nextSiblingClass = NSUUID
      demangledName = 0x0000000000000000
    }
    

    通过控制台 ,在ro里面打印出来了成员变量 jeName

    2.2 运行时:添加属性

    void je_Setter(NSString *value){
        printf("%s/n",__func__);
    }
    
    NSString *je_Name(){
        printf("%s/n",__func__);
        return @"je_Name value";
    }
    
    //在main.h中
    
    // 1、创建类
    Class student = objc_allocateClassPair([NSObject class], "JEStudent", 0);
    // 2、注册到内存
    objc_registerClassPair(student);
    // 3.1 添加property
    je_class_addProperty(student, "subject");
    // 3.2 打印属性
    je_printerProperty(student);
    
    // 3.3 添加setter  +  getter 方法
    class_addMethod(student, @selector(setSubject:), (IMP)je_Setter, "v@:@");
    class_addMethod(student, @selector(subject), (IMP)je_Name, "@@:");
    
    id stu = [student alloc];
    [stu setValue:@"student value" forKey:@"subject"];
    NSLog(@"%@",[stu valueForKey:@"subject"]);
    
    //objc_registerClassPair(student);   这行代码也可以写在这里
    
    // 打印结果
    subject T@"NSString",C,N,V_subject
    je_Name value
    

    从打印结果看,可以动态创建类并动态添加属性
    请注意️
    添加的属性 必须要实现其set、get方法才能正常进行赋值,因为动态添加的属性,是在运行时,编译器没有为其自动生成set、get、成员变量


    objc_registerClassPair(student);可以写在最后面,也就是说,动态创建属性,可以在注册类之前,也可以在注册类之后

    源码解释

    为何无法在注册类之后、已编译的类 动态添加属性?
    class_addIvar()的源码

    BOOL 
    class_addIvar(Class cls, const char *name, size_t size, 
                  uint8_t alignment, const char *type)
    {
    //.........
    
        // Can only add ivars to in-construction classes.
    //只能在构造类中  添加属性
        if (!(cls->data()->flags & RW_CONSTRUCTING)) {
            return NO;
        }
    // .......
        return YES;
    }
    

    objc_registerClassPair源码

    void objc_registerClassPair(Class cls)
    {
       //.....
    
    //  将cls和cls->isa  设置成  RW_CONSTRUCTING  | RW_REALIZING
        // Clear "under construction" bit, set "done constructing" bit
        cls->ISA()->changeInfo(RW_CONSTRUCTED, RW_CONSTRUCTING | RW_REALIZING);
        cls->changeInfo(RW_CONSTRUCTED, RW_CONSTRUCTING | RW_REALIZING);
    
        // Add to named class table.
        addNamedClass(cls, cls->data()->ro->name);
    }
    

    class_addIvar中 如果是RW_CONSTRUCTING 就不能添加,而在objc_registerClassPair中 将 cls 、cls->isa 设置为RW_CONSTRUCTING 所以在注册类之后就无法添加成员变量

    相关文章

      网友评论

          本文标题:iOS底层--runtime应用之动态添加属性/成员变量

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