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 0100
, 0b0000 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/
推给大家一个技术交流会裙: