谈谈Android中的SharedPreferences

提起SharedPreferences,应该所有的开发者都觉得很简单吧,会不会想:这个有啥好说的,还写博客?是不是装X啊?
其实我也觉得SharedPreferences很简单,除非你要用它实现多进程之间的数据共享。说到多进程之间使用SharedPreferences,会不会有人想说:创建它的时候不是有个参数Context.MODE_MULTI_PROCESS,这不是android官方提供的多进程支持吗?呵呵,少年,too young too simple啊!

Note: currently this class does not support use across multiple processes. This will be added later.
MODE_MULTI_PROCESS does not work reliably in some versions of Android, and furthermore does not provide any mechanism for reconciling concurrent modifications across processes. Applications should not attempt to use it. Instead, they should use an explicit cross-process data management approach such asContentProvider.
以上摘自android官方文档SharedPreferencesContext介绍部分

抛开多进程之间使用,在自己的App中使用SharedPreferences还是很简单的。当然这篇文章我不是说开发中如何使用它,而是记录自己在优化我们的App启动过程中发现的一些问题--SharedPreferences导致的问题。

进入正题

引子

SharedPreferences是Android SDK提供的工具,可以存储应用的一些配置信息,这些信息会以键值对的形式保存在/sdcard/data/data/packageName/shared_prefs/路径下的一个xml文件中。它提供了多种数据类型的存储,包括:intlongbooleanfloatString以及Set<String>

我真正想记录的

获取SharedPreferences的实例一般会有两种方式:

  • context.getSharedPreference(name, mode);
  • PreferenceManager.getDefaultSharedPreferences(context);
    这两种方式其实具体实现是一样的,只不过一个是开发者自己定义名字,另一个是使用包名+"_preference"作为存储文件名。

getSharedPreference(name, mode)

public SharedPreferences getSharedPreferences(String name, int mode) {
    return mBase.getSharedPreferences(name, mode);
}

PreferenceManager.getDefaultSharedPreferences(context)

public static SharedPreferences getDefaultSharedPreferences(Context context) {
    return
context.getSharedPreferences(getDefaultSharedPreferencesName(context),           
                                              getDefaultSharedPreferencesMode());
}
private static String getDefaultSharedPreferencesName(Context context) {
    return context.getPackageName() + "_preferences";
}
private static int getDefaultSharedPreferencesMode() {
    return Context.MODE_PRIVATE;
}

我们的App中充斥着大量的PreferenceManager.getDefaultSharedPreferences(context)使用方式,可能是大家觉得方便,又是API级的Manager管理,所以使用起来很放心,然而这也正是问题的根本所在。问题在哪里呢?接下来便告诉你。
因为在做App的优化,自然少不了使用MethodTrace,在分析启动流程时发现一个很诡异的点,那就是一个读取配置操作竟然耗时400+ms的时间(有图为证),

我当时就被震惊了,这里竟然耗时这么久!作为一个码农,这时必须要看源码呀,可惜通过AS并不能直接看到实现逻辑,不过通过Google还是可以轻松找到的,有兴趣的看这里
SharedPreferences是一个抽象类,所以具体实现的逻辑便在其子类SharedPreferencesImpl中,先看下它的构造方法:

SharedPreferencesImpl(File file, int mode) {

   mFile = file;
   mBackupFile = makeBackupFile(file);
   mMode = mode;
   mLoaded = false;
   mMap = null;
   startLoadFromDisk();//可以看到,当你使用时,这里变开始从sdcard读取xml文件

}

startLoadFromDisk()具体实现代码如下:

private void startLoadFromDisk() {
    synchronized (this) {
        mLoaded = false;
    }
    new Thread("SharedPreferencesImpl-load") {
        public void run() {
            synchronized (SharedPreferencesImpl.this) {
                loadFromDiskLocked();
            }
        }
    }.start();
}

private void loadFromDiskLocked() {
    if (mLoaded) {
        return;
    }
    if (mBackupFile.exists()) {
        mFile.delete();
        mBackupFile.renameTo(mFile);
    }
    // Debugging
    if (mFile.exists() && !mFile.canRead()) {
        Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");
    }
    Map map = null;
    StructStat stat = null;
    try {
        stat = Os.stat(mFile.getPath());
        if (mFile.canRead()) {
            BufferedInputStream str = null;
            try {
                str = new BufferedInputStream(
                        new FileInputStream(mFile), 16 * 1024);
                map = XmlUtils.readMapXml(str);
            } catch (XmlPullParserException e) {
                Log.w(TAG, "getSharedPreferences", e);
            } catch (FileNotFoundException e) {
                Log.w(TAG, "getSharedPreferences", e);
            } catch (IOException e) {
                Log.w(TAG, "getSharedPreferences", e);
            } finally {
                IoUtils.closeQuietly(str);
            }
        }
    } catch (ErrnoException e) {
    }
    mLoaded = true;
    if (map != null) {
        mMap = map;
        mStatTimestamp = stat.st_mtime;
        mStatSize = stat.st_size;
    } else {
        mMap = new HashMap<String, Object>();
    }
    notifyAll();
}

再看看get方法:

public String getString(String key, String defValue) {
    synchronized (this) {
        awaitLoadedLocked();
        String v = (String)mMap.get(key);
        return v != null ? v : defValue;
    }
}

从代码可以看到,在get之前是要调用awaitLoadedLocked ()方法的,那这个方法具体做了什么呢?看代码:

private void awaitLoadedLocked() {
    if (!mLoaded) {
        // Raise an explicit StrictMode onReadFromDisk for this
        // thread, since the real read will be in a different
        // thread and otherwise ignored by StrictMode.
        BlockGuard.getThreadPolicy().onReadFromDisk();
    }
    while (!mLoaded) {
        try {
            wait();
        } catch (InterruptedException unused) {
        }
    }
}

也许你已经看出来了,在真正get数据之前是要判断文件加载状态的。如果此时没加载出来,那就要等待。这时你有没有理解为什么第一次调用要花费400+ms的时间(就是MethodTrace图上显示的时间,虽然MethodTrace方法本身会增加方法的调用时间,但基本上还是可以反应出实际调用时间长短的)了吗?是不是心里在想:你的App中使用大量PreferenceManger获取SharedPreferences对象来存储配置信息,导致该文件变得很大,文件大肯定加载时间要长嘛。Bingo,you get it。前面铺垫那么多,就是为了这一点,文件大自然要等待很久,所以如果你的App对初始化时间敏感,并且你配置文件中存了太多的东西,那么你应该考虑把初始化所需要的配置放在单独的文件中(自然是使用context.getSharedPreferences()方法,传入单独的名称),而不是所有的配置放一起了。
后记:SharedPreferencesImpl的实例是有缓存的,cache在ContextImpl类中,所以在load之后,下次再调用SharedPreferences.getXXX()方法时你将看不到漫长的wait时间了。

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

推荐阅读更多精彩内容