iOS 底层原理 - 类的结构分析

指针和内存平移

普通指针

先看下代码:

int a = 10; //
 int b = 10; //
LGNSLog(@"%d -- %p",a,&a);
LGNSLog(@"%d -- %p",b,&b);

输出结果为:

KC打印: 10 -- 0x7ffeefbff49c
KC打印: 10 -- 0x7ffeefbff498

从而可以看出普通指针是值拷贝,两个不同的指针指向一个地址

对象指针

运行如下代码:

        LGPerson *p1 = [LGPerson alloc];
        LGPerson *p2 = [LGPerson alloc];
        LGNSLog(@"%@ -- %p",p1,&p1);
        LGNSLog(@"%@ -- %p",p2,&p2);

结果如下:

KC打印:  -- 0x7ffeefbff490
KC打印:  -- 0x7ffeefbff488

p1和p2的值还有指针都不一样,因为每次alloc的时候,系统会给对象单独分配一块内存空间,p1指向这个对象时候就是把该对象的内存地址copy过来存起来,因为每个对象的内存地址都不一样,所以p1和p2里面的地址都不一样,所以指针拷贝是两个不同的指针指向不同的内存地址。


屏幕快照 2020-02-12 下午3.29.37.png

数组指针

代码如下:

int c[4] = {1,2,3,4};
NSLog(@"%p - %p - %p",&c,&c[0],&c[1]);

运行结果如下:

0x7ffeefbff4b0 - 0x7ffeefbff4b0 - 0x7ffeefbff4b4

这里会发现&c,&c[0]的地址一样,是因为数组的指针就是第一个元素的地址,用lldb调试下

2020-02-12 15:17:30.235596+0800 002-内存偏移[4780:1553665] 0x7ffeefbff4b0 - 0x7ffeefbff4b0 - 0x7ffeefbff4b4
(lldb) x/4gx 0x7ffeefbff4b0
0x7ffeefbff4b0: 0x0000000200000001 0x0000000400000003
0x7ffeefbff4c0: 0x00007ffeefbff4e8 0x541e6ca04a6c00c8

可以看到c的地址里面存了四个值1,2,3,4,因为int占4个字节,并且内存是从低位开始读取,由于每块内存是8个字节,但是int是4个字节,所以每个内存上面可以存储2个int类型的值,所以结果就是0x7ffeefbff4b0: 0x0000000200000001 0x0000000400000003

指针偏移

代码如下:

        // 数组指针
        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 = c[I];
            int value = *(d+i);
            LGNSLog(@"%d",value);
        }

输入结果为:

2020-02-12 15:17:30.235596+0800 002-内存偏移[4780:1553665] 0x7ffeefbff4b0 - 0x7ffeefbff4b0 - 0x7ffeefbff4b4
2020-02-12 15:23:38.872493+0800 002-内存偏移[4780:1553665] 0x7ffeefbff4b0 - 0x7ffeefbff4b4 - 0x7ffeefbff4b8
KC打印: 1
KC打印: 2
KC打印: 3
KC打印: 4

d是c的内存地址,数组的内存地址是连续的,所以地址+1相当于内存地址偏移1个元素大小,也就是4个字节,所以for循环中我们可以通过指针偏移来取到每个元素的值


屏幕快照 2020-02-12 下午3.29.52.png

类的结构分析

类的结构

通过查阅源码,可以知道类Class的本质就是objc_class。objc_class继承自objc_object,验证万物皆对象。



然后问题来了,第一:objc_class与NSObject的关系?
NSObject就是一个类,其本质是objc_class,也就是说NSObject是objc_class的一种类型。
第二:objc_object与NSObject的关系?
NSObject是OC的类型,objc_object是c的类型。 NSObject是对objc_object的封装,OC的底层编译是C,也就是会转成objc_object。

类定义的属性方法的存储

先看下源码中类的结构

struct objc_class : objc_object {
    // Class ISA; // 8
    Class superclass; // 8
    cache_t cache;    // 16 不是8         // 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();
    }

    ...省略其他的信息...

类结构成员分析

从上面可以看出类有四个成员
第一个是isa,
第二个父类指针,
第三个是缓存,
第四个是bits,是一个结构体
所以类的属性最可能是存在bits里面,并且我们点击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

    void setFlags(uint32_t set) 
    {
        OSAtomicOr32Barrier(set, &flags);
    }

    void clearFlags(uint32_t clear) 
    {
        OSAtomicXor32Barrier(clear, &flags);
    }

......省略其他信息......

我们似乎看到了我们熟悉的属性,方法,代理等相关的数据类型。
但是这里要注意了,我们类的属性,方法并没有存在method_array_t、property_array_t这些类型的属性里面(它是什么,后面章节会介绍)。
而是存放在了class_ro_t这个结构体里面,我们看到定义的是const,可以说明这一块会在编译时候就确定好了,后面取出来使用是不可以更改的。
然后开始验证一下,根据上面的指针和内存偏移内容可以知道,如果我们知道类的地址,并且属性是按照顺序依次排列的,只要我们知道isa、superclass、cache的内存大小,那我们就可以通过内存偏移来得到bits的内存地址,然后取出bits里面的内容。

屏幕快照 2020-02-12 下午4.12.58.png

通过上图可以知道
1、ISA是指针,是8个字节,
2、superclass也是指针、8个字节
3、cache_t结构如下,他是一个结构体,结构体根据里面所容纳的内容计算内存空间 结构体指针只占8位

struct cache_t {
    struct bucket_t *_buckets; // 8
    mask_t _mask;  // 4
    mask_t _occupied; // 4

mask_t点进入为:
typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits

所以cache_t占16个字节,根据类的内存地址偏移32个字节就得到bits的内存地址,接下来通过lldb开始调试。
首先为LGPerson新建一个成员变量hobby以及属性nickName,并且添加了示例方法和类方法

#import 

NS_ASSUME_NONNULL_BEGIN

@interface LGPerson : NSObject{
    NSString *hobby;
}

@property (nonatomic, copy) NSString *nickName;

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

@end

NS_ASSUME_NONNULL_END

下面开始代码断点调试

 LGPerson *person = [LGPerson alloc];
Class pClass     = object_getClass(person);
(lldb) p/x pClass
(Class) $0 = 0x00000001000023b0 LGPerson

然后地址偏移32位就是class_data_bits_t bits,0x00000001000023D0,继续调试

lldb) p (class_data_bits_t *) 0x00000001000023D0
(class_data_bits_t *) $2 = 0x00000001000023d0

class_rw_t为类中存储属性和方法的地方,看一下class_rw_t的实现,返回的是bits.data(),我们这里调用一下data方法之后得出一个class_rw_t类型的指针,直接取值,结果如下

(lldb) p $2->data()
(class_rw_t *) $3 = 0x0000000101137820

查看目标ro:

(lldb) p $3->ro
(const class_ro_t *) $5 = 0x0000000100002308
(lldb) p *$5
(const class_ro_t) $6 = {
  flags = 388
  instanceStart = 8
  instanceSize = 24
  reserved = 0
  ivarLayout = 0x0000000100001f89 "\x02"
  name = 0x0000000100001f80 "LGPerson"
  baseMethodList = 0x0000000100002240
  baseProtocols = 0x0000000000000000
  ivars = 0x00000001000022a8
  weakIvarLayout = 0x0000000000000000
  baseProperties = 0x00000001000022f0
}

查看它的baseProperties

(lldb) p $6.baseProperties
(property_list_t *const) $7 = 0x00000001000022f0
(lldb) p *$7
(property_list_t) $8 = {
  entsize_list_tt = {
    entsizeAndFlags = 16
    count = 1
    first = (name = "nickName", attributes = "T@\"NSString\",C,N,V_nickName")
  }
}

发现有且仅有一个元素 nickName和我们定义的相同。
查看ivars

(lldb) p $6.ivars
(const ivar_list_t *const) $9 = 0x00000001000022a8
(lldb) p *$9
(const ivar_list_t) $10 = {
  entsize_list_tt = {
    entsizeAndFlags = 32
    count = 2
    first = {
      offset = 0x0000000100002378
      name = 0x0000000100001e64 "hobby"
      type = 0x0000000100001fa6 "@\"NSString\""
      alignment_raw = 3
      size = 8
    }
  }
}
(lldb) p $10.get(0)
(ivar_t) $20 = {
  offset = 0x0000000100002378
  name = 0x0000000100001e64 "hobby"
  type = 0x0000000100001fa6 "@\"NSString\""
  alignment_raw = 3
  size = 8
}
(lldb) p $10.get(1)
(ivar_t) $11 = {
  offset = 0x0000000100002380
  name = 0x0000000100001e6a "_nickName"
  type = 0x0000000100001fa6 "@\"NSString\""
  alignment_raw = 3
  size = 8
}

然后可以拿到成员变量与属性。

查看baseMothedList

(lldb) p $6.baseMethodList
(method_list_t *const) $12 = 0x0000000100002240
(lldb) p *$12
(method_list_t) $13 = {
  entsize_list_tt = {
    entsizeAndFlags = 26
    count = 4
    first = {
      name = "sayHello"
      types = 0x0000000100001f8b "v16@0:8"
      imp = 0x0000000100001b90 (LGTest`-[LGPerson sayHello] at LGPerson.m:13)
    }
  }
}
(lldb) p $13.get(0)
(method_t) $14 = {
  name = "sayHello"
  types = 0x0000000100001f8b "v16@0:8"
  imp = 0x0000000100001b90 (LGTest`-[LGPerson sayHello] at LGPerson.m:13)
}
(lldb) p $13.get(1)
(method_t) $15 = {
  name = "nickName"
  types = 0x0000000100001f93 "@16@0:8"
  imp = 0x0000000100001bf0 (LGTest`-[LGPerson nickName] at LGPerson.h:17)
}
(lldb) p $13.get(2)
(method_t) $16 = {
  name = "setNickName:"
  types = 0x0000000100001f9b "v24@0:8@16"
  imp = 0x0000000100001c20 (LGTest`-[LGPerson setNickName:] at LGPerson.h:17)
}
(lldb) p $13.get(3)
(method_t) $17 = {
  name = ".cxx_destruct"
  types = 0x0000000100001f8b "v16@0:8"
  imp = 0x0000000100001c60 (LGTest`-[LGPerson .cxx_destruct] at LGPerson.m:11)
}

sayHello定义的对象方法。
nickName底层生成的getter方法。
setNickName:底层生成的setter方法。
.cxx_destruct系统c++的析构函数。
那么发现没有sayHappy这个类方法,在之前的isa的指向分析文章中,了解到了类的isa指针,指向的是一个同名类,我们把它叫做元类,那么类方法会不会保存在元类中,我们测试一下。

(lldb) x/4gx pClass
0x1000023b0: 0x001d800100002389 0x0000000100afe140
0x1000023c0: 0x0000000101063670 0x000000050000000f
(lldb) p/x 0x001d800100002389&0x00007ffffffffff8ULL
(unsigned long long) $22 = 0x0000000100002388
(lldb) po 0x0000000100002388
LGPerson

然后按照上面的调试流程依次往下:

(lldb) p $25.baseMethodList
(method_list_t *const) $26 = 0x00000001000010c0
(lldb) p *$26
(method_list_t) $27 = {
  entsize_list_tt = {
    entsizeAndFlags = 26
    count = 1
    first = {
    name = "sayHappy"
    types = 0x0000000100000f8c "v16@0:8"
    imp = 0x0000000100000d80 (XDTest`+[XDPerson sayHappy] at XDPerson.m:15)
    }
  }
}

然后我们确定发现类的类方法是存在元类里面

你可能感兴趣的:(iOS 底层原理 - 类的结构分析)