SharePreferences的封装

想法的由来

最近在重构项目中的代码,采用仓储模式屏蔽了外界对数据层的直接操作,仓储类中包含了调用访问服务器、本地数据源的代码,部分数据采用本地 SharePreferences 存储,于是出现了下面的代码:

object StatusRepository {

    private val SP_NAME = "sp_status"
    private val KEY_CHOICENESS_CATEGORIES = "key_choiceness_categories"


    private val apiService by lazy {
        BooheeApiServiceProvider.getApiService(StatusApiService::class.java)
    }

    private val sharePreference by lazy {
        MyApplication.getContext().getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
    }

    fun listLocalChoicenessCategories():List<PostCategory> {
        val strList = sharePreference.getString(KEY_CHOICENESS_CATEGORIES,"")
        return FastJsonUtils.parseList(strList, PostCategory::class.java)
    }

    fun updateLocalChoicenessCategories(list: List<PostCategory>) {
       sharePreference.edit().putString(KEY_CHOICENESS_CATEGORIES, FastJsonUtils.toJson(list)).apply()
    }  

    fun listByChoicenessCategory(slug: String, page: Int): Single<StatusApiResult> =
            apiService.listByChoicenessCategory(slug, page)
                    .compose(IOtoUITransformer())                   
    ...
}

当使用 SharePeferences 的方法变多时,这个类就充满了 Key 值的定义,序列/反序列化,edit()apply() 的代码,且没有屏蔽具体访问数据源的方式, 直接操作了 SharePeferences 访问的代码,而类中对服务器数据源访问的代码,不仅屏蔽了具体细节,代码又简洁,所以就萌发了开发一个类似于 Retrofit 风格调用的 SharePeferences 封装库的想法.
(后来完成一个简易版后在 github 上发现了类似的库 Favor,出于对部分知识的实践还是继续完成后续的开发,相比于此库添加了一些功能,并且更简洁,完成了 BriefPeference 初版)

初步的需求

  • Retrofit Style
  • Support multiple SharePreferences
  • Support reactive
  • Support default Object value
  • Support custom Serializable

开发实现

涉及到的技术点:

  • 动态代理
  • Kotlin 注解及反射
  • RxJava

首先我们简化对 SharePeferences 操作的代码, 之前我们读写一个值的逻辑为:根据值的类型调用SharePreference 暴露的对应类型操作(put、get、remove、clear)方法, 基于代码复用的原则 这个过程其实可以抽取为一个泛型的方法,根据具体值的类型分支调用不同的操作方法,但是操作方法有好几种 如put、get、remove、clear,我们可以进一步复用这块逻辑,根据具体方法的操作类型去执行对应的操作逻辑,于是此过程初步可以统一成一个函数,输入为 值、操作类型、 输出为返回值,函数体的逻辑是完成实际上对 SharePreferences 操作。
而现在的目的是完成一个类似于 Retrofit 调用风格方式的 SharePreferences 封装库,Retrofit 是基于动态代理实现的一个网络库,通过动态代理我们可以拦截对方法的调用,在其中实现自己的逻辑,比如我们需要在一个类中所有方法执行前记录一个日志,如果我们在每一个方法里面去添加这个逻辑,当方法数较多时工作量大且代码冗杂,这个时候我们就可以使用动态代理去拦截每一个方法在拦截的逻辑里面加上日志记录代码,

在这里我们利用动态代理拦截对方法的调用,在其中实现前面我们抽象的那个函数中对 SharePeference 操作的逻辑过程:

class BriefPreference(private var converterFactory: Converter.Factory = DefaultConverterFactory()) {

    private lateinit var preferences: SharedPreferences
    private lateinit var preferenceChangeObservable: Observable<String>
    private val methodInfoCache = LinkedHashMap<String, MethodInfo>()

    fun <T> create(context: Context, service: Class<T>): T {
        createSharePreference(context, service)
        createPreferenceChangeObservable()
        return createServiceProxy(service)
    }
    //创建代理类
    private fun <T> createServiceProxy(service: Class<T>): T = Proxy.newProxyInstance(service.classLoader, arrayOf<Class<*>>(service), object : InvocationHandler {
            @Throws(Throwable::class)
            override fun invoke(proxy: Any, method: Method, args: Array<Any>?): Any? {
                return if (method.declaringClass == Any::class.java) {
                    method.invoke(this, args)
                } else adapterMethod(getMethodInfo(method, args), args)
            }
        }) as T
    // 调用接口方法时拦截执行的真正逻辑(获取接口上方法的信息完成具体对SharePeferences的操作)
    private fun adapterMethod(methodInfo: MethodInfo, args: Array<Any>?): Any?{
            return when (methodInfo.actionType) {
                GET -> getValue(methodInfo.key, methodInfo.returnType, args?.get(0))
                PUT -> putValue(methodInfo.key, args?.get(0))
                REMOVE -> preferences.edit().remove(methodInfo.key).apply()
                CLEAR -> preferences.edit().clear().apply()
                else -> null
            }
        }
    ...

} 
interface UserService {

        @Key("testName")
        fun putName(value: String)

        @Key("testName")
        fun getName(@Default value: String = "ethanhua"): String

}
private val localService: UserService by lazy {
            BriefPreference().create(AppContext.instance, UserService::class.java)
        }

        fun putUserName(name: String) = localService.putName(name) 

        fun getUserName() = localService.getName()
 }

输入参数:操作类型

比如 removeclear 我们通过定义的接口方法的注解来定义,然后在 InvocationHandler invoke 方法中第二个参数 method: Method 中来得到

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class Remove

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class Clear

interface UserService {

    @Remove
    fun removeUser()

    @Clear
    fun clear()
}

输入参数: 操作值

我们可以通过 Proxy 中的 InvocationHandler invoke 方法中第三个参数 args: Array<Any>? 来得到

其它

键值、返回默认值、SharePreferences文件名通过注解 @Key、@Default、@SpName 来定义

@SpName("user_preferences")
interface UserService {
    @Key("testName") 
    fun putName(value: String)

    @Key("testName") 
    fun getName(@Default defaultName: String = "ethanhua"): String
}

序列/反序列化

序列化采用同 Retrofit 中处理的方式,定义接口,用户可以实现接口处理自己的序列/反序列化逻辑,然后作为 BriefPreference 的构造参数传入进去,默认实现了 Serializable 、 Parcelable 的处理

interface Converter<F, T> {

    @Throws(IOException::class)
    fun convert(value: F?): T?

    interface Factory {

        fun <F> fromType(fromType: Type): Converter<F, String>

        fun <T> toType(toType: Type): Converter<String, T>
    }
}
class GsonConverterFactory : Converter.Factory {

    private val gson = Gson()

    override fun <F> fromType(fromType: Type): Converter<F, String> {
        return object : Converter<F, String> {
            override fun convert(value: F?): String? {
                return gson.toJson(value)
            }
        }
    }

    override fun <T> toType(toType: Type): Converter<String, T> {
        return object : Converter<String, T> {
            override fun convert(value: String?): T? {
                return gson.fromJson(value, toType)
            }
        }
    }

}
object UserRepository {

    private val localService: UserService by lazy {
        BriefPreference(GsonConverterFactory()).create(AppContext.instance, UserService::class.java)
    }
    ...
}

reactive 响应式支持

SharePreferences 作为本地数据源的载体,为了在值发生改变时,那些依赖的地方能够及时更新,所以提供了支持响应式编程的功能,这里我们没有实现自己的数据源(因为可能项目中有其它地方通过直接操作 SharePrefences 来改变了数据,这时我们就监听不到数据的改变),而是采用 SharePrefences 提供给我们的 registerOnSharedPreferenceChangeListener 来作为数据源,然后依托于 Rxjava 中的 Observable 来分发数据

preferenceChangeObservable = Observable.create<String>({ emitter ->
            val listener = SharedPreferences.OnSharedPreferenceChangeListener { preferences, key ->
                if (preferences == this.preferences) {
                    emitter.onNext(key)
                }
            }
            emitter.setCancellable {
                preferences.unregisterOnSharedPreferenceChangeListener(listener)
            }
            preferences.registerOnSharedPreferenceChangeListener(listener)
        }).share() // share() 来实现一个数据源对多个监听者

private fun getValue(key: String, type: Type, default: Any?): Any? {
        val returnType = Types.getRawType(type)
        return with(preferences) {
            val res: Any = when (returnType) {
                ...
                // 响应式
                Observable::class.java -> getObservable(key, type, default)
                Flowable::class.java -> getFlowable(key, type, default)
                else -> {
                    val str = getString(key, "")
                    if (str.isNullOrEmpty() && default != null) {
                        return default
                    }
                    val converter: Converter<String, Any>? = converterFactory.toType(getConverterType(type))
                    return converter?.convert(str)
                }
            }
            res
        }
    }

private fun getObservable(key: String, type: Type, default: Any?): Observable<Any> {
        if (default == null) {
            throw IllegalArgumentException("default value can not be null")
        }
        return preferenceChangeObservable.filter({
            key == it
        }).startWith(key).map {
            getValue(it, Types.getGenericActualType(type, Observable::class.java), default)
        }
    }

`

这样我们就实现了对响应式的支持,Google 最近推出的一个对数据库访问的库 Room 也是使用动态代理实现的,对远程数据、本地数据库、本地 SharePreferences 的访问现在都统一成了定义好接口方法,直接在仓储类中封装调用逻辑了。

最终代码:

BriefPreference

性能影响主要体现在第一次生成代理类上,而采用延迟初始化可以将影响降至最低,类的类型信息在类加载中已经赋值给对应的类的class字段了,所以利用使用反射和注解获取类型,注解信息的的性能(并没有反射调用类的方法和字段)影响几乎可以忽略不计。
而我们利用这种方式统一了数据访问接口,不论是访问服务器数据,还是本地的数据库和 SharePreferences 数据都是统一的调用方式,维护性、易读性都有所提高。

最后

谢谢阅读!
Thanks: Favor