總所周知,oc對象底層是由結(jié)構(gòu)體實現(xiàn)的,所以通過分析結(jié)構(gòu)體內(nèi)存占用情況可以更好的理解oc對象的內(nèi)存占用。
1.把OC對象編譯成結(jié)構(gòu)體
有如下代碼:
#import <Foundation/Foundation.h>
#import "Person.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
Person *per = [[Person alloc] init];
NSLog(@"per:%@",per);
}
return 0;
}
我們可以通過clang命名把.m文件編譯成.cpp文件,進而可以清楚的看到為什么說oc對象底層是結(jié)構(gòu)體實現(xiàn)。
//clang命令
clang -rewrite-objc main.m [-o 別名]
編譯后如下:
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS;
};
2.結(jié)構(gòu)體內(nèi)存占用分析
2.1 常用數(shù)據(jù)類型內(nèi)存占用大小
我們都知道,在不同位數(shù)的編譯器環(huán)境下,數(shù)據(jù)類型不同其占用字節(jié)大小也不相同,區(qū)別如下:
-
32位編譯器
32位編譯器下.png
-
64位編譯器
64位編譯器下.png
2.2 結(jié)構(gòu)體內(nèi)存對齊規(guī)則
為了方便cpu更加快速地讀取存放在內(nèi)存中的數(shù)據(jù),內(nèi)存在存放數(shù)據(jù)時會按照一定的規(guī)則來排列,規(guī)則如下:
- 數(shù)據(jù)成員對?規(guī)則:結(jié)構(gòu)(struct)(或聯(lián)合(union))的數(shù)據(jù)成員,第
?個數(shù)據(jù)成員放在offset為0的地?,以后每個數(shù)據(jù)成員存儲的起始位置要從該成員??或者成員的?成員??(只要該成員有?成員,?如說是數(shù)組,結(jié)構(gòu)體等)的整數(shù)倍開始(?如int為4字節(jié),則要從4的整數(shù)倍地址開始存
儲。 - 結(jié)構(gòu)體作為成員:如果?個結(jié)構(gòu)?有某些結(jié)構(gòu)體成員,則結(jié)構(gòu)體成員要從
其內(nèi)部最?元素??的整數(shù)倍地址開始存儲.(struct a?存有struct b,b?有char,int ,double等元素,那b應(yīng)該從8的整數(shù)倍開始存儲.) - 收尾?作:結(jié)構(gòu)體的總??,也就是sizeof的結(jié)果,.必須是其內(nèi)部最大成員的整數(shù)倍,不?的要補?。
2.3 內(nèi)存對齊計算
2.3.1 普通結(jié)構(gòu)體
例如有下面這樣一個結(jié)構(gòu)體,其內(nèi)存大小為24.分析如下:
struct AA{
double a; //[0,7]
char b; //[8]
int c; //[12,15]
int d; //[16,19]
};
printf("大小為:%d",sizeof(struct AA));//24
我們可以知道sizeof可以用來計算對象類型所占內(nèi)存大小,按照規(guī)則1我們可以將結(jié)構(gòu)體AA對象排列如下:

我們可以看到,a是第一個成員且長度大小為8,所以占位序號為[0,7];
b的長度大小為1,所以占位序號為[8];c的長度大小為4,但是根據(jù)規(guī)則一,這里c不能從序號9開始,因為9不滿足對齊數(shù)(4)的整數(shù)倍,所以要從12開始排列;同理d的占位序號為[16,19],那么整個結(jié)構(gòu)體大小為19+1=20字節(jié),又因為20不滿足規(guī)則三,所以總大小應(yīng)該是最大長度(8)的最小整數(shù)倍且不小于20字節(jié)的數(shù),即24.
2.3.2 嵌套結(jié)構(gòu)體
例如結(jié)構(gòu)體嵌套的情況:
struct BB{
double a; //[0,7]
struct AA b; //[8,31]
char c; //[32]
};
printf("BB:%lu\n",sizeof(struct BB));//40
思路分析:
- a長度為8且為首元素,所以占位序號為[0,7]
- b為結(jié)構(gòu)體變量且長度大小為24,根據(jù)規(guī)則二我們得出b不能從24開始,所以b的占位序號為[8,31]
- c的長度為1,占位序號為[32],所以總大小為:32+1=33,然后33并不是結(jié)構(gòu)體BB的最大對齊數(shù)(8)的整數(shù)倍,因此BB大小為ceil(33/8.0)*8=40.
3.IOS中對象內(nèi)存大小
前面說完了結(jié)構(gòu)體內(nèi)存占用大小的情況,下面說說OC對象占用大小和實際申請大小該怎么計算吧。
首頁,我們往Person類中新增name,nickName,age等幾個屬性.
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (nonatomic,copy)NSString *name;
@property (nonatomic,copy)NSString *nickName;
@property (nonatomic,assign)unsigned int age;
@property (nonatomic,assign)double score;
@end
并賦值如下:
#import <Foundation/Foundation.h>
#import "Person.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
Person *per = [[Person alloc] init];
per.name = @"James";
per.nickName = @"Potter";
per.age = 18;
NSLog(@"per:%@",per);
NSLog(@"需要申請大小:%ld",class_getInstanceSize([per class]));//40
NSLog(@"實際申請大小:%ld",malloc_size((__bridge const void *)per));//48
}
return 0;
}
因為Person有四個屬性:name和nickName是指針變量各占用8個字節(jié),age占用4個字節(jié),score占用8個字節(jié)。其次通過結(jié)構(gòu)體我們可以前面clang編譯我們可以看到,結(jié)構(gòu)體里面還有一個isa指針,所以Person類總大小為:8+8+8+4+8=36,對齊后應(yīng)該是最大長度的整數(shù)倍,即為40.
然而為什么在實際申請內(nèi)存過程中是48呢?其實蘋果底層在申請內(nèi)存是是按照16字節(jié)來申請的.
通過objc4中class_getInstanceSize源碼分析
/**
* Returns the size of instances of a class.
*
* @param cls A class object.
*
* @return The size in bytes of instances of the class \e cls, or \c 0 if \e cls is \c Nil.
*/
OBJC_EXPORT size_t
class_getInstanceSize(Class _Nullable cls)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
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() const {
return word_align(unalignedInstanceSize());
}
static inline uint32_t word_align(uint32_t x) {
//x+7 & (~7) --> 8字節(jié)對齊
return (x + WORD_MASK) & ~WORD_MASK;
}
//其中 WORD_MASK 為
# define WORD_MASK 7UL
實際申請內(nèi)存時instanceSize源碼分析
size_t instanceSize(size_t extraBytes) const {
//編譯器快速計算內(nèi)存大小
if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
return cache.fastInstanceSize(extraBytes);
}
// 計算類中所有屬性的大小 + 額外的字節(jié)數(shù)0
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
//如果size 小于 16,最小取16
if (size < 16) size = 16;
return size;
}
所以為什么蘋果要按照16字節(jié)對齊呢?
- 通常內(nèi)存是由一個個字節(jié)組成的,cpu在存取數(shù)據(jù)時,并不是以字節(jié)為單位存儲,而是以塊為單位存取,塊的大小為內(nèi)存存取力度。頻繁存取字節(jié)未對齊的數(shù)據(jù),會極大降低cpu的性能,所以可以通過減少存取次數(shù)來降低cpu的開銷。
- 由于在一個對象中,第一個屬性isa占8字節(jié),當(dāng)然一個對象肯定還有其他屬性,當(dāng)無屬性時,會預(yù)留8字節(jié),即16字節(jié)對齊,如果不預(yù)留,相當(dāng)于這個對象的isa和其他對象的isa緊挨著,容易造成訪問混亂。
- 16字節(jié)對齊后,可以加快CPU讀取速度,同時使訪問更安全,不會產(chǎn)生訪問混亂的情況
以上就是我對對象內(nèi)存大小占用情況的簡單分析,歡迎各位大佬們點贊。

