UFL7-3——UI管理器的异步加载优化
UFL7-3——UI管理器的异步加载优化
为什么要进行异步加载优化
我们之前制作UI管理器时,加载资源时是在测试模式下,始终使用的是编辑器同步加载模式
若真正使用异步加载时,可能会存在报错风险
举例重现问题:
-
构建AB包
-
采用异步加载方式加载AB包中的UI面板资源
将
ABResManager
的isDebug
设置为false
,使得加载会选择AB包加载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
28
29
30using UnityEngine;
using UnityEngine.Events;
/// <summary>
/// 用于进行加载AB相关资源的整合,在开发中可以通过EditorResManager去加载对应资源去进行测试
/// </summary>
public class ABResManager : BaseManager<ABResManager>
{
private bool isDebug = false;
private ABResManager() { }
public void LoadResAsync<T>(string abName, string resName, UnityAction<T> callBack, bool isSync = false) where T : Object
{
if (isDebug)
{
//我们自定义了一个AB包中资源的管理方式,对应文件夹名就是包名
T res = EditorResManager.Instance.LoadEditorRes<T>($"{abName}/{resName}");
callBack?.Invoke(res as T);
}
else
{
ABManager.Instance.LoadResAsync<T>(abName, resName, callBack, isSync);
}
ABManager.Instance.LoadResAsync<T>(abName, resName, callBack, isSync);
}
} -
同一帧显示两次UI面板
在实际开发时,有可能会不同地方出现先显示UI面板,然后加载尚未结束的时候再次调用显示同一面板的方法,
这里使用最极端的情况,在同一帧加载两次UI面板1
2
3
4
5void Start()
{
UIManager.Instance.ShowPanel<BeginPanel>();
UIManager.Instance.ShowPanel<BeginPanel>();
}输出:
可以发现,这样调用会导致重复加载和重复添加的问题,字典会因为重复加载而报错,
Canvas
对象上会挂载两个同一面板 -
同一帧显示又隐藏UI面板
在实际开发时,有可能会不同地方出现先显示UI面板,然后加载尚未结束的时候调用隐藏同一面板的方法,
1
2
3
4
5void Start()
{
UIManager.Instance.ShowPanel<BeginPanel>();
UIManager.Instance.HidePanel<BeginPanel>();
}面板会无视隐藏面板方法继续显示:
由于尚未加载成功,因此隐藏不会生效,面板依然会加载出来
优化异步加载问题
主要制作思路:
-
造成问题的关键点
由于异步加载,字典容器中没有及时存储将要显示的面板对象,我们需要在显示面板时,一开始就存储面板的相关信息
这样不管是二次显示还是隐藏,都能够知道是否已经在加载面板了1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// 主要是用于里氏替换原则,在字典中,用父类容器装载子类对象
private abstract class BasePanelInfo { }
// 用于存储面板消息和加载完成的回调函数
private class PanelInfo<T> : BasePanelInfo where T : BasePanel
{
public T panel;
public UnityAction<T> callBack;
public PanelInfo(UnityAction<T> callBack)
{
this.callBack += callBack;
}
}
// 用于存储所有的面板对象
private Dictionary<string, BasePanelInfo> panelDic = new Dictionary<string, BasePanelInfo>(); -
分情况考虑问题(异步加载中 和 异步加载结束)
-
显示相关
- 若加载中想要显示,应该记录回调,加载结束后统一调用
- 若加载结束后想显示,直接显示
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62// 用于存储面板消息和加载完成的回调函数
private class PanelInfo<T> : BasePanelInfo where T : BasePanel
{
public T panel;
public UnityAction<T> callBack;
public PanelInfo(UnityAction<T> callBack)
{
this.callBack += callBack;
}
}
// 用于存储所有的面板对象
private Dictionary<string, BasePanelInfo> panelDic = new Dictionary<string, BasePanelInfo>();
public void ShowPanel<T>(E_UILayer layer = E_UILayer.Middle,
UnityAction<T> callBack = null,
bool isSync = false) where T : BasePanel
{
//获取面板名,预设体名必须与面板类名一致
string panelName = typeof(T).Name;
//存在面板
if (panelDic.ContainsKey(panelName))
{
//取出消息
PanelInfo<T> panelInfo = panelDic[panelName] as PanelInfo<T>;
//正在异步加载中
if (panelInfo.panel == null)
{
//将回调添加到记录中
if (callBack != null)
panelInfo.callBack += callBack;
}
//已经加载结束
else
{
panelInfo.panel.ShowMe();
//直接执行回调,直接传递出去即可
callBack?.Invoke(panelInfo.panel);
}
return;
}
//不存在面板,先存入字典当中占位,之后如果又显示,我才能得到字典中的消息进行判断
panelDic.Add(panelName, new PanelInfo<T>(callBack));
ABResManager.Instance.LoadResAsync<GameObject>("ui", panelName, (res) =>
{
//层级的处理
Transform layerObj = GetLayerObj(layer);
//避免没有按照指定规则传递参数,避免为空
if (layerObj == null)
layerObj = middleLayer;
//将面板预设体创建到对应父对象下,并且保持原本的缩放大小
GameObject panelObj = GameObject.Instantiate(res, layerObj, false);
PanelInfo<T> panelInfo = panelDic[panelName] as PanelInfo<T>; //取出消息
T panel = panelObj.GetComponent<T>(); //获取对应的UI控件返回出去
panel.ShowMe(); //显示面板时执行的默认方法
panelInfo.panel = panel; //将加载出来的panel记录到panelInfo内
panelInfo.callBack?.Invoke(panel); //传递到外部使用
panelInfo.callBack = null; //清空回调,避免内存泄露
}, isSync);
} -
隐藏相关
- 若加载中想要隐藏,应该改变标识,并清空回调
- 若加载结束想要隐藏,直接隐藏
- 若压根没有,不用处理
同时,也要修改显示的逻辑,如果加载完毕后发现隐藏标识为
true
,则不执行后续逻辑
如果在加载期间,隐藏后又执行了显示方法,则将隐藏标识设置为false
,并添加回调,这样加载出来过后不会忽略后续隐藏逻辑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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95// 用于存储面板消息和加载完成的回调函数
private class PanelInfo<T> : BasePanelInfo where T : BasePanel
{
public T panel;
public UnityAction<T> callBack;
public PanelInfo(UnityAction<T> callBack)
{
this.callBack += callBack;
}
}
// 用于存储所有的面板对象
private Dictionary<string, BasePanelInfo> panelDic = new Dictionary<string, BasePanelInfo>();
public void HidePanel<T>() where T : BasePanel
{
string panelName = typeof(T).Name;
if (panelDic.ContainsKey(panelName))
{
//取出消息
PanelInfo<T> panelInfo = panelDic[panelName] as PanelInfo<T>;
//如果存在,但正在加载
if (panelInfo.panel == null)
{
//修改隐藏标识,标识这个面板将要隐藏,因为要隐藏,因此将回调置空
panelInfo.isHide = true;
panelInfo.callBack = null;
}
//已经加载结束
else
{
//销毁后从容器中移除
panelInfo.panel.HideMe();
GameObject.Destroy(panelInfo.panel.gameObject);
panelDic.Remove(panelName);
}
}
}
public void ShowPanel<T>(E_UILayer layer = E_UILayer.Middle,
UnityAction<T> callBack = null,
bool isSync = false) where T : BasePanel
{
//获取面板名,预设体名必须与面板类名一致
string panelName = typeof(T).Name;
//存在面板
if (panelDic.ContainsKey(panelName))
{
//取出消息
PanelInfo<T> panelInfo = panelDic[panelName] as PanelInfo<T>;
//正在异步加载中
if (panelInfo.panel == null)
{
//将回调添加到记录中
panelInfo.isHide = false;
if (callBack != null)
panelInfo.callBack += callBack;
}
//已经加载结束
else
{
panelInfo.panel.ShowMe();
//直接执行回调,直接传递出去即可
callBack?.Invoke(panelInfo.panel);
}
return;
}
//不存在面板,先存入字典当中占位,之后如果又显示,我才能得到字典中的消息进行判断
panelDic.Add(panelName, new PanelInfo<T>(callBack));
ABResManager.Instance.LoadResAsync<GameObject>("ui", panelName, (res) =>
{
//取出消息
PanelInfo<T> panelInfo = panelDic[panelName] as PanelInfo<T>;
if (panelInfo.isHide)
{
panelDic.Remove(panelName);
return;
}
//层级的处理
Transform layerObj = GetLayerObj(layer);
//避免没有按照指定规则传递参数,避免为空
if (layerObj == null)
layerObj = middleLayer;
//将面板预设体创建到对应父对象下,并且保持原本的缩放大小
GameObject panelObj = GameObject.Instantiate(res, layerObj, false);
T panel = panelObj.GetComponent<T>(); //获取对应的UI控件返回出去
panel.ShowMe(); //显示面板时执行的默认方法
panelInfo.panel = panel; //将加载出来的panel记录到panelInfo内
panelInfo.callBack?.Invoke(panel); //传递到外部使用
panelInfo.callBack = null; //清空回调,避免内存泄露
}, isSync);
} -
获取相关
- 若加载中想要获取,应该等待加载结束后再处理获取逻辑
- 若加载结束想要获取,直接获取
- 若压根没有,不用处理
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
28
29
30
31
32
33// 用于存储面板消息和加载完成的回调函数
private class PanelInfo<T> : BasePanelInfo where T : BasePanel
{
public T panel;
public UnityAction<T> callBack;
public PanelInfo(UnityAction<T> callBack)
{
this.callBack += callBack;
}
}
public void GetPanel<T>(UnityAction<T> callBack) where T : BasePanel
{
string panelName = typeof(T).Name;
if (panelDic.ContainsKey(panelName))
{
//取出消息
PanelInfo<T> panelInfo = panelDic[panelName] as PanelInfo<T>;
if (panelInfo.panel == null)
{
//加载中,应该等待加载结束,再通过回调传递给外部去使用
panelInfo.callBack += callBack;
}
//加载结束,并且不处于将要隐藏的状态
else if (!panelInfo.isHide)
{
callBack?.Invoke(panelInfo.panel as T);
}
}
}
-
使用示例
在同一帧调用显示面板方法,面板只会加载和显示一次,但两个回调函数都会执行,并且不会报错
1 | void Start() |
输出:
在同一帧调用完显示方法后,再去隐藏面板,面板不会显示,并且回调也不执行
1 | void Start() |
在同一帧调用完显示方法后,再去隐藏面板,再去显示面板,面板依然会显示,前面的回调不执行,后面的回调会执行
在面板加载时调用获取面板,则获取面板传入的回调方法会在面板加载完后执行
1 | void Start() |
输出:
主要解决的问题
- 同一帧连续显示同一面板,避免重复进行异步加载后回调重复往字典中添加面板数据
- 同一帧 显示 ——> 隐藏 ——> 显示 同一面板问题,面板能够正常显示
- 获取面板时如果正在加载中,等加载结束后再获取处理逻辑
具体代码
1 | using System.Collections.Generic; |