在對 OC 對象創建的探究過程中,我們發現一個很有趣的實現 isa
。isa
是將對象內存空間與 class
之間聯結起來的橋梁,而他的實現也很精妙,在有限的存儲空間(一個寄存器的存儲空間,在 64 位架構為 16 個字節,在 32 位的架構為 8 個字節)里,記錄了很多的內容。他的實現方式正是我們今天的主角 -- 聯合體與位域。
聯合體
我們知道在 C 語言中結構體 struct
是一種構造類型或復雜類型,它可以包含多個類型不同的成員。還有另外一種和結構體非常類似的語法,叫做共用體,也叫聯合體。
union 共用體名 {
成員列表
}
結構體和共用體的區別在于:結構體的各個成員會占用不同的內存,互相之間沒有影響;而共用體的所有成員占用同一段內存,修改一個成員會影響其余所有成員。
結構體占用的內存大于等于所有成員占用的內存的總和(成員之間可能會存在縫隙),共用體占用的內存等于最長的成員占用的內存。共用體使用了內存覆蓋技術,同一時刻只能保存一個成員的值,如果對新的成員賦值,就會把原來成員的值覆蓋掉。
union Data {
int a;
short b;
char c;
} data;
int main(int argc, const char * argv[]) {
data.a = 4;
data.b = 8;
data.c = 'c';
NSLog(@"union data: %lu - %lu", sizeof(data), sizeof(union Data)); // union data: 4 - 4
}
聯合體 data
中:
-
a
是int
類型,4 個字節 -
b
是double
類型,2 個字節,所占內存最多 -
c
是char
類型,1個字節
所以 data
所占內存是 4
個字節。
接下來我們來驗證下聯合體在計算機中是如何存儲數據的:
data.c = 'g';
NSLog(@"a: %d, b: %d, c: %c", data.a, data.b, data.c);
data.b = 0x5341;
NSLog(@"a: %d, b: %d, c: %c", data.a, data.b, data.c);
data.a = 0x123a4e63;
NSLog(@"a: %d, b: %d, c: %c", data.a, data.b, data.c);
結合圖中的輸出,即驗證了共用體的長度,也驗證了共用體成員之間會相互影響,修改一個成員的值會影響其他成員。
為了更好的理解上面的輸出結構,簡單畫了下其各個成員在內存中的分布:
在計算機中,數據多以小端模式(即從低地址開始存儲,數據的高字節保存在內存的高地址中,而數據的低字節保存在內存的低地址中)存儲,上圖就是以小端模式的內存分布。
位域
在理解位域之前,我們先來看一個例子:
我們有一輛坦克,他可以向前、后、左、右四個方向中其中幾個方向前進。
@interface SMTank : NSObject
@property (nonatomic, assign) BOOL front;
@property (nonatomic, assign) BOOL back;
@property (nonatomic, assign) BOOL left;
@property (nonatomic, assign) BOOL right;
@end
我們可以看到 front
、back
、left
、right
各占一個字節,但其實我們只需要一個二進制就可以表現出來,也就是 0 或則 1 就可以。
這個時候,我們就可以使用位域:
@interface SMTankCopy : NSObject
- (void)setFront:(BOOL)isFront;
- (BOOL)isFront;
- (void)setBack:(BOOL)isBack;
- (BOOL)isBack;
- (void)setLeft:(BOOL)isLeft;
- (BOOL)isLeft;
- (void)setRight:(BOOL)isRight;
- (BOOL)isRight;
@end
#define SMDirectionFrontMask (1 << 0)
#define SMDirectionBackMask (1 << 1)
#define SMDirectionLeftMask (1 << 2)
#define SMDirectionRightMask (1 << 3)
@interface SMTankCopy () {
union {
char bits;
struct {
char front: 1;
char back: 1;
char left: 1;
char right: 1;
};
} _direction;
}
@end
@implementation SMTankCopy
- (instancetype)init {
self = [super init];
if (self) {
_direction.bits = 0b00000000;
}
return self;
}
// 這里可以有兩種寫法,得到的結果是一樣的
- (void)setFront:(BOOL)isFront {
// if (isFront) {
// _direction.bits |= SMDirectionFrontMask;
// } else {
// _direction.bits &= ~SMDirectionFrontMask;
// }
_direction.left = isFront;
}
- (BOOL)isFront {
return !!(_direction.bits & SMDirectionFrontMask);
}
- (void)setBack:(BOOL)isBack {
_direction.back = isBack;
}
- (BOOL)isBack {
return !!(_direction.back & SMDirectionBackMask);
}
- (void)setLeft:(BOOL)isLeft {
_direction.left = isLeft;
}
- (BOOL)isLeft {
return !!(_direction.left & SMDirectionLeftMask);
}
- (void)setRight:(BOOL)isRight {
_direction.right = isRight;
}
- (BOOL)isRight {
return !!(_direction.left & SMDirectionLeftMask);
}
@end
如果我們以結構體來作為存儲結構的話,還可以節省內存:
struct SMTank {
char left;
char right;
char front;
char back;
} tank;
struct SMTank2 {
union {
char bits;
struct {
char front :1;
char back :1;
char left :1;
char right :1;
};
} direction;
} tank2;
tank.left = 1;
tank.right = 1;
tank.front = 1;
tank.back = 1;
NSLog(@"tank: %lu", sizeof(tank)); // tank: 4
tank2.direction.left = 1;
tank2.direction.right = 1;
tank2.direction.front = 1;
tank2.direction.back = 1;
NSLog(@"tank: %lu", sizeof(tank2)); // tank: 1
因為聯合體共用內存,所以 tank2
只占一個字節。
有些數據在存儲時并不需要占用一個完整的字節,只需要占用一個或幾個二進制位即可。基于這種的數據結構,就是位域。