SharedPreferences详情分析

  • 创建

    val prefs = getSharedPreferences("myPrefs",Context.MODE_PRIVATE)
    
    

    通过上下文创建的逻辑在ContextImpl中:

    @Override
    public SharedPreferences getSharedPreferences(String name, int mode) {
        ...
        File file;
        synchronized (ContextImpl.class) {
            if (mSharedPrefsPaths == null) {
                mSharedPrefsPaths = new ArrayMap<>();
            }
            file = mSharedPrefsPaths.get(name);
            if (file == null) {
                file = getSharedPreferencesPath(name);
                mSharedPrefsPaths.put(name, file);
            }
        }
        return getSharedPreferences(file, mode);
    }
    
    

    这里用缓存保存File,第一次则使用getSharedPreferencesPath方法创建,注意mSharedPrefsPaths是私有变量,因此每一个ContextImpl都有一个,也就是说每个Activity第一次获取SharedPreferences时都会创建一个:

    @Override
    public File getSharedPreferencesPath(String name) {
        return makeFilename(getPreferencesDir(), name + ".xml");
    }
    
    

    getPreferencesDir方法最终会从以下三个目录中的一个选为存储目录,而SharedPreferences的文件名则会“$name.xml”:

    File res = null;
    if (isCredentialProtectedStorage()) {
        res = mPackageInfo.getCredentialProtectedDataDirFile();
    } else if (isDeviceProtectedStorage()) {
        res = mPackageInfo.getDeviceProtectedDataDirFile();
    } else {
        res = mPackageInfo.getDataDirFile();
    }
    
    
    private File makeFilename(File base, String name) {
        if (name.indexOf(File.separatorChar) < 0) {
            final File res = new File(base, name);
            ...
            return res;
        }...
    }
    
    

    文件创建好了之后就会调用getSharedPreferences(File file, int mode)方法:

    @Override
    public SharedPreferences getSharedPreferences(File file, int mode) {
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
            final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
            sp = cache.get(file);
            if (sp == null) {
                checkMode(mode);
                ...
                sp = new SharedPreferencesImpl(file, mode);
                cache.put(file, sp);
                return sp;
            }
        }
        ...
        return sp;
    }
    
    

    这里也有一个SharedPreferencesImpl的缓存,getSharedPreferencesCacheLocked方法中按照包名保存了一个集合,对应着文件和SharedPreferencesImpl实例:

    private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
        if (sSharedPrefsCache == null) {
            sSharedPrefsCache = new ArrayMap<>();
        }
    
        final String packageName = getPackageName();
        ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
        if (packagePrefs == null) {
            packagePrefs = new ArrayMap<>();
            sSharedPrefsCache.put(packageName, packagePrefs);
        }
    
        return packagePrefs;
    }
    
    

    前面我们说到mSharedPrefsPaths保存的存在周期是当前上下文,而这里缓存的sSharedPrefsCache却是一个全局变量,因此只要是获取过SharedPreferences之后这个SharedPreferencesImpl和file会一直存在于内存中,直到进程结束。

  • 编辑

    我们知道,编辑SharedPreferences需要调用edit方法获取Editor,现在我们知道实际上调用的是SharedPreferencesImpl的edit方法:

    @Override
    public Editor edit() {
        ...
        synchronized (mLock) {
            awaitLoadedLocked();
        }
    
        return new EditorImpl();
    }
    
    
    private void awaitLoadedLocked() {
        ...
        while (!mLoaded) {
            try {
                mLock.wait();
            } catch (InterruptedException unused) {
            }
        }
          ...
    }
    
    

    我们看到这里会产生一个阻塞,等待mLoaded变成true,mLoaded又是什么标志呢?在前面SharedPreferencesImpl的构造方法中调用了一个startLoadFromDisk方法:

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

    loadFromDisk方法内可以概括成从SharedPreferences文件中读取数据到mMap中,然后修改mLoaded标志为true,因为loadFromDisk是异步调用的,所以前面要堵塞等待数据读取完成才能返回EditorImpl,那为什么需要先读取数据呢?因为下面的编辑需要用到mMap。

    @Override
    public Editor putString(String key, @Nullable String value) {
        synchronized (mEditorLock) {
            mModified.put(key, value);
            return this;
        }
    }
    
    

    以EditorImpl的一个putString方法为例,可以看到会把要设置的数据放到mModified中,mModified是一个HashMap,没什么好说的,编辑的主要逻辑在于提交操作。

    我们知道提交数据有commit和apply两个方法,前者是同步提交,后者是异步提交,它俩的通用伪代码如下:

    @Override
    public void 《apply和commit通用伪代码方法》() {
        ...
        final MemoryCommitResult mcr = commitToMemory();
        ...
        //apply方法会传postWriteRunnable,commit会传null
        SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
          //apply会通过子线程回调这句,commit则直接调用
          mcr.writtenToDiskLatch.await();
          ...
        notifyListeners(mcr);
    }
    
    

    其实apply就是多了一步把commit的操作放到了子线程队列中:

    private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
        final boolean isFromSyncCommit = (postWriteRunnable == null);
    
        final Runnable writeToDiskRunnable = new Runnable() {
                @Override
                public void run() {
                    synchronized (mWritingToDiskLock) {
                        writeToFile(mcr, isFromSyncCommit);
                    }
                    synchronized (mLock) {
                        mDiskWritesInFlight--;
                    }
                    if (postWriteRunnable != null) {
                        postWriteRunnable.run();
                    }
                }
            };
    
        // Typical #commit() path with fewer allocations, doing a write on
        // the current thread.
        if (isFromSyncCommit) {
            boolean wasEmpty = false;
            synchronized (mLock) {
                wasEmpty = mDiskWritesInFlight == 1;
            }
            if (wasEmpty) {
                writeToDiskRunnable.run();
                return;
            }
        }
    
        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
    }
    
    

    queue方法:

    public static void queue(Runnable work, boolean shouldDelay) {
        Handler handler = getHandler();
    
        synchronized (sLock) {
            sWork.add(work);
    
            if (shouldDelay && sCanDelay) {
                handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
            } else {
                handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
            }
        }
    }
    
    

    getHandler会开启一个子线程来存放异步apply消息:

    private static Handler getHandler() {
        synchronized (sLock) {
            if (sHandler == null) {
                HandlerThread handlerThread = new HandlerThread("queued-work-looper",
                        Process.THREAD_PRIORITY_FOREGROUND);
                handlerThread.start();
    
                sHandler = new QueuedWorkHandler(handlerThread.getLooper());
            }
            return sHandler;
        }
    }
    
    

    上面的handler就是QueueWorkHandler,可以看到它用的是handlerThread这个子线程的Looper,因此apply是开启了子线程来处理的。这里注意是先调用了start方法开启线程,因为这里的Looper我们需要依附于子线程,因此需要在子线程中调用Looper.prepare方法,也因此HandlerThread中初始化Looper的地方放在了run方法中:

    @Override
    public void run() {
        mTid = Process.myTid();
        Looper.prepare();
        synchronized (this) {
            mLooper = Looper.myLooper();
            notifyAll();
        }
        Process.setThreadPriority(mPriority);
        onLooperPrepared();
        Looper.loop();
        mTid = -1;
    }
    
    

    言归正传,不管是writeToDiskRunnable直接run还是通过子线程队列回调回来run,最终都会执行到writeToDiskRunnable的run方法中,从而执行writeToFile方法:

    private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
        ...
        try {
            FileOutputStream str = createFileOutputStream(mFile);
                  ...
            XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
            ...
            str.close();
              //设置的mode参数的用处
              ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
            ...
            mcr.setDiskWriteResult(true, true);
            ...
            return;
        } ...
        mcr.setDiskWriteResult(false, false);
    }
    
    

    可以看到,主要是调用XmlUtils的writeMapXml方法往mFile写入,内部使用了缓冲,最后会调用FastXmlSerializer的endDocument方法,进而调用它的flush方法:

    public void flush() throws IOException {
        ...
        mWriter.write(mText, 0, mPos);
        mWriter.flush();
          ...
    }
    
    

    在写入过程中,整个IO操作都没有开启子线程,这意味着如果在主线程中使用EditorImpl提交是会堵塞主线程的。

  • 数据逻辑

    回过头来看,在调用enqueueDiskWrite方法之前,二者都会先调用一个commitToMemory方法:

    private MemoryCommitResult commitToMemory() {
        ...
        Map<String, Object> mapToWriteToDisk;
        synchronized (SharedPreferencesImpl.this.mLock) {
              ...
            mapToWriteToDisk = mMap;
            ...
            synchronized (mEditorLock) {
                ...
                for (Map.Entry<String, Object> e : mModified.entrySet()) {
                    String k = e.getKey();
                    Object v = e.getValue();
                      //value不能设置为当前EditorImpl本身和null
                    if (v == this || v == null) {
                        if (!mapToWriteToDisk.containsKey(k)) {
                            continue;
                        }
                        mapToWriteToDisk.remove(k);
                    } else {
                          //这里就是更新已有数据的地方
                        if (mapToWriteToDisk.containsKey(k)) {
                            Object existingValue = mapToWriteToDisk.get(k);
                            if (existingValue != null && existingValue.equals(v)) {
                                continue;
                            }
                        }
                        mapToWriteToDisk.put(k, v);
                    }
                      ...
                }
                mModified.clear();
                  ...
            }
        }
        return new MemoryCommitResult(memoryStateGeneration, keysCleared, keysModified,
                listeners, mapToWriteToDisk);
    }
    
    

    可见这里是处理本次提交的数据的地方,最后MemoryCommitResult会保存本次要提交的最终数据,也就是上面出现的mcr.mapToWriteToDisk。

  • 总结

    由此,我们可以总结出SharedPreferences的几点不足:

    1. SharedPreferencesImpl的缓存使用了static全局容器存储,这会增加内存消耗;
    2. EditorImpl进行数据保存时会有IO操作,而这部分操作并没有在内部开启子线程处理,这就有堵塞主线程的使用风险;
    3. 每次数据更新都是全量更新,通常每次都只更新一个数据值,却需要整个xml文件数据的整体写入,这就造成了资源的浪费;
    4. 设置了很多锁来保证同步,但因此造成了多线程的读写效率低下问题。

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
在这里插入图片描述
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

全套视频资料:

一、面试合集

在这里插入图片描述
二、源码解析合集
在这里插入图片描述

三、开源框架合集
在这里插入图片描述
欢迎大家一键三连支持,若需要文中资料,直接扫描文末CSDN官方认证微信卡片免费领取↓↓↓

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值