找回密码
 立即注册
查看: 291|回复: 0

XLua中高效使用ProtoBuf

[复制链接]
发表于 2021-8-10 16:36 | 显示全部楼层 |阅读模式
游戏项目几乎必会用到ProtoBuf。然而如何正确使用ProtoBuf却还是有很多细节需要注意。
首先,我们希望Proto协议是经过编译的,而非直接使用明文。
其次,我们希望加载解析时间足够快。
最后,我们希望做到自动化。
那么,直接使用lua版的ProtoBuf不太可取。第一解析时间长,第二GC高。XLua的官方给了我们解决方案直接将protobuf编到xlua库中。这里就不做过多阐述,可以参考:
    接下来就是解决编译问题:
我们先从google官网下载protoc。然后写一段代码可搞定:
/*
* ==============================================================================
* Filename: ProtoConverter
* Created:  2021 / 6 / 17 11:56
* Author: HuaHua
* Purpose: proto 文件转换工具类
* ==============================================================================
**/
using UnityEngine;
using System.IO;
using UnityEditor;

public class ProtoConverter
{
    public static readonly string PROTO_PATH = Path.Combine(Application.dataPath, "Proto/").Replace('\\', '/');
    static readonly string DST_EXT = ".bytes";       //pb文件后缀名,bytes才会被unity当成AssetText处理
    static readonly string CMD = Path.Combine(EditorTools.AppPath, "dependence/proto/protoc");//protoc.exe的存放路径
    static readonly string ARGS = " -I=\"{0}\" --descriptor_set_out=\"{1}\" \"{2}\"";  //参数


    /// <summary>
    /// 编译proto文件
    /// </summary>
    /// <param name="srcAbsPath">文件绝对路径</param>
    /// <returns></returns>
    public static bool ConvertProto(string srcAbsPath)
    {
        if (!srcAbsPath.StartsWith(PROTO_PATH))
        {
            Debug.LogErrorFormat("proto file {0} not in folder: {1}", srcAbsPath, PROTO_PATH);
            return false;
        }

        string dstAbsPath = PROTO_PATH + Path.GetFileNameWithoutExtension(srcAbsPath) + DST_EXT;
        string argStr = string.Format(ARGS, PROTO_PATH, dstAbsPath, srcAbsPath);
        return EditorTools.ProcessCommand(CMD, argStr);
    }

    [MenuItem("Tools/ProtoBuf/ConvertProto")]
    public static void ConvertAll()
    {
        string[] files = Directory.GetFiles(PROTO_PATH, "*.txt", SearchOption.AllDirectories);
        int count = files.Length;
        for (int i = 0; i < count; i++)
        {
            ConvertProto(files);
            EditorUtility.DisplayProgressBar("编译Proto文件", string.Format(files + ":{0}/{1}", i, count), (float)i / count);
        }
        EditorUtility.ClearProgressBar();
        AssetDatabase.Refresh();
        Debug.Log("Convert All Protos, Done!");
    }
}
EditorTools工具类代码如下:
/*
* ==============================================================================
* Filename: EditorTools
* Created:  2021 / 6 / 17 11:56
* Author: HuaHua
* Purpose: 编辑器工具类
* ==============================================================================
**/
using UnityEngine;
using System.Text;

public class EditorTools
{
    public static string AppPath
    {
        get
        {
            if (string.IsNullOrEmpty(APP_PATH))
            {
                APP_PATH = Application.dataPath.Substring(0, Application.dataPath.Length - 6);
            }
            return APP_PATH;
        }
    }
    private static string APP_PATH;

    /// <summary>
    /// 调用命令行
    /// </summary>
    /// <param name="command">命令</param>
    /// <param name="argument">参数</param>
    /// <returns>有错误会返回false,否则true</returns>
    public static bool ProcessCommand(string command, string argument)
    {
        System.Diagnostics.ProcessStartInfo start = new System.Diagnostics.ProcessStartInfo(command)
        {
            Arguments = argument,
            CreateNoWindow = true,
            ErrorDialog = true,
            UseShellExecute = false
        };
        if (start.UseShellExecute)
        {
            start.RedirectStandardOutput = false;
            start.RedirectStandardError = false;
            start.RedirectStandardInput = false;
        }
        else
        {
            start.RedirectStandardOutput = true;
            start.RedirectStandardError = true;
            start.RedirectStandardInput = true;
            start.StandardOutputEncoding = Encoding.UTF8;
            start.StandardErrorEncoding = Encoding.UTF8;
        }

        System.Diagnostics.Process p = System.Diagnostics.Process.Start(start);

        bool bRet = true;
        if (!start.UseShellExecute)
        {
            string info = p.StandardOutput.ReadToEnd();
            if (!string.IsNullOrEmpty(info))
            {
                Debug.Log(info);
            }
            string err = p.StandardError.ReadToEnd();
            if (!string.IsNullOrEmpty(err))
            {
                bRet = false;
                Debug.LogError(err);
            }
        }

        p.WaitForExit();
        p.Close();

        return bRet;
    }
}
如果编译一切正常,那么恭喜你。
但如果项目之前使用的是lua版ProtoBuf,那么可能编译会遇到重复定义的报错。
is already defined in file不太希望去修改现有lua代码,也不希望去修改proto协议。那么我们只需要简单修改一下protoc的源码即可搞定。从google官网下载protobuf源码:
找到descriptor.cc,修改如下:
①、找到DescriptorBuilder::AddSymbol函数:
bool DescriptorBuilder::AddSymbol(const std::string& full_name,
                                  const void* parent, const std::string& name,
                                  const Message& proto, Symbol symbol) {
  // If the caller passed nullptr for the parent, the symbol is at file scope.
  // Use its file as the parent instead.
  if (parent == nullptr) parent = file_;

  if (full_name.find('\0') != std::string::npos) {
    AddError(full_name, proto, DescriptorPool::ErrorCollector::NAME,
             "\"" + full_name + "\" contains null character.");
    return false;
  }
  if (tables_->AddSymbol(full_name, symbol)) {
      //huahua
#if 0
    if (!file_tables_->AddAliasUnderParent(parent, name, symbol)) {
#else
    if (!file_tables_->AddAliasUnderParent(parent, full_name, symbol)) {
#endif
      // This is only possible if there was already an error adding something of
      // the same name.
      if (!had_errors_) {
        GOOGLE_LOG(DFATAL) << "\"" << full_name
                    << "\" not previously defined in "
                       "symbols_by_name_, but was defined in "
                       "symbols_by_parent_; this shouldn't be possible.";
      }
      return false;
    }

...②、找到DescriptorBuilder::BuildEnumValue函数:
void DescriptorBuilder::BuildEnumValue(const EnumValueDescriptorProto& proto,
                                       const EnumDescriptor* parent,
                                       EnumValueDescriptor* result) {
  result->name_ = tables_->AllocateString(proto.name());
  result->number_ = proto.number();
  result->type_ = parent;

  // Note:  full_name for enum values is a sibling to the parent's name, not a
  //   child of it.
  std::string* full_name = tables_->AllocateEmptyString();
  //huahua
#if 0
  size_t scope_len = parent->full_name_->size() - parent->name_->size();
  full_name->reserve(scope_len + result->name_->size());
  full_name->append(parent->full_name_->data(), scope_len);
#else
  full_name->reserve(parent->full_name_->size() + 1 + result->name_->size());
  full_name->append(*parent->full_name_);
  full_name->append(".");
#endif
  full_name->append(*result->name_);
  result->full_name_ = full_name;

...然后重新编译protoc,搞定。
2.我们希望只要修改了proto协议,就能做到自动编译。
/*
* ==============================================================================
* Filename: AssetMonitor
* Created:  2021 / 6 / 17 11:56
* Author: HuaHua
* Purpose: 资源监听类
* ==============================================================================
**/
using UnityEngine;
using UnityEditor;
using System.Reflection;
using System.IO;

public class AssetMonitor : AssetPostprocessor
{
    void HandleFileChange(string s)
    {
        string absPath = EditorTools.AppPath + s;

        bool isProcessing = false;
        try
        {
            if (absPath.StartsWith(ProtoConverter.PROTO_PATH))
            {
                isProcessing = true;
                ProtoConverter.ConvertProto(absPath);
                Debug.Log($"完成编译  {absPath}");
            }
        }
        catch (System.Exception e)
        {
            Debug.LogError($"{e.Message} {e.StackTrace}");
        }
        finally
        {
            if (isProcessing)
            {
                EditorUtility.ClearProgressBar();
            }
        }

    }

    void OnPreprocessAsset()
    {
        string ext = Path.GetExtension(assetPath);

        switch (ext)
        {
            case ".txt":
                {
                    HandleFileChange(assetPath);
                    break;
                }
        }
    }
}
一切准备就绪。
lua就只需要载入编译过后的proto文件。但惊奇的发现lua-protobuf只提供了lua版的加载接口pb_load。这意味着我们从c#读取proto文件还需要把文件内容传给lua,再由lua去调用c接口。我们都知道只要经过lua的字符串都会有一次拷贝(lua设计原则)。这无形太浪费。
于是我们动手修改lua-protobuf源码。
在pb.c中添加如下代码:
LUA_API int pb_loadBinary(lua_State *L, const char *s, int len)
{
        lpb_State *LS = default_lstate(L);
        pb_Slice slice;
        slice.p = s;
        slice.start = s;
        slice.end = s + len;

        int r = pb_load(&LS->local, &slice);
        if (r == PB_OK) global_state = &LS->local;
        lua_pushboolean(L, r == PB_OK);
        lua_pushinteger(L, pb_pos(slice) + 1);

        return 2;
}然后c#只需要引入:
    [DllImport(LUADLL, CallingConvention = CallingConvention.Cdecl)]
    public static extern void pb_loadBinary(IntPtr L, byte[] str, int size);
最后将读取的数据直接传入。
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-27 22:47 , Processed in 0.091341 second(s), 25 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表