計(jì)算內(nèi)存方法
首先我們要知道計(jì)算內(nèi)存大小的三種方式:
sizeof
-
class_getInstanceSize
; -
malloc_size
缀拭。
接下來我們定義一個LGPerson
類智厌,分析這三種方法盲赊。代碼如下:
LGPerson * p = [LGPerson alloc];
LGPerson * q;
NSLog(@"對象類型占用內(nèi)存大小=%lu",sizeof(p));
NSLog(@"對象類型占用內(nèi)存大小=%lu",sizeof(q));
NSLog(@"對象實(shí)際內(nèi)存大小====%lu",class_getInstanceSize([p class]));
NSLog(@"對象實(shí)際內(nèi)存大小====%lu",class_getInstanceSize([q class]));
NSLog(@"對象實(shí)際分配內(nèi)存大小=%lu",malloc_size((__bridge const void *)(p)));
NSLog(@"對象實(shí)際分配內(nèi)存大小=%lu",malloc_size((__bridge const void *)(q)));
打印結(jié)果:
2020-09-29 14:02:17.810194+0800 KCObjc[20870:761876] 對象類型占用內(nèi)存大小=8
2020-09-29 14:02:17.810897+0800 KCObjc[20870:761876] 對象類型占用內(nèi)存大小=8
2020-09-29 14:02:17.811068+0800 KCObjc[20870:761876] 對象實(shí)際內(nèi)存大小====8
2020-09-29 14:02:17.811165+0800 KCObjc[20870:761876] 對象實(shí)際內(nèi)存大小====0
2020-09-29 14:02:17.811265+0800 KCObjc[20870:761876] 對象實(shí)際分配內(nèi)存大小=16
2020-09-29 14:02:17.811352+0800 KCObjc[20870:761876] 對象實(shí)際分配內(nèi)存大小=0
由打印結(jié)果可以分析出
-
sizeof()
傳入是類型诚卸,可以放基本數(shù)據(jù)類型绘迁、對象缀台、指針
√鸥可用來計(jì)算類型占用內(nèi)存大小
,這個在編譯器編譯階段
就會確定辩涝,所以sizeof(p)
和sizeof(q)
的結(jié)果都是一樣的勘天,p和q都是指針類型捉邢,指針大小為8個字節(jié)伏伐。 -
class_getInstanceSize
計(jì)算對象的實(shí)際內(nèi)存大小
翘狱,大小由類的屬性和變量
來決定,實(shí)際上并不是嚴(yán)格意義上的對象內(nèi)存大小。由下面代碼可知茬缩,底層進(jìn)行8字節(jié)對齊
凰锡。
# define WORD_MASK 7UL
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
LGPerson類中沒有其他的屬性和變量圈暗,但是繼承了NSObject
员串,NSObject中有一個isa
指針寸齐,所以內(nèi)存大小是8字節(jié)。
-
malloc_size
系統(tǒng)分配的內(nèi)存大小扰法,是按16字節(jié)對齊
的方式塞颁,即是按16的倍數(shù)
分配 祠锣,不足則系統(tǒng)會自動填充字節(jié)珍语。
內(nèi)存對齊原則
每個特定平臺上的編譯器都有自己的默認(rèn)“對齊系數(shù)”(也叫對齊模數(shù))板乙。程序員可以通過預(yù)編譯命令#pragma pack(n)
拳氢,n=1,2,4,8,16來改變這一系數(shù)馋评,其中的n就是你要指定的“對齊系數(shù)”
留特。在iOS中蜕青,Xcode默認(rèn)為#pragma pack(8)
糊渊,即`8字節(jié)對齊渺绒。
內(nèi)存對齊原則主要有以下三點(diǎn):
-
數(shù)據(jù)成員對齊規(guī)則
:struct(結(jié)構(gòu))
或者union(聯(lián)合)
的數(shù)據(jù)成員宗兼,第一個數(shù)據(jù)成員放在offset為0
的地方殷绍,以后每個數(shù)據(jù)成員存儲的起始位置要從該成員大小或者成員的子成員大小(只要該成員有子成員殖侵,比如數(shù)據(jù)拢军、結(jié)構(gòu)體等)的整數(shù)倍開始(例如int在32位機(jī)中是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(1字節(jié))、int(4字節(jié))蹬蚁、double(8字節(jié))
等元素,則b應(yīng)該從8的整數(shù)倍開始存儲) -
結(jié)構(gòu)體的整體對齊規(guī)則
:結(jié)構(gòu)體的總大小贝乎,即sizeof
的結(jié)果览效,必須是其內(nèi)部做大成員的整數(shù)倍锤灿,不足的要補(bǔ)齊
下表是各種數(shù)據(jù)類型在iOS中的占用內(nèi)存大小,根據(jù)對應(yīng)類型來計(jì)算結(jié)構(gòu)體中內(nèi)存大小
結(jié)構(gòu)體對齊
如下代碼衡招,我們用實(shí)例進(jìn)行探究結(jié)構(gòu)體對齊:
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
由上述代碼可看出兩個結(jié)構(gòu)體定義的變量和變量類型都一致,唯一
的區(qū)別只是在于定義變量的順序不一致
空执,那么為什么會占用的內(nèi)存大小不相等
呢穗椅?其實(shí)這就是iOS中的內(nèi)存對齊原則
匹表。下面我們就根據(jù)內(nèi)存對齊原則
來進(jìn)行簡單的分析和計(jì)算LGStruct1
內(nèi)存大小的詳細(xì)過程:
- 變量
a
: 占8
個字節(jié)默蚌,從0
開始苇羡,min(0设江,8)
叉存,即0 ~ 7
存儲a
- 變量
b
: 占4
個字節(jié)歼捏,從8
開始笨篷,min(8冕屯,4)
安聘,即8 ~ 11
存儲b
- 變量
c
: 占2
個字節(jié)浴韭,從12
開始脯宿,min(12,2)
榴芳,即12~ 13
存儲c
- 變量
d
: 占1
個字節(jié)窟感,從14
開始歉井,min(14,1)
躏嚎,即14
存儲d
因此LGStruct1
的內(nèi)存大小是15字節(jié)卢佣,而LGStruct1
中最大的變量是a
占8個字節(jié)
菜谣,所以LGStruct1
需要實(shí)際內(nèi)存必須是8的倍數(shù)(內(nèi)存對齊原則)
珠漂,15
字節(jié)不是8
的倍數(shù),15
向上取整到16
尾膊,所以系統(tǒng)自動填充成16字節(jié)
媳危,最終sizeof(LGStruct1)的大小是16
.
LGStruct2
內(nèi)存大小的詳細(xì)過程
- 變量
a
: 占8
個字節(jié),從0
開始冈敛,min(0待笑,8)
,即0 ~ 7
存儲a
- 變量
d
: 占1
個字節(jié)抓谴,從8
開始暮蹂,min(8荆陆,1)
,即8
存儲d
- 變量
b
: 占4
個字節(jié)浓体,從9
開始,min(9生闲,4)
飞醉,9 % 4 != 0
,繼續(xù)往后移動直到找到可以整除4
的位置12
,min(12失暂,4)
,即12 ~ 15
存儲b
- 變量
c
: 占2
個字節(jié),從16
開始系宫,min(16椒惨,2)
,即16 ~ 17
存儲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)存中的存儲情況圖
結(jié)構(gòu)體嵌套結(jié)構(gòu)體
上面的2個示例只是簡單的定義數(shù)據(jù)成員碗短,如果我們在結(jié)構(gòu)體中嵌套結(jié)構(gòu)體結(jié)果又會是怎樣的?我們繼續(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)存對齊原則[22800:939799] LGStruct1----16
2020-09-30 11:02:46.511196+0800 001-內(nèi)存對齊原則[22800:939799] LGStruct2----24
2020-09-30 11:02:46.512537+0800 001-內(nèi)存對齊原則[22800:939799] LGStruct3----32
LGStruct3
內(nèi)存大小存儲情況的詳細(xì)過程
- 變量
a
: 占8
個字節(jié)炕舵,從0
開始,min(0奸攻,8)
部翘,即0 ~ 7
存儲a
- 變量
b
: 占4
個字節(jié),從8
開始,min(8假哎,4)
,即8 ~ 11
存儲b
- 變量
c
: 占2
個字節(jié)秆剪,從12
開始仅讽,min(12,2)
苫费,即12~ 13
存儲b
- 變量
d
: 占1
個字節(jié)牍汹,從14
開始嫁蛇,min(14闸拿,1)
书幕,即14
存儲d
- 變量
Str
: 結(jié)構(gòu)體變量Str
苟呐,根據(jù)內(nèi)存對齊原則結(jié)構(gòu)體成員要從其內(nèi)部最大元素大小的整數(shù)倍
地址開始存儲笆呆,LGStruct1
中最大的變量是long 8字節(jié)
榕堰,所以Str
從16
位置開始存儲踱讨,而Str
的為15
字節(jié)味混,即LGStruct1
存儲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)存存儲情況如下圖所示
內(nèi)存優(yōu)化(屬性重排)
從上述的示例中绊袋,我們可以得出一個結(jié)論即結(jié)構(gòu)體的內(nèi)存大小與結(jié)構(gòu)體成員內(nèi)存大小的順序有關(guān)
- 若
結(jié)構(gòu)體數(shù)據(jù)成員
是由內(nèi)存從小到大
的順序定義的,根據(jù)內(nèi)存對齊原則來計(jì)算內(nèi)存大小蹋笼,需要增加較多的內(nèi)存占位符胶滋,這樣做浪費(fèi)內(nèi)存。 - 若
結(jié)構(gòu)體數(shù)據(jù)成員
是由內(nèi)存從大到小
的順序定義的,根據(jù)內(nèi)存對齊規(guī)則來計(jì)算結(jié)構(gòu)體內(nèi)存大小作煌,我們只需要補(bǔ)齊少量內(nèi)存占位符即可滿足內(nèi)存對齊規(guī)則赚瘦。
第二種方式就是蘋果采用的將類中的屬性進(jìn)行重排起意,來達(dá)到優(yōu)化內(nèi)存的目的
亲善。以下面這個示例來進(jìn)行說明蘋果中屬性重排
,即內(nèi)存優(yōu)化:
- 自定義
LGPerson
類托享,并定義幾個屬性
//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í)例對象肤视,并對其屬性賦值
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的對象地址吗跋,查找出屬性的值
- 通過地址找出
name
&nickName
image.png
- 通過地址找出
-
通過
0x0000001200006261
地址找出age等數(shù)據(jù)時侧戴,發(fā)現(xiàn)無法找出age等數(shù)據(jù)值,這是因?yàn)?code>蘋果中針對age跌宛、c1酗宋、c2屬性的內(nèi)存進(jìn)行了重排,將他們存儲在同一塊內(nèi)存
中,-
age
通過0x00000012
讀取 -
c1
通過0x61
讀冉小(a的ASCII碼是97) -
c2
通過0x62
讀韧擅ā(b的ASCII碼是98)
image.png
-
特殊的
double
和float
我們嘗試把LGPerson
中的height
屬性類型修改為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
類型處理回右。p/x (double)178
:我們以16
進(jìn)制打印double
類型值打印稀颁,發(fā)現(xiàn)完全相同
。
- 綜上總結(jié)蘋果中的內(nèi)存對齊思想:
- 大部分的內(nèi)存都是通過固定的內(nèi)存塊進(jìn)行讀取楣黍。
- 盡管我們在內(nèi)存中采用了內(nèi)存對齊的方式,但并不是所有的內(nèi)存都可以進(jìn)行浪費(fèi)的棱烂,蘋果會自動對
屬性進(jìn)行重排
租漂,以此來優(yōu)化內(nèi)存
.
8字節(jié)對齊與16字節(jié)對齊
前面我們提及了8字節(jié)對齊
和16字節(jié)對齊
泳姐,這時我們就有疑問沮焕,什么時候在哪里采用哪種字節(jié)對齊贞瞒,接下來我們繼續(xù)源碼探索
- 我們在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);
在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é)對齊
return (x + WORD_MASK) & ~WORD_MASK;
}
可以看到:
- 系統(tǒng)內(nèi)部設(shè)定64位操作系統(tǒng)兔毒,統(tǒng)一使用
8字節(jié)對齊
信粮。對于一個對象來說姨俩,其真正的對齊方式是8字節(jié)
對齊匆赃。 - 因外部處理對象太多鸟赫,系統(tǒng)為了防止一些容錯蒜胖,會采用
align16
為內(nèi)存塊
來存取,主要是因?yàn)椴捎?字節(jié)對齊時抛蚤,兩個對象的內(nèi)存會緊挨著台谢,顯得比較緊湊,而16字節(jié)比較寬松岁经,避免越界訪問朋沮,提高效率,利于蘋果以后的擴(kuò)展缀壤。
16字節(jié)內(nèi)存對齊算法
目前已知的16字節(jié)內(nèi)存對齊算法有兩種
-
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é)對齊算法
筋夏,小于16就成0了
以 k = 2為例,如下圖所示
為什么需要16字節(jié)對齊
原因有一下幾點(diǎn):
- 通常內(nèi)存是由一個個
字節(jié)
組成的苍糠,cpu在存取數(shù)據(jù)時叁丧,并不是以字節(jié)為單位存儲,而是以塊
為單位存取岳瞭,塊的大小為內(nèi)存存取力度拥娄。頻繁存取字節(jié)未對齊的數(shù)據(jù),會極大降低cpu的性能瞳筏,所以可以通過減少存取次數(shù)
來降低cpu的開銷
稚瘾,同時使訪問更安全
,不會產(chǎn)生訪問混亂的情況姚炕。 - 16字節(jié)對齊摊欠,是由于在一個對象中丢烘,第一個屬性
isa占8字節(jié)
,當(dāng)然一個對象肯定還有其他屬性些椒,當(dāng)無屬性時播瞳,會預(yù)留8字節(jié),即16字節(jié)對齊免糕,如果不預(yù)留赢乓,相當(dāng)于這個對象的isa和其他對象的isa緊挨著,容易造成訪問混亂石窑。
總結(jié)
綜合前文提及的獲取內(nèi)存大小的方式
-
class_getInstanceSize
:是采用8字節(jié)對齊
牌芋,參照的對象的屬性內(nèi)存大小 -
malloc_size
:采用16字節(jié)對齊
,參照的整個對象的內(nèi)存大小松逊,對象實(shí)際分配的內(nèi)存大小必須是16
的整數(shù)倍