美文网首页
探索通过runtime能否给类添加成员变量

探索通过runtime能否给类添加成员变量

作者: Hamiltion | 来源:发表于2020-11-05 19:36 被阅读0次

    先看看添加成员变量的API

    class_addIvar(Class cls,const char *name, size_t size,
    uint8_t alignment, const char *type)

    • 第一个参数 Class cls: 需要添加成员变量的类
    • 第二个参数 const char *name:要添加的成员变量名称
    • 第三个参数 size_t size:成员变量占用内存大小
    • 第四个参数 uint8_t alignment: 这个没做过多研究,大概是跟内存对齐有关的
    • 第五个参数 const char *type:表示要添加的成员变量类型,数据类型在底层的表示符号,可以用@encode()获取,比如 int 用 i 表示

    测试是否能给类添加成员变量

    • 比如我们写一个Person类继承自NSObject,测试代码如下:
        Class cls = [Person class];
        BOOL isSuccess = class_addIvar(cls, "_age", sizeof(int), 0, @encode(int));
        NSLog(@"%@", isSuccess ? @"添加成员变量成功" : @"添加成员变量失败");
    

    打印结果:

       2020-11-05 17:16:00.752547+0800 ZZTest[72797:9439487] 添加成员变量失败
    

    经测试添加成员变量失败了

    • 如果使用runtime创建一个类,能添加成员变量吗,测试代码如下:
        Class cls = objc_allocateClassPair([NSObject class], "TestClass", 8);
        BOOL isSuccess = class_addIvar(cls, "_test_ivar", sizeof(int), 0, @encode(int));
        NSLog(@"%@", isSuccess ? @"添加成员变量成功" : @"添加成员变量失败");
    

    打印结果如下:

        2020-11-05 17:20:56.746436+0800 ZZTest[72917:9443074] 添加成员变量成功
    

    经测试runtime创建的类可以添加成员变量

    为什么runtime创建的类可以添加成员变量,常规的类不能添加成员变量?

    • 要想解开谜底,当然需要看添加成员变量这个方法的源码,我从源码中找出了关键代码,如下:
        #define RW_CONSTRUCTING       (1<<26)
        // Can only add ivars to in-construction classes.
        if (!(cls->data()->flags & RW_CONSTRUCTING)) {
            return NO;
        }
    

    从注释“Can only add ivars to in-construction classes”可以看出,只有当类构建过程中才能添加成员变量,显然我们常规的类,在APP启动的时候就已经构建完成了,所以不能添加成员变量,而通过objc_allocateClassPair方法创建的类,如果不去修改是否在构建过程的标识,始终处于构建过程中。
    从源码中就可以看出 cls->data()->flags 中存储着是否是在构建中的标记, cls->data()->flags & RW_CONSTRUCTING 表示是否是在构建中,RW_CONSTRUCTING 等于 1左移26位,也就是说cls->data()->flags从右到左第27位表示类是否在构建过程中(1表示在构建过程中,0表示不在构建过程中)

    • 验证cls->data()->flags从右到左第27位是否表示处于构建过程中的标记,把常规的类的flags构建标志位改掉,看是否可以添加成员变量,先看下flags的值,及第27位的值,常规方式我们无法获取到flags,我们需要模拟Class的底层结构,把class转成我们自己写的结构体,才能获取到flags,我们先导入写好的Class底层结构关键代码,这个代码出自李明杰,代码如下:
    // MJClassInfo.h文件内容
    
    #import <Foundation/Foundation.h>
    #ifndef MJClassInfo_h
    #define MJClassInfo_h
    
    # if __arm64__
    #   define ISA_MASK        0x0000000ffffffff8ULL
    # elif __x86_64__
    #   define ISA_MASK        0x00007ffffffffff8ULL
    # endif
    
    #if __LP64__
    typedef uint32_t mask_t;
    #else
    typedef uint16_t mask_t;
    #endif
    typedef uintptr_t cache_key_t;
    
    struct bucket_t {
        cache_key_t _key;
        IMP _imp;
    };
    
    struct cache_t {
        bucket_t *_buckets;
        mask_t _mask;
        mask_t _occupied;
    };
    
    struct entsize_list_tt {
        uint32_t entsizeAndFlags;
        uint32_t count;
    };
    
    struct method_t {
        SEL name;
        const char *types;
        IMP imp;
    };
    
    struct method_list_t : entsize_list_tt {
        method_t first;
    };
    
    struct ivar_t {
        int32_t *offset;
        const char *name;
        const char *type;
        uint32_t alignment_raw;
        uint32_t size;
    };
    
    struct ivar_list_t : entsize_list_tt {
        ivar_t first;
    };
    
    struct property_t {
        const char *name;
        const char *attributes;
    };
    
    struct property_list_t : entsize_list_tt {
        property_t first;
    };
    
    struct chained_property_list {
        chained_property_list *next;
        uint32_t count;
        property_t list[0];
    };
    
    typedef uintptr_t protocol_ref_t;
    struct protocol_list_t {
        uintptr_t count;
        protocol_ref_t list[0];
    };
    
    struct class_ro_t {
        uint32_t flags;
        uint32_t instanceStart;
        uint32_t instanceSize;  // instance对象占用的内存空间
    #ifdef __LP64__
        uint32_t reserved;
    #endif
        const uint8_t * ivarLayout;
        const char * name;  // 类名
        method_list_t * baseMethodList;
        protocol_list_t * baseProtocols;
        const ivar_list_t * ivars;  // 成员变量列表
        const uint8_t * weakIvarLayout;
        property_list_t *baseProperties;
    };
    
    struct class_rw_t {
        uint32_t flags;
        uint32_t version;
        const class_ro_t *ro;
        method_list_t * methods;    // 方法列表
        property_list_t *properties;    // 属性列表
        const protocol_list_t * protocols;  // 协议列表
        Class firstSubclass;
        Class nextSiblingClass;
        char *demangledName;
    };
    
    #define FAST_DATA_MASK          0x00007ffffffffff8UL
    struct class_data_bits_t {
        uintptr_t bits;
    public:
        class_rw_t* data() {
            return (class_rw_t *)(bits & FAST_DATA_MASK);
        }
    };
    
    /* OC对象 */
    struct mj_objc_object {
        void *isa;
    };
    
    /* 类对象 */
    struct mj_objc_class : mj_objc_object {
        Class superclass;
        cache_t cache;
        class_data_bits_t bits;
    public:
        class_rw_t* data() {
            return bits.data();
        }
        
        mj_objc_class* metaClass() {
            return (mj_objc_class *)((long long)isa & ISA_MASK);
        }
    };
    
    #endif /* MJClassInfo_h */
    
    

    在测试的代码里面导入MJClassInfo.h,并将.m文件改成.mm文件,打印flags的代码如下:

        Class cls = [Person class];
        NSLog(@"Person 的 flags = %p", ((__bridge struct mj_objc_class *)cls)->data()->flags);
        
        Class cls1 = [UIViewController class];
        NSLog(@"UIViewController 的 flags = %p", ((__bridge struct mj_objc_class *)cls1)->data()->flags);
    
        Class cls2 = objc_allocateClassPair([NSObject class], "TestClass", 8);
        NSLog(@"runtime创建的TestClass 的 flags = %p", ((__bridge struct mj_objc_class *)cls2)->data()->flags);
        
        Class cls3 = objc_allocateClassPair([NSObject class], "TestClass1", 8);
        NSLog(@"runtime创建的TestClass1 的 flags = %p", ((__bridge struct mj_objc_class *)cls3)->data()->flags);
    

    带引结果如下:

    2020-11-05 18:23:40.756127+0800 ZZTest[74302:9484306] Person 的 flags = 0x80080000
    2020-11-05 18:23:40.756291+0800 ZZTest[74302:9484306] UIViewController 的 flags = 0x80080000
    2020-11-05 18:23:40.756411+0800 ZZTest[74302:9484306] runtime创建的TestClass 的 flags = 0x8c080000
    2020-11-05 18:23:40.756524+0800 ZZTest[74302:9484306] runtime创建的TestClass1 的 flags = 0x8c080000
    

    从打印结果可以看出:
    常规的类Person、UIViewController的flags = 0x80080000,第27位是0
    runtime创建的类TestClass、TestClass1的flags = 0x8c080000,第27位是1
    两种类flags的第27位是不一样的

    如果我们把runtime创建的类的flags的27位由1改成0,看是否可以添加成员变量,代码如下:

        Class cls = objc_allocateClassPair([NSObject class], "TestClass", 8);
        NSLog(@"改之前的 flags = %p", ((__bridge struct mj_objc_class *)cls)->data()->flags);
        BOOL isSuccess = class_addIvar(cls, "_test_ivar", sizeof(int), 0, @encode(int));
        NSLog(@"修改flags前,%@", isSuccess ? @"添加成员变量成功" : @"添加成员变量失败");
        ((__bridge struct mj_objc_class *)cls)->data()->flags = 0x88080000;
        NSLog(@"改之前的 flags = %p", ((__bridge struct mj_objc_class *)cls)->data()->flags);
        BOOL isSuccess1 = class_addIvar(cls, "_test_ivar_1", sizeof(int), 0, @encode(int));
        NSLog(@"修改flags后,%@", isSuccess1 ? @"添加成员变量成功" : @"添加成员变量失败");
    

    打印结果如下:

    2020-11-05 18:40:26.430563+0800 ZZTest[74710:9495999] 改之前的 flags = 0x8c080000
    2020-11-05 18:40:26.430715+0800 ZZTest[74710:9495999] 修改flags前,添加成员变量成功
    2020-11-05 18:40:26.430841+0800 ZZTest[74710:9495999] 改之前的 flags = 0x88080000
    2020-11-05 18:40:26.430955+0800 ZZTest[74710:9495999] 修改flags后,添加成员变量失败
    

    从打印结果可以看出,runtime创建的类在修改flags的27前是可以添加成员变量的,当把27位由1改成0后(从RW_CONSTRUCTING的意思和前面那段注释来看,大概就是从类构建中改成了类构建完成),就不能添加成员变量了

    结论

    从以上分析,充分证实了,类能不能在运行时添加成员变量,class内部中的flags从右到左第27位(也就是calss是否在构建过程中)是很关键的因素(至于是不是绝对因素还有待进一步分析)
    在不修改class内部成员变量的情况下,常规的类不能添加成员变量,runtime创建的类可以添加成员变量

    相关文章

      网友评论

          本文标题:探索通过runtime能否给类添加成员变量

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