問題
在2016年4月份做項目的時候遇到過一個問題。
從BLE(低功耗藍牙)設(shè)備上收到數(shù)據(jù)(16進制的數(shù)據(jù)流),<840100ec d5045715 00010014 00240018 00>,17個bytes(字節(jié)),然后我定義了一個結(jié)構(gòu)體去接數(shù)據(jù):
typedef struct {
UInt8 cmd;
UInt16 index; // 目標:接到0x0100
UInt32 timeStamp; // 目標:接到0xECD50457
UInt16 steps;// 目標:接到0x1500
UInt16 calories;// 目標:接到0x0100
UInt16 distance;// 目標:接到0x1400
UInt16 sleep;// 目標:接到0x2400
UInt16 duration;// 目標:接到0x1800 (一共17 bytes)
} D2MHistoryDataPort;
如果這樣去接數(shù)據(jù),index接到是0xEC00(目標是0x0100),導致后面的也都全部錯位了。為什么會這樣?
當時問盡朋友,問盡Google,都沒有找到解決的辦法。最后硬著頭皮,用蹩腳的英語在StackOverFlow進行了第一次提問How to convert NSData to struct accurately(不久還因為問題表達不清被關(guān)閉(囧)——幸好在問題關(guān)閉前有人已經(jīng)明白,并給了解決問題的回答)
當時可以解決問題的答案是:
typedef struct __attribute__((packed)) {
UInt8 cmd;
UInt16 index;
UInt32 timeStamp;
UInt16 steps;// 步數(shù)
UInt16 calories;// 卡路里
UInt16 distance;// 距離,單位m
UInt16 sleep;// 睡眠
UInt16 duration;// 運動時長,單位minute
} D2MHistoryDataPort;
可以看到,多了__attribute__((packed))這部分。當時copy了答案,能work,也沒有多深究,忙著去趕項目進度了。
后來也一直在用這招,凡是結(jié)構(gòu)體中用到非UInt8的,都會加上__attribute__((packed))——否則接的時候,都會「錯位」。
最近寫得多了,「百無聊賴」之下又止不住自己的好奇心:為什么要這樣寫?
編譯器的特性
我們先看看下面兩個結(jié)構(gòu)體的定義:
typedef struct __attribute__((packed)) {
UInt8 cmd;
UInt16 index;
} D2MCommand;
typedef struct {
UInt8 cmd;
UInt16 index;
} D2MCommandNoAttribute;
一個有__attribute__((packed)),一個沒有。
再用sizeof()打印他們的長度:
NSLog(@"D2MCommand長度: %@", @(sizeof(D2MCommand)));
NSLog(@"D2MCommandNoAttribute長度: %@", @(sizeof(D2MCommandNoAttribute)));
// 打印結(jié)果
D2MCommand長度: 3
D2MCommandNoAttribute長度: 4
定義的數(shù)據(jù)一樣,為什么長度會不一樣?
其實是編譯器在「作祟」。
為了提高系統(tǒng)性能,CPU處理內(nèi)存時,會用「數(shù)據(jù)對齊(Data alignment)」這種方式。這種方式下,數(shù)據(jù)在內(nèi)存中都以固定size保存。而為了進行對齊,有時候就需要在數(shù)據(jù)中插入(data structure padding)一些無意義的字節(jié)。比如編譯器是以4個bytes為單位對齊的,當你聲明一個UInt8的數(shù)據(jù),后面就會補齊3個無意義的bytes。
參考:
Data alignment means putting the data at a memory offset equal to some multiple of the word size, which increases the system's performance due to the way the CPU handles memory.
To align the data, it may be necessary to insert some meaningless bytes between the end of the last data structure and the start of the next, which is data structure padding.
更詳細可參考:What is the meaning of “attribute((packed, aligned(4))) ”
__attribute__
而要改變編譯器的對齊方式,就要利用到__attribute__關(guān)鍵字,它是用于設(shè)置函數(shù)屬性(Function Attribute)、變量屬性(Variable Attribute)、類型屬性(Type Attribute)。也可以修飾結(jié)構(gòu)體(struct)或共用體(union)。
寫法為__attribute__ ((attribute-list)),后面的attribute-list大概有6個參數(shù)值可以設(shè)置:aligned, packed, transparent_union, unused, deprecated和 may_alias(我自己沒有全部試過)。
packed
packed屬性的主要目的是讓編譯器更緊湊地使用內(nèi)存。
所以再回頭看__attribute__((packed)),它的作用就是告訴編譯器:取消結(jié)構(gòu)體在編譯過程中的優(yōu)化對齊,按盡可能小的size對齊——也就是按1字節(jié)為單位對齊。
__attribute__((packed))和__attribute__((packed, aligned(1)))是等價的。(aligned(x)就是告訴編譯器,以x個字節(jié)為單位進行對齊,x只能是1,或2的冪)。
現(xiàn)在就可以解釋剛剛打印結(jié)果的不一樣的原因了:第一個結(jié)構(gòu)體,用__attribute__((packed))取消了在編譯階段的優(yōu)化對齊,返回的是實際占用字節(jié)數(shù)。而第二個結(jié)構(gòu)體,由于優(yōu)化對齊的存在,UInt8的cmd,后面會補(padding)一個byte去對齊,最后加起來就是4個byte了,后面的數(shù)據(jù)也會錯亂。
Conclusion
因此,保險的做法,iOS開發(fā)中,如果定義指令,用到非UInt8的數(shù)據(jù)類型(如UInt16, UInt32),結(jié)構(gòu)體盡量用__attribute__((packed))修飾,防止數(shù)據(jù)因為對齊而導致的錯位。