1. 程式人生 > >NSObject到底多麼大引發的一些思考

NSObject到底多麼大引發的一些思考

NSObject到底多麼大引發的一些思考

本文引用及參考文獻,感謝一下博主的分享:

一個問題,一個NSObject的例項佔多大記憶體?

幾個概念

我們先來明確幾個計算機概念,位(bit)、位元組(byte)、字

  • 位(bit)

計算機內部資料儲存的最小單位,我們所謂的幾位,就是常見的二進位制中的一位。

  • 位元組(byte)

計算機中資料處理的基本單位,計算機中以位元組為單位儲存和解釋資訊。一個位元組8bit

  • 字(word)

計算機進行資料處理時,一次存取、加工和傳送的資料長度稱為字。和它相關的一個概念叫字長,是標識字的bit數,在32位機器中,計算機匯流排一次傳輸32位=4位元組。字64位機器中,計算機匯流排一次傳輸64位=8位元組。所以64位機比32位機速度快很多

記憶體中的計算都是用bit來標識的,可能是因為記憶體本身就是稀缺資源,並沒有很大,儲存的內容也不會過大。

言歸正傳

實驗環境

MacBook Pro (Retina, 13-inch, Early 2015)
10.14 Beta (18A365a)

XCode Version 9.4.1 (9F2000)

iPhone8 Plus
iOS 11.4.1(15G77)

也就是64位的環境

一個NSObject的大小實驗

一個絕對乾淨的NSObject多麼大

#import <malloc/malloc.h>

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

NSLog(@"point size: %ld\n", sizeof(obj));
NSLog(@"object size: %ld\n", malloc_size((__bridge const void *)obj));

malloc_size: 返回指標所指向物件位元組數。但是這種方法不會考慮到物件成員變數指標所指向物件所佔用的記憶體。

sizeof: 返回一個物件或型別所佔的記憶體位元組數。詳細解釋sizeof用法

使用C++的malloc_size計算例項大小,sizeof計算指標大小

TEST[1650:139260] point size: 8
TEST[1650:139260] object size: 16

我們看到一個object佔16位元組,一個object指標佔8位元組

一個基礎型別變數多麼大

int i = 11111;
double d = 0.0;
float f = 0.3;
long l = 11111;

NSLog(@"int size: %ld\n", sizeof(i));
NSLog(@"double size: %ld\n", sizeof(d));
NSLog(@"float size: %ld\n", sizeof(f));
NSLog(@"long size: %ld\n", sizeof(l));

結果:
TEST[1741:146323] int size: 4
TEST[1741:146323] double size: 8
TEST[1741:146323] float size: 4
TEST[1741:146323] long size: 8

一個“不乾淨”的NSObject多麼大

根據上一個實驗,如果一個繼承自NSObject的類中有一個字串屬性,使用malloc_size方法計算出來的大小應該是 16(NSObject自身大小) + 8(NSString型別指標大小) = 24。
事實並不是這樣的,我們嘗試一下

定義一個We類

// We.h
#import <Foundation/Foundation.h>

@interface We : NSObject
- (void)logInfo;
@end

// We.m
@implementation We {
    NSString *str;
}

- (void)logInfo;
{
    NSLog(@"str size : %ld",sizeof(arr));
    NSLog(@"str  malloc size : %ld",malloc_size((__bridge const void *) str));
}

// 呼叫We類

We *we = [[We alloc] init];
[we logInfo];

NSLog(@"we point size: %ld\n", sizeof(we));
NSLog(@"we object size: %ld\n", malloc_size((__bridge const void *)we));

結果:

TEST[324:11186] str size : 8
TEST[324:11186] str malloc size : 0
TEST[324:11186] we point size: 8
TEST[324:11186] we object size: 16

這個和我們的預計結果又出入,他的size 是 16 而不是 24。我們再加一個看看

// We.m
@implementation We {
    NSString *str;
    NSString *str2;
}
TEST[324:11186] we object size: 32

好,再一次超出我的預料,上一個實驗表明,如果 一個字串的指標是8位元組,Object本身是16位元組,一個帶有一個字串指標的Object也是16位元組,那麼帶兩個呢?竟然是32位元組。那我們再試試3個

// We.m
@implementation We {
    NSString *str;
    NSString *str2;
    NSString *str3;
}
TEST[324:11186] we object size: 32

32!!!

探究原理

讓我們來捋一下

帶有字串個數 Object大小
1 16
2 32
3 32
4 48
5 48
6 64

不難看出,object最小就是16位元組,並且每次增長都是16的倍數,即便你新增的屬性並沒有佔到16,它會自動補齊到16位元組。

然後去查了一下,發現很多人寫過這個問題的總結(孤陋寡聞了),幾篇博主的套路都是一樣的

我們根據各位博主的程式碼做一下實驗:

先解釋一下所用的方法:

class_getInstanceSize

// 定義
size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}


// Class's ivar size rounded up to a pointer-size boundary.
// (類中成員變數的指標的空間範圍)
uint32_t alignedInstanceSize() {
    return word_align(unalignedInstanceSize());
}

實驗程式碼:

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

//獲得NSObject 類的例項物件的大小
NSLog(@"NSObject Instance Size:%zd",class_getInstanceSize([NSObject class])  );

//此處很多博主的註釋是“獲取obj物件指標獲取的大小”,我覺得是有問題的,
//更合適的解答是指標指向的記憶體空間大小
NSLog(@"NSObject Point Size:%zd",malloc_size((__bridge const void *)object));  
TEST[1359:104954] NSObject Instance Size:8
TEST[1359:104954] NSObject Point Size:16

也就是說Object的例項在記憶體中佔8位元組,但是指標所指向的記憶體空間確是16位元組。

也就是說他多分配了8個位元組的空間。為什麼?這不是浪費嗎?

我們再來測試一下:


@implementation We {
    NSString *str;
    int age;
}

We *we = [[We alloc] init];
NSLog(@"We Instance Size:%zd",class_getInstanceSize([We class]));
NSLog(@"We Point Size:%zd",malloc_size((__bridge const void *)we));

預測結果: 20 , 32.

實際結果: 24 , 32

TEST[1859:153420] We Instance Size:24
TEST[1859:153420] We Point Size:32

這裡牽扯到一個記憶體對齊的概念此處不展開解釋,大家自行觀看吧記憶體對齊是什麼

我們的核心問題

帶著這幾個問題我們看下邊的實驗:

1,為什麼NSObject分配的空間是16的倍數?

2,為什麼class_getInstanceSize獲取大小並不是真實的各個屬性指標所佔有的實際大小?

問題1 為什麼NSObject分配的空間是16的倍數?

使用
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o mian.cpp
命令,將OC程式碼轉換成c++程式碼

#ifndef _REWRITER_typedef_NSObject
#define _REWRITER_typedef_NSObject
typedef struct objc_object NSObject;
typedef struct {} _objc_exc_NSObject;
#endif

struct NSObject_IMPL {
    Class isa;
};

這個是NSObject的結構,只有一個Class 指標,所以class_getInstanceSize 計算 NSObject 例項的大小為8位元組也就可以解釋了,因為只有一個指標

我們再來看看為什麼malloc_size的結果是16.
malloc是申請記憶體空間的函式,OC的分配空間函式是alloc,那我們來看一下它的實現

+ (id)alloc {
    return _objc_rootAlloc(self);
}

// Replaced by ObjectAlloc
+ (id)allocWithZone:(struct _NSZone *)zone {
    return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
}

// Call [cls alloc] or [cls allocWithZone:nil], with appropriate 
// shortcutting optimizations.
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
    if (slowpath(checkNil && !cls)) return nil;

#if __OBJC2__
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        // No alloc/allocWithZone implementation. Go straight to the allocator.
        // fixme store hasCustomAWZ in the non-meta class and 
        // add it to canAllocFast's summary
        if (fastpath(cls->canAllocFast())) {
            // No ctors, raw isa, etc. Go straight to the metal.
            bool dtor = cls->hasCxxDtor();
            id obj = (id)calloc(1, cls->bits.fastInstanceSize());
            if (slowpath(!obj)) return callBadAllocHandler(cls);
            obj->initInstanceIsa(cls, dtor);
            return obj;
        }
        else {
            // Has ctor or raw isa or something. Use the slower path.
            id obj = class_createInstance(cls, 0);
            if (slowpath(!obj)) return callBadAllocHandler(cls);
            return obj;
        }
    }
#endif

    // No shortcuts available.
    if (allocWithZone) return [cls allocWithZone:nil];
    return [cls alloc];
}


id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone)
{
    id obj;

#if __OBJC2__
    // allocWithZone under __OBJC2__ ignores the zone parameter
    (void)zone;
    obj = class_createInstance(cls, 0);
#else
    if (!zone) {
        obj = class_createInstance(cls, 0);
    }
    else {
        obj = class_createInstanceFromZone(cls, 0, zone);
    }
#endif

    if (slowpath(!obj)) obj = callBadAllocHandler(cls);
    return obj;
}

程式碼冗長,我們來解讀一下,我們在例項化一個物件的時候會用alloc申請空間,alloc 方法呼叫了 callAlloc方法, 在此方法的最後有一個判斷,如果存在allocWithZone還是會呼叫allocWithZone,所以最終的記憶體空間的申請會落實到allocWithZone方法中。

allocWithZone方法中呼叫了_objc_rootAllocWithZone,此方法中再呼叫class_createInstance,此方法實現如下:

id  
class_createInstance(Class cls, size_t extraBytes)
{
    return _class_createInstanceFromZone(cls, extraBytes, nil);
}


static __attribute__((always_inline)) 
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, 
                              bool cxxConstruct = true, 
                              size_t *outAllocatedSize = nil)
{
    if (!cls) return nil;

    assert(cls->isRealized());

    // Read class's info bits all at once for performance
    bool hasCxxCtor = cls->hasCxxCtor();
    bool hasCxxDtor = cls->hasCxxDtor();
    bool fast = cls->canAllocNonpointer();

    size_t size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if (!zone  &&  fast) {
        obj = (id)calloc(1, size);
        if (!obj) return nil;
        obj->initInstanceIsa(cls, hasCxxDtor);
    } 
    else {
        if (zone) {
            obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
        } else {
            obj = (id)calloc(1, size);
        }
        if (!obj) return nil;

        // Use raw pointer isa on the assumption that they might be 
        // doing something weird with the zone or RR.
        obj->initIsa(cls);
    }

    if (cxxConstruct && hasCxxCtor) {
        obj = _objc_constructOrFree(obj, cls);
    }

    return obj;
}

_class_createInstanceFromZone方法中我們看到了我們期盼的size字眼,但是他是通過一個方法計算的,我們再看它的實現

size_t instanceSize(size_t extraBytes) {
    size_t size = alignedInstanceSize() + extraBytes;
    // CF requires all objects be at least 16 bytes.
    if (size < 16) size = 16;
    return size;
}

至此,問題1就真相大白了。

問題2 為什麼class_getInstanceSize獲取大小並不是真實的各個屬性指標所佔有的實際大小?

objc_runtime_new 檔案中我們找到

// May be unaligned depending on class's ivars.
// 非記憶體對齊的例項大小
uint32_t unalignedInstanceSize() {
    assert(isRealized());
    return data()->ro->instanceSize;
}

// Class's ivar size rounded up to a pointer-size boundary.
// 記憶體對齊的例項大小
uint32_t alignedInstanceSize() {
    return word_align(unalignedInstanceSize());
}

然後我們再瞭解一下記憶體對齊的概念,也就不言而喻了

總結:

1,OC中的物件是按16位元組的倍數來分配記憶體的,會存在記憶體對齊的問題。
2,使用class_getInstanceSize獲取的也並不是實際的物件、指標的記憶體空間,也會存在記憶體對齊問題。
3,基礎型別變數的會比物件型別要節省記憶體。
4,原始碼解釋一切。