前言
從現代計算機電路來說,只有通電/沒電
兩種狀態,即為0/1
狀態,計算機中所有的資料按照具體的編碼格式以二進位制的形式儲存在裝置中。
直接操作這些二進位制資料的位資料就是位運算,在iOS中基本所有的位運算都通過列舉宣告傳值的方式將位運算的實現細節隱藏了起來:
1 2 3 4 5 6 7 8 |
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); |
位運算是一種極為高效乃至可以說最為高效的計算方式,雖然現代程式開發中編譯器已經為我們做了大量的優化,但是合理的使用位運算可以提高程式碼的可讀性以及執行效率。
基礎計算
在瞭解怎麼使用位運算之前,筆者簡單說一下CPU處理計算的過程。如果你對CPU
的計算方式有所瞭解,可以跳過這一節。
當程式碼int sum = 11 + 79
被執行的時候,計算機直接將兩個數的二進位制位進行相加和進位操作:
1 2 3 4 |
11: 0 0 0 0 1 0 1 1 79: 0 1 0 0 1 1 1 1 ———————————————————— 90: 0 1 0 1 1 0 1 0 |
通常來說CPU執行兩個數相加操作所花費的時間被我們稱作一個時鐘週期,而2.0GHz頻率的CPU表示可以在一秒執行運算2.0*1024*1024*1024
個時鐘週期。相較於加法運算,下面看一下11*2
、11*4
的二進位制結果:
1 2 3 4 5 6 7 |
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 |
簡單來說,不難發現當某個數乘以2的N次冪
的時候,結果等同於將這個數的二進位制位置向左移動N
位,在程式碼中我們使用num 表示將
num
的二進位制資料左移N
個位置,其效果等同於下面這段程式碼:
1 2 3 |
for (int idx = 0; idx < N; idx++) { num *= 2; } |
假如相乘的兩個數都不是2的N次冪
,這時候編譯器會將其中某個值分解成多個2的N次冪
相加的結果進行運算。比如37 * 69
,這時候CPU會將37
分解成32+4+1
,然後換算成(69的方式計算出結果。因此,計算兩個數相乘通常需要十個左右的時鐘週期。 同理,程式碼
num >> N
的作用等效於:
1 2 3 |
for (int idx = 0; idx < N; idx++) { num /= 2; } |
但是兩個數相除花費的時鐘週期要比乘法還要多得多,其大部分消耗在將數值分解成多個2的N次冪
上。除此之外,浮點數涉及到的計算更為複雜,這裡也簡單聊聊浮點數的準確度問題。拿float
型別來說,總共使用了32bit
的儲存空間,其中第一位表示正負,2~13位
表示整數部分的值,14~32位
之中分別儲存了小數位以及科學計數的標識值(這裡可能並不那麼準確,主要是為了給讀者一個大概的介紹)。由於小數位的二進位制資料依舊保持2的N次冪
特性,假如下面的二進位制屬於小數位:
1 |
1 0 1 1 1 0 0 1 |
那麼這部分小數位的值等於:1/2 + 1/4 + 1/8 + 1/16 + 1/128 = 0.9453125
。因此,當你把一個沒有任何規律的小數例如3.1415926535898
存入計算機的時候,小數點後面會被拆解成很多的2的N次冪
進行儲存。由於小數位總是有限的,因此當分解的N
超出這些位數時導致儲存不下,就會出現精度偏差。另一方面,這樣的分解計算勢必要消耗大量的時鐘週期,這也是大量的浮點數運算(cell動態計算)
容易引發卡頓的原因。所以,當小數位過多時,改用字串儲存是一個更優的選擇。
位運算子
使用的運算子包括下面:
含義 | 運算子 |
---|---|
左移 | |
右移 | >> |
按位或 | ︳ |
按位並 | & |
按位取反 | ~ |
按位異或 | ^ |
- & 操作
12340 0 1 0 1 1 1 0 461 0 0 1 1 1 0 1 157———————————————0 0 0 0 1 1 0 0 12 - | 操作
12340 0 1 0 1 1 1 0 461 0 0 1 1 1 0 1 157———————————————1 0 1 1 1 1 1 1 191 - ~ 操作
1230 0 1 0 1 1 1 0 46———————————————1 1 0 1 0 0 0 1 225 - ^ 操作
12340 0 1 0 1 1 1 0 461 0 0 1 1 1 0 1 157———————————————1 0 1 1 0 0 1 1 179
色彩儲存
使用位運算包括下面幾個原因:
1、程式碼更簡潔
2、更高的效率
3、更少的記憶體
簡單來說,我們如何單純的儲存一張RGB
色彩空間下的圖片?由於圖片由一系列的畫素組成,每個畫素有著自己表達的顏色,因此需要這麼一個類用來表示圖片的單個畫素:
1 2 3 4 5 6 7 8 |
@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寸的螢幕上,啟動圖需要750*1334
個這樣的類,不計算其他資料,單單是變數的儲存需要750*1334*4*8
= 32016000
個位元組的佔用記憶體。但實際上我們使用到的圖片總是將RGBA
這四個屬性儲存在一個int
型別或者其它相似的少位元組變數中。
由於色彩取值範圍為0~255
,即2^1 ~ 2^8-1
不超過一個位元組的整數佔用記憶體。因此可以通過左移運算保證每一個位元組只儲存了一個決定色彩的值:
1 2 3 4 5 6 7 |
- (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; } |
同理,通過右移操作保證數值的最後一個位元組儲存著需要的資料,並用0xff
將值取出來:
1 2 3 4 5 6 7 8 9 |
- (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); } |
對比使用類和位運算儲存,效率跟記憶體佔用上可以說是完敗。
位運算應用
蘋果在類物件的結構中使用了位運算這一設計:每個物件都有一個整型型別的識別符號flags
,其中多個不同的位表示了是否存在弱引用、是否被初始化等資訊,對於這些儲存的資料通過&
、|
等運算子獲取出來。這些在runtime原始碼中都能看到,借鑑蘋果的運算操作,可以宣告一個應用常用許可權的列舉,來獲取我們的應用許可權:
1 2 3 4 5 6 7 8 9 10 |
typedef NS_ENUM(NSInteger, LXDAuthorizationType) { LXDAuthorizationTypeNone = 0, LXDAuthorizationTypePush = 1 << 0, ///< 推送授權 LXDAuthorizationTypeLocation = 1 << 1, ///< 定位授權 LXDAuthorizationTypeCamera = 1 << 2, ///< 相機授權 LXDAuthorizationTypePhoto = 1 << 3, ///< 相簿授權 LXDAuthorizationTypeAudio = 1 << 4, ///< 麥克風授權 LXDAuthorizationTypeContacts = 1 << 5, ///< 通訊錄授權 }; |
通過宣告一個全域性的許可權變數來儲存不同的授權資訊。當應用擁有對應的授權時,通過|
操作符保證對應的二進位制位的值被修改成1
。否則對對應授權列舉進行~
取反後再&
操作消除二進位制位的授權表達。為了完成這些工作,建立一個工具類來獲取以及更新授權的狀態:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
/*! * @brief 獲取應用授權資訊工具,最低使用版本:iOS8.0 */ NS_CLASS_AVAILABLE_IOS(8_0) @interface LXDAuthObtainTool : NSObject /// 獲取當前應用許可權 + (LXDAuthorizationType)obtainAuthorization; /// 更新應用許可權 + (void)updateAuthorization; @end #pragma mark - LXDAuthObtainTool.m static LXDAuthorizationType kAuthorization; @implementation LXDAuthObtainTool + (void)initialize { kAuthorization = LXDAuthorizationTypeNone; [self updateAuthorization]; } /// 獲取當前應用許可權 + (LXDAuthorizationType)obtainAuthorization { return kAuthorization; } /// 更新應用許可權 + (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); } /// 相機 if ([AVCaptureDevice authorizationStatusForMediaType: AVMediaTypeVideo] == AVAuthorizationStatusAuthorized) { kAuthorization |= LXDAuthorizationTypeCamera; } else { kAuthorization &= (~LXDAuthorizationTypeCamera); } /// 相簿 if ([PHPhotoLibrary authorizationStatus] == PHAuthorizationStatusAuthorized) { kAuthorization |= LXDAuthorizationTypePhoto; } else { kAuthorization &= (~LXDAuthorizationTypePhoto); } /// 麥克風 [[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 |
在我們需要使用某些授權的時候,例如開啟相簿時,直接使用&
運算子判斷許可權即可:
1 2 3 4 5 6 7 8 |
- (void)openCamera { LXDAuthorizationType type = [LXDAuthObtainTool obtainAuthorization]; if (type & LXDAuthorizationTypeCamera) { /// open camera } else { /// alert } } |
在資料儲存的方面位運算擁有著佔用記憶體少,高效率的優點,當然位運算能做的不僅僅是這些,比如筆者專案有這樣的一個需求:使用者登入成功之後在首頁介面請求伺服器下載所有金額相關的資料。這個需求最大的問題是:
AFN2.3+
版本的請求庫不支援同步請求,當需要多個請求任務一次性執行時,判斷請求任務完成是很麻煩的一件事情。
由於NSInteger
擁有8個位元組64位的二進位制位,因此筆者將每一個二進位制位用來表示單個任務請求的完成狀態。已知登陸後需要同步資料的介面為N(個,因此可以宣告一個全部請求任務完成後的狀態變數:
1 2 |
NSInteger complete = 0; for (int idx = 0; idx |
然後使用一個標誌變數flags
用來記錄當前任務請求的完成情況,每一個資料同步的任務完成之後對應的二進位制位就置為1
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
__block NSInteger flags = 0; NSArray<NSString *> * urls = @[......]; NSArray<NSDictionary *> * 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]; } }]; } |
位運算與演算法
在普遍使用高階語言開發的大環境下,位運算的實現更多的被封裝起來,因此大多數開發者在專案開發中不見得會使用這一機制。在上面基礎計算
一節中筆者說過兩個數相加只需要一個時鐘週期(雖然CPU
從暫存器讀取存放資料也需要額外的時鐘週期,但通常這部分的花銷總是常量級,可以忽略不計)
由於位運算的處理基本也在一個時鐘週期完成,位運算這一操作備受演算法封裝者的喜愛。比如交換兩個變數的值一般情況下程式碼是:
1 2 3 |
int sum = a; a = b; b = sum; |
又或者:
1 2 3 |
a = a + b; b = a - b; a = a - b; |
如果通過位運算的方式則不需要任何加減操作或者臨時變數:
1 2 3 |
a ^= b; b = a ^ b; a = a ^ b; |
上面的程式碼和第二種方式的實現思路類似,都是將a
和b
合併成單個變數,再分別消除變數中的a
和b
的值(^
運算會對相同二進位制位的值置0,意味著b^b
的結果等於0)
進階題:找出整型陣列中唯一的單獨數字,陣列中的其他數字的個數為2個
通過上面不用中間變數交換a
和b
的值可以得出下面的最簡程式碼:
1 2 3 4 5 6 7 |
- (int)singleDog(int * nums, int length) { int singleDog = 0; for (int idx = 0; idx < length; idx++) { singleDog ^= nums[idx]; } return singleDog; } |