前期準備
1.lldb打印規則
po
: 對象信息
(lldb) po person
<HTPerson: 0x101875c70>
p
: 對象信息
(lldb) p person
(HTPerson *) $1 = 0x0000000101875c70
x
: memory read
的簡寫,讀取內存信息 (iOS是小端模式,內存讀取要反著讀)
例如: e5 22 00 00 01 80 1d 00
應讀取為0x001d8001000022e5
(lldb) memory read person
0x1024aef20: c9 21 00 00 01 80 1d 00 00 00 00 00 00 00 00 00 .!..............
0x1024aef30: 2d 5b 4e 53 56 69 73 75 61 6c 54 61 62 50 69 63 -[NSVisualTabPic
(lldb) x person
0x1024aef20: c9 21 00 00 01 80 1d 00 00 00 00 00 00 00 00 00 .!..............
0x1024aef30: 2d 5b 4e 53 56 69 73 75 61 6c 54 61 62 50 69 63 -[NSVisualTabPic
x/4gx
: 打印4條16進制的16字符長度的內存信息
(lldb) x/4gx person
0x101875c70: 0x001d8001000022e5 0x0000000000000012
0x101875c80: 0x0000000100001010 0x0000000100001030
x/4gw
: 打印4條16進制的8字符長度的內存信息
(lldb) x/4gw person
0x1024aef20: 0x000021c9 0x001d8001 0x00000000 0x00000000
p/t
: 二進制打印
(lldb) p/t person
(HTPerson *) $2 = 0b0000000000000000000000000000000100000010010010101110111100100000
2.獲取內存大小
-
sizeof
:操作符。傳入
數據類型
,輸出內存大小。編譯時固定
,
只與類型相關,與具體數值無關。(如:bool
2字節,int
4字節,對象
(指針)8字節) -
class_getInstanceSize
:runtime
的api,傳入對象
,輸出對象所占的內存大小
,本質是對象中成員變量的大小
。 -
malloc_size
:獲取
系統實際分配
的內存大小,符合前面章節align16
對齊標準
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSObject * objc = [[NSObject alloc] init];
NSLog(@"[sizeof] 內存大小: %lu字節", sizeof(objc));
NSLog(@"[class_getInstanceSize] 內存大小: %lu字節", class_getInstanceSize([objc class]));
NSLog(@"[malloc_size] 內存大小: %lu字節", malloc_size((__bridge const void *)(objc)));
}
return 0;
}
- 今天我們就來了解,
對象內部
的內存對齊
。
內存對齊
我們知道對象
對外,蘋果系統會采用align16
字節對齊開辟內存大小,提高系統存取性能。
那對象內部
呢?
對象的本質是
結構體
,這個在后續篇章中我們會詳細了解。所以研究對象內部的內存
,就是研究結構體
的內存布局
。內存對齊目的:
最大程度提高資源利用率
。
我們從一個小案例開始入手
struct MyStruct1 {
char a; // 1字節
double b; // 8字節
int c; // 4字節
short d; // 2字節
NSString * e; // 8字節(指針)
} MyStruct1;
struct MyStruct2 {
NSString * a; // 8字節(指針)
double b; // 8字節
int c; // 4字節
short d; // 2字節
char e; // 1字節
} MyStruct2;
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"%lu - %lu", sizeof(MyStruct1), sizeof(MyStruct2));
}
return 0;
}
打印結果:
MyStruct1 和 MyStruct2 的構成元素都一樣,為何打印出的內存大小不一致?
- 結構體內部的
元素排序
影響內存大小
。這就是內存字節對齊
的作用。
結構體內存對齊規則
每個特定平臺上的編譯器都有自己的默認“對齊系數”
(也叫對齊模數)。程序員可以通過預編譯命令#pragma pack(n)
,n=1,2,4,8,16來改變這一系數,其中的n
就是你要指定的“對齊系數”。在ios中,Xcode默認
為#pragma pack(8),即8字節對齊
注意: 這里的
8字節
對齊是結構體內部對齊規則
,對象在系統中對外
實際分配的空間是遵循16字節對齊
原則。
【三條結構體對齊規則】:
(先把規則寫出來,我們下面用實例來理解)
數據成員的對齊規則可以理解為
min(m, n)
的公式, 其中m
表示當前成員的開始位置
,n
表示當前成員所需位數
。如果滿足條件m 整除 n
(即 m % n == 0), n 從m 位置
開始存儲
,反之
繼續檢查 m+1
能否整除 n, 直到可以整除, 從而就確定
了當前成員的開始位置
。數據成員為結構體:當結構體
嵌套
了結構體
時,作為數據成員的結構體的自身長度
作為外部結構體的最大成員的內存大小,比如結構體a嵌套結構體b,b中有char、int、double等,則b的自身長度為8最后
結構體的內存大小
必須是結構體中最大成員
內存大小的整數倍
,不足的需要補齊。
iOS 基礎數據類型 字節數表:
結構體中的結構體
struct MyStruct3 {
NSString * a; // 8字節(指針)
double b; // 8字節
int c; // 4字節
short d; // 2字節
char e; // 1字節
struct MyStruct2 str;
} MyStruct3;
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"MyStruct3內存大小: %lu", sizeof(MyStruct3));
NSLog(@"MyStruct3中的結構體(MyStruct2)內存大小 %lu", sizeof(MyStruct2));
}
return 0;
}
內存優化(屬性重排)
我們觀察到
MyStruct1
和MyStruct2
的成員屬性一樣,但是在內存管理上,MyStruct2
比MyStruct1
利用率更高(白色空白區域更少
)。MyStruct2
中int
、short
和char
4 + 2 + 1組合,空間利用得更合理。蘋果
會進行屬性重排
,對屬性進行合理排序,盡可能保持保持
屬性之間的內存連續
,減少padding
(白色部分,屬性之間置空的內存)。
如果你還記得
align16
對齊方式,你應該能理解屬性重排的好處了
- align16, 是空間換取時間,保障系統在
處理對象
時能快速存取
- 屬性重排,保障一個對象盡可能少的占用內存資源。
屬性重排案例
- 創建
HTPerson
類
@interface HTPerson : NSObject
@property(nonatomic, copy) NSString * name;
@property(nonatomic, copy) NSString * nickname;
@property(nonatomic, assign) int age;
@property(nonatomic, assign) long height;
@property(nonatomic, assign) char c1;
@property(nonatomic, assign) char c2;
@end
-
main.m
加入測試代碼
#import "HTPerson.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
HTPerson * person = [[HTPerson alloc]init];
person.age = 18;
person.height = 190;
person.name = @"mark";
person.nickname = @"哈哈";
person.c1 = 'A';
person.c2 = 'B';
NSLog(@"%@", person);
}
return 0;
}
-
x/8gx person
: 16進制打印8行內存信息
image.png 我們分析屬性,
name
、nickname
、height
都是各自占用8字節。可以直接打印出來。而
age
是Int占用4字節,c1
和c2
是char,各自占用1字節。我們推測系統可能屬性重排
,將他們存放在了一個塊區。
特殊的double
和float
我們嘗試把height
屬性類型修改為double
@property(nonatomic, assign) double height;
我們發現直接
po
打印0x4067c00000000000
,打印不出來height
的數值190。 這是因為編譯器po
打印默認當做int
類型處理。
-
p/x (double)190
:我們以16進制
打印double
類型值打印,發現完全相同。
如果
height
熟悉換成float
,也是一樣的使用p/x (float)190
驗證。
我們可以封裝2個驗證函數:
// float轉換為16進制
void ht_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");
}
// float轉換為16進制
void ht_double2HEX(float f){
union uuf { float f; char s[8];} uf;
uf.f = f;
printf("0x");
for (int i = 7; i>=0; i--) {
printf("%02x", 0xff & uf.s[i]);
}
printf("\n");
}
為什么對象內部字節對齊是8字節
我們在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();
}
進入alignedInstanceSize
:
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
進入word_align
:
#ifdef __LP64__ // 64位操作系統
# define WORD_SHIFT 3UL
# define WORD_MASK 7UL // 7字節遮罩
# 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字節對齊
return (x + WORD_MASK) & ~WORD_MASK;
}
可以看到,系統內部設定64位操作系統,統一使用8字節對齊
總結
外部處理,系統面對的對象太多,我們統一按照
align16
為內存塊
來存取,效率很快。(所以malloc_size
讀取的都是16的倍數)但為了
避免浪費
太多內存空間。系統會在每個對象內部
進行屬性重排
,并使用8字節對齊
,使單個對象占用的資源盡可能小。(所以class_getInstanceSize
讀取的都是8
的倍數)外部使用16字節對齊,給類留足夠間距,避免越界訪問,對象內部使用8字節對齊完全足夠。
至此, OC底層原理三:探索alloc (你好,alloc大佬 )中提到的三大核心方法,我們已掌握了initstanceSize
計算內存大小。