从底层原理看Android的序列化是如何实现的

对于Java的序列化,我们可以认为是在数据传输的时候的一套协议或者是一个标准,因为Java存在自己特定的一个数据结构(class),举个例子

data class User(val name: String,val age: Int
)

User是一个对象,我们可以创建一个User对象自己使用,但是实际的场景中,我们往往不会自己单独使用,而是让其他人也用,或者做持久化存储;例如我们之前讲的IPC通信框架,在Server端创建的User,想作为参数传递给客户端使用,所以这个时候就需要将这个特定的数据结构转化为一组基本的二进制字节数据,这个过程就是一次序列化的过程;而反序列化则是将这组字节数据转换为特定数据结构

在实际的业务场景中,如果我们使用序列化传输数据,一般就是两种:Serializable 和 Parcelable。

1 Serializable原理分析

一般我们在使用Serializable的时候,都是实现Serializable接口,例如:

data class User(val name: String,val age: Int
):Serializable

按照序列化的定义,只要是实现了Serializable接口,那么就会将特定的数据结构转换为一组二进制字节数据。

public fun serialize(user: User): ByteArray {val bos = ByteArrayOutputStream()val objectInputSystem = ObjectOutputStream(bos)//写数据objectInputSystem.writeObject(user)return bos.toByteArray()
}

对于序列化,底层的实现就是通过ObjectOutputStream包装类将User转换为一组二进制字节数据。那么反序列化,将一组二进制数据转换为User类,就需要使用ObjectInputStream读取二进制字节数据。

public fun unSerialize(byteArray: ByteArray): User {val bis = ByteArrayInputStream(byteArray)val objectInputSystem = ObjectInputStream(bis)//读数据return objectInputSystem.readObject() as User
}

所以ObjectOutputStream和ObjectInputStream可以认为是Java提供的一个序列化工具,用于将数据拆分和组装。

1.1 与序列化相关的问题

在熟悉了序列化的原理之后,针对序列化相关的一些问题,我们需要了解一下,如果伙伴们在准备面试,这块可能会对你有一些帮助。

object SerializeUtils {fun <T> writeObject(t: T,path:String) {val objectOutputStream = ObjectOutputStream(FileOutputStream(path))//读数据objectOutputStream.writeObject(t)}fun <T> readObject(path: String): T {val objectInputSystem = ObjectInputStream(FileInputStream(path))//读数据return objectInputSystem.readObject() as T}}

serialVersioUID是什么?

首先我们先看一个场景:

//先序列化
val user = User("layz4android", 25)
SerializeUtils.writeObject(user,"/storage/emulated/0/a.out")
//再反序列化
val result = SerializeUtils.readObject<User>("/storage/emulated/0/a.out")
Log.e("TAG", "result==>$result")

这是常规的序列化和反序列化

result==>User(name=layz4android, age=25)

那么此时我做什么操作呢?先序列化将数据存储本地,然后修改一下User实体类的数据结构,加了一个sex字段。

data class User(val name: String,val age: Int,val sex:String
) : Serializable

这个时候报了一个错,具体原因就是要反序列化的这组数据与本地现有类的serialVersionUID不一致。因为每个类默认有一个serialVersionUID,如果没有定义那么就会默认生成,因为此时User类已经发生了变化(新加了一个字段),此时这个类就是一个新的类,与本地存储的序列化数组不一致,从而导致序列化失败。

Caused by: java.io.InvalidClassException: com.lay.learn.asm.binder.User; local class incompatible: stream classdesc serialVersionUID = 7638979641876441127, local class serialVersionUID = -6727369848665126143

所以为了做版本的统一管理,需要引进serialVersionUID这个字段,即便是修改了类中的字段,只要是serialVersionUID一致,在序列化、反序列化的时候就会找到这个类。

data class User(val name: String,val age: Int,
) : Serializable {companion object {val serialVersionUID = 1}
}

即便是新增了一个字段,也不会报错,而是会将这个参数值为空

E/TAG: result==>User(name=layz4android, age=25, sex=null)

transient关键字

还是拿User来说,默认情况下,所有的字段都会参与序列化,那么如果某个字段不想参与序列化,那么有什么手段吗?就是使用transient关键字。

data class User(@Transientval name: String,val age: Int,
) : Serializable {companion object {val serialVersionUID = 1}
}

反序列化后,我们就可以看到,因为name没有参与序列化,所以拿到的值为null

E/TAG: result==>User(name=null, age=25)

如果一个类中的类成员变量不支持序列化,会发生什么情况?

场景如下,UserInner没有实现序列化接口

data class UserInner(val a: Int,val b: String
)

data class User(@Transientval name: String,val age: Int,val inner: UserInner
) : Serializable {companion object {val serialVersionUID = 1}
}

那么当进行序列化操作的时候报错,就是因为UserInner不支持序列化。

Caused by: java.io.NotSerializableException: com.lay.learn.asm.binder.UserInner

那么这里我们需要了解,序列化其实是一次深拷贝的操作。对于浅拷贝(这里不考虑基本数据类型)只是将引用地址做一次拷贝;深拷贝则是需要重新创建一个对象,并把数据拷贝过去。

所以在序列化的时候,因为还需要做一次本地化存储,所以必定是需要拿到UserInner的数据并存储下来,所以就需要把UserInner内部的数据序列化,但是UserInner又不支持序列化,所以就会报错。

如果某个类可以序列化,但是其父类不可以序列化,那么这个类可以序列化吗?

父类:

open class Person(val des: String
)

子类:

data class User(@Transientval name: String,val age: Int,
) : Serializable, Person(name) {companion object {val serialVersionUID = 1}
}

此时进行序列化操作的时候,报错:没有可用的构造函数。

Caused by: java.io.InvalidClassException: com.lay.learn.asm.binder.User; no valid constructorat com.lay.learn.asm.SerializeUtils.readObject(Unknown Source:22)

从堆栈信息中,我们看是在反序列化的时候报错了,说明在序列化存储的时候是没问题的,那么这里我们就需要看下readObject的源码了

private Object readOrdinaryObject(boolean unshared)throws IOException
{//......省略部分代码Class<?> cl = desc.forClass();if (cl == String.class || cl == Class.class|| cl == ObjectStreamClass.class) {throw new InvalidClassException("invalid class descriptor");}Object obj;try {obj = desc.isInstantiable() ? desc.newInstance() : null;} catch (Exception ex) {throw (IOException) new InvalidClassException(desc.forClass().getName(),"unable to create instance").initCause(ex);}//......省略部分代码return obj;
}

这里我们先关注下核心代码,我们发现最终反序列化的时候,是通过反射创建一个Object对象,此时我们看直接调用了newInstance()方法。

所以这里我们这样想,在反序列化的时候类似于创建一个子类的过程,此时应该先创建父类,调用父类的构造方法,因为父类没有实现序列化接口,那么父类信息是缺失的,只能调用一个无参构造方法,那么此时父类没有空参构造方法,因此直接报错

open class Person(val des: String
){constructor() : this("") {}
}

反之,如果子类没实现序列化接口,而父类实现了,那么这种情况下是可以完成序列化的,因为继承关系,子类就能够获取父类的序列化能力。

1.2 Java处理序列化的过程

如果我们想要从源码角度去看序列化的过程,其实只需要关注两个类:ObjectInputStream和ObjectOutputStream。

ObjectOutputStream:用于将特定数据结构拆分成二进制数据,例如类的信息,并本地化存储,那么这个过程如何完成的?

ObjectInputStream:用于将二进制数据合并成想要的数据结构。

1.2.1 数据怎么拆

拆数据的核心方法就是ObjectOutputStream的writeObject方法,在这个中传入要拆解的对象,我们以User为例。

public final void writeObject(Object obj) throws IOException {if (enableOverride) {writeObjectOverride(obj);return;}try {writeObject0(obj, false);} catch (IOException ex) {if (depth == 0) {// BEGIN Android-changed: Ignore secondary exceptions during writeObject().// writeFatalException(ex);try {writeFatalException(ex);} catch (IOException ex2) {// If writing the exception to the output stream causes another exception there// is no need to propagate the second exception or generate a third exception,// both of which might obscure details of the root cause.}// END Android-changed: Ignore secondary exceptions during writeObject().}throw ex;}
}

我们看writeObject0这个方法干了什么?

private void writeObject0(Object obj, boolean unshared)throws IOException{boolean oldMode = bout.setBlockDataMode(false);depth++;try {// ......省略部分代码// check for replacement objectObject orig = obj;Class<?> cl = obj.getClass();ObjectStreamClass desc;Class repCl;// 核心代码 1desc = ObjectStreamClass.lookup(cl, true);if (desc.hasWriteReplaceMethod() &&(obj = desc.invokeWriteReplace(obj)) != null &&(repCl = obj.getClass()) != cl){cl = repCl;desc = ObjectStreamClass.lookup(cl, true);}// END Android-changed: Make only one call to writeReplace.if (enableReplace) {Object rep = replaceObject(obj);if (rep != obj && rep != null) {cl = rep.getClass();desc = ObjectStreamClass.lookup(cl, true);}obj = rep;}// if object replaced, run through original checks a second timeif (obj != orig) {subs.assign(orig, obj);if (obj == null) {writeNull();return;} else if (!unshared && (h = handles.lookup(obj)) != -1) {writeHandle(h);return;}}// remaining cases// BEGIN Android-changed: Make Class and ObjectStreamClass replaceable.if (obj instanceof Class) {writeClass((Class) obj, unshared);} else if (obj instanceof ObjectStreamClass) {writeClassDesc((ObjectStreamClass) obj, unshared);// END Android-changed: Make Class and ObjectStreamClass replaceable.} else if (obj instanceof String) {writeString((String) obj, unshared);} else if (cl.isArray()) {writeArray(obj, desc, unshared);} else if (obj instanceof Enum) {writeEnum((Enum<?>) obj, desc, unshared);} else if (obj instanceof Serializable) {// 核心代码 2writeOrdinaryObject(obj, desc, unshared);} else {if (extendedDebugInfo) {throw new NotSerializableException(cl.getName() + "\n" + debugInfoStack.toString());} else {throw new NotSerializableException(cl.getName());}}} finally {depth--;bout.setBlockDataMode(oldMode);}}
核心代码1

在这里,我们看到拿到User类的class对象,然后创建了一个ObjectStreamClass类,我们看下这个类是干啥的。

private ObjectStreamClass(final Class<?> cl) {this.cl = cl;// 获取类名name = cl.getName();isProxy = Proxy.isProxyClass(cl);isEnum = Enum.class.isAssignableFrom(cl);// 是否实现了serializable接口serializable = Serializable.class.isAssignableFrom(cl);externalizable = Externalizable.class.isAssignableFrom(cl);Class<?> superCl = cl.getSuperclass();superDesc = (superCl != null) ? lookup(superCl, false) : null;localDesc = this;if (serializable) {AccessController.doPrivileged(new PrivilegedAction<Void>() {public Void run() {// 获取serialVersionUIDsuid = getDeclaredSUID(cl);try {// 获取全部的字段信息fields = getSerialFields(cl);computeFieldOffsets();} catch (InvalidClassException e) {serializeEx = deserializeEx =new ExceptionInfo(e.classname, e.getMessage());fields = NO_FIELDS;}if (externalizable) {cons = getExternalizableConstructor(cl);} else {cons = getSerializableConstructor(cl);// 看是否有 writeObject readObject readObjectNoData方法writeObjectMethod = getPrivateMethod(cl, "writeObject",new Class<?>[] { ObjectOutputStream.class },Void.TYPE);readObjectMethod = getPrivateMethod(cl, "readObject",new Class<?>[] { ObjectInputStream.class },Void.TYPE);readObjectNoDataMethod = getPrivateMethod(cl, "readObjectNoData", null, Void.TYPE);hasWriteObjectData = (writeObjectMethod != null);}writeReplaceMethod = getInheritableMethod(cl, "writeReplace", null, Object.class);readResolveMethod = getInheritableMethod(cl, "readResolve", null, Object.class);return null;}});} else {suid = Long.valueOf(0);fields = NO_FIELDS;}try {fieldRefl = getReflector(fields, this);} catch (InvalidClassException ex) {// field mismatches impossible when matching local fields vs. selfthrow new InternalError(ex);}if (deserializeEx == null) {if (isEnum) {deserializeEx = new ExceptionInfo(name, "enum type");} else if (cons == null) {deserializeEx = new ExceptionInfo(name, "no valid constructor");}}for (int i = 0; i < fields.length; i++) {if (fields[i].getField() == null) {defaultSerializeEx = new ExceptionInfo(name, "unmatched serializable field(s) declared");}}initialized = true;
}

在这个类当中,我们看到其实就是拿到User类的class对象之后,获取类中的全部信息,像Class类名、是否实现了serialize接口、serialVersionUID、全部字段信息、是否存在writeObject readObject readObjectNoData方法,总之就是将类的全部信息,封装成了ObjectStreamClass类。

核心代码 2

回到前面的代码中,我们看会做一系列的判断,判断User是什么类型的对象,因为User实现了Serializable接口,所以会走到writeOrdinaryObject方法中,同时将ObjectStreamClass也传了进来。

private void writeOrdinaryObject(Object obj,ObjectStreamClass desc,boolean unshared)throws IOException
{if (extendedDebugInfo) {debugInfoStack.push((depth == 1 ? "root " : "") + "object (class "" +obj.getClass().getName() + "", " + obj.toString() + ")");}try {desc.checkSerialize();bout.writeByte(TC_OBJECT);writeClassDesc(desc, false);handles.assign(unshared ? null : obj);if (desc.isExternalizable() && !desc.isProxy()) {writeExternalData((Externalizable) obj);} else {writeSerialData(obj, desc);}} finally {if (extendedDebugInfo) {debugInfoStack.pop();}}
}

首先调用了writeClassDesc方法,在这个方法中,做的主要事情就是把之前获取的类信息全部全部转换为二进制数据,从writeNonProxyDesc方法中就可以看到,太深入的我就不介绍了,有精力的伙伴可以深入看一下。

private void writeClassDesc(ObjectStreamClass desc, boolean unshared)throws IOException{int handle;if (desc == null) {writeNull();} else if (!unshared && (handle = handles.lookup(desc)) != -1) {writeHandle(handle);} else if (desc.isProxy()) {writeProxyDesc(desc, unshared);} else {writeNonProxyDesc(desc, unshared);}}

然后紧接着调用writeSerialData方法,在这个方法中首先会判断这个类中是否存在writeObject、readObject这种方法,这两个方法的时候,我会在Parcelable专题中介绍,如果有的话,那么就会调用invokeWriteObject方法执行这个方法;如果没有,那么就会调用defaultWriteFields方法,将所有的字段数据转换为二进制数据。

private void writeSerialData(Object obj, ObjectStreamClass desc)throws IOException
{ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();for (int i = 0; i < slots.length; i++) {ObjectStreamClass slotDesc = slots[i].desc;if (slotDesc.hasWriteObjectMethod()) {PutFieldImpl oldPut = curPut;curPut = null;SerialCallbackContext oldContext = curContext;if (extendedDebugInfo) {debugInfoStack.push("custom writeObject data (class "" +slotDesc.getName() + "")");}try {curContext = new SerialCallbackContext(obj, slotDesc);bout.setBlockDataMode(true);slotDesc.invokeWriteObject(obj, this);bout.setBlockDataMode(false);bout.writeByte(TC_ENDBLOCKDATA);} finally {curContext.setUsed();curContext = oldContext;if (extendedDebugInfo) {debugInfoStack.pop();}}curPut = oldPut;} else {defaultWriteFields(obj, slotDesc);}}
}
总结

所以在writeObject方法中,主要就是干了两件事:

(1)获取User类的全部类信息,包括方法、字段、SUID等等,将其封装在ObjectStreamClass中;

(2)在拿到全部类信息后,会将全部的类信息以及字段数据转换成二进制数据。

从源码中我们可以看到,在统计类信息的时候,会检查两个方法是否存在,readObject和writeObject,而且只要实现了这两个方法,那么就不会走defaultWriteFields方法,所以如果我们添加了这两个方法,就能够修改系统默认序列化的实现方式,从而在readObject和writeObject方法中,对字段数据进行调整

1.2.2 数据怎么组装

其实这个在之前1.1小节中的一个问题中提到过了,就是通过反射的方式调用无参构造函数创建一个新的对象。因为在序列化的时候,是将类的全部信息封装在了ObjectStreamClass中,所以反序列化的时候也会获取这些类信息,从而通过反射对所有的字段赋值

2 Parcelable的出现是为了解决什么问题

Parcelable是Google为了Android单独做的一套序列化协议,因为我们知道,Android为了响应速度,舍弃了JVM,选择了基于寄存器的Dalvik和ART,所以Parcelable的出现目的也是一致的—速度。

通过前面我们对于Serializable的理解,首先Serializable的序列化和反序列化是基于IO的,需要做本地化的磁盘存储;还有一个问题就是,在反序列化的过程中,需要通过反射的形式创建一些新的对象,这些对象也是被存放在堆内存中,会产生内存碎片,其本质还是一个深拷贝,如果发生频繁的反射调用对于性能上是有损耗的

因此Parcelable的出现就是为了解决这些问题,首当其冲就是速度问题,因为少了磁盘IO的读写,据说速度是Serializable的10倍;因为Parcelable是基于Binder的,直接在内存中操作数据,减少了磁盘IO操作,但是因为Binder内存的限制,因此不能超过1M,但是Serializable是没有限制的。

除此之外,Parcelable出现的另一个目的就是为了进程间通信提供数据模型,像在使用AIDL的时候,如果想要进行数据传递,那么就会使用到Parcelable。

Android 学习笔录

Android 性能优化篇:https://qr18.cn/FVlo89
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 车载篇:https://qr18.cn/F05ZCM
Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
Android 音视频篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集:https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/147004.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

产品经理必备技能:如何快速锁定种子用户群体?

大家好&#xff0c;我是小米&#xff0c;一名热爱技术、热衷分享的90后小青年。今天我们要探讨的话题是一个在产品经理面试中经常被问到的问题&#xff1a;“产品上线后的种子用户该如何获取&#xff1f;”作为一个热爱挑战、乐于探讨的小伙伴&#xff0c;我将和大家分享一些我…

一、MySQL.pratice.search

MySQL是一种常用的关系型数据库管理系统&#xff0c;广泛应用于各种Web应用程序中。在编程中&#xff0c;使用MySQL进行数据操作是非常常见的操作。在MySQL中&#xff0c;查询是最常用的操作之一&#xff0c;可以查询整个表或者根据特定的条件查询数据。 文章目录 一、查询&am…

第七部分:Maven(项目管理工具)

目录 Maven简介 7.1&#xff1a;为什么学习Maven&#xff1f; 7.1.1、Maven是一个依赖管理工具 7.1.2&#xff1a;Maven是一个构建工具 7.1.3&#xff1a;结论 7.2&#xff1a;Maven介绍 7.3&#xff1a;Maven的优点 Maven安装和配置 7.4&#xff1a;安装教程及环境配置 …

Linux给根目录扩容

需求&#xff1a;Linux系统挂载到根目录的磁盘空间满了&#xff0c;如何扩容&#xff1f; 一、添加磁盘并分区 [rootcdn ~]# fdisk /dev/sdbWelcome to fdisk (util-linux 2.37.2). Changes will remain in memory only, until you decide to write them. Be careful before u…

什么是Java伪随机数,基础打牢。 #程序员 #Java #编程

你一定听说过这样一个词&#xff0c;伪随机数&#xff0c;你有没有这样的疑惑&#xff0c;为什么不用真随机&#xff0c;要用的个假的&#xff1f; 先说一个结论&#xff1a; Java Random英/ˈrndəm/ 随机数生成不安全&#xff0c;如果同时泄漏第一个和第二个随机数&#xf…

uniapp自定义组件

在UniApp中&#xff0c;你可以使用自定义组件来拓展应用程序的功能和界面。自定义组件是由多个Vue组件构成的&#xff0c;可以在应用程序中重复使用。 要创建一个自定义组件&#xff0c;你需要在UniApp项目中的components目录下创建一个新的文件夹&#xff0c;并在该文件夹中创…

最大似然估计的介绍

最大似然估计&#xff08;Maximum Likelihood Estimation&#xff0c;简称MLE&#xff09;是一种用于估计概率分布中参数的方法。该方法的核心思想是选择使得观察到的数据在给定模型下出现的概率最大的参数值作为估计值。 最大似然估计具有很好的性质&#xff0c;包括渐进正态性…

SystemVerilog学习 (9)——随机化

目录 一、概述 二、随机化 2.1、如何简单地产生一个随机数 2.1.1 利用系统函数产生随机数 2.1.2 urandom() 2.2、什么需要随机化 2.3、随机约束 2.3.1 rand 和 randc 2.3.2 随机约束的使用 2.3.3 约束块 三、总结 一、概述 随着设计变得越来越大,要产生一个完整的激…

面试资料快速复习 Git常用命令(简单实用)

Git-command Git常用命令、面试复习、简单实用命令 ​ 一、概念理解 &#xff08;一&#xff09;工作区、暂存区、本地仓库、远程仓库 workspace&#xff1a;工作区staging area&#xff1a;暂存区/缓存区local repository&#xff1a;本地仓库remote repository&#xff…

Apache Airflow (九) :Airflow Operators及案例之BashOperator及调度Shell命令及脚本

&#x1f3e1; 个人主页&#xff1a;IT贫道_大数据OLAP体系技术栈,Apache Doris,Clickhouse 技术-CSDN博客 &#x1f6a9; 私聊博主&#xff1a;加入大数据技术讨论群聊&#xff0c;获取更多大数据资料。 &#x1f514; 博主个人B栈地址&#xff1a;豹哥教你大数据的个人空间-豹…

03_SHELL编程之嵌套循环+随机数及综合案例

###课程目标 掌握for循环语句的基本语法结构 掌握while和until循环语句的基本语法结构 能会使用RANDOM产生随机数 理解嵌套循环 一、随机数 bash默认有一个$RANDOM的变量 默认是0~32767。使用set |grep RANDOM 查看上一次产生的随机数 echo $RANDOM ​ 产生0~1之间…

C#单例模式懒汉式与饿汉式

单例模式一般分为懒汉模式和饿汉模式&#xff0c;懒汉式单例在第一次引用时创建实例&#xff0c;不是在类加载时&#xff1b;饿汉式单例模式是一种在类加载时就创建实例的方式&#xff0c;因此也称为静态初始化。 单例模式实现的技巧时构造私有&#xff0c;向外提供静态实例。…

12-2- DCGAN -简单网络-卷积网络

功能 随机噪声→生成器→MINIST图像。 训练方法 0 损失函数:gan的优化目标是一个对抗损失,是二分类问题,用BCELoss 1 判别器的训练,首先固定生成器参数不变,其次判别器应当将真实图像判别为1,生成图像判别为0 loss=loss(real_out, 1)+loss(fake_out, 0) 2 生成器的…

react-router-dom 版本6.18.0中NavLink的api和属性介绍

React Router 是一个基于 React 的路由库&#xff0c;它可以帮助我们在 React 应用中实现页面的切换和路由的管理。而 NavLink 则是 React Router 中的一个组件&#xff0c;它可以帮助我们实现导航栏的样式设置和路由跳转。 在 React Router 版本6.18.0 中&#xff0c;NavLink…

【工具使用-VScode】设置 VSCode 的自动保存功能

要设置 VSCode 的自动保存功能&#xff0c;请按照以下步骤进行操作&#xff1a; 打开 VSCode 编辑器。在顶部菜单中选择 “文件&#xff08;File&#xff09;”。选择 “首选项&#xff08;Preferences&#xff09;”。在下拉菜单中选择 “设置&#xff08;Settings&#xff0…

[Android] libcutils - native 获取/设置 property

前言&#xff1a; Android 的property系统类似于linux的环境变量&#xff0c;但是更加精细。可以通过adb 设置和读取 property&#xff0c;同时也可以在代码 (JAVA/C/C) 中设置和获取属性。这有助于我们在运行时控制代码执行逻辑。比如打开 测试开关 或者 dump源数据文件。 工…

Matalab插值详解和源码

转载&#xff1a;Matalab插值详解和源码 - 知乎 (zhihu.com) 插值法 插值法又称“内插法”&#xff0c;是利用函数f (x)在某区间中已知的若干点的函数值&#xff0c;作出适当的特定函数&#xff0c;在区间的其他点上用这特定函数的值作为函数f (x)的近似值&#xff0c;这种方…

windows快捷方式图标变成空白

今天突然有客户说应用程序快捷方式图标变成了空白&#xff0c;就研究了一下&#xff0c;网上找了一下很多都说是什么图标缓存有问题&#xff0c;试过之后发现并不能解决问题。 然后发现用户的文件上都一把黄色的小锁的标志&#xff0c;查了一下说是文件属性里面设置加密之后就会…

高防CDN:构筑网络安全的钢铁长城

在当今数字化的世界里&#xff0c;网络安全问题日益突显&#xff0c;而高防CDN&#xff08;高防御内容分发网络&#xff09;正如一座坚不可摧的钢铁长城&#xff0c;成为互联网安全的不可或缺之物。本文将深入剖析高防CDN在网络安全环境中的关键作用&#xff0c;探讨其如何构筑…

Microsoft SQL Server Management Studio(2022版本)启动无法连接到服务器

Microsoft SQL Server Management Studio&#xff08;2022版本&#xff09;启动无法连接到服务器 解决方法&#xff1a; 打开SQL Server 2022 配置管理器。 启动即可。