位运算(位掩码BitMask)的应用场景浅析

浅析

Posted by dks on January 18, 2019

位运算(位掩码BitMask)的应用场景浅析

在Java中,位运算符有:与(&)、非(~)、或(|)、异或(^)、移位(<< 和 >>)、无符移位(<<< 和 >>>)。这些运算符在日常编码中运用并不多,但在看 Android 源码时发现其运用并不少,那么位运算究竟有什么利弊,合适的应用场景是什么呢?下面我们通过几个例子来进行探讨。

例一 简单例子

先看两段代码,对比一下

// 下面是不使用位运算的代码
void enableFeatures(boolean enableFeature1, boolean enableFeature2, boolean enableFeature3) {
  this.feature1Enabled = enableFeature1;
  this.feature2Enabled = enableFeature2;
  this.feature3Enabled = enableFeature3;
}
boolean isFeature1Enabled() {
  return this.feature1Enabled;
}
boolean isFeature2Enabled() {
  return this.feature2Enabled;
}
boolean isFeature3Enabled() {
  return this.feature2Enabled;
}
void toggleFeature1() {
  this.feature1Enabled = !this.feature1Enabled;
}
void toggleFeature2() {
  this.feature2Enabled = !this.feature2Enabled;
}
void toggleFeature3() {
  this.feature3Enabled = !this.feature3Enabled;
}

..... 还有好多个方法没写。。。

// 下面是使用位运算的代码
public final static int FEATURE_FLAG_1 = 1 << 0;
public final static int FEATURE_FLAG_2 = 1 << 1;
public final static int FEATURE_FLAG_3 = 1 << 2;
boolean isFeatureEnabled(int featureFlag) {
  return (this.featureFlags & featureFlag) > 0;
}
void enableFeatures(int featureFlags) {
  this.featureFlags |= featureFlags;
}
void disableFeatures(int featureFlags) {
  this.featureFlags &= ~featureFlags;
}
void toggleFeatures(int featureFlags) {
  this.featureFlags ^= featureFlags;
}

在上面的例子中,如果不使用位运算,那么每加一个 feature 就得加几个方法。而使用位运算,仅仅需要加一行表示该 feature 的常量值即可。而且当这个数据需要被写到数据库的一个字段的时候,这种按位存储的方式的优势就更加明显了。当然,使用位运算表达方式 32 位整数只能表达 32 种 feature,64 位整数就只能表达 64 种 feature。这在大多数情况下也够多了。如果实在不够,那你还可以用两个、三个,甚至更多的整数。

下面我们模拟一个具体例子来实践一下

例二 一个实例

假设有这样一个场景:有一个排涝站对象,它的属性有:站编号、排水量、工作状态,工作状态有运行、故障、停止、其他4种,另外它有四台水泵,编号1、2、3、4,每台水泵有开、关两个状态,同时有一个特殊情况,当运行状态为停止时,四台泵状态都是关状态。这个例子已经很接近我们现实应用场景了,现在我们分别用传统方式和位运算的方式编写此对象。

以熟悉的面向对象的方式,我很快写完了:

public class Station {
	//站的四个状态
    public final static int STATE_NORMAL   = 0;
    public final static int STATE_ABNORMAL = 1;
    public final static int STATE_STOP     = 2;
    public final static int STATE_OTHER    = 3;

    @IntDef({STATE_NORMAL, STATE_ABNORMAL, STATE_STOP, STATE_OTHER})
    @Retention(RetentionPolicy.SOURCE)
    public @interface StationState {
    }

    private int stationCode;//站编号
    private int state;//站状态
    private int workload;//站工作量

	//四个泵
    private boolean pump1working;
    private boolean pump2working;
    private boolean pump3working;
    private boolean pump4working;

    public int getStationCode() {
        return stationCode;
    }

    public void setStationCode(int stationCode) {
        this.stationCode = stationCode;
    }

    public int getState() {
        return state;
    }

    public void setState(@StationState int state) {
        this.state = state;
        //停止状态时,四个泵是关状态
        if (state == STATE_STOP) {
            setPump1working(false);
            setPump2working(false);
            setPump3working(false);
            setPump4working(false);
        }
    }

    public int getWorkload() {
        return workload;
    }

    public void setWorkload(int workload) {
        this.workload = workload;
    }

    public boolean isPump1working() {
        return pump1working;
    }

    public void setPump1working(boolean pump1working) {
        this.pump1working = pump1working;
    }

    public boolean isPump2working() {
        return pump2working;
    }

    public void setPump2working(boolean pump2working) {
        this.pump2working = pump2working;
    }

    public boolean isPump3working() {
        return pump3working;
    }

    public void setPump3working(boolean pump3working) {
        this.pump3working = pump3working;
    }

    public boolean isPump4working() {
        return pump4working;
    }

    public void setPump4working(boolean pump4working) {
        this.pump4working = pump4working;
    }

下面是花了几根头发写完位运算的方式:

public class Test {
    private int flag; //站

    public int getFlag() {
        return flag & ((1 << (4 + 2 + 8 + 8)) - 1);
    }

    // 从右起: 4位 泵;5-6位 状态;7-14位 workload工作量;其他站号
//    private static final int PUMP_SHIFT     = 0;
    private static final int STATE_SHIFT    = 4;
    private static final int WORKLOAD_SHIFT = 6;
    private static final int CODE_SHIFT     = 14;


    public final static int MUSK_PUMP     = 7;//  00000000  0000 0000  00  1111
    public final static int MUSK_STATE    = 3 << STATE_SHIFT;//  00000000  0000 0000  11  0000
    public final static int MUSK_WORKLOAD = 255 << WORKLOAD_SHIFT;//  00000000  1111 1111  00  0000
    public final static int MUSK_CODE     = 255 << CODE_SHIFT;//  11111111  0000 0000  00  0000


    // 泵4位
    public final static int PUMP_1 = 0x1; //泵号1
    public final static int PUMP_2 = 0x2;
    public final static int PUMP_3 = 0x4;
    public final static int PUMP_4 = 0x8;

    @IntDef({PUMP_1, PUMP_2, PUMP_3, PUMP_4})
    @Retention(RetentionPolicy.SOURCE)
    public @interface Pump {
    }

    /**
     * @param pump 0x1 0x2 0x4 0x8 分别为单个泵,可以组合,如 0x6 代表 是否是 2、3号开启,1、4号关闭 状态
     */
    public boolean isPumpWorking(@Pump int pump) {
        return (flag & MUSK_PUMP & pump) == pump;
    }

    /**
     * @param pump 泵号
     */
    public void setPumpWorking(@Pump int pump) {
        this.flag |= (pump & MUSK_PUMP);
    }

    public void setPumpStopping(@Pump int pump) {
        this.flag &= ~(pump & MUSK_PUMP);
    }

    public void setPump(int working) {
        this.flag = flag;
    }

    /**
     * 直接设置四个泵状态
     *
     * @param pumpByte 四个泵状态
     */
    public void setAllPumpWorkingOrStopping(@IntRange(from = 0, to = (1 << STATE_SHIFT) - 1) int pumpByte) {
        this.flag = (flag & ~MUSK_PUMP) | (pumpByte & MUSK_PUMP);
    }


    // 状态2位
    public final static int BYTE_STATE_NORMAL   = 0;
    public final static int BYTE_STATE_ABNORMAL = 1 << STATE_SHIFT;
    public final static int BYTE_STATE_STOP     = 2 << STATE_SHIFT;
    public final static int BYTE_STATE_OTHER    = 3 << STATE_SHIFT;

    @IntDef({BYTE_STATE_NORMAL, BYTE_STATE_ABNORMAL, BYTE_STATE_STOP, BYTE_STATE_OTHER})
    @Retention(RetentionPolicy.SOURCE)
    public @interface ByteState {
    }


    public int getStateByByte() {
        return flag & MUSK_STATE;
    }

    public void setStateByByte(@ByteState int byteState) {
        this.flag = (flag & ~MUSK_STATE) | byteState;
        if (byteState == BYTE_STATE_STOP) {
            setAllPumpWorkingOrStopping(0);
        }
    }


    //工作量8位 0-255
    public int getWorkloadByByte() {
        return (flag & MUSK_WORKLOAD) >> WORKLOAD_SHIFT;
    }

    public void setWorkloadByByte(@IntRange(from = 0, to = ((1 << 8) - 1)) int workload) {
        this.flag = (flag & ~MUSK_WORKLOAD) | (workload << WORKLOAD_SHIFT);
    }


    //站编号 8位 0-255
    public int getStationCodeByByte() {
        return (flag & MUSK_CODE) >> CODE_SHIFT;
    }

    public void setStationCodeByByte(@IntRange(from = 0, to = ((1 << 8) - 1)) int code) {
        this.flag = (flag & ~MUSK_CODE) | (code << CODE_SHIFT);
    }


    public String getFlagString() {
        try {
            String       binaryString = Integer.toBinaryString(getFlag());
            StringBuffer buffer       = new StringBuffer(binaryString);

            for (int i = 0; i < 22 - binaryString.length(); i++) {
                buffer.insert(0, "0");
            }
            buffer.insert(4, " ");
            buffer.insert(9, ",");
            buffer.insert(14, " ");
            buffer.insert(19, ",");
            buffer.insert(22, ",");
            return buffer.toString();
        } catch (Exception e) {
            e.printStackTrace();
            return "";
        }

    }
    
    //移位运算的优先级低于算数运算,之前犯错了,需注意
}

解释一下,这里其实用一个22位的flag,来表示排涝站,从右向左,1~4位分别表示四台泵,1开0关;5、6位表示站的四种状态,00、01、10、11;7~14位表示工作量,范围是0-255;15~22位表示站编号,范围0-255;可以看到,我还贴心的写了得到flag的二进制字符串的方法,格式:0000 0001,0001 1000,00,0101,它表示站编号为1,工作量为24(二进制0001 1000,转换为10进制为24),状态为正常(00),四台泵分别为关开关开。并分别提供了 get、set 方法,get、set 方法的实现就是利用位运算。

我们可以这样使用:

        Station station = new Station();
        station.setStateByByte(BYTE_STATE_STOP);//状态
        station.setStationCodeByByte(255);//编号
        station.setWorkloadByByte(18);//工作量
        station.setPumpWorking(PUMP_1);//泵
        station.setPumpStopping(PUMP_1);
        station.setAllPumpWorkingOrStopping(6);//0101 即1关2开3关4开

		//打印结果
        System.out.print("\n" + station.getFlagString() + "\n");//格式化了的各个位信息

        System.out.print("\n" + "State: " + station.getStateByByte() + "\n");
        System.out.print("\n" + "State == BYTE_STATE_STOP: " + (station.getStateByByte() == 						BYTE_STATE_STOP) + "\n");

        System.out.print("\n" + "Workload: " + station.getWorkloadByByte() + "\n");

        System.out.print("\n" + "Code: " + station.getStationCodeByByte() + "\n");

        System.out.print("\n" + "PUMP_1: " + station.isPumpWorking(PUMP_1) + "\n");
        System.out.print("\n" + "PUMP_2: " + station.isPumpWorking(PUMP_2) + "\n");

上述代码的输出结果为:

1111 1111,0001 0010,10,0110

State: 32

State == BYTE_STATE_STOP: true

Workload: 18

Code: 255

PUMP_1: false

PUMP_2: true

接下来我们对比一下优劣。

  • 使用上:单从get和set方法上来看,两者使用起来并没有太大的区别,特别是站号、工作量、状态,但在四个泵状态的设置上,后者要精简许多。
  • 编写上:前者编写容易,我们很熟练;后者编写难度要高一些,而且很容易出错。
  • 可读性:源码上前者要好很多,后者如果没有注释可能要骂娘了。使用时的代码(对象的各种操作)后者可读性稍微好一些,并不显著。
  • 性能上:显然后者采用位运算,其运算速度要快一些,同时其内存开销也小,但是在数量级较小的情况下我们是感受不到的。
  • 扩展性:当属性增多时,增加属性,两者差不多;当泵大量增加时前者可能要改为List<Pump>,并合理处置编号问题,后者需增加相应的位。

结论:显然对于这种复合型的对象,位运算方式并不合适,但对于像各个泵,只有开关状态的场景,使用起来要方便很多,这应该是它的应用场景。

下面我们来看Android中的运用

例三 Intent

以下三段是 Android 中 Intent 的部分源码

    /** @hide */
    @IntDef(flag = true, prefix = { "FLAG_" }, value = {
            FLAG_GRANT_READ_URI_PERMISSION,
            FLAG_GRANT_WRITE_URI_PERMISSION,
            FLAG_FROM_BACKGROUND,
            FLAG_DEBUG_LOG_RESOLUTION,
            FLAG_EXCLUDE_STOPPED_PACKAGES,
            FLAG_INCLUDE_STOPPED_PACKAGES,
            FLAG_GRANT_PERSISTABLE_URI_PERMISSION,
            FLAG_GRANT_PREFIX_URI_PERMISSION,
            FLAG_DEBUG_TRIAGED_MISSING,
            FLAG_IGNORE_EPHEMERAL,
            FLAG_ACTIVITY_NO_HISTORY,
            FLAG_ACTIVITY_SINGLE_TOP,
            FLAG_ACTIVITY_NEW_TASK,
            FLAG_ACTIVITY_MULTIPLE_TASK,
            FLAG_ACTIVITY_CLEAR_TOP,
            FLAG_ACTIVITY_FORWARD_RESULT,
            FLAG_ACTIVITY_PREVIOUS_IS_TOP,
            FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS,
            FLAG_ACTIVITY_BROUGHT_TO_FRONT,
            FLAG_ACTIVITY_RESET_TASK_IF_NEEDED,
            FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY,
            FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET,
            FLAG_ACTIVITY_NEW_DOCUMENT,
            FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET,
            FLAG_ACTIVITY_NO_USER_ACTION,
            FLAG_ACTIVITY_REORDER_TO_FRONT,
            FLAG_ACTIVITY_NO_ANIMATION,
            FLAG_ACTIVITY_CLEAR_TASK,
            FLAG_ACTIVITY_TASK_ON_HOME,
            FLAG_ACTIVITY_RETAIN_IN_RECENTS,
            FLAG_ACTIVITY_LAUNCH_ADJACENT,
            FLAG_RECEIVER_REGISTERED_ONLY,
            FLAG_RECEIVER_REPLACE_PENDING,
            FLAG_RECEIVER_FOREGROUND,
            FLAG_RECEIVER_NO_ABORT,
            FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT,
            FLAG_RECEIVER_BOOT_UPGRADE,
            FLAG_RECEIVER_INCLUDE_BACKGROUND,
            FLAG_RECEIVER_EXCLUDE_BACKGROUND,
            FLAG_RECEIVER_FROM_SHELL,
            FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface Flags {}
   public static final int FLAG_GRANT_READ_URI_PERMISSION = 0x00000001;
   
   ...
   
   public static final int FLAG_ACTIVITY_MULTIPLE_TASK = 0x08000000;

   public static final int FLAG_ACTIVITY_CLEAR_TOP = 0x04000000;
    
   public static final int FLAG_ACTIVITY_FORWARD_RESULT = 0x02000000;
    
   ...
  
    public static boolean isAccessUriMode(int modeFlags) {
        return (modeFlags & (Intent.FLAG_GRANT_READ_URI_PERMISSION
                | Intent.FLAG_GRANT_WRITE_URI_PERMISSION)) != 0;
    }
    
    
    public @NonNull Intent setFlags(@Flags int flags) {
        mFlags = flags;
        return this;
    }

    public @NonNull Intent addFlags(@Flags int flags) {
        mFlags |= flags;
        return this;
    }

    public void removeFlags(@Flags int flags) {
        mFlags &= ~flags;
    }

而我们使用 Intent 时,可以如下这样使用,同时传入多个FLAG,相当方便。

    Intent intent = new Intent();
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION);

根据这个例子我们可以看到,当存在大量状态时,可采用标志位的方式,且16进制更可读性更强,通过合理配置每个位的表示,可做到灵活配置,不仅写代码容易,用起来也容易。

例四 View.MeasureSpec

我们来看一下它的部分源码

 public static class MeasureSpec { 
     
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

    /**
     * @hide
     */
    @IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
    @Retention(RetentionPolicy.SOURCE)
    public @interface MeasureSpecMode {
    }

    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;

     //构造方法
    public static int makeMeasureSpec(
            @IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
            @MeasureSpecMode int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }
     
    public static int makeSafeMeasureSpec(int size, int mode) {
        if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
            return 0;
        }
        return makeMeasureSpec(size, mode);
    }

    public static int getMode(int measureSpec) {
        //noinspection ResourceType
        return (measureSpec & MODE_MASK);
    }

    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }
 
 }

代码写的非常漂亮,MeasureSpec 实际上表现为一个32位的 int 值,高2位代表SpecMode,低3位代表了SpecSize。SpecMode指的是View的测量模式,有三种:UNSPECIFIED、EXACTLY、AT_MOST;SpecSize指的是当前测量模式下测量大小的值。不同于上个例子,这里不仅是标志位,还有值的表示。思考一下,我们在例二里的结论是这种对象并不适合位运算的方式,为什么这里要这么写呢?其实源码注释里写的很清楚了:MeasureSpecs are implemented as ints to reduce object allocation.This class is provided to pack and unpack the size, mode tuple into the int. MeasureSpecs 被打包成一个 int 来避免过多的对象内存分配,并提供了打包和解包方法。由于 View 中存在大量的频繁的测量操作,因此这里更多的是出于性能的考虑。

总结

通过这四个例子,我们可以总结出,适合位运算的应用场景有:

  1. 存在大量的状态标志位,表示各种不同的状态的组合时。通过16进制表示,位运算方式操作,可省去大量的get、set方法,调用时也直接了当,代码更易使用,尤其是 Boolean 类型。采用位运算优势在于代码的可读性易用性。

  2. 频繁创建和销毁或对象数量庞大,同时对性能有要求的对象。如同 MeasureSpec ,属性并不复杂,性能要求高,将其打包为 int ,对外部不透明,位运算很合适。采用位运算优势在于更小的内存分配。

  3. 其他特殊场景。如,颜色信息转换、C中的大端转小端等

    需要强调的是,位运算的核心优势是减小对象创建销毁的消耗和减少内存占用这两点,因为像int的创建销毁开销比自定义的对象要小的多、而且位运算的使用能够大大减少基本类型的使用数量,这些基本类型也会占用内存。还有其他优势如调用时的易用性、算数运算等。是否要使用位运算,其实是一个取舍的过程,是否牺牲可读性、编写难度等来换取性能。

其他方案

以下内容未做深入探究,仅供参考。个人认为性能不会太优。

可以替代位域(即我们所说的位运算)的更好的方案在《Effective Java》一书中,更推荐用EnumSet来代替位域:

位域表示法也允许利用位操作,有效的执行像union和intersection这样的集合操作。但位域有着int枚举常量所有的缺点,甚至更多。当位域以数字形式打印时,翻译位域比翻译简单的int枚举常量要困难很多。甚至要遍历位域表示的所有元素也没有很容易的方法。
public class Text {
    public static final int STYLE_BOLD          = 1 << 0;
    public static final int STYLE_ITALIC        = 1 << 1;
    public static final int STYLE_UNDERLINE     = 1 << 2;
    public static final int STYLE_STRIKETHROUGH = 1 << 3;

    public void applyStyles(int styles) {...}
}

调用:

text.applyStyles(STYLE_BOLD | STYLE_ITALIC);

改成EnumSet的写法是:

public class Text {

    public enum Style {
        BOLD, ITALIC, UNDERLINE, STRIKETHROUGH
    }

    public void applyStyles(Set<Style> styles) {
        System.out.println(styles);
    }
}

调用:

text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));

参考