面试的时候突然遇到答不上的问题怎么办?---草稿(1)

本文分析了Android中的SharedPreferences存储问题,包括性能、安全性和使用场景限制,然后介绍了ContentProvider作为替代方案的优势和注意事项,以及Serializable、Parcelable和JSON/ProtocolBuffers在序列化方面的特点和适用场景。
摘要由CSDN通过智能技术生成

二、SharedPreferences

SharedPreferences是 Android 中比较常用的存储方法,它可以用来存储一些比较小的键值对集合。虽然 SharedPreferences 使用非常简便,但也是我们诟病比较多的存储方法。它的性能问题比较多:

  • 跨进程不安全。由于没有使用跨进程的锁,就算使用MODE_MULTI_PROCESS,SharedPreferences 在跨进程频繁读写有可能导致数据全部丢失。根据有心人统计,SP 大约会有万分之一的损坏率。

  • 加载缓慢。SharedPreferences 文件的加载使用了异步线程,而且加载线程并没有设置线程优先级,如果这个时候主线程读取数据就需要等待文件加载线程的结束。这就导致出现主线程等待低优先级线程锁的问题,比如一个 100KB 的 SP 文件读取等待时间大约需要 50~100ms,我建议提前用异步线程预加载启动过程用到的 SP 文件。

  • 全量写入。无论是调用 commit() 还是 apply(),即使我们只改动其中的一个条目,都会把整个内容全部写到文件。而且即使我们多次写入同一个文件,SP 也没有将多次修改合并为一次,这也是性能差的重要原因之一。

  • 卡顿。由于提供了异步落盘的 apply 机制,在崩溃或者其他一些异常情况可能会导致数据丢失。所以当应用收到系统广播,或者被调用 onPause 等一些时机,系统会强制把所有的 SharedPreferences 对象数据落地到磁盘。如果没有落地完成,这时候主线程会被一直阻塞。这样非常容易造成卡顿,甚至是 ANR,从线上数据来看 SP 卡顿占比一般会超过 5%。

坦白来讲,系统提供的 SharedPreferences 的应用场景是用来存储一些非常简单、轻量的数据。我们不要使用它来存储过于复杂的数据,例如 HTML、JSON 等。而且 SharedPreference 的文件存储性能与文件大小相关,每个 SP 文件不能过大,我们不要将毫无关联的配置项保存在同一个文件中,同时考虑将频繁修改的条目单独隔离出来。

我们也可以替换通过复写 Application 的 getSharedPreferences 方法替换系统默认实现,比如优化卡顿、合并多次 apply 操作、支持跨进程操作等。具体:

public class MyApplication extends Application {
@Override
public SharedPreferences getSharedPreferences(String name, int mode)
{
return SharedPreferencesImpl.getSharedPreferences(name, mode);
}
}

对系统提供的 SharedPreferences 的小修小补虽然性能有所提升,但是依然不能彻底解决问题。

三、ContentProvider

为什么 Android 系统不把 SharedPreferences 设计成跨进程安全的呢?那是因为 Android 系统更希望我们在这个场景选择使用 ContentProvider 作为存储方式。ContentProvider 作为 Android 四大组件中的一种,为我们提供了不同进程甚至是不同应用程序之间共享数据的机制。

Android 系统中比如相册、日历、音频、视频、通讯录等模块都提供了 ContentProvider 的访问支持。它的使用也比较简单。
当然,在使用过程也需要注意以下几点。

3.1 启动性能

ContentProvider 的生命周期默认在 Application onCreate() 之前,而且都是在主线程创建的。我们自定义的 ContentProvider 类的构造函数、静态代码块、onCreate 函数都尽量不要做耗时的操作,会拖慢启动速度。

3.2 稳定性

ContentProvider 在进行跨进程数据传递时,利用了 Android 的 Binder 和匿名共享内存机制。就是通过 Binder 传递 CursorWindow 对象内部的匿名共享内存的文件描述符。这样在跨进程传输中,结果数据并不需要跨进程传输,而是在不同进程中通过传输的匿名共享内存文件描述符来操作同一块匿名内存,这样来实现不同进程访问相同数据的目的。

Android 的 Binder 传输是有大小限制的,一般来说限制是 1~2 MB。ContentProvider 的接口调用参数和 call 函数调用并没有使用匿名共享机制,比如要批量插入很多数据,那么就会出现一个插入数据的数组,如果这个数组太大了,那么这个操作就可能会出现数据超大异常。

3.3 安全性

虽然 ContentProvider 为应用程序之间的数据共享提供了很好的安全机制,但是如果 ContentProvider 是 exported,当支持执行 SQL 语句时就需要注意 SQL 注入的问题。另外如果我们传入的参数是一个文件路径,然后返回文件的内容,这个时候也要校验合法性,不然整个应用的私有数据都有可能被别人拿到,在 intent 传递参数的时候可能经常会犯这个错误。

总的来说,ContentProvider 这套方案实现相对比较笨重,适合传输大的数据。

四、序列化

对于大部分的开发者来说,我们不一定有精力去“创造”一种数据序列化的格式。

对象的序列化:应用程序中的对象存储在内存中,如果我们想把对象存储下来或者在网络上传输,这个时候就需要用到对象的序列化和反序列化。

对象序列化就是把一个 Object 对象所有的信息表示成一个字节序列,这包括 Class 信息、继承关系信息、访问权限、变量类型以及数值信息等。

4.1 Serializable

Serializable 是 Java 原生的序列化机制,在 Android 中也有被广泛使用。我们可以通过 Serializable 将对象持久化存储,也可以通过 Bundle 传递 Serializable 的序列化数据。

Serializable 的原理

Serializable 的原理是通过 ObjectInputStream 和 ObjectOutputStream 来实现的,ObjectOutputStream的部分源码实现:

private void writeFieldValues(Object obj, ObjectStreamClass classDesc) {
for (ObjectStreamField fieldDesc : classDesc.fields()) {

Field field = classDesc.checkAndGetReflectionField(fieldDesc);

}

}

整个序列化过程使用了大量的反射和临时变量,而且在序列化对象的时候,不仅会序列化当前对象本身,还需要递归序列化对象引用的其他对象。

整个过程计算非常复杂,而且因为存在大量反射和 GC 的影响,序列化的性能会比较差。另外一方面因为序列化文件需要包含的信息非常多,导致它的大小比 Class 文件本身还要大很多,这样又会导致 I/O 读写上的性能问题。

Serializable 的进阶

既然 Serializable 性能那么差,那它有哪些优势呢?Serializable 序列化支持替代默认流程,它会先反射判断是否存在我们自己实现的序列化方法 writeObject 或反序列化方法 readObject。通过这两个方法,我们可以对某些字段做一些特殊修改,也可以实现序列化的加密功能。

writeReplace 和 readResolve 方法。这两个方法代理序列化的对象,可以实现自定义返回的序列化实例。那它有什么用呢?我们可以通过它们实现对象序列化的版本兼容,例如通过 readResolve 方法可以把老版本的序列化对象转换成新版本的对象类型。

Serializable 的序列化与反序列化的调用流程如下。

// 序列化
E/test:SerializableTestData writeReplace
E/test:SerializableTestData writeObject

// 反序列化
E/test:SerializableTestData readObject
E/test:SerializableTestData readResolve

Serializable 的注意事项

Serializable 虽然使用非常简单,但是也有一些需要注意的事项字段。

不被序列化的字段。类的 static 变量以及被声明为 transient 的字段,默认的序列化机制都会忽略该字段,不会进行序列化存储。当然我们也可以使用进阶的 writeReplace 和 readResolve 方法做自定义的序列化存储。

serialVersionUID。在类实现了 Serializable 接口后,我们需要添加一个 Serial Version ID,它相当于类的版本号。这个 ID 我们可以显式声明也可以让编译器自己计算。通常我建议显式声明会更加稳妥,因为隐式声明假如类发生了一点点变化,进行反序列化都会由于 serialVersionUID 改变而导致 InvalidClassException 异常。

构造方法。Serializable 的反序列默认是不会执行构造函数的,它是根据数据流中对 Object 的描述信息创建对象的。如果一些逻辑依赖构造函数,就可能会出现问题,例如一个静态变量只在构造函数中赋值,当然我们也可以通过进阶方法做自定义的反序列化修改。

4.2 Parcelable

由于 Java 的 Serializable 的性能较低,Android 需要重新设计一套更加轻量且高效的对象序列化和反序列化机制。Parcelable 正是在这个背景下产生的,它核心的作用就是为了解决 Android 中大量跨进程通信的性能问题。

Parcelable 的永久存储

Parcelable 的原理十分简单,它的核心实现都在Parcel.cpp。

你可以发现 Parcel 序列化和 Java 的 Serializable 序列化差别还是比较大的,Parcelable 只会在内存中进行序列化操作,并不会将数据存储到磁盘里。

当然我们也可以通过Parcel.java的 marshall 接口获取 byte 数组,然后存在文件中从而实现 Parcelable 的永久存储。

// Returns the raw bytes of the parcel.
public final byte[] marshall() {
return nativeMarshall(mNativePtr);
}

// Set the bytes in data to be the raw bytes of this Parcel.
public final void unmarshall(byte[] data, int offset, int length) {
nativeUnmarshall(mNativePtr, data, offset, length);
}

Parcelable 的注意事项

在时间开销和使用成本的权衡上,Parcelable 机制选择的是性能优先。

所以它在写入和读取的时候都需要手动添加自定义代码,使用起来相比 Serializable 会复杂很多。但是正因为这样,Parcelable 才不需要采用反射的方式去实现序列化和反序列化。

虽然通过取巧的方法可以实现 Parcelable 的永久存储,但是它也存在两个问题。

  • 系统版本的兼容性。由于 Parcelable 设计本意是在内存中使用的,我们无法保证所有 Android 版本的Parcel.cpp实现都完全一致。如果不同系统版本实现有所差异,或者有厂商修改了实现,可能会存在问题。

  • 数据前后兼容性。Parcelable 并没有版本管理的设计,如果我们类的版本出现升级,写入的顺序及字段类型的兼容都需要格外注意,这也带来了很大的维护成本。

一般来说,如果需要持久化存储的话,一般还是不得不选择性能更差的 Serializable 方案。

4.3 数据的序列化

对象的序列化要记录的信息还是比较多,在操作比较频繁的时候,对应用的影响还是不少的,这个时候我们可以选择使用数据的序列化。

JSON

JSON 是一种轻量级的数据交互格式,它被广泛使用在网络传输中,很多应用与服务端的通信都是使用 JSON 格式进行交互。

JSON 的确有很多得天独厚的优势,主要有:

  • 相比对象序列化方案,速度更快,体积更小。
  • 相比二进制的序列化方案,结果可读,易于排查问题。
  • 使用方便,支持跨平台、跨语言,支持嵌套引用。

因为每个应用基本都会用到 JSON,所以每个大厂也基本都有自己的“轮子”。例如 Android 自带的 JSON 库、Google 的Gson、阿里巴巴的Fastjson、美团的MSON。

各个自研的 JSON 方案主要在下面两个方面进行优化:

便利性。例如支持 JSON 转换成 JavaBean 对象,支持注解,支持更多的数据类型等。

性能。减少反射,减少序列化过程内存与 CPU 的使用,特别是在数据量比较大或者嵌套层级比较深的时候效果会比较明显。

在数据量比较少的时候,系统自带的 JSON 库还稍微有一些优势。但在数据量大了之后,差距逐渐被拉开。总的来说,Gson 的兼容性最好,一般情况下它的性能与 Fastjson 相当。但是在数据量极大的时候,Fastjson 的性能更好。

Protocol Buffers

相比对象序列化方案,JSON 的确速度更快、体积更小。不过为了保证 JSON 的中间结果是可读的,它并没有做二进制的压缩,也因此 JSON 的性能还没有达到极致。

如果应用的数据量非常大,又或者对性能有更高的要求,此时Protocol Buffers是一个非常好的选择。它是 Google 开源的跨语言编码协议,Google 内部的几乎所有 RPC 都在使用这个协议。

总结一下它的优缺点:
  • 性能。使用了二进制编码压缩,相比 JSON 体积更小,编解码速度也更快,感兴趣的同学可以参考protocol-buffers 编码规则。
  • 兼容性。跨语言和前后兼容性都不错,也支持基本类型的自动转换,但是不支持继承与引用类型。
  • 使用成本。Protocol Buffers 的开发成本很高,需要定义.proto 文件,并用工具生成对应的辅助类。辅助类特有一些序列化的辅助方法,所有要序列化的对象,都需要先转化为辅助类的对象,这让序列化代码跟业务代码大量耦合,是侵入性较强的一种方式。

对于 Android 来说,官方的 Protocol Buffers 会导致生成的方法数很多。我们可以修改它的自动代码生成工具,例如在微信中,每个.proto 生成的类文件只会包含一个方法即 op 方法。

/**

  • Protobuf enum {@code Transport}
    /
    public enum Transport
    implements com.google.protobuf.Internal.EnumLite {
    /
    *
  • BLUETOOTH_LOW_ENERGY = 0;
    /
    BLUETOOTH_LOW_ENERGY(0),
    /
    *
  • BLUETOOTH_RFCOMM = 1;
    /
    BLUETOOTH_RFCOMM(1),
    /
    *
  • BLUETOOTH_IAP = 2;
    */
    BLUETOOTH_IAP(2),
    UNRECOGNIZED(-1),
    ;

/**

  • BLUETOOTH_LOW_ENERGY = 0;
    /
    public static final int BLUETOOTH_LOW_ENERGY_VALUE = 0;
    /
    *
  • BLUETOOTH_RFCOMM = 1;
    /
    public static final int BLUETOOTH_RFCOMM_VALUE = 1;
    /
    *
  • BLUETOOTH_IAP = 2;
    */
    public static final int BLUETOOTH_IAP_VALUE = 2;

@Override
public final int getNumber() {
if (this == UNRECOGNIZED) {
throw new IllegalArgumentException(
“Can’t get the number of an unknown enum value.”);
}
return value;
}

结语

由于篇幅限制,文档的详解资料太全面,细节内容太多,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!以下是目录截图:

由于整个文档比较全面,内容比较多,篇幅不允许,下面以截图方式展示 。

再附一部分Android架构面试视频讲解:

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值