[Android開源]EasySharedPreferences:優雅的進行SharedPreferences資料儲存操作

Haoge發表於2018-06-28

什麼是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
複製程式碼

相關文章