iOS 開發 – 位運算

林欣達發表於2016-10-31

前言

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

11783864-fec270d6b0b498e9

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

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

基礎計算

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

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

通常來說CPU執行兩個數相加操作所花費的時間被我們稱作一個時鐘週期,而2.0GHz頻率的CPU表示可以在一秒執行運算2.0*1024*1024*1024個時鐘週期。相較於加法運算,下面看一下11*211*4的二進位制結果:

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

假如相乘的兩個數都不是2的N次冪,這時候編譯器會將其中某個值分解成多個2的N次冪相加的結果進行運算。比如37 * 69,這時候CPU會將37分解成32+4+1,然後換算成(69的方式計算出結果。因此,計算兩個數相乘通常需要十個左右的時鐘週期。 同理,程式碼num >> N的作用等效於:

但是兩個數相除花費的時鐘週期要比乘法還要多得多,其大部分消耗在將數值分解成多個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動態計算)容易引發卡頓的原因。所以,當小數位過多時,改用字串儲存是一個更優的選擇。

位運算子

使用的運算子包括下面:

含義 運算子
左移
右移 >>
按位或
按位並 &
按位取反 ~
按位異或 ^
  • & 操作
  • | 操作
  • ~ 操作
  • ^ 操作

色彩儲存

使用位運算包括下面幾個原因:
1、程式碼更簡潔
2、更高的效率
3、更少的記憶體

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

那麼在4.7寸的螢幕上,啟動圖需要750*1334個這樣的類,不計算其他資料,單單是變數的儲存需要750*1334*4*8 = 32016000個位元組的佔用記憶體。但實際上我們使用到的圖片總是將RGBA這四個屬性儲存在一個int型別或者其它相似的少位元組變數中。

由於色彩取值範圍為0~255,即2^1 ~ 2^8-1不超過一個位元組的整數佔用記憶體。因此可以通過左移運算保證每一個位元組只儲存了一個決定色彩的值:

同理,通過右移操作保證數值的最後一個位元組儲存著需要的資料,並用0xff將值取出來:

對比使用類和位運算儲存,效率跟記憶體佔用上可以說是完敗。

位運算應用

蘋果在類物件的結構中使用了位運算這一設計:每個物件都有一個整型型別的識別符號flags,其中多個不同的位表示了是否存在弱引用、是否被初始化等資訊,對於這些儲存的資料通過&|等運算子獲取出來。這些在runtime原始碼中都能看到,借鑑蘋果的運算操作,可以宣告一個應用常用許可權的列舉,來獲取我們的應用許可權:

通過宣告一個全域性的許可權變數來儲存不同的授權資訊。當應用擁有對應的授權時,通過|操作符保證對應的二進位制位的值被修改成1。否則對對應授權列舉進行~取反後再&操作消除二進位制位的授權表達。為了完成這些工作,建立一個工具類來獲取以及更新授權的狀態:

在我們需要使用某些授權的時候,例如開啟相簿時,直接使用&運算子判斷許可權即可:

在資料儲存的方面位運算擁有著佔用記憶體少,高效率的優點,當然位運算能做的不僅僅是這些,比如筆者專案有這樣的一個需求:使用者登入成功之後在首頁介面請求伺服器下載所有金額相關的資料。這個需求最大的問題是:

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

由於NSInteger擁有8個位元組64位的二進位制位,因此筆者將每一個二進位制位用來表示單個任務請求的完成狀態。已知登陸後需要同步資料的介面為N(個,因此可以宣告一個全部請求任務完成後的狀態變數:

然後使用一個標誌變數flags用來記錄當前任務請求的完成情況,每一個資料同步的任務完成之後對應的二進位制位就置為1

位運算與演算法

在普遍使用高階語言開發的大環境下,位運算的實現更多的被封裝起來,因此大多數開發者在專案開發中不見得會使用這一機制。在上面基礎計算一節中筆者說過兩個數相加只需要一個時鐘週期(雖然CPU從暫存器讀取存放資料也需要額外的時鐘週期,但通常這部分的花銷總是常量級,可以忽略不計)

由於位運算的處理基本也在一個時鐘週期完成,位運算這一操作備受演算法封裝者的喜愛。比如交換兩個變數的值一般情況下程式碼是:

又或者:

如果通過位運算的方式則不需要任何加減操作或者臨時變數:

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

進階題:找出整型陣列中唯一的單獨數字,陣列中的其他數字的個數為2個

通過上面不用中間變數交換ab的值可以得出下面的最簡程式碼: