位運算的那些事(三)位掩碼

前兩篇我重點針對位運算基礎以及運算過程詳細的進行了講解說明,相信看過的小夥伴也都很明瞭了。那麼基礎有了,也知道運算過程了,那我們常見的戰場在哪裏呢?這就像排兵佈陣一樣,只閱讀兵法,而沒有實踐和模擬,只能算紙上談兵了。本篇就拉開帷幕直面開發中這個最常見的戰場——位掩碼(BitMask)。

什麼是掩碼

說起掩碼大家都聽過子網掩碼吧,子網掩碼的主要作用是判斷當前IP是屬於什麼樣的網絡,是A類還是B類還是C類;當前IP處於什麼樣的網段,網段內可以擁有多少個機子。比如我們公司電腦的子網掩碼是255.255.255.0,很明顯就是一個局域網。如果你對子網掩碼還是不清晰,可以看一下《如何理解子網掩碼》

掩碼就是一串二進制代碼對目標字段進行位與運算,屏蔽當前的輸入位,最終得到一個合理的需求。說白了,掩碼就是一把輔助鑰匙,你給我一個盒子我幫助你打開看看裏面是什麼

說到這不知道大家有沒有對掩碼有一個概念性的認識呢?不清楚沒關係,這只是位運算中一個插曲,下邊的講解中也會相應用到,到時候你就明白了,本篇的目的是爲了講位運算在項目開發中的一些典型用法。

拋磚引玉

有一個很經典的算法題,說是有1000個一模一樣的瓶子,其中有999瓶是普通的水,有一瓶是毒藥。任何喝下毒藥的生物都會在一星期之後死亡。現在,你只有10只小白鼠和一星期的時間,如何檢驗出哪個瓶子裏有毒藥?如果按照常規的解法是不是很繁瑣,我們不妨思考一下用二進制來處理。

具體實現跟3個老鼠確定8個瓶子原理一樣:

000=0
001=1
010=2
011=3
100=4
101=5
110=6
111=7

一位表示一個老鼠,0-7表示8個瓶子。也就是分別將1、3、5、7號瓶子的藥混起來給老鼠1吃,2、3、6、7號瓶子的藥混起來給老鼠2吃,4、5、6、7號瓶子的藥混起來給老鼠3吃,哪個老鼠死了,相應的位標爲1。如老鼠1死了、老鼠2沒死、老鼠3死了,那麼就是101=5號瓶子有毒。同樣道理10個老鼠可以確定1000個瓶子。

經典場景

在開發過程中,有些時候我們要定義很多種狀態標,舉一個經典的權限操作的例子(來源於網上),假設這裏有四種權限狀態如下:

public class Permission {
	// 是否允許查詢
	private boolean allowSelect;
	
	// 是否允許新增
	private boolean allowInsert;
	
	// 是否允許刪除
	private boolean allowDelete;
	
	// 是否允許更新
	private boolean allowUpdate;
}

我們的目的是判斷當前用戶是否擁有某種權限,如果單個判斷好說,也就四種。但如果混合這來呢,就是2的4次方,共有16種,這就繁瑣了。那如果有更多權限呢?組合起來複雜度也就成倍往上升了。

應用分析

還是拿上邊的權限例子來說事,我們改造一下,運用二進制移位來表示:

public class NewPermission {
	// 是否允許查詢,二進制第1位,0表示否,1表示是
	public static final int ALLOW_SELECT = 1 << 0; // 0001

	// 是否允許新增,二進制第2位,0表示否,1表示是
	public static final int ALLOW_INSERT = 1 << 1; // 0010

	// 是否允許修改,二進制第3位,0表示否,1表示是
	public static final int ALLOW_UPDATE = 1 << 2; // 0100

	// 是否允許刪除,二進制第4位,0表示否,1表示是
	public static final int ALLOW_DELETE = 1 << 3; // 1000

	// 存儲目前的權限狀態
	private int flag;

	/**
	 *  重新設置權限
	 */
	public void setPermission(int permission) {
		flag = permission;
	}

	/**
	 *  添加一項或多項權限
	 */
	public void enable(int permission) {
		flag |= permission;
	}

	/**
	 *  刪除一項或多項權限
	 */
	public void disable(int permission) {
		flag &= ~permission;
	}

	/**
	 *  是否擁某些權限
	 */
	public boolean isAllow(int permission) {
		return (flag & permission) == permission;
	}

	/**
	 *  是否禁用了某些權限
	 */
	public boolean isNotAllow(int permission) {
		return (flag & permission) == 0;
	}

	/**
	 *  是否僅僅擁有某些權限
	 */
	public boolean isOnlyAllow(int permission) {
		return flag == permission;
	}
}

上邊代碼就是拋開常規的狀態表示法(移位表示),例如:

ALLOW_SELECT = 1 << 0,轉成二進制就是0001,二進制第一位表示Select權限。
ALLOW_INSERT = 1 << 1,轉成二進制就是0010,二進制第二位表示Insert權限。
ALLOW_UPDATE = 1 << 2,轉成二進制就是0100,二進制第三位表示Update權限。
ALLOW_DELETE = 1 << 3,轉成二進制就是1000,二進制第四位表示Delete權限。

你會發現上邊四種權限表示都有一個特點,那就是轉化成二進制中的“1”只佔用其中的某一位,其餘的全部都是0,這就爲接下來的位運算提供了極大的便利。我們用一個全局的整形變量flag來存儲各種權限的啓用和停用狀態,那麼得到的二進制結果中每一位的0或1都代表當前所在位的權限關閉和開啓,四種權限有16種組合方式,下邊就列舉一部分,大家可以看一下:

flag 查詢 新增 修改 刪除 說明
1(0001) 0 0 0 1 只允許查詢(即等於ALLOW_SELECT)
2(0010) 0 0 1 0 只允許新增(即等於ALLOW_INSERT)
4(0100) 0 1 0 0 只允許修改(即等於ALLOW_UPDATE)
8(1000) 1 0 0 0 只允許刪除(即等於ALLOW_DELETE)
3(0011) 0 0 1 1 只允許查詢和新增
12(1100) 1 1 0 0 只允許修改和刪除
0(0000) 0 0 0 0 都不允許
15(1111) 1 1 1 1 全都允許

四種權限有16種組合方式,這16種組合方式就都是通過位運算得來的,其中參與位運算的每個因子你都可以叫做掩碼(MASK),例如我要查詢是否有修改和刪除的權限我可以這樣:

if (permission.isAllow(NewPermission.ALLOW_UPDATE | ALLOW_DELETE)){
    ...
}

當然我也可以定義一個isAllowUpdateDelete()這樣的方法,這樣處理:

// 定義擁有修改和刪除權限的mask
private static final int ALLOW_UPDATE_DELETE_MASK = 12; 
// 是否擁有修改和刪除的權限
public boolean isAllowUpdateDelete(){
    return flag & ALLOW_UPDATE_DELETE_MASK;
}

...

// 用的時候這樣既可
if (permission.isAllowUpdateDelete()){
    ...
}

代碼中的常量ALLOW_UPDATE_DELETE_MASK就是我們定義的擁有某些操作的掩碼,這在Android源碼也是很常見的,這樣處理我們就不用建立List或者專門遍歷判斷一些相關權限了。

至此應該對掩碼有一個清楚的瞭解了吧,那位掩碼(BitMask)是什麼呢?

BitMask並不是一個類,也不是某種特殊的單位,它更像是一種思想。在BitMask中,使用一個數值來記錄各種狀態的集合,使用這個數值的每一位來表達每一種狀態。在Android中,一個普通的int類型,是32位,則可以表達32中不同的狀態而互不影響。

其實在開發過程中除了移位表示標識,大部分採用的是十六進制表示,還有十六進制和移位混合形式,這些在一些系統源碼中普遍體現。

源碼實例

在Android源碼中主要針對FLAG的運算有三種:

1.增加屬性 “|” 。

如果需要向flag變量中增加某個FLAG,使用"|"運算符 flag |= XXX_FLAG;

原因: 如果flag變量沒有XXX_FLAG,則“|”完後flag對應的位的值爲1,如果已經有XXX_FLAG,則“|”完後值不會變,對應位還是1。

2.包含屬性 “&” 。

如果需要判斷flag變量中是否包含XXX_FLAG,使用"&"運算符,flag & XXX_FLAG != 0 或者 flag & XXX_FLAG = XXX_FLAG。

原因: 如果flag變量裏包含XXX_FLAG,則“&”完後flag對應的位的值爲1,因爲XXX_FLAG的定義保證了只有一位非0,其他位都爲0,所以如果是包含的話進行“&”運算後值不爲0,該位上的值爲此XXX_FLAG的所在位上的值,不包含的話值爲0。

3.去除屬性 “&~” 。

如果需要去除flag變量的XXX_FLAG, 使用 “&~”, flag &= ~XXX_FLAG;

原因: 先對XXX_FLAG進行取反則XXX_FLAG原來非0的那一位變爲0,然後使用“&”運算後如果flag變量非0的那一位變爲0,則意味着flag變量不包含XXX_FLAG。

Configuration 類

比如Android源碼中的Configuration類。Configuration類專門描述手機設備上的配置信息,包括屏幕旋轉、屏幕方向、字體設置、縮放因子、軟鍵盤、移動信號等等,因此有很多種狀態配置,以下是部分配置:

    /** Constant for {@link #colorMode}: bits that encode whether the screen is wide gamut. */
    public static final int COLOR_MODE_WIDE_COLOR_GAMUT_MASK = 0x3;
    /**
     * Constant for {@link #colorMode}: a {@link #COLOR_MODE_WIDE_COLOR_GAMUT_MASK} value
     * indicating that it is unknown whether or not the screen is wide gamut.
     */
    public static final int COLOR_MODE_WIDE_COLOR_GAMUT_UNDEFINED = 0x0;
    /**
     * Constant for {@link #colorMode}: a {@link #COLOR_MODE_WIDE_COLOR_GAMUT_MASK} value
     * indicating that the screen is not wide gamut.
     * <p>Corresponds to the <code>-nowidecg</code> resource qualifier.</p>
     */
    public static final int COLOR_MODE_WIDE_COLOR_GAMUT_NO = 0x1;
    /**
     * Constant for {@link #colorMode}: a {@link #COLOR_MODE_WIDE_COLOR_GAMUT_MASK} value
     * indicating that the screen is wide gamut.
     * <p>Corresponds to the <code>-widecg</code> resource qualifier.</p>
     */
    public static final int COLOR_MODE_WIDE_COLOR_GAMUT_YES = 0x2;

    /** Constant for {@link #colorMode}: bits that encode the dynamic range of the screen. */
    public static final int COLOR_MODE_HDR_MASK = 0xc;
    /** Constant for {@link #colorMode}: bits shift to get the screen dynamic range. */
    public static final int COLOR_MODE_HDR_SHIFT = 2;
    /**
     * Constant for {@link #colorMode}: a {@link #COLOR_MODE_HDR_MASK} value
     * indicating that it is unknown whether or not the screen is HDR.
     */
    public static final int COLOR_MODE_HDR_UNDEFINED = 0x0;
    /**
     * Constant for {@link #colorMode}: a {@link #COLOR_MODE_HDR_MASK} value
     * indicating that the screen is not HDR (low/standard dynamic range).
     * <p>Corresponds to the <code>-lowdr</code> resource qualifier.</p>
     */
    public static final int COLOR_MODE_HDR_NO = 0x1 << COLOR_MODE_HDR_SHIFT;
    /**
     * Constant for {@link #colorMode}: a {@link #COLOR_MODE_HDR_MASK} value
     * indicating that the screen is HDR (dynamic range).
     * <p>Corresponds to the <code>-highdr</code> resource qualifier.</p>
     */
    public static final int COLOR_MODE_HDR_YES = 0x2 << COLOR_MODE_HDR_SHIFT;

Configuration類標識這設備的詳細信息,但是源碼編寫者也不可能把每一個很微小的細節都標識進來,這樣就太龐大了,他們會把基本使用標識進來,然後在定義一些場景掩碼(_MASK),通過這些場景掩碼在代碼邏輯中進行位掩碼實現所需要的功能:

    /**
     * Return whether the screen has a round shape. Apps may choose to change styling based
     * on this property, such as the alignment or layout of text or informational icons.
     *
     * @return true if the screen is rounded, false otherwise
     */
    public boolean isScreenRound() {
        return (screenLayout & SCREENLAYOUT_ROUND_MASK) == SCREENLAYOUT_ROUND_YES;
    }

    /**
     * Return whether the screen has a wide color gamut and wide color gamut rendering
     * is supported by this device.
     *
     * @return true if the screen has a wide color gamut and wide color gamut rendering
     * is supported, false otherwise
     */
    public boolean isScreenWideColorGamut() {
        return (colorMode & COLOR_MODE_WIDE_COLOR_GAMUT_MASK) == COLOR_MODE_WIDE_COLOR_GAMUT_YES;
    }

    /**
     * Return whether the screen has a high dynamic range.
     *
     * @return true if the screen has a high dynamic range, false otherwise
     */
    public boolean isScreenHdr() {
        return (colorMode & COLOR_MODE_HDR_MASK) == COLOR_MODE_HDR_YES;
    }

View繪製過程中onMeasure的參數

在自定義view中我們常常實現三種方法,其中有一個onMeasure方法,主要用於view繪製過程中的一個測量,Android開發的同學這點很清楚:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

其入參中有兩個參數“widthMeasureSpec”、“heightMeasureSpec”。這兩個參數都是32位int值,其中高2位是SpecMode(測量模式),低30位是SpecSize(在某種測量模式下,所測得的精確值)。

針對測量模式,系統預製了三種:

    /**
     * Measure specification mode: The parent has not imposed any constraint
     * on the child. It can be whatever size it wants.
     */
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;

    /**
     * Measure specification mode: The parent has determined an exact size
     * for the child. The child is going to be given those bounds regardless
     * of how big it wants to be.
     */
    public static final int EXACTLY     = 1 << MODE_SHIFT;

    /**
     * Measure specification mode: The child can be as large as it wants up
     * to the specified size.
     */
    public static final int AT_MOST     = 2 << MODE_SHIFT;

那我們在平時開發中如何取view的精確值(寬、高)呢,按理說只需要取後30位的值即可,左移兩位。如果用api去處理:MeasureSpec.getSize(widthMeasureSpec),然後我們深入系統源碼看一下系統是如何運作的:

    /**
     * Extracts the size from the supplied measure specification.
     *
     * @param measureSpec the measure specification to extract the size from
     * @return the size in pixels defined in the supplied measure specification
     */
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }

這裏就清楚了,原來系統也是用位掩碼處理的,我們再看一下掩碼MODE_MASK是怎麼表示的:

    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

看到有同學可能會問爲什麼是0x3呢?當你想到上邊的三種模式,不由的驚喜,原來MODE_MASK = UNSPECIFIED | EXACTLY | AT_MOST。MODE_MASK左移30位剛好是view的SpecMode,然後measureSpec再將SpecMode去除,剛好就是我們想要的SpecSize。

一個小問題

上邊也提到開發過程中針對位掩碼這些FLAG,會用到移位表示法、十六進制表示法、混合表示法,但十六進制表示法更爲常見,那麼這裏拋出一個小問題:爲什麼開發普遍用十六進制來定義FLAG?

其實開發過程中不固定使用哪種進制,8進制的也有用到,但是最終迴歸到的都是二進制,開發者普遍用十六進制主要是編碼習慣和更爲方便,具體原因個人總結有兩條:

  1. 縮短編寫空間,總不能用二進制32個1或者0來定義一個整形常量吧。
  2. 十六進制更容易轉化成二進制,因此在代碼閱讀和邏輯分析尤其是運用在位運算上更有優勢。

其他用法

1.判斷int型變量a是奇數還是偶數

a&1 = 0 偶數
a&1 = 1 奇數 

2.整數的平均值

對於兩個整數x,y,如果用 (x+y)/2 求平均值,會產生溢出,因爲 x+y 可能會大於INT_MAX,但是我們知道它們的平均值是肯定不會溢出的,我們用如下算法:

public int average(int x, int y){ 
    return (x&y)+((x^y)>>1); 
}

3.判斷一個正整數是不是2的冪

public boolean power2(int x) { 
    return ((x&(x-1))==0)&&(x!=0)}

總結

到此針對位運算的相關知識點終於完了,從起初的機器碼,到位運算規則,再到本篇的實用戰場,相信讀過這三篇的小夥伴一定有很大收穫。

在開發過程中運用位運算,有些時候可以極好的縮短編寫空間和良好的程序擴展性,但是並不是說位運算就是最好的,畢竟代碼是寫給人看的,我們的代碼要有可讀性可持續維護性,所以在開發過程中針對場景的不用,運用的策略也不同,避免濫用,良好運用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章