深入理解Newtonsoft.Json库的内部序列化和反序列化过程详解

在这里插入图片描述

1. 前言

Newtonsoft.Json是一个常用的JSON序列化和反序列化库,我相信很多人在使用Newtonsoft.Json进行序列化和反序列化操作的时候,都会使用如下代码,那么有没有思考过他们内部是怎样实现的一个过程,它的核心原理是什么?

/// <summary>
/// JSON帮助类
/// </summary>
public class JsonHelper
{
    /// <summary>
    /// 序列化
    /// </summary>
    /// <param name="value"></param>
    /// <returns></returns>
    public static string SerializeObject(object value)
    {
        return JsonConvert.SerializeObject(value);
    }

    /// <summary>
    /// 反序列化
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="value"></param>
    /// <returns></returns>
    public static T DeserializeObject<T>(string value)
    {
        return JsonConvert.DeserializeObject<T>(value);
    }
}

2. Newtonsoft.Json库内部序列化和反序列化的核心原理:

Newtonsoft.Json库内部序列化和反序列化的核心原理与.NET内置的JsonSerializer类类似,但实现更加灵活和高效。它使用反射机制获取对象的属性和字段信息,递归地遍历对象,并将其转换为JSON键值对或将JSON键值对转换为对象的属性值。在此过程中,还可以通过配置选项来自定义序列化和反序列化的行为,同时采用缓存和延迟反序列化等技术来提高性能。

JSON序列化:

JsonConvert.SerializeObject方法将对象转换为JSON字符串。

  • 反射机制获取对象的类型信息,并通过属性和字段访问器获取对象的属性值。 遍历对象的属性和字段,将它们转换为相应的JSON键值对。
  • 对象的属性和字段的命名约定(例如,JsonProperty特性)可以影响JSON中的键名。
  • 内置的JsonConverter类使用特定的转换器来处理特殊类型的对象,例如DateTime、Enum等。
  • 将JSON键值对拼接成字符串,生成最终的JSON字符串。

JSON反序列化:

JsonConvert.DeserializeObject方法将JSON字符串转换为对象。

  • 读取JSON字符串,并解析为键值对的集合(JToken)。
  • 根据对象的类型信息创建一个新的实例。
  • 遍历JSON键值对,将其转换为相应的属性值,并设置到对象中。
  • 内置的JsonConverter类使用特定的转换器来处理特殊类型的对象。
  • 对象的属性和字段的命名约定和JsonProperty特性可以影响JSON中的键名识别和属性赋值。

序列化和反序列化配置:

  • JsonConvert提供了一些配置选项,可以通过设置JsonSerializerSettings类的属性来影响序列化和反序列化过程。
  • 这些配置选项包括日期格式、空值处理、类型名称处理等。
  • 可以通过传递JsonSerializerSettings对象给JsonConvert的方法来应用这些配置选项。

3. Newtonsoft.Json库的内部序列化过程

Newtonsoft.Json库的内部序列化过程主要是由JsonSerializer类和JsonTextWriter类来完成的。下面我们将通过源代码的方式来介绍Newtonsoft.Json库的内部序列化过程:

1. 创建JsonWriter对象

public static string SerializeObject(object? value)
{
    return SerializeObject(value, null, (JsonSerializerSettings?)null);
}

public static string SerializeObject(object? value, Type? type, JsonSerializerSettings? settings)
{
   JsonSerializer jsonSerializer = JsonSerializer.CreateDefault(settings);
   return SerializeObjectInternal(value, type, jsonSerializer);
}

private static string SerializeObjectInternal(object? value, Type? type, JsonSerializer jsonSerializer)
{
    StringBuilder sb = new StringBuilder(256);
    StringWriter sw = new StringWriter(sb, CultureInfo.InvariantCulture);
    using (JsonTextWriter jsonWriter = new JsonTextWriter(sw))
    {
        jsonWriter.Formatting = jsonSerializer.Formatting;
        jsonSerializer.Serialize(jsonWriter, value, type);
    }
    return sw.ToString();
}

在SerializeObjectInternal方法中,首先创建了一个StringWriter对象sw,用于将JSON字符串写入内存流中。然后,创建了一个JsonTextWriter对象jsonWriter,并将sw作为其构造函数参数传入。这个jsonWriter对象用于将JSON数据写入目标输出流中。

2. 序列化对象开始

public void Serialize(JsonWriter jsonWriter, object? value, Type? objectType)
{
    SerializeInternal(jsonWriter, value, objectType);
}

internal virtual void SerializeInternal(JsonWriter jsonWriter, object? value, Type? objectType)
{
    // ...省略部分代码...
    JsonSerializerInternalWriter serializerWriter = new JsonSerializerInternalWriter(this);
    serializerWriter.Serialize(traceJsonWriter ?? jsonWriter, value, objectType);
    // ...省略部分代码...     
}

在Serialize方法中,调用了一个名为SerializeInternal的私有方法。这个方法是整个序列化过程的核心方法,并负责处理各种类型的值,并将它们写入JsonWriter中。

3. 遍历对象的属性和字段

public void Serialize(JsonWriter jsonWriter, object? value, Type? objectType)
{
    // ...省略部分代码...   
    JsonContract? contract = GetContractSafe(value);
    try
    {
        if (ShouldWriteReference(value, null, contract, null, null))
        {
            WriteReference(jsonWriter, value!);
        }
        else
        {
            SerializeValue(jsonWriter, value, contract, null, null, null);
        }
    }
    catch (Exception ex)
    {
        // ...省略部分代码... 
    }
    // ...省略部分代码... 
}

private void SerializeValue(JsonWriter writer, object? value, JsonContract? valueContract, JsonProperty? member, JsonContainerContract? containerContract, JsonProperty? containerProperty)
{
    // ...省略部分代码... 
    switch (valueContract.ContractType)
    {
        case JsonContractType.Object:
            SerializeObject(writer, value, (JsonObjectContract)valueContract, member, containerContract, containerProperty);
            break;
        case JsonContractType.Array:
            JsonArrayContract arrayContract = (JsonArrayContract)valueContract;
            if (!arrayContract.IsMultidimensionalArray)
            {
                SerializeList(writer, (IEnumerable)value, arrayContract, member, containerContract, containerProperty);
            }
            else
            {
                SerializeMultidimensionalArray(writer, (Array)value, arrayContract, member, containerContract, containerProperty);
            }
            break;
        case JsonContractType.Primitive:
            SerializePrimitive(writer, value, (JsonPrimitiveContract)valueContract, member, containerContract, containerProperty);
            break;
        case JsonContractType.String:
            SerializeString(writer, value, (JsonStringContract)valueContract);
            break;
        case JsonContractType.Dictionary:
            JsonDictionaryContract dictionaryContract = (JsonDictionaryContract)valueContract;
            SerializeDictionary(writer, (value is IDictionary dictionary) ? dictionary : dictionaryContract.CreateWrapper(value), dictionaryContract, member, containerContract, containerProperty);
            break;
#if HAVE_DYNAMIC
        case JsonContractType.Dynamic:
            SerializeDynamic(writer, (IDynamicMetaObjectProvider)value, (JsonDynamicContract)valueContract, member, containerContract, containerProperty);
            break;
#endif
#if HAVE_BINARY_SERIALIZATION
        case JsonContractType.Serializable:
            SerializeISerializable(writer, (ISerializable)value, (JsonISerializableContract)valueContract, member, containerContract, containerProperty);
            break;
#endif
        case JsonContractType.Linq:
            ((JToken)value).WriteTo(writer, Serializer.Converters.ToArray());
            break;
    }
}

在JsonSerializerInternalWriter类中,根据契约类型使用不同的序列化方法。例如,如果值是一个对象,则调用SerializeObject方法;如果是一个数组,则调用SerializeArray方法。这一步相当于遍历对象的属性和字段,并获取它们的名称和值。

4. 写入属性名

private void SerializeObject(JsonWriter writer, object value, JsonObjectContract contract, JsonProperty member, JsonContainerContract collectionContract, JsonProperty containerProperty)
{
    // ...省略部分代码...

     for (int index = 0; index < contract.Properties.Count; index++)
     {
         JsonProperty property = contract.Properties[index];
         try
         {
             if (!CalculatePropertyValues(writer, value, contract, member, property, out JsonContract? memberContract, out object? memberValue))
             {
                 continue;
             }

             property.WritePropertyName(writer);
             SerializeValue(writer, memberValue, memberContract, property, contract, member);
         }
         catch (Exception ex)
         {
             if (IsErrorHandled(value, contract, property.PropertyName, null, writer.ContainerPath, ex))
             {
                 HandleError(writer, initialDepth);
             }
             else
             {
                 throw;
             }
         }
     }
     // ...省略部分代码...
}

在SerializeObject方法中,对于每个属性或字段,使用
property.WritePropertyName方法将其名称写入JSON字符串中。这个名称通常是属性或字段的名称,但也可以受到JsonProperty特性的影响。

5. 获取属性值

private bool CalculatePropertyValues(JsonWriter writer, object value, JsonContainerContract contract, JsonProperty? member, JsonProperty property, [NotNullWhen(true)]out JsonContract? memberContract, out object? memberValue)
    {
        if (!property.Ignored && property.Readable && ShouldSerialize(writer, property, value) && IsSpecified(writer, property, value))
        {
            if (property.PropertyContract == null)
            {
                property.PropertyContract = Serializer._contractResolver.ResolveContract(property.PropertyType!);
            }

            memberValue = property.ValueProvider!.GetValue(value);
            memberContract = (property.PropertyContract.IsSealed) ? property.PropertyContract : GetContractSafe(memberValue);

            if (ShouldWriteProperty(memberValue, contract as JsonObjectContract, property))
            {
                if (ShouldWriteReference(memberValue, property, memberContract, contract, member))
                {
                    property.WritePropertyName(writer);
                    WriteReference(writer, memberValue!);
                    return false;
                }

                if (!CheckForCircularReference(writer, memberValue, property, memberContract, contract, member))
                {
                    return false;
                }
                // ...省略部分代码...
            }
        }

        memberContract = null;
        memberValue = null;
        return false;
    }
    
internal void WritePropertyName(JsonWriter writer)
{
     // ...省略部分代码... 
    writer.WritePropertyName(propertyName);
}

在CalculatePropertyValues方法中,使用property.ValueProvider!.GetValue(value)获取属性或字段的值。这个ValueProvider对象是用于访问属性或字段的属性访问器。

6. 写入属性值

private void WriteReference(JsonWriter writer, object value)
 {
     string reference = GetReference(writer, value);
    // ...省略部分代码...
     writer.WriteStartObject();
     writer.WritePropertyName(JsonTypeReflector.RefPropertyName, false);
     writer.WriteValue(reference);
     writer.WriteEndObject();
 }

在WriteReference方法中,根据值的类型选择适当的JSON表示形式,并使用JsonWriter将它们写入JSON字符串中。例如,对于字符串值,使用writer.WriteValue((string)value);对于数字值,使用writer.WriteValue((double)value)。

7. 处理特殊类型的对象

public sealed class IsoDateTimeConverter : DateTimeConverterBase
{
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        // ...省略部分代码...

        writer.WriteValue(dateTimeString);
    }
}

对于一些特殊类型的对象,如日期时间、枚举等,Newtonsoft.Json库内置了一些JsonConverter类来处理它们。例如,在IsoDateTimeConverter类中,使用writer.WriteValue(dateTimeString)将日期时间值转换为ISO 8601格式的字符串。

8. 递归处理复杂对象

private void SerializeArray(JsonWriter writer, IEnumerable values, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerProperty)
{
    // ...省略部分代码...

    foreach (object value in values)
    {
        // ...省略部分代码...

        SerializeValue(writer, value, elementType, null, elementContract, contract, member);
    }

    // ...省略部分代码...
}

对于数组或集合等复杂对象,Newtonsoft.Json库使用递归的方式进行序列化。例如,在SerializeArray方法中,遍历数组的每个元素,并使用SerializeValue方法递归地序列化其中的复杂对象。

9. 完成序列化

internal void InternalWriteEnd(JsonContainerType container)
{
    AutoCompleteClose(container);
}

private void AutoCompleteClose(JsonContainerType type)
{
    // ...省略部分代码...
    WriteEnd(token);
    // ...省略部分代码...
}

在序列化过程的最后,使用JsonWriter的WriteEndObject或WriteEndArray等方法,通知序列化过程已经完成。这个方法会调用InternalWriteEnd方法,将JSON结束标记写入输出流中。至此,整个序列化过程就完成了。

10. 小结

Newtonsoft.Json库内部的序列化过程由JsonSerializer类和JsonTextWriter类来完成,涉及创建JsonWriter对象、遍历对象的属性和字段、写入属性名和属性值等步骤。它会根据对象的类型信息和属性访问器来获取属性和字段的名称和值,并使用适当的JSON表示形式将它们写入最终的JSON字符串中。对于特殊类型的对象,可能会使用内置的JsonConverter类进行处理。通过递归处理复杂对象,最终完成整个序列化过程,并将JSON数据写入目标输出流中。

4. Newtonsoft.Json库的内部反序列化过程

Newtonsoft.Json库的内部反序列化过程主要是由JsonSerializer类和JsonTextReader类来完成的。下面我们将通过源代码的方式来介绍Newtonsoft.Json库的内部反序列化过程:

1. 创建JsonTextReader对象

public static T? DeserializeObject<T>(string value)
{
 return DeserializeObject<T>(value, (JsonSerializerSettings?)null);
}

public static object? DeserializeObject(string value, Type? type, JsonSerializerSettings? settings)
{
    // ...省略部分代码...
    JsonSerializer jsonSerializer = JsonSerializer.CreateDefault(settings);
    // ...省略部分代码...
    using (JsonTextReader reader = new JsonTextReader(new StringReader(value)))
    {
        return jsonSerializer.Deserialize(reader, type);
    }
}

在DeserializeObject方法中,首先创建了一个JsonSerializer对象jsonSerializer,用于从JSON字符串中读取数据。然后,创建了一个JsonTextReader对象reader,并调用jsonSerializer的Deserialize的方法用于从输入流中读取JSON数据。

2. 反序列化对象开始

public object? Deserialize(JsonReader reader, Type? objectType)
{
    return DeserializeInternal(reader, objectType);
}

internal virtual object? DeserializeInternal(JsonReader reader, Type? objectType)
{
   // ...省略部分代码...
    JsonSerializerInternalReader serializerReader = new JsonSerializerInternalReader(this);
    object? value = serializerReader.Deserialize(traceJsonReader ?? reader, objectType, CheckAdditionalContent);

    // ...省略部分代码...
    return value;
}

在Deserialize方法中,调用了一个名为DeserializeInternal的私有方法。这个方法是整个反序列化过程的核心方法,并负责根据值的类型创建相应的对象,并逐步填充其属性和字段的值。

3. 读取属性名

public object? Deserialize(JsonReader reader, Type? objectType, bool checkAdditionalContent)
{
    // ...省略部分代码...
    JsonContract contractSafe = GetContractSafe(objectType);
    // ...省略部分代码...
    object result = ((converter == null || !converter.CanRead) ? CreateValueInternal(reader, objectType, contractSafe, null, null, null, null) : DeserializeConvertable(converter, reader, objectType, null));
       // ...省略部分代码...
        return result; 
    // ...省略部分代码...
}

private object? DeserializeConvertable(JsonConverter converter, JsonReader reader, Type objectType, object? existingValue)
 {
     // ...省略部分代码...
     object? value = converter.ReadJson(reader, objectType, existingValue, GetInternalSerializer());
     // ...省略部分代码...
     return value;
 }

在Deserialize方法中,使用converter属性判断当前读取的转换类型,包括Binary,BsonObjectId,DataSet,DataTable,DateTime,IsoDateTime,String,StringUmum,Version,XmlNode,UnixDateTime,EntityKey,DiscriminatedUnion,ExpandoObject等等。

4. 获取属性值

public abstract class JsonConverter
 {
 // ...省略部分代码...
     public abstract object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer);
// ...省略部分代码...
 }
 public abstract class JsonConverter<T> : JsonConverter
 { 
  // ...省略部分代码...
     public sealed override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
     {
         bool existingIsNull = existingValue == null;
         if (!(existingIsNull || existingValue is T))
         {
             throw new JsonSerializationException("Converter cannot read JSON with the specified existing value. {0} is required.".FormatWith(CultureInfo.InvariantCulture, typeof(T)));
         }
         return ReadJson(reader, objectType, existingIsNull ? default : (T?)existingValue, !existingIsNull, serializer);
     }
 // ...省略部分代码...
     
     public abstract T? ReadJson(JsonReader reader, Type objectType, T? existingValue, bool hasExistingValue, JsonSerializer serializer);
      // ...省略部分代码...
 }
 
namespace Newtonsoft.Json.Converters
{
    pblic abstract class DateTimeConverterBase : JsonConverter
    {
      // ...省略部分代码...
    }
    public class IsoDateTimeConverter : DateTimeConverterBase
    {
        // ...省略部分代码...
        public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
        {
            // ...省略部分代码...
            if (reader.TokenType == JsonToken.Date)
            {
                if (t == typeof(DateTimeOffset))
                {
                    return (reader.Value is DateTimeOffset) ? reader.Value : new DateTimeOffset((DateTime)reader.Value!);
                }

                // converter is expected to return a DateTime
                if (reader.Value is DateTimeOffset offset)
                {
                    return offset.DateTime;
                }
                return reader.Value;
            }
            string? dateText = reader.Value?.ToString();
            if (StringUtils.IsNullOrEmpty(dateText) && nullable)
            {
                return null;
            }
            if (t == typeof(DateTimeOffset))
            {
                if (!StringUtils.IsNullOrEmpty(_dateTimeFormat))
                {
                    return DateTimeOffset.ParseExact(dateText, _dateTimeFormat, Culture, _dateTimeStyles);
                }
                else
                {
                    return DateTimeOffset.Parse(dateText, Culture, _dateTimeStyles);
                }
            }
            if (!StringUtils.IsNullOrEmpty(_dateTimeFormat))
            {
                return DateTime.ParseExact(dateText, _dateTimeFormat, Culture, _dateTimeStyles);
            }
            else
            {
                return DateTime.Parse(dateText, Culture, _dateTimeStyles);
            }
        }
    }
}

在支持的基本类型如:IsoDateTimeConverter通过继承JsonConverter类中ReadJson方法,实现方法重写。根据属性或字段的类型和特性,决定是直接读取属性值,并填充其属性和字段的值。

5. 写入属性值

private object? CreateValueInternal(JsonReader reader, Type? objectType, JsonContract? contract, JsonProperty? member, JsonContainerContract? containerContract, JsonProperty? containerMember, object? existingValue)
{
    // ...省略部分代码...
    do
    {
        switch (reader.TokenType)
        {
            case JsonToken.StartObject:
                return CreateObject(reader, objectType, contract, member, containerContract, containerMember, existingValue);
            case JsonToken.StartArray:
                return CreateList(reader, objectType, contract, member, existingValue, null);
            case JsonToken.Integer:
            case JsonToken.Float:
            case JsonToken.Boolean:
            case JsonToken.Date:
            case JsonToken.Bytes:
                return EnsureType(reader, reader.Value, CultureInfo.InvariantCulture, contract, objectType);
            case JsonToken.String:
                {
                    string text = (string)reader.Value;
                    if (objectType == typeof(byte[]))
                    {
                        return Convert.FromBase64String(text);
                    }

                    if (CoerceEmptyStringToNull(objectType, contract, text))
                    {
                        return null;
                    }

                    return EnsureType(reader, text, CultureInfo.InvariantCulture, contract, objectType);
                }
            case JsonToken.StartConstructor:
                {
                    string value = reader.Value!.ToString();
                    return EnsureType(reader, value, CultureInfo.InvariantCulture, contract, objectType);
                }
            case JsonToken.Null:
            case JsonToken.Undefined:
                if (objectType == typeof(DBNull))
                {
                    return DBNull.Value;
                }

                return EnsureType(reader, reader.Value, CultureInfo.InvariantCulture, contract, objectType);
            case JsonToken.Raw:
                return new JRaw((string)reader.Value);
            default:
                throw JsonSerializationException.Create(reader, "Unexpected token while deserializing object: " + reader.TokenType);
            case JsonToken.Comment:
                break;
        }
    }
    while (reader.Read());
   // ...省略部分代码...
}

根据不同的契约类型,选中不同的解析方法。

6. 递归处理复杂对象

private object? CreateObject(JsonReader reader, Type? objectType, JsonContract? contract, JsonProperty? member, JsonContainerContract? containerContract, JsonProperty? containerMember, object? existingValue)
        {
            // ...省略部分代码...
            switch (contract.ContractType)
            {
                case JsonContractType.Object:
                {
                     // ...省略部分代码...
                    return PopulateObject(targetObject, reader, objectContract, member, id);
                }
                case JsonContractType.Primitive:
                {
                      // ...省略部分代码...
                        object? value = CreateValueInternal(reader, resolvedObjectType, primitiveContract, member, null, null, existingValue);

                        reader.ReadAndAssert();
                        return value;
                    }
                    break;
                }
                case JsonContractType.Dictionary:
                {
                     // ...省略部分代码...
                     PopulateDictionary(dictionary, reader, dictionaryContract, member, id);
                    // ...省略部分代码...
                    return targetDictionary;
                }
#if HAVE_DYNAMIC
                case JsonContractType.Dynamic:
                    JsonDynamicContract dynamicContract = (JsonDynamicContract)contract;
                    return CreateDynamic(reader, dynamicContract, member, id);
#endif
#if HAVE_BINARY_SERIALIZATION
                case JsonContractType.Serializable:
                    JsonISerializableContract serializableContract = (JsonISerializableContract)contract;
                    return CreateISerializable(reader, serializableContract, member, id);
#endif
            }
 // ...省略部分代码...
        }
        
private object PopulateObject(object newObject, JsonReader reader, JsonObjectContract contract, JsonProperty? member, string? id)
{
    // ...省略部分代码...
    do
    {
        switch (reader.TokenType)
        {
            case JsonToken.PropertyName:
            {
                string propertyName = reader.Value!.ToString()!;
                 // ...省略部分代码...
                break;
            }
     // ...省略部分代码...
    return newObject;
}

对于复杂对象,如嵌套对象或集合等,Newtonsoft.Json库使用递归的方式进行反序列化。例如,在PopulateObject方法中,遍历对象的每个属性,并使用CreateValueInternal方法递归地填充其中的复杂对象。

7. 处理特殊类型的对象

public sealed class IsoDateTimeConverter : DateTimeConverterBase
{
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        // ...省略部分代码...

        return ParseIsoDateTime(dateTimeString);
    }
}

对于一些特殊类型的对象,如日期时间、枚举等,Newtonsoft.Json库内置了一些JsonConverter类来处理它们。例如,在IsoDateTimeConverter类中,使用ParseIsoDateTime方法将ISO 8601格式的字符串转换为日期时间值。

8. 完成反序列化

private void OnDeserialized(JsonReader reader, JsonContract contract, object value)
 {
      // ...省略部分代码...
     contract.InvokeOnDeserialized(value, Serializer._context);
 }

在反序列化过程的最后,使用JsonContract的OnDeserialized方法,通知反序列化过程已经完成。这个方法会调用注册的回调函数,执行额外的操作。至此,整个反序列化过程就完成了。

9. 小结:

Newtonsoft.Json库内部的反序列化过程由JsonSerializer类和JsonTextReader类来完成,涉及创建JsonReader对象、读取属性名和属性值、填充属性值等步骤。它会根据JSON数据的标记类型和属性的特性,递归地创建对象,并将读取到的属性值填充到相应的属性和字段中。对于特殊类型的对象,可能会使用内置的JsonConverter类进行处理。通过递归处理复杂对象,最终完成整个反序列化过程,并返回反序列化得到的对象。

5. 参考文档:

Introduction

Samples

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

dotnet研习社

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值