阅读 9138

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

前言

很高兴见到你!

上上周我在掘金碰巧遇到了一篇 用设计模式管理状态 的文章,一时兴奋不已,在评论区安利了,一直以来我司在封装商业级 SDK 时,使用的十六进制状态管理机制。

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

遗憾的是,通过评论区的只言片语,并不能让人体会到 十六进制状态管理 的真正魅力。

于是作为回馈,我特地分享了这一篇:当我们封装商业级 SDK 时,我们是怎么使用十六进制来完成状态管理。

😉

此外,是只有封装 SDK 这种大动作,才值得使用十六进制吗?不是的,恰恰相反,正因为 十六进制状态管理是如此地普适,乃至于连封装 SDK 都优先使用这种方式。

考虑到部分读者可能对十六进制本身不太了解,本文会连同十六进制一起介绍。

所以如果阅读完这篇文章,你对 十六进制的状态管理 有了感性的认识,那我的愿望也就达到了。

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

最开始对十六进制产生了兴趣,或者说,知道了它在什么时候能派上用场,是在 2015 年观看《火星救援》这部电影时发现的。

13.gif

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

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

12.gif

第二次接触十六进制是在 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 年经验的同事,联手完成当年扛鼎项目的核心功能时,因同事提出使用十六进制管理状态,而亲眼见证了十六进制在状态管理方面的绝佳优势。

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

4.png

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

该项目有个需求:当指定图形编辑的模式时,图形工具栏的按钮状态要随之发生配套性地变化。

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

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

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

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

map.png

如果是传统方式编写,我们势必会在类中为 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。
  • 模式的切换无需手动清洗,只需为状态集变量注入预置的常量状态集。
  • 模式的存取一步到位。
  • 模式的存储只需一个数据表字段。
  • 可直接在数据库中完成查询状态。

这样说,你理解了吗?

xzl短

看不过瘾?这里只为你 而准备了一份 简洁有力的 《重学安卓》认知地图 😉

作为额外附赠的答疑

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

为什么这样使用呢?

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

0x0001  0x0004    0x0020
   👇      👇        👇
  0001    0100   0010 0000
复制代码

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

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

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

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

关注下面的标签,发现更多相似文章
评论