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; | 
