Objective-C(七)對象內存分析

導言

​ 本系列接《Effective Objective-C 2.0》一書中的系列文章。git

本文主要針對幾個類來窺探實例對象在內存中的存儲,咱們從成員變量和屬性入手,本文相關代碼在這兒

咱們平時編寫的Objective-C代碼,底層實現其實都是C\C++代碼

屏幕快照 2018-11-08 下午7.24.53

因此Objective-C的面向對象都是基於C\C++的數據結構實現的

Objective-C的對象、類主要是基於C\C++的什麼數據結構實現的——結構體。

1、 NSObject 佔用多少內存

NSObject *obj = [[NSObject alloc] init];

// 得到NSObject實例對象的成員變量所佔用的大小 >> 8
NSLog(@"%zd", class_getInstanceSize([NSObject class]));

// 得到obj指針所指向內存的大小 >> 16
NSLog(@"%zu", malloc_size((__bridge const void *)obj));
複製代碼

上面有兩個函數:

建立一個實例對象,至少須要多少內存?

# import <objc/runtime.h>

class_getInstanceSize([NSObject class]);

建立一個實例對象,實際上分配了多少內存?

#import <malloc/malloc.h>

malloc_size((__bridge const void *)obj);

將Objective-C代碼轉換爲C\C++代碼

//若是須要連接其餘框架,使用-framework參數。好比-framework UIKit
// xcrun  -sdk  iphoneos  clang  -arch  arm64  -rewrite-objc  OC源文件  -o  輸出的CPP文件
$ xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
複製代碼

其中,咱們能夠發現NSObject轉換爲C++的底層結構體爲:

//main.cpp
struct NSObject_IMPL {
	Class isa;		//typedef struct objc_class *Class;
};
複製代碼

咱們直接經過斷點調試也能夠發現,obj的確只有一個isa成員變量。

2018-11-09_17-11-37

1.1 查看實時內存

下面咱們經過查看obj對應的內存,來觀察:

image-20181109171709554

從上圖中能夠看到,從0x1029000a0地址開始的8個字節,是有數據的,後面8個字節,都是0。

根據最開始打印的:

// 得到NSObject實例對象的成員變量所佔用的大小 >> 8
NSLog(@"%zd", class_getInstanceSize([NSObject class]));

// 得到obj指針所指向內存的大小 >> 16
NSLog(@"%zu", malloc_size((__bridge const void *)obj));
複製代碼

咱們猜想,前8個字節就是obj中isa佔用的內存空間,後8個字節,是爲了內存對齊而分配的填充字節。

1.2 結構體轉換

爲了驗證這個猜想,咱們將obj對象轉換爲對應的結構體:

struct NSObject_IMPL {
    Class isa;
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *obj = [[NSObject alloc] init];
        
        NSLog(@"%zd", class_getInstanceSize([NSObject class]));
        NSLog(@"%zu", malloc_size((__bridge const void *)(obj)));
        
        struct NSObject_IMPL *objImpl = (__bridge struct NSObject_IMPL *)obj;
        NSLog(@"obj address: %p", obj);
        NSLog(@"objImpl address: %p, objImpl isa: %p", objImpl, objImpl->isa);
        NSLog(@"-----");
    }
    return 0;
}
複製代碼

再次運行,輸出結果以下:

image-20181109180237592

從上圖咱們能夠看出:

  • obj能轉換爲NSObject_IMPL結構體,地址一致;
  • isa的值爲0x1dffffa4575141,與上一次運行一致,且只佔8個字節。
  • obj最終佔用16個字節,其中8個字節分配給isa,8個字節爲內存對齊的填充字節。

在這裏,爲何是16個字節,須要說明一下,iOS系統會給對象至少分配16*n字節的大小。

2、 更復雜的對象:BFPerson

@interface BFPerson : NSObject
{
    @public
    int _age;
    int _male;
}
@property (nonatomic, assign) double height;
@end

@implementation BFPerson

@end
    
int main(int argc, const char * argv[]) {
    @autoreleasepool {
       
        BFPerson *jack = [[BFPerson alloc] init];
        jack->_age = 24;
        jack->_male = 1;
        jack.height = 185;
        NSLog(@"jack age is %d, male: %d, height: %f", jack->_age, jack->_male, jack.height);
        
        BFPerson *rose = [[BFPerson alloc] init];
        rose->_age = 21;
        rose->_male = 0;
        rose.height = 165;
        NSLog(@"rose age is %d, male: %d, height: %f", rose->_age, rose->_male, rose.height);

        NSLog(@"%zd", class_getInstanceSize([BFPerson class]));
        NSLog(@"%zd", malloc_size((__bridge const void *)jack));
        
    }
    return 0;
}
複製代碼

這一次,對象更復雜,並且繼承了NSObject,那麼其中實例對象中成員變量分配了多少字節,實際佔用了多少本身呢?

最後輸出:

image-20181109181326036

經過將上面代碼轉換爲C++代碼,咱們能夠獲得BFPerson的結構:

其中,BFPerson_IMPL包含了NSObject_IMPL結構體,因此最後能夠規整爲:

image-20181109182017738

能夠看到,第一個變量仍是isa指針,後面跟着咱們定義的兩個成員變量,及定義的一個屬性。

咱們知道屬性最後會轉換一個對應的成員變量,因此總共有三個成員變量。

其中isa,咱們知道是一個佔用8字節,因此獲得下面的各個成員變量的佔用字節數:

image-20181109182338345

這和咱們打印該實例對象佔用的爲24個字節相符,可是系統仍是給它分配了32個字節。

咱們經過兩種方式來查看內存中的實例對象,是否是按咱們預想的方式存儲這些成員變量。

2.1 實時查看內存

以下;

image-20181110151749966

  • 查看BFPerson類,jack對象地址0x100683240
  • jack實例對象成員對象佔用24個字節,系統分配32個字節。
    • 實例對象起始8個字節爲:71 12 00 00 01 80 1D 00,爲isa值;
    • 接着4個字節:18 00 00 00,爲_age值,即_age = 24;
    • 接着4個字節:01 00 00 00,爲_male值,即_male=1;
    • 緊接着8個字節:00 00 00 00 00 20 67 40,爲浮點數185的IEEE表示,即height=185;
    • 最後8個字節爲填充字節
      • 應用於系統對齊,均爲0:00 00 00 00 00 00 00 00;
      • 非結構體內存對齊,結構體內存對齊,指結構體的內存佔用是其中最大內存佔用的變量的整數倍,其中isa佔8個字節,但結構體最後佔用24字節,已是其整數倍。
  • Mac CPU是小端模式。
  • 一樣可觀察rose對象是否符合預期,不贅述。

2.2 結構體轉換

咱們經過前面轉換C++代碼結構體的分析,將jack轉換爲咱們自定義的結構體。

struct NSObject_IMPL {
    Class isa;
};

struct BFPerson_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int _age;
    int _male;
    double _height;
};

struct BFPerson_IMPL *jackImpl = (__bridge struct BFPerson_IMPL *)jack;
NSLog(@"jack age is %d, male: %d, height: %f", jackImpl->_age, jackImpl->_male, jackImpl->_height);
複製代碼

輸出以下,能夠看到結果徹底一致,因此咱們的符合咱們的猜測。

image-20181110152805292

3、繼承的對象:BFProgrammer

繼承關係以下:

@interface BFPerson : NSObject
{
@public
    int _age;
    int _male;
}
@property (nonatomic, assign) double height;
@end

@implementation BFPerson
@end

@interface BFProgrammer : BFPerson
{
    @public
    char *company;
}
@end

@implementation BFProgrammer
@end
複製代碼

測試代碼以下;

BFPerson *jack = [[BFPerson alloc] init];
        jack->_age = 24;
        jack->_male = 1;
        jack.height = 185;
        NSLog(@"jack age is %d, male: %d, height: %f", jack->_age, jack->_male, jack.height);
        NSLog(@"jack instance size: %zd", class_getInstanceSize([BFPerson class]));
        NSLog(@"jack malloc size: %zd", malloc_size((__bridge const void *)jack));
        
        BFProgrammer *tony = [[BFProgrammer alloc] init];
        tony->_age = 28;
        tony->_male = 1;
        tony.height = 178;
        tony->company = "Google";
        NSLog(@"tony age is %d, male: %d, height: %f, company: %s", tony->_age, tony->_male, tony.height, tony->company);
        
        NSLog(@"tony instance size:%zd", class_getInstanceSize([BFProgrammer class]));
        NSLog(@"tony malloc size: %zd", malloc_size((__bridge const void *)tony));
複製代碼

對應的結果:

image-20181110170304953

3.1 結構體

image-20181110194752711

下面咱們分析下結果:

對於tony這個程序員:

  • 其成員變量大小爲32字節,相對於jack這個BFPerson,多了8字節的內存變量。那麼這8個字節,用於存放char *company的指針。

    • 最後實例內存變量佔用32字節,系統實際分配也爲32字節。

3.2 company 的內存分析

咱們如今更進一步,從內存中直接讀取tony的公司名稱:Google。

3.2.1 Google 的ASCII字符

Google是個C語言字符串常量,其存儲在內存中,採用ASCII字符編碼,其最後的結構爲:

從上面圖中,咱們發現tony所在地址爲0x103300700,根據其結構體,咱們能夠知道company所在地址爲:

compyan地址 = 0x103300700 + isa+ _age + _male + _height

​ = 0x103300700 + 8 + 4 + 4 + 8

= 0x103300718
複製代碼

3.2.2 lldb查看內存

對應的指令以下:

(lldb) x/2wx 0x103300718
0x103300718: 0x00000f52 0x00000001
(lldb) x/4wx 0x0000000100000f52
0x100000f52: 0x676f6f47 0x7400656c 0x20796e6f 0x20656761
(lldb) x/4wx 0x103300700
0x103300700: 0x00001391 0x001d8001 0x0000001c 0x00000001
複製代碼

3.2.3 分析

image-20181110192444681

  • 32字節成員變量,均符合分析;
  • company字符指針地址爲0x0000000100000f52,指向Google字符串,Google佔7個字節,注意是7個字節,最後一個字符爲'\0';
  • 注意大小端的書寫方式,不要寫反,或者讀錯。
    • lldb顯示的爲正常可讀的大端模式,及高位顯示高字節,低位顯示低字節;
    • 內存中顯示未小端模式,52 0F 00 00 01 00 00 00,改成咱們正常的讀寫大端模式:0x0000000100000f52。即從右向左讀取每一個字節便可。

4、源碼分析

下面咱們分析上面咱們經常使用的打印語句:

NSLog(@"%zd", class_getInstanceSize([NSObject class]));
        NSLog(@"%zu", malloc_size((__bridge const void *)(obj)));
複製代碼

alloc以及class_getInstanceSize源碼z在Apple souce objc4庫中。

4.1 alloc

**[NSObject alloc]**代碼調用流程:

image-20181110212729733

  • calloc返回的爲最後內存分配的字節數
    • _class_createInstanceFromZone中的size是從instanceSize獲取;
    • _class_createInstanceFromZone中的size 是實例變量佔用的空間,不是最後分配的內存空間;
    • _class_createInstanceFromZone中的size 是通過結構體字節對齊的空間(instanceSize爲字節對齊的空間);
  • instanceSize獲取的是alignedInstanceSize
    • alignedInstanceSize小於16字節,會補齊爲16字節。
      • _class_createInstanceFromZone中size = 16
    • alignedInstanceSize大於16字節,直接返回alignedInstanceSize
      • _class_createInstanceFromZone中的size = alignedInstanceSize

能夠看到,其中當上圖最後一步中的alignedInstanceSize,即通過結構體字節對齊後字節仍小於16字節,就會補齊爲16字節。

爲何須要補齊16字節呢?

代碼文檔有一行註釋:

CF requires all objects be at least 16 bytes.

或者咱們能夠理解爲OC對象爲了提升系統分配及查找地址的效率,而作的一個這樣的規定。

這也是上面第一個實例NSObject分析中,爲何實例對象實際佔用8字節,會分配16字節的緣由。alignedInstanceSize實際實際返回8字節,但calloc中的時候,size爲16字節。

4.2 class_getInstanceSize

size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}
    
uint32_t alignedInstanceSize() {
    return word_align(unalignedInstanceSize());
}
複製代碼

能夠看出,class_getInstanceSize最後返回的就是上面所說的,通過字節對齊,可是在alloccalloc以前的大小。

因此咱們看到其實際返回的是成員變量實際須要的空間大小。

4.3 calloc

calloc代碼在另一個庫中——libmalloc

其中源碼比較難以理解,因此直接給出結論。

  • calloc返回的是系統實際分配的內存,最後返回的大小必定是16的倍數

  • 因此BFPerson實例中,成員變量佔24字節,但最後通過calloc返回的是32字節(16*2);

  • malloc_size返回的是對象指針所指向的大小,就是calloc實際分配的內存大小;

    extern size_t malloc_size(const void *ptr);

    ​ /* Returns size of given ptr */

5、查看內存

5.1 LLDB

LLDB的使用請參考:待補--iOS調試(二)LLDB

5.2 View Memory

Debug -> Debug Workfllow -> View Memory (Shift + Command + M)

image-20181109181240334

參考

連接

示例源碼

相關文章
相關標籤/搜索