iOS程序猿必知的位運算相關知識

從現代計算機電路來講,只有 通電/沒電 兩種狀態,即爲 0/1 狀態,計算機中全部的數據按照具體的編碼格式以二進制的形式存儲在設備中。ios

  直接操做這些二進制數據的位數據就是位運算,在iOS開發中基本全部的位運算都經過枚舉聲明傳值的方式將位運算的實現細節隱藏了起來:算法

typedef NS_OPTIONS(NSUInteger, UIRectEdge) {數組

UIRectEdgeNone = 0,服務器

UIRectEdgeTop = 1 << 0,ide

UIRectEdgeLeft = 1 << 1,工具

UIRectEdgeBottom = 1 << 2,優化

UIRectEdgeRight = 1 << 3,this

UIRectEdgeAll = UIRectEdgeTop | UIRectEdgeLeft | UIRectEdgeBottom | UIRectEdgeRight編碼

} NS_ENUM_AVAILABLE_IOS(7_0);atom

  位運算是一種極爲高效乃至能夠說最爲高效的計算方式,雖然現代程序開發中編譯器已經爲咱們作了大量的優化,可是合理的使用位運算能夠提升代碼的可讀性以及執行效率。

基礎計算

  在瞭解怎麼使用位運算以前,筆者簡單說一下CPU處理計算的過程。若是你對 CPU的計算方式有所瞭解,能夠跳過這一節。

  當代碼 int sum = 11 + 79 被執行的時候,計算機直接將兩個數的二進制位進行相加和進位操做:

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

  一般來講CPU執行兩個數相加操做所花費的時間被咱們稱做一個時鐘週期,而2.0GHz頻率的CPU表示能夠在一秒執行運算2.0*1024*1024*1024 個時鐘週期。相較於加法運算,下面看一下 11*2 、 11*4 的二進制結果:

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 << N 表示將 num 的二進制數據左移N 個位置,其效果等同於下面這段代碼:

for (int idx = 0; idx < N; idx++) {

num *= 2;

}

  假如相乘的兩個數都不是 2的N次冪 ,這時候編譯器會將其中某個值分解成多個 2的N次冪 相加的結果進行運算。好比 37 * 69 ,這時候CPU會將 37 分解成 32+4+1,而後換算成 (69<<5) + (69<<2) + (69<<0)的方式計算出結果。所以,計算兩個數相乘一般須要十個左右的時鐘週期。 同理,代碼 num >> N 的做用等效於:

for (int idx = 0; idx < N; idx++) {

num /= 2;

}

  可是兩個數相除花費的時鐘週期要比乘法還要多得多,其大部分消耗在將數值分解成多個 2的N次冪 上。除此以外,浮點數涉及到的計算更爲複雜,這裏也簡單聊聊浮點數的準確度問題。拿 float 類型來講,總共使用了 32bit 的存儲空間,其中第一位表示正負, 2~13位 表示整數部分的值, 14~32位 之中分別存儲了小數位以及科學計數的標識值(這裏可能並不那麼準確,主要是爲了給讀者一個大概的介紹)。因爲小數位的二進制數據依舊保持 2的N次冪 特性,假以下面的二進制屬於小數位:

  那麼這部分小數位的值等於: 1/2 + 1/4 + 1/8 + 1/16 + 1/128 = 0.9453125。所以,當你把一個沒有任何規律的小數例如3.1415926535898 存入計算機的時候,小數點後面會被拆解成不少的 2的N次冪 進行保存。因爲小數位老是有限的,所以當分解的 N 超出這些位數時致使存儲不下,就會出現精度誤差。另外一方面,這樣的分解計算勢必要消耗大量的時鐘週期,這也是大量的浮點數運算 (cell動態計算) 容易引起卡頓的緣由。因此,當小數位過多時,改用字符串存儲是一個更優的選擇。

位運算符

  使用的運算符包括下面:

含義運算符

 

& 操做

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

 

色彩存儲

  使用位運算包括下面幾個緣由: 一、代碼更簡潔 二、更高的效率 三、更少的內存

  簡單來講,咱們如何單純的保存一張 RGB 色彩空間下的圖片?因爲圖片由一系列的像素組成,每一個像素有着本身表達的顏色,所以須要這麼一個類用來表示圖片的單個像素:

@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 不超過一個字節的整數佔用內存。所以能夠經過左移運算保證每個字節只存儲了一個決定色彩的值:

- (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 將值取出來:

- (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源碼 中都能看到,下面是一段僞代碼(參數請勿對號入座)

#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;

}

}

......

  借鑑蘋果的運算操做,能夠聲明一個應用經常使用權限的枚舉,來獲取咱們的應用權限:

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 。不然對對應受權枚舉進行~ 取反後再 & 操做消除二進制位的受權表達。爲了完成這些工做,創建一個工具類來獲取以及更新受權的狀態:

/*!

* @brief 獲取應用受權信息工具,最低使用版本:iOS8.0

*/NS_CLASS_AVAILABLE_IOS(8_0) @interface LXDAuthObtainTool : NSObject

/// 獲取當前應用權限

+ (LXDAuthorizationType)obtainAuthorization;/// 更新應用權限

+ (void)updateAuthorization;

@end

#pragma mark - LXDAuthObtainTool.mstatic 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

  在咱們須要使用某些受權的時候,例如打開相冊時,直接使用 & 運算符判斷權限便可:

- (void)openCamera {

LXDAuthorizationType type = [LXDAuthObtainTool obtainAuthorization];

if (type & LXDAuthorizationTypeCamera) {

/// open camera

else {

/// alert

}

}

  在數據存儲的方面位運算擁有着佔用內存少,高效率的優勢,固然位運算能作的不只僅是這些,好比筆者項目有這樣的一個需求:用戶登陸成功以後在首頁界面請求服務器下載全部金額相關的數據。這個需求最大的問題是:

AFN2.3+ 版本的請求庫不支持同步請求,當須要多個請求任務一次性執行時,判斷請求任務完成是很麻煩的一件事情。

  因爲 NSInteger 擁有8個字節64位的二進制位,所以筆者將每個二進制位用來表示單個任務請求的完成狀態。已知登錄後須要同步數據的接口爲 N(<64)個,所以能夠聲明一個所有請求任務完成後的狀態變量:

NSInteger complete = 0;for (int idx = 0; idx < N; idx++) {

complete |= (1 << idx);

}

  而後使用一個標誌變量 flags 用來記錄當前任務請求的完成狀況,每個數據同步的任務完成以後對應的二進制位就置爲 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];

}

}];

}

位運算與算法

  在廣泛使用高級語言開發的大環境下,位運算的實現更多的被封裝起來,所以大多數開發者在項目開發中不見得會使用這一機制。在上面基礎計算 一節中筆者說過兩個數相加只須要一個時鐘週期(雖然 CPU 從寄存器讀取存放數據也須要額外的時鐘週期,但一般這部分的花銷老是常量級,能夠忽略不計)

  因爲位運算的處理基本也在一個時鐘週期完成,位運算這一操做備受算法封裝者的喜好。好比交換兩個變量的值通常狀況下代碼是:

int sum = a;a = b;b = sum;

  又或者:

a = a + b;b = a - b;a = a - b;

  若是經過位運算的方式則不須要任何加減操做或者臨時變量:

a ^= b;b = a ^ b;a = a ^ b;

  上面的代碼和第二種方式的實現思路相似,都是將 a 和 b 合併成單個變量,再分別消除變量中的 a 和 b 的值( ^ 運算會對相同二進制位的值置0,意味着 b^b 的結果等於0)

  進階題:找出整型數組中惟一的單獨數字,數組中的其餘數字的個數爲2個

  經過上面不用中間變量交換 a 和 b 的值能夠得出下面的最簡代碼:

- (int)singleDog(int * nums) {

int singleDog = 0;

for (int idx = 0; idx < sizeof(nums)/sizeof(int); idx++) {

singleDog ^= nums[idx];

}

return singleDog;

}

 

文章來源:Bison的技術博客

相關文章
相關標籤/搜索