iOS底层探索六(类的分析上)

   前言


    相关文章:   
       iOS底层探索一(底层探索方法)       

       iOS底层探索二(OC 中 alloc 方法 初探)

       iOS底层探索三(内存对齐与calloc分析)  

iOS底层探索四(isa初探-联合体,位域,内存优化)     

       iOS底层探索五(isa与类的关系)  

iOS底层探索七(类的分析下)

iOS底层探索八(方法本质上)

iOS底层探索九(方法的本质下objc_msgSend慢速及方法转发初探)

iOS底层探索十(方法的本质下-消息转发流程)

    相关代码:
      objc4_752源码 

      前几篇文章对alloc方法和isa进行了初步探究,了解了类是怎么进过创建的,这篇文章我们对类进行初步分析,类中都有什么,我们创建的成员变量,方法等,类是怎样进行存储的:

上篇文章我们描述了使用clangmain.m文件进行编译,实现了看到底层实现逻辑,这篇我们直接使用objc4_752源码方式来看类结构的底层实现:

 类的结构

要查看类的结构,首先我们先声明类

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface XZPerson : NSObject
@end

NS_ASSUME_NONNULL_END

我们在main函数中添加


int main(int argc, const char * argv[]) {
    @autoreleasepool {

        XZPerson *p1 = [XZPerson alloc];
        Class pClass = object_getClass(p1);
     
        NSLog(@"%@ --- %@",p1,pClass);
        
    }
    return 0;
}

进入Class类进行查看:
typedef struct objc_class *Class;

发现类Class对象在底层只是转换为objc_class结构体,继续深入查看objc_class:

struct objc_class : objc_object {
//    Class ISA;    //8字节
    Class superclass;  //父类  8字节
    cache_t cache; //缓存   结构体所占大小需要看内部定义16字节16字节
    // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

    class_rw_t *data() { 
        return bits.data();
    }
    void setData(class_rw_t *newData) {
        bits.setData(newData);
    }
...
}

可以看到继承objc_object类:继续查看objc_object

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */

这里我们发现了里面必定会有isa这个属性,这也就是为什么我们在objc_class 类中第一个属性为什么是isa,而且还被注释掉了,因为isa是从父类继承的,它一定会有这个属性,后面苹果开发人员还亲切的注释了Use `Class` instead of `struct objc_class ,让我们使用Class ,而不要使用 objc_class;

这就是我们声明的类再底层的大致结构,我们看到了这么多东西,但是没有看到我们在意的,我们最在意的是,我们开发人员申明的成员变量,属性,代理,方法都存在了哪里,我们慢慢进行分析

我们继续查看结构体objc_class:中的属性进行分析,isa存储的是isa,superclass存储的是父类,cache存储的是父类,还有一个就是bits了,那就我们要存的东西只能是在bits里面了,刚好下面有个方法就是获取bits中的data,进入查看一下
 class_rw_t *data() { 
        return bits.data();
    }

进入class_rw_t中进行查看:

struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;

    method_array_t methods;  //方法
    property_array_t properties; // 属性
    protocol_array_t protocols; // 代理

    Class firstSubclass;
    Class nextSiblingClass;

    char *demangledName;

#if SUPPORT_INDEXED_ISA
    uint32_t index;
#endif

。。。
}

我们就看到了 methods,properties,protocols等属性,不出意外的话,应该就是这几个东西了,找到了这几个东西,那我们怎么使用代码拿出,或者怎么样在内存中看到这些确实的存储呢,继续分析。

类中属性与成员变量存储位置探索

首先我们需要在XZPerson类中定义好属性,成员变量,方法等

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface XZPerson : NSObject
{
    NSString *hobby;
}
@property (nonatomic, copy) NSString *nickName;

- (void)sayHello;
+ (void)sayHappy;


@end

NS_ASSUME_NONNULL_END


#import "XZPerson.h"

@implementation XZPerson


- (void)sayHello{
    NSLog(@"XZPerson say : Hello!!!");
}

+ (void)sayHappy{
    NSLog(@"XZPerson say : Happy!!!");
}

@end

这里我们需要先进行分析,在内存中我们应该怎样取到bits,这里我们就要对内存进行分析了,这里给大家找了个字节图片

首先拿出objc_class结构体

其中4个属性第一个Class isa  属性,指针(pointer)类型8字节,  Class superclass 指针类型8字节,cache_t cache这个是结构体,需要进入内部看

struct cache_t {
    struct bucket_t *_buckets; // 结构体指针8字节
    mask_t _mask;  //typedef uint32_t mask_t;  4字节
    mask_t _occupied; // 4字节

public:
//    向下为函数,函数不占内存
    struct bucket_t *buckets();
    mask_t mask();
。。。
}
typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits

(这里需要注意的是struct bucket_t *_buckets; 这个是结构体指针,属于指针类型所以占8字节,mask_t _mask;  内部为int 在64位中占4字节,mask_t _occupied也是站4字节)

综合的出cache_t cache这个结构体就是站16字节,class_data_bits_t bits结构体类型进入查看

struct class_data_bits_t {

    // Values are the FAST_ flags above.
    uintptr_t bits; //8字节
private:
    bool getBit(uintptr_t bit)
    {
        return bits & bit;
    }
...
}

typedef unsigned long           uintptr_t;

可以看出里面只有一个属性,所以占8字节

综上所述这个时候我们查看类内存就不能值查看4段了,

我们需要使用x/6gx pClass 打印出6段地址 ,

根据上图操作我们获取到了bits同时操作得到其中的class_rw_t;我们主要是找到属性,和方法存在哪里,现在我们已经可以查看到class_rw_t,继续查看属性properties:

 
首先这里建议我们使用 $3->properties进行访问,使用 $3.properties进行修复了;其次我们看到了 properties属性返回的是 property_array_t这种类型我们来查看一下这个类型

class property_array_t : 
    public list_array_tt<property_t, property_list_t> 
{
    typedef list_array_tt<property_t, property_list_t> Super;

 public:
    property_array_t duplicate() {
        return Super::duplicate<property_array_t>();
    }
};

它继承于list_array_tt,这里我们可以看到它是一个二维数组,继续查看

class list_array_tt {
    struct array_t {
        uint32_t count;
        List* lists[0];//存储数据使用

        static size_t byteSize(uint32_t count) {
            return sizeof(array_t) + count*sizeof(lists[0]);
        }
        size_t byteSize() {
            return byteSize(count);
        }
    };

 protected:
    class iterator {
        List **lists;
        List **listsEnd;
        typename List::iterator m, mEnd;

。。。
//数组方式遍历取值
 const iterator& operator ++ () {
            assert(m != mEnd);
            m++;
            if (m == mEnd) {
                assert(lists != listsEnd);
                lists++;
                if (lists != listsEnd) {
                    m = (*lists)->begin();
                    mEnd = (*lists)->end();
                }
            }
            return *this;
        }

}

我们可以看到,其中只有一个list,显然数据是在这里进行存储的,还有一些方法,有兴趣的可以进入进行自行查看,这里拿出一个方法为遍历取值方法;

我们接着上面的思路先取出list 查看

这个里面我们可以看到nickName 但是没有看到hobby,但是这里rw中其他属性应该也不会有存hobby的地方,我们查看上面的ro
这里只是一个地址段,我们进行查看ro这个地址中存储的东西到底都有什么东西,我们看下源码:
struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#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; //属性列表

    // This field exists only when RO_HAS_SWIFT_INITIALIZER is set.
    _objc_swiftMetadataInitializer __ptrauth_objc_method_list_imp _swiftMetadataInitializer_NEVER_USE[0];
...
}

看到这个结构体我们又重燃希望baseProperties;属性可能在这里面,我们继续查看

这里又提示我们使用 $4.ro 有点尴尬,其实这里我们需要做很多尝试的,也很容易报错,例如:p 对象,不能输出,可以尝试po ,使用箭头调用属性或函数不行就使用点语法调用,都可以尝试,多试试就好了,小编也经常报错!
我们继续输出$8查看
 
 
继续查看 baseProperties;
 
 
我们发现这里还是count是1 ,内部只保存了 nickName属性,我们的hobby字段还是没有找到,这里还有一个 ivars,我们来进行
查看
 
我去,终于找到了hobby属性存储地方了,我们可以看到count =2,说明这个里面还有一个值,继续查看
这样我们就清楚了,属性也会生成一个带下划线的成员变量;
 
我们可以得出属性与成员变量存储区别:

 成员变量存储地方ro -> ivar_list_t

 属性存储地方 ro ->baseProperties ;属性会生成一个带下划线的成员变量存储到ro->ivar_list_t;

  属性还会存储在class_rw_t->properties

类中实例方法存储位置探索

根据上面属性的分析我们可以看到ro中有baseMethodList,我们进行查看

 

我们可以看到sayHello这个实力方法,这里的count=3,说明有3个方法继续查看
我们可以看到,剩余的2个方法为,属性生成的get 和 set方法
这里我们可以看到方法中存储的method_t,我们进行查看一下这个在底层是什么结构
struct method_t {
    SEL name;  //方法名
    const char *types; //方法签名
    MethodListIMP imp; //方法的函数实现

    struct SortBySELAddress :
        public std::binary_function<const method_t&,
                                    const method_t&, bool>
    {
        bool operator() (const method_t& lhs,
                         const method_t& rhs)
        { return lhs.name < rhs.name; }
    };
};

name  :方法名

types:为方法签名 :@16@0:8

这个@16@0:8为方法签名

 @ 返回值 类型: id   16为偏移位置

 @ 参数一类型 : id  self 

 : 参数二类型 : sel  select

其实iOS中每个方法都会带2个隐式参数为(id self, SEL _cmd)

其中imp:为函数的真正实现,我们开发职工经常会使用的rutime交换方法,实际就是交换2个方法的imp实现,

select与imp之间的关系,就可以认为是一本书的目录,和章节的实际内容,我们可以根据目录找到具体章节的内容,所以我们可以根据select找到imp的具体实现,如果我们将两个方法的imp进行交换相当于就相当于菜谱的:第一页我们本身是红烧肉做法,直接换成了小葱拌豆腐做法,你用Select找到第一页,就找到小葱拌豆腐,不会找到红烧肉做法了。

回到刚才那个话题,我们在baseMethodList找到了nicknameset方法,get方法,包括sayhello方法,我们有想到了rw中有个methods进行查看

发现这里的数据和baseMethodList中数据一样,其实这里就是ro(read only,顾名思义,只读的,这里面是开发人员不能进行修改的)中数据的备份,class_rw_t (rw read write可读可写,其实我们经常在开发中添加动态添加分类,方法,属性等,其实是添加到这个里面的)。

但是我们的类方法呢,sayhappy方法没找到,既然是类方法,就应该存在类对象中嘛,我们就得先用isa找到类对象,然后进行查看

类中类方法存储探索

 既然是在类中,我们首先先拿出类对象进行查看

继续查看,我们先查看rw中的method中有没有sayhappy方法

可以看出rw中的Method是存在sayHappy方法的,继续查看ro中是否存在sayHappy方法

根据上图我们可以看出,在ro中也存在sayHappy方法。

根据以上分析,我们可以得出结论,在类方法(+号方法)存储在类对象中。

总结

根据以上我们可以的出结论,在Class中存在bits属性此属性中存储类中声明的实例方法,成员变量,属性代理等其中,

属性:属性声明后会在bits有class_rw_t属性内Method属性中生产set,get方法,properties生成变量,同事在ro中ivars生产带下划线变量(_nickName),baseMethodList属性中生产set,get方法;

成员变量:只在bits中class_rw_t内ro中ivars属性中有变量进行保存

实例方法(-方法):在bits中class_rw_t内的methods,和ro中baseMethodList都有进行保存

类方法(+方法):在类中isa指针指向的类对象的bits中class_rw_t内的methods,和ro中baseMethodList都有进行保存

rw中的内容其实是运行时在DYLD过程中从ro中拷贝过来的;(这里在分析DYLD过程中再进行详细分析)

以上便是我对OC中类的初步探究,如果有错误的地方还请指正,大家一起讨论,开发水平一般(文章中有错误后,我发现会第一时间对博文进行修改),还望大家体谅,欢迎大家点赞,关注我的CSDN,我会定期做一些技术分享!

写给自己:

学会不在意,约束好自己,把该做的事做好,把该走的路走好,保持善良,做到真诚,宽容待别人,严以律自己,其他一切随意就好,未完待续。。。

 

原创文章 88 获赞 21 访问量 2万+

猜你喜欢

转载自blog.csdn.net/ZhaiAlan/article/details/104768502