空间卷轴(一)——保存当前运行时状态
作者:陈小默(http://www.jianshu.com/u/798fe2ac8685)
声明:未经允许禁止转载
适用场景
在执行任务的过程中,难免需要携带一些物件。自己携带这些物件不仅不方便还可能会丢失。这时候我们就需要一个空间卷轴来盛放这些物件。
对于Android开发来说,内存的稀有令人发指,只要页面运行在非前台,他就有可能被Android系统给回收,于是当该Activity重新回到前台时,实际上是Android系统根据之前保存的状态重新创建了一个Activity对象。所以在这种状态下我们原先的全局变量将会消失,导致我们运行时出现空指针异常或者状态异常。
通常我们处理这种情况有两种方式:
- 1,每次使用全局变量前判空;
- 2,在相应的位置保存状态,然后在重启时恢复。
对于第一种方式,我们提倡无论任何时刻,全局变量的使用之前都需要进行判空操作,这是一种良好的编码习惯,可是对于当前场景,数据都已经消失了,即使判空了还能有什么用呢?
对于第二种方式,我们可以在API提供的方法中进行数据保存,然后在相应的位置取出:
class MyActivity : AppCompatActivity(), OnclickListener {
val token: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if(savedInstanceState!=null){
token = savedInstanceState.getString("token")
}
}
override fun onClick(view: View?){
when(view?.id){
// -> get token
// -> use token
else -> // to do something
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString("token", token)
}
}
API给出的解决方案固然有效,但是这繁琐的使用方式和大数据量时的冗长代码怎么看都不符合我们忍者的行事作风。那么有没有什么方法能够让数据的保存变得简单呢?接下来介绍我们的行走江湖利器——空间卷轴登场。
忍具的使用方式
罗嗦了一大堆,我们来看看忍具的使用方式
class SaveInstanceActivity : BaseActivity() {
var mName by saveString()
var mDate by saveSerializable<Date>()
var mAge by save(0)
}
从上面的部分可以看到,我们仅仅需要将需要被保存的全局变量通过by
委托给相应的save*
函数即可,这里函数支持全部的Bundle
支持的类型,比如:
1,基本数据类型举例:
save(defaultValue)
比如save(true)
表示委托的时Boolean类型变量,其默认值为真,save(10240L)
表示委托的时Long类型变量,其默认值为10240。
2,基本数据类型数组:
saveIntArray
,saveByteArray
等等。
3,字符串以及字符链表和数组
saveString
,saveStringArray
和saveStringArrayList
4,序列化对象
saveSerializable<T: Serializable>
5,Parcelable对象及其数据和链表
saveParcelable<T: Parcelable>
,saveParcelableArray<T: Parcelable>
和saveParcelableArrayList<T: Parcelable>
亲自打造忍具卷轴
这个卷轴的功能看起来挺好用,那么我们应该如何获得这么一个卷轴呢?
所需材料
1,所需环境:Kotlin语言
2,所需忍术:属性的委托机制(还不会的可以去看 心转身之术——属性的委托机制、Kotlin的扩展机制、Lambda语法
凝聚查克拉
此忍具的目标粗略可分为两种类型,基本数据类型和引用数据类型。在代码中的表现就是一个不允许为空的另一种是允许为空的。
所以,我们就需要创建两种用于委托的属性类。第一种,非空属性:
private class `BaseActivity$$SavePrimaryProperty`<in T, V>(private val getter: (T, name: String) -> V,
private val setter: (T, V, name: String) -> Unit)
: ReadWriteProperty<T, V> {
override fun getValue(thisRef: T, property: KProperty<*>): V {
return getter.invoke(thisRef, property.name)
}
override fun setValue(thisRef: T, property: KProperty<*>, value: V) {
return setter.invoke(thisRef, value, property.name)
}
}
对于非空属性,我们直接使用ReadWriteProperty
作为基类,但是对于可空的属性,我们就没有现成的基类可用了:
interface NullableReadWriteProperty<in R, T> {
operator fun getValue(thisRef: R, property: KProperty<*>): T?
operator fun setValue(thisRef: R, property: KProperty<*>, value: T?)
}
private class `BaseActivity$$SaveProperty`<in T, V>(private val getter: (T, name: String) -> V?,
private val setter: (T, V?, name: String) -> Unit)
: NullableReadWriteProperty<T, V> {
private var field: V? = null
override fun getValue(thisRef: T, property: KProperty<*>): V? {
if (field == null)
field = getter.invoke(thisRef, property.name)
return field
}
override fun setValue(thisRef: T, property: KProperty<*>, value: V?) {
field = value
return setter.invoke(thisRef, value, property.name)
}
}
无论对于可空的还是不可为空的属性类,我们都没有在其中实现任何具体的代码,而是将设置和获得操作通过Lambda转换了回去。所以这时我们凝聚出的查克拉是没有用的,需要进行提炼。
提炼查克拉
查克拉凝聚完成之后,我们需要提炼出查克拉以便注入到忍具当中。在这之前我们需要一个辅助工具:
open class BaseActivity : AppCompatActivity() {
val mSavedInstanceState = Bundle()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState != null)
mSavedInstanceState.putAll(savedInstanceState)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putAll(mSavedInstanceState)
}
}
该类作为需要被委托的基类,其中声明了一个全局的变量mSavedInstanceState
,这就是数据被存放的异次元空间。
提炼整型查克拉
private fun <T : BaseActivity> `BaseActivity$$getPrimaryProperty`(default: Int)
= `BaseActivity$$SavePrimaryProperty`(
{ t: T, name -> t.mSavedInstanceState.getInt(name, default) },
{ t: T, v, name -> t.mSavedInstanceState.putInt(name, v) })
在这里,我们或得到了BaseActivity
的mSavedInstanceState
,并进行相应的操作。其他基本数据类型的提炼过程相似。
提炼字符串查克拉
private fun <T : BaseActivity> `BaseActivity$$getStringProperty`(default: String?)
= `BaseActivity$$SaveProperty`(
{ t: T, name -> t.mSavedInstanceState.getString(name, default) },
{ t: T, v, name -> t.mSavedInstanceState.putString(name, v) })
可以看到对于基本数据类型和非基本数据类型的提炼过程基本相同。
注入查克拉到忍具
在这个过程中,我们用到了扩展机制,来保证我们创建的对象一定处于其可用的类兑现中。
Int类型:
fun BaseActivity.save(default: Int)
: ReadWriteProperty<BaseActivity, Int> = `BaseActivity$$getPrimaryProperty`(default)
String类型:
fun BaseActivity.saveString(default: String? = null)
: NullableReadWriteProperty<BaseActivity, String> = `BaseActivity$$getStringProperty`(default)
忍具评价
对于以上忍具的使用,是一定会有查克拉损失和空间转换的时间损失的,经粗略计算,使用该忍具进行委托的时间消耗是普通方式的5倍,乍一看好像很严重,但对于计算机来说其速度损耗可忽略。对于一个字符串的每万次委托调用时间统计为92ms,普通调用为20ms
保险起见,我们可以将重要但不常被调用的属性通过此方式委托。如果数据量小的话就不必要拘泥于一点点的性能损耗,毕竟鱼与熊掌不可兼得。
好了,本期忍者世界大讲堂就到这里了,欢迎下次光临。