從現(xiàn)代計(jì)算機(jī)電路來(lái)說(shuō),只有 通電/沒電 兩種狀態(tài),即為 0/1 狀態(tài),計(jì)算機(jī)中所有的數(shù)據(jù)按照具體的編碼格式以二進(jìn)制的形式存儲(chǔ)在設(shè)備中。
直接操作這些二進(jìn)制數(shù)據(jù)的位數(shù)據(jù)就是位運(yùn)算,在iOS開發(fā)中基本所有的位運(yùn)算都通過(guò)枚舉聲明傳值的方式將位運(yùn)算的實(shí)現(xiàn)細(xì)節(jié)隱藏了起來(lái):
typedef NS_OPTIONS(NSUInteger, UIRectEdge) {
UIRectEdgeNone = 0,
UIRectEdgeTop = 1 << 0,
UIRectEdgeLeft = 1 << 1,
UIRectEdgeBottom = 1 << 2,
UIRectEdgeRight = 1 << 3,
UIRectEdgeAll = UIRectEdgeTop | UIRectEdgeLeft | UIRectEdgeBottom | UIRectEdgeRight
} NS_ENUM_AVAILABLE_IOS(7_0);
位運(yùn)算是一種極為高效乃至可以說(shuō)最為高效的計(jì)算方式,雖然現(xiàn)代程序開發(fā)中編譯器已經(jīng)為我們做了大量的優(yōu)化,但是合理的使用位運(yùn)算可以提高代碼的可讀性以及執(zhí)行效率。
基礎(chǔ)計(jì)算
在了解怎么使用位運(yùn)算之前,筆者簡(jiǎn)單說(shuō)一下CPU處理計(jì)算的過(guò)程。如果你對(duì) CPU的計(jì)算方式有所了解,可以跳過(guò)這一節(jié)。
當(dāng)代碼 int sum = 11 + 79 被執(zhí)行的時(shí)候,計(jì)算機(jī)直接將兩個(gè)數(shù)的二進(jìn)制位進(jìn)行相加和進(jìn)位操作:
11: 0 0 0 0 1 0 1 179: 0 1 0 0 1 1 1 1
————————————————————90: 0 1 0 1 1 0 1 0
通常來(lái)說(shuō)CPU執(zhí)行兩個(gè)數(shù)相加操作所花費(fèi)的時(shí)間被我們稱作一個(gè)時(shí)鐘周期,而2.0GHz頻率的CPU表示可以在一秒執(zhí)行運(yùn)算2.0*1024*1024*1024 個(gè)時(shí)鐘周期。相較于加法運(yùn)算,下面看一下 11*2 、 11*4 的二進(jìn)制結(jié)果:
11: 0 0 0 0 1 0 1 1 * 2
————————————————————22: 0 0 0 1 0 1 1 0
11: 0 0 0 0 1 0 1 1 * 4
————————————————————44: 0 0 1 0 1 1 0 0
簡(jiǎn)單來(lái)說(shuō),不難發(fā)現(xiàn)當(dāng)某個(gè)數(shù)乘以 2的N次冪 的時(shí)候,結(jié)果等同于將這個(gè)數(shù)的二進(jìn)制位置向左移動(dòng) N 位,在代碼中我們使用 num << N 表示將 num 的二進(jìn)制數(shù)據(jù)左移N 個(gè)位置,其效果等同于下面這段代碼:
for (int idx = 0; idx < N; idx++) {
num *= 2;
}
假如相乘的兩個(gè)數(shù)都不是 2的N次冪 ,這時(shí)候編譯器會(huì)將其中某個(gè)值分解成多個(gè) 2的N次冪 相加的結(jié)果進(jìn)行運(yùn)算。比如 37 * 69 ,這時(shí)候CPU會(huì)將 37 分解成 32+4+1,然后換算成 (69<<5) + (69<<2) + (69<<0)的方式計(jì)算出結(jié)果。因此,計(jì)算兩個(gè)數(shù)相乘通常需要十個(gè)左右的時(shí)鐘周期。 同理,代碼 num >> N 的作用等效于:
for (int idx = 0; idx < N; idx++) {
num /= 2;
}
但是兩個(gè)數(shù)相除花費(fèi)的時(shí)鐘周期要比乘法還要多得多,其大部分消耗在將數(shù)值分解成多個(gè) 2的N次冪 上。除此之外,浮點(diǎn)數(shù)涉及到的計(jì)算更為復(fù)雜,這里也簡(jiǎn)單聊聊浮點(diǎn)數(shù)的準(zhǔn)確度問題。拿 float 類型來(lái)說(shuō),總共使用了 32bit 的存儲(chǔ)空間,其中第一位表示正負(fù), 2~13位 表示整數(shù)部分的值, 14~32位 之中分別存儲(chǔ)了小數(shù)位以及科學(xué)計(jì)數(shù)的標(biāo)識(shí)值(這里可能并不那么準(zhǔn)確,主要是為了給讀者一個(gè)大概的介紹)。由于小數(shù)位的二進(jìn)制數(shù)據(jù)依舊保持 2的N次冪 特性,假如下面的二進(jìn)制屬于小數(shù)位:
那么這部分小數(shù)位的值等于: 1/2 + 1/4 + 1/8 + 1/16 + 1/128 = 0.9453125。因此,當(dāng)你把一個(gè)沒有任何規(guī)律的小數(shù)例如3.1415926535898 存入計(jì)算機(jī)的時(shí)候,小數(shù)點(diǎn)后面會(huì)被拆解成很多的 2的N次冪 進(jìn)行保存。由于小數(shù)位總是有限的,因此當(dāng)分解的 N 超出這些位數(shù)時(shí)導(dǎo)致存儲(chǔ)不下,就會(huì)出現(xiàn)精度偏差。另一方面,這樣的分解計(jì)算勢(shì)必要消耗大量的時(shí)鐘周期,這也是大量的浮點(diǎn)數(shù)運(yùn)算 (cell動(dòng)態(tài)計(jì)算) 容易引發(fā)卡頓的原因。所以,當(dāng)小數(shù)位過(guò)多時(shí),改用字符串存儲(chǔ)是一個(gè)更優(yōu)的選擇。
位運(yùn)算符
使用的運(yùn)算符包括下面:
含義運(yùn)算符
& 操作
0 0 1 0 1 1 1 0 46
1 0 0 1 1 1 0 1 157
———————————————
0 0 0 0 1 1 0 0 12
操作
0 0 1 0 1 1 1 0 46
1 0 0 1 1 1 0 1 157
———————————————
1 0 1 1 1 1 1 1 191
~ 操作
0 0 1 0 1 1 1 0 46
———————————————
1 1 0 1 0 0 0 1 225
^ 操作
0 0 1 0 1 1 1 0 46
1 0 0 1 1 1 0 1 157
———————————————
1 0 1 1 0 0 1 1 179
色彩存儲(chǔ)
使用位運(yùn)算包括下面幾個(gè)原因: 1、代碼更簡(jiǎn)潔 2、更高的效率 3、更少的內(nèi)存
簡(jiǎn)單來(lái)說(shuō),我們?nèi)绾螁渭兊谋4嬉粡?/span> RGB 色彩空間下的圖片?由于圖片由一系列的像素組成,每個(gè)像素有著自己表達(dá)的顏色,因此需要這么一個(gè)類用來(lái)表示圖片的單個(gè)像素:
@interface Pixel
@property (nonatomic, assign) CGFloat red;@property (nonatomic, assign) CGFloat green;@property (nonatomic, assign) CGFloat blue;@property (nonatomic, assign) CGFloat alpha;
@end
那么在4.7寸的屏幕上,啟動(dòng)圖需要 750*1334 個(gè)這樣的類,不計(jì)算其他數(shù)據(jù),單單是變量的存儲(chǔ)需要 750*1334*4*8 = 32016000 個(gè)字節(jié)的占用內(nèi)存。但實(shí)際上我們使用到的圖片總是將 RGBA 這四個(gè)屬性保存在一個(gè) int 類型或者其它相似的少字節(jié)變量中。
由于色彩取值范圍為 0~255 ,即 2^1 ~ 2^8-1 不超過(guò)一個(gè)字節(jié)的整數(shù)占用內(nèi)存。因此可以通過(guò)左移運(yùn)算保證每一個(gè)字節(jié)只存儲(chǔ)了一個(gè)決定色彩的值:
- (int)rgbNumberWithRed: (int)red green: (int)green blue: (int)blue alpha: (float)alpha {
int bitPerByte = 8;
int maxNumber = 255;
int alphaInt = alpha * maxNumber;
int rgbNumber = (red << (bitPerByte*3)) + (green << (bitPerByte*2)) + (blue << bitPerByte) + alphaInt;
}
同理,通過(guò)右移操作保證數(shù)值的最后一個(gè)字節(jié)存儲(chǔ)著需要的數(shù)據(jù),并用 0xff 將值取出來(lái):
- (void)obtainRGBA: (int)rgbNumber {
int mask = 0xff;
int bitPerByte = 8;
double alphaInt = (rgbNumber & mask) / 255.0;
int blue = ((rgbNumber >> bitPerByte) & mask);
int green = ((rgbNumber >> (bitPerByte*2)) & mask);
int red = ((rgbNumber >> (bitPerByte*3)) & mask);
}
對(duì)比使用類和位運(yùn)算存儲(chǔ),效率跟內(nèi)存占用上可以說(shuō)是完敗。
位運(yùn)算應(yīng)用
蘋果在類對(duì)象的結(jié)構(gòu)中使用了位運(yùn)算這一設(shè)計(jì):每個(gè)對(duì)象都有一個(gè)整型類型的標(biāo)識(shí)符 flags ,其中多個(gè)不同的位表示了是否存在弱引用、是否被初始化等信息,對(duì)于這些存儲(chǔ)的數(shù)據(jù)通過(guò) & 、 | 等運(yùn)算符獲取出來(lái)。這些在 runtime源碼 中都能看到,下面是一段偽代碼(參數(shù)請(qǐng)勿對(duì)號(hào)入座)
#define IS_TAGGED_POINTER (1 << 12);
#define HAS_WEAK_REFERENCE (1 << 13);
inline void objc_object::free() {
if (this->flags | HAS_WEAK_REFERENCE) {
/// set all weak reference point to nil
}
}
inline int objc_object::retainCount() {
if (this.flags | IS_TAGGED_POINTER) {
return (int)INT_MAX;
} else {
return this->retainCount;
}
}
......
借鑒蘋果的運(yùn)算操作,可以聲明一個(gè)應(yīng)用常用權(quán)限的枚舉,來(lái)獲取我們的應(yīng)用權(quán)限:
typedef NS_ENUM(NSInteger, LXDAuthorizationType)
{
LXDAuthorizationTypeNone = 0,
LXDAuthorizationTypePush = 1 << 0, ///< 推送授權(quán)
LXDAuthorizationTypeLocation = 1 << 1, ///< 定位授權(quán)
LXDAuthorizationTypeCamera = 1 << 2, ///< 相機(jī)授權(quán)
LXDAuthorizationTypePhoto = 1 << 3, ///< 相冊(cè)授權(quán)
LXDAuthorizationTypeAudio = 1 << 4, ///< 麥克風(fēng)授權(quán)
LXDAuthorizationTypeContacts = 1 << 5, ///< 通訊錄授權(quán)
};
通過(guò)聲明一個(gè)全局的權(quán)限變量來(lái)保存不同的授權(quán)信息。當(dāng)應(yīng)用擁有對(duì)應(yīng)的授權(quán)時(shí),通過(guò) | 操作符保證對(duì)應(yīng)的二進(jìn)制位的值被修改成 1 。否則對(duì)對(duì)應(yīng)授權(quán)枚舉進(jìn)行~ 取反后再 & 操作消除二進(jìn)制位的授權(quán)表達(dá)。為了完成這些工作,建立一個(gè)工具類來(lái)獲取以及更新授權(quán)的狀態(tài):
/*!
* @brief 獲取應(yīng)用授權(quán)信息工具,最低使用版本:iOS8.0
*/NS_CLASS_AVAILABLE_IOS(8_0) @interface LXDAuthObtainTool : NSObject
/// 獲取當(dāng)前應(yīng)用權(quán)限
+ (LXDAuthorizationType)obtainAuthorization;/// 更新應(yīng)用權(quán)限
+ (void)updateAuthorization;
@end
#pragma mark - LXDAuthObtainTool.mstatic LXDAuthorizationType kAuthorization;
@implementation LXDAuthObtainTool
+ (void)initialize
{
kAuthorization = LXDAuthorizationTypeNone;
[self updateAuthorization];
}
/// 獲取當(dāng)前應(yīng)用權(quán)限
+ (LXDAuthorizationType)obtainAuthorization
{
return kAuthorization;
}
/// 更新應(yīng)用權(quán)限
+ (void)updateAuthorization
{
/// 推送
if ([UIApplication sharedApplication].currentUserNotificationSettings.types == UIUserNotificationTypeNone) {
kAuthorization &= (~LXDAuthorizationTypePush);
} else {
kAuthorization |= LXDAuthorizationTypePush;
}
/// 定位
if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusAuthorizedAlways || [CLLocationManager authorizationStatus] == kCLAuthorizationStatusAuthorizedWhenInUse) {
kAuthorization |= LXDAuthorizationTypeLocation;
} else {
kAuthorization &= (~LXDAuthorizationTypeLocation);
}
/// 相機(jī)
if ([AVCaptureDevice authorizationStatusForMediaType: AVMediaTypeVideo] == AVAuthorizationStatusAuthorized) {
kAuthorization |= LXDAuthorizationTypeCamera;
} else {
kAuthorization &= (~LXDAuthorizationTypeCamera);
}
/// 相冊(cè)
if ([PHPhotoLibrary authorizationStatus] == PHAuthorizationStatusAuthorized) {
kAuthorization |= LXDAuthorizationTypePhoto;
} else {
kAuthorization &= (~LXDAuthorizationTypePhoto);
}
/// 麥克風(fēng)
[[AVAudioSession sharedInstance] requestRecordPermission: ^(BOOL granted) {
if (granted) {
kAuthorization |= LXDAuthorizationTypeAudio;
} else {
kAuthorization &= (~LXDAuthorizationTypeAudio);
}
}];
/// 通訊錄
if ([UIDevice currentDevice].systemVersion.doubleValue >= 9) {
if ([CNContactStore authorizationStatusForEntityType: CNEntityTypeContacts] == CNAuthorizationStatusAuthorized) {
kAuthorization |= LXDAuthorizationTypeContacts;
} else {
kAuthorization &= (~LXDAuthorizationTypeContacts);
}
} else {
if (ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusAuthorized) {
kAuthorization |= LXDAuthorizationTypeContacts;
} else {
kAuthorization &= (~LXDAuthorizationTypeContacts);
}
}
}
@end
在我們需要使用某些授權(quán)的時(shí)候,例如打開相冊(cè)時(shí),直接使用 & 運(yùn)算符判斷權(quán)限即可:
- (void)openCamera {
LXDAuthorizationType type = [LXDAuthObtainTool obtainAuthorization];
if (type & LXDAuthorizationTypeCamera) {
/// open camera
} else {
/// alert
}
}
在數(shù)據(jù)存儲(chǔ)的方面位運(yùn)算擁有著占用內(nèi)存少,高效率的優(yōu)點(diǎn),當(dāng)然位運(yùn)算能做的不僅僅是這些,比如筆者項(xiàng)目有這樣的一個(gè)需求:用戶登錄成功之后在首頁(yè)界面請(qǐng)求服務(wù)器下載所有金額相關(guān)的數(shù)據(jù)。這個(gè)需求最大的問題是:
AFN2.3+ 版本的請(qǐng)求庫(kù)不支持同步請(qǐng)求,當(dāng)需要多個(gè)請(qǐng)求任務(wù)一次性執(zhí)行時(shí),判斷請(qǐng)求任務(wù)完成是很麻煩的一件事情。
由于 NSInteger 擁有8個(gè)字節(jié)64位的二進(jìn)制位,因此筆者將每一個(gè)二進(jìn)制位用來(lái)表示單個(gè)任務(wù)請(qǐng)求的完成狀態(tài)。已知登陸后需要同步數(shù)據(jù)的接口為 N(<64)個(gè),因此可以聲明一個(gè)全部請(qǐng)求任務(wù)完成后的狀態(tài)變量:
NSInteger complete = 0;for (int idx = 0; idx < N; idx++) {
complete |= (1 << idx);
}
然后使用一個(gè)標(biāo)志變量 flags 用來(lái)記錄當(dāng)前任務(wù)請(qǐng)求的完成情況,每一個(gè)數(shù)據(jù)同步的任務(wù)完成之后對(duì)應(yīng)的二進(jìn)制位就置為 1 :
__block NSInteger flags = 0;NSArray* urls = @[......];NSArray* params = @[......];
for (NSInteger idx = 0; idx < urls.count; idx++) {
NSString * url = urls[idx];
NSDictionary * param = params[idx];
[LXDDataSyncTool syncWithUrl: url params: param complete: ^{
flags |= (1 << idx);
if ( (flags ^ complete) == 0 ) {
[self completeDataSync];
}
}];
}
位運(yùn)算與算法
在普遍使用高級(jí)語(yǔ)言開發(fā)的大環(huán)境下,位運(yùn)算的實(shí)現(xiàn)更多的被封裝起來(lái),因此大多數(shù)開發(fā)者在項(xiàng)目開發(fā)中不見得會(huì)使用這一機(jī)制。在上面基礎(chǔ)計(jì)算 一節(jié)中筆者說(shuō)過(guò)兩個(gè)數(shù)相加只需要一個(gè)時(shí)鐘周期(雖然 CPU 從寄存器讀取存放數(shù)據(jù)也需要額外的時(shí)鐘周期,但通常這部分的花銷總是常量級(jí),可以忽略不計(jì))
由于位運(yùn)算的處理基本也在一個(gè)時(shí)鐘周期完成,位運(yùn)算這一操作備受算法封裝者的喜愛。比如交換兩個(gè)變量的值一般情況下代碼是:
int sum = a;a = b;b = sum;
又或者:
a = a + b;b = a - b;a = a - b;
如果通過(guò)位運(yùn)算的方式則不需要任何加減操作或者臨時(shí)變量:
a ^= b;b = a ^ b;a = a ^ b;
上面的代碼和第二種方式的實(shí)現(xiàn)思路類似,都是將 a 和 b 合并成單個(gè)變量,再分別消除變量中的 a 和 b 的值( ^ 運(yùn)算會(huì)對(duì)相同二進(jìn)制位的值置0,意味著 b^b 的結(jié)果等于0)
進(jìn)階題:找出整型數(shù)組中唯一的單獨(dú)數(shù)字,數(shù)組中的其他數(shù)字的個(gè)數(shù)為2個(gè)
通過(guò)上面不用中間變量交換 a 和 b 的值可以得出下面的最簡(jiǎn)代碼:
- (int)singleDog(int * nums) {
int singleDog = 0;
for (int idx = 0; idx < sizeof(nums)/sizeof(int); idx++) {
singleDog ^= nums[idx];
}
return singleDog;
}
文章來(lái)源:Bison的技術(shù)博客