位运算的那些事(三)位掩码

前两篇我重点针对位运算基础以及运算过程详细的进行了讲解说明,相信看过的小伙伴也都很明了了。那么基础有了,也知道运算过程了,那我们常见的战场在哪里呢?这就像排兵布阵一样,只阅读兵法,而没有实践和模拟,只能算纸上谈兵了。本篇就拉开帷幕直面开发中这个最常见的战场——位掩码(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)}

总结

到此针对位运算的相关知识点终于完了,从起初的机器码,到位运算规则,再到本篇的实用战场,相信读过这三篇的小伙伴一定有很大收获。

在开发过程中运用位运算,有些时候可以极好的缩短编写空间和良好的程序扩展性,但是并不是说位运算就是最好的,毕竟代码是写给人看的,我们的代码要有可读性可持续维护性,所以在开发过程中针对场景的不用,运用的策略也不同,避免滥用,良好运用。

发布了47 篇原创文章 · 获赞 38 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/li0978/article/details/100714987