基础结构总览

参考这篇文章 用树实现客户端红点系统 ,在说明红点系统的结构的时候,独立的将整个系统分为了独立的三个部分:结构层,驱动层和表现层。
接下来我们也会以这样的结构来进行阐述。

这篇文章的讲明了基本的红点树的结构和思路.

  1. 结构层
    对于树的构建,通过枚举字符串命名去确定树节点的数据来源,然后整合数据区初始化树的数据。
    而它与驱动层的关联可以体现在节点的两个方面:
  • 定义一个委托,用于外面注册传递红点数量发生变化时候的自定义回调函数。方便实现每个节点的红点数量变化的自定义内容。
  • 封装一下节点的红点数量变化的接口,并把功能和委托调用进行关联。
  1. 驱动层
    驱动层是用来提供给外界修改对应节点红点数量以触发其回调事件用的,我们往往会在管理器里面封装成接口,供外部调用。

  2. 表现层
    表现层来自UI自定义设置,案例里面给的能够在初始化的时候就设置好所有的红点数据和页面信息。当然有不能完全展示之类的其他情况,这个我们后面会说。

通过这种实现形式我们能做到的是:

  • 整个系统,三个层面的逻辑分明,各司其职。
  • 红点的数据来源和控制和数的结构以及整个红点系统并没发生耦合。

但是看完之后你可能会产生一些觉得疑惑的点:

  1. 对于树构建的写法是否还可以进行优化?
  2. 是否需要像DEMO演示一样一开始就需要拿到所有红点数据?
  3. 实际开发中,不可能在一开始就初始化好所有的UI页面,对于未初始化的页面和需要动态生成的item这些,其页面功能显示和数据是如何处理的?

重点思路

使用前缀树构建树节点

前缀树,是一种多叉树,多用于对字符串的保存和文本的索引,具有相同前缀的字符串,拥有相同的父节点,结合合理的索引方式,可以高效便捷的查找到目标文本内容。
通过将红点项转化为路径,再将路径和当前红点项的红点数量存储于节点中,可以十分方便的供其他模块进行查询和管理。

用树实现客户端红点系统这篇文章中,我们其实也能看见把枚举路径字符串给切割成数组去初始化树 和 在搜索查找的时候都是通过把字符串才分节点列表去查找的。
只是对于每一个字符串的切割,每需要得到一个我们想要的节点的时候就要进行一次切割,当然这种形式也可以进行优化,比如使用可变传输,传入逐级的单项字符串路径等。

红点系统的显示不应该依赖于UI

正如上面我们考虑到的疑惑3一样,对于未初始化或者需要动态生层的页面,但是根据路径层层往上的话,最终还是会来需要显示的页面,比如主页面这种。
这其实就是数据和页面显示的分离,当页面没有被初始化的时候,如果我们需要显示其对应路径的红点,其实我们只需要有对应的节点的数据就可以。
也就是说如果需要显示红点,其页面可以不初始化或者生成,但是我们需要有对应节点的数据。
而主页面这种,处于路径中的节点,在其页面被初始化的使用就应该动态的去获取经过其节点的红点数据,从而进行显示。
所以概括一下就是 “UI红点基于数据动态显示红点,而红点的数据不依赖UI的页面情况”。

首先理解这个思想,然后基于这个思想,在说一下像是邮件这种可能在运行时候动态创建的显示需求,其红点要如何处理。
要实现这样的需求,首先对于其节点的构建我们就不能够写死,我们需要实现基本的路径不变,而能够路径中部分的发生改变,因此需要为动态实例设置复合的键名,我们可以做出如下定义:

1
public const string MAIL_PATH_PREFIX = "Mail.{邮箱ID}.{邮件ID}";

当我们受到服务器或者其他来源的数据,比如邮箱1里面的邮件101,其路径就可以是: “Mail.1.101”。

根据这样的节点信息,我们就可以通过调用结构层注入数据,去动态的创建对应的红点树节点,因为结构层内置的逻辑会刷新其上层红点数量信息,从而实现动态添加。
放在这个案例中就是我们的页面目前显示着的是主页面,邮件页面根本没有被初始化,但是主页面存在邮件的按钮入口,此按钮入口是注册了对应邮件红点树节点的回调函数,并关联了自身的红点表现效果。此时邮件信息更新,对应深层的红点树节点的红点数量得到了刷新,而层层往上进行通知,最终通过驱动层关联上了主页面“邮件”按钮的红点变化表示,因此其红点表示能正常显示。
而当我们点击邮件按钮,邮件页面初始化的时候就可以进行动态绑定和红点数据检查,从而实现邮件页面的红点显示。这个效果对于使用无限列表的邮件Item也是一样的逻辑。

红点数量刷新的优化

当一个节点存在多个子节点,且其多个子节点在同一时间需要更新数据的时候,如果都去逐个通知其父节点,就会造成多次冗余的刷新。这种情况在类似背包道具大量获取的情况下会出现。
用树实现客户端红点系统这篇文章中作者为此创建了一个用来存储需要更新的父节点的“脏池子”,当节点数量变化导致其父节点的红点数量需要更新的时候,就会把对父节点做脏标记,当一下帧的Update方法驱动调用的时候,遍历截止当前帧的所有被标记位脏的父节点,由父节点来主动查询自己所需的节点值,如果节点值发生变化,则响应外部注册的监听方法。
这就是经典的“脏标记+延迟更新”的手法,可以有效的减少刷新次数,提高效率。

处理红点逻辑需要依赖多个条件组合

这个需求可以从两个方面进行考虑

  • 拓展红点系统,无论是考虑拓展红点的字符串表达式并添加对应的解析器,还是选择通过配表的形式给对应红点路径节点添加条件,其根本核心就是在红点系统里面拓展条件判定功能,也就是说在刷新红点数据的时候会同时通过条件判断系统去判断给出的条件是否满足,只有满足了才会进行此节点红点数据的刷新。
    这种就是用来处理数据会早一步拿到手,而条件判定的功能只是限制手段。

  • 拓展条件判定功能模块。比如当玩家等级提升派发事件的时候,再去刷新对应的红点数据,也就是达成这个条件之后,红点的数据才会拿到手。

如果只是简单的红点树拓展的话,第二种方式其实够用了,实际项目中很多也用的是第二种形式。

分帧

首先 分帧的原理一句话概括就是把一帧里面的内容分到几帧里面去执行。为了实现这个效果其实只需要做到以下几个点就行了:

  1. 把要执行的方法缓存下来,储存到缓存器中
  2. 执行缓存器里面的方法,执行到限定的数量之后,不再执行,把它放到下一帧去

说到这里可能就有人会说,这样的会不会导致页面刷新出现延迟的情况?
答案是会的,当数量实在是太多的,如果采用这种形式,就必然会出现这样的问题,而相比把这众多的量的刷新都放在一帧里面去所造成的性能问题,这更倾向于一种妥协。
也因此,合理的调整好每一帧执行的数量便显得至关重要。

为了实现这样的功能效果,我们首先就需要制定对应的规则,也就是要确定“哪个模块的功能,一帧限制执行几次” 这两个问题。

首先说第二个问题,原则上这里我们可以通过配表和枚举来做,其实配表也需要通过枚举,且这本来来就是前端的内容,不需要策划掺一脚,因此我们采用枚举来做就好。

那么第一件事,使用枚举定义好每个需要分帧的功能模块的定义和其对应限制分帧的次数

比如说,我们可以做出如下定义:

1
2
3
local FrameTypeDefine = {
RedPoint_BagItem = {1001,2},
}

其中,1001代表这个功能模块的ID,2代表限制一帧执行的次数。

那么既然我们的待执行事件都是添加列表缓存起来,然后限制执行的,接下来我们就应当考虑如何封装执行分帧的方法,也就是需要把我们要执行的内容添加到待执行列表中去。
这里主要要考虑 对应执行的方法,执行的对象,传参,以及执行次数。
这里用Lua来表示一个类型的缓存数据结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
local cacheData = {
[1001] = {
funcs = {
[114514] = function()
end,
[114515] = function()
end
},
objs = {
[114514] = { "模拟对象1" },
[114515] = { "模拟对象2" }
},
params = {
[114514] = {
param1 = 1,
param2 = 2,
param3 = 3,
},
[114515] = {
param1 = 4,
param2 = 5,
param3 = 6,
}
},
frameCount = 2,
},
}

其中,1001表示功能模块,funcs表示需要执行的方法,objs表示方法执行的对象,params表示方法执行的参数,frameCount表示限制执行的次数。而114514和114515则是执行此方法判断的唯一标识。

在确保拿到正确的缓存数据后,我们接下来就需要把它加入待执行的列表中去。
注意,这里准确来说把当前的缓存数据缓存下来,加入待执行列表后,会是在下一帧去开始执行这个功能。当执行了对应的次数之后,如果没有执行完成,则会放到下一帧去,否则只是会移除当前的数据。

把它关联到红点的话,其实就是,当我们需要刷新红点的时候,我们并不直接调用对应红点模块的刷新方法,而是把它注册到分帧事件里面去,通过分帧的机制,去执行这样的一个功能,从而达到性能优化的目的。

参考文章