Android SharedPreferences封装实践2019-12-09

为什么要封装?

系统提供SharedPreferences很不好用,参数多余,也不友好,用起来很繁琐,基本上都需要封装一下才能用。

创建方法1

// 创建函数式context的一个实例方法
public abstract SharedPreferences getSharedPreferences(String var1, int var2);

// 使用的一个例子
SharedPreferences sp=context.getSharedPreferences("名称", Context.MODE_PRIVATE);
  • context,在安卓中很常见。不过从含义上来说,这和工具,垃圾箱等等一样,是不明确的,不不是好用的一个参数。从大多数使用场景来看,context可以代表一个Activity,也可以代表一个Application。但不论怎么说,这是一个实例,有个体的含义在。

  • getSharedPreferences,从方法的名字看应该是静态类型的,但是却出现先在一个对象的成员方法中,怎么看都感觉别扭。

  • "名称"参数,代表的是一个文件名,这说明了SharedPreferences本质上是文件操作,这个函数的真正作用,其实是打开或者新建一个文件。

  • Context.MODE_PRIVATE,这个参数看起来很乞丐,不过了解了SharedPreferences的本质是读写xml文件之后,也可以理解,这个参数是为了再多进程之间通过xml配置文件进行信息共享。不得不说,当初的设计者的脑洞真大,野心也真大。

  • 这个方法作为xml文件的底层读写方法是合适的,但是作为key-value形式的缓存信息的读写,可以说是很不好用。

创建方法2

// 方法定义
public static SharedPreferences getDefaultSharedPreferences(Context context) {
        throw new RuntimeException("Stub!");
    }

// 使用例子
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
  • 这个方法就简单多了,省去了文件名,多文件共享等参数,看起来更像缓存读写类函数。

  • 静态方法,并且代表的是整个Application的缓存。

  • context参数是一个大败笔,就是“坏了那锅粥的老鼠屎”。前面已经分析过context名字含有不明确,既可以是一个Activity,也可以是一个Application。既然这个静态方法是代表整个Application,放这么一个不伦不类的参数在这里干什么呢?只能说明设计者的脑子真二。

创建方法3

// 方法定义
public SharedPreferences getPreferences(int mode) {
        throw new RuntimeException("Stub!");
}

// 使用实例
SharedPreferences sharedPref = getActivity().getPreferences(Context.MODE_PRIVATE);
  • 这是某个Activity特有的缓存读写,所以是Activity对象的成员方法,有一定的使用场景。

  • Context.MODE_PRIVATE,又是一个脑子被门缝夹的参数,真不知道设计者脑子能蠢成什么样子。既然是某个Activity特有的,加这么个用来共享的参数真是不伦不类。

写入操作

//可以创建一个新的SharedPreference来对储存的文件进行操作
SharedPreferences sp=context.getSharedPreferences("名称", Context.MODE_PRIVATE);
//像SharedPreference中写入数据需要使用Editor
SharedPreference.Editor editor = sp.edit();
//类似键值对
editor.putString("name", "string");
editor.putInt("age", 0);
editor.putBoolean("read", true);
//editor.apply();
editor.commit();
  • 既然只是简单的put,知觉中只要一条语句就可以了,为什么要这么多?

  • edit(),commit()这些都是什么鬼?跟put有关系吗?

  • 这些事写文件的常见操作,但不是写缓存应该有的操作。

  • apply(),commit()这两个又是什么作用,有什么区别?方法的意义在于让使用者用起来简单舒服,而不是带来困扰。

小结:使用者的期望是简单信息的缓存读写,不过看到的是不伦不类的文件读写。广大Android开发者的眼睛是雪亮的,几乎所有人都觉得要二次封装一下,不然用起来就觉得恶心。能设计出人人喊打的API,设计者的脑子该有多二啊

Android SharedPreference的使用
SharedPreference使用

iOS的缓存读写

作为对比,可以参考一下iOS的缓存读写,就知道简单信息的缓存API应该是什么样子的。

    //将NSString 对象存储到 NSUserDefaults 中

    NSString *passWord = @"1234567";

    NSUserDefaults *user = [NSUserDefaults standardUserDefaults];

    [user setObject:passWord forKey:@"userPassWord"];

NSUserDefaults 简介,使用 NSUserDefaults 存储自定义对象

第1层封装

封装对象

// 创建函数式context的一个实例方法
public abstract SharedPreferences getSharedPreferences(String var1, int var2);

// 使用的一个例子
SharedPreferences sp=context.getSharedPreferences("名称", Context.MODE_PRIVATE);
  • Context.MODE_PRIVATE这个参数多余,应该隐藏掉。

  • "名称",文件名这个参数反而是需要的。由于本质是xml文件的学些。一方面出于性能考虑,多分几个文件,可以减小文件大小。另外一方面,从逻辑上,配置信息也是分块的,多几个文件也是现实需求。
    Android之不要滥用SharedPreferences

  • context这个参数是Android的习惯,不想要,但是作为通用工具的话又隐藏不了。要等到具体的APP之后,可以用Applicationcontext来统一替代,一般在二次封装的时候再隐藏。

实例还是静态方法

  • 通过静态方法封装,让很多步骤的put方法简单好用。这是很普遍的做法,也是很好的一种做法,比如下面这样的例子
    /**
     * 保存数据的方法,我们需要拿到保存数据的具体类型,然后根据类型调用不同的保存方法
     *
     * @param context  上下文
     * @param key      关键字
     * @param object   数据
     * @param fileName 文件名
     */
    public static void put(Context context, String key, Object object, String fileName)
  • 作为通用工具,这种封装方法是很灵活的,也很独立,没有依赖。

  • 这篇文章就是一篇很好的封装参考,强烈推荐:
    SharedPreferences封装类SPUtils

  • Context context; String fileName这两个参数是拖油瓶,在具体的put操作中用处不大,感觉有点多余。

  • 所以,这里考虑进行封装成实例对象的尝试,目的是将Context context; String fileName这两个拖油瓶参数在对象初始化的时候统一确定,减少put等缓存实际操作方法的参数。

  • 静态方法封装,名字可以叫SPUtil; 而现在的实例化封装,那么名字就叫SPCache

构造函数

统一确定Context context; String fileName这两个参数是拖油瓶,并且作为内部静态变量,对外隐藏。

public final class SPCache {
    // 隐藏两个参数
    private Context context;
    private SharedPreferences sp;

    // 禁止无参默认构造函数
    private SPCache() {}

    // 统一确定两个参数,一个fileName,对应于内部一个sp
    public SPCache(@NonNull Context context, @NonNull String fileName) {
        this.context = context.getApplicationContext();
        this.sp = this.context.getSharedPreferences(fileName, Context.MODE_PRIVATE);
    }
}

put操作封装

  • 关于文件操作的内容,比如edit,commit之类的,需要隐藏,对外应该是key-value风格的。

  • putXXX有一类方法,有点杂,可以统一为Objectput操作。

    /**
     * 存入缓存,类型可以是int,long,float,Boolean,String;自定义对象会通过toString()转换为字符串
     * @param key     键
     * @param object  值
     */
    public void put(@NonNull String key, Object object) {
        SharedPreferences.Editor editor = sp.edit();

        // instanceof 用来 指出对象是否是特定类的一个实例
        if (object instanceof String) {
            editor.putString(key, (String) object);
        }
        else if (object instanceof Integer) {
            editor.putInt(key, (Integer) object);
        }
        else if (object instanceof Boolean) {
            editor.putBoolean(key, (Boolean) object);
        }
        else if (object instanceof Float) {
            editor.putFloat(key, (Float) object);
        }  else if (object instanceof Long) {
            editor.putLong(key, (Long) object);
        } else {
            editor.putString(key, object.toString());
        }

        editor.commit();
    }

get操作封装

put类似,将一堆getXXX操作统一为get Object

    /**
     * 根据默认值类型,获取相应的数据。类型可以是int,long,float,Boolean,String;
     * @param key               键
     * @param defaultObject     默认值
     * @return                  数据
     */
    public Object get(@NonNull String key, @NonNull Object defaultObject) {
        if (defaultObject instanceof String) {
            return sp.getString(key, (String) defaultObject);
        }
        else if (defaultObject instanceof Integer) {
            return sp.getInt(key, (Integer) defaultObject);
        }
        else if (defaultObject instanceof Boolean) {
            return sp.getBoolean(key, (Boolean) defaultObject);
        }
        else if (defaultObject instanceof Float) {
            return sp.getFloat(key, (Float) defaultObject);
        }
        else if (defaultObject instanceof Long) {
            return sp.getLong(key, (Long) defaultObject);
        }
        return null;
    }

删除和清空

    /**
     * 移除键对应的数据
     * @param key  键
     */
    public void remove(@NonNull String key) {
        SharedPreferences.Editor editor = sp.edit();
        editor.remove(key);
        editor.commit();
    }

    /**
     * 清空缓存
     */
    public void clear() {
        SharedPreferences.Editor editor = sp.edit();
        editor.clear();
        editor.commit();
    }

讨论:SharedPreferencesCompat

  • SharedPreferences适合读写少量的配置信息,正常使用,没什么问题。但是如果滥用的话,比如,信息量大的时候,毕竟是xml文件读写,所以性能比较低。
    Android之不要滥用SharedPreferences

  • 为了提升性能,引入了editor.apply();,将文件读写操作移到工作线程中。但是,这样又引入了偶尔会崩溃的新问题。

  • 为了解决崩溃问题,引入了SharedPreferencesCompat,核心代码就是默认用editor.apply();,提高性能;如果崩溃了,就用editor.commit();

 public void apply(@NonNull SharedPreferences.Editor editor) {
       try {
            editor.apply();
        } catch (AbstractMethodError unused) {
            // The app injected its own pre-Gingerbread
            // SharedPreferences.Editor implementation without
            // an apply method.
            editor.commit();
       }
}
  • 不知道什么原因,Android又把SharedPreferencesCompat的废弃了。
image.png

小结:既然被废弃,就不用了,直接使用editor.commit();保证安全。如果担心性能问题,应该考虑信息拆分,正确使用,符合SharedPreferences少量配置信息读写的定位

第2层封装: 静态化

  • 到了具体的APP,context可以用Application对应的那个全局化的。

  • 根据逻辑模块划分,相应的fileName也就可以指定了。

  • context,fileName两个参数确定之后,就能得到一个sp对象。这个sp对象可以作为这层封装类的静态对象,进行实际的读写操作。

  • fileNameXXXKey等可以定义为私有的静态字符串在这个包装类中进行隐藏。

  • 对外可以提供一些公有的静态方法,聚焦在信息的读写。静态方法的使用是最方便的,比单例访问还少了getInstance()这一步骤。

  • 以用户信息读写为例,这里做个简单封装,可以是这样的:

public final class SPUserInfo {
    // sp 对象
    private final static SPCache spCache = new SPCache(MainApplication.getContext(), "SPUserInfo");

    // 各种key常数定义
    private final static String userNameKey = "userNameKey";
    private final static String passwordKey = "passwordKey";

    // 读写接口
    public static void put(UserInfo userInfo) {
        spCache.put(userNameKey, userInfo.getUserName());
        spCache.put(passwordKey, userInfo.getPassword());
    }

    public static UserInfo get() {
        String userName = (String) spCache.get(userNameKey, "默认的名字");
        String password = (String) spCache.get(passwordKey, "默认的密码");
        return new UserInfo(userName, password);
    }
}

Demo

  • 界面:


    image.png
  • xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    >

    <EditText
        android:layout_marginTop="20dp"
        android:id="@+id/edit_user_name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="用户名"
        android:singleLine="true"
        />
    <EditText
        android:layout_marginTop="20dp"
        android:id="@+id/edit_password"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="密码"
        android:inputType="textPassword"
        android:singleLine="true"
        />
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:layout_marginTop="20dp">
        <Button
            android:id="@+id/input"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="存储"
            android:onClick="writeInfo"/>
        <Button
            android:id="@+id/output"
            android:layout_marginLeft="10dp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="读取"
            android:onClick="readInfo"/>
    </LinearLayout>
    <TextView
        android:id="@+id/showtext"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:layout_marginLeft="15dp"
        android:text="存储的信息"
        android:textSize="25dp"/>
    <TextView
        android:id="@+id/text_user_name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:layout_marginLeft="15dp"
        android:text="用户名"
        android:textSize="20dp"/>
    <TextView
        android:id="@+id/text_password"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:layout_marginLeft="15dp"
        android:text="密码"
        android:textSize="20dp"/>
</LinearLayout>
  • Activity
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void writeInfo(View view) {
        EditText userNameEdit = (EditText)findViewById(R.id.edit_user_name);
        EditText passwordEdit = (EditText)findViewById(R.id.edit_password);
        String userName = userNameEdit.getText().toString();
        String password = passwordEdit.getText().toString();
        UserInfo userInfo = new UserInfo(userName, password);
        SPUserInfo.put(userInfo);
    }

    public void readInfo(View view) {
        TextView userNameText = (TextView)findViewById(R.id.text_user_name);
        TextView passwordText = (TextView)findViewById(R.id.text_password);
        UserInfo userInfo = SPUserInfo.get();
        userNameText.setText(userInfo.getUserName());
        passwordText.setText(userInfo.getPassword());
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343