iOS 底层探索04——iOS类的结构分析(上)

948 阅读11分钟

这是我参与更文挑战的第6天,活动详情查看: 更文挑战

引言

在iOS开发中几乎每一个对象都是的实例,今天我们就来分析一下在内存中是以什么样的结构存在的;

实例对象、Class、MetaClass

Class VS MetaClass的区别

  1. 首先查看以下调试代码
#import "GCWorker.h"
@interface GCPerson : NSObject

@end
@implementation GCPerson

@end
int main(int argc, const char * argv[]) {
    GCPerson *person = [[GCPerson alloc] init];
    NSLog(@"%@",person);
    return 0;
}
  1. 通过LLDB进行调试,查看isa的相关信息
(lldb) x/4gx person
0x10303da70: 0x001d800100008179 0x0000000000000000
0x10303da80: 0x72626956534e5b2d 0x74696c7053746e61
(lldb) p/x 0x001d800100008179 & 0x00007ffffffffff8
(long) $1 = 0x0000000100008178
(lldb) po 0x0000000100008178
GCPerson

(lldb) x/4gx 0x0000000100008178
0x100008178: 0x0000000100008150 0x00007fff90c33118
0x100008188: 0x00000001030498f0 0x000680100000000f
(lldb) p/x 0x0000000100008150 & 0x00007ffffffffff8
(long) $3 = 0x0000000100008150
(lldb) po 0x0000000100008150
GCPerson

(lldb) 
  1. 通过实例对象获得实例对象isa,通过isa & isa_mask获得 Class对象
  2. 通过Class对象获得Class对象的isa,通过isa & isa_mask获得另一个Class对象
  3. 步骤1和步骤2得到的地址分别是0x00000001000081500x0000000100008178,但都指向GCPerson,我们猜测,实例对象isa指向Class,Classisa指向的MetaClass
  4. 通过machoview 查看__DATA段中的__objc_classrefs,可以看到当前macho文件中的Class信息;查看__DATA段中的__objc_classlist,可以看到当前macho文件中的Class对象信息; 在symbol Tables中的*Symbols中可以看到ClassMetaclass信息如下图所示;最终验证我们的猜测是正确的;

001.jpg

ClassMetaClass的数量

  1. class,MetaClass会不会和实例对象一样可以无限开辟?经过以下代码验证实际上不是的,class和metaclass是编译期器生成的,在启动阶段的pre-main阶段通过dyld加载进内存的,只有一份;如下图所示

002.jpg

总结

  1. 实例对象isa指向Class,Classisa指向MetaClass
  2. 实例对象 可以无限开辟,ClassMetaClass只有一份;

isa走位 和superClass继承链

isa走位

  1. 运行以下代码
    GCPerson *person = [[GCPerson alloc] init];
    NSLog(@"%@",person);
    
    NSObject *obj = [[NSObject alloc] init];
    NSLog(@"%@",obj);
  1. 在LLDB中进行以下调试

自定义对象的isa走向 005.jpg

NSObject的isa走向 006.jpg

  1. 经过分析,自定义对象相关的isa的走位如下
  • 自定义实例对象isa指向 自定义Class
  • 自定义Classisa指向自定义MetaClass
  • 自定义MetaClassisa指向NSObjectMetaClass
  1. 经过分析NSObject相关的isa走位如下
  • NSObject实例对象isa指向NSObject的Class;
  • NSObjectclass指向NSObjectMetaClass
  • NSObjectMetaClass指向自身;
  1. 结果如图所示

003.png

superClass继承关系

  1. 执行以下调试代码
void testNSObject(void) {
    // NSObject实例对象
    NSObject *object1 = [NSObject alloc];
    // NSObject类
    Class class = object_getClass(object1);
    // NSObject元类
    Class metaClass = object_getClass(class);
    // NSObject根元类
    Class rootMetaClass = object_getClass(metaClass);
    // NSObject根根元类
    Class rootRootMetaClass = object_getClass(rootMetaClass);
    NSLog(@"\n%p 实例对象\n%p 类\n%p 元类\n%p 根元类\n%p 根根元类",object1,class,metaClass,rootMetaClass,rootRootMetaClass);
    
    // GCPerson元类
    Class pMetaClass = object_getClass(GCPerson.class);
    Class psuperClass = class_getSuperclass(pMetaClass);
    NSLog(@"%@ - %p",psuperClass,psuperClass);
    
    // GCTeacher -> GCPerson -> NSObject
    // 元类也有一条继承链
    Class tMetaClass = object_getClass(GCPerson.class);
    Class tsuperClass = class_getSuperclass(tMetaClass);
    NSLog(@"%@ - %p",tsuperClass,tsuperClass);
    
    // NSObject 根类特殊情况
    Class nsuperClass = class_getSuperclass(NSObject.class);
    NSLog(@"%@ - %p",nsuperClass,nsuperClass);
    // 根元类 -> NSObject
    Class rnsuperClass = class_getSuperclass(metaClass);
    NSLog(@"%@ - %p",rnsuperClass,rnsuperClass);
}
  1. 查看输出信息
0x102c04ae0 实例对象
0x7fff90c331180x7fff90c330f0 元类
0x7fff90c330f0 根元类
0x7fff90c330f0 根根元类
2021-06-19 23:41:10.223618+0800 isa走位Demo[33613:662699] NSObject - 0x7fff90c330f0
2021-06-19 23:41:10.223670+0800 isa走位Demo[33613:662699] NSObject - 0x7fff90c330f0
2021-06-19 23:41:10.223713+0800 isa走位Demo[33613:662699] (null) - 0x0
2021-06-19 23:41:10.223748+0800 isa走位Demo[33613:662699] NSObject - 0x7fff90c33118
  1. 经过分析Class对象的继承关系如下
  • GCTeacher ClassSuperClass指向GCPerson Class
  • GCPerson ClassSuperClass指向NSObject Class
  • NSObject ClassSuperClass指向nil
  1. 经过分析MetaClass对象的继承关系如下
  • GCTeacher MetaClassSuperClass指向GCPerson MetaClass
  • GCPerson MetaClassSuperClass指向NSObject MetaClass
  • NSObject MetaClassSuperClass指向NSObject Class
  1. 结果如图所示

004.png

isasuperClass结合

isasuperClass结合后,就是下面这幅经典图了,此图一出无须多言!

isa流程图.png

类结构分析

前置知识:内存地址偏移读取数据

我们都知道要读取内存中的数据必须找到数据对应的内存地址,对于不同类型的数据存储,查找存储数据的内存地址方式也是不同的,常见的数据地址查找有以下几种;

  1. 普通基础数据类型的指针指向的是数据的地址,数据直接存在地址上;

007.jpg

  1. 对象数据类型的指针指向的是对象在堆上的存储时的首地址,可以通过内存地址偏移来获取对象内部的数据;

调试代码如下所示

@interface GCPerson : NSObject
@property (nonatomic,copy) NSString *name;
@property (nonatomic,copy) NSString *address;
@end
@implementation GCPerson

@end

// OC 类 结构 首地址 - 平移一些大小 -> 内容
GCPerson *person1 = [[GCPerson alloc] init];
person1.name = @"person1";
person1.address = @"SuZhou";
NSLog(@"%@ -- %p",person1,&person1);

GCPerson对象在堆上的地址为0x0000000100426040,那么偏移8个字节即为第一个属性name的地址,偏移16个字节即为第二个属性address的地址;

验证代码如下所示

(lldb) p &person1 
(GCPerson **) $32 = 0x00007ffeefbff4b8 //对象在栈上的地址

(lldb) p person1
(GCPerson *) $33 = 0x0000000100426040 //对象在堆上的地址
(lldb) x/gx 0x0000000100426040
0x100426040: 0x001d800100008225
(lldb) po (Class)0x001d800100008225
GCPerson

(lldb) x/gx 0x0000000100426040+8 //偏移8个字节为第一个属性name的地址
0x100426048: 0x0000000100004070
(lldb) po (NSString *)0x0000000100004070
person1

(lldb) x/gx 0x0000000100426040+8+8   //偏移16个字节为第一个属性name的地址
0x100426050: 0x0000000100004090
(lldb) po (NSString *)0x0000000100004090
SuZhou
  1. 数组类型的指针指向的是数组在堆上的存储时的首个元素的地址,可以通过内存地址偏移来一次获取数组其他索引位置的数据;需要注意的是如果是数组指针类型直接+1即可,系统会根据数组内部的数据类型宽度作为本次相加的步长; 调试代码如下所示
// 数组指针
int c[4] = {1,2,3,4};
int *d   = c;//数组指针指向起始位置首个元素的地址
NSLog(@"%p - %p - %p",&c,&c[0],&c[1]);
NSLog(@"%p - %p - %p",d,d+1,d+2);

for (int i = 0; i<4; i++) {
    int value =  *(d+i);
    NSLog(@"%p",(d+i));//当前内存位置的地址值
    NSLog(@"%d",value);//当前内存位置的数据
}
NSLog(@"指针 - 内存偏移");

验证代码如下所示

2021-06-20 13:20:06.276616+0800 isa走位Demo[36200:835553] 0x7ffeefbff4c0 - 0x7ffeefbff4c0 - 0x7ffeefbff4c4
2021-06-20 13:20:06.277084+0800 isa走位Demo[36200:835553] 0x7ffeefbff4c0 - 0x7ffeefbff4c4 - 0x7ffeefbff4c8
2021-06-20 13:20:06.277164+0800 isa走位Demo[36200:835553] 0x7ffeefbff4c0
2021-06-20 13:20:06.277226+0800 isa走位Demo[36200:835553] 1
2021-06-20 13:20:06.277274+0800 isa走位Demo[36200:835553] 0x7ffeefbff4c4
2021-06-20 13:20:06.277319+0800 isa走位Demo[36200:835553] 2
2021-06-20 13:20:06.277363+0800 isa走位Demo[36200:835553] 0x7ffeefbff4c8
2021-06-20 13:20:06.277428+0800 isa走位Demo[36200:835553] 3
2021-06-20 13:20:06.277473+0800 isa走位Demo[36200:835553] 0x7ffeefbff4cc
2021-06-20 13:20:06.277516+0800 isa走位Demo[36200:835553] 4
2021-06-20 13:20:06.277621+0800 isa走位Demo[36200:835553] 指针 - 内存偏移

class对象的内存布局

  1. 为了了解class的内部存储信息,我们首先查看OBJC源码来确定class对象的成员变量等信息,我们已经知道isaClass类型的。Class类型是objc_class *objc_class是一个结构体,并且所有的Class底层实现都是objc_class
  2. 先搜索struct objc_class发现在runtime.hobjc-runtime-old.hobjc-runtime-new.h这三个类都有实现,因为现在使用的都是objc2,所以可以排除掉runtime.h中的实现,最终从objc-runtime-new.h可以找到结构体内部除了从父结构体objec_object继承来的isa之外还有superclass、cache_t、class_data_bits_t等成员变量;还有getclass、setclass等方法;
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 getSuperclass() const {
    }
    void setSuperclass(Class newSuperclass) {
    }
    ...
    ...
}
  1. objc源码还有一个__has_feature(ptrauth_calls)__has_feature()主要作用是判断当前编译器是否支持某个功能;ptrauth_calls是指身份验证针对arm64e架构;使用A12或更高版本的A系列处理器的设备支持arm64e;相当于一个架构判断,从iPhoneXS开始的设备都支持这个结构;
  2. 阅读源码之后我们发现结构体objc_class的内部首个成员是占8个字节的Class ISA,其次是占8个字节的Class superclass 和 结构体类型的cache_t cache,然后是 struct class_data_bits_t类型的bits;
explicit_atomic<uintptr_t> _bucketsAndMaybeMask;// 范型真正的大小是<uintptr_t> 无符号长整型 是8字节;
    union {
        struct {
            explicit_atomic<mask_t>    _maybeMask;//uint32_t //2个字节
#if __LP64__
            uint16_t                   _flags;//uint16_t2个字节
#endif
            uint16_t                   _occupied;//uint16_t2个字节
        };
       
      explicit_atomic<preopt_cache_t *> _originalPreoptCache;//指针类型8个字节
    };
  1. chache所占的内存空间,按照结构体内存计算是16字节,那么最终的bits的位置就是class本身的内存地址偏移8+8+16个字节即可获得;

class_data_bits_t bits信息

  • 首先经过分析我们得出struct objc_class内部的数据结构如下图所示:

  • 注意list_array_tt 相当于数组 存储了很多property_list_t,property_list_t又是一个数组存储了很多property_t

009.png 基于这张图上的数据结构信息,我们可以依次递进取值最终找到对应的property

类的property探究

通过LLDB调试,逐步获取struct objc_class内部的数据, 可以看到GCPersonproperty信息,调试代码代码如下;

(lldb) x/4gx GCPerson.class     //步骤1
0x100004568: 0x0000000100004540 0x0000000100354140
0x100004578: 0x00000001007889b0 0x0001802000000003
(lldb) p/x 0x100004568 + 0x20    //步骤2结构体指针偏移32个字节
(long) $1 = 0x0000000100004588
(lldb) p (class_data_bits_t *)$1    //步骤3
(class_data_bits_t *) $2 = 0x0000000100004588
(lldb) p $2->data()    //步骤4获取data
(class_rw_t *) $3 = 0x0000000100788970    
(lldb) p *$3    //重要步骤5还原数据
(class_rw_t) $4 = {
  flags = 2148007936
  witness = 0
  ro_or_rw_ext = {
    std::__1::atomic<unsigned long> = {
      Value = 4294983960
    }
  }
  firstSubclass = nil
  nextSiblingClass = NSUUID
}
(lldb) p $4.properties()    //步骤6
(const property_array_t) $6 = {
  list_array_tt<property_t, property_list_t, RawPtr> = {
     = {
      list = {
        ptr = 0x0000000100004210
      }
      arrayAndFlag = 4294984208
    }
  }
}
(lldb) p $6.list  //步骤7获取list
(const RawPtr<property_list_t>) $7 = {
  ptr = 0x0000000100004210
}
(lldb) p $7.ptr //步骤8获取ptr
(property_list_t *const) $8 = 0x0000000100004210
(lldb) p *$8    //重要步骤9还原数据
(property_list_t) $9 = {
  entsize_list_tt<property_t, property_list_t, 0, PointerModifierNop> = (entsizeAndFlags = 16, count = 2)
}
(lldb) p $9.get(0)  //步骤10
(property_t) $10 = (name = "name", attributes = "T@\"NSString\",C,N,V_name")
(lldb) 

类的method探究

通过与上一步骤类似的方法,逐步获取struct objc_class内部的数据, 最终可以看到GCPersonmethod信息了,调试代码代码如下;

(lldb) x/4gx GCPerson.class    //步骤1
0x100004630: 0x0000000100004608 0x0000000100354140
0x100004640: 0x00000001006fa530 0x0001803000000003
(lldb) p/x 0x100004630 + 0x20   //步骤2结构体指针偏移32个字节
(long) $1 = 0x0000000100004650
(lldb) p (class_data_bits_t *)$1   //步骤3
(class_data_bits_t *) $2 = 0x0000000100004650
(lldb) p $2->data()     //步骤4获取data
(class_rw_t *) $3 = 0x00000001006fa4e0
(lldb) p *$3     //重要步骤5还原数据
(class_rw_t) $4 = {
  flags = 2148007936
  witness = 0
  ro_or_rw_ext = {
    std::__1::atomic<unsigned long> = {
      Value = 4294984056
    }
  }
  firstSubclass = nil
  nextSiblingClass = NSUUID
}
(lldb) p $4.methods()    //步骤6
(const method_array_t) $5 = {
  list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> = {
     = {
      list = {
        ptr = 0x00000001000041c0
      }
      arrayAndFlag = 4294984128
    }
  }
}
(lldb) p $5.list   //步骤7获取list
(const method_list_t_authed_ptr<method_list_t>) $6 = {
  ptr = 0x00000001000041c0
}
(lldb) p $6.ptr    //步骤8获取ptr
(method_list_t *const) $7 = 0x00000001000041c0
(lldb) p *$7    //重要步骤9还原数据
(method_list_t) $8 = {    
  entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 27, count = 5)
}
(lldb) p $8.get(0).big()// 步骤10 获取下标为0的方法,并通过big()打印方法;
(method_t::big) $9 = {
  name = "sayHello"
  types = 0x0000000100003eda "v16@0:8"
  imp = 0x0000000100003aa0 (KCObjcBuild`-[GCPerson sayHello] at main.m:26)
}
(lldb) 

步骤10返回的是method_t类型不能直接打印,需要通过其内部的结构体big才可以看到相关信息;

成员变量探究

  1. 经过阅读源码,发现通过class_rw_t* data() const获取的class_rw_t里面有一个const class_ro_t *ro() const方法,可以获取class_ro_t信息,在 class_ro_t里面又有ivarsconst char *getName() const等一系列方法和成员,如下图所示

010.png 2. 基于这个信息 我们是否可以返照获取methodsproperties的相关方法获取ivar呢?经过尝试是可以的,代码如下;

(lldb) x/4gx GCPerson.class //步骤1
0x100004630: 0x0000000100004608 0x0000000100354140
0x100004640: 0x0000000102b2bda0 0x0001803000000003
(lldb) p/x 0x100004630 + 0x20    //步骤2结构体指针偏移32个字节
(long) $1 = 0x0000000100004650
(lldb) p (class_data_bits_t *)$1    //步骤3
(class_data_bits_t *) $2 = 0x0000000100004650
(lldb) p $2->data()  //步骤4获取data;
(class_rw_t *) $3 = 0x0000000102b2bd50
(lldb) p *$3    //重要步骤5还原数据
(class_rw_t) $4 = {
  flags = 2148007936
  witness = 0
  ro_or_rw_ext = {
    std::__1::atomic<unsigned long> = {
      Value = 4294984056
    }
  }
  firstSubclass = nil
  nextSiblingClass = NSUUID
}
(lldb) p $4.ro() // 步骤6获取ro
(const class_ro_t *) $5 = 0x0000000100004178
(lldb) p $5.ivars    //步骤7获取ivars
(const ivar_list_t *const) $6 = 0x0000000100004240
  Fix-it applied, fixed expression was: 
    $5->ivars
(lldb) p $5->ivars->get(1)  //步骤8获取ivars的首个元素ivar_t
(ivar_t) $8 = {
  offset = 0x00000001000045d8
  name = 0x0000000100003e67 "job"
  type = 0x0000000100003ee2 "@\"NSString\""
  alignment_raw = 3
  size = 8
}
(lldb) p $5.name    //步骤9获取name
(const explicit_atomic<const char *>) $9 = {
  std::__1::atomic<const char *> = "GCPerson" {
    Value = 0x0000000100003e3e "GCPerson"
  }
}
(lldb) 

类方法变量探究

  1. 我们已经知道instance是由class实例化得到的,class是由MetaClass实例化得到的,instance的方法存储在class中,那么class的方法是否也类似的存储与MetaClass当中呢?我们尝试进行了以下分析
@interface GCPerson : NSObject
{
    NSString *_hobby;
    NSString *job;
}
@property (nonatomic,copy) NSString *name;
@property (nonatomic,copy) NSString *address;
- (void)sayHello;
+ (void)sayHi;
@end
@implementation GCPerson
- (void)sayHello {
    NSLog(@"hello");
}
+ (void)sayHi {
    NSLog(@"Hi");
}
@end

GCPerson *p1 = [[GCPerson alloc] init];
Class cla = object_getClass(p1);
Class metaClass = object_getClass(p1.class);
NSLog(@"%p, %p ",cla,metaClass);
  1. 这里我们创建了一个包含类方法的GCPerson,同是获取了MetaClass;

3.在LLDB中模仿获取对象方法的步骤来获取MetaClass中的method,最终成功找到了类方法;

(lldb) x/4gx metaClass     //步骤1获取metaClass的地址
0x100004638: 0x00000001003540f0 0x00000001003540f0
0x100004648: 0x000000010034b360 0x0000e03100000000
(lldb) p/x 0x100004638 + 0x20    //步骤2结构体指针偏移32个字节
(long) $1 = 0x0000000100004658
(lldb) p (class_data_bits_t *)$1    //步骤3
(class_data_bits_t *) $2 = 0x0000000100004658
(lldb) p $2->data()    //步骤4获取data
(class_rw_t *) $3 = 0x0000000100719a60
(lldb) p *$3    //重要步骤5
(class_rw_t) $4 = {
  flags = 2684878849
  witness = 0
  ro_or_rw_ext = {
    std::__1::atomic<unsigned long> = {
      Value = 4294983992
    }
  }
  firstSubclass = nil
  nextSiblingClass = 0x00007fff884e2cd8
}
(lldb) p $4.methods()  //步骤6 获取methods
(const method_array_t) $5 = {
  list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> = {
     = {
      list = {
        ptr = 0x0000000100004180
      }
      arrayAndFlag = 4294984064
    }
  }
}
(lldb) p $5.list    //步骤7获取list
(const method_list_t_authed_ptr<method_list_t>) $6 = {
  ptr = 0x0000000100004180
}
(lldb) p $6.ptr    //步骤8获取ptr
(method_list_t *const) $7 = 0x0000000100004180
(lldb) p *$7      // 重要步骤9还原数据
(method_list_t) $8 = {
  entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 27, count = 1)
}
(lldb) p $8.get(0).big()  //步骤10通过big打印method的信息
(method_t::big) $9 = {
  name = "sayHi"
  types = 0x0000000100003ed2 "v16@0:8"
  imp = 0x0000000100003a10 (KCObjcBuild`+[GCPerson sayHi] at main.m:30)
}
(lldb) 

至此,我们已经找到了对象的Ivar类方法的存储位置了;