本篇會多角度多種方式分析內(nèi)存地址部分內(nèi)容,需掌握一些計算機基礎(chǔ)知識:
1、大小端模式 傳送門
2、常用數(shù)據(jù)類型所占存儲空間
3、與OC內(nèi)存對齊算法相同的移位運算 傳送門
下面開始本篇的分析:
一、OC對象大小
新建工程,新建一個類 BMPerson
,添加三個屬性,代碼如下
@interface BMPerson : NSObject
@property (nonatomic, assign)int age;//年齡
@property (nonatomic, copy)NSString * name;//姓名
@property (nonatomic, assign)int height;//身高
@end
在viewDidLoad
新建一個該對象的實例,并給這三個屬性賦值,然后引入 #import <objc/runtime.h>
,使用runtimeAPI提供的方法 class_getInstanceSize()
來打印大小
- (void)viewDidLoad {
[super viewDidLoad];
BMPerson * person = [[BMPerson alloc] init];
person.name = @"張三";
person.age = 18;
person.height = 180;
NSLog(@" class_getInstanceSize 大小%zd", class_getInstanceSize([person class]));
NSLog(@" malloc_size 大小%zu", malloc_size((__bridge const void *)(person)));
}
打印結(jié)果如下:
2020-06-18 23:29:36.955972+0800 TextProject[18720:1508857] class_getInstanceSize 大小24
2020-06-18 23:29:36.956165+0800 TextProject[18720:1508857] malloc_size 大小32
這兩種大小是如何計算的呢?接下來我們進入這兩個方法的底層實現(xiàn)進行分析,打開objc
源碼:
- 搜索
alloc {
進入方法實現(xiàn) - 進入方法
_objc_rootAlloc
- 進入方法
callAlloc
- 在
callAlloc
中找到class_createInstance
,點擊進入,記住此時的兩個入?yún)ⅲ粋€是cls,一個是0 - 繼續(xù)進入到
_class_createInstanceFromZone
方法中,發(fā)現(xiàn)這個函數(shù)會先后調(diào)用cls->instanceSize
,calloc
這兩個方法,先找到cls->instanceSize;
- 進入這個
instanceSize
找到實現(xiàn)部分代碼,貼出來如下:
size_t instanceSize(size_t extraBytes) {
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
由于我們之前 extraBytes 傳的是0,所以相當(dāng)于 `` 內(nèi)部僅僅是調(diào)用了這個函數(shù)alignedInstanceSize
以及 做了一個size最小值16的限制。
其實runtime
提供的這個API方法class_getInstanceSize
底層實現(xiàn)也是一樣的
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
都是調(diào)用了alignedInstanceSize
這個方法,我們看一下它的實現(xiàn)部分
uint32_t alignedInstanceSize() {
return word_align(unalignedInstanceSize());
}
我們發(fā)現(xiàn) alignedInstanceSize()
直接返回了一個函數(shù) word_align()
, 入?yún)⑹且粋€函數(shù)unalignedInstanceSize()
, 這里先看一下 unalignedInstanceSize()
這個函數(shù),這個函數(shù)直接返回了 data()->ro->instanceSize
,unalignedInstanceSize
這個函數(shù)實際上就是去類里面的取值。
我們知道類在未添加任何屬性時,例如BMPerson類經(jīng)過編譯后如下:(可以用clang命令進行編譯得到)
struct BMPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
}
那么BMPerson對象大小其實是下面這種情況
struct NSObject_IMPL NSObject_IVARS;//isa 8個字節(jié)
int _age; //int 4個字節(jié)
NSString * _name;//NSString 8字節(jié)
int _height;//int 4個字節(jié)
所以 unalignedInstanceSize()
返回的是8+4+8+4=24個字節(jié)。
不過這里還有一個關(guān)鍵操作word_align()
字節(jié)對齊,點擊進入這個函數(shù)的定義部分
#ifdef __LP64__
# define WORD_SHIFT 3UL
# define WORD_MASK 7UL
# 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) {
return (x + WORD_MASK) & ~WORD_MASK;
}
分析一下上面這串代碼,等同于下面
static inline uint32_t word_align(uint32_t x) {
return (x + 7) & ~7;
}
下面我們開始分析這個函數(shù)的return部分
當(dāng)輸入x = 4
x+7 = 11,11用二進制表示
0000 1011
右邊的7,用二進制表示
0000 0111
~是非運算符,~7就是對7的二進制取反,得到
1111 1000
然后把上面兩個數(shù)字進行 &(與)操作
0000 1011
1111 1000
相同為1,不同為0,得到
0000 1000
結(jié)果為8
即輸入4,經(jīng)過以上結(jié)果得到的是8,參考一下文章開頭第3條傳送門的 移位運算 向上取整 是不是感覺很相似?
再舉個例子:
當(dāng)輸入X = 15
X+7 = 22,22用二進制表示
0001 0110
右邊的7,用二進制表示
0000 0111
~是非運算符,~7就是對7的二進制取反
1111 1000
然后把上面兩個數(shù)字進行 &(與)操作
0001 0110
1111 1000
相同為1,不同為0,得到
0001 0000
結(jié)果為16
即輸入x = 15,得到的結(jié)果為16
我們根據(jù)BMPerson
的成員變量算出結(jié)果是24,24個字節(jié)以8的倍數(shù)進行對齊,還是24,效果不明顯,但是如果再給BMPerson
加一個int
類型的屬性,unalignedInstanceSize()
計算方式還是8+4+8+4+4 = 28,經(jīng)過word_align()
字節(jié)對齊就是32了,所以class_getInstanceSize
返回的就會是32,這個我已經(jīng)驗證過了,放個截圖:
我們之前分析alloc
流程會進入到_class_createInstanceFromZone
方法中,發(fā)現(xiàn)這個函數(shù)會先后調(diào)用cls->instanceSize
,calloc
這兩個方法,前面這個函數(shù)和class_getInstanceSize
實現(xiàn)是基本相同的,我們已經(jīng)分析完成了,接下來我們要分析的后面這個函數(shù)calloc
和malloc_size
關(guān)鍵實現(xiàn)部分也是相通的,因為alloc
的流程本身就是先計算這個類所需要的內(nèi)存空間,然后系統(tǒng)再根據(jù)這個內(nèi)存空間值進行calloc
操作開辟實際的內(nèi)存空間,從最開始打印的結(jié)果來看,class_getInstanceSize
得出的結(jié)果是24,而系統(tǒng)實際開辟的內(nèi)存空間大小是32,為什么會不同呢?接下來我們繼續(xù)深入calloc
,看看它的底層是如何實現(xiàn)的:
我們點擊這個calloc
函數(shù)底層,發(fā)現(xiàn)點不進去了,其實這個函數(shù)的底層在不在objc
中,而在libmalloc
庫中 ,還需要引入頭文件#import <malloc/malloc.h>
- 找到calloc實現(xiàn)函數(shù)
nano_calloc
,找到其中_nano_malloc_check_clear
方法,然后在其中找到segregated_size_to_fit
下面貼上_nano_malloc_check_clear
這個方法的實現(xiàn)
static void *
_nano_malloc_check_clear(nanozone_t *nanozone, size_t size, boolean_t cleared_requested)
{
MALLOC_TRACE(TRACE_nano_malloc, (uintptr_t)nanozone, size, cleared_requested, 0);
void *ptr;
size_t slot_key;
size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key); // Note slot_key is set here
mag_index_t mag_index = nano_mag_index(nanozone);
nano_meta_admin_t pMeta = &(nanozone->meta_data[mag_index][slot_key]);
ptr = OSAtomicDequeue(&(pMeta->slot_LIFO), offsetof(struct chained_block_s, next));
...此處省略
return ptr;
}
調(diào)用流程就不分析了,重點來了,在_nano_malloc_check_clear
這個函數(shù)中我們發(fā)現(xiàn) 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 = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM;
slot_bytes = k << SHIFT_NANO_QUANTUM;
點擊這幾個宏,我們發(fā)現(xiàn)
#define NANO_MAX_SIZE 256 /* Buckets sized {16, 32, 48, ..., 256} */
#define SHIFT_NANO_QUANTUM 4
#define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM) // 16
那么上面這個代碼就可以變成
k = (size + 16 - 1) >>4<<4
和文章開頭第3個一樣的位移算法,內(nèi)存16位對齊,這里的對齊數(shù)字是 NANO_MAX_SIZE
這個宏,是16的倍數(shù)
#define NANO_MAX_SIZE 256 /* Buckets sized {16, 32, 48, ..., 256} */
到這里我們就明白了,為什么之前得到的size=24 經(jīng)過 calloc(1,24)操作后,打印的結(jié)果會是32,就是再把 24 進行16倍數(shù)對齊,所以是32
接下來我們換個角度,通過匯編分析的方式,換個角度來分析BMPerson
實例在內(nèi)存中的地址
二、從匯編角度分析對象大小及內(nèi)存分配
把斷點斷到NSLog
處,運行
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
BMPerson * person = [[BMPerson alloc] init];
person.name = @"張三";
person.age = 18;
person.height = 180;
NSLog(@" class_getInstanceSize 大小%zd", class_getInstanceSize([person class]));
NSLog(@" malloc_size 大小%zu", malloc_size((__bridge const void *)(person)));
}
使用lldb調(diào)試打印person
的地址
(lldb) po person
<BMPerson: 0x6000039f0380>
(lldb) x 0x6000039f0380
0x6000039f0380: 58 26 85 0e 01 00 00 00 12 00 00 00 b4 00 00 00 X&..............
0x6000039f0390: 18 00 85 0e 01 00 00 00 00 00 00 00 00 00 00 00 ................
(lldb)
(lldb) p 0x00000012
(int) $2 = 18
(lldb) p 0x000000b4
(int) $3 = 180
(lldb) p (NSString *)0x000000010e850018
(__NSCFConstantString *) $5 = 0x000000010e850018 @"張三"
由于iOS是小端模式,所以上面這串地址的數(shù)據(jù)分布如下圖:
總結(jié):
1、alloc
底層流程就是先 調(diào)用class_getInstanceSize()
獲取實例變量最小的分配空間,然后再調(diào)用calloc
返回系統(tǒng)實際分配的內(nèi)存空間大小。
2、由runtimeAPI提供的class_getInstanceSize()方法,用來計算該對象的實例最少需要分配多少空間,其中底層實現(xiàn)包含內(nèi)存對齊算法,使用時需要導(dǎo)入#import <objc/runtime.h>
頭文件。
3、malloc_size()返回系統(tǒng)實際分配的內(nèi)存空間大小,其中底層實現(xiàn)包含內(nèi)存對齊算法,需要導(dǎo)入 #import <malloc/malloc.h>
頭文件。
再來補充幾個基本概念:
數(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獲取)**必須是其內(nèi)部最大成員的整數(shù)倍,不足的要對齊
**