UE4对象系统_序列化和uasset文件格式

虚幻的序列化这块是个人比较喜欢的技术点,个人在工作中也山寨了个简化版,UE4延续了UE3的序列化方案。它使用了访问者模式(Vistor Pattern),将序列化的存档接口抽象化,其中FArchive为访问者, 其它实现了void Serialize( FArchive& Ar )接口的类为被访问者。FArchive可以是磁盘文件访问, 内存统计,对象统计等功能。

FArchive

FArchive的类继承体系如下:

Paste_Image.png

定义接口如下:

/**
 * Base class for archives that can be used for loading, saving, and garbage
 * collecting in a byte order neutral way.
 */
class CORE_API FArchive
{
public:

    /** Default constructor. */
    FArchive();

    /** Copy constructor. */
    FArchive(const FArchive&);

    /**
     * Copy assignment operator.
     *
     * @param ArchiveToCopy The archive to copy from.
     */
    FArchive& operator=(const FArchive& ArchiveToCopy);

    /** Destructor. */
    virtual ~FArchive();

public:

    virtual FArchive& operator<<(class FName& Value);
    virtual FArchive& operator<<(class FText& Value);
    virtual FArchive& operator<<(class UObject*& Value);
    virtual FArchive& operator<<(class FLazyObjectPtr& Value);
    virtual FArchive& operator<<(class FAssetPtr& Value);
    virtual FArchive& operator<<(struct FStringAssetReference& Value);
    virtual FArchive& operator<<(struct FWeakObjectPtr& Value);
    virtual void ForceBlueprintFinalization() {}
public:
    FORCEINLINE friend FArchive& operator<<(FArchive& Ar, ANSICHAR& Value);
    FORCEINLINE friend FArchive& operator<<(FArchive& Ar, WIDECHAR& Value);

    FORCEINLINE friend FArchive& operator<<(FArchive& Ar, uint8& Value);
    template<class TEnum>
    FORCEINLINE friend FArchive& operator<<(FArchive& Ar, TEnumAsByte<TEnum>& Value);
    FORCEINLINE friend FArchive& operator<<(FArchive& Ar, int8& Value);
    FORCEINLINE friend FArchive& operator<<(FArchive& Ar, uint16& Value);
    FORCEINLINE friend FArchive& operator<<(FArchive& Ar, int16& Value);
    FORCEINLINE friend FArchive& operator<<(FArchive& Ar, uint32& Value);
    FORCEINLINE friend FArchive& operator<<(FArchive& Ar, bool& D);

    FORCEINLINE friend FArchive& operator<<(FArchive& Ar, int32& Value);
    FORCEINLINE friend FArchive& operator<<(FArchive& Ar, long& Value);
    FORCEINLINE friend FArchive& operator<<( FArchive& Ar, float& Value);
    FORCEINLINE friend FArchive& operator<<(FArchive& Ar, double& Value);
    FORCEINLINE friend FArchive& operator<<(FArchive &Ar, uint64& Value);
    /*FORCEINLINE*/friend FArchive& operator<<(FArchive& Ar, int64& Value);
    template <
        typename EnumType,
        typename = typename TEnableIf<TIsEnumClass<EnumType>::Value>::Type
    >
    FORCEINLINE friend FArchive& operator<<(FArchive& Ar, EnumType& Value)
    {
        return Ar << (__underlying_type(EnumType)&)Value;
    }

    friend FArchive& operator<<(FArchive& Ar, struct FIntRect& Value);
    friend CORE_API FArchive& operator<<(FArchive& Ar, FString& Value);

public:
    virtual void Serialize(void* V, int64 Length) ;
    virtual void SerializeBits(void* V, int64 LengthBits);
    virtual void SerializeInt(uint32& Value, uint32 Max)

    /** Packs int value into bytes of 7 bits with 8th bit for 'more' */
    virtual void SerializeIntPacked(uint32& Value);
    virtual void Preload(UObject* Object) { }

    virtual void CountBytes(SIZE_T InNum, SIZE_T InMax) { }
    virtual FString GetArchiveName() const;
    virtual class FLinker* GetLinker()
    {
        return nullptr;
    }

    virtual int64 Tell();
    virtual int64 TotalSize();
    virtual bool AtEnd();
    virtual void Seek(int64 InPos) { }
    virtual void AttachBulkData(UObject* Owner, FUntypedBulkData* BulkData) { }
    virtual void DetachBulkData(FUntypedBulkData* BulkData, bool bEnsureBulkDataIsLoaded) { }

    /**
     * Sets mapping from offsets/ sizes that are going to be used for seeking and serialization to what
     * is actually stored on disk. If the archive supports dealing with compression in this way it is 
     * going to return true.
     *
     * @param   CompressedChunks    Pointer to array containing information about [un]compressed chunks
     * @param   CompressionFlags    Flags determining compression format associated with mapping
     *
     * @return true if archive supports translating offsets & uncompressing on read, false otherwise
     */
    virtual bool SetCompressionMap(TArray<struct FCompressedChunk>* CompressedChunks, ECompressionFlags CompressionFlags);
    virtual void Flush() { }

    virtual bool Close();
    virtual bool GetError();
    void SetError() ;

    /**
     * Serializes and compresses/ uncompresses data. This is a shared helper function for compression
     * support. The data is saved in a way compatible with FIOSystem::LoadCompressedData.
     *
     * @param   V       Data pointer to serialize data from/ to
     * @param   Length  Length of source data if we're saving, unused otherwise
     * @param   Flags   Flags to control what method to use for [de]compression and optionally control memory vs speed when compressing
     * @param   bTreatBufferAsFileReader true if V is actually an FArchive, which is used when saving to read data - helps to avoid single huge allocations of source data
     * @param   bUsePlatformBitWindow use a platform specific bitwindow setting
     */
    void SerializeCompressed(void* V, int64 Length, ECompressionFlags Flags, bool bTreatBufferAsFileReader = false, bool bUsePlatformBitWindow = false);


    FORCEINLINE bool IsByteSwapping(); // 平台的大端、小端

    // Used to do byte swapping on small items. This does not happen usually, so we don't want it inline
    void ByteSwap(void* V, int32 Length);

    FORCEINLINE FArchive& ByteOrderSerialize(void* V, int32 Length);

    /** Sets a flag indicating that this archive contains code. */
    void ThisContainsCode();

    /** Sets a flag indicating that this archive contains a ULevel or UWorld object. */
    void ThisContainsMap() ;

    /** Sets a flag indicating that this archive contains data required to be gathered for localization. */
    void ThisRequiresLocalizationGather();

    /** Sets a flag indicating that this archive is currently serializing class/struct defaults. */
    void StartSerializingDefaults() ;
    /** Indicate that this archive is no longer serializing class/struct defaults. */
    void StopSerializingDefaults() ;
    /**
     * Called when an object begins serializing property data using script serialization.
     */
    virtual void MarkScriptSerializationStart(const UObject* Obj) { }

    /**
     * Called when an object stops serializing property data using script serialization.
     */
    virtual void MarkScriptSerializationEnd(const UObject* Obj) { }

    FORCEINLINE bool IsLoading() const
    {
        return ArIsLoading;
    }

    FORCEINLINE bool IsSaving() const
    {
        return ArIsSaving;
    }

    FORCEINLINE bool IsTransacting() const
    {
        if (FPlatformProperties::HasEditorOnlyData())
        {
            return ArIsTransacting;
        }
        else
        {
            return false;
        }
    }

    FORCEINLINE bool WantBinaryPropertySerialization() const
    {
        return ArWantBinaryPropertySerialization;
    }

    FORCEINLINE bool IsForcingUnicode() const
    {
        return ArForceUnicode;
    }

    FORCEINLINE bool IsPersistent() const
    {
        return ArIsPersistent;
    }

    FORCEINLINE bool IsError() const
    {
        return ArIsError;
    }

    FORCEINLINE bool IsCriticalError() const
    {
        return ArIsCriticalError;
    }

    FORCEINLINE bool ContainsCode() const
    {
        return ArContainsCode;
    }

    FORCEINLINE bool ContainsMap() const
    {
        return ArContainsMap;
    }

    FORCEINLINE bool RequiresLocalizationGather() const
    {
        return ArRequiresLocalizationGather;
    }

    FORCEINLINE bool ForceByteSwapping() const
    {
        return ArForceByteSwapping;
    }

    FORCEINLINE bool IsSerializingDefaults() const
    {
        return (ArSerializingDefaults > 0) ? true : false;
    }

    FORCEINLINE bool IsIgnoringArchetypeRef() const
    {
        return ArIgnoreArchetypeRef;
    }

    FORCEINLINE bool DoDelta() const
    {
        return !ArNoDelta;
    }

    FORCEINLINE bool IsIgnoringOuterRef() const
    {
        return ArIgnoreOuterRef;
    }

    FORCEINLINE bool IsIgnoringClassGeneratedByRef() const
    {
        return ArIgnoreClassGeneratedByRef;
    }

    FORCEINLINE bool IsIgnoringClassRef() const
    {
        return ArIgnoreClassRef;
    }

    FORCEINLINE bool IsAllowingLazyLoading() const
    {
        return ArAllowLazyLoading;
    }

    FORCEINLINE bool IsObjectReferenceCollector() const
    {
        return ArIsObjectReferenceCollector;
    }

    FORCEINLINE bool IsModifyingWeakAndStrongReferences() const
    {
        return ArIsModifyingWeakAndStrongReferences;
    }

    FORCEINLINE bool IsCountingMemory() const
    {
        return ArIsCountingMemory;
    }

    FORCEINLINE uint32 GetPortFlags() const
    {
        return ArPortFlags;
    }
private:

    /** Copies all of the members except CustomVersionContainer */
    void CopyTrivialFArchiveStatusMembers(const FArchive& ArchiveStatusToCopy);

public:

    /** Whether this archive is for loading data. */
    uint8 ArIsLoading : 1;

    /** Whether this archive is for saving data. */
    uint8 ArIsSaving : 1;

    /** Whether archive is transacting. */
    uint8 ArIsTransacting : 1;

    /** Whether this archive wants properties to be serialized in binary form instead of tagged. */
    uint8 ArWantBinaryPropertySerialization : 1;

    /** Whether this archive wants to always save strings in unicode format */
    uint8 ArForceUnicode : 1;

    /** Whether this archive saves to persistent storage. */
    uint8 ArIsPersistent : 1;

    /** Whether this archive contains errors. */
    uint8 ArIsError : 1;

    /** Whether this archive contains critical errors. */
    uint8 ArIsCriticalError : 1;

    /** Quickly tell if an archive contains script code. */
    uint8 ArContainsCode : 1;

    /** Used to determine whether FArchive contains a level or world. */
    uint8 ArContainsMap : 1;

    /** Used to determine whether FArchive contains data required to be gathered for localization. */
    uint8 ArRequiresLocalizationGather : 1;

    /** Whether we should forcefully swap bytes. */
    uint8 ArForceByteSwapping : 1;

    /** If true, we will not serialize the ObjectArchetype reference in UObject. */
    uint8 ArIgnoreArchetypeRef : 1;

    /** If true, we will not serialize the ObjectArchetype reference in UObject. */
    uint8 ArNoDelta : 1;

    /** If true, we will not serialize the Outer reference in UObject. */
    uint8 ArIgnoreOuterRef : 1;

    /** If true, we will not serialize ClassGeneratedBy reference in UClass. */
    uint8 ArIgnoreClassGeneratedByRef : 1;

    /** If true, UObject::Serialize will skip serialization of the Class property. */
    uint8 ArIgnoreClassRef : 1;

    /** Whether to allow lazy loading. */
    uint8 ArAllowLazyLoading : 1;

    /** Whether this archive only cares about serializing object references. */
    uint8 ArIsObjectReferenceCollector : 1;

    /** Whether a reference collector is modifying the references and wants both weak and strong ones */
    uint8 ArIsModifyingWeakAndStrongReferences : 1;

    /** Whether this archive is counting memory and therefore wants e.g. TMaps to be serialized. */
    uint8 ArIsCountingMemory : 1;

    /** Whether bulk data serialization should be skipped or not. */
    uint8 ArShouldSkipBulkData : 1;

    /** Whether editor only properties are being filtered from the archive (or has been filtered). */
    uint8 ArIsFilterEditorOnly : 1;

    /** Whether this archive is saving/loading game state */
    uint8 ArIsSaveGame : 1;

    /** Set TRUE to use the custom property list attribute for serialization. */
    uint8 ArUseCustomPropertyList : 1;

    /** Whether we are currently serializing defaults. > 0 means yes, <= 0 means no. */
    int32 ArSerializingDefaults;

    /** Modifier flags that be used when serializing UProperties */
    uint32 ArPortFlags;
            
    /** Max size of data that this archive is allowed to serialize. */
    int64 ArMaxSerializeSize;

protected:

    /** Holds the archive version. */
    int32 ArUE4Ver;

    /** Holds the archive version for licensees. */
    int32 ArLicenseeUE4Ver;

    /** Holds the engine version. */
    FEngineVersionBase ArEngineVer;

    /** Holds the engine network protocol version. */
    uint32 ArEngineNetVer;

    /** Holds the game network protocol version. */
    uint32 ArGameNetVer;
};

通过重载operater <<来实现对数据的访问。

UObject的序列化接口

  • void UObject::Serialize( FArchive& Ar )
    UObject通过实现Serialize接口来序列化对象数据。
void UObject::Serialize( FArchive& Ar )
{
    // These three items are very special items from a serialization standpoint. They aren't actually serialized.
    UClass *ObjClass = GetClass();
    UObject* LoadOuter = GetOuter();
    FName LoadName = GetFName();

    // Make sure this object's class's data is loaded.
    if(ObjClass->HasAnyFlags(RF_NeedLoad) )
    {
        Ar.Preload(ObjClass);

        // make sure this object's template data is loaded - the only objects
        // this should actually affect are those that don't have any defaults
        // to serialize.  for objects with defaults that actually require loading
        // the class default object should be serialized in FLinkerLoad::Preload, before
        // we've hit this code.
        if ( !HasAnyFlags(RF_ClassDefaultObject) && ObjClass->GetDefaultsCount() > 0 )
        {
            Ar.Preload(ObjClass->GetDefaultObject());
        }
    }

    // Special info.
    if( (!Ar.IsLoading() && !Ar.IsSaving() && !Ar.IsObjectReferenceCollector()) )
    {
        Ar << LoadName;   // 对象名
        if(!Ar.IsIgnoringOuterRef())
        {
            Ar << LoadOuter;        // Outer对象
        }
        if ( !Ar.IsIgnoringClassRef() )
        {
            Ar << ObjClass;  // UClass对象
        }
    }
    // Special support for supporting undo/redo of renaming and changing Archetype. 编辑器中使用
    else if( Ar.IsTransacting() )
    {
        if(!Ar.IsIgnoringOuterRef())
        {
            if(Ar.IsLoading())
            {
                Ar << LoadName << LoadOuter;

                // If the name we loaded is different from the current one,
                // unhash the object, change the name and hash it again.
                bool bDifferentName = GetFName() != NAME_None && LoadName != GetFName();
                bool bDifferentOuter = LoadOuter != GetOuter();
                if ( bDifferentName == true || bDifferentOuter == true )
                {
                    LowLevelRename(LoadName,LoadOuter);
                }
            }
            else
            {
                Ar << LoadName << LoadOuter;
            }
        }
    }

    // Serialize object properties which are defined in the class.
    // Handle derived UClass objects (exact UClass objects are native only and shouldn't be touched)
        // 判断当前对象是否为UClass的实例, 如果是普通的Object的化就序列化它的所有带UPROPERTY()的成员变量
    if (ObjClass != UClass::StaticClass())
    {
        SerializeScriptProperties(Ar);
    }

    // Keep track of pending kill
    if( Ar.IsTransacting() )  // 编辑中undo/redo时序列化
    {
        bool WasKill = IsPendingKill();
        if( Ar.IsLoading() )
        {
            Ar << WasKill;
            if (WasKill)
            {
                MarkPendingKill();
            }
            else
            {
                ClearPendingKill();
            }
        }
        else if( Ar.IsSaving() )
        {
            Ar << WasKill;
        }
    }

    // Serialize a GUID if this object has one mapped to it
    FLazyObjectPtr::PossiblySerializeObjectGuid(this, Ar);

    // Invalidate asset pointer caches when loading a new object
    if (Ar.IsLoading() )
    {
        FStringAssetReference::InvalidateTag();
    }

    // Memory counting (with proper alignment to match C++)
    SIZE_T Size = GetClass()->GetStructureSize();
    Ar.CountBytes( Size, Size );
}
  • void UObject::SerializeScriptProperties( FArchive& Ar ) const
    该函数用来序列化Object体系类中声明为UPROPERTY()的成员变量。
Paste_Image.png
  • UStruct::SerializeTaggedProperties
    序列化对象属性,并且加入tag,这个是为了处理对象类发生变化导致属性匹配不上(版本升级和容错)。
void UStruct::SerializeTaggedProperties(FArchive& Ar, uint8* Data, UStruct* DefaultsStruct, uint8* Defaults, const UObject* BreakRecursionIfFullyLoad) const
{
    //SCOPED_LOADTIMER(SerializeTaggedPropertiesTime);

    // Determine if this struct supports optional property guid's (UBlueprintGeneratedClasses Only)
    const bool bArePropertyGuidsAvailable = (Ar.UE4Ver() >= VER_UE4_PROPERTY_GUID_IN_PROPERTY_TAG) && !FPlatformProperties::RequiresCookedData() && ArePropertyGuidsAvailable();

    if( Ar.IsLoading() )
    {
        // Load tagged properties.

        // This code assumes that properties are loaded in the same order they are saved in. This removes a n^2 search 
        // and makes it an O(n) when properties are saved in the same order as they are loaded (default case). In the 
        // case that a property was reordered the code falls back to a slower search.
        UProperty*  Property            = PropertyLink;
        bool        bAdvanceProperty    = false;
        int32       RemainingArrayDim   = Property ? Property->ArrayDim : 0;

        // Load all stored properties, potentially skipping unknown ones.
        while (1)
        {
            FPropertyTag Tag;
            Ar << Tag;  // 读取标记

            if( Tag.Name == NAME_None )
            {
                break;
            }
            if (!Tag.Name.IsValid())
            {
                UE_LOG(LogClass, Warning, TEXT("Invalid tag name: struct '%s', archive '%s'"), *GetName(), *Ar.GetArchiveName());
                break;
            }

            // Move to the next property to be serialized
            if( bAdvanceProperty && --RemainingArrayDim <= 0 )
            {
                Property = Property->PropertyLinkNext;
                // Skip over properties that don't need to be serialized.
                while( Property && !Property->ShouldSerializeValue( Ar ) )
                {
                    Property = Property->PropertyLinkNext;
                }
                bAdvanceProperty        = 0;
                RemainingArrayDim   = Property ? Property->ArrayDim : 0;
            }
            
            // Optionally resolve properties using Guid Property tags in non cooked builds that support it.
            if (bArePropertyGuidsAvailable && Tag.HasPropertyGuid)
            {
                // Use property guids from blueprint generated classes to redirect serialised data.
                FName Result = FindPropertyNameFromGuid(Tag.PropertyGuid);
                if (Result != NAME_None && Tag.Name != Result)
                {
                    Tag.Name = Result;
                }
            }
            // If this property is not the one we expect (e.g. skipped as it matches the default value), do the brute force search.
            if( Property == nullptr || Property->GetFName() != Tag.Name )
            {
                // No need to check redirects on platforms where everything is cooked. Always check for save games
                if ((!FPlatformProperties::RequiresCookedData() || Ar.IsSaveGame()) && !Ar.HasAnyPortFlags(PPF_DuplicateForPIE|PPF_Duplicate))
                {
                    FName EachName = GetFName();
                    FName PackageName = GetOutermost()->GetFName();
                    // Search the current class first, then work up the class hierarchy to see if theres a match for our fixup.
                    UStruct* Owner = GetOwnerStruct();
                    if( Owner )
                    {
                        UStruct* CheckStruct = Owner;
                        while(CheckStruct)
                        {
                            FName NewTagName = UProperty::FindRedirectedPropertyName(CheckStruct, Tag.Name);

                            if (NewTagName != NAME_None)
                            {
                                Tag.Name = NewTagName;
                                break;
                            }

                            CheckStruct = CheckStruct->GetSuperStruct();
                        }
                    }
                }

                UProperty* CurrentProperty = Property;
                // Search forward...
                for ( ; Property; Property=Property->PropertyLinkNext ) // 查找到对应的Property
                {
                    if( Property->GetFName() == Tag.Name )
                    {
                        break;
                    }
                }
                // ... and then search from the beginning till we reach the current property if it's not found. 从头再找,这块利用了大部分情况下UProperty是顺序序列化的,提高查找效率。
                if( Property == nullptr )
                {
                    for( Property = PropertyLink; Property && Property != CurrentProperty; Property = Property->PropertyLinkNext )
                    {
                        if( Property->GetFName() == Tag.Name )
                        {
                            break;
                        }
                    }

                    if( Property == CurrentProperty )
                    {
                        // Property wasn't found.
                        Property = nullptr;
                    }
                }

                RemainingArrayDim = Property ? Property->ArrayDim : 0;
            }
#if WITH_EDITOR
            if (!Property)
            {
                Property = CustomFindProperty(Tag.Name);
            }
#endif // WITH_EDITOR

            FName PropID = Property ? Property->GetID() : NAME_None;
            FName ArrayInnerID = NAME_None;

            // Check if this is a struct property and we have a redirector
            // No need to check redirects on platforms where everything is cooked. Always check for save games
            if (!FPlatformProperties::RequiresCookedData() || Ar.IsSaveGame())
            {
                if (Tag.Type == NAME_StructProperty && PropID == NAME_StructProperty)
                {
                    const FName NewName = FLinkerLoad::FindNewNameForStruct(Tag.StructName);
                    const FName StructName = CastChecked<UStructProperty>(Property)->Struct->GetFName();
                    if (NewName == StructName)
                    {
                        Tag.StructName = NewName;
                    }
                }
                else if ((PropID == NAME_EnumProperty) && ((Tag.Type == NAME_EnumProperty) || (Tag.Type == NAME_ByteProperty)))
                {
                    const FName NewName = FLinkerLoad::FindNewNameForEnum(Tag.EnumName);
                    if (!NewName.IsNone())
                    {
                        Tag.EnumName = NewName;
                    }
                }
            }

            const int64 StartOfProperty = Ar.Tell();
            if( !Property )
            {
                //UE_LOG(LogClass, Warning, TEXT("Property %s of %s not found for package:  %s"), *Tag.Name.ToString(), *GetFullName(), *Ar.GetArchiveName() );
            }
#if WITH_EDITOR
            else if (BreakRecursionIfFullyLoad && BreakRecursionIfFullyLoad->HasAllFlags(RF_LoadCompleted))
            {
            }
#endif // WITH_EDITOR
            // editoronly properties should be skipped if we are NOT the editor, or we are 
            // the editor but are cooking for console (editoronly implies notforconsole)
            else if ((Property->PropertyFlags & CPF_EditorOnly) && !FPlatformProperties::HasEditorOnlyData() && !GForceLoadEditorOnly)
            {
            }
            // check for valid array index
            else if( Tag.ArrayIndex >= Property->ArrayDim || Tag.ArrayIndex < 0 )
            {
                UE_LOG(LogClass, Warning, TEXT("Array bound exceeded (var %s=%d, exceeds %s [0-%d] in package:  %s"), 
                    *Tag.Name.ToString(), Tag.ArrayIndex, *GetName(), Property->ArrayDim-1, *Ar.GetArchiveName());
            }

            else if( !Property->ShouldSerializeValue(Ar) )
            {
                UE_CLOG((Ar.IsPersistent() && FPlatformProperties::RequiresCookedData()), LogClass, Warning, TEXT("Skipping saved property %s of %s since it is no longer serializable for asset:  %s. (Maybe resave asset?)"), *Tag.Name.ToString(), *GetName(), *Ar.GetArchiveName() );
            }

            else if (Property->ConvertFromType(Tag, Ar, Data, DefaultsStruct, bAdvanceProperty))
            {
                if (bAdvanceProperty)
                {
                    continue;
                }
            }

            else if (Tag.Type != PropID)
            {
                UE_LOG(LogClass, Warning, TEXT("Type mismatch in %s of %s - Previous (%s) Current(%s) for package:  %s"), *Tag.Name.ToString(), *GetName(), *Tag.Type.ToString(), *PropID.ToString(), *Ar.GetArchiveName() );
            }
            else
            {
                uint8* DestAddress = Property->ContainerPtrToValuePtr<uint8>(Data, Tag.ArrayIndex);  
                uint8* DefaultsFromParent = Property->ContainerPtrToValuePtrForDefaults<uint8>(DefaultsStruct, Defaults, Tag.ArrayIndex);

                // This property is ok. 读取数据内存          
                Tag.SerializeTaggedProperty(Ar, Property, DestAddress, DefaultsFromParent);

                bAdvanceProperty = true;
                if (!Ar.IsCriticalError())
                {
                    continue;
                }
            }

            bAdvanceProperty = false;

            // Skip unknown or bad property.
            const int64 RemainingSize = Tag.Size - (Ar.Tell() - StartOfProperty);
            uint8 B;
            for( int64 i=0; i<RemainingSize; i++ )
            {
                Ar << B;
            }
        }
    }
    else
    {
        check(Ar.IsSaving() || Ar.IsCountingMemory());

        UScriptStruct* DefaultsScriptStruct = dynamic_cast<UScriptStruct*>(DefaultsStruct);

        /** If true, it means that we want to serialize all properties of this struct if any properties differ from defaults */
        bool bUseAtomicSerialization = false;
        if (DefaultsScriptStruct)
        {
            bUseAtomicSerialization = DefaultsScriptStruct->ShouldSerializeAtomically(Ar);
        }

        // Save tagged properties.

        // Iterate over properties in the order they were linked and serialize them.
        const FCustomPropertyListNode* CustomPropertyNode = Ar.ArUseCustomPropertyList ? Ar.ArCustomPropertyList : nullptr;
        for (UProperty* Property = Ar.ArUseCustomPropertyList ? (CustomPropertyNode ? CustomPropertyNode->Property : nullptr) : PropertyLink;
            Property;
            Property = Ar.ArUseCustomPropertyList ? FCustomPropertyListNode::GetNextPropertyAndAdvance(CustomPropertyNode) : Property->PropertyLinkNext)
        {
            if( Property->ShouldSerializeValue(Ar) )// 判断是否要序列化该属性
            {
                const int32 LoopMin = CustomPropertyNode ? CustomPropertyNode->ArrayIndex : 0;
                const int32 LoopMax = CustomPropertyNode ? LoopMin + 1 : Property->ArrayDim;
                for( int32 Idx = LoopMin; Idx < LoopMax; Idx++ )
                {
                    uint8* DataPtr      = Property->ContainerPtrToValuePtr           <uint8>(Data, Idx);
                    uint8* DefaultValue = Property->ContainerPtrToValuePtrForDefaults<uint8>(DefaultsStruct, Defaults, Idx);
                                        // 判断该属性是否为CDO中的属性值相同,如果相同就不必存了(节约)
                    if( CustomPropertyNode || !Ar.DoDelta() || Ar.IsTransacting() || (!Defaults && !dynamic_cast<const UClass*>(this)) || !Property->Identical( DataPtr, DefaultValue, Ar.GetPortFlags()) )
                    {
                        if (bUseAtomicSerialization)
                        {
                            DefaultValue = NULL;
                        }
#if WITH_EDITOR
                        static const FName NAME_PropertySerialize = FName(TEXT("PropertySerialize"));
                        FArchive::FScopeAddDebugData P(Ar, NAME_PropertySerialize);
                        FArchive::FScopeAddDebugData S(Ar, Property->GetFName());
#endif
                        FPropertyTag Tag( Ar, Property, Idx, DataPtr, DefaultValue );
                        // If available use the property guid from BlueprintGeneratedClasses, provided we aren't cooking data.
                        if (bArePropertyGuidsAvailable && !Ar.IsCooking())
                        {
                            const FGuid PropertyGuid = FindPropertyGuidFromName(Tag.Name);
                            Tag.SetPropertyGuid(PropertyGuid);
                        }
                        Ar << Tag;

                        // need to know how much data this call to SerializeTaggedProperty consumes, so mark where we are
                        int64 DataOffset = Ar.Tell();

                        // if using it, save the current custom property list and switch to its sub property list (in case of UStruct serialization)
                        const FCustomPropertyListNode* SavedCustomPropertyList = nullptr;
                        if(Ar.ArUseCustomPropertyList && CustomPropertyNode)
                        {
                            SavedCustomPropertyList = Ar.ArCustomPropertyList;
                            Ar.ArCustomPropertyList = CustomPropertyNode->SubPropertyList;
                        }

                        Tag.SerializeTaggedProperty( Ar, Property, DataPtr, DefaultValue );

                        // restore the original custom property list after serializing
                        if (SavedCustomPropertyList)
                        {
                            Ar.ArCustomPropertyList = SavedCustomPropertyList;
                        }

                        // set the tag's size
                        Tag.Size = Ar.Tell() - DataOffset;

                        if ( Tag.Size >  0 )
                        {
                            // mark our current location
                            DataOffset = Ar.Tell();

                            // go back and re-serialize the size now that we know it
                            Ar.Seek(Tag.SizeOffset);
                            Ar << Tag.Size;

                            // return to the current location
                            Ar.Seek(DataOffset);
                        }
                    }
                }
            }
        }
        static FName Temp(NAME_None);
        Ar << Temp;
    }
}
  • UStruct::SerializeBinEx
    特殊情况才会走这个函数,目前个人认为正常存文件不走这个函数。
void UStruct::SerializeBinEx( FArchive& Ar, void* Data, void const* DefaultData, UStruct* DefaultStruct ) const
{
    if ( !DefaultData || !DefaultStruct )
    {
        SerializeBin(Ar, Data);
        return;
    }

    for( TFieldIterator<UProperty> It(this); It; ++It )
    {
                // Serializes the property with the struct's data residing in Data, unless it matches the default
                // 序列化跟CDO中不一样的属性
        It->SerializeNonMatchingBinProperty(Ar, Data, DefaultData, DefaultStruct);
    }
}
  • UStruct::SerializeBin
//
// Serialize all of the class's data that belongs in a particular
// bin and resides in Data.
//
void UStruct::SerializeBin( FArchive& Ar, void* Data ) const
{
    if( Ar.IsObjectReferenceCollector() )
    {
        for( UProperty* RefLinkProperty=RefLink; RefLinkProperty!=NULL; RefLinkProperty=RefLinkProperty->NextRef )
        {
            RefLinkProperty->SerializeBinProperty( Ar, Data );
        }
    }
    else if( Ar.ArUseCustomPropertyList )
    {
        const FCustomPropertyListNode* CustomPropertyList = Ar.ArCustomPropertyList;
        for (auto PropertyNode = CustomPropertyList; PropertyNode; PropertyNode = PropertyNode->PropertyListNext)
        {
            UProperty* Property = PropertyNode->Property;
            if( Property )
            {
                // Temporarily set to the sub property list, in case we're serializing a UStruct property.
                Ar.ArCustomPropertyList = PropertyNode->SubPropertyList;

                Property->SerializeBinProperty(Ar, Data, PropertyNode->ArrayIndex);

                // Restore the original property list.
                Ar.ArCustomPropertyList = CustomPropertyList;
            }
        }
    }
    else
    {
        for (UProperty* Property = PropertyLink; Property != NULL; Property = Property->PropertyLinkNext)
        {
            Property->SerializeBinProperty(Ar, Data);
        }
    }
}

下面为调试时的几张堆栈图:

Paste_Image.png
Paste_Image.png

uasset文件格式

UE中使用统一的格式存储资源(uasset, umap),每个uasset对应一个包(package),存储一个UPackage对象时,会将该包下的所有对象都存到uasset中。UE的uasset文件格式很像Windows下的DLL文件格式(PE格式),并且使用起来神似(下一节分析Linker)。

UE4文件格式.png
  • File Summary 文件头信息
  • Name Table 包中对象的名字表
  • Import Table 存放被该包中对象引用的其它包中的对象信息(路径名和类型)
  • Export Table 该包中的对象信息(路径名和类型)
  • Export Objects 所有Export Table中对象的实际数据。
/**
 * A "table of contents" for an Unreal package file.  Stored at the top of the file.
 */
struct FPackageFileSummary
{
    /**
    * Magic tag compared against PACKAGE_FILE_TAG to ensure that package is an Unreal package.
    */
    int32       Tag;

private:
    /* UE4 file version */
    int32       FileVersionUE4;
    /* Licensee file version */
    int32       FileVersionLicenseeUE4;
    /* Custom version numbers. Keyed off a unique tag for each custom component. */
    FCustomVersionContainer CustomVersionContainer;

public:
    /**
    * Total size of all information that needs to be read in to create a FLinkerLoad. This includes
    * the package file summary, name table and import & export maps.
    */
    int32       TotalHeaderSize;

    /**
    * The flags for the package
    */
    uint32  PackageFlags;

    /**
    * The Generic Browser folder name that this package lives in
    */
    FString FolderName;

    /**
    * Number of names used in this package
    */
    int32       NameCount;

    /**
    * Location into the file on disk for the name data
    */
    int32   NameOffset;

    /**
    * Number of gatherable text data items in this package
    */
    int32   GatherableTextDataCount;

    /**
    * Location into the file on disk for the gatherable text data items
    */
    int32   GatherableTextDataOffset;

    /**
    * Number of exports contained in this package
    */
    int32       ExportCount;

    /**
    * Location into the file on disk for the ExportMap data
    */
    int32       ExportOffset;

    /**
    * Number of imports contained in this package
    */
    int32       ImportCount;

    /**
    * Location into the file on disk for the ImportMap data
    */
    int32       ImportOffset;

    /**
    * Location into the file on disk for the DependsMap data
    */
    int32       DependsOffset;

    /**
    * Number of references contained in this package
    */
    int32       StringAssetReferencesCount;

    /**
    * Location into the file on disk for the string asset references map data
    */
    int32       StringAssetReferencesOffset;

    /**
    * Location into the file on disk for the SearchableNamesMap data
    */
    int32       SearchableNamesOffset;

    /**
    * Thumbnail table offset
    */
    int32       ThumbnailTableOffset;

    /**
    * Current id for this package
    */
    FGuid   Guid;

    /**
    * Data about previous versions of this package
    */
    TArray<FGenerationInfo> Generations;

    /**
    * Engine version this package was saved with. For hotfix releases and engine versions which maintain strict binary compatibility with another version, this may differ from CompatibleWithEngineVersion.
    */
    FEngineVersion SavedByEngineVersion;

    /**
    * Engine version this package is compatible with. See SavedByEngineVersion.
    */
    FEngineVersion CompatibleWithEngineVersion;

    /**
    * Flags used to compress the file on save and uncompress on load.
    */
    uint32  CompressionFlags;

    /**
    * Value that is used to determine if the package was saved by Epic (or licensee) or by a modder, etc
    */
    uint32  PackageSource;

    /**
    * Array of compressed chunks in case this package was stored compressed.
    */
    TArray<FCompressedChunk> CompressedChunks;

    /**
    * If true, this file will not be saved with version numbers or was saved without version numbers. In this case they are assumed to be the current version.
    * This is only used for full cooks for distribution because it is hard to guarantee correctness
    **/
    bool bUnversioned;

    /**
    * Location into the file on disk for the asset registry tag data
    */
    int32   AssetRegistryDataOffset;

    /** Offset to the location in the file where the bulkdata starts */
    int64   BulkDataStartOffset;
    /**
    * Offset to the location in the file where the FWorldTileInfo data starts
    */
    int32   WorldTileInfoDataOffset;

    /**
    * Streaming install ChunkIDs
    */
    TArray<int32>   ChunkIDs;

    int32       PreloadDependencyCount;

    /**
    * Location into the file on disk for the preload dependency data
    */
    int32       PreloadDependencyOffset;
};

导入表条目FObjectImport

/**
 * UObject resource type for objects that are referenced by this package, but contained
 * within another package.
 */
struct FObjectImport 
{
    /**
     * The name of the UObject represented by this resource.
     * Serialized
     */
    FName           ObjectName;  // 对象名称

    /**
     * Location of the resource for this resource's Outer.  Values of 0 indicate that this resource
     * represents a top-level UPackage object (the linker's LinkerRoot).
     * Serialized
     */
    FPackageIndex   OuterIndex;  // 对象的Outer的Index

    /**
     * The name of the package that contains the class of the UObject represented by this resource.
     * Serialized
     */
    FName           ClassPackage;  // 该对象的类元数据(UClass对象)所在的包名

    /**
     * The name of the class for the UObject represented by this resource.
     * Serialized
     */
    FName           ClassName;   // 该对象的类元数据名称

// 后面的数据为运行时填写
    /**
     * The UObject represented by this resource.  Assigned the first time CreateImport is called for this import.
     * Transient
     */
    UObject*        XObject;

    /**
     * The linker that contains the original FObjectExport resource associated with this import.
     * Transient
     */
    FLinkerLoad*    SourceLinker;  // 该对象由哪个Linker加载的

    /**
     * Index into SourceLinker's ExportMap for the export associated with this import's UObject.
     * Transient
     */
    int32                SourceIndex;
    bool            bImportPackageHandled;
    bool            bImportSearchedFor;
    bool            bImportFailed;
};

导出表的条目FObjectExport

/**
 * UObject resource type for objects that are contained within this package and can
 * be referenced by other packages.
 */
struct FObjectExport 
{
    /**
     * The name of the UObject represented by this resource.
     * Serialized
     */
    FName           ObjectName;  // 对象名称

    /**
     * Location of the resource for this resource's Outer.  Values of 0 indicate that this resource
     * represents a top-level UPackage object (the linker's LinkerRoot).
     * Serialized
     */
    FPackageIndex   OuterIndex;  // 对象的Outer的Index

    /**
     * Location of the resource for this export's class (if non-zero).  A value of zero
     * indicates that this export represents a UClass object; there is no resource for
     * this export's class object
     * Serialized
     */
    FPackageIndex   ClassIndex;

    /**
    * Location of this resource in export map. Used for export fixups while loading packages.
    * Value of zero indicates resource is invalid and shouldn't be loaded.
    * Not serialized.
    */
    FPackageIndex ThisIndex;

    /**
     * Location of the resource for this export's SuperField (parent).  Only valid if
     * this export represents a UStruct object. A value of zero indicates that the object
     * represented by this export isn't a UStruct-derived object.
     * Serialized
     */
    FPackageIndex   SuperIndex;

    /**
    * Location of the resource for this export's template/archetypes.  Only used
    * in the new cooked loader. A value of zero indicates that the value of GetArchetype
    * was zero at cook time, which is more or less impossible and checked.
    * Serialized
    */
    FPackageIndex   TemplateIndex; // 对象原形的Index

    /**
     * The object flags for the UObject represented by this resource.  Only flags that
     * match the RF_Load combination mask will be loaded from disk and applied to the UObject.
     * Serialized
     */
    EObjectFlags    ObjectFlags;

    /**
     * The number of bytes to serialize when saving/loading this export's UObject.
     * Serialized
     */
    int64           SerialSize;      //对象占据的磁盘大小

    /**
     * The location (into the FLinker's underlying file reader archive) of the beginning of the
     * data for this export's UObject.  Used for verification only.
     * Serialized
     */
    int64           SerialOffset;  // 文件中的偏移

    /**
     * The location (into the FLinker's underlying file reader archive) of the beginning of the
     * portion of this export's data that is serialized using script serialization.
     * Transient
     */
    int32               ScriptSerializationStartOffset;

    /**
     * The location (into the FLinker's underlying file reader archive) of the end of the
     * portion of this export's data that is serialized using script serialization.
     * Transient
     */
    int32               ScriptSerializationEndOffset;

    /**
     * The UObject represented by this export.  Assigned the first time CreateExport is called for this export.
     * Transient
     */
    UObject*        Object;

    /**
     * The index into the FLinker's ExportMap for the next export in the linker's export hash table.
     * Transient
     */
    int32               HashNext;

    /**
     * Whether the export was forced into the export table via OBJECTMARK_ForceTagExp.
     * Serialized
     */
    bool            bForcedExport;   

    /**
     * Whether the export should be loaded on clients
     * Serialized
     */
    bool            bNotForClient;   // 游戏Client是否使用该对象

    /**
     * Whether the export should be loaded on servers
     * Serialized
     */
    bool            bNotForServer;  // 游戏server是否使用该对象

    /**
     * Whether the export should be always loaded in editor game
     * False means that the object is 
     * True doesn't means, that the object won't be loaded.
     * Serialized
     */
    bool            bNotAlwaysLoadedForEditorGame;

    /**
     * True if this export is an asset object.
     */
    bool            bIsAsset;

    /**
     * Force this export to not load, it failed because the outer didn't exist.
     */
    bool            bExportLoadFailed;

    /**
     * Export is a dynamic type.
     */
    enum class EDynamicType : uint8
    {
        NotDynamicExport,
        DynamicType,
        ClassDefaultObject,
    };

    EDynamicType    DynamicType;

    /**
     * Export was filtered out on load
     */
    bool            bWasFiltered;

    /** If this object is a top level package (which must have been forced into the export table via OBJECTMARK_ForceTagExp)
     * this is the GUID for the original package file
     * Serialized
     */
    FGuid           PackageGuid;

    /** If this object is a top level package (which must have been forced into the export table via OBJECTMARK_ForceTagExp)
     * this is the package flags for the original package file
     * Serialized
     */
    uint32          PackageFlags;
};

备注: FPackageIndex表示Linker中Import Table或Export Table中的索引, 分如下情形

  1. PackageIndex > 0 表示在Export Table中的索引,实际索引 Index = PackageIndex - 1;
  2. PackageIndex < 0 表示在Export Table中的索引,实际索引 Index = -(PackageIndex + 1);
  3. PackageIndex == 0 表示当前UPackage对象

FLinkerLoad

负责将uasset文件中的对象加载到内存中,起桥梁作用。相关源码:
Engine\Source\Runtime\CoreUObject\Public\UObject\LinkerLoad.h
Engine\Source\Runtime\CoreUObject\Private\UObject\LinkerLoad.cpp

  1. FLinkerLoad::ELinkerStatus FLinkerLoad::Tick( float InTimeLimit, bool bInUseTimeLimit, bool bInUseFullTimeLimit );
    用于解析uasset文件,当bInUseTimeLimit为true时, Tick不会一次性做完解析工作,分时间片进行加载,在一帧里,Tick()不会占用太多时间。
  2. UObject* CreateExport( int32 Index );
    创建Export Table中Index位置的对象。
  3. void LoadAllObjects(bool bForcePreload);
    加载Export Table中的所有对象。

在阅读CreateExport, CreateImport等对象加载代码时需要明白Outer, Class, PackageIndex这些概念,先根据类型创建出对象,然后才Serialize对象的数据。

: 读取包中对象时,可以一次性加载所有export table中的对象,也可以按需加载某个对象(比如包P0中的对象A被包P1引用,在加载P1时,可能只会加载P0中的对象A,而不是P0中的所有对象)。

关于异步加载
Engine\Source\Runtime\CoreUObject\Private\Serialization\AsyncLoading.cpp
Engine\Source\Runtime\CoreUObject\Private\Serialization\AsyncLoadingThread.h

FLinkerSave

负责将内存Package中的对象存储到uasset文件。相关源码:
Engine\Source\Runtime\CoreUObject\Public\UObject\LinkerSave.h
Engine\Source\Runtime\CoreUObject\Private\UObject\LinkerSave.cpp
LinkerSave做的活不多, 要特别注意序列化Object时的巧妙.

FArchive& FLinkerSave::operator<<( UObject*& Obj )
{
    FPackageIndex Save;
    if (Obj)
    {
        Save = MapObject(Obj);   // 返回的是PackageIndex
    }
    return *this << Save;
}

在保存一个Package时主要工作是在FSavePackageResultStruct UPackage::Save(UPackage* InOuter, UObject* Base, EObjectFlags TopLevelFlags, const TCHAR* Filename, FOutputDevice* Error, FLinkerLoad* Conform, bool bForceByteSwapping, bool bWarnOfLongFilename, uint32 SaveFlags, const class ITargetPlatform* TargetPlatform, const FDateTime& FinalTimeStamp, bool bSlowTask);函数中。
源码路径:Engine\Source\Runtime\CoreUObject\Private\UObject\SavePackage.cpp

部分值得注意的代码截图:

Paste_Image.png
Paste_Image.png
Paste_Image.png
写入Name Table
Paste_Image.png
Paste_Image.png
写入Export Object

结尾

还有以下几点没有讲述

  1. uasset文件的压缩
  2. uasset的Cook(过滤掉与游戏发布时无关的数据)
  3. BulkData(BulkData是指一大块数据, 它也被存在uasset文件中,但是加载对象的时候可以不加载它,等到需要时在问LinkerLoad要数据,例如UTexture2D的纹理数据的加载)。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • JAVA序列化机制的深入研究 对象序列化的最主要的用处就是在传递,和保存对象(object)的时候,保证对象的完整...
    时待吾阅读 10,739评论 0 24
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,635评论 0 9
  • Swift版本点击这里欢迎加入QQ群交流: 594119878最新更新日期:18-09-17 About A cu...
    ylgwhyh阅读 24,860评论 7 249
  • 我的人生愿望清单里有一条:做个小面品鉴师。 当然,这是我自己发明的。作为一个成都人,对小面的热爱那是天生的。 我打...
    轩轩Amour阅读 1,173评论 4 6
  • 50 2017年4月8 感恩妈妈的唠叨,感恩她以她的方式在支持我的成长。 感恩大地万物,空气, 感恩引领正能量的人...
    疗愈师李玉阅读 81评论 0 2