IOS 底層原理 物件的本質--(1)

fgyong發表於2019-07-01

物件的本質

探尋OC物件的本質,我們平時編寫的Objective-C程式碼,底層實現其實都是C\C++程式碼。 那麼一個OC物件佔用多少記憶體呢?看完這篇文章你將瞭解OC物件的記憶體佈局和記憶體分配機制。

使用的程式碼下載 要用的工具:

首先我們使用最基本的程式碼驗證物件是什麼?

int main(int argc, const char * argv[]) {
	@autoreleasepool {
	    // insert code here...
		NSObject *obj=[[NSObject alloc]init];
	    NSLog(@"Hello, World!");
	}
	return 0;
}
複製程式碼

使用clang編譯器編譯成cpp, 執行clang -rewrite-objc main.m -o main.cpp之後生成的cpp,這個生成的cpp我們不知道是跑在哪個平臺的,現在我們指定iphoeosarm64重新編譯一下。xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main64.cpp,將main64.cpp拖拽到Xcode中並開啟。

clang 編譯器
xcrun 命令
sdk 指定編譯的平臺
arch arm64架構
-rewrite-objc 重寫
main.m 重寫的檔案
main64.cpp 匯出的檔案
-o 匯出

command + F查詢int main,找到關鍵程式碼,這就是main函式的轉化成c/c++的程式碼:

int main(int argc, const char * argv[]) {
 /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

  NSObject *obj=((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));

     NSLog((NSString *)&__NSConstantStringImpl__var_folders_c0_7nm4_r7s4xd0mbs67ljb_b8m0000gn_T_main_1b47c1_mi_0);
 }
 return 0;
}
複製程式碼

然後搜尋

struct NSObject_IMPL {
	Class isa;
};
複製程式碼

那麼這個結構體是什麼呢? 其實我們Object-C編譯之後物件會編譯成結構體,如圖所示:

IOS 底層原理 物件的本質--(1)
那麼isa是什麼嗎?通過檢視原始碼得知:

typedef struct objc_class *Class; 
複製程式碼

class其實是一個指向結構體的指標,然後com+點選class得到:

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
複製程式碼

class是一個指標,那麼佔用多少記憶體呢?大家都知道指標在32位是4位元組,在64位是8位元組。

NSObject *obj=[[NSObject alloc]init];
複製程式碼

可以理解成例項物件是一個指標,指標佔用8或者4位元組,那麼暫時假設機器是64位,記為物件佔用8位元組。 obj就是指向結構體class的一個指標。 那麼我們來驗證一下:

int main(int argc, const char * argv[]) {
	@autoreleasepool {
	    // insert code here...
		NSObject *obj=[[NSObject alloc]init];
		//獲得NSobject物件例項大小
		size_t size = class_getInstanceSize(obj.class);
		//獲取NSObjet指標的指向的記憶體大小
		//需要匯入:#import <malloc/malloc.h>
		size_t size2 = malloc_size((__bridge const void *)(obj));
		NSLog(@"size:%zu size2:%zu",size,size2);
	}
	return 0;
}
複製程式碼

得出結果是:

size:8 size2:16
複製程式碼

結論是:指標是8位元組,指標指向的的記憶體大小為16位元組。 檢視原始碼得知[[NSObject alloc]init]的函式執行順序是:

class_createInstance
    -_class_createInstanceFromZone
複製程式碼
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, 
                              bool cxxConstruct = true, 
                              size_t *outAllocatedSize = nil)
{
    if (!cls) return nil;
    **
    size_t size = cls->instanceSize(extraBytes);
    **
    return obj;
}

複製程式碼

這個函式前邊後邊省略,取出關鍵程式碼,其實sizecls->instanceSize(extraBytes)執行的結果。那麼我們再看下cls->instanceSize的原始碼:

//成員變數大小 8bytes
    uint32_t alignedInstanceSize() {
        return word_align(unalignedInstanceSize());
    }
    
    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;
    }
複製程式碼

可以通過原始碼註釋得知:CF要求所有的objects 最小是16bytes。

class_getInstanceSize函式的內部執行順序是class_getInstanceSize->cls->alignedInstanceSize() 查閱原始碼:

//成員變數大小 8bytes
    uint32_t alignedInstanceSize() {
        return word_align(unalignedInstanceSize());
    }
複製程式碼

所以最終結論是:物件指標實際大小為8bytes,記憶體分配為16bytes,其實是空出了8bytes

驗證: 在剛才 的程式碼打斷點和設定Debug->Debug Workflow->View Memory,然後執行程式,

IOS 底層原理 物件的本質--(1)

IOS 底層原理 物件的本質--(1)
點選obj->view *objc得到上圖所示的記憶體佈局,從address看出和obj記憶體一樣,左上角是16位元組,8個位元組有資料,8個位元組是空的,預設是0.

使用lldb命令memory read 0x100601f30輸出記憶體佈局,如下圖:

IOS 底層原理 物件的本質--(1)
或者使用x/4xg 0x100601f30輸出:

IOS 底層原理 物件的本質--(1)
x/4xg 0x100601f304是輸出4個資料,x 是16進位制,後邊g是8位元組為單位。可以驗證剛才的出的結論。

那麼我們再使用複雜的一個物件來驗證:

@interface Person : NSObject
{
	int _age;
	int _no;
}
@end
複製程式碼

使用xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main64.cpp編譯之後對應的原始碼是:

struct NSObject_IMPL {
 Class isa;
};
struct Person_IMPL {
	struct NSObject_IMPL NSObject_IVARS;// 8 bytes
	int _age;//4 bytes
	int _no;//4 bytes
};
複製程式碼

Person——IMPL結構體佔用16bytes

Person *obj=[[Person alloc]init];
		obj->_age = 15;
		obj->_no = 14;
複製程式碼

使用程式碼驗證:

		Person *obj=[[Person alloc]init];
		obj->_age = 15;
		obj->_no = 14;
		
		struct Person_IMPL *p =(__bridge struct Person_IMPL*)obj;
		NSLog(@"age:%d no:%d",p->_age,p->_no);
		
		//age:15 no:14
複製程式碼

使用記憶體佈局驗證:

IOS 底層原理 物件的本質--(1)
以十進位制輸出每個4位元組
IOS 底層原理 物件的本質--(1)
使用記憶體佈局檢視資料驗證,Person佔用16 bytes。

下邊是一個直觀的記憶體佈局圖:

IOS 底層原理 物件的本質--(1)

再看一下更復雜的繼承關係的記憶體佈局:

@interface Person : NSObject
{
	@public
	int _age;//4bytes 
}
@end
@implementation Person
@end

//Student
@interface Student : Person
{
@public
	int _no;//4bytes
}
@end
@implementation Student
@end
複製程式碼

那小夥伴可能要說這一定是32位元組,因為Person上邊已經證明是16位元組,Student又多了個成員變數_no,由於記憶體對齊,一定是16的整倍數,那就是16+16=32位元組。 其實不然,Person是記憶體分配16位元組,其實佔用了8+4=12位元組,剩餘4位元組位子空著而已,Student是一個物件,不可能在成員變數和指標中間有記憶體對齊的,引數和指標是物件指標+偏移量得出來的,多個不同的物件才會存在記憶體對齊。所以Student是佔用了16位元組。

那麼我們來證明一下:

Student *obj=[[Student alloc]init];
		obj->_age = 6;
		obj->_no = 7;
		
		//獲得NSobject物件例項成員變數佔用的大小 ->8
		size_t size = class_getInstanceSize(obj.class);
		//獲取NSObjet指標的指向的記憶體大小 ->16
		size_t size2 = malloc_size((__bridge const void *)(obj));
		NSLog(@"size:%zu size2:%zu",size,size2);
		//size:16 size2:16
		
		
複製程式碼

再看一下LLDB檢視的記憶體佈局:

(lldb) x/8xw 0x10071ae30
0x10071ae30: 0x00001299 0x001d8001 0x00000006 0x00000007
0x10071ae40: 0xa0090000 0x00000007 0x8735e0b0 0x00007fff

(lldb) memory read 0x10071ae30
0x10071ae30: 99 12 00 00 01 80 1d 00 06 00 00 00 07 00 00 00  ................
0x10071ae40: 00 00 09 a0 07 00 00 00 b0 e0 35 87 ff 7f 00 00  ..........5.....

(lldb) x/4xg 0x10071ae30
0x10071ae30: 0x001d800100001299 0x0000000700000006
0x10071ae40: 0x00000007a0090000 0x00007fff8735e0b0

複製程式碼

可以看出來0x000000060x00000007就是兩個成員變數的值,佔用記憶體是16位元組。

我們將Student新增一個成員變數:

//Student
@interface Student : Person
{
@public
	int _no;//4bytes
	int _no2;//4bytes
}
@end
@implementation Student
@end
複製程式碼

然後檢視記憶體佈局:

(lldb) x/8xg 0x102825db0
0x102825db0: 0x001d8001000012c1 0x0000000700000006
0x102825dc0: 0x0000000000000000 0x0000000000000000
0x102825dd0: 0x001dffff8736ae71 0x0000000100001f80
0x102825de0: 0x0000000102825c60 0x0000000102825890

複製程式碼

LLDB可以看出來,記憶體變成了32位元組。(0x102825dd0-0x102825db0=0x20)

我們再增加一個屬性看下:

@interface Person : NSObject
{
	@public
	int _age;//4bytes 
}
@property (nonatomic,assign) int level; //4位元組
@end
@implementation Person
@end

//InstanceSize:16 malloc_size:16 
複製程式碼

為什麼新增了一個屬性,記憶體還是和沒有新增的時候一樣呢? 因為property=setter+getter+ivar,method是存在類物件中的,所以例項Person佔用的記憶體還是_age,_level和一個指向類的指標,最後結果是4+4+8=16bytes

再看下成員變數是3個的時候是多少呢?看結果之前先猜測一下:三個int成員變數是12,一個指標是8,最後是20,由於記憶體是8的倍數,所以是24。

@interface Person : NSObject
{
	@public
	int _age;//4bytes
	int _level;//4bytes
	int _code;//4bytes
}
@end
@implementation Person
@end

Person *obj=[[Person alloc]init];
		obj->_age = 6;
		
		
		//獲得NSobject物件例項成員變數佔用的大小 ->24
		Class ocl = obj.class;
		size_t size = class_getInstanceSize(ocl);
		//獲取NSObjet指標的指向的記憶體大小 ->32
		size_t size2 = malloc_size((__bridge const void *)(obj));
		printf("InstanceSize:%zu malloc_size:%zu \n",size,size2);
		
InstanceSize:24 malloc_size:32
複製程式碼

為什麼和我們猜測的不一樣呢? 那麼我們再探究一下: 例項物件佔用多少記憶體,當然是在申請記憶體的時候建立的,則查詢原始碼NSObject.mm 2306行得到建立物件函式呼叫順序allocWithZone->_objc_rootAllocWithZone->_objc_rootAllocWithZone->class_createInstance->_class_createInstanceFromZone->_class_createInstanceFromZone最後檢視下_class_createInstanceFromZone的原始碼,其他已省略,只留關鍵程式碼:

id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, 
                              bool cxxConstruct = true, 
                              size_t *outAllocatedSize = nil)
{
    if (!cls) return nil;
**
    size_t size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;
    **
    obj = (id)calloc(1, size);
   **
    return obj;
}

複製程式碼

那麼我們在看一下instanceSize中的實現:

	//物件指標的大小
    uint32_t alignedInstanceSize() {
        return word_align(unalignedInstanceSize());
    }

    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;
    }
複製程式碼

最後呼叫的obj = (id)calloc(1, size);傳進去的值是24,但是結果是申請了32位元組的記憶體,這又是為什麼呢? 因為這是c函式,我們去蘋果開源官網下載原始碼看下,可以找到這句程式碼:

#define NANO_MAX_SIZE			256 /* Buckets sized {16, 32, 48, 64, 80, 96, 112, ...} */
複製程式碼

看來NANO_MAX_SIZE在申請空間的時候做完優化就是16的倍數,並且最大是256。所以size = 24 ;obj = (id)calloc(1, size);申請的結果是32位元組。 然後再看下Linux空間申請的機制是什麼? 下載gnu資料, 得到:

#ifndef _I386_MALLOC_ALIGNMENT_H
#define _I386_MALLOC_ALIGNMENT_H

#define MALLOC_ALIGNMENT 16

#endif /* !defined(_I386_MALLOC_ALIGNMENT_H) */


/* MALLOC_ALIGNMENT is the minimum alignment for malloc'ed chunks.  It
   must be a power of two at least 2 * SIZE_SZ, even on machines for
   which smaller alignments would suffice. It may be defined as larger
   than this though. Note however that code and data structures are
   optimized for the case of 8-byte alignment.  */
   //最少是2倍的SIZE_SZ 或者是__alignof__(long double)
#define MALLOC_ALIGNMENT (2 * SIZE_SZ < __alignof__ (long double) \
			  ? __alignof__ (long double) : 2 * SIZE_SZ)
			  
			  
/* The corresponding word size.  */
#define SIZE_SZ (sizeof (INTERNAL_SIZE_T))

#ifndef INTERNAL_SIZE_T
# define INTERNAL_SIZE_T size_t
#endif
複製程式碼

在i386中是16,在其他系統中按照巨集定義計算, __alignof__ (long double)在iOS中是16,size_t是8,則上面的程式碼簡寫為#define MALLOC_ALIGNMENT (2*8 < 16 ? 16:2*8)最終是16位元組。

總結:

例項物件其實是結構體,佔用的記憶體是16的倍數,最少是16,由於記憶體對齊,實際使用的記憶體為M,則實際分配記憶體為(M%16+M/16)*16。例項物件的大小不受方法影響,受例項變數影響。


IOS 底層原理 物件的本質--(1)

相關文章