iOS-OC底層二 :對象內(nèi)存對齊

一、內(nèi)存對齊

獲取內(nèi)存大小的三種方式分別是:

  • sizeof:sizeof計算內(nèi)存大小時,傳入的主要對象是數(shù)據(jù)類型,這個在編譯器的編譯階段(即編譯時)就會確定大小而不是在運行時確定。最終得到的結(jié)果是該數(shù)據(jù)類型占用空間的大小。
  • class_getInstanceSize:用于獲取類的實例對象所占用的內(nèi)存大小,并返回具體的字節(jié)數(shù),其本質(zhì)就是獲取實例對象中成員變量的內(nèi)存大小
  • malloc_size:這個函數(shù)是獲取系統(tǒng)實際分配的內(nèi)存大小
extern size_t malloc_size(const void *ptr);/* Returns size of given ptr */
    
 /** 
 * 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);

通過下面例子驗證一下:

#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(@"objc對象類型占用的內(nèi)存大小:%lu",sizeof(objc));
        NSLog(@"objc對象實際占用的內(nèi)存大?。?lu",class_getInstanceSize([objc class]));
        NSLog(@"objc對象實際分配的內(nèi)存大?。?lu",malloc_size((__bridge const void*)(objc)));
        
    }
    return 0;
}

運行后的結(jié)果如下:

獲取內(nèi)存方式驗證.png

分析結(jié)果原因:

  • sizeof打印出來8:因為objc是NSObject定義的實例對象,實例對象的本質(zhì)是一個結(jié)構(gòu)體指針,指針類型占用的空間是8字節(jié)。
  • class_getInstanceSize打印出來的也是8:因為objc對象沒有屬性,只有指針占用8字節(jié),所以占用的真實內(nèi)存大小是8字節(jié)。
  • malloc_size:計算機實際分配的內(nèi)存大小是16字節(jié)。因為16字節(jié)對齊,在上一篇研究alloc源碼時已經(jīng)了解過。

結(jié)構(gòu)體內(nèi)存對齊

那么蘋果是怎么計算一個對象占用多少字節(jié)的?也就是內(nèi)存對齊究竟是怎么做的。

內(nèi)存對齊的原則:

1、數(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ù)倍地址開始存儲)。

2、結(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ù)倍開始存儲.)

3、收尾工作:結(jié)構(gòu)體的總大小,也就是sizeof的結(jié)果,必須是其內(nèi)部最大成員的整數(shù)倍,不足的要補?

另外一種更加形象的描述:

  • 【原則一】 數(shù)據(jù)成員的對齊規(guī)則可以理解為min(m, n) 的公式, 其中 m表示當(dāng)前成員的開始位置, n表示當(dāng)前成員所需要的位數(shù)。如果滿足條件 m 整除 n (即 m % n == 0), nm 位置開始存儲, 反之繼續(xù)檢查 m+1 能否整除 n, 直到可以整除, 從而就確定了當(dāng)前成員的開始位置。
  • 【原則二】數(shù)據(jù)成員為結(jié)構(gòu)體:當(dāng)結(jié)構(gòu)體嵌套了結(jié)構(gòu)體時,作為數(shù)據(jù)成員的結(jié)構(gòu)體的自身長度作為外部結(jié)構(gòu)體的最大成員的內(nèi)存大小,比如結(jié)構(gòu)體a嵌套結(jié)構(gòu)體b,b中有char、int、double等,則b的自身長度為8
  • 【原則三】最后結(jié)構(gòu)體的內(nèi)存大小必須是結(jié)構(gòu)體中最大成員內(nèi)存大小的整數(shù)倍,不足的需要補齊。

例子一:結(jié)構(gòu)體占用內(nèi)存對比

#import <Foundation/Foundation.h>

//1、定義兩個結(jié)構(gòu)體
struct Mystruct1{
    char a;     //1字節(jié)
    double b;   //8字節(jié)
    int c;      //4字節(jié)
    short d;    //2字節(jié)
}Mystruct1;

struct Mystruct2{
    double b;   //8字節(jié)
    int c;      //4字節(jié)
    short d;    //2字節(jié)
    char a;     //1字節(jié)
}Mystruct2;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        //計算 結(jié)構(gòu)體占用的內(nèi)存大小
        NSLog(@"%lu-%lu",sizeof(Mystruct1),sizeof(Mystruct2));
        
    }
    return 0;
}

運行的結(jié)果如下:


結(jié)構(gòu)體順序不一致導(dǎo)致占用內(nèi)存不一樣.png

分析原因:根據(jù)內(nèi)存對齊規(guī)則我們模擬兩個結(jié)構(gòu)體的內(nèi)存對齊過程:

結(jié)構(gòu)體計算規(guī)則流程示例.png

Mystruct1的內(nèi)存對齊過程如上圖所示,Mystruct2的過程如下:

根據(jù)內(nèi)存對齊規(guī)則計算MyStruct2的內(nèi)存大小,詳解過程如下:

  • 變量b:占8個字節(jié),從0開始,此時min(0,8),即 0-7 存儲 b
  • 變量c:占4個字節(jié),從8開始,此時min(8,4),8可以整除4,即 8-11 存儲 c
  • 變量d:占2個字節(jié),從12開始,此時min(12, 2),12可以整除2,即12-13 存儲 d
  • 變量a:占1個字節(jié),從14開始,此時min(14,1),即 14 存儲 a

因此MyStruct2的需要的內(nèi)存大小為 15字節(jié),而MyStruct2中最大變量的字節(jié)數(shù)為8,所以 MyStruct2 實際的內(nèi)存大小必須是 8 的整數(shù)倍,15向上取整到16,主要是因為16是8的整數(shù)倍,所以 sizeof(MyStruct2) 的結(jié)果是 16

例子二:結(jié)構(gòu)體嵌套結(jié)構(gòu)體

代碼如下:

//1、結(jié)構(gòu)體嵌套結(jié)構(gòu)體
struct Mystruct3{
    double b;   //8字節(jié)
    int c;      //4字節(jié)
    short d;    //2字節(jié)
    char a;     //1字節(jié)
    struct Mystruct2 str; 
}Mystruct3;

//2、打印 Mystruct3 的內(nèi)存大小
NSLog(@"Mystruct3內(nèi)存大?。?lu", sizeof(Mystruct3));
NSLog(@"Mystruct3中結(jié)構(gòu)體成員內(nèi)存大?。?lu", sizeof(Mystruct3.str));

運行結(jié)果:


結(jié)構(gòu)體嵌套結(jié)構(gòu)的運行結(jié)果.png

根據(jù)內(nèi)存對齊規(guī)則,來一步一步分析Mystruct3內(nèi)存大小的計算過程

  • 變量b:占8個字節(jié),從0開始,此時min(0,8),即 0-7 存儲 b
  • 變量c:占4個字節(jié),從8開始,此時min(8,4),8可以整除4,即 8-11 存儲 c
  • 變量d:占2個字節(jié),從12開始,此時min(12, 2),12可以整除2,即12-13 存儲 d
  • 變量a:占1個字節(jié),從14開始,此時min(14,1),即 14 存儲 a
  • 結(jié)構(gòu)體成員str:str是一個結(jié)構(gòu)體,根據(jù)內(nèi)存對齊原則二,結(jié)構(gòu)體成員要從其內(nèi)部最大成員大小的整數(shù)倍開始存儲,而MyStruct2最大的成員大小為8,所以str要從8的整數(shù)倍開始,當(dāng)前是從15開始,所以不符合要求,需要往后移動到16,16是8的整數(shù)倍,符合內(nèi)存對齊原則,所以 16-31 存儲 str

因此MyStruct3的需要的內(nèi)存大小為 32字節(jié),而MyStruct3中最大變量為str, 其最大成員內(nèi)存字節(jié)數(shù)為8,根據(jù)內(nèi)存對齊原則,所以 MyStruct3 實際的內(nèi)存大小必須是 8 的整數(shù)倍,32正好是8的整數(shù)倍,所以 sizeof(MyStruct3) 的結(jié)果是 32

例子三:結(jié)構(gòu)體嵌套結(jié)構(gòu)體二次驗證

struct Mystruct4{
    int a;              //4字節(jié) min(0,4)--- (0,1,2,3)
    struct Mystruct5{   //從4開始,存儲開始位置必須是最大的整數(shù)倍(最大成員為8),min(4,8)不符合 4,5,6,7,8 -- min(8,8)滿足,從8開始存儲
        double b;       //8字節(jié) min(8,8)  --- (8,9,10,11,12,13,14,15)
        short c;         //2字節(jié),從16開始,min(16,2) -- (16,17)
    }Mystruct5;
    /*結(jié)構(gòu)體Mystruct5結(jié)束,按照【原則三】`結(jié)構(gòu)體的內(nèi)存大小`必須是結(jié)構(gòu)體中`最大成員內(nèi)存大小`的整數(shù)倍,不足的需要補齊。
     當(dāng)前結(jié)構(gòu)體長度為10,補充6字節(jié)。(18,19,20,21,22,23)留空。
    */
    short d; //2字節(jié),offset從24開始,能整除。 (24,25)存放
}Mystruct4;
/*結(jié)構(gòu)體結(jié)束,按照【原則三】`結(jié)構(gòu)體的內(nèi)存大小`必須是結(jié)構(gòu)體中`最大成員內(nèi)存大小`的整數(shù)倍,不足的需要補齊。
 當(dāng)前結(jié)構(gòu)體長度為26,補充6字節(jié)。(26,27,28,29,30,31)留空。
*/

NSLog(@"Mystruct4內(nèi)存大?。?lu", sizeof(Mystruct4));
NSLog(@"Mystruct5內(nèi)存大小:%lu", sizeof(Mystruct4.Mystruct5));
結(jié)構(gòu)體嵌套結(jié)構(gòu)體二次驗證.png

二、內(nèi)存優(yōu)化(屬性重排)

我們知道對象在底層就是一個結(jié)構(gòu)體對象。從上面結(jié)構(gòu)體內(nèi)存對齊的內(nèi)容,得出結(jié)構(gòu)體占用內(nèi)存大小跟結(jié)構(gòu)體中成員變量的順序有關(guān)。那么對象的內(nèi)存大小是否跟成員變量的順序有關(guān)呢?

思考:如果是的話,那么我們平時寫代碼的時候,豈不是要時時刻刻注意,成員變量的順序,所以答案肯定是否定的。蘋果系統(tǒng)底層會幫我們自動進(jìn)行內(nèi)存優(yōu)化。

對象雖然會以結(jié)構(gòu)體的形式進(jìn)行存儲,但是在保存時,蘋果會計算出最優(yōu)的存儲順序,達(dá)到減少內(nèi)存消耗的目的。

例一:驗證內(nèi)存優(yōu)化

新建兩個類,Man和Women,分別添加兩個int和一個long。將順序打亂,如果純粹按照結(jié)構(gòu)體內(nèi)存對齊,它將分別是16字節(jié)和24字節(jié)。代碼如下:

#import <objc/runtime.h>
#import <malloc/malloc.h>

@interface Man : NSObject
@property (nonatomic, assign) int age;
@property (nonatomic, assign) int wight;
@property (nonatomic, assign) long height;
@end

@implementation Man
@end

@interface Woman : NSObject
@property (nonatomic, assign) int wight;
@property (nonatomic, assign) long height;
@property (nonatomic, assign) int age;
@end

@implementation Woman
@end


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        Man *man = [[Man alloc] init];
        man.age = 18;
        man.height = 185;
        man.wight = 75;
        
        Woman *woman = [[Woman alloc] init];
        woman.age = 18;
        woman.height = 185;
        woman.wight = 75;
        
        NSLog(@"man對象類型占用的內(nèi)存大?。?lu,woman對象類型占用的內(nèi)存大?。?lu",sizeof(man),sizeof(woman));
        NSLog(@"man對象實際占用的內(nèi)存大?。?lu,woman對象實際占用的內(nèi)存大?。?lu",class_getInstanceSize([man class]),class_getInstanceSize([woman class]));
        NSLog(@"man對象實際分配的內(nèi)存大?。?lu,woman對象實際分配的內(nèi)存大?。?lu",malloc_size((__bridge const void*)(man)),malloc_size((__bridge const void*)(woman)));
    }
    return 0;
}

類的成員變量順序不會影響類對象的內(nèi)存大小.png

結(jié)果:類對象是指針,占8字節(jié)。一個int占4字節(jié),兩個int占8字節(jié),一個long占8字節(jié),加上指針占8字節(jié),實際一共24字節(jié)。對象類型需要16字節(jié)對齊,必須是16的倍數(shù),所以對象占用32字節(jié)。

分析:按照結(jié)構(gòu)體內(nèi)存對齊,Man和Woman類所占的內(nèi)存空間肯定不一樣,可結(jié)果確是完全一模一樣,說明了蘋果在類轉(zhuǎn)換成結(jié)構(gòu)體存儲時,做了內(nèi)存優(yōu)化。

通過打印內(nèi)存二次驗證

通過打印內(nèi)存驗證.png

分析結(jié)果:第一個字節(jié)是ISA,第二個字節(jié)順序不同了,第三個8字節(jié)一模一樣,第四字節(jié)都是補充0。第二8字節(jié)0x12和0x4b順序不同,正是蘋果對內(nèi)存做的優(yōu)化??梢圆乱幌?,蘋果先是忽略了long,按int的順序補齊。真正要研究它是怎么優(yōu)化的過程,需要去看LLVM的處理邏輯,在加載類時的邏輯。

關(guān)于字節(jié)對齊的底層代碼

我們可以通過objc4中class_getInstanceSize的源碼來進(jìn)行分析

/** 
 * 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);

??

size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}

??

// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() const {
    return word_align(unalignedInstanceSize());
}

??

static inline uint32_t word_align(uint32_t x) {
    //x+7 & (~7) --> 8字節(jié)對齊
    return (x + WORD_MASK) & ~WORD_MASK;
}


//其中 WORD_MASK 為
#   define WORD_MASK 7UL
  • 對于一個對象來說,其真正的對齊方式8字節(jié)對齊,8字節(jié)對齊已經(jīng)足夠滿足對象的需求了
  • apple系統(tǒng)為了防止一切的容錯,采用的是16字節(jié)對齊的內(nèi)存,主要是因為采用8字節(jié)對齊時,兩個對象的內(nèi)存會緊挨著,顯得比較緊湊,而16字節(jié)比較寬松,利于蘋果以后的擴展。

目前已知的16字節(jié)內(nèi)存對齊算法有兩種

  • alloc源碼分析中的align16
  • malloc源碼分析中的segregated_size_to_fit

align16: 16字節(jié)對齊算法

static inline size_t align16(size_t x) {
    return (x + size_t(15)) & ~size_t(15);
}

segregated_size_to_fit: 16字節(jié)對齊算法

#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;
}

三、改變編譯器字節(jié)對齊方式

在一些與用硬件設(shè)備交互的場景中,比如C語言寫的加密算法,可能需要直接iOS和安卓端按照設(shè)備端C語言編譯器的處理方式,而不是通常情況下Xcode的編譯處理方式。因為字節(jié)對齊的問題,加密位數(shù)的不一致,會導(dǎo)致校驗失敗。在安卓端可以通過JNI實現(xiàn)java和C兩個編譯環(huán)境,不需要處理這種問題。但是在iOS端,雖然是可以直接運行C語言,但卻和極其在意內(nèi)存使用的嵌入式設(shè)備端C語言編譯器有區(qū)別。請看如下代碼,還是上面的Mystruct1和Mystruct2:

改變字節(jié)對齊方式.png

分析結(jié)果:加了#pragma pack(2)之后,根本不理會所謂的字節(jié)對齊的內(nèi)存存儲方式。加了#pragma pack之后,就可以實現(xiàn)完全跟各種編譯器匹配,直接使用C語言完成加密算法的校驗。簡直不要太爽。

#pragma pack的作用

程序編譯器對結(jié)構(gòu)的存儲的特殊處理確實提高CPU存儲變量的速度,但是有時候也帶來了一些麻煩,我們也屏蔽掉變量默認(rèn)的對齊方式,自己可以設(shè)定變量的對齊方式。

編譯器中提供了#pragma pack(n)來設(shè)定變量以n字節(jié)對齊方式。n字節(jié)對齊就是說變量存放的起始地址的偏移量有兩種情況:

第一、如果n大于等于該變量所占用的字節(jié)數(shù),那么偏移量必須滿足默認(rèn)的對齊方式,

第二、如果n小于該變量的類型所占用的字節(jié)數(shù),那么偏移量為n的倍數(shù),不用滿足默認(rèn)的對齊方式。

結(jié)構(gòu)的總大小也有個約束條件,分下面兩種情況:如果n大于所有成員變量類型所占用的字節(jié)數(shù),那么結(jié)構(gòu)的總大小必須為占用空間最大的變量占用的空間數(shù)的倍數(shù);否則必須為n的倍數(shù)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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