獲取內(nèi)存大小的三種方式
- sizeof
- class_getInstanceSize
- malloc_size
sizeof
sizeof
是一個操作符,不是函數(shù)稼跳,一把用于計算內(nèi)存大小鸟雏。傳入的主要對象是數(shù)據(jù)類型(基本數(shù)據(jù)類型物臂、對象瘟滨、指針)
锯蛀,這個在編譯器的編譯階段
就會確定大小而不是在運行時。sizeof
最終得到的結(jié)果是該數(shù)據(jù)類型
占用空間的大小
class_getInstanceSize
這個方法在OC底層原理02 - alloc & init & new 源碼分析分析時就已經(jīng)分析了馅精,是runtime
提供的api严嗜,用于獲取類的實例對象所占用的內(nèi)存大小
,并返回具體的字節(jié)數(shù)洲敢,其本質(zhì)就是獲取實例對象中成員變量的內(nèi)存大小
malloc_size
這個函數(shù)是獲取系統(tǒng)實際分配的內(nèi)存大小
我們通過運行以下代碼驗證上述所說
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *objc = [[NSObject alloc] init];
NSLog(@"objc對象類型占用的內(nèi)存大新:%lu", sizeof(objc));
NSLog(@"objc對象實際占用的內(nèi)存大小:%lu", class_getInstanceSize([objc class]));
NSLog(@"objc對象實際分配的內(nèi)存大新偌病:%lu", malloc_size((__bridge const void*)(objc)));
}
return 0;
}
運行結(jié)果如下總結(jié)
sizeof
- 計算
類型占用的內(nèi)存大小
称近,其中可以放基本數(shù)據(jù)類型
、對象
哮塞、指針
- 對于類似于
int
這樣的基本數(shù)據(jù)
而言刨秆,sizeof
獲取的就是數(shù)據(jù)類型占用的內(nèi)存大小
,不同的數(shù)據(jù)類型所占用的內(nèi)存大小是不一樣的 - 而對于類似于
NSObject
定義的實例對象
而言忆畅,其對象類型
的本質(zhì)就是一個結(jié)構(gòu)體(即 struct objc_object)的指針
衡未,所以sizeof(objc)
打印的是對象objc的指針大小
,我們知道一個指針的內(nèi)存大小是8
家凯,所以sizeof(objc) 打印是 8
- 對于
指針
而言缓醋,sizeof
打印的就是本身的內(nèi)存大小8
class_getInstanceSize
計算對象實際占用的內(nèi)存大小
,這個需要依據(jù)類的屬性而變化
绊诲,如果自定義類沒有自定義屬性送粱,僅僅只是繼承自NSObject
,則類的實例對象實際占用的內(nèi)存大小是8
malloc_size
計算對象實際分配的內(nèi)存大小
掂之,這個是由系統(tǒng)完成的抗俄,可以從上面的打印結(jié)果看出,實際分配的和實際占用的內(nèi)存大小并不相等世舰,這個問題可以通過OC底層原理02 - alloc & init & new 源碼分析中的16字節(jié)對齊算法
來解釋這個問題
結(jié)構(gòu)體內(nèi)存對齊
首先我們定義兩個結(jié)構(gòu)體动雹,分別計算他們的內(nèi)存大小。
struct HLStruct1 {
double a;
char b;
int c;
short d;
}struct1;
struct HLStruct2 {
double a;
int b;
char c;
short d;
}struct2;
//計算 結(jié)構(gòu)體占用的內(nèi)存大小
NSLog(@"%lu-%lu", sizeof(HLstruct1), sizeof(HLstruct2));
輸出結(jié)果如下
變量
和變量類型
都是一致的胰蝠,唯一的不同是定義變量的順序不一致
,但是他們所占用的內(nèi)存大小不相等
卻不相同震蒋。這就是iOS中的內(nèi)存字節(jié)對齊
內(nèi)存對齊規(guī)則
每個特定平臺上的編譯器都有自己的默認“對齊系數(shù)”(也叫對齊模數(shù))
茸塞。程序員可以通過預編譯命令#pragma pack(n)
,n = 1, 2, 4, 8, 16來改變這一系數(shù)喷好,其中的n
就是你要指定的“對齊系數(shù)”
翔横。在iOS中,Xcode默認為#pragma pack(8)
梗搅,即8字節(jié)對齊
內(nèi)存字節(jié)對齊原則
數(shù)據(jù)成員對齊規(guī)則
:struct 或者 union 的數(shù)據(jù)成員禾唁,第一個數(shù)據(jù)成員放在offset為0的地方效览,以后每個數(shù)據(jù)成員存儲的起始位置要從該成員大小或者成員的子成員大小(只要該成員有子成員荡短,比如數(shù)據(jù)丐枉、結(jié)構(gòu)體等)的整數(shù)倍開始(例如int在32位機中是4字節(jié),則要從4的整數(shù)倍地址開始存儲)
數(shù)據(jù)成員為結(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)體的整體對齊規(guī)則
:結(jié)構(gòu)體的總大小,即sizeof的結(jié)果泪掀,必須是其內(nèi)部最大成員的整數(shù)倍听绳,不足的要補齊
分析
下表是各數(shù)據(jù)類型在C和OC中所占內(nèi)存大小
HLStruct1
和HLStruct2
的內(nèi)存結(jié)構(gòu)示意圖計算過程詳解:
HLStruct1:
-
變量a
:占8
個字節(jié)异赫,從0開始椅挣,即0-7字節(jié)存儲變量a
; -
變量b
:占1
個字節(jié)塔拳,從8開始鼠证,即8字節(jié)儲存變量b
-
變量c
:占4
個字節(jié),9不能整除4靠抑,故往后順移量九,直到12能整除4,所以從12開始颂碧,即12-15字節(jié)儲存變量c
-
變量d
:占2
個字節(jié)娩鹉,從16開始,即16-17字節(jié)儲存變量d
因此HLStruct1
的需要的內(nèi)存大小為18
字節(jié)稚伍,而HLStruct1
中最大變量
的字節(jié)數(shù)為8
,HLStruct1
實際的內(nèi)存大小必須是8的整數(shù)倍
戚宦,18
向上取整到24
个曙,所以sizeof(HLStruct1)
的結(jié)果是24
HLStruct2: -
變量a
:占8
個字節(jié),從0開始受楼,即0-7字節(jié)存儲變量a
垦搬; -
變量b
:占4
個字節(jié),從8開始艳汽,即8-11字節(jié)儲存變量b
-
變量c
:占1
個字節(jié)猴贰,從12開始,即12字節(jié)儲存變量c
-
變量d
:占2
個字節(jié)河狐,13不能整除2米绕,故往后順移瑟捣,直到14能整除2,所以從14開始栅干,即14-15字節(jié)儲存變量d
因此HLStruct2
的需要的內(nèi)存大小為16
字節(jié)迈套,而HLStruct2
中最大變量
的字節(jié)數(shù)為8
,HLStruct2
實際的內(nèi)存大小必須是8的整數(shù)倍
碱鳞,16
剛好為8
的倍數(shù)桑李,所以sizeof(HLStruct2)
的結(jié)果是16
結(jié)構(gòu)體嵌套結(jié)構(gòu)體
上面的兩個結(jié)構(gòu)體只是簡單的定義數(shù)據(jù)成員,下面來一個比較復雜的窿给,結(jié)構(gòu)體中嵌套結(jié)構(gòu)體的內(nèi)存大小計算情況
定義一個結(jié)構(gòu)體HLStruct3贵白,在HLStruct3中嵌套HLStruct2,如下所示
struct HLStruct3 {
double a;
int b;
char c;
short d;
int e;
struct HLStruct2 f;
}struct3;
輸出
NSLog(@"%lu-%lu", sizeof(struct3), sizeof(struct3.f));
輸出結(jié)果如下
struct3內(nèi)存計算
-
變量a
:占8
個字節(jié)崩泡,從0開始禁荒,即0-7字節(jié)存儲變量a
; -
變量b
:占4
個字節(jié)允华,從8開始圈浇,即8-11字節(jié)儲存變量b
-
變量c
:占1
個字節(jié),從12開始靴寂,即12字節(jié)儲存變量c
-
變量d
:占2
個字節(jié)磷蜀,13不能整除2,故往后順移百炬,直到14能整除2褐隆,所以從14開始,即14-15字節(jié)儲存變量d
-
變量e
:占4
個字節(jié)剖踊,從16開始庶弃,即16-19字節(jié)儲存變量e
-
變量f
:占16
個字節(jié),f
是一個結(jié)構(gòu)體
德澈,根據(jù)內(nèi)存對齊原則二歇攻,結(jié)構(gòu)體成員要從其內(nèi)部最大成員大小的整數(shù)倍
開始存儲,而HLStruct2
中最大的成員大小為8
梆造,所以f
要從8
的整數(shù)倍開始缴守,當前是從20開始,所以不符合要求镇辉,需要往后移動到24屡穗,24是8的整數(shù)倍,符合內(nèi)存對齊原則忽肛,即24-39存儲變量f
因此HLStruct3
的需要的內(nèi)存大小為40
字節(jié)村砂,而HLStruct3
中最大變量
的字節(jié)數(shù)為8
,HLStruct3
實際的內(nèi)存大小必須是8的整數(shù)倍
屹逛,40
剛好為8
的倍數(shù)础废,所以sizeof(HLStruct3)
的結(jié)果是40
image.png
二次驗證
在定義一個結(jié)構(gòu)體汛骂,如下所示
struct HLStruct4 {
short a;
double b;
}struct4;
struct HLStruct5 {
char a;
int b;
struct HLStruct4 c;
}struct5;
HLStruct4內(nèi)存計算
-
變量a
:占2
個字節(jié),從0開始色迂,即0-2字節(jié)存儲變量a
香缺; -
變量b
:占8
個字節(jié),3不能整除8歇僧,故往后順移图张,直到8能整除8,所以從8開始诈悍,即8-15字節(jié)存儲變量b
祸轮;
因此HLStruct4
的需要的內(nèi)存大小為16
字節(jié),而HLStruct4
中最大變量
的字節(jié)數(shù)為8
侥钳,HLStruct4
實際的內(nèi)存大小必須是8的整數(shù)倍
适袜,16
剛好為8
的倍數(shù),所以sizeof(HLStruct4)
的結(jié)果是16
HLStruct5內(nèi)存計算 -
變量a
:占1
個字節(jié)舷夺,從0開始苦酱,即0字節(jié)存儲變量a
; -
變量b
:占2
個字節(jié)给猾,1不能整除2疫萤,故往后順移,直到2能整除2敢伸,所以從2開始扯饶,即2-3字節(jié)存儲變量b
; -
變量c
:占16
個字節(jié)池颈,HLStruct4
中最大的成員大小為8
尾序,所以c
要從8
的整數(shù)倍開始多矮,當前是從4開始潜必,所以不符合要求,需要往后移動到8嗡害,8是8的整數(shù)倍琢歇,符合內(nèi)存對齊原則脯爪,即8-23存儲變量c
因此HLStruct5
的需要的內(nèi)存大小為24
字節(jié),而HLStruct4
中最大變量
的字節(jié)數(shù)為8
矿微,HLStruct5
實際的內(nèi)存大小必須是8的整數(shù)倍
,24
剛好為8
的倍數(shù)尚揣,所以sizeof(HLStruct5)
的結(jié)果是24
下圖為輸出及打印image.png
內(nèi)存優(yōu)化(屬性重排)
根據(jù)內(nèi)存對齊原則涌矢,HLStruct1
補齊了9
個字節(jié),而HLStruct2
只補齊1
個字節(jié)即可滿足該規(guī)則快骗,因此得出一個結(jié)論結(jié)構(gòu)體內(nèi)存大小與結(jié)構(gòu)體成員內(nèi)存大小的順序有關(guān)
娜庇。
創(chuàng)建一個對象來探索
- 首先定義一個自定義類HLPerson類塔次,并添加幾個屬性
@interface HLPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) long height;
@property (nonatomic) char c1;
@property (nonatomic) char c2;
@end
- 在main中創(chuàng)建HLPerson的實例對象,并對其屬性賦值
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
HLPerson *person = [HLPerson alloc];
person.name = @"HL";
person.nickName = @"Lay";
person.age = 17;
person.height = 180;
person.c1 = 'a';
person.c2 = 'b';
NSLog(@"%@", person);
}
return 0;
}
- 斷點調(diào)試person
-
x person
:x
是memory read
的簡寫名秀,讀取內(nèi)存信息
tips:iOS是小端模式励负,所以內(nèi)存的讀取要反著讀,即cd 83 00 00 01 80 1d 00
應(yīng)該讀取為0x001d8001000083cd
image.png -
x/8gx person
:以16進制打印8行內(nèi)存信息匕得,并分別打印其指向继榆,此方法讀取地址更為便捷
image.png
這里雖然打印了8行內(nèi)存信息,但實際上person
對象變量并沒有使用這么多內(nèi)存汁掠,可以通過class_getInstanceSize
方法獲取實際上該對象只占用了40
字節(jié)的內(nèi)存略吨,也就是上圖中前五段內(nèi)存,所以后三段全都為0x00000000考阱,但是有幾個屬性值卻并沒有找到翠忠。
分析
沒有找到age
、c1
及c2
對應(yīng)的值乞榨,是不是蘋果做了什么處理避免內(nèi)存過度消耗秽之,我們用沒有正常輸出信息的內(nèi)存嘗試解析下image.png
結(jié)論
name
、nickname
吃既、height
都是各自占用8
字節(jié)考榨。可以直接打印出來态秧;而age
是Int
占用4
字節(jié)董虱,c1
和c2
是char
,各自占用1
字節(jié)申鱼。我們推測系統(tǒng)可能進行屬性重排愤诱,將他們存放在了一個塊區(qū)。
下圖是HLPerson的內(nèi)存分布情況
image.png
特殊的double
和float
把height
屬性類型修改為double
//@property (nonatomic, assign) long height;
@property (nonatomic, assign) double height;
重新運行
直接po打印0x4066800000000000捐友,并不能正確輸出變量
height
的值淫半,這是因為編譯器po打印默認當做int類型處理
。
-
p/x (double)180
:將180轉(zhuǎn)成double
類型然后以16進制進行打印匣砖,發(fā)現(xiàn)地址完全一樣科吭。
image.pngheight
改成float
類型也可以用p/x (float)180
驗證
封裝2個驗證函數(shù)
// float轉(zhuǎn)換為16進制
void hl_float2HEX(float f) {
union uuf { float f; char s[4]; } uf;
uf.f = f;
printf("0x");
for (int i = 3; i >= 0; i--) {
printf("%02x", 0xff & uf.s[i]);
}
printf("\n");
}
// double轉(zhuǎn)換為16進制
void hl_double2HEX(double d) {
union uud { double d; char s[8]; } ud;
ud.d = d;
printf("0x");
for (int i = 7; i >= 0; i--) {
printf("%02x", 0xff & ud.s[i]);
}
printf("\n");
}
打印驗證
字節(jié)對齊到底采用多少字節(jié)對齊?
在objc4
源碼中搜索class_getInstanceSize
猴鲫,可以在runtime.h
找到:
/**
* 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) {
return (x + WORD_MASK) & ~WORD_MASK;
}
// 其中 WORD_MASK 為
# define WORD_MASK 7UL
通過源碼可以看出对人,對于一個對象
來說,其真正的對齊方式是8字節(jié)對齊
拂共,8字節(jié)對齊已經(jīng)足夠滿足對象的需求了
總結(jié)
class_getInstanceSize
:是采用8字節(jié)對齊
牺弄,參照的對象的屬性內(nèi)存大小
malloc_size
:采用16字節(jié)對齊
,參照的整個對象的內(nèi)存大小宜狐,對象實際分配的內(nèi)存大小必須是16的整數(shù)倍
內(nèi)存對齊算法
至此势告,我們已知的16字節(jié)對齊算法有兩種
-
alloc
源碼分析中的align16
-
malloc
源碼分析中的segregated_size_to_fit
align16
static inline size_t align16(size_t x) {
return (x + size_t(15)) & ~size_t(15);
}
segregated_size_to_fit
static MALLOC_INLINE size_t
segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
{
size_t k, slot_bytes;
if (0 == size) {
size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
}
k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
slot_bytes = k << SHIFT_NANO_QUANTUM; // multiply by power of two quanta size
*pKey = k - 1; // Zero-based!
return slot_bytes;
}
算法原理:k + (16 - 1) >> 4 << 4 蛇捌,其中右移4+左移4
相當于將后4位抹零
,跟k / 16 * 16
一樣 咱台,小于16就成0了