SharedPreferences源码分析学习

简单使用

SharedPreferences sharedPreferences = getSharedPreferences("ouwen", Context.MODE_PRIVATE);
SharedPreferences.Editor edit = sharedPreferences.edit();
edit.putString("ouwen","123456");
保存数据:edit.apply(); 或者 edit.commit()
读取数据:sharedPreferences.getString("ouwen","");

通常查看源码都是带着问题去分析, 避免在源码里面迷失了,

那么关于SharedPreferences的几点问题:

  • 怎么保存数据的?
  • 怎么读取数据的?
  • 它是线程安全的吗?
  • apply和commit都可以提交数据,那么有什么区别?
  • 有什么需要注意的吗?要怎么优化?

怎么保存数据的?

先说结论再看源码分析
本质是把Map中的数据转成xml文件, 以键值对的方式存储在本地

  • 第一步: 获取SharedPreferences实例
  • 第二步: 创建一个Map把我们当前修改的数据存储在内存中
  • 第三步: 调用commit()或者apply() 把内存中的数据存储到本地

getSharedPreferences()源码

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

mBase: 是ContextImpl
传入一个name和mode, 首先会尝试从mSharedPrefsPaths中获取name在本地的File, 如果为null, 就调用getSharedPreferencesPath(name) 创建一个name +".xml"的文件并且加入mSharedPrefsPaths缓存中, 继续调用getSharedPreferences(file,mode)

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

为了简洁容易看, 删除了部分代码

  1. 先调用getSharedPreferencesCacheLocked()获取File对应的缓存cache, 这个ArrayMap是ConextImpl里的一个静态成员属性
  2. 第一次缓存是null, 所以会调用new SharedPreferencesImpl(file, mode) 创建一个实例加入缓存中并且返回

然后调用edit()方法获取Editor的实例EditorImpl

 @Override
    public Editor edit() {
        ...
        synchronized (mLock) {
            awaitLoadedLocked();  这里会阻塞等待,直到读取文件完成
        }
        return new EditorImpl();
    }

public final class EditorImpl implements Editor {
        ...
        private final Map mModified = new HashMap<>();
        @Override
        public Editor putString(String key, @Nullable String value) {
            synchronized (mEditorLock) {
                mModified.put(key, value);
                return this;
            }
        }
        @Override
        public Editor putInt(String key, int value) {
            synchronized (mEditorLock) {
                mModified.put(key, value);
                return this;
            }
        }
        ...
}

拿到SharedPreferences.Editor后就可以往里面putString(), putInt 等等...

哦~~~原来我们put的数据都是保存在mModified这个Map集合里面了,
这个时候数据还是在内存里面, 只有调用commit()或者apply()才会保存到本地文件, 那就继续看源码吧

先对比一下commit() 和 apply() 源码上有什么区别?

public boolean commit() {
            ... 
            MemoryCommitResult mcr = commitToMemory(); 
            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null);
            try {
                mcr.writtenToDiskLatch.await();
            } catch (InterruptedException e) {
                return false;
            }
            ...
            notifyListeners(mcr);
            return mcr.writeToDiskResult;
}

 public void apply() {
            ...
            final MemoryCommitResult mcr = commitToMemory();
            final Runnable awaitCommit = new Runnable() {
                    @Override
                    public void run() {
                        try {
                            mcr.writtenToDiskLatch.await();  等待提交完成
                        } catch (InterruptedException ignored) {
                        }
                    }
                };
            //把这个等待提交的Runnable, 加入到QueuedWork中
            QueuedWork.addFinisher(awaitCommit);
            Runnable postWriteRunnable = new Runnable() {
                    @Override
                    public void run() {
                        awaitCommit.run(); 执行上面的Runnable
                        //从QueuedWork中移出等待提交的Runnable
                        QueuedWork.removeFinisher(awaitCommit); 
                    }
                };
            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
            notifyListeners(mcr);
        }

相同点:

  • 都会调用commitToMemory(), 然后调用enqueueDiskWrite()把Map的数据写入本地文件中

不同点:

  • commit() 有返回值, 可以知道成功还是失败
  • apply() 没有返回值, 并且多了2个Runnable

commitToMemory()方法会遍历之前put数据的mModified这个Map, 把我们修改的数据同步到另外一个Map中, 并且清空mModified自己

enqueueDiskWrite()

 private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {

        commit()方法调用enqueueDiskWrite的时候, 这个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();
                    }
                }
            };
        if (isFromSyncCommit) {
            boolean wasEmpty = false;
            synchronized (mLock) {
                wasEmpty = mDiskWritesInFlight == 1;
            }
            if (wasEmpty) {
                writeToDiskRunnable.run();  这里会在主线程执行, 写入本地文件
                return;
            }
        }

        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

创建一个writeToDiskRunnable里面会执行当writeToFile()把数据写入文件
调用commit的时候, isFromSyncCommit = true, 所以会当前线程执行
调用apply的时候会把写入文件的Runnable加入QueuedWork.queue()中执行, 其实是把Runnable发送到HandlerThread的子线程中执行

 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);
            }
        }
}

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;
        }
}

怎么读取数据的?

我们的程序启动后,当在某个地方想从SharedPreferences 里面获取数据的时候
会调用下面代码:

SharedPreferences sharedPreferences = getSharedPreferences("name", Context.MODE_PRIVATE);
sharedPreferences.getString(key,"");
  • 获取SharedPreferences实例
  • 调用getString(key,"") 方法获取数据

第一次调用的时候, 会调用new SharedPreferencesImpl()来创建SharedPreferences 的实例, 也就是在SharedPreferencesImpl的构造方法里, 会从本地的xml文件加载数据到内存

 SharedPreferencesImpl(File file, int mode) {
        ...
        mLoaded = false;
        mMap = null;
        startLoadFromDisk();
}

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

private void loadFromDisk() {
        synchronized (mLock) {
            if (mLoaded) {
                return;
            }
            ...
        }
        try {
           ...
           BufferedInputStream str = new BufferedInputStream(
                            new FileInputStream(mFile), 16 * 1024);
           map = (Map) XmlUtils.readMapXml(str);
        } catch (Throwable t) {
            thrown = t;
        }

        synchronized (mLock) {
            mLoaded = true;
            try {
                if (thrown == null) {
                    if (map != null) {
                        mMap = map;
                    } else {
                        mMap = new HashMap<>();
                    }
                }
            } catch (Throwable t) {
                mThrowable = t;
            } finally {
                mLock.notifyAll();  加载完成, 通知正在等待的代码继续执行
            }
        }
    }

startLoadFromDisk()方法会启动一个线程, 然后异步调用loadFromDisk()把xml文件的内容加载到内存Map中

思考: new Thread? 为啥不用线程池呢?
其实不是所有需要创建线程都用线程池来实现, 当我们确认了调用的次数很少的时候,直接new Thread也是可以的

 public String getString(String key, @Nullable String defValue) {
        synchronized (mLock) {
            awaitLoadedLocked();  阻塞等待加载文件完成
            String v = (String)mMap.get(key);
            return v != null ? v : defValue;
        }
}

思考: 读取本地文件是异步的, 那么如果保证数据正确?
在getString()方法中可以看到, 先会调用awaitLoadedLocked(), 再从mMap中根据key获取value. 这个awaitLoadedLocked()会判断当前文件是否下载完成, 如果没有完成就阻塞等待; 上面异步加载文件的loadFromDisk()方法, 在加载完成后会调用mLock.notifyAll()通知这里继续执行

 private void awaitLoadedLocked() {
        ...
        while (!mLoaded) {
            try {
                mLock.wait();
            } catch (InterruptedException unused) {
            }
        }
    }

有什么需要注意的吗? 要怎么优化?

SP表示SharedPreferences

1.apply()也有可能导致卡顿甚至ANR

  final Runnable awaitCommit = new Runnable() {
                    @Override
                    public void run() {
                        try {
                            mcr.writtenToDiskLatch.await();  等待提交完成
                        } catch (InterruptedException ignored) {
                        }
                    }
                };
      //把这个等待提交的Runnable, 加入到QueuedWork中
      QueuedWork.addFinisher(awaitCommit);

apply()虽然在子线程写入文件, 但是在提交的时候会把awaitCommit加入到QueuedWork中, 并且在ActivityThread.handlePauseActivity(), ActivityThread.handleStopActivity(), ActivityThread.handleStopService(), ActivityThread.handleSleeping() 等等多处地方都会调用QueuedWork.waitToFinish()去阻塞等待SP保存数据完成, 也就是说如果apply()执行写入文件, Activity会等待它执行完成才能关闭页面...

更多关于 apply() 的问题, 可以看一下字节跳动技术团队的这篇文章

2.不要保存比较大的内容, 会导致卡顿
因为SP加载的key和value以及put的内容, 会一直保存在内存当中,占有内存
第一次从SP中获取数据的时候, 如果文件过大可能会导致卡顿,虽然读取文件是在子线程中执行, 但是看getString的源码知道, 会调用awaitLoadedLocked阻塞在当前线程等读取文件完成

优化: 可以提前在子线程先初始化SP, 过一段时间再去getString()获取数据

3.不相关的配置, 不要保存在同一个文件中
因为文件越大加载越慢, 从而导致保存在内存的数据越多, 如果有的配置用的比较频繁,有的又很少用到,那分开文件存储可以提高一点性能(文件比较小可以忽略)

4.避免频繁提交, 尽量批量修改一起提交
因为写入本地文件的时候会加锁, 并且可能阻塞线程, 所以要避免频繁提交,提交使用apply()

5.最好不要用来跨进程使用
SP无法保证跨进程使用时数据正常

如果项目对性能要求比较高, 或者想要跨进程使用
可以看下微信的MMKV, 使用也很简单, 听说性能高,稳定性强(暂时没用过= =)
微信MMKV项目地址

小结:

  • 怎么保存数据的?
    修改的数据会存储内存Map中,当调用commit或者apply,就把数据写入本地文件

  • 怎么读取数据的?
    第一次会创建子线程从本地文件读取到内存Map中, 所以第一次稍微慢一点, 后面的都在内存操作所以比较快

  • 它是线程安全的吗?
    是线程安全的, 因为所有的读和写操作都加了synchronized,保证线程安全

  • apply和commit都可以提交数据,有什么区别?
    commit有返回值,可以马上知道提交结果, 但是commit是在当前线程执行写入文件操作的
    apply没有返回值, 是在HandlerThread的子线程中写入到文件

你可能感兴趣的:(SharedPreferences源码分析学习)