SharedPreferences阅读笔记

简介

SharedPreferences是Android系统API提供的一种数据存储和读取机制,内部通过xml的格式存储一些数据,可以作为一种简易的硬盘缓存来使用

public interface SharedPreferences

SharedPreferences本身只是一个接口而已

例子

先看一个简单的使用例子

    private String tryGetStringFromSP(String fileName,String key,String defaultValue){
        if(null == fileName || null == key){
            return null;
        }
        SharedPreferences sharedPreferences = mContext.getSharedPreferences(fileName,Context.MODE_PRIVATE);
        return sharedPreferences.getString(key,defaultValue);
    }

    private void tryStoreStringToSP(String fileName,String key,String value){
        if(null == fileName || null == key){
            return;
        }
        SharedPreferences sharedPreferences = mContext.getSharedPreferences(fileName,Context.MODE_PRIVATE);
        SharedPreferences.Editor editor = sharedPreferences.edit();
        editor.putString(key,value);
        editor.apply();
    }

先通过文件名获得对应的SharedPreferences,然后通过键值对的方式来使用。
存储的时候需要额外开启类似数据库操作的事务的editor。

获得SharedPreferences实例

默认通过Context获取SharedPreferences,Context的实现类为ContextImpl:

    @Override
    public SharedPreferences getSharedPreferences(String name, int mode) {
        // 在API19之前,支持name为null,默认为null.xml文件
        if (mPackageInfo.getApplicationInfo().targetSdkVersion <
                Build.VERSION_CODES.KITKAT) {
            if (name == null) {
                name = "null";
            }
        }
        
        File file;
        synchronized (ContextImpl.class) {//这是个类锁
            //private ArrayMap<String, File> mSharedPrefsPaths;
            //可以看到是一个String->File的键值对
            if (mSharedPrefsPaths == null) {
                mSharedPrefsPaths = new ArrayMap<>();
            }
            //其实name就是文件名前缀(不包括.xml),通过名称获取文件
            //也可以看出来这是一个缓存机制
            file = mSharedPrefsPaths.get(name);
            if (file == null) {//当前文件没在缓存中
                //这个其实就是组装一下文件的真实路径,然后获取到文件
                file = getSharedPreferencesPath(name);
                //存入缓存之中,这样就可以在后续使用中直接从缓存中拿对应的文件
                mSharedPrefsPaths.put(name, file);
            }
        }
        return getSharedPreferences(file, mode);
    }

    @Override
    public File getSharedPreferencesPath(String name) {
        // 代码有整合
        File base = null;
        synchronized (mSync) {
            //这里就是data里面应用包名里面的shared_prefs目录
            //用DDMS查看的话就是data/data/应用的包名/shared_prefs
            if (mPreferencesDir == null) {
                mPreferencesDir = new File(getDataDir(), "shared_prefs");
            }
            //确保目录存在,否则尝试建立一次当前目录
            base = ensurePrivateDirExists(mPreferencesDir);
        }
        String fileName = name + ".xml";//以.xml结尾,可以看出是一个正常的xml文件
        if (fileName.indexOf(File.separatorChar) < 0) {//不允许有文件路径分隔符,比方说/
            return new File(base, fileName);
        }
        throw new IllegalArgumentException(
                "File " + fileName + " contains a path separator");
    }

    private static File ensurePrivateDirExists(File file) {
        if (!file.exists()) {
            try {//如果当前目录不存在,尝试创建一次,并且给予指定的权限
                Os.mkdir(file.getAbsolutePath(), 0771);
                Os.chmod(file.getAbsolutePath(), 0771);
            } catch (ErrnoException e) {
                if (e.errno == OsConstants.EEXIST) {
                    // We must have raced with someone; that's okay
                } else {
                    Log.w(TAG, "Failed to ensure " + file + ": " + e.getMessage());
                }
            }
        }
        return file;
    }

可以看到在从ContextImpl中获取SharedPreferences的时候有一级缓存,文件名前缀和对应文件的缓存,在使用app的过程中多次使用同一个文件名的SharedPreferences是很常见的事情,这里优化了这个操作。
此外,可以看到SharedPreferences本质上是一个存储在data/data/应用的包名/shared_prefs下面的xml文件。

    @Override
    public SharedPreferences getSharedPreferences(File file, int mode) {
        checkMode(mode);//API24以上不再支持MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
            //获得当前应用已经缓存的文件和SharedPreferences实现类
            final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
            sp = cache.get(file);
            if (sp == null) {//没有命中缓存
                sp = new SharedPreferencesImpl(file, mode);//新建实现类
                cache.put(file, sp);//放入缓存当中
                return sp;
            }
        }
        //当前命中缓存
        if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
                getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
            //在多进程模式下或者API11以前,如果文件发生变化,要重新从硬盘中读取数据
            sp.startReloadIfChangedUnexpectedly();
        }//不过实际上官方不推荐多进程模式下的SP,在某一些版本下会有问题,推荐使用ContentProvider来替代
        return sp;
    }

至此,经过两级的缓存成功的获取SharedPreferences实现类SharedPreferencesImpl

SharedPreferencesImpl

    SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        mBackupFile = makeBackupFile(file);//在同一级目录下构建.bak备份文件
        mMode = mode;
        mLoaded = false;
        mMap = null;
        startLoadFromDisk();
    }

    /**
     * 在子线程中读取SP文件中的数据
     */
    private void startLoadFromDisk() {
        synchronized (this) {
            mLoaded = false;
        }
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                loadFromDisk();
            }
        }.start();
    }

    private void loadFromDisk() {
        synchronized (SharedPreferencesImpl.this) {
            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);//解析文件中的xml内容转为map
                } catch (XmlPullParserException | IOException e) {
                    Log.w(TAG, "getSharedPreferences", e);
                } finally {
                    IoUtils.closeQuietly(str);
                }
            }
        } catch (ErrnoException e) {
            /* ignore */
        }

        synchronized (SharedPreferencesImpl.this) {
            mLoaded = true;//标记读取完成
            if (map != null) {
                mMap = map;//记录新的读取数据
                mStatTimestamp = stat.st_mtime;
                mStatSize = stat.st_size;
            } else {
                mMap = new HashMap<>();
            }
            notifyAll();
        }
    }

在第一次构建SharedPreferencesImpl的时候会新开线程读取文件中的数据,并且保存到SharedPreferencesImpl的map中。

    public String getString(String key, @Nullable String defValue) {
        synchronized (this) {//这里有锁,意味着可能会阻塞线程
            awaitLoadedLocked();//如果当前在进行IO操作从硬盘中读取新的数据,等待数据更新完成,这里可能也会阻塞线程
            //绝大部分操作都是在主线程进行的话那么没有阻塞线程的可能,主要的场景是在startLoadFromDisk中
            //这个会在第一次创建SP的时候调用
            //这意味着如果File过大,读取时间过长,可能会出现问题,这里需要特别注意
            String v = (String)mMap.get(key);//直接从map中获取数据
            return v != null ? v : defValue;
        }
    }

从SharedPreferencesImpl中获取数据实际上就是从map也就是内存中获取数据。
有一点需要注意,每一次获取数据的时候都有一把锁,这个锁的异步操作主要是在读取文件到map这个过程中持有,即startLoadFromDisk中,一般这个读取操作是在创建SharedPreferencesImpl时候进行,这意味着如果是第一次使用并且快速的进行get操作,可能会导致主线程卡顿。
所以为了避免阻塞的情况,可以利用ContextImpl中SharedPreferencesImpl的缓存,然后通过预加载尽可能的在使用SP之前完成从硬盘读取数据到内存中这个步骤。同时也要注意SharedPreferences文件在存储时候的大小,尽量不要存储太大的东西,加快IO操作的速度,这也是SharedPreferences被称为轻量级缓存的一个原因。

EditorImpl

在SharedPreferencesImpl进行写入操作的时候要开启一个类似事务的存在,这个就是EditorImpl

    public Editor edit() {
        synchronized (this) {//等待从硬盘中读取数据到内存中
            awaitLoadedLocked();
        }
        return new EditorImpl();
    }

    //当前数据变化的集合
    private final Map<String, Object> mModified = Maps.newHashMap();
    //是否标记clear数据
    private boolean mClear = false;

    /**
     * 放入String类型的数据
     * @param key 数据的键名
     * @param value 数据
     */
    public SharedPreferences.Editor putString(String key, @Nullable String value) {
        synchronized (this) {
            mModified.put(key, value);//只是单纯的把数据修改在EditorImpl中的集合
            return this;
        }
    }

    /**
     * 移除指定的key的数据
     */
    public SharedPreferences.Editor remove(String key) {
        synchronized (this) {
            mModified.put(key, this);//并没有直接移除key,而是标记value为EditorImpl
            return this;
        }
    }

    /**
     * 清理
     */
    public SharedPreferences.Editor clear() {
        synchronized (this) {
            mClear = true;//标记清理标志
            return this;
        }
    }

EditorImpl内部也维持一个map,用于存储需要变化的数据,但是也注意到单纯的操作是没有任何的效果的。

    /**
     * 在当前线程执行内存和硬盘数据更新
     */
    public boolean commit() {
        MemoryCommitResult mcr = commitToMemory();//先尝试将数据的修改同步到SP中的内存数据
        //尝试将数据写入硬盘当中
        SharedPreferencesImpl.this.enqueueDiskWrite(
                mcr, null /* sync write on this thread okay */);
        try {
            //在任务完成之前,这里都会阻塞
            //可能导致ANR
            mcr.writtenToDiskLatch.await();
        } catch (InterruptedException e) {
            return false;
        }
        notifyListeners(mcr);
        return mcr.writeToDiskResult;
    }

    /**
     * 同步更新内存,异步写入硬盘
     */
    public void apply() {
        final MemoryCommitResult mcr = commitToMemory();
        //该任务在QueuedWork中自动调用,一般是在activity的onPause之类的情况下进行
        //用于等待IO操作的完成,从而避免数据丢失
        //但是这个也有可能导致ANR
        final Runnable awaitCommit = new Runnable() {
            public void run() {
                try {
                    mcr.writtenToDiskLatch.await();//等待写入任务完成
                } catch (InterruptedException ignored) {
                }
            }
        };

        QueuedWork.add(awaitCommit);

        Runnable postWriteRunnable = new Runnable() {
            public void run() {
                awaitCommit.run();
                QueuedWork.remove(awaitCommit);
            }
        };
        //尝试将数据写入硬盘当中
        SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

        //因为内存中的数据已经更新,所以一般来说通知观察者变化也是没有问题的
        notifyListeners(mcr);
    }

    /**
     * 将变化数据提交到SP的内存数据中
     */
    private MemoryCommitResult commitToMemory() {
        MemoryCommitResult mcr = new MemoryCommitResult();
        synchronized (SharedPreferencesImpl.this) {//map修改的锁
            if (mDiskWritesInFlight > 0) {//一般来说在apply操作的时候有可能导致flight>1
                mMap = new HashMap<String, Object>(mMap);//此时拷贝一份内存数据
            }
            mcr.mapToWriteToDisk = mMap;//标记当前SP内部的数据
            mDiskWritesInFlight++;//写入数量

            boolean hasListeners = mListeners.size() > 0;
            if (hasListeners) {
                mcr.keysModified = new ArrayList<String>();
                mcr.listeners =
                        new HashSet<SharedPreferences.OnSharedPreferenceChangeListener>(mListeners.keySet());
            }

            synchronized (this) {
                if (mClear) {//Editor中标记clear数据
                    if (!mMap.isEmpty()) {//当前SP中的数据不为空
                        mcr.changesMade = true;//标记数据发生变化
                        mMap.clear();//清除SP中的所有数据
                    }
                    mClear = false;
                }
                //可以看到clear的优先级不是太高
                for (Map.Entry<String, Object> e : mModified.entrySet()) {
                    String k = e.getKey();
                    Object v = e.getValue();
                    if (v == this || v == null) {//editor中标记移除或者value为空
                        if (!mMap.containsKey(k)) {//当前元素本来就不在SP中,不需要处理
                            continue;
                        }
                        mMap.remove(k);//移除当前元素,SP当中不存在value为null的元素
                    } else {
                        if (mMap.containsKey(k)) {
                            Object existingValue = mMap.get(k);
                            //当前SP已有改数据,不需要处理
                            if (existingValue != null && existingValue.equals(v)) {
                                continue;
                            }
                        }
                        mMap.put(k, v);//修改或者添加数据到SP中
                    }

                    mcr.changesMade = true;//标记数据发生变化
                    if (hasListeners) {//添加对应键值的监听
                        mcr.keysModified.add(k);
                    }
                }

                mModified.clear();//数据变化已经更新到SP的内存数据中,清空editor的数据
            }
        }
        return mcr;
    }

    /**
     * 将数据写入硬盘中
     */
    private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
        //新建一个将变化后的数据写到硬盘xml文件的任务
        final Runnable writeToDiskRunnable = new Runnable() {
            public void run() {
                synchronized (mWritingToDiskLock) {
                    writeToFile(mcr);//写入硬盘中
                }
                synchronized (SharedPreferencesImpl.this) {
                    mDiskWritesInFlight--;
                }
                if (postWriteRunnable != null) {
                    postWriteRunnable.run();
                }
            }
        };
        //commit时候为null,标记同步提交
        final boolean isFromSyncCommit = (postWriteRunnable == null);

        if (isFromSyncCommit) {
            boolean wasEmpty = false;
            //如果之前存在apply异步操作,那么可能在mDiskWritesInFlight--之前再次commit
            //此时也应该在apply之后执行
            synchronized (SharedPreferencesImpl.this) {
                wasEmpty = mDiskWritesInFlight == 1;
            }
            if (wasEmpty) {//如果可以同步进行
                writeToDiskRunnable.run();//在当前线程写入数据到硬盘中
                return;
            }
        }
        //apply或者commit但是之前的apply没有执行完成,这两种情况下都在单线程池中排队进行
        QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
    }

    /**
     * 将数据写入文件当中
     */
    private void writeToFile(MemoryCommitResult mcr) {
        if (mFile.exists()) {
            if (!mcr.changesMade) {
                //如果文件已经存在,而且数据没有变化,不需要重写文件
                mcr.setDiskWriteResult(true);
                return;
            }
            if (!mBackupFile.exists()) {
                //尝试将旧的文件备份
                if (!mFile.renameTo(mBackupFile)) {
                    Log.e(TAG, "Couldn't rename file " + mFile
                            + " to backup file " + mBackupFile);
                    mcr.setDiskWriteResult(false);
                    return;//备份失败直接结束当前写入操作
                }
            } else {
                mFile.delete();//已经有备份文件,直接删除之前的旧文件
            }
        }

        // Attempt to write the file, delete the backup and return true as atomically as
        // possible.  If any exception occurs, delete the new file; next time we will restore
        // from the backup.
        try {
            FileOutputStream str = createFileOutputStream(mFile);
            if (str == null) {
                mcr.setDiskWriteResult(false);
                return;
            }
            //将xml数据写入文件当中
            XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
            FileUtils.sync(str);
            str.close();
            ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
            try {
                final StructStat stat = Os.stat(mFile.getPath());
                synchronized (this) {//修改文件的时间戳和大小
                    mStatTimestamp = stat.st_mtime;
                    mStatSize = stat.st_size;
                }
            } catch (ErrnoException e) {
                // Do nothing
            }
            //写入完成,删除备份文件
            mBackupFile.delete();
            mcr.setDiskWriteResult(true);
            return;
        } catch (XmlPullParserException e) {
            Log.w(TAG, "writeToFile: Got exception:", e);
        } catch (IOException e) {
            Log.w(TAG, "writeToFile: Got exception:", e);
        }
        // Clean up an unsuccessfully written file
        if (mFile.exists()) {
            if (!mFile.delete()) {
                Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
            }
        }
        mcr.setDiskWriteResult(false);
    }

可以看到SharedPreferences的数据实际上有两层,在应用中使用的数据绝大部分都是内存中的数据,这样保证了操作的速度,内存中的数据修改都是在当前线程中进行,完成之后便可以使用最新数据,但是写入文件的操作有一点区别。
apply:在特定的单线程池中异步执行文件修改操作,频繁的apply将会导致IO操作串行排队进行。为了保证IO操作的完成,QueuedWork会在一些时机等待IO的操作完成,比方说activity的onPause,这样极端情况下会导致ANR。
commit:在调用线程中同步写入文件,如果当前在UI线程中进行,那么要注意卡顿的问题,一般来说如果不是必须等待返回值,采用apply更为合适。
此外如果之前的apply写入文件操作还在进行中,此时写入操作也会在apply的单线程池中进行,不过阻塞的依然是当前的执行线程,这个需要注意。

总结

SharedPreferences本质上是一个xml的文件存储。
个人不推荐SharedPreferences存储太大的数据,还是相对希望一个比较快的读取和写入效率,作为一个轻量级的存储机制。
SharedPreferences最好进行预加载,然后Editor在修改数据的时候最好在完成所有数据的修改之后再进行commit或者apply,免得造成频繁的IO操作。
commit和apply的时候注意阻塞的可能,一般来说采用apply即可,因为内存中的数据会同步更新,可以接着使用。除非关心本次IO写入操作的结果,可以考虑commit,但也不应该阻塞UI线程,可以考虑新建线程使用之,但是数据同步性需要注意。总之需要根据具体的场景灵活使用。
SharedPreferences不应该使用多进程模式。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,594评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,431评论 25 707
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,174评论 11 349
  • 发错啦发错啦发错啦发错啦-_-||-_-||
    王万锋阅读 138评论 0 0
  • 这个手臂我一直弄不好啊唉 这是原图
    goodnight_阅读 254评论 2 0