SharedPreferences

Android自带操作SharedPreferences的API个人觉得很麻烦, 也一直在想办法去改进. 最初的封装很简单也很直接, 通过简化实例化SharedPreferences以及Editor的代码, 提供一个saveString与getString的公共方法以供外部调用. 这样可以比较方便的存取字符串. 但是这种方式只能存字符串, 其他类型都要转换成字符串再存起来. 取出来也需要转换一遍. 并且每一个字符串都需要额外的去维护与之对应的键名, 因此还是不怎么方便.

设想一下, 如果需要将一个对象都存入SharedPreferences中, 用之前的方法, 则对象的属性越多, 需要维护的键就越多. 存与取的代码量会非常大.

这里可能会存在疑惑, 为何要在SharedPreferences里存取对象, 为何不使用数据库, 或者是文件? 首先, 数据库太重, 如果不是不得已的情况下还是不要用数据库的好. 文件与SharedPreferences比起来, 文件可以存更大的数据, 但是对象一般都是比较轻量的数据, 轻量的数据还是建议使用SharedPreferences来存取会方便一些. 试想一下, 登录之后我们需要缓存一些用户的数据, 如用户名, id, 头像之类的数据. 这些数据如果以User对象的形式存起来, 取的时候能直接返回User对象, 这样是不是会很方便?

要实现上述想法, 有两种办法:

  • 反射

  • 序列化

反射

在我脑海里, 反射性的想到了反射, 但是呢, 虽然知道有反射这个黑科技, 却从来没用过, 理所当然要试验一下. 思路不复杂, 但是需要先将存与取的Api想好. 因为我们不能确定要存与要取的对象类型, 理所当然这里会用到泛型, 请看:

public <T> void saveData(T data);
public <T> T getData(Class<T> clz);

这里使用T来代替实际我们想要存取的数据类型, 在存数据的时候, 只需要调用saveData方法并将要存的数据实体传入即可. 取对象的时候, 则在getData中要传入一个Class类型, Class是何类型, 最后getData就会返回什么类型. 跟着这组API我们来继续完善:

  • SharedPreferences每一个值都需要一个键, 我们的键名可以直接使用属性名, 值就是属性的值

  • 由于SharedPreferences里相同的键的值会被覆盖, 而不同对象的属性名不可避免的会相同, 因此每一个不同的对象都应该使用不同的文件. 文件的名字可以以类名命名.

因为设计上每一个类都对应一个SharedPreferences文件, 因此需要专门写一个类, 去持有一个SharedPreferences对象, 并提供读与写的方法:

    class SharedPreferencesHolder<T> {
        private SharedPreferences preferences;
        private Class<T> clz;

        public SharedPreferencesHolder(Context context, Class<T> clz) {
            this.clz = clz;
            preferences = context.getSharedPreferences(clz.getName(), Context.MODE_PRIVATE);
        }

        public T getData() {}

        public void saveData(T data) {}
    }

这里先省略getData与saveData的方法体. 在构造函数中, 使用clz.getName()作为文件名来来初始化SharedPreferences. 接下来我们就可以利用Class对象做一些事情, 先来看看saveData方法:

public void saveData(T data) {
    Class clz = data.getClass(); // 获取Class对象
    Field[] fields = clz.getDeclaredFields(); // 获取该类中声明的属性
    for (Field field : fields) {
        field.setAccessible(true);
        String type = field.getType().toString(); // 获取属性的属性名
        try {
            if (type.endsWith("int") || type.endsWith("Integer")) {
                preferences.edit().putInt(field.getName(), field.getInt(data)).apply();
            } else if (type.endsWith("long") || type.endsWith("Long")) {
                preferences.edit().putLong(field.getName(), field.getLong(data)).apply();
            } else if (type.endsWith("boolean") || type.endsWith("Boolean")) {
                preferences.edit().putBoolean(field.getName(), field.getBoolean(data)).apply();
            } else {
                preferences.edit().putString(field.getName(), field.get(data).toString()).apply();
            }
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

上述代码不复杂, 即遍历类中声明的属性, 判断属性的类型, 并将属性名作为键, 属性值作为值存入SharedPreferences中. 接下来看getData方法:

public T getData() {
    T data = null;
    try {
        data = clz.newInstance();
        Field[] fields = clz.getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);
            String type = field.getType().toString();
            if (type.endsWith("int") || type.endsWith("Integer")) {
                field.set(data, preferences.getInt(field.getName(), 0));
            } else if (type.endsWith("long") || type.endsWith("Long")) {
                field.set(data, preferences.getLong(field.getName(), 0));
            } else if (type.endsWith("boolean") || type.endsWith("Boolean")) {
                field.set(data, preferences.getBoolean(field.getName(), false));
            } else {
                field.set(data, preferences.getString(field.getName(), ""));
            }
        }
    } catch (InstantiationException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }

    return data;
}

getData中则通过构造函数里传入的Class对象, 生成一个空的对象, 并遍历类的属性, 一一从SharedPreferences中取值并赋值给相应的属性.

接着我们需要写一个工具类来操作SharedPreferencesHolder:

public class SharedPreferencesClassHelper {
    private static HashMap<String, SharedPreferencesHolder> preferencesHolders = new HashMap<>();
    private static Context appContext;
    private static SharedPreferencesClassHelper instance = new SharedPreferencesClassHelper();

    public static void init(Context context) {
        appContext = context;
    }

    public static SharedPreferencesClassHelper getInstance() {
        return instance;
    }

    public <T> T getData(Class<T> clz) {
        SharedPreferencesHolder<T> holder = getPreferencesHolder(clz);
        return holder.getData();
    }

    public <T> void saveData(T data) {
        SharedPreferencesHolder<T> holder = getPreferencesHolder((Class<T>) data.getClass());
        holder.saveData(data);
    }

    private <T> SharedPreferencesHolder<T> getPreferencesHolder(Class<T> clz) {
        SharedPreferencesHolder<T> holder = preferencesHolders.get(clz.getName());
        if (holder == null) {
            holder = new SharedPreferencesHolder<>(appContext, clz);
            preferencesHolders.put(clz.getName(), holder);
        }

        return holder;
    }
}

SharedPreferencesClassHelper使用了HashMap将SharedPreferencesHolder缓存起来, 避免每次调用都去新建SharedPreferencesHolder对象. 最后来看看使用方式:

SharedPreferencesClassHelper.init(context);
SharedPreferencesClassHelper.getInstance().saveData(user);
User user = SharedPreferencesClassHelper.getInstance().getData(User.class);

序列化

通过自己写反射的实现方式有明显的缺陷, 首先目前只支持了4种类型int, long, boolean, String, 如果要扩展只能修改SharedPreferencesHolder的getData与setData方法. 其次, 每一个类对应一个文件比较浪费资源, 同时效率也不高. 最后SharedPreferencesClassHelper会缓存使用过的SharedPreferencesHolder对象, 内存有一定开销.

因此上述代码已经被废弃了, 接下来来实现第二种方式, 通过序列化来存储对象. 其主要思想是将对象转换成JSON字符串存入SharedPreferences中, 取对象的时候再对JSON做一次转换. 第三方序列化库有挺多, 但是由于CoreLibs里本身使用了GSON, 因此此处也选用GSON来对对象进行转换.

由于对象是转换成字符串的形式, 所以只需要在同一个文件下, 配置不同的键名就能存储不同的对象. 因此通过序列化实现的新的工具类的所有方法都可以声明成静态的. 其中最关键的代码如下:

gson.toJson(data);
gson.fromJson(json, clz);

其余逻辑都很简单:

/**
 * SharedPreferences工具类, 可以通过传入实体对象保存其至SharedPreferences中,
 * 并通过实体的类型Class将保存的对象取出. 支持不带泛型的对象以及List集合
 */
public class PreferencesHelper {

    private static final String LIST_TAG = ".LIST";
    private static SharedPreferences sharedPreferences;
    private static Gson gson;

    /**
     * 使用之前初始化, 可在Application中调用
     * @param context 请传入ApplicationContext避免内存泄漏
     */
    public static void init(Context context) {
        sharedPreferences = context.getSharedPreferences(context.getPackageName(),
                Context.MODE_PRIVATE);
        gson = new Gson();
    }

    private static void checkInit() {
        if (sharedPreferences == null || gson == null) {
            throw new IllegalStateException("Please call init(context) first.");
        }
    }

    /**
     * 保存对象数据至SharedPreferences, key默认为类名, 如
     * <pre>
     * PreferencesHelper.saveData(saveUser);
     * </pre>
     * @param data 不带泛型的任意数据类型实例
     */
    public static <T> void saveData(T data) {
        saveData(data.getClass().getName(), data);
    }

    /**
     * 根据key保存对象数据至SharedPreferences, 如
     * <pre>
     * PreferencesHelper.saveData(key, saveUser);
     * </pre>
     * @param data 不带泛型的任意数据类型实例
     */
    public static <T> void saveData(String key, T data) {
        checkInit();
        if (data == null)
            throw new IllegalStateException("data should not be null.");
        sharedPreferences.edit().putString(key, gson.toJson(data)).apply();
    }

    /**
     * 保存List集合数据至SharedPreferences, 请确保List至少含有一个元素, 如
     * <pre>
     * PreferencesHelper.saveData(users);
     * </pre>
     * @param data List类型实例
     */
    public static <T> void saveData(List<T> data) {
        checkInit();
        if (data == null || data.size() <= 0)
            throw new IllegalStateException(
                    "List should not be null or at least contains one element.");
        Class returnType = data.get(0).getClass();
        sharedPreferences.edit().putString(returnType.getName() + LIST_TAG,
                gson.toJson(data)).apply();
    }

    /**
     * 将数据从SharedPreferences中取出, key默认为类名, 如
     * <pre>
     * User user = PreferencesHelper.getData(key, User.class)
     * </pre>
     */
    public static <T> T getData(Class<T> clz) {
        return getData(clz.getName(), clz);
    }

    /**
     * 根据key将数据从SharedPreferences中取出, 如
     * <pre>
     * User user = PreferencesHelper.getData(User.class)
     * </pre>
     */
    public static <T> T getData(String key, Class<T> clz) {
        checkInit();
        String json = sharedPreferences.getString(key, "");
        return gson.fromJson(json, clz);
    }

    /**
     * 将数据从SharedPreferences中取出, 如
     * <pre>List<User> users = PreferencesHelper.getData(List.class, User.class)</pre>
     */
    public static <T> List<T> getData(Class<List> clz, Class<T> gClz) {
        checkInit();
        String json = sharedPreferences.getString(gClz.getName() + LIST_TAG, "");
        return gson.fromJson(json, ParameterizedTypeImpl.get(clz, gClz));
    }

    /**
     * 简易字符串保存, 仅支持字符串
     */
    public static void saveData(String key, String data) {
        sharedPreferences.edit().putString(key, data).apply();
    }

    /**
     * 简易字符串获取, 仅支持字符串
     */
    public static String getData(String key) {
        return sharedPreferences.getString(key, "");
    }

    /**
     * 删除保存的对象
     */
    public static void remove(String key) {
        sharedPreferences.edit().remove(key).apply();
    }

    /**
     * 删除保存的对象
     */
    public static void remove(Class clz) {
        remove(clz.getName());
    }

    /**
     * 删除保存的数组
     */
    public static void removeList(Class clz) {
        sharedPreferences.edit().remove(clz.getName() + LIST_TAG).apply();
    }
}

使用方法:

PreferencesHelper.init(getApplicationContext); // 此处一定要使用ApplicationContext
PreferencesHelper.saveData(user);
User user = PreferencesHelper.getData(User.class);

使用起来要方便多了, 建议不要使用PreferencesHelper存过于复杂的对象, 也不要存带有Bitmap或其他复杂属性的对象, 仅仅存储一些简单的, 由基本类型构成的实体类.

Last updated