探尋Gson解析遇到不存在鍵值時引發的Kotlin的空指針異常的原因

文章目錄

  • 一、問題背景
  • 二、問題原因
  • 三、問題探析
    • Kotlin空指針校驗
    • Gson.fromJson(String json, Class<T> classOfT)
    • TypeToken
    • Gson.fromJson(JsonReader reader, TypeToken<T> typeOfT)
    • TypeAdapter 和 TypeAdapterFactory
    • ReflectiveTypeAdapterFactory
    • RecordAdapter 和 FieldReflectionAdapter
  • 四、解決方法

一、問題背景

在一次開發過程中,由于在 Kotlin 定義的實體類多了一個 json 不存在的 時,即使是對象類型是不可空的對象且指定了默認值,使用 Gson 庫解析出來的實體對象中的那個變量是null,導致后面使用的此變量的時候導致出現空指針異常。比如:
實體類對象定義如下

data class Entity(/*** 存在的元素*/val existParam: String,/*** 不存在的元素*/val nonExistParam: String = ""
)

nonExistParamjson 結構中不存在的 keyjson 如下

{"existParam" : "exist"
}

使用 Gson 進行解析 json

val jsonEntity = Gson().fromJson(json, Entity::class.java)
println("entity = $jsonEntity")

最后得到的輸出為:
entity = Entity(existParam=exist, nonExistParam=null)

此時可以發現,nonExistParam 已經被指定為不可空的String 類型,且使用了默認值 "",但解析出來的實體類中nonExistParam=null,如果此時不注意直接使用 nonExistParam,可能引發空指針異常。

二、問題原因

此問題的原因是,Gson 在解析實體類的時候會使用反射構造方法創建對象,在通過反射的方式設置對象的值。因此,如果實體類的成員在json中不存在,則不會有機會被賦值,其會保持一個默認值(對于對象來說即為空)。而在 Kotlin 中,只要在調用實際方法的時候,會觸發Kotlin的空校驗,從而拋出空指針異常,提早發現問題。但是Gson的反射的方式避開了這個空校驗,所以成員的值為 null,直到使用時可能會出現空指針異常

三、問題探析

我們需要探尋 Gson 在解析 json 的時候,究竟發生了什么,導致會出現解析出來的對象出現了 null

Kotlin空指針校驗

但是,我們知道Kotlin是對可空非常敏感的,已經指定了成員是不可空的,為什么會把 null 賦值給了不可空成員呢。

我們可以看 Kotlin 的字節碼,并反編譯成java源碼,可以看到最后由Kotlin生成的java源碼是怎樣的。
在這里插入圖片描述
我們可以得到如下的兩個方法。

public Entity(@NotNull String existParam, @NotNull String nonExistParam) {Intrinsics.checkNotNullParameter(existParam, "existParam");Intrinsics.checkNotNullParameter(nonExistParam, "nonExistParam");super();this.existParam = existParam;this.nonExistParam = nonExistParam;
}// $FF: synthetic method
public Entity(String var1, String var2, int var3, DefaultConstructorMarker var4) {if ((var3 & 2) != 0) {var2 = "";}this(var1, var2);
}

第一個即為構造方法,傳遞了兩個參數,且有 Intrinsics.checkNotNullParameter 可空檢查。如果這里有空,則會拋出異常。而下一個則是因為對 nonExistParam 的變量設置了默認值生成的構造方法,默認值為 “”
因此,只有正常調用構造方法的時候,才會觸發可空的檢查。

Gson.fromJson(String json, Class classOfT)

首先,我們使用的方法是 Gson.fromJson(String json, Class<T> classOfT),這個方法是傳進一個 json 的字符串和實體對象的 Class 類型,隨后的返回值就是一個實體對象。方法如下:

public <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException {T object = fromJson(json, TypeToken.get(classOfT));return Primitives.wrap(classOfT).cast(object);
}

我們先看 Primitives.wrap(classOfT).cast(object); 這句的作用,點進去看 Primitives.wrap()方法:

/*** Returns the corresponding wrapper type of {@code type} if it is a primitive type; otherwise* returns {@code type} itself. Idempotent.** <pre>*     wrap(int.class) == Integer.class*     wrap(Integer.class) == Integer.class*     wrap(String.class) == String.class* </pre>*/
@SuppressWarnings({"unchecked", "MissingBraces"})
public static <T> Class<T> wrap(Class<T> type) {if (type == int.class) return (Class<T>) Integer.class;if (type == float.class) return (Class<T>) Float.class;if (type == byte.class) return (Class<T>) Byte.class;if (type == double.class) return (Class<T>) Double.class;if (type == long.class) return (Class<T>) Long.class;if (type == char.class) return (Class<T>) Character.class;if (type == boolean.class) return (Class<T>) Boolean.class;if (type == short.class) return (Class<T>) Short.class;if (type == void.class) return (Class<T>) Void.class;return type;
}

從代碼中可以看出,這個方法的作用就是將基本數據類型轉換成包裝類,即將 int 轉換成 Integer,將 float 轉換成 Float 等。如果非基本數據類,則直接返回類的本身。而隨后接的 .cast(object) 則是強制數據類型轉換的的Class類接口,即是 (T) object

因此最后一句的作用只是用來強制轉換對象的,與解析 json 無關。我們回到第一句 T object = fromJson(json, TypeToken.get(classOfT));,這句代碼調用了 Gson.fromJson(String json, TypeToken<T> typeOfT),并使用 TypeToken 包裝了 class

TypeToken

我們先看 TypeToken 的官方文檔解釋:

Represents a generic type T. Java doesn’t yet provide a way to represent generic types, so this class does. Forces clients to create a subclass of this class which enables retrieval the type information even at runtime.

這是一個代表泛型T(generic type T)的類,在 Java 運行時會進行泛型擦除,因此在運行過程中是無法拿到泛型的準確類型,因此 TypeToken 被創建出來,可以在運行時創建基于此類的子類并拿到泛型的信息。也即這個類通過包裝泛型類,提供了在運行時獲取泛型對象的類信息的能力。

Gson.fromJson(JsonReader reader, TypeToken typeOfT)

Gson.fromJson(String json, TypeToken<T> typeOfT) 方法開始,層次往下只是將 String 或 其他類型的來源封裝成 JsonReader類,代碼如下:

public <T> T fromJson(String json, TypeToken<T> typeOfT) throws JsonSyntaxException {if (json == null) {return null;}StringReader reader = new StringReader(json);return fromJson(reader, typeOfT);
}public <T> T fromJson(Reader json, TypeToken<T> typeOfT)throws JsonIOException, JsonSyntaxException {JsonReader jsonReader = newJsonReader(json);T object = fromJson(jsonReader, typeOfT);assertFullConsumption(object, jsonReader);return object;
}

首先使用 StringReader 包裝 json 字符串,隨后使用 JsonReader 包裝 StringReader,隨后再調用 Gson.fromJson(JsonReader reader, TypeToken<T> typeOfT) 進行解析 json,得到 <T> 對象。因此我們來看 Gson.fromJson(String json, TypeToken<T> typeOfT) 方法。

public <T> T fromJson(JsonReader reader, TypeToken<T> typeOfT)throws JsonIOException, JsonSyntaxException {boolean isEmpty = true;Strictness oldStrictness = reader.getStrictness();if (this.strictness != null) {reader.setStrictness(this.strictness);} else if (reader.getStrictness() == Strictness.LEGACY_STRICT) {// For backward compatibility change to LENIENT if reader has default strictness LEGACY_STRICTreader.setStrictness(Strictness.LENIENT);}try {JsonToken unused = reader.peek();isEmpty = false;TypeAdapter<T> typeAdapter = getAdapter(typeOfT);return typeAdapter.read(reader);} catch (EOFException e) {/** For compatibility with JSON 1.5 and earlier, we return null for empty* documents instead of throwing.*/if (isEmpty) {return null;}throw new JsonSyntaxException(e);} catch (IllegalStateException e) {throw new JsonSyntaxException(e);} catch (IOException e) {// TODO(inder): Figure out whether it is indeed right to rethrow this as JsonSyntaxExceptionthrow new JsonSyntaxException(e);} catch (AssertionError e) {throw new AssertionError("AssertionError (GSON " + GsonBuildConfig.VERSION + "): " + e.getMessage(), e);} finally {reader.setStrictness(oldStrictness);}
}

這個方法的一開始是將Gson的 Strictness設置給 JsonReader。隨后再獲取 類型的 TypeAdapter,使用TypeAdapterread.read(JsonReader in),進行解析 json得到實體對象。

TypeAdapter 和 TypeAdapterFactory

TypeAdapter 是一個抽象類,其有兩個抽象方法

/*** Writes one JSON value (an array, object, string, number, boolean or null) for {@code value}.** @param value the Java object to write. May be null.*/
public abstract void write(JsonWriter out, T value) throws IOException;/*** Reads one JSON value (an array, object, string, number, boolean or null) and converts it to a* Java object. Returns the converted object.** @return the converted Java object. May be {@code null}.*/
public abstract T read(JsonReader in) throws IOException;

也就是 write() 方法定義如何把 實體對象 轉換成 json字符串 的實現,和 read() 方法定義如何把 json字符串 轉換成 實體對象 的實現。默認已經有部分實現了 Java 常用類的轉換方式,如基礎數據類 int,float,boolean等 和 map 、set、list 提供轉換方式。

TypeAdapterFactory是一個接口,只有一個 creat() 的方法

/*** Returns a type adapter for {@code type}, or null if this factory doesn't support {@code type}.*/
<T> TypeAdapter<T> create(Gson gson, TypeToken<T> type);

此接口將支持的類型 type 返回一個 TypeAdapter,支持的 type 可以是多種類型。如果不支持的話就返回null。因此 TypeAdapterFactoryTypeAdapter 互相配合,可以生成解析和生成json的具體實現方法。

通過一個類型獲取 TypeAdapterGson.getAdapter() 方法如下

public <T> TypeAdapter<T> getAdapter(TypeToken<T> type) {Objects.requireNonNull(type, "type must not be null");TypeAdapter<?> cached = typeTokenCache.get(type);if (cached != null) {@SuppressWarnings("unchecked")TypeAdapter<T> adapter = (TypeAdapter<T>) cached;return adapter;}Map<TypeToken<?>, TypeAdapter<?>> threadCalls = threadLocalAdapterResults.get();boolean isInitialAdapterRequest = false;if (threadCalls == null) {threadCalls = new HashMap<>();threadLocalAdapterResults.set(threadCalls);isInitialAdapterRequest = true;} else {// the key and value type parameters always agree@SuppressWarnings("unchecked")TypeAdapter<T> ongoingCall = (TypeAdapter<T>) threadCalls.get(type);if (ongoingCall != null) {return ongoingCall;}}TypeAdapter<T> candidate = null;try {FutureTypeAdapter<T> call = new FutureTypeAdapter<>();threadCalls.put(type, call);for (TypeAdapterFactory factory : factories) {candidate = factory.create(this, type);if (candidate != null) {call.setDelegate(candidate);// Replace future adapter with actual adapterthreadCalls.put(type, candidate);break;}}} finally {if (isInitialAdapterRequest) {threadLocalAdapterResults.remove();}}if (candidate == null) {throw new IllegalArgumentException("GSON (" + GsonBuildConfig.VERSION + ") cannot handle " + type);}if (isInitialAdapterRequest) {/** Publish resolved adapters to all threads* Can only do this for the initial request because cyclic dependency TypeA -> TypeB -> TypeA* would otherwise publish adapter for TypeB which uses not yet resolved adapter for TypeA* See https://github.com/google/gson/issues/625*/typeTokenCache.putAll(threadCalls);}return candidate;
}

首先,從緩存Map 的 typeTokenCache 中取出 TypeAdapter,如果有的話,則直接返回此 TypeAdapter 進行使用。

Objects.requireNonNull(type, "type must not be null");
TypeAdapter<?> cached = typeTokenCache.get(type);
if (cached != null) {@SuppressWarnings("unchecked")TypeAdapter<T> adapter = (TypeAdapter<T>) cached;return adapter;
}

隨后從 ThreadLocal 中去取出 TypeAdapter,如果有的話,則直接返回此 TypeAdapter 進行使用。如果沒有當前線程的 threadCalls Map,則直接創建新的threadCalls

Map<TypeToken<?>, TypeAdapter<?>> threadCalls = threadLocalAdapterResults.get();
boolean isInitialAdapterRequest = false;
if (threadCalls == null) {threadCalls = new HashMap<>();threadLocalAdapterResults.set(threadCalls);isInitialAdapterRequest = true;
} else {// the key and value type parameters always agree@SuppressWarnings("unchecked")TypeAdapter<T> ongoingCall = (TypeAdapter<T>) threadCalls.get(type);if (ongoingCall != null) {return ongoingCall;}
}

隨后遍歷 Gson 對象的 TypeAdapterFactory List,如果是適合的對象,即通過 TypeAdapterFactory.create() 方法可以創建 TypeAdapter,則直接返回此對象。如果找不到,則會拋出異常。

TypeAdapter<T> candidate = null;
try {FutureTypeAdapter<T> call = new FutureTypeAdapter<>();threadCalls.put(type, call);for (TypeAdapterFactory factory : factories) {candidate = factory.create(this, type);if (candidate != null) {call.setDelegate(candidate);// Replace future adapter with actual adapterthreadCalls.put(type, candidate);break;}}
} finally {if (isInitialAdapterRequest) {threadLocalAdapterResults.remove();}
}if (candidate == null) {throw new IllegalArgumentException("GSON (" + GsonBuildConfig.VERSION + ") cannot handle " + type);
}

因此需要去研究不同類型的 TypeAdapter 的做了什么。

ReflectiveTypeAdapterFactory

Gson 的構造方法中,會將支持的 TypeAdapterFactory 添加進 Gson 類的 fatories 中,有以下語句:

List<TypeAdapterFactory> factories = new ArrayList<>();// built-in type adapters that cannot be overridden
factories.add(TypeAdapters.JSON_ELEMENT_FACTORY);
factories.add(ObjectTypeAdapter.getFactory(objectToNumberStrategy));// the excluder must precede all adapters that handle user-defined types
factories.add(excluder);// users' type adapters
factories.addAll(factoriesToBeAdded);// type adapters for basic platform types
factories.add(TypeAdapters.STRING_FACTORY);
factories.add(TypeAdapters.INTEGER_FACTORY);
factories.add(TypeAdapters.BOOLEAN_FACTORY);
factories.add(TypeAdapters.BYTE_FACTORY);
factories.add(TypeAdapters.SHORT_FACTORY);
TypeAdapter<Number> longAdapter = longAdapter(longSerializationPolicy);
factories.add(TypeAdapters.newFactory(long.class, Long.class, longAdapter));
factories.add(TypeAdapters.newFactory(double.class, Double.class, doubleAdapter(serializeSpecialFloatingPointValues)));
factories.add(TypeAdapters.newFactory(float.class, Float.class, floatAdapter(serializeSpecialFloatingPointValues)));
factories.add(NumberTypeAdapter.getFactory(numberToNumberStrategy));
factories.add(TypeAdapters.ATOMIC_INTEGER_FACTORY);
factories.add(TypeAdapters.ATOMIC_BOOLEAN_FACTORY);
factories.add(TypeAdapters.newFactory(AtomicLong.class, atomicLongAdapter(longAdapter)));
factories.add(TypeAdapters.newFactory(AtomicLongArray.class, atomicLongArrayAdapter(longAdapter)));
factories.add(TypeAdapters.ATOMIC_INTEGER_ARRAY_FACTORY);
factories.add(TypeAdapters.CHARACTER_FACTORY);
factories.add(TypeAdapters.STRING_BUILDER_FACTORY);
factories.add(TypeAdapters.STRING_BUFFER_FACTORY);
factories.add(TypeAdapters.newFactory(BigDecimal.class, TypeAdapters.BIG_DECIMAL));
factories.add(TypeAdapters.newFactory(BigInteger.class, TypeAdapters.BIG_INTEGER));
// Add adapter for LazilyParsedNumber because user can obtain it from Gson and then try to
// serialize it again
factories.add(TypeAdapters.newFactory(LazilyParsedNumber.class, TypeAdapters.LAZILY_PARSED_NUMBER));
factories.add(TypeAdapters.URL_FACTORY);
factories.add(TypeAdapters.URI_FACTORY);
factories.add(TypeAdapters.UUID_FACTORY);
factories.add(TypeAdapters.CURRENCY_FACTORY);
factories.add(TypeAdapters.LOCALE_FACTORY);
factories.add(TypeAdapters.INET_ADDRESS_FACTORY);
factories.add(TypeAdapters.BIT_SET_FACTORY);
factories.add(DefaultDateTypeAdapter.DEFAULT_STYLE_FACTORY);
factories.add(TypeAdapters.CALENDAR_FACTORY);if (SqlTypesSupport.SUPPORTS_SQL_TYPES) {
factories.add(SqlTypesSupport.TIME_FACTORY);
factories.add(SqlTypesSupport.DATE_FACTORY);
factories.add(SqlTypesSupport.TIMESTAMP_FACTORY);
}factories.add(ArrayTypeAdapter.FACTORY);
factories.add(TypeAdapters.CLASS_FACTORY);// type adapters for composite and user-defined types
factories.add(new CollectionTypeAdapterFactory(constructorConstructor));
factories.add(new MapTypeAdapterFactory(constructorConstructor, complexMapKeySerialization));
this.jsonAdapterFactory = new JsonAdapterAnnotationTypeAdapterFactory(constructorConstructor);
factories.add(jsonAdapterFactory);
factories.add(TypeAdapters.ENUM_FACTORY);
factories.add(new ReflectiveTypeAdapterFactory(constructorConstructor,fieldNamingStrategy,excluder,jsonAdapterFactory,reflectionFilters));this.factories = Collections.unmodifiableList(factories);

首先我們根據這個列表順序,結合 for (TypeAdapterFactory factory : factories) 分析得到,對于自己定義的實體類,使用的 TypeAdapterFactoryReflectiveTypeAdapterFactory,即是反射型的 TypeAdapterFactory

我們先來看 ReflectiveTypeAdapterFactory.create() 方法創建 TypeAdapter,這段代碼的作用是根據 class的類型生成不同的TypeAdapter

public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {Class<? super T> raw = type.getRawType();if (!Object.class.isAssignableFrom(raw)) {return null; // it's a primitive!}// Don't allow using reflection on anonymous and local classes because synthetic fields for// captured enclosing values make this unreliableif (ReflectionHelper.isAnonymousOrNonStaticLocal(raw)) {// This adapter just serializes and deserializes null, ignoring the actual values// This is done for backward compatibility; troubleshooting-wise it might be better to throw// exceptionsreturn new TypeAdapter<T>() {@Overridepublic T read(JsonReader in) throws IOException {in.skipValue();return null;}@Overridepublic void write(JsonWriter out, T value) throws IOException {out.nullValue();}@Overridepublic String toString() {return "AnonymousOrNonStaticLocalClassAdapter";}};}FilterResult filterResult =ReflectionAccessFilterHelper.getFilterResult(reflectionFilters, raw);if (filterResult == FilterResult.BLOCK_ALL) {throw new JsonIOException("ReflectionAccessFilter does not permit using reflection for "+ raw+ ". Register a TypeAdapter for this type or adjust the access filter.");}boolean blockInaccessible = filterResult == FilterResult.BLOCK_INACCESSIBLE;// If the type is actually a Java Record, we need to use the RecordAdapter instead. This will// always be false on JVMs that do not support records.if (ReflectionHelper.isRecord(raw)) {@SuppressWarnings("unchecked")TypeAdapter<T> adapter =(TypeAdapter<T>)new RecordAdapter<>(raw, getBoundFields(gson, type, raw, blockInaccessible, true), blockInaccessible);return adapter;}ObjectConstructor<T> constructor = constructorConstructor.get(type);return new FieldReflectionAdapter<>(constructor, getBoundFields(gson, type, raw, blockInaccessible, false));
}

首先,對于 私有類 、 匿名內部類 、非靜態內部類是不支持生成json的,此時會返回 nullTypeAdapter 或者 不生成 jsonTypeAdapter

Class<? super T> raw = type.getRawType();if (!Object.class.isAssignableFrom(raw)) {return null; // it's a primitive!
}// Don't allow using reflection on anonymous and local classes because synthetic fields for
// captured enclosing values make this unreliable
if (ReflectionHelper.isAnonymousOrNonStaticLocal(raw)) {// This adapter just serializes and deserializes null, ignoring the actual values// This is done for backward compatibility; troubleshooting-wise it might be better to throw// exceptionsreturn new TypeAdapter<T>() {@Overridepublic T read(JsonReader in) throws IOException {in.skipValue();return null;}@Overridepublic void write(JsonWriter out, T value) throws IOException {out.nullValue();}@Overridepublic String toString() {return "AnonymousOrNonStaticLocalClassAdapter";}};
}

如果是 Java 14 之后 Record類,則使用 RecordAdapterTypeAdapter

// If the type is actually a Java Record, we need to use the RecordAdapter instead. This will
// always be false on JVMs that do not support records.
if (ReflectionHelper.isRecord(raw)) {@SuppressWarnings("unchecked")TypeAdapter<T> adapter =(TypeAdapter<T>)new RecordAdapter<>(raw, getBoundFields(gson, type, raw, blockInaccessible, true), blockInaccessible);return adapter;
}

而如果是普通的類型,則使用 FieldReflectionAdapterTypeAdapter

ObjectConstructor<T> constructor = constructorConstructor.get(type);
return new FieldReflectionAdapter<>(constructor, getBoundFields(gson, type, raw, blockInaccessible, false));

RecordAdapter 和 FieldReflectionAdapter

RecordAdapterFieldReflectionAdapter 都是 Adapter 的子類,其都沒有覆寫 writeread 的方法,因此我們直接看 Adapter 的的 read 方法。

@Override
public T read(JsonReader in) throws IOException {if (in.peek() == JsonToken.NULL) {in.nextNull();return null;}A accumulator = createAccumulator();Map<String, BoundField> deserializedFields = fieldsData.deserializedFields;try {in.beginObject();while (in.hasNext()) {String name = in.nextName();BoundField field = deserializedFields.get(name);if (field == null) {in.skipValue();} else {readField(accumulator, in, field);}}} catch (IllegalStateException e) {throw new JsonSyntaxException(e);} catch (IllegalAccessException e) {throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e);}in.endObject();return finalize(accumulator);
}

首先,會通過 A accumulator = createAccumulator(); 方法獲取到一個指定類型的對象,從方法中可以看到,其實是調用 constructor.construct(); 反射調用構造方法生成指定類型的對象。

// FieldReflectionAdapter.java
@Override
T createAccumulator() {return constructor.construct();
}// RecordAdapter.java
@Override
Object[] createAccumulator() {return constructorArgsDefaults.clone();
}

在初始化的時候,會先調用 getBoundFields() 方法,通過反射的方式,獲取指定類型已經聲明了的成員。因此通過get 方法,去判斷 jsonkey 是否存在,

BoundField field = deserializedFields.get(name);
if (field == null) {in.skipValue();
} else {readField(accumulator, in, field);
}

可以看 FieldReflectionAdapterreadField 方法 (Kotlin對象未使用Recond

@Override
void readField(T accumulator, JsonReader in, BoundField field)throws IllegalAccessException, IOException {field.readIntoField(in, accumulator);
}

繼續往下看 BoundField.readIntoField()

@Override
void readIntoField(JsonReader reader, Object target)throws IOException, IllegalAccessException {Object fieldValue = typeAdapter.read(reader);if (fieldValue != null || !isPrimitive) {if (blockInaccessible) {checkAccessible(target, field);} else if (isStaticFinalField) {// Reflection does not permit setting value of `static final` field, even after calling// `setAccessible`// Handle this here to avoid causing IllegalAccessException when calling `Field.set`String fieldDescription = ReflectionHelper.getAccessibleObjectDescription(field, false);throw new JsonIOException("Cannot set value of 'static final' " + fieldDescription);}field.set(target, fieldValue);}
}

最后是通過反射的方式,field.set(target, fieldValue);json 中的 value 設置到指定對象中具體的成員中。

因此,如果實體類的成員在json中不存在,則不會有機會被賦值,其會保持一個默認值(對于對象來說即為空)

四、解決方法

Gson 解析 json 的源碼中可以得出,由于使用了反射的方式,所以最后生成對象中可能會出現null,尤其是實體類中存在 json 沒有的 key ,或者雖然 key 存在時但 value 就是null。因此,在設計json的實體類的時候,需要考慮成員是可空的情況,盡量使用可空類型,避免出現空指針異常。或者使用kotlinx.serialization 進行Kotlin JSON序列化,保證數據的可空安全性。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/902445.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/902445.shtml
英文地址,請注明出處:http://en.pswp.cn/news/902445.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

ESP-ADF外設子系統深度解析:esp_peripherals組件架構與核心設計(存儲類外設之SPIFFS)

目錄 ESP-ADF外設子系統深度解析&#xff1a;esp_peripherals組件架構與核心設計&#xff08;存儲類外設之SPIFFS&#xff09;1. 簡介2. 模塊概述功能定義架構位置核心特性 SPIFFS外設SPIFFS外設概述SPIFFS外設層次架構圖 SPIFFS外設API和數據結構外設層API公共API內部API內部數…

【Pandas】pandas DataFrame truediv

Pandas2.2 DataFrame Binary operator functions 方法描述DataFrame.add(other)用于執行 DataFrame 與另一個對象&#xff08;如 DataFrame、Series 或標量&#xff09;的逐元素加法操作DataFrame.add(other[, axis, level, fill_value])用于執行 DataFrame 與另一個對象&…

開發網頁程序時預覽時遇到跨域問題解決方法

CocosCreator 開發h5游戲要用接口、開發html程序網頁程序在chrome中預覽時都會遇到跨域問題,怎么辦? 網上有很多方法,主要是通過服務器端去配置,但那個相對來說消弱安全問題,這個不建議,因為是開發,個人行業,我們知道問題所以,簡單點就主要是通過chrome的參數來禁用: 關閉 Ch…

C語言main的參數;argc與argv

目錄 前言 什么是命令行參數 argc與argv argc (Argument Count) argv (Argument Vector) 示例 前言 在C語言中&#xff0c;main函數的標準形式通常有兩種&#xff1a; int main(void)int main(int argc, char *argv[]) 其中&#xff0c;argc 和 argv 是用于處理命令行參數…

實驗一 進程控制實驗

一、實驗目的 1、掌握進程的概念&#xff0c;理解進程和程序的區別。 2、認識和了解并發執行的實質。 3、學習使用系統調用fork()創建新的子進程方法&#xff0c;理解進程樹的概念。 4、學習使用系統調用wait()或waitpid()實現父子進程同步。 5、學習使用getpid()和getppi…

【Python Web開發】01-Socket網絡編程01

文章目錄 1.套接字(Socket)1.1 概念1.2 類型1.3 使用步驟 Python 的網絡編程主要用于讓不同的計算機或者程序之間進行數據交換和通信&#xff0c;就好像人與人之間打電話、發消息一樣。 下面從幾個關鍵方面通俗易懂地介紹一下&#xff1a; 1.套接字(Socket) 在 Python 網絡編…

Git 配置 GPG 提交簽名

使用 GPG 對 Git 提交進行簽名&#xff0c;可以證明該提交確實是你本人提交的。這在團隊協作和代碼審核中非常有用&#xff0c;GitHub/GitLab 等平臺也會顯示 “Verified” 標簽。 &#x1f9e9; 一、檢查是否已安裝 GPG gpg --version 如果未安裝&#xff0c;可使用以下命令…

MySQL運維三部曲初級篇:從零開始打造穩定高效的數據庫環境

文章目錄 一、服務器選型——給數據庫一個舒適的家二、系統調優——打造高性能跑道三、MySQL配置——讓數據庫火力全開四、監控體系——數據庫的體檢中心五、備份恢復——數據安全的最后防線六、主從復制——數據同步的藝術七、安全加固——守護數據長城 引言&#xff1a;從小白…

實踐項目開發-hbmV4V20250407-跨平臺開發框架深度解析與VSCode一站式開發實踐

跨平臺開發框架深度解析與VSCode一站式開發實踐 在當今多端應用開發需求激增的背景下&#xff0c;跨平臺開發框架成為了眾多開發者的首選。本文將圍繞React Native、Taro及其結合方案&#xff0c;以及Uni-app、MUI、Quasar等輕量級框架展開詳細分析&#xff0c;并探討如何在VS…

Android15沉浸式界面頂部有問題

Android15沉浸式界面頂部有問題 往往開發人員的手機沒這么高級&#xff0c;客戶或者老板的手機是Android15的。 我明明就設了狀態欄透明&#xff0c;我的手機也沒問題。但Android15是有問題的。 先看下有問題的界面&#xff1a; 解決方案&#xff1a; 處理1&#xff1a; if (…

uni-app 狀態管理深度解析:Vuex 與全局方案實戰指南

uni-app 狀態管理深度解析&#xff1a;Vuex 與全局方案實戰指南 一、Vuex 使用示例 1. 基礎 Vuex 配置 1.1 項目結構 src/ ├── store/ │ ├── index.js # 主入口文件 │ └── modules/ │ └── counter.js # 計數器模塊 └── main.js …

【STM32單片機】#11 I2C通信(軟件讀寫)

主要參考學習資料&#xff1a; B站江協科技 STM32入門教程-2023版 細致講解 中文字幕 開發資料下載鏈接&#xff1a;https://pan.baidu.com/s/1h_UjuQKDX9IpP-U1Effbsw?pwddspb 單片機套裝&#xff1a;STM32F103C8T6開發板單片機C6T6核心板 實驗板最小系統板套件科協 實驗&…

每天一道面試題@第一天

1&#xff1a;TCP和UDP的區別&#xff0c;TCP為什么是三次握手&#xff0c;不是兩次&#xff1f; 因為TCP是全雙工協議&#xff0c;區別在于TCP可靠&#xff0c;UDP不可靠&#xff0c;效率更高。 詳解&#xff1a; TCP&#xff08;傳輸控制協議&#xff09;和 UDP&#xff08;…

一款強大的實時協作Markdown工具 | CodiMD 9.6K ?

CodiMD 介紹 CodiMD 是一個開源的實時協作 Markdown 筆記工具&#xff0c;它允許用戶在任何平臺上共同編輯 Markdown 文檔。核心功能是實時協作&#xff0c;它允許多個用戶同時編輯同一個文檔&#xff0c;并實時看到彼此的更改。支持實時渲染預覽&#xff0c;支持超多的富文本格…

若依如何切換 tab 不刷新

方法 如上圖配置 菜單中選是否緩存&#xff1a;緩存 資料 前端手冊 |RuoYi:

【浙江大學DeepSeek公開課】回望AI三大主義與加強通識教育

回望AI三大主義與加強通識教育 一、人工智能三大主義二、人工智能發展歷程三、從 ChatGPT 到 DeepSeek四、人工智能通識教育五、人工智能的挑戰與未來 一、人工智能三大主義 符號主義 &#xff1a;邏輯推理&#xff0c;將推理視為計算過程。如蘇格拉底三段論&#xff0c;通過前…

邊緣計算全透視:架構、應用與未來圖景

邊緣計算全透視&#xff1a;架構、應用與未來圖景 一、產生背景二、本質三、特點&#xff08;一&#xff09;位置靠近數據源&#xff08;二&#xff09;分布式架構&#xff08;三&#xff09;實時性要求高 四、關鍵技術&#xff08;一&#xff09;硬件技術&#xff08;二&#…

C++——多態、抽象類和接口

目錄 多態的基本概念 如何實現多態 在C中&#xff0c;派生類對象可以被當作基類對象使用 編程示例 關鍵概念總結 抽象類 一、抽象類的定義 基本語法 二、抽象類的核心特性 1. 不能直接實例化 2. 派生類必須實現所有純虛函數才能成為具體類 3. 可以包含普通成員函數和…

初級達夢dba的技能水準

在x86環境&#xff08;windows、linux&#xff09;安裝單機軟件&#xff0c;安裝客戶端創建過至少20套數據庫&#xff0c;優化參數并更新過正式許可會用邏輯導出導入以及dmrman備份了解manager工具的使用配置sqllog日志&#xff0c;并能解釋輸出內容能夠分析因磁盤空間不足、內…

監控頁面卡頓PerformanceObserver

監控頁面卡頓PerformanceObserver 性能觀察器掘金 const observer new PerformanceObserver((list) > {}); observer.observe({entryTypes: [longtask], })