美文网首页
iOS-OC底层四:类的属性、方法和协议

iOS-OC底层四:类的属性、方法和协议

作者: 轰天裂天罗三炮 | 来源:发表于2022-05-30 01:19 被阅读0次

    一、对象、ISA、类、元类、根元类间的关系

    核心知识点:

    • 类声明对象,给对象分配多少内存是依据类,对象的ISA指向类
    • 对象在内存中第一个8字节储存的是ISA
    • ISA中的shiftClass段就是类,即类为对象的isa&mask
    • 类也是一种对象,objc_class继承自objc_object,类的第一个8字节也是ISA,所以万物皆对象,万物皆有isa
    • 类的ISA中的shiftClass为元类,即元类为类的isa&mask,类是由元类进行声明
    • NSObject类的ISA指向NSObject类的元类,NSObject类的元类的ISA指向它自己
    • 元类的ISA指向元类的元类,我们称NSObject类的元类为根元类
    1. 元类的创建和声明是编译器完成,元类用来存储类方法的相关信息,元类本身是没有名称的,由于与相关联,所以使用了同类名一样的名称
    2. 对象之间不存在继承关系,只有类和元类之间存在继承关系
    3. 类与元类在系统中,只存在一份,即类对象只有一份
    4. 无论类继承层次多深,类的元类的ISA指向的都是NSObject根元类,由对象开始查找ISA,第三次及以后绝对指向NSObject
    ISA指向和类继承关系图.png

    1.1 准备验证代码

    #import <Foundation/Foundation.h>
    #import <objc/runtime.h>
    
    @interface TESTPerson : NSObject
    @property (nonatomic, copy) NSString *testname;
    @end
    
    @implementation TESTPerson
    
    @end
    
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            
            TESTPerson *obj = [TESTPerson alloc];
            TESTPerson *obj1 = [TESTPerson alloc];
            NSLog(@"class:%@",[TESTPerson class]);
            NSLog(@"class:%@",[obj class]);
            NSLog(@"class:%@", object_getClass(obj));
            NSLog(@"\n");
            NSLog(@"class:%@",[[obj class] class]);
            NSLog(@"class:%@",[[[obj class] class] class]);
            NSLog(@"class:%@",[[[[obj class] class] class] class]);
            NSLog(@"\n");
            NSLog(@"class:%@", object_getClass(object_getClass(obj)));
            NSLog(@"class:%@", object_getClass(object_getClass(object_getClass(obj))));
            NSLog(@"class:%@", object_getClass(object_getClass(object_getClass(object_getClass(obj)))));
            NSLog(@"\n");
            
        }
        return 0;
    }
    

    1.2 获取类的内存分布的三种方式:

    1. 根据对象的实例化方法:[obj class]
    2. 根据类对象的类方法:[TESTPerson class]
    3. 根据runtime中的object_getClass(obj)

    为什么嵌套调用时,结果不一致?

    • class获取的永远是obj的类,因为传的是self
    • object_getClass会将obj->getIsa()当成下一次的obj,如果嵌套两层,相当于调用两次getIsa(),对应的代码obj->getIsa()->getIsa()

    三种方式的源码分析:

    - (Class)class {
        return object_getClass(self);
    }
    
    //第一步,走类方法class
    + (Class)class {
        return self;
    }
    
    //第二步,走objc_opt_class获取类信息
    Class
    objc_opt_class(id obj)
    {
    #if __OBJC2__
        if (slowpath(!obj)) return nil;
        Class cls = obj->getIsa();
        if (fastpath(!cls->hasCustomCore())) {
            return cls->isMetaClass() ? obj : cls;
        }
    #endif
        return ((Class(*)(id, SEL))objc_msgSend)(obj, @selector(class));//走这里
    }
    
    Class object_getClass(id obj)
    {
        if (obj) return obj->getIsa();
        else return Nil;
    }
    

    1.3 Class的本质是objc_class

    打开源码,在main方法里面输入Class *class。然后通过Class点进去,可以发现都是typedef struct objc_class *Class;,这说明Class(类)都是struct objc_class创建而来的。

    #if !OBJC_TYPES_DEFINED
    /// An opaque type that represents an Objective-C class.
    typedef struct objc_class *Class;
    

    可以看到objc_class是继承于objc_object的,而objc_class本身是没有isa的,isa继承自objc_object,说明万物皆对象,对象是由类创建的,类由元类创建。

    objec_class继承自object_object.png

    1.4 验证类信息只有一份

    验证类信息只有一份.png

    二、类结构分析

    2.1 重要:分析方法

    分析的原理:基于类在内存中占用的地址是连续的,我们只需要从类的定义中,算出我们想查找的信息在哪个位置。具体做法就是通过struct objc_class : objc_object {} ,在这个定义类的结构体中,计算各成员变量所占的字节数,然后通过类的首地址,做相应字节的偏移,就能得到我们想要的信息。

    再仔细看看objc_class的源码:

    struct objc_class : objc_object {
      objc_class(const objc_class&) = delete;
      objc_class(objc_class&&) = delete;
      void operator=(const objc_class&) = delete;
      void operator=(objc_class&&) = delete;
        // Class ISA;
        Class superclass;
        cache_t cache;             // formerly cache pointer and vtable
        class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
        
        //底下全是方法
        ......
    }
    

    C语言无法在结构体中定义函数,但是C++可以做到。C++中delete是删除地址空间,此处operator用作重载运算符。我们大概可以猜到前4句代码是析构函数功能,用作内存释放的函数。其他函数可以很确定绝对是函数的声明或者是内部函数实现。

    所以我要弄清以下问题:

    方法是否占用内存空间,会不会造成成员变量偏移?

    通过以下代码予以验证

    static声明的变量和方法不影响偏移位置.png

    同样的道理:计算struct cache_t所占用的内存大小:

    struct cache_t {
    #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
        explicit_atomic<struct bucket_t *> _buckets; // 是一个结构体指针类型,占8字节
        explicit_atomic<mask_t> _mask; //是mask_t 类型,而 mask_t 是 unsigned int 的别名,占4字节
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
        explicit_atomic<uintptr_t> _maskAndBuckets; //是指针,占8字节
        mask_t _mask_unused; //是mask_t 类型,而 mask_t 是 uint32_t 类型定义的别名,占4字节
        
    #if __LP64__
        uint16_t _flags;  //是uint16_t类型,uint16_t是 unsigned short 的别名,占 2个字节
    #endif
        uint16_t _occupied; //是uint16_t类型,uint16_t是 unsigned short 的别名,占 2个字节
    

    计算出cache类的内存大小 = 12 + 2 + 2 = 16字节。

    我们所需要查看的信息在class_data_bits_t bits;中,在bits前面有3个成员变量,isa,superclass,这两个都是Class类型,都占有8字节,cache占用16字节,那么bits的内存就是基于类的首地址偏移32字节。

    2.2 class_data_bits_t具体操作

    什么是class_data_bits_t?在源码中,全局搜索,共有三处。

    //第一处
    // class_data_bits_t is the class_t->data field (class_rw_t pointer plus flags)
    // The extra bits are optimized for the retain/release and alloc/dealloc paths.
    
    //第二处
    struct class_data_bits_t {
        friend objc_class;
    
        // Values are the FAST_ flags above.
        uintptr_t bits;
    
         // 代码过多,自动省略
        ...
    public:
    
        class_rw_t* data() const {
            return (class_rw_t *)(bits & FAST_DATA_MASK);
        }
        // 代码过多,自动省略
        ...
    };
    
    //第三处
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    

    struct class_rw_t{...} 中,有我们熟悉的methodspropertiesprotocols。所以,我们应该先获取bits中class_rw_t *类型的data,再访问data中的方法,属性和协议。

    相关文章

      网友评论

          本文标题:iOS-OC底层四:类的属性、方法和协议

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