就算不去火星种土豆,也请务必掌握的 Android 状态管理最佳实践!

18,909 阅读7分钟

前言

上周在掘金巧遇一篇 “用设计模式管理状态” 文章,作为补充,在评论区安利我司封装商业级 SDK 时常用的 “十六进制状态管理机制”。

原以为无人对此感兴趣,没想到留言很快便收到文章作者回复,且在评论区耐心和我探讨设计模式 独占式状态机 和十六进制 复合状态管理 使用场景区别。

遗憾的是,通过评论区只言片语,难让人体会 “十六进制状态管理” 真正魅力,

故今日我们以封装商业级 SDK 为例,拆解我们是如何使用十六进制完成状态管理,相信阅读后你会豁然开朗。

文章目录一览

  • 前言
  • 我和十六进制的 “三次握手”
  • 使用十六进制前的混沌世界
  • 十六进制能很好解决这些问题
  • 十六进制运作机制
  • 十六进制状态管理实战
  • 十六进制状态存取实战
  • 综上
  • 作为额外附赠的答疑

我和十六进制的 “三次握手”

最初对十六进制产生兴趣,或说知道它何时可派上用场,是 2015 年观看电影《火星救援》时发现。

图一

为和 “远在 4 亿公里外、电磁波需 40 分钟才能完成一次完整请求响应” 的地球通信,孑然一身主角 Mark 想到一办法,即是通过十六进制和 ACSII 码表:

将字符转换成字母 ,这样地球上的人便可通过控制 “Mark 从沙漠中掏来的 1997 年服役的探路者号(PathFinder)” 摄像头偏移角度,来指明一连串字符,从而 Mark 可将它们转译成英文。

图二

第二次接触十六进制是在 2016 年,当我阅读 View/ViewGroup 源码,发现状态多通过十六进制管理,但当时因不知为何这么使用、这样使用究竟有什么好处,也就没大注意。

@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
    if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
        // We're already in this state, assume our ancestors are too
        return;
    }
    if (disallowIntercept) {
        mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
    } else {
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    }
    // Pass it up to our parent
    if (mParent != null) {
        mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
    }
}

直到 2017 年夏天,我和一位彼时 3 年经验同事联手完成核心项目重构时,因同事提出使用十六进制管理状态,而亲眼见证十六进制在状态管理方面绝佳优势。

为纪念这一分享,此后每当有新同事入职,我提供的培训课程必包含十六进制状态管理。

使用十六进制前的混沌世界

该项目有个需求:当指定图形编辑模式,图形工具栏按钮状态需随之发生改变。

例如,存在 3 种图形编辑模式,和 8 个图形编辑按钮。

模式 A 下,要求 按钮1、按钮2、按钮3 可用,其余按钮禁用。

模式 B 下,要求 按钮1、按钮4、按钮5、按钮6 可用,其余按钮禁用。

模式 C 下,要求 按钮1、按钮7、按钮8 可用,其余按钮禁用。

图三

如是传统方式编写,我们势必会在类中为 3 个模式定义 boolean 变量,为 8 个按钮状态定义 boolean 变量。

那么模式切换时,就需将每个按钮状态的变量都 “清洗” 一遍。例如:

public void setModeA() {
    status1 = true;
    status2 = true;
    status3 = true;
    status4 = false;
    status5 = false;
    status6 = false;
    status7 = false;
    status8 = false;
}
​
public void setModeB() {
    status1 = true;
    status2 = false;
    status3 = false;
    status4 = true;
    status5 = true;
    status6 = true;
    status7 = false;
    status8 = false;
}
​
public void setModeC() {
    ...
}

当日后模式变多、按钮状态变多,类中就会满是这种 setMode 方法,看起来很蠢,且密密麻麻的 true、false 极易出错。

这是一点。

另一点是,如按钮状态是用 boolean 变量管理,那么状态的存储和读取便难办,

  • 每个 boolean 变量都需转换成 int 类型 0 或 1 存储在数据库中。
  • 数据库需为每个状态准备一个字段。
  • 读取时又需负责将每个状态转译回 boolean。

这工作量很大,且日后每添加或修改一状态,数据库都需新增或修改字段,十分低效和不安全。

十六进制能很好解决这些问题

十六进制可做到:

  • 通过状态集的注入,一行代码即可完成模式切换。
  • 无论再多状态,都只需一个字段来存储。状态被存放在 int 类型状态集中,可直接读写于数据库。

十六进制运作机制

在具体了解十六进制是怎么做到状态管理最佳实践前,我们先简单过一遍十六进制本身运作机制。

首先,在编程中,利用开头 0x 表示十六进制数。

例如 0x0001,0x0002。

然后,十六进制的计算,可借助二进制 “按位计算” 方式理解。

二进制存在 与、或、异或、取反 等操作:

a & ba | ba ^ b,~a

例如,十六进制数 0x0004 | 0x0008,可理解为:

0100 
 |
1000
 =
1100

十六进制 (0x0004 | 0x0008) & 0x0004 可得:

1100 
 &
0100
 =
0100

也即 “状态集” 包含某状态时,再 & 该状态,便得非 0 结果。

于是,我们便可利用该特性完成状态管理:

十六进制状态管理实战

  • 首先定义一个 “状态集” 变量,用于存放 “当前状态集”,例如:
private int STATUSES;
  • 然后定义十六进制状态常量,和 “模式状态集”,例如:
private final int STATUS_1 = 0x0001;
private final int STATUS_2 = 0x0002;
private final int STATUS_3 = 0x0004;
private final int STATUS_4 = 0x0008;
private final int STATUS_5 = 0x0010;
private final int STATUS_6 = 0x0020;
private final int STATUS_7 = 0x0040;
private final int STATUS_8 = 0x0080;
​
private final int MODE_A = STATUS_1 | STATUS_2 | STATUS_3;
private final int MODE_B = STATUS_1 | STATUS_4 | STATUS_5 | STATUS_6;
private final int MODE_C = STATUS_1 | STATUS_7 | STATUS_8;
  • 当需往 “状态集” 添加状态时,就通过 “或” 运算。例如:
STATUSES | STATUS_1
  • 当需从 “状态集” 移除状态时,就通过 “取反” 运算。例如:
STATUSES & ~ STATUS_1
  • 当需判断 “状态集” 是否包含某状态时,就通过 “与” 运算。结果为 0 即代表无,反之有。
public static boolean isStatusEnabled(int statuses, int status) {
   return (statuses & status) != 0;
}
  • 当需切换模式时,可直接将预先定义的 “模式状态集” 赋予给 “状态集” 变量。例如:
STATUSES = MODE_A;

如此,复杂度从 m * n 骤减为 m + n,随着日后模式和状态增多,十六进制优势将指数级增长。

是不是超简洁?再也无需定义和修改各种 “setModeXXX” 方法。

而且这还只是一半。另一半是关于十六进制状态的存取。

十六进制状态存取实战

由于状态集是 int 类型,因而我们最少只需一个字段,即可存储状态集:

insert into tableXXX TITLE,DATE,STATUS values ('xxx','20190703',32)

读取也十分简单,读取后直接赋值给 STATUSES 即可。

除此之外,你还可直接在 SQL 中通过按位计算来查询。例如查询包含状态 0x0004 的记录:

select * from tableXXX where STATUS & 4 != 0

综上

未用十六进制的日子里,状态管理是个繁琐、极易出错的操作。

有了十六进制后:

  • 模式管理复杂度从 m * n 骤减至 m + n。
  • 模式切换无需手动清洗,只需为状态集变量注入预置的常量状态集。
  • 模式存取一步到位。
  • 模式存储只需一个数据表字段。
  • 可直接在数据库中完成查询状态。

作为额外附赠的答疑

Q1: 细心朋友可能注意到,声明状态都是 1、2、4、8,然后进一位继续。

为何这样使用?

因为当十六进制转成二进制计算时,十六进制每位数占 4 个二进制位,例如:

0x0001  0x0004    0x0020
   👇      👇        👇
  0001    0100   0010 0000

且唯有独占每个二进制位,我们才能区分出不同状态。

因而我们对十六进制数的每一位只安排 1、2、4、8,一旦用完,就前进一位继续。

Q2: 既然如此,状态声明为何不直接用二进制来表示?

原因很简单 —— 一目了然 0x4218 和密密麻麻 0100001000011000b,在代码中声明哪个更费时、更费眼、更易出错? —— 特别是当有 20、30 个状态要声明呢?