物件的本質
探尋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
我們不知道是跑在哪個平臺的,現在我們指定iphoeos
和arm64
重新編譯一下。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
編譯之後物件會編譯成結構體,如圖所示:
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;
}
複製程式碼
這個函式前邊後邊省略,取出關鍵程式碼,其實size
是cls->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
,然後執行程式,
obj->view *objc
得到上圖所示的記憶體佈局,從address
看出和obj
記憶體一樣,左上角是16位元組,8個位元組有資料,8個位元組是空的,預設是0.
使用lldb命令memory read 0x100601f30
輸出記憶體佈局,如下圖:
x/4xg 0x100601f30
輸出:
x/4xg 0x100601f30
中4
是輸出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
複製程式碼
使用記憶體佈局驗證:
以十進位制輸出每個4位元組 使用記憶體佈局檢視資料驗證,Person
佔用16 bytes。
下邊是一個直觀的記憶體佈局圖:
再看一下更復雜的繼承關係的記憶體佈局:
@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
複製程式碼
可以看出來0x00000006
和0x00000007
就是兩個成員變數的值,佔用記憶體是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。例項物件的大小不受方法影響,受例項變數影響。