[Android開源]EasySharedPreferences:優雅的進行SharedPreferences數據存儲操做

什麼是EasySharedPreferences

EasySharedPreferences是開源基礎組件集成庫EasyAndroid中的基礎組件之一。java

其做用是:使用具體的實體類去進行SharedPreferences數據存取。避免key值硬編碼android

EasyAndroid做爲一款集成組件庫,此庫中所集成的組件,均包含如下特色,你能夠放心使用~~git

1. 設計獨立github

組件間獨立存在,不相互依賴,且若只須要集成庫中的部分組件。也能夠很方便的只copy對應的組件文件進行使用sql

2. 設計輕巧json

由於是組件集成庫,因此要求每一個組件的設計儘可能精練、輕巧。避免由於一個小功能而引入大量無用代碼.緩存

每一個組件的方法數均不超過100. 大部分組件甚至不超過50bash

得益於編碼時的高內聚性,若你只須要使用EasySharedPreferences. 那麼能夠直接去copy EasySharedPreferences源碼文件到你的項目中,直接進行使用,也是沒問題的。markdown

EasyAndroid開源庫地址:https://github.com/yjfnypeu/EasyAndroidapp

特性

  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
複製代碼
相關文章
相關標籤/搜索