計(jì)算內(nèi)存方法
首先我們要知道計(jì)算內(nèi)存大小的三種方式:
sizeof-
class_getInstanceSize; -
malloc_size。
接下來(lái)我們定義一個(gè)LGPerson類(lèi),分析這三種方法。代碼如下:
LGPerson * p = [LGPerson alloc];
LGPerson * q;
NSLog(@"對(duì)象類(lèi)型占用內(nèi)存大小=%lu",sizeof(p));
NSLog(@"對(duì)象類(lèi)型占用內(nèi)存大小=%lu",sizeof(q));
NSLog(@"對(duì)象實(shí)際內(nèi)存大小====%lu",class_getInstanceSize([p class]));
NSLog(@"對(duì)象實(shí)際內(nèi)存大小====%lu",class_getInstanceSize([q class]));
NSLog(@"對(duì)象實(shí)際分配內(nèi)存大小=%lu",malloc_size((__bridge const void *)(p)));
NSLog(@"對(duì)象實(shí)際分配內(nèi)存大小=%lu",malloc_size((__bridge const void *)(q)));
打印結(jié)果:
2020-09-29 14:02:17.810194+0800 KCObjc[20870:761876] 對(duì)象類(lèi)型占用內(nèi)存大小=8
2020-09-29 14:02:17.810897+0800 KCObjc[20870:761876] 對(duì)象類(lèi)型占用內(nèi)存大小=8
2020-09-29 14:02:17.811068+0800 KCObjc[20870:761876] 對(duì)象實(shí)際內(nèi)存大小====8
2020-09-29 14:02:17.811165+0800 KCObjc[20870:761876] 對(duì)象實(shí)際內(nèi)存大小====0
2020-09-29 14:02:17.811265+0800 KCObjc[20870:761876] 對(duì)象實(shí)際分配內(nèi)存大小=16
2020-09-29 14:02:17.811352+0800 KCObjc[20870:761876] 對(duì)象實(shí)際分配內(nèi)存大小=0
由打印結(jié)果可以分析出
-
sizeof()傳入是類(lèi)型,可以放基本數(shù)據(jù)類(lèi)型、對(duì)象、指針。可用來(lái)計(jì)算類(lèi)型占用內(nèi)存大小,這個(gè)在編譯器編譯階段就會(huì)確定,所以sizeof(p)和sizeof(q)的結(jié)果都是一樣的,p和q都是指針類(lèi)型,指針大小為8個(gè)字節(jié)。 -
class_getInstanceSize計(jì)算對(duì)象的實(shí)際內(nèi)存大小,大小由類(lèi)的屬性和變量來(lái)決定,實(shí)際上并不是嚴(yán)格意義上的對(duì)象內(nèi)存大小。由下面代碼可知,底層進(jìn)行8字節(jié)對(duì)齊。
# define WORD_MASK 7UL
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
LGPerson類(lèi)中沒(méi)有其他的屬性和變量,但是繼承了NSObject,NSObject中有一個(gè)isa指針,所以?xún)?nèi)存大小是8字節(jié)。
-
malloc_size系統(tǒng)分配的內(nèi)存大小,是按16字節(jié)對(duì)齊的方式,即是按16的倍數(shù)分配 ,不足則系統(tǒng)會(huì)自動(dòng)填充字節(jié)。
內(nèi)存對(duì)齊原則
每個(gè)特定平臺(tái)上的編譯器都有自己的默認(rèn)“對(duì)齊系數(shù)”(也叫對(duì)齊模數(shù))。程序員可以通過(guò)預(yù)編譯命令#pragma pack(n),n=1,2,4,8,16來(lái)改變這一系數(shù),其中的n就是你要指定的“對(duì)齊系數(shù)”。在iOS中,Xcode默認(rèn)為#pragma pack(8),即`8字節(jié)對(duì)齊。
內(nèi)存對(duì)齊原則主要有以下三點(diǎn):
-
數(shù)據(jù)成員對(duì)齊規(guī)則:struct(結(jié)構(gòu))或者union(聯(lián)合)的數(shù)據(jù)成員,第一個(gè)數(shù)據(jù)成員放在offset為0的地方,以后每個(gè)數(shù)據(jù)成員存儲(chǔ)的起始位置要從該成員大小或者成員的子成員大?。ㄖ灰摮蓡T有子成員,比如數(shù)據(jù)、結(jié)構(gòu)體等)的整數(shù)倍開(kāi)始(例如int在32位機(jī)中是4字節(jié),則要從4的整數(shù)倍地址開(kāi)始存儲(chǔ)) -
數(shù)據(jù)成員為結(jié)構(gòu)體:如果一個(gè)結(jié)構(gòu)里有某些結(jié)構(gòu)體成員,則結(jié)構(gòu)體成員要從其內(nèi)部最大元素大小的整數(shù)倍地址開(kāi)始存儲(chǔ)(例如:struct a里面存有struct b,b里面有char(1字節(jié))、int(4字節(jié))、double(8字節(jié))等元素,則b應(yīng)該從8的整數(shù)倍開(kāi)始存儲(chǔ)) -
結(jié)構(gòu)體的整體對(duì)齊規(guī)則:結(jié)構(gòu)體的總大小,即sizeof的結(jié)果,必須是其內(nèi)部做大成員的整數(shù)倍,不足的要補(bǔ)齊
下表是各種數(shù)據(jù)類(lèi)型在iOS中的占用內(nèi)存大小,根據(jù)對(duì)應(yīng)類(lèi)型來(lái)計(jì)算結(jié)構(gòu)體中內(nèi)存大小

結(jié)構(gòu)體對(duì)齊
如下代碼,我們用實(shí)例進(jìn)行探究結(jié)構(gòu)體對(duì)齊:
struct LGStruct1{
long a; // 8
int b; // 4
short c; // 2
char d; // 1
} LGStruct1;
struct LGStruct2{
long a; // 8
char d; // 1
int b; // 4
short c; // 2
} LGStruct2;
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"---%lu------%lu",sizeof(LGStruct1),sizeof(LGStruct2));
}
return 0;
}
打印結(jié)果
2020-09-29 15:52:17.811352+0800 KCObjc[20870:761876] -----16------24
由上述代碼可看出兩個(gè)結(jié)構(gòu)體定義的變量和變量類(lèi)型都一致,唯一的區(qū)別只是在于定義變量的順序不一致,那么為什么會(huì)占用的內(nèi)存大小不相等呢?其實(shí)這就是iOS中的內(nèi)存對(duì)齊原則。下面我們就根據(jù)內(nèi)存對(duì)齊原則來(lái)進(jìn)行簡(jiǎn)單的分析和計(jì)算LGStruct1內(nèi)存大小的詳細(xì)過(guò)程:
- 變量
a: 占8個(gè)字節(jié),從0開(kāi)始,min(0,8),即0 ~ 7存儲(chǔ)a - 變量
b: 占4個(gè)字節(jié),從8開(kāi)始,min(8,4),即8 ~ 11存儲(chǔ)b - 變量
c: 占2個(gè)字節(jié),從12開(kāi)始,min(12,2),即12~ 13存儲(chǔ)c - 變量
d: 占1個(gè)字節(jié),從14開(kāi)始,min(14,1),即14存儲(chǔ)d
因此LGStruct1的內(nèi)存大小是15字節(jié),而LGStruct1中最大的變量是a占8個(gè)字節(jié),所以LGStruct1需要實(shí)際內(nèi)存必須是8的倍數(shù)(內(nèi)存對(duì)齊原則),15字節(jié)不是8的倍數(shù),15向上取整到16 ,所以系統(tǒng)自動(dòng)填充成16字節(jié),最終sizeof(LGStruct1)的大小是16.

LGStruct2內(nèi)存大小的詳細(xì)過(guò)程
- 變量
a: 占8個(gè)字節(jié),從0開(kāi)始,min(0,8),即0 ~ 7存儲(chǔ)a - 變量
d: 占1個(gè)字節(jié),從8開(kāi)始,min(8,1),即8存儲(chǔ)d - 變量
b: 占4個(gè)字節(jié),從9開(kāi)始,min(9,4),9 % 4 != 0,繼續(xù)往后移動(dòng)直到找到可以整除4的位置12,min(12,4),即12 ~ 15存儲(chǔ)b - 變量
c: 占2個(gè)字節(jié),從16開(kāi)始,min(16,2),即16 ~ 17存儲(chǔ)c
因此LGStruct2的需要的內(nèi)存大小為18字節(jié),而LGStruct2中最大變量long的字節(jié)數(shù)為8,所以LGStruct2實(shí)際的內(nèi)存大小必須是8的整數(shù)倍,18向上取整到24,主要是因?yàn)?4是8的整數(shù)倍,所以 sizeof(LGStruct2) 的結(jié)果是24
LGStruct2內(nèi)存中的存儲(chǔ)情況圖
結(jié)構(gòu)體嵌套結(jié)構(gòu)體
上面的2個(gè)示例只是簡(jiǎn)單的定義數(shù)據(jù)成員,如果我們?cè)诮Y(jié)構(gòu)體中嵌套結(jié)構(gòu)體結(jié)果又會(huì)是怎樣的?我們繼續(xù)探究,看下面代碼:
struct LGStruct3{
long a; // 8
int b; // 4
short c; // 2
char d; // 1
struct LGStruct1 Str;
}LGStruct3;
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"LGStruct1----%lu",sizeof(LGStruct1));
NSLog(@"LGStruct2----%lu",sizeof(LGStruct2));
NSLog(@"LGStruct3----%lu",sizeof(LGStruct3));
}
return 0;
}
//結(jié)果
2020-09-30 11:02:46.509957+0800 001-內(nèi)存對(duì)齊原則[22800:939799] LGStruct1----16
2020-09-30 11:02:46.511196+0800 001-內(nèi)存對(duì)齊原則[22800:939799] LGStruct2----24
2020-09-30 11:02:46.512537+0800 001-內(nèi)存對(duì)齊原則[22800:939799] LGStruct3----32
LGStruct3內(nèi)存大小存儲(chǔ)情況的詳細(xì)過(guò)程
- 變量
a: 占8個(gè)字節(jié),從0開(kāi)始,min(0,8),即0 ~ 7存儲(chǔ)a - 變量
b: 占4個(gè)字節(jié),從8開(kāi)始,min(8,4),即8 ~ 11存儲(chǔ)b - 變量
c: 占2個(gè)字節(jié),從12開(kāi)始,min(12,2),即12~ 13存儲(chǔ)b - 變量
d: 占1個(gè)字節(jié),從14開(kāi)始,min(14,1),即14存儲(chǔ)d - 變量
Str: 結(jié)構(gòu)體變量Str,根據(jù)內(nèi)存對(duì)齊原則結(jié)構(gòu)體成員要從其內(nèi)部最大元素大小的整數(shù)倍地址開(kāi)始存儲(chǔ),LGStruct1中最大的變量是long 8字節(jié),所以Str從16位置開(kāi)始存儲(chǔ),而Str的為15字節(jié),即LGStruct1存儲(chǔ)16-31位置
因此LGStruct3的內(nèi)存大小是32字節(jié),而LGStruct1中最大變量為Str,其最大成員內(nèi)存字節(jié)數(shù)為8,所以LGStruct3內(nèi)存必須是8的倍數(shù),32是8的倍數(shù),最終sizeof(LGStruct3)的大小是32
其內(nèi)存存儲(chǔ)情況如下圖所示

內(nèi)存優(yōu)化(屬性重排)
從上述的示例中,我們可以得出一個(gè)結(jié)論即結(jié)構(gòu)體的內(nèi)存大小與結(jié)構(gòu)體成員內(nèi)存大小的順序有關(guān)
- 若
結(jié)構(gòu)體數(shù)據(jù)成員是由內(nèi)存從小到大的順序定義的,根據(jù)內(nèi)存對(duì)齊原則來(lái)計(jì)算內(nèi)存大小,需要增加較多的內(nèi)存占位符,這樣做浪費(fèi)內(nèi)存。 - 若
結(jié)構(gòu)體數(shù)據(jù)成員是由內(nèi)存從大到小的順序定義的,根據(jù)內(nèi)存對(duì)齊規(guī)則來(lái)計(jì)算結(jié)構(gòu)體內(nèi)存大小,我們只需要補(bǔ)齊少量?jī)?nèi)存占位符即可滿(mǎn)足內(nèi)存對(duì)齊規(guī)則。
第二種方式就是蘋(píng)果采用的將類(lèi)中的屬性進(jìn)行重排,來(lái)達(dá)到優(yōu)化內(nèi)存的目的。以下面這個(gè)示例來(lái)進(jìn)行說(shuō)明蘋(píng)果中屬性重排,即內(nèi)存優(yōu)化:
- 自定義
LGPerson類(lèi),并定義幾個(gè)屬性
//LGPerson.h
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
// @property (nonatomic, copy) NSString *hobby;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) long height;
@property (nonatomic) char c1;
@property (nonatomic) char c2;
@end
//LGPerson.m
@implementation LGPerson
@end
- 在
main中創(chuàng)建LGPerson的實(shí)例對(duì)象,并對(duì)其屬性賦值
int main(int argc, char * argv[]) {
@autoreleasepool {
LGPerson *person = [LGPerson alloc];
person.name = @"Cooci";
person.nickName = @"KC";
person.age = 18;
person.c1 = 'a';
person.c2 = 'b';
NSLog(@"%@",person);
}
return 0;
}
-
斷點(diǎn)調(diào)試person,根據(jù)LGPerson的對(duì)象地址,查找出屬性的值
- 通過(guò)地址找出
name&nickName
image.png
- 通過(guò)地址找出
-
通過(guò)
0x0000001200006261地址找出age等數(shù)據(jù)時(shí),發(fā)現(xiàn)無(wú)法找出age等數(shù)據(jù)值,這是因?yàn)?code>蘋(píng)果中針對(duì)age、c1、c2屬性的內(nèi)存進(jìn)行了重排,將他們存儲(chǔ)在同一塊內(nèi)存中,-
age通過(guò)0x00000012讀取 -
c1通過(guò)0x61讀取(a的ASCII碼是97) -
c2通過(guò)0x62讀?。╞的ASCII碼是98)
image.png
-
特殊的
double和float
我們嘗試把LGPerson中的height屬性類(lèi)型修改為double,并賦值
@property (nonatomic, assign) double height;
//賦值身高
person.height = 178;

我們發(fā)現(xiàn)直接po打印
0x4066400000000000,打印不出height的數(shù)值178。 這是因?yàn)榫幾g器po打印默認(rèn)當(dāng)做int類(lèi)型處理。p/x (double)178:我們以16進(jìn)制打印double類(lèi)型值打印,發(fā)現(xiàn)完全相同。
- 綜上總結(jié)蘋(píng)果中的內(nèi)存對(duì)齊思想:
- 大部分的內(nèi)存都是通過(guò)固定的內(nèi)存塊進(jìn)行讀取。
- 盡管我們?cè)趦?nèi)存中采用了內(nèi)存對(duì)齊的方式,但并不是所有的內(nèi)存都可以進(jìn)行浪費(fèi)的,蘋(píng)果會(huì)自動(dòng)對(duì)
屬性進(jìn)行重排,以此來(lái)優(yōu)化內(nèi)存.
8字節(jié)對(duì)齊與16字節(jié)對(duì)齊
前面我們提及了8字節(jié)對(duì)齊和16字節(jié)對(duì)齊,這時(shí)我們就有疑問(wèn),什么時(shí)候在哪里采用哪種字節(jié)對(duì)齊,接下來(lái)我們繼續(xù)源碼探索
- 我們?cè)趏bjc4源碼中搜索
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);
在objc-class.mm可以找到:
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
進(jìn)入alignedInstanceSize:
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
進(jìn)入word_align:
#ifdef __LP64__ // 64位操作系統(tǒng)
# define WORD_SHIFT 3UL
# define WORD_MASK 7UL // 7字節(jié)遮罩
# define WORD_BITS 64
#else
# define WORD_SHIFT 2UL
# define WORD_MASK 3UL
# define WORD_BITS 32
#endif
static inline uint32_t word_align(uint32_t x) {
// (x + 7) & (~7) --> 8字節(jié)對(duì)齊
return (x + WORD_MASK) & ~WORD_MASK;
}
可以看到:
- 系統(tǒng)內(nèi)部設(shè)定64位操作系統(tǒng),統(tǒng)一使用
8字節(jié)對(duì)齊。對(duì)于一個(gè)對(duì)象來(lái)說(shuō),其真正的對(duì)齊方式是8字節(jié)對(duì)齊。 - 因外部處理對(duì)象太多,系統(tǒng)為了防止一些容錯(cuò),會(huì)采用
align16為內(nèi)存塊來(lái)存取,主要是因?yàn)椴捎?字節(jié)對(duì)齊時(shí),兩個(gè)對(duì)象的內(nèi)存會(huì)緊挨著,顯得比較緊湊,而16字節(jié)比較寬松,避免越界訪問(wèn),提高效率,利于蘋(píng)果以后的擴(kuò)展。
16字節(jié)內(nèi)存對(duì)齊算法
目前已知的16字節(jié)內(nèi)存對(duì)齊算法有兩種
-
alloc源碼分析中的align16
static inline size_t align16(size_t x) {
return (x + size_t(15)) & ~size_t(15);
}
-
malloc源碼分析中的segregated_size_to_fit
#define SHIFT_NANO_QUANTUM 4
#define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM) // 16
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 + 15 >> 4 << 4 ,其中右移4 + 左移4相當(dāng)于將后4位抹零,跟 k/16 * 16一樣 ,是16字節(jié)對(duì)齊算法,小于16就成0了
以 k = 2為例,如下圖所示

為什么需要16字節(jié)對(duì)齊
原因有一下幾點(diǎn):
- 通常內(nèi)存是由一個(gè)個(gè)
字節(jié)組成的,cpu在存取數(shù)據(jù)時(shí),并不是以字節(jié)為單位存儲(chǔ),而是以塊為單位存取,塊的大小為內(nèi)存存取力度。頻繁存取字節(jié)未對(duì)齊的數(shù)據(jù),會(huì)極大降低cpu的性能,所以可以通過(guò)減少存取次數(shù)來(lái)降低cpu的開(kāi)銷(xiāo),同時(shí)使訪問(wèn)更安全,不會(huì)產(chǎn)生訪問(wèn)混亂的情況。 - 16字節(jié)對(duì)齊,是由于在一個(gè)對(duì)象中,第一個(gè)屬性
isa占8字節(jié),當(dāng)然一個(gè)對(duì)象肯定還有其他屬性,當(dāng)無(wú)屬性時(shí),會(huì)預(yù)留8字節(jié),即16字節(jié)對(duì)齊,如果不預(yù)留,相當(dāng)于這個(gè)對(duì)象的isa和其他對(duì)象的isa緊挨著,容易造成訪問(wèn)混亂。
總結(jié)
綜合前文提及的獲取內(nèi)存大小的方式
-
class_getInstanceSize:是采用8字節(jié)對(duì)齊,參照的對(duì)象的屬性?xún)?nèi)存大小 -
malloc_size:采用16字節(jié)對(duì)齊,參照的整個(gè)對(duì)象的內(nèi)存大小,對(duì)象實(shí)際分配的內(nèi)存大小必須是16的整數(shù)倍


