談談位運算和在Android中的運用

Cchanges發表於2019-02-26

位運算,不論是計算機底層處理編碼的時候,還是我們看原始碼的時候都一定有概率能夠看見它。某種程度來說也算是比較熟悉了吧。
個人認為,位運算某種程度上是契合了數位電路之中的邏輯運算,通過一定的邏輯關係將二進位制的資料進行快速處理。
雖然本質來說,位運算就是直接操作記憶體中的整數進行邏輯運算。這個從程式碼到硬體的過程大概從程式碼到記憶體到不同的晶片之間,不同的邏輯閘之間進行運算從而得到結果,再反饋給計算機本身,不過這些東西並不是重點,點到即止就是。

位運算之中的運算步驟都需要將數字轉換為二進位制之後才進行操作,畢竟是針對計算機內部本身的一個運算,使用二進位制來進行處理也情有可原。所以說這跟邏輯電路也有那麼些聯絡。既然是要轉換為二進位制來計算,那麼肯定有兩個數字轉換為二進位制之後位數不相同的情況(指人為進行計算),在這裡,通常的解決方式是在位數少的那個數前面補零。

運算

要使用位運算之前,肯定是要了解下面的這些運算子號以及作用的。

  • 與運算(and)
    &表示,當兩個相同位對應的數都是1的時候,該位獲得的結果才是1,否則為0
    例如說6 & 11,轉換為二進位制就是0110 & 1011,結果為0010

  • 或運算(or)
    |表示,當兩個相同位對應的數只要有一個數1的時候,該位獲得的結果為1,否則為0
    還是用6和11這兩個數做例子,就是0110 | 1011 = 1111

  • 非運算(not)
    ~表示,這個運算只針對一個數字,將數字全部取反(原來是0結果就是1,原來是1結果就是0)
    例如說:~110 = 001

  • 異或運算(xor)
    ^表示,當兩個相同位對應的數字不同的時候為1,否則為0
    例如說:0110 ^ 1011 = 1101

  • 左移(shl)
    <<表示,a << b表示a左移b位,由於移位在末位多出來的未知數字補零。
    在這裡面可以等價為a * 2^b這個運算(針對十進位制)。

  • 右移(shr)
    >>表示,a >> b表示將a右移b位,原本的末位進行右移後會被捨棄,若有需求會在高位進行補零。
    同樣的,右移在十進位制裡面也可以近似為a / (2^b)的形式,不過要對結果取整,也不一定準確,只能夠說意思大概如此。

針對位運算的左移右移,民間一直有一種說法,就是若對數字做對2以及二的倍數的乘法或者除法,使用位運算會比直接使用乘號或者除號的處理速度來的快。對此他們的解釋一直都是以位運算是直接對記憶體進行操作導致運算效率來解釋的,說乘號或者除號都是對位運算的一種包裹,當然我一直也是這麼認為的,不過總覺得這種東西很微妙。未必這真的在一定程度上影響整個程式的執行效率?

還是廢話不多說,直接程式碼驗證。(這裡指Java)

想法其實很簡單粗暴,也就不貼程式碼上來了,思路就是對相同的一個數字進行相同次數(這個數足夠大)的右移或者左移,記錄時間差值,另外一邊就是做等價的乘除法運算,記錄時間差值並比較。

結果如下:(運算次數的話是999999999)

a/2,8281
a>>1,682
a*2,354
a<<1,345
複製程式碼

就結果來說,針對乘法和左移,兩者的效率相近(執行速度差異不大)
但是就針對除法和右移來說,顯然右移效率遠高於除法,某種程度來說是相當微妙了。
不過並沒有去了解產生這種差異的根本原因。順便一提,假如說這個次數不大的話整體的移位和乘除法的效率是基本相似的。所以說,非必要情況還是別用位運算了,影響程式碼閱讀體驗。

在某些工作情境下,位運算是妙用,有些情況下就是影響閱讀了。所以使用它的時候也要考慮情境(如果是效率高於一切的選手這句話當我沒說…)

寫這篇部落格的初衷自然不是來探究左右移的效率的,更想談談的是這個東西在Android裡面的運用。在看書的時候發現很多部分的原始碼都有位運算的成分在裡面,假如說一知半解的話觀看體驗極差就是了,所以說寫一寫具體應用。

Android中的應用

在Android原始碼裡面對於它的應用還是算是較多的,一般是用於儲存多種變數,或者說是flag的儲存和判斷,在這裡也就舉幾個例子。

MeasureSpec

興許最早在Android原始碼裡面接觸位運算的話就是在自定義View部分的時候,當書裡面提及MeasureSpec這個變數的時候採用如下描述:

MeasureSpec代表一個32位int值,高2位代表SpecMode,低30位代表SpecSizeSpecMode是指測量模式,而SpecSize是指在某種測量模式下的規格大小。(引用自《Android開發藝術探索》)

這裡面就是典型的位運算的運用,不論是這個變數需要分別拆分獲得SpecModeSpecSize,還是其他的一些相關的操作,都需要位運算。

首先是SpecMode,在描述之中是高2位的部分,那麼在處理之中一定會運用到左移或者右移來完成需求。

Talk is cheap, show me the code. :)

在這個類裡面,一開始就宣告瞭兩個基本變數以及不同測量模式的值,已經用到了位運算中的左移

private static final int MODE_SHIFT = 30;
// 宣告位移量
private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
// 後期擷取SpecMode或SpecSize時使用的變數
// 3對應的二進位制是11,左移30位後,int值的前2位就都是1,後30位為0

public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY     = 1 << MODE_SHIFT;
public static final int AT_MOST     = 2 << MODE_SHIFT;
// 三種測量模式對應的值
複製程式碼

先看看是如何通過位運算來獲取SpecModeSpecSize

@MeasureSpecMode
// 等價於@IntDef(value={...})。一種列舉類註釋
public static int getMode(int measureSpec) {
    return (measureSpec & MODE_MASK);
    // 讓低30位的值變為0,只保留高2位的值
}

public static int getSize(int measureSpec) {
    return (measureSpec & ~MODE_MASK);
    // 非運算直接讓MASK值變成int值高2位為0,低30位為1
    // 進行與運算,直接將高2位的值變為0
}
複製程式碼

這裡就是典型的通過位運算來擷取對應值,利用的是x & 1 = x, x & 0 = 0,其中x代表0與1兩種值。
這種方式讓一個變數能夠儲存多個內容方式實現,甚至也可以使用這樣的方式將合成的值作為特定的key來做匹配或者相似需求。

接下來是如何獲得MeasureSpec值:

public static int makeMeasureSpec(
    @IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
    // 要求傳入的size值在指定範圍內
    @MeasureSpecMode int mode) {
            if (sUseBrokenMakeMeasureSpec) {// 是否用原來的方法對MeasureSpec進行構建
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }
複製程式碼

在API17及其以下的時候,是按照size + mode的方式進行構建,一個是隻有int前2位有值,一個是隻有int後30位有值,這麼思考處理也情有可原。但假若兩個值有溢位情況就會嚴重影響MeasureSpec的結果。故Google官方在API17之後就對該方法進行了修正,也是採用的位運算的形式:
(size & ~MODE_MASK) | (mode & MODE_MASK)
相當於就是分別獲得了SpecSizeSpecMode後通過或運算獲得結果。

有關Flag儲存和運算

上文提到過可以通過一些操作來實現一個變數儲存多個內容,而在Android原始碼之中在很多也確實做到了,下面就簡單舉個例子。

因為原始碼中涉及這種運算的太多了,就不具體拿原始碼中的某個內容舉例子了,想深究的可以去看看原始碼…大概Flag關鍵字就能找到挺多相關的內容。

使用的時候需要注意,對應的標誌位需要設計好,假如說有內容交叉的話就會非常影響結果。Google官方工程師為了保證這一點也是費盡心思

談談位運算和在Android中的運用

在這之前,得先知道位運算的運算優先順序:
~><</>>>&>^>|>&=/^=/|=

  • A | B
    新增標誌B,可以新增多個(只要不衝突)
    例如:mViewFlags = SOUND_EFFECTS_ENABLED | HAPTIC_FEEDBACK_ENABLED | FOCUSABLE_AUTO;

  • A & B
    判斷A中是否有標誌B
    原理就是與運算之中的1 & 1 = 1, 0 & 1 = 0,以此來判斷flag對應位是否存在該flag
    (mViewFlags & FOCUSABLE_AUTO) != 0這個判斷語句為例:
    若為真,則與的結果不等於0,表示flag之中有該標誌

  • A & ~B
    去除標誌B
    上者的逆運算
    例如:mViewFlags = (mViewFlags & ~FOCUSABLE) | newFocus用於去除原有的標誌位並附上新的標誌位(相當於更新)

  • A ^ B
    取出A與B不同的部分,一般用於判斷A是否發生改變

    int changed = mViewFlags ^ old;
    if (changed == 0) {
        return;
    }
    複製程式碼

(mViewFlags & ENABLED_MASK) == ENABLED類似於這種型別的原理就等同於在MeasureSpec中獲得SpecModeSpecSize,採取對應位直接截斷的方式拿到對應值,然後跟指定flag進行比較。

總結

雖然說位運算常用於演算法裡面,不過在開發過程之中的某些需求之下還是可以巧用位運算得到高效率。但是不要濫用,畢竟影響觀感。

因為本人算接觸Android方面以及相關內容不久,有些言論可能會有錯誤或偏差。若有疑問或者是有地方有誤需要指正,歡迎下方留言討論

相關文章