iOS開發之位運算

stevenshi 8年前發布 | 5K 次閱讀 iOS開發 移動開發

前言

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

直接操作這些二進制數據的位數據就是位運算,在iOS中基本所有的位運算都通過枚舉聲明傳值的方式將位運算的實現細節隱藏了起來:

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 被執行的時候,計算機直接將兩個數的二進制位進行相加和進位操作:

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 的二進制結果:

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</code></pre>

簡單來說,不難發現當某個數乘以 2的N次冪 的時候,結果等同于將這個數的二進制位置向左移動 N 位,在代碼中我們使用 num << N 表示將 num 的二進制數據左移 N 個位置,其效果等同于下面這段代碼:

for (int idx = 0; idx < N; idx++) {
    num = 2;
}</code></pre> 
  

假如相乘的兩個數都不是 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

色彩存儲

使用位運算包括下面幾個原因: 1、代碼更簡潔 2、更高的效率 3、更少的內存

簡單來說,我們如何單純的保存一張 RGB 色彩空間下的圖片?由于圖片由一系列的像素組成,每個像素有著自己表達的顏色,因此需要這么一個類用來表示圖片的單個像素:

@interface Pixel

@property (nonatomic, assign) CGFloat red; @property (nonatomic, assign) CGFloat green; @property (nonatomic, assign) CGFloat blue; @property (nonatomic, assign) CGFloat alpha;

@end</code></pre>

那么在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;

}</code></pre>

同理,通過右移操作保證數值的最后一個字節存儲著需要的數據,并用 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);

}</code></pre>

對比使用類和位運算存儲,效率跟內存占用上可以說是完敗。

位運算應用

蘋果在類對象的結構中使用了位運算這一設計:每個對象都有一個整型類型的標識符 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; } }

......</code></pre>

借鑒蘋果的運算操作,可以聲明一個應用常用權限的枚舉,來獲取我們的應用權限:

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.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</code></pre>

在我們需要使用某些授權的時候,例如打開相冊時,直接使用 & 運算符判斷權限即可:

- (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<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];
    }
}];

}</code></pre>

位運算與算法

在普遍使用高級語言開發的大環境下,位運算的實現更多的被封裝起來,因此大多數開發者在項目開發中不見得會使用這一機制。在上面 基礎計算 一節中筆者說過兩個數相加只需要一個時鐘周期(雖然 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;
}

 

來自:http://allluckly.cn/投稿/tougao69

 

 本文由用戶 stevenshi 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!