C#字符串解析相关优化

前两天做了游戏(unity项目)中新手引导配置文件检查的功能,配置写在Lua脚本中,询问项目中的前辈后,调用了统一的接口加载游戏中所有Lua文件,获取到LuaTable,对其中按钮路径进行了检查,主要目的是防止prefab更新后未更新配置文件,这会导致新手引导中对应的按钮找不到。反复调试检查后,自信的提交了任务,主程来验收后表示错漏百出,代码格式没遵守规范,循环中重复加载资源,反复调用得到相同结果的函数……

代码风格每个人习惯不一样没有好坏之分就忽略他了,其他通用的记下来自省,也希望能对大家有帮助。

1.函数重复调用

先上优化前代码

    Hashtable ReadLuaTableAsHash()
    {
        Hashtable table = new Hashtable();
        // ...具体实现读取LuaTable功能
        // ...
        return table;
    }
    bool CheckSingle(string wnd, string btn)
    {
        bool result = true;
        // 具体实现检查路径功能
        return result;
    }
    void Main()
    {
        Hashtable userGuideConfigTable = ReadLuaTableAsHash();
        if(userGuideConfigTable!=null)
        {
            IEnumerator itr = userGuideConfigTable.Keys.GetEnumerator();
            while(itr.MoveNext())
            {
                // 得到临时的table,其中包含了单个配置信息
                Hashtable temp = userGuideConfigTable[itr.Current] as Hashtable;
                // 单个配置信息中 interWndName 记录prefab名字
                // interBtnPath 记录了prefab中 btn的路径
                if (!CheckSingle(temp["interWndName"].ToString(), temp["interBtnPath"].ToString()))
                {
                    Debug.Log(temp["interWndName"] + "中的节点" + temp["interBtnPath"] + "  请修改" + temp["interWndName"] + "  或者配置文件");
                }
            }
        }
    }

代码运行起来是完全正确的,程序老鸟和细心的朋友应该已经发现哪里不对了,“[]”只是重载了方法,不管数据结构如何优化都是需要耗费性能的,需要重复使用的数据,只获取一次到变量中,于是做了如下修改。

void Main()
    {
        Hashtable userGuideConfigTable = ReadLuaTableAsHash();
        if (userGuideConfigTable != null)
        {
            IEnumerator itr = userGuideConfigTable.Keys.GetEnumerator();
            while (itr.MoveNext())
            {
                // 得到临时的table,其中包含了单个配置信息
                Hashtable temp = userGuideConfigTable[itr.Current] as Hashtable;
                // 单个配置信息中 interWndName 记录prefab名字
                // interBtnPath 记录了prefab中 btn的路径
                object objInterWndName = temp["interWndName"];
                object objInterBtnPath = temp["objInterBtnPath"];
                if (objInterWndName == null || objInterBtnPath == null)
                {
                    continue;
                }
                string strInterWndName = objInterWndName.ToString();
                string strInterBtnPath = objInterBtnPath.ToString();
                if (!CheckSingle(strInterWndName, strInterBtnPath))
                {
                    Debug.Log(strInterWndName + "中的节点" + strInterBtnPath + "  请修改" + strInterWndName + "  或者配置文件");
                }
            }
        }
    }

其实这个问题很基础,偶尔会忘记,但却是代码中常用到的,用得多了对性能影响自然也就大了。

2.字符串解析优化

调用统一接口加载Lua文件的时间有些长,而且对于检查配置这一功能来说,这样的操作有些浪费,所以新的需求是用读文本的形式读取Lua文件。

小科普
所有文件本质上都是二进制文件,一堆0和1,文件本身并没有意义,关键看如何解读。

于是把Lua文件当做纯文本来解析,要解决的问题主要有两个:
1.去除备注以及文本中LuaTable以外的元素
2.将LuaTable解析出来

这其中肯定要识别关键字,例如

        public static Hashtable ReadLuaTable(string[] content)
        {
            Hashtable table = new Hashtable();
#if LOG_PARSE_TIME
        long currentTime = System.DateTime.Now.Ticks;
#endif
            string strFinal = "";
            bool isFunction = false;
            for (int i = 0; i < content.Length; i++)
            {
                string tempStr = content[i];
                #region 检查是否为注释行
                if (tempStr.Contains("--"))
                {
                    if (tempStr.IndexOf("--") - 1 > 0)
                    {
                        tempStr = tempStr.Substring(0, tempStr.IndexOf("--"));
                    }
                    else
                    {
                        tempStr = "";
                    }
                }
                if (tempStr.Replace(" ", "").Replace("\t", "") == "")
                {
                    tempStr = "";
                }
                #endregion
                #region 检查是否为函数,忽略他
                if (tempStr.Contains("function"))
                {
                    isFunction = true;
                    tempStr = "";
                }
                else if (tempStr.Contains("end"))
                {
                    if (tempStr.IndexOf("end") == 0)
                    {
                        isFunction = false;
                        tempStr = "";
                    }
                }
                if (isFunction)
                {
                    tempStr = "";
                }
                #endregion
                content[i] = tempStr;
            }
            strFinal = string.Join("", content);
            dicMatchLength = new Hashtable();
#if LOG_PARSE_TIME
        long elapsed = System.DateTime.Now.Ticks - currentTime;
        Debug.Log("预处理lua文件消耗时长 :  " + elapsed);
        currentTime = System.DateTime.Now.Ticks;
#endif
            table = ReadLuaTable(strFinal.Replace("\\\\", "\\"), 0) as Hashtable;
#if LOG_PARSE_TIME
        elapsed = System.DateTime.Now.Ticks - currentTime;
        Debug.Log("解析lua文件消耗时长 :  " + elapsed);
#endif

            return table;
        }

我一开始的做法显得比较笨拙,首先读取文件,采用了File.ReadAllLines()来读取文件,这里会把二进制文件逐行读成string,对每行string查找关键字,以检测是否为注释或函数,这些地方会影响到之后解析LuaTable。解析LuaTable需要逐字解析,所以最后我用string.Join()将数组重新变为单个string,不难察觉,生成这么多临时的string并不合适,这将会占用存储空间,且效率不高。

同时有几个小技巧,可以进一步优化代码。
1.string.IndexOf()与string.Contains()这个函数能够达到相同效果,当string.IndexOf(‘,’)得到的值小于0时则当前string不存在’,’。
2.content.Replace();content.Substring();等函数将会生成临时string以返回结果,尽量少用。
3.当有大量字符串操作时StringBuilder.Append();StringBuilder.Remove();要比直接用几个string相加,或者substring要快得多,StringBuilder已经事先申请好内存,所以在频繁操作的过程中省下了申请内存的时间。

嗯,这几点我当然本来也没注意,又是主程一秒点破,于是之后重构了代码。

        public static Hashtable ReadLuaTable2(System.IO.StreamReader file)
        {
            string totalFile = file.ReadToEnd();
            int nFileLength = totalFile.Length, nLineStartAt = 0, nLineEndAt = 0, nCount = 0;
            Hashtable table = new Hashtable();
            System.Text.StringBuilder strFinal = new System.Text.StringBuilder(1 << 19);
            bool isFunction = false;
#if LOG_PARSE_TIME
        long currentTime = System.DateTime.Now.Ticks;
#endif
            for (int i = 0; i < nFileLength; nLineStartAt = nLineEndAt + 1, i++)
            {
                //TODO: \r
                nLineEndAt = totalFile.IndexOf('\n', nLineStartAt);
                if (nLineEndAt < 0)
                    break;
                int nSubStringLength = nCount = nLineEndAt - nLineStartAt;
                #region 检查是否为注释行
                if (!isFunction)
                {
                    int indexOfComment = totalFile.IndexOf("--", nLineStartAt, nCount);
                    if (indexOfComment > 0)
                    {
                        nSubStringLength = indexOfComment - nLineStartAt;
                    }
                    else if (indexOfComment == 0)
                    {
                        continue;
                    }
                    bool bEmptyLine = true;
                    for (int j = nLineStartAt, jMax = nLineStartAt + nSubStringLength; j < jMax; j++)
                    {
                        char character = totalFile[j];
                        if (character != ' ' && character != '\t')
                        {
                            bEmptyLine = false;
                            break;
                        }
                    }
                    if (bEmptyLine)
                    {
                        continue;
                    }
                }
                #endregion
                #region 检查是否为函数,忽略他
                if (!isFunction)
                {
                    if (totalFile[nLineStartAt] == 'f' && totalFile[nLineStartAt + 1] == 'u' && totalFile[nLineStartAt + 2] == 'n' && totalFile[nLineStartAt + 3] == 'c' &&
                        totalFile[nLineStartAt + 4] == 't' && totalFile[nLineStartAt + 5] == 'i' && totalFile[nLineStartAt + 6] == 'o' && totalFile[nLineStartAt + 7] == 'n')
                    {
                        isFunction = true;
                        continue;
                    }
                }
                else
                {
                    if (totalFile[nLineStartAt] == 'e' && totalFile[nLineStartAt + 1] == 'n' && totalFile[nLineStartAt + 2] == 'd')
                    {
                        isFunction = false;
                        continue;
                    }
                }
                if (isFunction)
                {
                    continue;
                }
                #endregion
                strFinal.Append(totalFile, nLineStartAt, nSubStringLength);
            }
#if LOG_PARSE_TIME
        long elapsed = System.DateTime.Now.Ticks - currentTime;
        Debug.Log("预处理lua文件消耗时长 :  " + elapsed);
#endif
            string strFinalFinal = strFinal.ToString().Replace("\\\\", "\\");
            dicMatchLength = new Hashtable();
#if LOG_PARSE_TIME
        currentTime = System.DateTime.Now.Ticks;
#endif
            table = ReadLuaTable(strFinalFinal, 0) as Hashtable;
#if LOG_PARSE_TIME
        elapsed = System.DateTime.Now.Ticks - currentTime;
        Debug.Log("解析lua文件消耗时长 :  " + elapsed);
#endif
            return table;
        }

前后对比了时间,后者更稳定且一直略快于前者,第一次快20ms,多运行几次后,前者时间越来越接近后者,应该是unity内部对string的操作进行了什么神秘的优化,但总体来说,后者策略是要优于前者的,当然也视各位的具体情况而定。

3.资源重复加载

动态加载资源,无论是游戏本身还是类似我在写的检查工具都经常用到。
UnityEditor.AssetDatabase.LoadAssetAtPath<GameObject>(path);
这次我吸取了上次的教训,没有重复调用这个函数,而是赋给了局部变量,之后的检查过程中,调用局部变量无需重新加载。
这么做仍然有可以优化的地方,拿我做的工具举例,每当有prefab修改都会进行一次检查,每次检查都会需要加载许多资源,但其实,常常有一些资源并没有发生改变,这里可以做一个“池”,存放我们需要的资源,避免重复加载的现象。

    GameObject GetAssets(string path)
    {
        if(dicAssets.ContainsKey(path))
        {
            return dicAssets[path];
        }
        else
        {
            GameObject temp = UnityEditor.AssetDatabase.LoadAssetAtPath<GameObject>(path);
            dicAssets.Add(path, temp);
            return temp;
        }
    }

封装这么一个方法就可以在第一次检查之后省下加载资源的时间。

好的不喜欢说自己是萌新的萌新分享结束了,有错误的地方大家纠正,想要讨论问题也欢迎发邮件到lguanyuan@qq.com,之前放qq号上去发现这样qq会加了许多平时不联系的人,还是邮箱来得舒服,哈哈。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值