位运算的那些事系列:
前两篇我重点针对位运算基础以及运算过程详细的进行了讲解说明,相信看过的小伙伴也都很明了了。那么基础有了,也知道运算过程了,那我们常见的战场在哪里呢?这就像排兵布阵一样,只阅读兵法,而没有实践和模拟,只能算纸上谈兵了。本篇就拉开帷幕直面开发中这个最常见的战场——位掩码(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进制的也有用到,但是最终回归到的都是二进制,开发者普遍用十六进制主要是编码习惯和更为方便,具体原因个人总结有两条:
- 缩短编写空间,总不能用二进制32个1或者0来定义一个整形常量吧。
- 十六进制更容易转化成二进制,因此在代码阅读和逻辑分析尤其是运用在位运算上更有优势。
其他用法
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);
}
总结
到此针对位运算的相关知识点终于完了,从起初的机器码,到位运算规则,再到本篇的实用战场,相信读过这三篇的小伙伴一定有很大收获。
在开发过程中运用位运算,有些时候可以极好的缩短编写空间和良好的程序扩展性,但是并不是说位运算就是最好的,毕竟代码是写给人看的,我们的代码要有可读性可持续维护性,所以在开发过程中针对场景的不用,运用的策略也不同,避免滥用,良好运用。
最后
我是i猩人,总结不易,转载注明出处,喜欢本篇文章的童鞋欢迎点赞、关注哦。