巧用Flag--Android Flag一探究竟

Android中存在着各种各样的Flag的使用,这篇文章主要研究一下这种Flag的运用方式。

为什么要用Flag

程序开发中,不同的场景可能需要不同的控制流;不同的控制流中,后置的控制流可能依赖于前置控制流的某一个属性或状态,这个时候,我们可以考虑引入标志位Flag,记录前置控制流中的属性或状态,在后置控制流执行时再去访问。在相对简单的场景中,我们完全可以这么处理:

boolean success = false;
public void putElephantIntoFridge(){

    //step 1
    openTheDoor();

    //step 2
    putElephant();

    //step3
    closeTheDoor();
}

private void closeTheDoor() {
    if (success) {
        lockTheDoor();
    } else {
        runAway();
    }
}

private void putElephant() {
    Log.i(TAG, "putElephant: ");
    Random random = new Random();
    success = random.nextInt(2) == 1;
}

private void openTheDoor() {
    Log.i(TAG, "openTheDoor: ");
}

private void lockTheDoor() {
    Log.i(TAG, "lockTheDoor: ");
}

private void runAway(){
    Log.i(TAG, "runAway: ");
}

像上面这种比较简单的控制流,我们可以直接考虑加一个标志位flag,来确定后续控制流的执行逻辑。 但是,如果控制流程中会有多个case影响到后续的执行分支,或者这些case对应的flag需要传递给其他的函数或方法,一昧地添加新的flag,要么会让整个函数控制流变得无比混乱,要么就会徒增大量的离散flag,导致关联的控制流需要新增多个flag输入参数。

这种情况下,我们可以考虑将flag聚拢在一起,通过状态集StateSet或者位运算Bit Operation来消除离散的flag信息,我们现在主要来探讨一下使用位运算来聚合flag的方案。

位运算与Flag

开始之前,先简单复习一下位运算的几个运算符:

操作符符 运算 使用 结果 例子
& 按位与 exp1 & exp2 按位比较exp1和exp2的data,如果都为1,结果为1,否则结果为0 1100&1010 = 1000
| 按位或 exp1 | exp2 按位比较exp1和exp2的data,如果有一个为1,结果为1;否则结果为0 1100 | 1010 = 1110
^ 按位异或 exp1 ^ exp2 按位比较exp1和exp2的data,如果取值不同,结果为1;否则结果为0 1100 ^ 1010 =0110
~ 按位非 !exp 按位翻转exp每一位的data,使得1变为0,0变为1 ~1100 =0011
<< 左移 exp<<n 将exp向左位移n位,丢弃左侧溢出的位数,右侧补0 1 << 1 = 2, 1<<3 = 8
>> 右移 exp>>n 将exp向右位移n位,丢弃右侧b位信息,左侧补0 2 >> 1 = 1, 1>>2 = 1/2

基于上述的运算符,我们可以考虑将二元的flag映射为某一个bit的取值,0 或 1,从而可以将多个flag最终汇总成一个总的flag集合,这个时候,我们仍需要提供获取原始flag的方法。这里&运算符刚好可以给我们提供类似于取值的能力:

  • A&B:判断A中是否有对应的B flag的取值,如果没有,那么A&B=0
  • A|B:往A中添加B,在每一个bit代表的flag都相互独立的前提下,我们可以将所有的flag汇总到一个参数里面
  • A&~B:移除A中B的flag,相当于重置B的flag。
  • A^B: 判断A和B的每一位是否有不同的bit取值,可以用来判断两个Flag是否有变化

如果一个flag有超过2个选项,那么,单纯使用一个bit没法完成flag的展示。此时,我们可以通过扩展bit的位数来实现,以flag有3个选项为例:

int FLAG_1 = 0b0001;
int FLAG_2 = 0b0010;
//int FLAG_3 = 0b0011;//wrong,当FLAG_1|FLAG_2时,有歧义
int FLAG_3 = 0b0100;
int MASK = 0b0111;

int PFLAG_1 = 0b1000;
int flag = PFLAG_1 | FLAG_1;//0b1001
boolean flag1Set = (flag & FLAG_1) != 0;//true
int flagValue = flag & MASK;
boolean flag1Set2 = flagValue == FLAG_1;//true
boolean flag2Set = (flag & FLAG_2) != 0;//false

需要注意的是,扩展了bit的位数之后,获取原始flag需要通过掩码MASK来获取,掩码相关的知识可以参考: en.wikipedia.org/wiki/Mask_(… 简单来说,通过设置掩码MASK指定bit的取值为1,再通过&运算,可以从混合的flag中提取到原始的flag。

Android中View Flag

基于以上的内容,我们可以看下Android 中View的Flag的相关使用。


以下内容基于Android 29源码

在View中,存在着mViewFlags以及mPrivateFlags, mPrivateFlags2, mPrivateFlags3这些flag,在现有的逻辑中,mViewFlags主要会记录UI展示相关的View状态,如是否可获焦,是否可点击,是否可长按点击等,而其余的三个privateFlags则是记录了view绘制流程的控制,或者当前是否正在执行动画,或者是否已经触发了layout等等。简单地展示一下mPrivateFlags关联的MASK

    /*
     * Masks for mPrivateFlags, as generated by dumpFlags():
     *
     * |-------|-------|-------|-------|
     *                                 1 PFLAG_WANTS_FOCUS
     *                                1  PFLAG_FOCUSED
     *                               1   PFLAG_SELECTED
     *                              1    PFLAG_IS_ROOT_NAMESPACE
     *                             1     PFLAG_HAS_BOUNDS
     *                            1      PFLAG_DRAWN
     *                           1       PFLAG_DRAW_ANIMATION
     *                          1        PFLAG_SKIP_DRAW
     *                        1          PFLAG_REQUEST_TRANSPARENT_REGIONS
     *                       1           PFLAG_DRAWABLE_STATE_DIRTY
     *                      1            PFLAG_MEASURED_DIMENSION_SET
     *                     1             PFLAG_FORCE_LAYOUT
     *                    1              PFLAG_LAYOUT_REQUIRED
     *                   1               PFLAG_PRESSED
     *                  1                PFLAG_DRAWING_CACHE_VALID
     *                 1                 PFLAG_ANIMATION_STARTED
     *                1                  PFLAG_SAVE_STATE_CALLED
     *               1                   PFLAG_ALPHA_SET
     *              1                    PFLAG_SCROLL_CONTAINER
     *             1                     PFLAG_SCROLL_CONTAINER_ADDED
     *            1                      PFLAG_DIRTY
     *            1                      PFLAG_DIRTY_MASK
     *          1                        PFLAG_OPAQUE_BACKGROUND
     *         1                         PFLAG_OPAQUE_SCROLLBARS
     *         11                        PFLAG_OPAQUE_MASK
     *        1                          PFLAG_PREPRESSED
     *       1                           PFLAG_CANCEL_NEXT_UP_EVENT
     *      1                            PFLAG_AWAKEN_SCROLL_BARS_ON_ATTACH
     *     1                             PFLAG_HOVERED
     *    1                              PFLAG_NOTIFY_AUTOFILL_MANAGER_ON_CLICK
     *   1                               PFLAG_ACTIVATED
     *  1                                PFLAG_INVALIDATED
     * |-------|-------|-------|-------|
     */

由于位数的限制,单个flag的能支持汇总的flag最多也就32个,而随着版本的增加,View中新增了部分新的属性,需要新的flag去控制逻辑。因此,现在的private的汇总flag已经有3个了。随着后续Android版本的提升,更多的feature会被引入,可能后面我们能看到更多的privateFlags出现在代码中。

几个常见的Flag解析

Visibility

控制View的可见性应该是我们在开发中比较常操作的事情,在控制Visibility中,实际上我们调用的View.setVisibility,直接对接了mViewFlags的修改,

public void setVisibility(@Visibility int visibility) {
	setFlags(visibility, VISIBILITY_MASK);
}
...

void setFlags(int flags, int mask) {
    final boolean accessibilityEnabled =
            AccessibilityManager.getInstance(mContext).isEnabled();
    final boolean oldIncludeForAccessibility = accessibilityEnabled && includeForAccessibility();

    int old = mViewFlags;
    mViewFlags = (mViewFlags & ~mask) | (flags & mask);
	.....
}

在获取view的可见性时,实际上是通过VISIBLITY_MASK,从mViewFlags中直接访问出来的。

@Visibility
public int getVisibility() {
    return mViewFlags & VISIBILITY_MASK;
}

public static final int VISIBLE = 0x00000000;

/**
 * This view is invisible, but it still takes up space for layout purposes.
 * Use with {@link #setVisibility} and <a href="#attr_android:visibility">{@code
 * android:visibility}.
 */
public static final int INVISIBLE = 0x00000004;

/**
 * This view is invisible, and it doesn't take any space for layout
 * purposes. Use with {@link #setVisibility} and <a href="#attr_android:visibility">{@code
 * android:visibility}.
 */
public static final int GONE = 0x00000008;

/**
 * Mask for use with setFlags indicating bits used for visibility.
 * {@hide}
 */
static final int VISIBILITY_MASK = 0x0000000C;

这里其实可以看到,掩码需要能够覆盖到各个可选项的各个bit,从0b0000 0000, ob0000 01000b0000 1000, 三者中找到最小的,可覆盖三个可选项的掩码,应该就是0b0000 1100, 即0x000C

Focusable

同样地,Focusable的对应API中,也包含着对三个FLAG和一个MASK的应用

public static final int NOT_FOCUSABLE = 0x00000000;
public static final int FOCUSABLE = 0x00000001;
public static final int FOCUSABLE_AUTO = 0x00000010;

/**
 * Mask for use with setFlags indicating bits used for focus.
 */
private static final int FOCUSABLE_MASK = 0x00000011;

而判断View是否可获焦的API: isFocusable,实际上也可以这么实现:

//原版
public final boolean isFocusable() {
    return FOCUSABLE == (mViewFlags & FOCUSABLE);
}

//修改版
public final boolean isFocusable() {
    return FOCUSABLE == (mViewFlags & FOCUSABLE_MASK);
}
···

结语

除了View相关代码使用到了Flag之外,Android中还有很多地方使用到这种Flag参数来简化参数,比如常见的Intent.setFlag, MeasureSpec, WindowManager.LayoutParams等等,有兴趣的童鞋可以找其他的资料深入了解一下,本文就不再一一介绍了。

这里有我自己整理的一些进阶资料与面试题:https://shimo.im/docs/VYcc3wyJRpy9jJ83/ 

推给大家一个技术交流会裙:

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