問題
在2016年4月份做項(xiàng)目的時(shí)候遇到過一個(gè)問題。
從BLE(低功耗藍(lán)牙)設(shè)備上收到數(shù)據(jù)(16進(jìn)制的數(shù)據(jù)流),<840100ec d5045715 00010014 00240018 00>
,17個(gè)bytes(字節(jié)),然后我定義了一個(gè)結(jié)構(gòu)體去接數(shù)據(jù):
typedef struct {
UInt8 cmd;
UInt16 index; // 目標(biāo):接到0x0100
UInt32 timeStamp; // 目標(biāo):接到0xECD50457
UInt16 steps;// 目標(biāo):接到0x1500
UInt16 calories;// 目標(biāo):接到0x0100
UInt16 distance;// 目標(biāo):接到0x1400
UInt16 sleep;// 目標(biāo):接到0x2400
UInt16 duration;// 目標(biāo):接到0x1800 (一共17 bytes)
} D2MHistoryDataPort;
如果這樣去接數(shù)據(jù),index
接到是0xEC00
(目標(biāo)是0x0100
),導(dǎo)致后面的也都全部錯(cuò)位了。為什么會這樣?
當(dāng)時(shí)問盡朋友,問盡Google,都沒有找到解決的辦法。最后硬著頭皮,用蹩腳的英語在StackOverFlow進(jìn)行了第一次提問How to convert NSData to struct accurately(不久還因?yàn)閱栴}表達(dá)不清被關(guān)閉(囧)——幸好在問題關(guān)閉前有人已經(jīng)明白,并給了解決問題的回答)
當(dāng)時(shí)可以解決問題的答案是:
typedef struct __attribute__((packed)) {
UInt8 cmd;
UInt16 index;
UInt32 timeStamp;
UInt16 steps;// 步數(shù)
UInt16 calories;// 卡路里
UInt16 distance;// 距離,單位m
UInt16 sleep;// 睡眠
UInt16 duration;// 運(yùn)動時(shí)長,單位minute
} D2MHistoryDataPort;
可以看到,多了__attribute__((packed))
這部分。當(dāng)時(shí)copy了答案,能work,也沒有多深究,忙著去趕項(xiàng)目進(jìn)度了。
后來也一直在用這招,凡是結(jié)構(gòu)體中用到非UInt8
的,都會加上__attribute__((packed))
——否則接的時(shí)候,都會「錯(cuò)位」。
最近寫得多了,「百無聊賴」之下又止不住自己的好奇心:為什么要這樣寫?
編譯器的特性
我們先看看下面兩個(gè)結(jié)構(gòu)體的定義:
typedef struct __attribute__((packed)) {
UInt8 cmd;
UInt16 index;
} D2MCommand;
typedef struct {
UInt8 cmd;
UInt16 index;
} D2MCommandNoAttribute;
一個(gè)有__attribute__((packed))
,一個(gè)沒有。
再用sizeof()
打印他們的長度:
NSLog(@"D2MCommand長度: %@", @(sizeof(D2MCommand)));
NSLog(@"D2MCommandNoAttribute長度: %@", @(sizeof(D2MCommandNoAttribute)));
// 打印結(jié)果
D2MCommand長度: 3
D2MCommandNoAttribute長度: 4
定義的數(shù)據(jù)一樣,為什么長度會不一樣?
其實(shí)是編譯器在「作祟」。
為了提高系統(tǒng)性能,CPU處理內(nèi)存時(shí),會用「數(shù)據(jù)對齊(Data alignment)」這種方式。這種方式下,數(shù)據(jù)在內(nèi)存中都以固定size保存。而為了進(jìn)行對齊,有時(shí)候就需要在數(shù)據(jù)中插入(data structure padding)一些無意義的字節(jié)。比如編譯器是以4個(gè)bytes為單位對齊的,當(dāng)你聲明一個(gè)UInt8
的數(shù)據(jù),后面就會補(bǔ)齊3個(gè)無意義的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.
更詳細(xì)可參考: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個(gè)參數(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)))
是等價(jià)的。(aligned(x)
就是告訴編譯器,以x個(gè)字節(jié)為單位進(jìn)行對齊,x只能是1,或2的冪)。
現(xiàn)在就可以解釋剛剛打印結(jié)果的不一樣的原因了:第一個(gè)結(jié)構(gòu)體,用__attribute__((packed))
取消了在編譯階段的優(yōu)化對齊,返回的是實(shí)際占用字節(jié)數(shù)。而第二個(gè)結(jié)構(gòu)體,由于優(yōu)化對齊的存在,UInt8
的cmd,后面會補(bǔ)(padding)一個(gè)byte去對齊,最后加起來就是4個(gè)byte了,后面的數(shù)據(jù)也會錯(cuò)亂。
Conclusion
因此,保險(xiǎn)的做法,iOS開發(fā)中,如果定義指令,用到非UInt8
的數(shù)據(jù)類型(如UInt16
, UInt32
),結(jié)構(gòu)體盡量用__attribute__((packed))
修飾,防止數(shù)據(jù)因?yàn)閷R而導(dǎo)致的錯(cuò)位。