iOS進階專項分析(二)、OC對象大小及內(nèi)存分配原理

本篇會多角度多種方式分析內(nèi)存地址部分內(nèi)容,需掌握一些計算機基礎(chǔ)知識:

1、大小端模式 傳送門

2、常用數(shù)據(jù)類型所占存儲空間

常用數(shù)據(jù)類型所占存儲空間表.png

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源碼:

  1. 搜索alloc {進入方法實現(xiàn)
  2. 進入方法_objc_rootAlloc
  3. 進入方法callAlloc
  4. callAlloc中找到class_createInstance,點擊進入,記住此時的兩個入?yún)ⅲ粋€是cls,一個是0
  5. 繼續(xù)進入到_class_createInstanceFromZone方法中,發(fā)現(xiàn)這個函數(shù)會先后調(diào)用cls->instanceSize,calloc這兩個方法,先找到 cls->instanceSize;
  6. 進入這個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->instanceSizeunalignedInstanceSize這個函數(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)驗證過了,放個截圖:

字節(jié)對齊驗證.png

我們之前分析alloc流程會進入到_class_createInstanceFromZone方法中,發(fā)現(xiàn)這個函數(shù)會先后調(diào)用cls->instanceSize,calloc這兩個方法,前面這個函數(shù)和class_getInstanceSize實現(xiàn)是基本相同的,我們已經(jīng)分析完成了,接下來我們要分析的后面這個函數(shù)callocmalloc_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>

  1. 找到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ù)分布如下圖:

存儲方式.png

總結(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ù)倍,不足的要對齊
**

溪浣雙鯉的技術(shù)摸爬滾打之路

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。