[[NSObject alloc] init]佔用多少空間?

Objective-C的本質

Objective-C代碼底層實現其實都是C\C++代碼。面試

編譯流程
編譯流程
  • Objective-C的面向對象都是基於C\C++的 結構體這種數據結構實現的。

將Objective-C轉爲C/C++

clang -rewrite-objc main.m -o main.cppxcode

main.m文件所在的目錄執行上面的命令,將Objective-C代碼轉爲C++代碼,輸出文件爲main.cpp。這個命令並無指定編輯的平臺與架構,針對iOS系統,咱們能夠用下面這個命令:bash

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp數據結構

指定sdk爲iphoneos,針對iOS系統,-arch arm64 表示針對arm64架構進行編譯,輸出文件爲main-arm64.cpp架構

注: iOS架構:模擬器i386 32bitarmv7 64bitarm64app

xcrun 是Xcode的命令,若是沒法執行多是由於裝了兩個版本的Xcode。在命令行指定一下Xcode路徑:iphone

sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer/函數

NSObject底層實現

NSObject.h 文件中,咱們能夠看到NSObject的聲明以下圖左, 在main-arm64.cpp 中能夠發現NSObject的實現以下圖右。能夠得出結論,NSObject的底層是使用了C/C++的結構體來實現的。佈局

OC->C/C++
OC->C/C++

接下來,咱們繼續查看Class的聲明,以下,能夠看到,Class是一個指向結構體變量的指針。由於在64位的系統中,一個指針佔用8個字節,全部上圖中的isa佔用8個字節。ui

typedef struct objc_class *Class;
複製代碼

爲了獲取NSOject對象所佔的內存空間,咱們能夠經過malloc_size 函數獲取對象所佔用內存空間的大小。注意,須要導入庫 malloc/malloc.h

#import <malloc/malloc.h>

NSObject *obj = [[NSObject alloc] init];
//得到obj指針所指向內存的大小 輸出結果爲 16
NSLog(@"%zd", malloc_size((__bridge const void*)obj)); 
複製代碼

另外,咱們還能夠經過class_getInstanceSize方法獲取對象的全部成員變量所佔用的內存空間。注意,須要導入庫objc/runtime.h。在上面的分析中,咱們已經確認NSObject內部只有一個isa成員變量,它是一個指向Class結構體的指針變量。

#import <objc/runtime.h>

//得到NSObject實例對象的成員變量所佔用的大小。 輸出結果爲 8
NSLog(@"%zd", class_getInstanceSize([NSObject class]));
複製代碼

咱們也能夠分析一下class_getInstanceSize的源碼實現,以下:

//源碼版本:objc4-723

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

//Class's ivar size rounded up to a pointer-size boundary. //類的成員變量的大小,word_align 對齊操做 uint32_t alignedInstanceSize() { return word_align(unalignedInstanceSize()); } // May be unaligned depending on class's ivars.
// 類的成員變量的未對齊的大小
uint32_t unalignedInstanceSize() {
  return data()->ro->instanceSize;
}

//對齊函數實現
static inline size_t word_align(size_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}

#ifdef __LP64__
# define WORD_SHIFT 3UL
# define WORD_MASK 7UL
# define WORD_BITS 64
#else
# define WORD_SHIFT 2UL
# define WORD_MASK 3UL
# define WORD_BITS 32
#endif
複製代碼

alloc 源碼:

//源碼版本:objc4-723

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

//Base class implementation of +alloc. cls is not nil.
// Calls [cls allocWithZone:nil].
id _objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}

// 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 (fastpath(!cls->ISA()->hasCustomAWZ())) {
        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;
        }
    }
    if (allocWithZone) return [cls allocWithZone:nil];
    return [cls alloc];
}

//建立實例
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 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; obj->initIsa(cls); } if (cxxConstruct && hasCxxCtor) { obj = _objc_constructOrFree(obj, cls); } return obj; } size_t instanceSize(size_t extraBytes) { //alignedInstanceSize 就是 class_getInstanceSize 也就是內部變量所佔用的空間大小 size_t size = alignedInstanceSize() + extraBytes; // CF requires all objects be at least 16 bytes. //最小爲16 if (size < 16) size = 16; return size; } 複製代碼

咱們能夠畫出[[NSObject alloc] init] 執行後的內存結構圖。

image-20200505220829847

至此,咱們分析獲得結論,NSObject對象佔用16個字節的內存空間,其中前8個字節是isa變量,它是一個指向Class結構體的指針。

查看內存佈局

  1. Debug —> Debug Workflow —> View Memory
查看內存
查看內存
  1. 經過LLDB查看內存佈局:

經過memory read 指令能夠讀取內存地址。簡寫爲x。其後面能夠跟上數量/格式/字節數。

如:x/4xg 0x10312b010 表示 讀寫每次讀取8個字節,讀寫4組數據,並以16進制的形式表示。

格式:x是16進制,f是浮點數,d是10進制。

字節大小:b byte是1字節, h half world 是2字節,w world是4字節, g giant world是8字節。

(lldb) po obj

<NSObject: 0x10312b010>

(lldb) memory read 0x10312b010

0x10312b010: 19 91 62 98 ff ff 1d 00 00 00 00 00 00 00 00 00 ..b.............

0x10312b020: 2d 5b 4e 53 54 61 62 50 69 63 6b 65 72 56 69 65 -[NSTabPickerVie

(lldb) x 0x10312b010

0x10312b010: 19 91 62 98 ff ff 1d 00 00 00 00 00 00 00 00 00 ..b.............

0x10312b020: 2d 5b 4e 53 54 61 62 50 69 63 6b 65 72 56 69 65 -[NSTabPickerVie

(lldb) x/3xg 0x10312b010

0x10312b010: 0x001dffff98629119 0x0000000000000000

0x10312b020: 0x50626154534e5b2d

(lldb) x/4xg 0x10312b010

0x10312b010: 0x001dffff98629119 0x0000000000000000

0x10312b020: 0x50626154534e5b2d 0x65695672656b6369

修改內存中的值

使用 memory write 命令能夠修改內存中某個位置所表示的內容。下面命令是要修改0x10312b010起始第9個字節,將其內容從0改成8。

(lldb) memory read 0x10312b010

0x10312b010: 19 91 62 98 ff ff 1d 00 00 00 00 00 00 00 00 00 ..b.............

0x10312b020: 2d 5b 4e 53 54 61 62 50 69 63 6b 65 72 56 69 65 -[NSTabPickerVie

(lldb) memory write 0x10312b018 8

(lldb) memory read 0x10312b010

0x10312b010: 19 91 62 98 ff ff 1d 00 08 00 00 00 00 00 00 00 ..b.............

0x10312b020: 2d 5b 4e 53 54 61 62 50 69 63 6b 65 72 56 69 65 -[NSTabPickerVie

複雜的模型

定義一個類繼承自NSObject

@interface Student : NSObject {
    @public
    int _no;
    int _age;
}
@end

@implementation Student
@end
複製代碼

將該類編譯成C/C++代碼,其實現以下:

struct NSObject_IMPL {
 Class isa;
};

struct Student_IMPL {
 struct NSObject_IMPL NSObject_IVARS;
 int _no;
 int _age;
};
複製代碼

Student將其父類NSObject的成員變量都繼承過來了,至關於:

struct Student_IMPL {
 Class isa;
 int _no;
 int _age;
};
複製代碼

在這個結構中,isa佔用8個字節,_no_age各佔用4個字節,因此,Student內部的成員變量一共佔用16個字節的空間。

即:NSLog(@"%zd", class_getInstanceSize([Student class])); 輸出結果爲16。

NSLog(@"%zd", malloc_size((__bridge const void *)student)); 輸出結果也爲16,由於內部成語變量的大小爲16,恰好爲alloc建立出來的最小空間相等。

Student的內存結構以下:

image-20200506124926322

咱們能夠構造一個結構體,將student對象強制轉換爲結構體,以下:咱們就能夠經過結構體正確的訪問到內部變量。

struct Student_Impl {
    Class isa;
    int _no;
    int _age;
};

struct Student_Impl *impl = (__bridge struct Student_Impl *)student;
NSLog(@"no=%d, age=%d", impl->_no, impl->_age);
複製代碼

咱們能夠進一步證明isa的起始地址就是student指向的地址。

(lldb) po &impl->isa

<Student: 0x103905a40>

(lldb) po student

<Student: 0x103905a40>

更爲複雜的模型

@interface Person : NSObject {
    @public
    int _age;
}
@end

@implementation Person
@end

@interface Student : Person {
    @public
    int _no;
}
@end

@implementation Student
@end
複製代碼

轉換爲C/C++爲:

struct Student_IMPL {
    struct Person_IMPL Person_IVARS;
    int _no;
};
 
struct Person_IMPL {
  struct NSObject_IMPL NSObject_IVARS;
  int _age;
};
struct NSObject_IMPL {
  Class isa;
};
複製代碼

內存分佈:

image-20200506132936304
Person *person = [[Person alloc] init];
//16 雖然真實大小爲12,因爲內存對齊,爲16
NSLog(@"%zd", class_getInstanceSize([person class])); 
NSLog(@"%zd", malloc_size((__bridge const void *)person)); //16

Student *student = [[Student alloc] init];
NSLog(@"%zd", class_getInstanceSize([student class])); //16
NSLog(@"%zd", malloc_size((__bridge const void *)student)); //16
複製代碼

注,內存對齊:結構體的大小必須是最大成員大小的倍數。

開闢內存空間大小規則

@interface Person : NSObject {
    @public
    int _age;
    int _no;
}
@end

@implementation Person
@end

@interface Student : Person{
     @public
    int data;
}
@end
複製代碼

在上面這種類的結構中,咱們打印出變量空間大小和內存大小。

NSLog(@"%zd", class_getInstanceSize([Student class])); //24
NSLog(@"%zd", malloc_size((__bridge const void *)student)); //32
複製代碼

class_getInstanceSize 這個大小是16+8(int 4, 內存對齊爲8) = 24,應該不難理解。

malloc_size 這個爲32 就有點難理解了,緣由是操做系統在開闢內存的時候不是你須要多少就開闢多少,而是要根據必定的規則,爲了方便內存管理與尋址,iOS系統開闢內存的空間都是16的倍數,全部咱們須要24個字節的大小,操做系統給咱們分配了32個字節。具體開闢方式能夠參考資料一。

參考

參考一:https://yq.aliyun.com/articles/3065

參考二:MJ底層原理

歡迎關注面試小集,期待與你交流。

掃碼關注
相關文章
相關標籤/搜索