第二篇:iOS的内存分布探索

2022-04-20  本文已影响0人  坚持才会看到希望

1.对象的内存分布

首先我们那看下HPWPerson这个对象,其占用了多少的内存空间,我们通过malloc_size((__bridge const void *)(p))进行打印后其显示的是48字节,那为什么是48字节呢,我们带着这个问题去探究下。

@interface HPWPerson : NSObject
@property (nonatomic ,copy) NSString *name;
@property (nonatomic ,copy) NSString *hobby;
@property (nonatomic ,assign) int age;
@property (nonatomic ,assign) double hight;
@property (nonatomic ,assign) short number;
@end
int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {

        appDelegateClassName = NSStringFromClass([AppDelegate class]);
       
        HPWPerson *p = [HPWPerson new];
        NSLog(@"%lu",malloc_size((__bridge const void *)(p)));
        
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
2022-04-20 12:53:53.236808+0800 001--对象的内存分布[53714:782522] 48

我们这个时候去分析下HPWPerson里的成员变量,通过下面的各个成员变量占用的空间为8+8+4+8+2=30,因为oc里其对象都有一个isa指针,这个指针占8字节,所以一起是38.但是苹果是8字节对其的最小为16字节所以HPWPerson创建的对象需要分配48字节的空间

@property (nonatomic ,copy) NSString *name;  NSString占用8字节
@property (nonatomic ,copy) NSString *hobby; NSString占用8字节
@property (nonatomic ,assign) int age; int占用4字节
@property (nonatomic ,assign) double hight; double占用8字节
@property (nonatomic ,assign) short number; short占用2字节

那我们如果在HPWPerson类里添加一个类方法+ (void)test,一个实例方法- (void)test,那分配的空间会改变吗?其实答案是不变的,这个是因为,我们的对象存储的是isa指针和成员变量的值。

@interface HPWPerson : NSObject
@property (nonatomic ,copy) NSString *name;
@property (nonatomic ,copy) NSString *hobby;
@property (nonatomic ,assign) int age;
@property (nonatomic ,assign) double hight;
@property (nonatomic ,assign) short number;
- (void)test;
+ (void)test;
@end
    HPWPerson *p = [HPWPerson new];
        p.name = @"鹏伟";
        p.hobby = @"girl";
        p.hight = 1.80;
        p.age = 18;
        p.number = 123;

接着我们再分析下,苹果分配空间是按成员变量里的顺序,还是按我们给每个变量赋值时候的顺序来的,这个时候我们需要用到一些lldb调试,其中用到了p指令,po指令,p/x指令打印16进制数据,p/o指令打印8进制数据,p/t指令打印2进制数据,p/f指令打印浮点类型数据。x/6gx指令指的是以16进制形式去打印6个8字节的内存地址(g代表每个为8字节大小,x代表16进制形式 )

(lldb) x/6gx p
0x600001df4060: 0x010000010280d5c1 0x00000012007b0000
0x600001df4070: 0x00000001028080c8 0x00000001028080e8
0x600001df4080: 0x3ffccccccccccccd 0x0000000000000000

第一个0x010000010280d5c1地址存的是8字节的isa指针,打印第3个和第5个地址如下:

(lldb) po 0x00000001028080c8
鹏伟
(lldb) p/f 0x3ffccccccccccccd
(long) $3 = 1.8

通过上面我们发现,复制的时候 p.name = @"鹏伟";是放在第一位的,并且在HPWPerson的成员变量里name也是放在第一位的。但是我们通过指令打印的时候其并不是近排在isa指针后面的,而是放在第三个地址里的,这个是因为苹果其里面有自动重排属性的顺序进行优化操作,其还会把几个不满足8字节的排在一起进行优化。

分析到这里,我们再来分析个有趣的现象:

我们创建一个OSTestObject1类,它继承OSTestObject类,然后在OSTestObject类,也就是父类里改变int count的位置,发现其实际占用的内存空间不同。

@interface OSTestObject : NSObject
{
    @public
    int count;
    NSObject *objc1;
    NSObject *objc2;
//    int count;
}
@end
@implementation OSTestObject
@end
@interface OSTestObject1 : OSTestObject
{
    @public
    int _count2;
}
@end

@implementation OSTestObject1
@end

- (void)viewDidLoad {
    [super viewDidLoad];
    
    OSTestObject1 *objc1 = [[OSTestObject1 alloc] init];
    NSLog(@"objc1实际占用的内存空间为%zd",class_getInstanceSize([OSTestObject1 class]));
    NSLog(@"系统为objc1开辟的内存空间为%zd",malloc_size((__bridge const void *)objc1));
    
}

当为这个顺序的时候,其实际内存空间为40字节

@interface OSTestObject : NSObject
{
    @public
    int count;
    NSObject *objc1;
    NSObject *objc2;
//    int count;
}

2022-04-20 13:56:40.160391+0800 Test[54706:831054] objc1实际占用的内存空间为40
2022-04-20 13:56:40.160449+0800 Test[54706:831054] 系统为objc1开辟的内存空间为48

当为这个顺序的时候,其实际内存空间为32字节

@interface OSTestObject : NSObject
{
    @public
//  int count;
    NSObject *objc1;
    NSObject *objc2;
    int count;
}

2022-04-20 13:58:22.191690+0800 Test[54755:832662] objc1实际占用的内存空间为32
2022-04-20 13:58:22.191746+0800 Test[54755:832662] 系统为objc1开辟的内存空间为32

通过上面两个输出打印发现父类里的成员变量顺序不同的时候,其实际占用的内存空间不同,这个是为什么呢?这个是因为苹果在重排的时候子类不影响父类的内存分配,如果在排到父类最后里有位置可以塞子类里成员变量的时候,就会把子类里往里面塞。当我们子类继承父类的时候,其父类是一块连续的内存空间,子类是无法修改父类的空间排列。

2.结构体的内存分布

这个我们来分析下结构体里暂用的内存大小,LGStruct1的大小是4字节,char是占1字节。LGStruct2占用的是1字节。

struct LGStruct1 {
    char a;
    char b;
    char c;
    char d;
}struct1;

struct LGStruct2 {
    // 1的位置代表: 位域
    char a : 1;//有的时候不需要8字节存储,只要1个byte位就可以
    char b : 1;
    char c : 1;
    char d : 1;
}struct2;

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);    
        NSLog(@"%lu %lu",sizeof(struct1),sizeof(struct2));
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

2022-04-20 14:39:10.946945+0800  [55403:857022] 4 1

如果这里设置如下:那么是占用4字节,因为不满8byte可以自行换到另外一个8byte里

struct LGStruct2 {
    // 1的位置代表: 位域
    char a : 7;
    char b : 2;
    char c : 7;
    char d : 2;
}struct2;

3.结构体和联合体的内存分布区别

struct LGTeacher1 {
    char *name;
    int age;
    double height;
}t1;

union LGTeacher2 {
    char *name;
    int age;
    int height;
}t2;

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
        t1.name = "HPW";
        t1.age = 18;
        t1.height = 2.2;
        
        t2.name = "HPW";
        t2.age = 18;
        t2.height = 2.2;        
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

打断点调试后打印如下:

t1  LGTeacher1  
name    char *  "HPW"   0x0000000100b43e1f
age int 18
height  double  2.2000000000000002

t2  LGTeacher2  
name    char *  ""  0x0000000100000002
age int 2
height  int 2

通过上面打印发现,结构体里打印是各个属性的值,但是联合体打印的都是乱七八糟的东西,这个是因为联合体分配的是一个空间,联合体里面的各个属性同时共用这个空间,只有在走到对应赋值语句时候打印才能显示对应的地址的值,接着继续运行这个空间就会释放了,就会看到一些乱值,这里char,int都是指针,所以联合体最大占用8字节,联合体总结是能容纳最大的成员变量,通过1计算出的大小必须是最大的成员变量(基础数据类型整数倍)。
1)结构体里的变量可以共存,内存开辟比较粗放
2)联合体里的变量互斥,可节省一定的内存空间

为什么我们这里要说到联合体呢,其实我们的isa指针就是个联合体,下面我们来对isa指针进行一个探索:

4.isa指针探索

之前我们说了alloc一个对象后,如何和分配的内存地址进行一个关联呢,这个时候就用到了一个isa的指针,我们惊奇的发现isa指针竟然是一个联合体,这里我们再讲一个nonpointerIsa的概念,这个是苹果用来进行优化的,因为其是8字节,有64个byte位存储,像我们引用计数就存在里面。


WechatIMG1912.jpeg

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    uintptr_t bits;

private:
    // Accessing the class requires custom ptrauth operations, so
    // force clients to go through setClass/getClass by making this
    // private.
    Class cls;

public:
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };

    bool isDeallocating() {
        return extra_rc == 0 && has_sidetable_rc == 0;
    }
    void setDeallocating() {
        extra_rc = 0;
        has_sidetable_rc = 0;
    }
#endif

    void setClass(Class cls, objc_object *obj);
    Class getClass(bool authenticated);
    Class getDecodedClass(bool authenticated);
};

我们看到在isa指针里union这个联合体里有struct结构体,我们看到有ISA_BITFIELD这个,我们搜索下也就是对应isa指针的结构,在这里模拟器或者真机调试时候,区分是x86结构,还是arm64结构等,不同的结构其数据架构是不同的,nonpointer是对isa指针的优化。其中我们看到 uintptr_t shiftcls_and_sig : 52; 这个里面其实存的是类对象也就是HPWPerson

#   if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
#     define ISA_MASK        0x007ffffffffffff8ULL
#     define ISA_MAGIC_MASK  0x0000000000000001ULL
#     define ISA_MAGIC_VALUE 0x0000000000000001ULL
#     define ISA_HAS_CXX_DTOR_BIT 0
#     define ISA_BITFIELD                                                      \
        uintptr_t nonpointer        : 1;                                       \
        uintptr_t has_assoc         : 1;                                       \
        uintptr_t weakly_referenced : 1;                                       \
        uintptr_t shiftcls_and_sig  : 52;                                      \
        uintptr_t has_sidetable_rc  : 1;                                       \
        uintptr_t extra_rc          : 8 //3
#     define RC_ONE   (1ULL<<56)
#     define RC_HALF  (1ULL<<7)
上一篇下一篇

猜你喜欢

热点阅读