美文网首页
类的结构分析

类的结构分析

作者: 深圳_你要的昵称 | 来源:发表于2020-09-14 19:53 被阅读0次

前言

书接上回isa结构分析,我们得知,对象的isa指针指向类,确切的说,是isa指针的shiftcls位域中指向类对象Class的地址。那OC层的Class在底层C、C++层面里,所对应的结构体里,有哪些成员变量和函数呢?下面将大致分析下类的底层结构。

isa指向

首先我们来看看isa指针的指向问题,我们都知道,对象的isa指针指向类对象Class,那类对象Class的isa指针指向哪里呢?我们先通过在lldb中打印地址来查看详情。

我们先定义一个类LGPerson

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface LGPerson : NSObject

@end

NS_ASSUME_NONNULL_END

//--------------------分割线----------------------

#import "LGPerson.h"

@implementation LGPerson

@end

在程序入口main.m中生成LGperson对象

#import <Foundation/Foundation.h>
#import "LGPerson.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        LGPerson *person = [[LGPerson alloc] init];
        

    }
    return 0;
}

打断点查看person对象的地址信息

(lldb) po person
<LGPerson: 0x10124dcb0>

(lldb) p person
(LGPerson *) $2 = 0x000000010124dcb0

我们常用的LLDB指令po,p,他俩的区别如下:

指令名称 释义
p 是 expression 的别名,打印某个东西,可以是变量和表达式
po 一般用于打印对象,是 expression -O 的别名,会输出对应的值

再来看看另外两个LLDB指令

指令名称 释义
p/x 以16进制形式读取对象的地址或者值
x/4gx 以16进制形式读取4段8位的内存空间里面存储的地址或者值

用上面指令看看person对象的地址

(lldb) p/x person
(LGPerson *) $4 = 0x000000010124dcb0

(lldb) x/4gx person
0x10124dcb0: 0x001d8001000020e9 0x0000000000000000
0x10124dcc0: 0x0000000080080000 0x00007fff8d07d208

首先第一段0x10124dcb0: 0x001d8001000020e9这个就是person对象的isa指针地址,再查看isa指针中位域shiftcls的值,有个小技巧,通过和ISA_MASK进行与运算,过滤出shiftcls的地址值

在查看isa_t结构体中,可以找到ISA_MASK的宏定义(以x86_64为例)
define ISA_MASK 0x00007ffffffffff8ULL

(lldb) p/x 0x001d8001000020e9 & 0x00007ffffffffff8ULL
(unsigned long long) $6 = 0x00000001000020e8
(lldb) po $6
LGPerson

验证得出:person对象isa指针的位域shiftcls中指向的是类LGPerson的地址。
同理,我们在继续看看类LGPerson对象的isa指针位域shiftcls中存放的什么值?

// 以4段方式查看LGPeson类对象地址
(lldb) x/4gx $6
0x1000020e8: 0x00000001000020c0 0x00000001003ef190
0x1000020f8: 0x0000000101063830 0x0004801000000007

// 首地址isa & ISA_MASK,得到位域shiftcls中存放的值
(lldb) p/x 0x00000001000020c0 & 0x00007ffffffffff8ULL
(unsigned long long) $7 = 0x00000001000020c0

// 打印shiftcls值
(lldb) po $7
LGPerson

发现,类对象LGPerson的isa指针中shiftcls存储的值也是LGPerson,但是$6 = 0x00000001000020e8$7 = 0x00000001000020c0的地址值显然不同,说明$7明显不是指向类对象LGPerson它自己,但是打印出的值是LGPerson,这就是苹果提出的一个概念元类(meta class)

验证得出,类对象的isa指向与对象的isa指向不同,对象的isa指向类,而类的isa指向元类(苹果爸爸定义为meta class)。

接着我们继续查看$7的isa指针的位域shiftcls中存放的地址值是什么?

(lldb) x/4gx $7
0x1000020c0: 0x00000001003ef140 0x00000001003ef140
0x1000020d0: 0x0000000101063b40 0x0003e03100000007
(lldb) p/x 0x00000001003ef140 & 0x00007ffffffffff8ULL
(unsigned long long) $8 = 0x00000001003ef140
(lldb) po $8
NSObject

$7指针的位域shiftcls中存放的地址$8值是NSObject。接着看$8shiftcls的值,如下:

(lldb) x/4gx $8
0x1003ef140: 0x00000001003ef140 0x00000001003ef190
0x1003ef150: 0x0000000100640ff0 0x0004e03100000007

(lldb) p/x 0x00000001003ef140 & 0x00007ffffffffff8ULL
(unsigned long long) $9 = 0x00000001003ef140

(lldb) po $9
NSObject

$8 = 0x00000001003ef1400x1003ef140: 0x00000001003ef140(isa指针地址)一样,shiftcls打印出的地址也是0x00000001003ef140,值也是NSObject,说明$8isa指针指向自己。
那这个$9的值NSObject,是不是我们通常的基类NSObject类呢?接下来验证一下

(lldb) p/x NSObject.class
(Class) $10 = 0x00000001003ef190 NSObject

$10的地址与$9不同,说明不是基类。

验证得出,元类的isa指针指向NSObject根元类(苹果爸爸定义为root meta class)。

上一张isa指向经典图:

isa指向.png

以上通过LLDB中的指令查看地址,得出isa指针的指向是:

对象 --> 类 --> 元类 --> 根元类-->根元类 指向自身

特殊问题:类信息在内存中存在几份

以上我们得知,类对象的isa指针指向元类,那类对象Class的isa指针指向的地址是同一个还是不同的?我们还是用代码验证一下:

Class class1 = [LGPerson class];
Class class2 = [LGPerson alloc].class;
Class class3 = object_getClass([LGPerson alloc]);
NSLog(@"\n%p\n%p\n%p", class1, class2, class3);

编译运行后得出:

2020-09-14 17:23:07.926664+0800 KCObjc[9752:305959] 
0x100002100
0x100002100
0x100002100

地址一模一样,说明类信息在内存中只存在一份。

类Class结构分析

言归正传,分析了isa指针指向问题后,现在再来回头看看Class结构的具体情况:

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

Class对应底层结构体是objc_class,再来看看objc_class

struct objc_class : objc_object {
    // 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

    class_rw_t *data() const {
        return bits.data();
    }
    
    // 省略部分代码.......
}
  • objc_class继承objc_object
  • superclass是父类Class
  • cache_t cache缓存
  • class_data_bits_t bits成员

cache_t结构体

// 只列举了成员变量
struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
    explicit_atomic<struct bucket_t *> _buckets;
    explicit_atomic<mask_t> _mask;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    explicit_atomic<uintptr_t> _maskAndBuckets;
    mask_t _mask_unused;
    
#if __LP64__
    uint16_t _flags;
#endif
    uint16_t _occupied;
}

// 省略部分代码...
  • CACHE_MASK_STORAGE_OUTLINED这种缓存模式,则占用内存大小是:bucket_t +mask_t+uint16_t+uint16_t

  • CACHE_MASK_STORAGE这种缓存模式,则占用内存大小是:uintptr_t+mask_t+uint16_t+uint16_t

uintptr_t内存大小
#ifndef _UINTPTR_T
#define _UINTPTR_T
typedef unsigned long           uintptr_t;
#endif /* _UINTPTR_T */

uintptr_t是long类型,8个字节

bucket_t内存大小
struct bucket_t {
private:
    // IMP-first is better for arm64e ptrauth and no worse for arm64.
    // SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
    explicit_atomic<uintptr_t> _imp;
    explicit_atomic<SEL> _sel;
#else
    explicit_atomic<SEL> _sel;
    explicit_atomic<uintptr_t> _imp;
#endif
// 省略部分代码...
}

不管是__arm64__还是其它,都是uintptr_t+_sel指针 --> 4 + 8 = 12

mask_t内存大小
#if __LP64__
typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits
#else
typedef uint16_t mask_t;
#endif

一目了然,即整型,大小为4。

综上所述,

  • CACHE_MASK_STORAGE_OUTLINED模式下,explicit_atomic<struct bucket_t *> _buckets是结构体bucket_t的指针 8,+ mask_t 4 + 2 * uint16_t 2 = 16
  • CACHE_MASK_STORAGE模式下是8+4+2+2,也是16;

结论cache_t 所占内存大小:16

重点 class_data_bits_t

结构体class_data_bits_t大致分为几个部分:

  1. bits操作相关,为私有函数,包含获取、设置、清空等操作
 // Values are the FAST_ flags above.
    uintptr_t bits;
private:
    bool getBit(uintptr_t bit) const
    {
        return bits & bit;
    }

    // Atomically set the bits in `set` and clear the bits in `clear`.
    // set and clear must not overlap.
    void setAndClearBits(uintptr_t set, uintptr_t clear)
    {
       // ...省略部分代码
    }

    void setBits(uintptr_t set) {
        __c11_atomic_fetch_or((_Atomic(uintptr_t) *)&bits, set, __ATOMIC_RELAXED);
    }

    void clearBits(uintptr_t clear) {
        __c11_atomic_fetch_and((_Atomic(uintptr_t) *)&bits, ~clear, __ATOMIC_RELAXED);
    }

2.重要部分data(),返回结构体class_rw_t *指针类型,里面存放了属性列表,实例方法列表,协议列表等信息

class_rw_t* data() const {
    return (class_rw_t *)(bits & FAST_DATA_MASK);
}
void setData(class_rw_t *newData) {
    // ...省略部分代码
}

3.ro data相关

// Get the class's ro data, even in the presence of concurrent realization.
// fixme this isn't really safe without a compiler barrier at least
// and probably a memory barrier when realizeClass changes the data field
const class_ro_t *safe_ro() {
    // ...省略部分代码
}

void setClassArrayIndex(unsigned Idx) {
#if SUPPORT_INDEXED_ISA
    // 0 is unused as then we can rely on zero-initialisation from calloc.
    ASSERT(Idx > 0);
    data()->index = Idx;
#endif
}

unsigned classArrayIndex() {
#if SUPPORT_INDEXED_ISA
    return data()->index;
#else
    return 0;
#endif
}

4.swift相关

bool isAnySwift() {
    return isSwiftStable() || isSwiftLegacy();
}

bool isSwiftStable() {
    return getBit(FAST_IS_SWIFT_STABLE);
}
void setIsSwiftStable() {
    setAndClearBits(FAST_IS_SWIFT_STABLE, FAST_IS_SWIFT_LEGACY);
}

bool isSwiftLegacy() {
    return getBit(FAST_IS_SWIFT_LEGACY);
}
void setIsSwiftLegacy() {
    setAndClearBits(FAST_IS_SWIFT_LEGACY, FAST_IS_SWIFT_STABLE);
}

// fixme remove this once the Swift runtime uses the stable bits
bool isSwiftStable_ButAllowLegacyForNow() {
    return isAnySwift();
}

_objc_swiftMetadataInitializer swiftMetadataInitializer() {
    // This function is called on un-realized classes without
    // holding any locks.
    // Beware of races with other realizers.
    return safe_ro()->swiftMetadataInitializer();
}        
class_rw_t内部信息

class_rw_t里存放了类的实例方法列表,属性列表,协议列表等信息,如何查看呢?下面举例说明。

首先在LGPerson类中添加属性和方法

@interface LGPerson : NSObject

@property (nonatomic, strong) NSString *name;
@property (nonatomic, copy) NSString *nikeName;

- (void)sayHello;
+ (void)sayByebye;

@end

查看LGPerson类的地址

(lldb) p/x LGPerson.class
(Class) $0 = 0x00000001000021f8
// 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

根据上面objc_class的结构分析,要获取class_data_bits_t bits的地址,需要位移isa8位+Class8位 + cache_t16位 = 32位

0x00000001000021f8 + 0x20 = 0x0000000100002218

获取class_data_bits_t

(lldb) p (class_data_bits_t *)0x0000000100002218
(class_data_bits_t *) $1 = 0x0000000100002218

获取data()

(lldb) p $1->data()
(class_rw_t *) $2 = 0x0000000101142b80
(lldb) p *$2
(class_rw_t) $3 = {
  flags = 2148007936
  witness = 1
  ro_or_rw_ext = {
    std::__1::atomic<unsigned long> = 4294975608
  }
  firstSubclass = nil
  nextSiblingClass = NSUUID
}
1. 查看属性列表信息
(lldb) p $3.properties()
(const property_array_t) $4 = {
  list_array_tt<property_t, property_list_t> = {
     = {
      list = 0x0000000100002188
      arrayAndFlag = 4294975880
    }
  }
}

(lldb) p $4.list
(property_list_t *const) $5 = 0x0000000100002188

(lldb) p *$5
(property_list_t) $6 = {
  entsize_list_tt<property_t, property_list_t, 0> = {
    entsizeAndFlags = 16
    count = 2
    first = (name = "name", attributes = "T@\"NSString\",&,N,V_name")
  }
}

count = 2说明有2个属性,再仔细查看这两个属性:


(lldb) p $6.get(0)
(property_t) $7 = (name = "name", attributes = "T@\"NSString\",&,N,V_name")
(lldb) p $6.get(1)
(property_t) $8 = (name = "nikeName", attributes = "T@\"NSString\",C,N,V_nikeName")
(lldb) p $6.get(2)
Assertion failed: (i < count), function get, file /Users/Aron/objc4_debug/objc4-781/runtime/objc-runtime-new.h, line 438.
error: Execution was interrupted, reason: signal SIGABRT.
The process has been returned to the state before expression evaluation.

name nikeName都有了,当获取第三个时越界,Assertion failed: (i < count)表示下标必须小于count:2

2. 查看方法列表信息

同理,查看方法列表methods()

(lldb) p $3.methods()
(const method_array_t) $9 = {
  list_array_tt<method_t, method_list_t> = {
     = {
      list = 0x00000001000020c0
      arrayAndFlag = 4294975680
    }
  }
}
(lldb) p $9.list
(method_list_t *const) $10 = 0x00000001000020c0

(lldb) p *$10
(method_list_t) $11 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 5
    first = {
      name = "nikeName"
      types = 0x0000000100000f98 "@16@0:8"
      imp = 0x0000000100000e20 (KCObjc`-[LGPerson nikeName])
    }
  }
}

count = 5说明有5个方法,明明我只定义了2个方法,- (void)sayHello;+ (void)sayByebye;,再一一打印看看是哪5个方法:

(lldb) p $11.get(0)
(method_t) $12 = {
  name = "nikeName"
  types = 0x0000000100000f98 "@16@0:8"
  imp = 0x0000000100000e20 (KCObjc`-[LGPerson nikeName])
}
(lldb) p $11.get(1)
(method_t) $13 = {
  name = "setNikeName:"
  types = 0x0000000100000fa0 "v24@0:8@16"
  imp = 0x0000000100000e50 (KCObjc`-[LGPerson setNikeName:])
}
(lldb) p $11.get(2)
(method_t) $14 = {
  name = ".cxx_destruct"
  types = 0x0000000100000f90 "v16@0:8"
  imp = 0x0000000100000d90 (KCObjc`-[LGPerson .cxx_destruct])
}
(lldb) p $11.get(3)
(method_t) $15 = {
  name = "name"
  types = 0x0000000100000f98 "@16@0:8"
  imp = 0x0000000100000dd0 (KCObjc`-[LGPerson name])
}
(lldb) p $11.get(4)
(method_t) $16 = {
  name = "setName:"
  types = 0x0000000100000fa0 "v24@0:8@16"
  imp = 0x0000000100000df0 (KCObjc`-[LGPerson setName:])
}

原来是2个属性的get set方法和一个析构函数cxx_destruct,那定义的2个方法去哪了?

补坑:
原来之前只定义了方法,并未实现sayHello sayByebye这2个方法。。。

先补上方法实现,重新编译

@implementation LGPerson

- (void)sayHello {
    
}

+ (void)sayByebye {
    
}

@end

再打印方法列表看看:

(lldb) p *$5
(method_list_t) $6 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 6
    first = {
      name = "sayHello"
      types = 0x0000000100000f35 "v16@0:8"
      imp = 0x0000000100000d60 (KCObjc`-[LGPerson sayHello])
    }
  }
}

发现count = 6,不是7个,看看少了哪个?

(lldb) p $6.get(0)
(method_t) $7 = {
  name = "sayHello"
  types = 0x0000000100000f35 "v16@0:8"
  imp = 0x0000000100000d60 (KCObjc`-[LGPerson sayHello])
}
(lldb) p $6.get(1)
(method_t) $8 = {
  name = "nikeName"
  types = 0x0000000100000f49 "@16@0:8"
  imp = 0x0000000100000e00 (KCObjc`-[LGPerson nikeName])
}
(lldb) p $6.get(2)
(method_t) $9 = {
  name = "setNikeName:"
  types = 0x0000000100000f51 "v24@0:8@16"
  imp = 0x0000000100000e30 (KCObjc`-[LGPerson setNikeName:])
}
(lldb) p $6.get(3)
(method_t) $10 = {
  name = ".cxx_destruct"
  types = 0x0000000100000f35 "v16@0:8"
  imp = 0x0000000100000d70 (KCObjc`-[LGPerson .cxx_destruct])
}
(lldb) p $6.get(4)
(method_t) $11 = {
  name = "name"
  types = 0x0000000100000f49 "@16@0:8"
  imp = 0x0000000100000db0 (KCObjc`-[LGPerson name])
}
(lldb) p $6.get(5)
(method_t) $12 = {
  name = "setName:"
  types = 0x0000000100000f51 "v24@0:8@16"
  imp = 0x0000000100000dd0 (KCObjc`-[LGPerson setName:])
}

发现少了sayByebye,这是类方法,根据isa指针走位分析,实例方法存储在类中,那类方法应该存储在元类中。去元类里看看:

(lldb) p/x LGPerson.class
(Class) $0 = 0x0000000100002230

(lldb) x/4gx 0x0000000100002230
0x100002230: 0x0000000100002208 0x00000001003f0190
0x100002240: 0x000000010142d880 0x0001802400000003

LGPersonisa指针地址是0x0000000100002208,偏移32位获取class_data_bits_t,即0x0000000100002228,再打印出这个地址对应的方法列表信息:

(lldb) p (class_data_bits_t *)0x0000000100002228
(class_data_bits_t *) $1 = 0x0000000100002228
(lldb) p $1->data()
(class_rw_t *) $2 = 0x000000010142d7e0
(lldb) p *$2
(class_rw_t) $3 = {
  flags = 2684878849
  witness = 1
  ro_or_rw_ext = {
    std::__1::atomic<unsigned long> = 4294975536
  }
  firstSubclass = nil
  nextSiblingClass = 0x00007fff8dcd5c60
}
(lldb) p $3.methods()
(const method_array_t) $4 = {
  list_array_tt<method_t, method_list_t> = {
     = {
      list = 0x0000000100002078
      arrayAndFlag = 4294975608
    }
  }
}
(lldb) p $4.list
(method_list_t *const) $5 = 0x0000000100002078
(lldb) p *$5
(method_list_t) $6 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 1
    first = {
      name = "sayByebye"
      types = 0x0000000100000f35 "v16@0:8"
      imp = 0x0000000100000d50 (KCObjc`+[LGPerson sayByebye])
    }
  }
}

count = 1只有1个方法sayByebye给自己一个666,哈哈!印证了isa的走位!

补充:objc_class继承objc_object
struct objc_class : objc_object {
    // 省略部分代码.......
}

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

分析:

  • 结构体类型objc_class 继承自objc_object类型,其中objc_object也是一个结构体,且有一个isa属性,所以objc_class也拥有了isa属性;
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
  • NSObject中的isa在底层是Class类型,而Class的底层编码来自 objc_class类型,所以NSObject也拥有了isa指针

  • NSObject类初始化一个实例对象objc,objc 满足 objc_object 的特性(即有isa指针),主要是因为isaNSObjectobjc_class继承过来的,而objc_class继承自objc_objectobjc_objectisa指针。所以对象都有一个 isaisa表示指向,来自于当前的objc_object

  • objc_object就好比一个模板,不论是Class对象还是继承NSObject的对象,都最终指向继承objc_object(实质是继承了成员 isa),所以说万物皆对象。

总结

1.首先从底层源码分析了isa指针的走位,并且通过示例得以证明。

2.通过isa的走位,我们分析了类Class的底层结构objc_class ,通过示例打印属性列表信息和方法列表信息,证明了实例方法存储在类中,类方法存储在元类中

3.最后通过objc_classobjc_object的继承关系,得出万物皆对象,但始终离不开isa

相关文章

  • iOS 类原理探索:类的结构分析

    OC 类原理探索 系列文章 OC 类原理探索:类的结构分析 OC 类原理探索:类结构分析补充[https://ju...

  • 多线程基础(十三):java中的FutureTask

    [toc] FutureTask源码分析 1.类结构及常量、变量 1.1 类结构 FutureTask类结构如下:...

  • iOS类结构:cache_t分析

    一、cache_t 内部结构分析 1.1 在iOS类的结构分析中,我们已经分析过类(Class)的本质是一个结构体...

  • 类,类结构分析

    忙不是不学习的借口 在isa和类的关联[https://www.jianshu.com/p/079a6ad90f1...

  • iOS-OC底层04:类结构分析

    类结构分析 通过lldb来分析类结构 查看objc2的内存情况 类对象只有一份,isa对象-> 类(LGPerso...

  • iOS-底层分析之类的结构分析

    类的结构分析 本文主要分析iOS中的类以及类的结构,下面我们通过一个例子来探索类的结构 我们定义一个WPerson...

  • 类结构分析

    这片文章主要分析的是类的结构以及对象-类-元类-根元类之间的走位. 一. isa的指向以及类之间的关系 准备工作定...

  • 类结构分析

    类结构分析 回顾 前面我们讲了alloc 流程中对象的创建过程,下面我们来探索一下类的结构,废话不多说,开始~ 类...

  • 类结构分析

    开发中经常创建一个 TestClass.h 和 TestClass.m 文件,而这个 TestClass 就是我们...

  • 类的结构分析

    神图镇楼,相信做过iOS开发的同学一定非常熟悉这张经典图,每次看这张图都有不一样的体会,今天我们就借这张图,引出我...

网友评论

      本文标题:类的结构分析

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