一件Intent教我的事: Bitwise Operation
A.K.A. Learning bitwise operation from Intent(Android Framework)
寫在前面
最近開發了一個功能,code review 的時候夥伴告訴我其實也可以使用 Bitwise 的方式來表示功能中的大概將近十個表示狀態的 boolean 值。我對 Bitwise 本來也就只有停留在聽過的階段,剛好趁這次的機會學習了一下,並且這邊分享一下學習的結果。
給個栗子🌰
「三八阿花吹喇叭~DO~SO~LA~SI~FA~」
還記得楊祐寧在電影「總舖師」裡面曾說:「就算是番茄炒蛋,每個媽媽做出來的味道都不一樣。」
沒錯,番茄炒蛋是記憶中的味道,也不管你到底有沒有吃過媽媽做的番茄炒蛋,或是根本你媽媽就不會做番茄炒蛋,今天的例子就是要用番茄炒蛋。
這會是個什麼樣的例子呢?讓我們繼續看下去:
假設今天有一個餐館(Restaurant
),他有一道料理,叫番茄炒蛋(Tomato Egg Stir-fry)。每當客人點菜的時候,餐館會為客人提供(serve — )這道料理,所以會請廚師(chef
)來進行料理(cook — )。
但廚師要料理前,會先確認番茄炒蛋的原料(番茄、蛋、油)是否備齊,否則,他就不工作。
寫成code之後大概會長成這樣:
上面使用了early-return,其實也可以改成下面這樣:
目前這樣看起來閱讀性非常地好呀!非常地清楚明瞭,不是嗎?
但目前是因為只有3種狀態:
isTomatoPrepared
isEggPrepared
isOilPrepared
當如果有更多的狀態要保存,就必須列舉更多的boolean,用掉更多的記憶體,但目標卻只有一個 :讓廚師決定要不要開始做番茄炒蛋。
其實有一個我覺得算是比較難以理解,但卻非常精緻(Elegant)的解法:
「Bitwise Operation」。
Bitwise Operation in Android
身為一個Android工程師,我想大家一定或多或少都用過這個神奇的東西,就拿startActivity()
來說吧:
Java版:
Kotlin版:
從範例中可以看到,Android Framework下的Intent類別,就有個setFlags()
方法,我們透過setFlags()
指定了
FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP
給intent
這個instance。
這邊的「|」就是bitwise operator的「or」,是一個bitwise operator,詳細可參考下面的網站,有進一步的解說:
Bitwise Operator
我們這邊就引用維基百科的資料,快速的帶大家了解一下bitwise operator:
AND(在Java中以&表示)
定義:兩個相同長度的二進位數值進行AND運算,如果在相同位置上的位元數值「都是1」的話,運算後在對應位置上的結果也會為1:
0101 (十進位的5)
AND 0011 (十進位的3)
= 0001 (十進位的1)
因為0101
的第一個位元和0011
的第一個位元都是1,其他位置並沒有都是1,所以AND運算的結果就是0001
。
0110 (十進位的6)
AND 0001 (十進位的1)
= 0000 (十進位的0)
因為0110
與0001
並沒有相同位置上都是1的部分,所以AND運算的結果就是0000
。
OR(在Java中以|表示)
定義:兩個相同長度的二進位數值進行OR運算,如果在相同位置上的位元數值「有一者為1」的話,運算後在對應位置上的結果會是1:
0101 (十進位的5)
OR 0011 (十進位的3)
= 0111 (十進位的7)
因為0101
的第一個位元與第三個位元都是1,做OR運算時可以斷定結果的第一個位元與第三個位元都是1。
然後0011
的第一個位元與第二個位元都是1,做OR運算時可以斷定結果的第一個位元與第二個位元都是1。
所以可以得出結果的第一個、第二個與第三個位元都是1,也就是0111
。
0110 (十進位的6)
OR 0001 (十進位的1)
= 0111 (十進位的7)
因為0110
的第二個位元與第三個位元都是1,做OR運算時可以斷定結果的第二個位元與第三個位元都是1。
然後0001
的第一個位元是1,做OR運算時可以斷定結果的第一個位元是1。
所以可以得出結果的第一個、第二個與第三個位元都是1,也就是0111
。
Intent用Bitwise operator來幹嘛?
AND與OR的運算與使用方法可以在Intent
這個類別中看得非常清楚(請搭配服用source code)。當我們要啟動Activity
時,有時候會特別透過setFlags()
來指定flag,來讓啟動的Activity
達到我們想要的效果:
看一下Intent
的source code,可以發現setFlags()
是mFlags
的setter方法:
而mFlags
保存的狀態有這些,光和Activity
有關的就有22種狀態:
坦白說如果今天因為要記錄每個flag個別的狀態,而根據每個情況都設計一個boolean
來進行保存,在不同的地方針對不同的flag去指定true
或false
,這樣在flag的數量一多時看起來真的會滿亂的……例如我想在執行A()
的時候改變一下狀態,執行B()
的時候改變一下狀態,寫起來可能會變成:
那在Intent
中是怎麼去管理這麼多flag的狀態呢?它使用了int這個primitive data type來保存。在Java中,int
是一個容量為32bit的資料類型,而一個bit可以是1或0,所以可以想像成:
1與0可以想像成「有」與「無」、「是」與「非」、「對」與「錯」:
由此可見一個int
可以容納32個true
與false
,我們可以使用一個int來代表至多32個true
與false
的組合。
以FLAG_ACTIVITY_CLEAR_TOP
與FLAG_ACTIVITY_SINGLE_TOP
來說,它們分別代表的是0x04000000
與0x20000000
:
在透過FLAG_ACTIVITY_CLEAR_TOP|FLAG_ACTIVITY_SINGLE_TOP
使用OR運算後setFlags()
給Intent
,Intent
中mFlags
的欄位就會變成:
達成了使用一個變數(mFlags
)來紀錄多種狀態的效果。
新增/移除flag與判斷
除了直接調用setFlags()
來對mFlags
設定值以外,Intent
還有提供addFlags()
與removeFlags()
等方法,來讓我們新增與移除mFlags
中紀錄的狀態:
新增Flag
addFlags()
是單純的OR運算。
移除Flag
removeFlags()
則是先將要移除的flag換成補數,再進行AND運算。假設我們現在的mFlags
是1111
,我們想要移除的flag R
是0001
:
mFlags 1111
R 0001
先對R
做補數運算(把1變成0,0變成1),得出R’
:
mFlags 1111
R' 1110
再對mFlags
與 R’
進行AND運算:
mFlags 1111
AND R' 1110
= 1110
就可以得到結果1110
,成功地將0001
從原來的1111
中移除。
判斷某個Flag是否已啟用(bit = 1)
Intent
中的方法為isDocument()
,判斷方式是讓mFlags
與要判斷的flag進行AND運算。假設我們現在的mFlags
是1111
,我們想判斷flag F(0001)
是否啟用:
mFlags 1111
AND R 0001
= 0001
當mFlags
與要判斷的flag進行AND運算後,得出的結果與要判斷的flag一致,就可以斷定這個flag已經啟用。
那如果我們現在的mFlags
是0011
,我們想判斷flag G(1000)
是否啟用呢?
mFlags 0011
AND G 1000
= 0000
mFlags
與G
AND運算後的結果為0000
,因此可以判斷G
這個flag未被啟用。
所以bite-twice operator到底是啥?我看了還是不懂,怎麼辦?
應該是bitwise operator啦。不過說實話這真的不是個很好懂的東西,確實需要一段時間來消化與體會😝但是不用擔心,我這邊提供了一個比較語意化/比較白話/比較好消化的程式碼,可以把他copy下來,自己調整數值來細細咀嚼與體會:
這種方式真的有這麼好嗎?可以完全取代掉boolean嗎?
當然當一個feature很單純,只有兩三個boolean在表示狀態的話,是沒有必要這麼做的,如何取捨,要看個人與專案的需求。使用到目前為止,我覺得bitwise operator的好處大概就是:
- 用一個field來表示所有狀態,相較於
boolean
可以省下更多記憶體空間。(當然現在手機記憶體動輒好幾GB,省下的記憶體空間微乎其微,但這種做法很容易可以在記憶體受限的韌體程式碼中發現) - 相較於多個
boolean
值各自擁有自己的true
與false
狀態,需要個別maintain,使用bitwise operator的方式只需要判斷某個flag是不是已經啟用就行了。 - 可以在一個地方列出所有的flag,除了一目瞭然以外,也不用擔心這個flag會被改成
true
或false
(好像跟第二點一樣齁)。 - 暫時想不到了,可能就是潮吧!
結語
以上是這次研究bitwise operator的一些心得紀錄,好像不小心認真了……不過還是希望可以幫大家認識這個神奇的東西。如果有錯誤、或是有需要補充的地方,再請大家留言給我,謝謝大家😁😁😁