巧用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/ 

推給大家一個技術交流會裙:

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