目录
前言
上周在掘金巧遇一篇 “用设计模式管理状态” 文章,作为补充,在评论区安利我司封装商业级 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 & b,a | b,a ^ 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