当前位置: 首页 > news >正文

[Android开源]EasySharedPreferences:优雅的进行SharedPreferences数据存储操作

什么是EasySharedPreferences

EasySharedPreferences是开源基础组件集成库EasyAndroid中的基础组件之一。

其作用是:使用具体的实体类去进行SharedPreferences数据存取。避免key值硬编码

EasyAndroid作为一款集成组件库,此库中所集成的组件,均包含以下特点,你可以放心使用~~

1. 设计独立

组件间独立存在,不相互依赖,且若只需要集成库中的部分组件。也可以很方便的只copy对应的组件文件进行使用

2. 设计轻巧

因为是组件集成库,所以要求每个组件的设计尽量精练、轻巧。避免因为一个小功能而引入大量无用代码.

每个组件的方法数均不超过100. 大部分组件甚至不超过50

得益于编码时的高内聚性,若你只需要使用EasySharedPreferences. 那么可以直接去copy EasySharedPreferences源码文件到你的项目中,直接进行使用,也是没问题的。

EasyAndroid开源库地址:https://github.com/yjfnypeu/EasyAndroid

特性

  1. 通过具体的实体类进行SharedPreferences数据存取操作。避免key值硬编码
  2. 自动同步,即使别的地方是直接使用SharedPreferences进行赋值,也能自动同步相关数据。
  3. 打破SharedPreferences限制。支持几乎任意类型数据存取

用法与原理

用法概览

这里先来通过一个例子来先进行一下大致的了解:

比如现在有这么个配置文件:文件名为user_info,内部存储了一些用户特有的信息:

使用原生的方式。读取时,我们需要这样写:

val preference = context.getSharedPreferences("user_info", Context.MODE_PRIVATE)
val username = preference.getString("username")
val address = preference.getString("address")
val age = preference.getInt("age")
复制代码

而在需要进行数据修改时:我们需要这样写:

val editor = context.getSharedPreferences("user_info", Context.MODE_PRIVATE).edit()
editor.putString("username", newName)
editor.putString("address", newAddress)
editor.putInt("age", newAge)
复制代码

可以看到。原生的写法中含有很多的硬编码的key值, 这在进行大量使用时,其实是很容易出问题的。

而如果使用组件EasySharedPreferences来进行SharedPreferences的数据存取。则方便多了:

  1. 创建映射实体类
@PreferenceRename("user_info")
class User:PreferenceSupport() {
    var username:String
    var age:Int
    var address:String
}
复制代码
  1. 进行读取
// 直接加载即可
val user = EasySharedPreferences.load(User::class.java)
复制代码
  1. 进行修改
// 直接使用load出来的user实例进行数值修改
user.age = 16
user.username = "haoge"

// 修改完毕后,apply更新修改到SharedPreferences文件。
user.apply()
复制代码

可以看到。不管是进行读取数据。还是修改数据EasySharedPreferences的操作方式都是比原生的方式方便很多的。

下面开始对EasySharedPreferences组件的用法做更详细的说明:

映射实体类的定义

映射实体类即是上方示例中的User类:通过将SP中需要的关键数据映射到具体的实体类中,可以有效的避免key值硬编码的问题。

映射实体类的定义,需要遵循以下一些规则:

  1. 实体类必须继承PreferenceSupport, 且提供无参构造
class Entity:PreferenceSupport()
复制代码
  1. 默认采用实体类的类名作为SP的缓存文件名,当需要指定特殊的缓存文件名时。需要使用PreferenceRename注解进行指定
@PreferenceRename("rename_shared_name")
class Entity:PreferenceSupport()
复制代码
  1. 通过直接在实体类中添加不同的成员变量,进行SP的属性配置:
var name:String // 代表此SP文件中。新增key值为name, 类型为String的属性
复制代码
  1. 也可以指定属性的key值:同样使用PreferenceRename注解进行指定
@PreferenceRename("rename_key")
var name:String
复制代码
  1. 有时候。我们会需要定义一下中间存储变量(此部分数据不需要同步存储到SP中的)。可以使用PreferenceIgnore注解
@PreferenceIgnore
val ignore:Address
复制代码

支持存储任意数据

都知道,原生的SP只支持几种特定的数据进行存储:Int, Float, Boolean, Long, String, Set<String>.

EasySharedPreferences组件,通过提供中间类型的方式。打破了此数据限制:

  1. 存储时:将不支持的数据类型,转换为String格式。再进行存储:

核心源码

// type为接收者类型
// value为从SP中读取出的数据
when {
	type == Int::class.java -> editor.putInt(name, value as? Int?:0)
	type == Long::class.java -> editor.putLong(name, value as? Long?:0L)
	type == Boolean::class.java -> editor.putBoolean(name, value as? Boolean?:false)
	type == Float::class.java -> editor.putFloat(name, value as? Float?:0f)
	type == String::class.java -> editor.putString(name, value as? String?:"")
	// 不支持的类型。统统转换为String进行存储
	type == Byte::class.java
	    || type == Char::class.java
	    || type == Double::class.java
	    || type == Short::class.java
	    || type == StringBuilder::class.java
	    || type == StringBuffer::class.java
	    -> editor.putString(name, value.toString())
	GSON -> value?.let { editor.putString(name, Gson().toJson(it)) }
	FASTJSON -> value?.let { editor.putString(name, JSON.toJSONString(value)) }
}
复制代码
  1. 读取时:接收者类型与取出数据格式不匹配(此种场景取出的数据格式均为String)。进行自动转换后再赋值:

核心源码

// type为接收者类型
// value为从SP中读取出的数据
val result:Any? = when {
    type == Int::class.java -> value as Int
    type == Long::class.java -> value as Long
    type == Boolean::class.java -> value as Boolean
    type == Float::class.java -> value as Float
    type == String::class.java -> value as String
    // 不支持的类型。读取出的都是String,直接进行转换兼容
    type == Byte::class.java -> (value as String).toByte()
    type == Short::class.java -> (value as String).toShort()
    type == Char::class.java -> (value as String).toCharArray()[0]
    type == Double::class.java -> (value as String).toDouble()
    type == StringBuilder::class.java -> StringBuilder(value as String)
    type == StringBuffer::class.java -> StringBuffer(value as String)
    GSON -> Gson().fromJson(value as String, type)
    FASTJSON -> JSON.parseObject(value as String, type)
    else -> null
}
复制代码

有细心的可以看到。这里有对GSON与FASTJSON进行兼容。

EasySharedPreference组件。会在运行时判断当前运行环境是否存在具体的JSON解析库。然后选择存在的解析库进行中间类型数据的生成器与解析器:而组件本身是没有直接强制依赖此两种解析库的:

private val FASTJSON by lazy { return@lazy exist("com.alibaba.fastjson.JSON") }
private val GSON by lazy { return@lazy exist("com.google.gson.Gson") }
复制代码

所以。如果你需要存储一个原生不支持的类型。直接添加即可,比如需要存储一个address_detail:

@PerferenceRename("address_detail")
var detail:Address
复制代码

缓存加速

在上面的例子中。我们是直接通过load方法进行的数据加载读取:

val user = EasySharedPreferences.load(User::class.java)
复制代码

这样一行代码,起到的效果即是:

  1. 加载User类所对应的SharedPreferences文件数据
  2. 创建User实例,并将SP文件中的数据。注入到User类中的对应变量中去。

所以相对来说。load方法其实是会有一定的耗时。毕竟注入操作都离不开反射,当然,如果你不在同一个SP文件中去存储大量的数据内容的话,其实对于现在的机型来说。影响还是可以忽略不计的。

但是毕竟如果每次去读取都去读取注入的话。总归是一种性能影响,也不便于体验。

所以组件提供了对应的缓存控制处理:只在首次加载时进行读取与注入:

fun <T> load(clazz: Class<T>):T {
	container[clazz]?.let { return it.entity as T}
	
	val instance = EasySharedPreferences(clazz)
	container[clazz] = instance
	return instance.entity as T
}
复制代码

所以。通过同一个clazz加载读取出来的实例,都是同一个实例!

自动同步

因为缓存加速的原因,我们通过load方法加载出来的实例都是一样的,所以应该会有人担心:当在使用EasySharedPreferences组件的同时。如果在别的业务线上,有人对此SP文件直接使用原生的方式进行了修改,会不会导致数据出现不同步?即数据污染现象?

讲道理。这是不会的!因为EasySharedPreferences组件,专门针对此种场景进行了兼容:

原理说明

原生的SharedPreferences提供了OnSharedPreferenceChangeListener监听器。此监听器的作用为:对当前的SharedPreferences容器中的数据做监听。当容器中有数据改变了。则通过此接口对外通知。便于进行刷新

public interface OnSharedPreferenceChangeListener {
    void onSharedPreferenceChanged(
    			SharedPreferences sharedPreferences, // 被监听的容器实例
    			String key);// 被修改的数据的key。
}
复制代码

然后,需要指出的是:其实系统本身也有对SharedPreferences容器实例做缓存。所以:通过同样的文件名获取到的SharedPreferences实例,其实都是同一个对象实例

所以,同步的流程即是:只要对组件中自身绑定的SharedPreferences容器,注册此监听器,即可在外部进行修改时。同步获取到被修改的key值。再相对的进行指定key的数据同步即可:

所以,最终的自动同步逻辑核心逻辑代码即是:

class EasySharedPreferences(val clazz: Class<*>):SharedPreferences.OnSharedPreferenceChangeListener {

	// 绑定的SharedPreference实例
	private val preferences:SharedPreferences
	init {
		// 创建时,注册内容变动监听器
		preferences.registerOnSharedPreferenceChangeListener(this)
		...
	}
	
	override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
		// 回调中进行数据同步处理
	}
	
	fun write() {
		synchronized(this) {
			// 自身的修改需要更新到文件中去时,暂时注销掉监听器。不对自身的数据处理做监听
			preferences.unregisterOnSharedPreferenceChangeListener(this)
			... 
			preferences.registerOnSharedPreferenceChangeListener(this)
		}
	}
}
复制代码

PreferenceIgnore的使用场景

在映射实体类的定义这一节的最后。我们有提到使用PreferenceIgnore注解配置中间存储变量。当时只是简单提了一句,所以可能会有部分朋友对此注解的使用场景存在疑惑

这里我将通过举一个具体的例子进行使用场景说明:

比如说需要存储登录用户的信息,比如登录时的密码(当然只是举例,对于密码类型的数据。推荐的存储容器还是使用sql)。我们想把它存储到SharedPreferences中去:

@PreferenceRename("login_info")
class Login:PreferenceSupport() {
    var password:String
}
复制代码

但是我们又不能直接对密码进行明文存储。所以我们需要在每次进行使用的时候,主动的去再进行加密解密

// 读取时进行解密:
var password = EncryptTool.decode(user.password)

// 存储时进行加密:
user.password = EncryptTool.encode(password)
复制代码

但是这样的用法相当不优雅。所以我们推荐使用PreferenceIgnore创建一个中间存储数据出来:

@PreferenceRename("login_info")
class Login:PreferenceSupport() {
    // 将实际存储的密码使用private修饰,避免外部直接修改
    private var password:String 
    @PreferenceIgnore
    var passwordWithEncrypt:String
        get() { return EncryptTool.decode(password) }
        set(value:String) { this.password = EncryptTool.encode(value)}
}
复制代码

通过配置一个中间的存储变量,自动去进行存取时的加解密操作。对上层隐藏具体的加解密逻辑。这样上层使用起来就相当优雅了:

// 读取
var password = user.passwordWithEncrypty

// 存储
user.passwordWithEncrypty = password
复制代码

混淆配置

最后,为了避免混淆后导致使用异常,请添加以下混淆配置:

-keep class * implements com.haoge.easyandroid.easy.PreferenceSupport
复制代码

相关文章:

  • github上fork别人的代码之后,如何保持和原作者同步的更新
  • 026 UI调试
  • Spring框架心得笔记
  • 筛法求素数模板
  • 通过创建脚本代替scrapy crawl Test命令
  • Linux系统运维常见面试简答题系列(二)(14题)
  • jQuery实现发送短信验证码后60秒倒计时
  • 与专门团队一起持续交付
  • java.io.File
  • Oracle宣布提供新的Java支持价格体系
  • 第二棵树:Splay
  • MySQL之表lock信息
  • set serveroutput on
  • java基础-网络编程(Socket)技术选型入门之NIO技术
  • Java 类的加载机制
  • 2017 年终总结 —— 在路上
  • Bytom交易说明(账户管理模式)
  • gops —— Go 程序诊断分析工具
  • iOS编译提示和导航提示
  • JS函数式编程 数组部分风格 ES6版
  • PermissionScope Swift4 兼容问题
  • Quartz实现数据同步 | 从0开始构建SpringCloud微服务(3)
  • SpiderData 2019年2月23日 DApp数据排行榜
  • TypeScript实现数据结构(一)栈,队列,链表
  • 从0搭建SpringBoot的HelloWorld -- Java版本
  • 大快搜索数据爬虫技术实例安装教学篇
  • 分布式熔断降级平台aegis
  • 近期前端发展计划
  • 排序算法之--选择排序
  • 线性表及其算法(java实现)
  • 再谈express与koa的对比
  • 分布式关系型数据库服务 DRDS 支持显示的 Prepare 及逻辑库锁功能等多项能力 ...
  • ​ 无限可能性的探索:Amazon Lightsail轻量应用服务器引领数字化时代创新发展
  • ​3ds Max插件CG MAGIC图形板块为您提升线条效率!
  • #数学建模# 线性规划问题的Matlab求解
  • (04)Hive的相关概念——order by 、sort by、distribute by 、cluster by
  • (2)(2.10) LTM telemetry
  • (4)logging(日志模块)
  • (c语言版)滑动窗口 给定一个字符串,只包含字母和数字,按要求找出字符串中的最长(连续)子串的长度
  • (ibm)Java 语言的 XPath API
  • (JS基础)String 类型
  • (附源码)ssm教师工作量核算统计系统 毕业设计 162307
  • (紀錄)[ASP.NET MVC][jQuery]-2 純手工打造屬於自己的 jQuery GridView (含完整程式碼下載)...
  • (亲测成功)在centos7.5上安装kvm,通过VNC远程连接并创建多台ubuntu虚拟机(ubuntu server版本)...
  • (三)模仿学习-Action数据的模仿
  • (译)2019年前端性能优化清单 — 下篇
  • (轉貼) 蒼井そら挑戰筋肉擂台 (Misc)
  • .NET 发展历程
  • .sh文件怎么运行_创建优化的Go镜像文件以及踩过的坑
  • ::什么意思
  • @Repository 注解
  • @Transactional 竟也能解决分布式事务?
  • [ web基础篇 ] Burp Suite 爆破 Basic 认证密码
  • [8481302]博弈论 斯坦福game theory stanford week 1
  • [Android 13]Input系列--获取触摸窗口