iOS-底層原理 04:內(nèi)存對(duì)齊

計(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)存大小


數(shù)據(jù)類(lèi)型對(duì)應(yīng)的字節(jié)數(shù)表格

結(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中最大的變量是a8個(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.

image.png

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é),所以Str16位置開(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ù),328的倍數(shù),最終sizeof(LGStruct3)的大小是32
其內(nèi)存存儲(chǔ)情況如下圖所示

結(jié)構(gòu)體嵌套結(jié)構(gòu)體的內(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ò)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
  • 特殊的doublefloat
    我們嘗試把LGPerson中的height屬性類(lèi)型修改為double,并賦值

@property (nonatomic, assign) double height;
//賦值身高
person.height    = 178;

image.png

我們發(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ì)齊思想:
    1. 大部分的內(nèi)存都是通過(guò)固定的內(nèi)存塊進(jìn)行讀取。
    2. 盡管我們?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為例,如下圖所示

malloc中16字節(jié)對(duì)齊算法原理

為什么需要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ù)倍
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容