Pergunta

Estou usando a serialização binária (Bininaryformatter) como um mecanismo temporário para armazenar informações de estado em um arquivo para uma estrutura de objetos relativamente complexa (jogo); Os arquivos estão saindo Muito de Maior do que eu espero, e minha estrutura de dados inclui referências recursivas - por isso, estou me perguntando se o BinaryFormatter está realmente armazenando várias cópias dos mesmos objetos, ou se meu número básico de objetos e valores que eu deveria ter "aritmental é base, ou de onde mais o tamanho excessivo vem.

Pesquisando no Stack Overflow, consegui encontrar a especificação para o formato binário remoto binário da Microsoft:http://msdn.microsoft.com/en-us/library/cc236844(prot.10).aspx

O que não consigo encontrar é nenhum espectador existente que permita que você "espie" no conteúdo de um arquivo de saída Binaryformatter - obtenha contagens de objetos e bytes totais para diferentes tipos de objetos no arquivo, etc;

Eu sinto que esse deve ser o meu "Google -fu" falhando em mim (o pouco que tenho) - alguém pode ajudar? este devo já foram feitos antes, certo ??


ATUALIZAR: Não consegui encontrá -lo e não obtive respostas, então coloquei algo relativamente rápido (link para o projeto para download abaixo); Posso confirmar que o BinaryFormatter não armazena várias cópias do mesmo objeto, mas ele imprime muitos metadados no fluxo. Se você precisar de armazenamento eficiente, crie seus próprios métodos de serialização personalizados.

Foi útil?

Solução

Porque talvez seja de interesse para alguém que eu decidi fazer este post sobre Como é o formato binário de objetos .NET serializados e como podemos interpretá -lo corretamente?

Eu baseei toda a minha pesquisa sobre o .NET Remoting: estrutura de dados de formato binário especificação.



Classe de exemplo:

Para ter um exemplo de funcionamento, criei uma classe simples chamada A que contém 2 propriedades, uma corda e um valor inteiro, eles são chamados SomeString e SomeValue.

Classe A se parece com isso:

[Serializable()]
public class A
{
    public string SomeString
    {
        get;
        set;
    }

    public int SomeValue
    {
        get;
        set;
    }
}

Para a serialização, usei o BinaryFormatter é claro:

BinaryFormatter bf = new BinaryFormatter();
StreamWriter sw = new StreamWriter("test.txt");
bf.Serialize(sw.BaseStream, new A() { SomeString = "abc", SomeValue = 123 });
sw.Close();

Como pode ser visto, passei por uma nova instância de classe A contendo abc e 123 como valores.



Exemplo de dados do resultado:

Se olharmos para o resultado serializado em um editor hexadecimente, temos algo assim:

Example result data



Vamos interpretar os dados de resultado de exemplo:

De acordo com a especificação acima mencionada (aqui está o link direto para o PDF: Ms-nrbf] .pdf) Cada registro dentro do fluxo é identificado pelo RecordTypeEnumeration. Seção 2.1.2.1 RecordTypeNumeration estados:

Essa enumeração identifica o tipo de registro. Cada registro (exceto para membros do PrimitiveUntyped) começa com uma enumeração de tipo de registro. O tamanho da enumeração é um byte.



SerializationHeaderCord:

Então, se olharmos para os dados que obtemos, podemos começar a interpretar o primeiro byte:

SerializationHeaderRecord_RecordTypeEnumeration

Como afirmado em 2.1.2.1 RecordTypeEnumeration um valor de 0 identifica o SerializationHeaderRecord que é especificado em 2.6.1 SerializationHeaderRecord:

O registro da serializationHeaderCord deve ser o primeiro registro em uma serialização binária. Esse registro possui a versão principal e menor do formato e os IDs do objeto superior e dos cabeçalhos.

Isso consiste de:

  • RecordTypeenum (1 byte)
  • Rootid (4 bytes)
  • Headerid (4 bytes)
  • Majorversion (4 bytes)
  • Minorversion (4 bytes)



Com esse conhecimento, podemos interpretar o registro contendo 17 bytes:

SerializationHeaderRecord_Complete

00 representa o RecordTypeEnumeration qual é SerializationHeaderRecord no nosso caso.

01 00 00 00 representa o RootId

Se nem o registro binarymethodCall nem binarymethodreturn estiver presente no fluxo de serialização, o valor desse campo deverá conter o objeto de uma classe, matriz ou registro binaryObjectString contido no fluxo de serialização.

Então, no nosso caso, esse deve ser o ObjectId com o valor 1 (porque os dados são serializados usando pouco endiano), que esperamos ver novamente ;-)

FF FF FF FF representa o HeaderId

01 00 00 00 representa o MajorVersion

00 00 00 00 representa o MinorVersion



Binária BILIBRARY:

Conforme especificado, cada registro deve começar com o RecordTypeEnumeration. À medida que o último registro está concluído, devemos assumir que um novo começa.

Vamos interpretar o próximo byte:

BinaryLibraryRecord_RecordTypeEnumeration

Como podemos ver, em nosso exemplo o SerializationHeaderRecord é seguido pelo BinaryLibrary registro:

O registro binário associa um ID INT32 (conforme especificado na seção 2.2.22 [MS-DTYP]) ao nome da biblioteca. Isso permite que outros registros faça referência ao nome da biblioteca usando o ID. Essa abordagem reduz o tamanho do fio quando existem vários registros que fazem referência ao mesmo nome da biblioteca.

Isso consiste de:

  • RecordTypeenum (1 byte)
  • LibraryID (4 bytes)
  • Biblioteca nome (número variável de bytes (que é um LengthPrefixedString))



Como afirmado em 2.1.1.6 LengthPrefixedString...

O LengthPrefixedString representa um valor de string. A sequência é prefixada pelo comprimento da string codificada UTF-8 em bytes. O comprimento é codificado em um campo de comprimento variável com um mínimo de 1 byte e um máximo de 5 bytes. Para minimizar o tamanho do fio, o comprimento é codificado como um campo de comprimento variável.

Em nosso exemplo simples, o comprimento é sempre codificado usando 1 byte. Com esse conhecimento, podemos continuar a interpretação dos bytes no fluxo:

BinaryLibraryRecord_RecordTypeEnumeration_LibraryId

0C representa o RecordTypeEnumeration que identifica o BinaryLibrary registro.

02 00 00 00 representa o LibraryId qual é 2 no nosso caso.



Agora o LengthPrefixedString segue:

BinaryLibraryRecord_RecordTypeEnumeration_LibraryId_LibraryName

42 representa a informação de comprimento do LengthPrefixedString que contém o LibraryName.

No nosso caso, as informações de comprimento de 42 (decimal 66) nos diz que precisamos ler os próximos 66 bytes e interpretá -los como o LibraryName.

Como já foi dito, a corda é UTF-8 codificado, então o resultado dos bytes acima seria algo como: _WorkSpace_, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null



ClassWithMembersAndTypes:

Novamente, o registro está completo, então interpretamos o RecordTypeEnumeration do próximo:

ClassWithMembersAndTypesRecord_RecordTypeEnumeration

05 identifica a ClassWithMembersAndTypes registro. Seção 2.3.2.1 ClassWithMembersAndTypes estados:

O ClassWithMembersAndTypes Record é o mais detalhado dos registros de classe. Ele contém metadados sobre os membros, incluindo os nomes e tipos remotos dos membros. Ele também contém um ID da biblioteca que faz referência ao nome da biblioteca da classe.

Isso consiste de:

  • RecordTypeenum (1 byte)
  • Classinfo (número variável de bytes)
  • MEMBORTYPEINFO (Número variável de bytes)
  • LibraryID (4 bytes)



Classinfo:

Como afirmado em 2.3.1.1 ClassInfo O registro consiste em:

  • ObjectId (4 bytes)
  • Nome (número variável de bytes (que é novamente um LengthPrefixedString))
  • MemberCount (4 bytes)
  • Nomes de membros (que é uma sequência de LengthPrefixedStringonde o número de itens deve ser igual ao valor especificado no MemberCount campo.)



De volta aos dados brutos, passo a passo:

ClassWithMembersAndTypesRecord_RecordTypeEnumeration_ClassInfo_ObjectId

01 00 00 00 representa o ObjectId. Já vimos este, foi especificado como o RootId no SerializationHeaderRecord.

ClassWithMembersAndTypesRecord_RecordTypeEnumeration_ClassInfo_ObjectId_Name

0F 53 74 61 63 6B 4F 76 65 72 46 6C 6F 77 2E 41 representa o Name da classe que é representada usando um LengthPrefixedString. Como mencionado, em nosso exemplo, o comprimento da corda é definido com 1 byte, então o primeiro byte 0F Especifica que 15 bytes devem ser lidos e decodificados usando o UTF-8. O resultado se parece com o seguinte: StackOverFlow.A - Então, obviamente, eu usei StackOverFlow como nome do espaço para nome.

ClassWithMembersAndTypesRecord_RecordTypeEnumeration_ClassInfo_ObjectId_Name_MemberCount

02 00 00 00 representa o MemberCount, nos diz que 2 membros, ambos representados com LengthPrefixedString's seguirá.

Nome do primeiro membro:ClassWithMembersAndTypesRecord_MemberNameOne

1B 3C 53 6F 6D 65 53 74 72 69 6E 67 3E 6B 5F 5F 42 61 63 6B 69 6E 67 46 69 65 6C 64 representa o primeiro MemberName, 1B é novamente o comprimento da corda com 27 bytes de comprimento e resulta em algo assim: <SomeString>k__BackingField.

Nome do segundo membro:ClassWithMembersAndTypesRecord_MemberNameTwo

1A 3C 53 6F 6D 65 56 61 6C 75 65 3E 6B 5F 5F 42 61 63 6B 69 6E 67 46 69 65 6C 64 representa o segundo MemberName, 1A Especifica que a string tem 26 bytes de comprimento. Resulta em algo assim: <SomeValue>k__BackingField.



MembroTypeInfo:

Depois de ClassInfo a MemberTypeInfo segue.

Seção 2.3.1.2 - MemberTypeInfo Estados, que a estrutura contém:

  • BinaryTypeenums (variável em comprimento)

Uma sequência de valores de binarypeenumeration que representa os tipos de membros que estão sendo transferidos. A matriz deve:

  • Tenha o mesmo número de itens do campo Nomes de membros da estrutura Classinfo.

  • Seja ordenado de modo que a binarypeenumeration corresponda ao nome do membro no campo Nomes de membros da estrutura Classinfo.

  • AdicionalInfos (variável em comprimento), dependendo do BinaryTpeEnum Informações adicionais podem ou não estar presentes.

| BinaryTypeEnum | AdditionalInfos |
|----------------+--------------------------|
| Primitive | PrimitiveTypeEnumeration |
| String | None |

Então, levando isso em consideração, estamos quase lá ... esperamos 2 BinaryTypeEnumeration valores (porque tínhamos 2 membros no MemberNames).



Novamente, de volta aos dados brutos do completo MemberTypeInfo registro:

ClassWithMembersAndTypesRecord_MemberTypeInfo

01 representa o BinaryTypeEnumeration do primeiro membro, de acordo com 2.1.2.2 BinaryTypeEnumeration Podemos esperar um String e é representado usando um LengthPrefixedString.

00 representa o BinaryTypeEnumeration do segundo membro, e novamente, de acordo com a especificação, é um Primitive. Como afirmado acima, Primitivesão seguidos por informações adicionais, neste caso a PrimitiveTypeEnumeration. É por isso que precisamos ler o próximo byte, que é 08, combine com a tabela declarada em 2.1.2.3 PrimitiveTypeEnumeration e fique surpreso ao perceber que podemos esperar um Int32 que é representado por 4 bytes, conforme declarado em algum outro documento sobre tipos de dados básicos.



LibraryID:

Depois de MemerTypeInfo a LibraryId segue -se, é representado por 4 bytes:

ClassWithMembersAndTypesRecord_LibraryId

02 00 00 00 representa o LibraryId que é 2.



Os valores:

Conforme especificado em 2.3 Class Records:

Os valores dos membros da classe devem ser serializados como registros que seguem esse registro, conforme especificado na Seção 2.7. A ordem dos registros deve corresponder à ordem dos nomes de membros, conforme especificado na estrutura Classinfo (Seção 2.3.1.1).

É por isso que agora podemos esperar os valores dos membros.

Vejamos os últimos bytes:

BinaryObjectStringRecord_RecordTypeEnumeration

06 identifica um BinaryObjectString. Representa o valor do nosso SomeString propriedade (o <SomeString>k__BackingField para ser exato).

De acordo com 2.5.7 BinaryObjectString contém:

  • RecordTypeenum (1 byte)
  • ObjectId (4 bytes)
  • Valor (comprimento variável, representado como um LengthPrefixedString)



Então, sabendo disso, podemos identificar claramente que

BinaryObjectStringRecord_RecordTypeEnumeration_ObjectId_MemberOneValue

03 00 00 00 representa o ObjectId.

03 61 62 63 representa o Value Onde 03 é o comprimento da própria corda e 61 62 63 são os bytes de conteúdo que se traduzem em abc.

Espero que você possa lembrar que havia um segundo membro, um Int32. Sabendo disso Int32 é representado usando 4 bytes, podemos concluir que

BinaryObjectStringRecord_RecordTypeEnumeration_ObjectId_MemberOneValue_MemberTwoValue

deve ser o Value do nosso segundo membro. 7B Igualções hexadecimais 123 Decimal, que parece se encaixar no nosso código de exemplo.

Então aqui está o completo ClassWithMembersAndTypes registro:ClassWithMembersAndTypesRecord_Complete



MessageEnd:

MessageEnd_RecordTypeEnumeration

Finalmente o último byte 0B representa o MessageEnd registro.

Outras dicas

O Vasiliy está certo, pois, em última análise, precisarei implementar meu próprio processo de formatação/serialização para lidar melhor com a versão e para emitir um fluxo muito mais compacto (antes da compactação).

Eu queria entender o que estava acontecendo no fluxo, no entanto, então escrevi uma aula (relativamente) rápida que faz o que eu queria:

  • Aparece o seu riacho, construindo coleções de nomes de objetos, conta e tamanhos
  • Uma vez feito, produz um resumo rápido do que encontrou - classes, contagens e tamanhos totais no fluxo

Não é útil o suficiente para eu colocá -lo em algum lugar visível como o CodeProject, então eu apenas larguei o projeto em um arquivo zip no meu site: http://www.architectshack.com/binaryserializationAnálise.ashx

No meu caso específico, acontece que o problema era duplo:

  • O Binário Formatter é muito detalhado (isso é conhecido, eu simplesmente não percebi a extensão)
  • Eu tive problemas na minha aula, aconteceu que eu estava armazenando objetos que eu não queria

Espero que isso ajude alguém em algum momento!


Atualização: Ian Wright entrou em contato comigo com um problema com o código original, onde travou quando o (s) objeto (s) de origem (s) continha valores "decimais". Agora isso está corrigido, e eu usei a ocasião para mover o código para o GitHub e dar uma licença (permissiva, BSD).

Nosso aplicativo opera dados maciços. Pode levar até 1-2 GB de RAM, como o seu jogo. Encontramos o mesmo problema "armazenando várias cópias dos mesmos objetos". Também a serialização binária armazena muitos dados. Quando foi implementado pela primeira vez, o arquivo serializado levou cerca de 1-2 GB. Atualmente, consegui diminuir o valor - 50-100 MB. O que nós fizemos.

A resposta curta - não use a serialização binária .NET, crie seu próprio mecanismo de serialização binária. Temos uma classe BinaryFormatter e interface iserializável (com dois métodos serializam, desesteram).

O mesmo objeto não deve ser serializado mais de uma vez. Salvamos seu ID exclusivo e restauramos o objeto do cache.

Posso compartilhar algum código se você perguntar.

EDITAR: Parece que você está correto. Veja o código a seguir - prova que eu estava errado.

[Serializable]
public class Item
{
    public string Data { get; set; }
}

[Serializable]
public class ItemHolder
{
    public Item Item1 { get; set; }

    public Item Item2 { get; set; }
}

public class Program
{
    public static void Main(params string[] args)
    {
        {
            Item item0 = new Item() { Data = "0000000000" };
            ItemHolder holderOneInstance = new ItemHolder() { Item1 = item0, Item2 = item0 };

            var fs0 = File.Create("temp-file0.txt");
            var formatter0 = new BinaryFormatter();
            formatter0.Serialize(fs0, holderOneInstance);
            fs0.Close();
            Console.WriteLine("One instance: " + new FileInfo(fs0.Name).Length); // 335
            //File.Delete(fs0.Name);
        }

        {
            Item item1 = new Item() { Data = "1111111111" };
            Item item2 = new Item() { Data = "2222222222" };
            ItemHolder holderTwoInstances = new ItemHolder() { Item1 = item1, Item2 = item2 };

            var fs1 = File.Create("temp-file1.txt");
            var formatter1 = new BinaryFormatter();
            formatter1.Serialize(fs1, holderTwoInstances);
            fs1.Close();
            Console.WriteLine("Two instances: " + new FileInfo(fs1.Name).Length); // 360
            //File.Delete(fs1.Name);
        }
    }
}

Parece BinaryFormatter usa object.equals para encontrar os mesmos objetos.

Você já olhou para dentro dos arquivos gerados? Se você abrir "Temp-File0.txt" e "Temp-File1.txt" do exemplo do código, você verá que possui muitos meta-dados. É por isso que eu recomendei que você criasse seu próprio mecanismo de serialização.

Desculpe por estar cofusando.

Talvez você possa executar seu programa no modo de depuração e tentar adicionar um ponto de controle.

Se isso for impossível devido ao tamanho do jogo ou a outras dependências, você sempre pode coar um aplicativo simples/pequeno que inclui o código de deseralização e espiar do modo de depuração lá.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top