乐趣区

关于android:Android源码进阶之深入理解SharedPreference原理机制

一、SharedPreferences 简略应用

1、创立

第一个参数是贮存的 xml 文件名称,第二个是打开方式,个别就用

Context.MODE_PRIVATE;SharedPreferences sp=context.getSharedPreferences("名称", Context.MODE_PRIVATE);

2、写入

// 能够创立一个新的 SharedPreference 来对贮存的文件进行操作
SharedPreferences sp=context.getSharedPreferences("名称", Context.MODE_PRIVATE);
// 像 SharedPreference 中写入数据须要应用 Editor
SharedPreference.Editor editor = sp.edit();
// 相似键值对
editor.putString("name", "string");
editor.putInt("age", 0);
editor.putBoolean("read", true);
//editor.apply();
editor.commit();
  • apply 和 commit 都是提交保留,区别在于 apply 是异步执行的,不须要期待。不管删除,批改,减少都必须调用 apply 或者 commit 提交保留;
  • 对于更新:如果曾经插入的 key 曾经存在。那么将更新原来的 key;
  • 应用程序一旦卸载,SharedPreference 也会被删除;

3、读取

SharedPreference sp=context.getSharedPreferences("名称", Context.MODE_PRIVATE);
// 第一个参数是键名,第二个是默认值
String name=sp.getString("name", "暂无");
int age=sp.getInt("age", 0);
boolean read=sp.getBoolean("isRead", false);

4、检索

SharedPreferences sp=context.getSharedPreferences("名称", Context.MODE_PRIVATE);
// 查看以后键是否存在
boolean isContains=sp.contains("key");
// 应用 getAll 能够返回所有可用的键值
//Map<String,?> allMaps=sp.getAll();

5、删除

当咱们要革除 SharedPreferences 中的数据的时候肯定要先 clear()、再 commit(),不能间接删除 xml 文件;

SharedPreference sp=getSharedPreferences("名称", Context.MODE_PRIVATE);
SharedPrefence.Editor editor=sp.edit();
editor.clear();
editor.commit();
  • getSharedPreference() 不会生成文件,这个大家都晓得;
  • 删除掉文件后,再次执行 commit(),删除的文件会新生,新生文件的数据和删除之前的数据雷同;
  • 删除掉文件后,程序在没有齐全退出进行运行的状况下,Preferences 对象所存储的内容是不变的,尽管文件没有了,但数据仍然存在;程序齐全退出进行之后,数据才会失落;
  • 革除 SharedPreferences 数据肯定要执行 editor.clear(),editor.commit(),不能只是简略的删除文件,这也就是最初的论断,须要留神的中央

    二、SharedPreferences 源码剖析

1、创立

SharedPreferences preferences = getSharedPreferences("test", Context.MODE_PRIVATE);

实际上 context 的真正实现类是 ContextImp,所以进入到 ContextImp 的 getSharedPreferences 办法查看:

@Override
    public SharedPreferences getSharedPreferences(String name, int mode) {
        ......
        File file;
        synchronized (ContextImpl.class) {if (mSharedPrefsPaths == null) {
            // 定义类型:ArrayMap<String, File> mSharedPrefsPaths;
                mSharedPrefsPaths = new ArrayMap<>();}
            // 从 mSharedPrefsPaths 中是否可能失去 file 文件
            file = mSharedPrefsPaths.get(name);
            if (file == null) {// 如果文件为 null
            // 就创立 file 文件
                file = getSharedPreferencesPath(name);
                将 name,file 键值对存入汇合中
                mSharedPrefsPaths.put(name, file);
            }
        }
        return getSharedPreferences(file, mode);
    }

ArrayMap<String, File> mSharedPrefsPaths; 对象是用来存储 SharedPreference 文件名称和对应的门路,获取门路是在下列办法中,就是获取 data/data/ 包名 /shared_prefs/ 目录下的

@Override
public File getSharedPreferencesPath(String name) {return makeFilename(getPreferencesDir(), name + ".xml");
}
private File getPreferencesDir() {synchronized (mSync) {if (mPreferencesDir == null) {mPreferencesDir = new File(getDataDir(), "shared_prefs");
            }
            return ensurePrivateDirExists(mPreferencesDir);
        }
}

门路之后才开始创建对象

@Override
    public SharedPreferences getSharedPreferences(File file, int mode) {
    // 重点 1
        checkMode(mode);
    .......
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
        // 获取缓存对象(或者创立缓存对象)final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
            // 通过键 file 从缓存对象中获取 Sp 对象
            sp = cache.get(file);
            // 如果是 null,就阐明缓存中还没后该文件的 sp 对象
            if (sp == null) {
            // 重点 2:从磁盘读取文件
                sp = new SharedPreferencesImpl(file, mode);
                // 增加到内存中
                cache.put(file, sp);
                // 返回 sp
                return sp;
            }
        }
        // 如果设置为 MODE_MULTI_PROCESS 模式,那么将执行 SP 的 startReloadIfChangedUnexpectedly 办法。if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
            getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {sp.startReloadIfChangedUnexpectedly();
        }
        return sp;
    }

就是重载之前的办法,只是入参由文件名改为 File 了,给创立过程加锁了 synchronized,通过办法 getSharedPreferencesCacheLocked() 获取零碎中存储的所有包名以及对应的文件,这就是每个 sp 文件只有一个对应的 SharedPreferencesImpl 实现对象起因

流程:

  • 获取缓存区,从缓存区中获取数据,看是否存在 sp 对象,如果存在就间接返回
  • 如果不存在,那么就从磁盘获取数据,
  • 从磁盘获取的数据之后,增加到内存中,
  • 返回 sp;

getSharedPreferencesCacheLocked

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;
    }
  • getSharedPreferences(File file, int mode) 办法中,从下面的零碎缓存中分局 File 获取 - SharedPreferencesImpl 对象,如果之前没有应用过,就须要创立一个对象了,通过办法 checkMode(mode);
  • 先查看 mode 是否是三种模式,而后通过 sp = new SharedPreferencesImpl(file, mode);
  • 创建对象,并将创立的对象放到零碎的 packagePrefs 中,不便当前间接获取;

    SharedPreferencesImpl(File file, int mode) {
          mFile = file; // 存储文件
          // 备份文件(灾备文件)mBackupFile = makeBackupFile(file);
          // 模式
          mMode = mode;
          // 是否加载过了
          mLoaded = false;
          // 存储文件内的键值对信息
          mMap = null;
          // 从名字能够晓得是:开始加载数据从磁盘
          startLoadFromDisk();}
  • 次要是设置了几个参数,mFile 是原始文件;mBackupFile 是后缀.bak 的备份文件;
  • mLoaded 标识是否正在加载批改文件;
  • mMap 用来存储 sp 文件中的数据,存储时候也是键值对模式,获取时候也是通过这个获取,这就是示意每次应用 sp 的时候,都是将数据写入内存,也就是 sp 数据存储数据快的起因,所以 sp 文件不能存储大量数据,否则执行时候很容易会导致 OOM;
  • mThrowable 加载文件时候报的谬误;
  • 上面就是加载数据的办法 startLoadFromDisk(); 从 sp 文件中加载数据到 mMap 中

2、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;}
            // 备份文件是否存在,if (mBackupFile.exists()) {
            // 删除 file 原文件
                mFile.delete();
                // 将备份文件命名为:xml 文件
                mBackupFile.renameTo(mFile);
            }
        }
        .......
        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 (Exception e) {Log.w(TAG, "Cannot read" + mFile.getAbsolutePath(), e);
                } finally {IoUtils.closeQuietly(str);
                }
            }
        } catch (ErrnoException e) {/* ignore */}
        synchronized (mLock) {
        // 曾经加载结束,mLoaded = true;
            // 数据不是 null
            if (map != null) {
            // 将 map 赋值给全局的存储文件键值对的 mMap 对象
                mMap = map;
                // 更新内存的批改工夫以及文件大小
                mStatTimestamp = stat.st_mtime;
                mStatSize = stat.st_size;
            } else {mMap = new HashMap<>();
            }
            // 重点:唤醒所有以 mLock 锁的期待线程
            mLock.notifyAll();}
    }
  • 首先判断备份文件是否存在,如果存在,就更该备份文件的后缀名;接着就开始读取数据,而后将读取的数据赋值给全局变量存储文件键值对的 mMap 对象,并且更新批改工夫以及文件大小变量;
  • 唤醒所有以 mLock 为锁的期待线程;
  • 到此为止,初始化 SP 对象就算实现了,其实能够看进去就是一个二级缓存流程:磁盘到内存;

3、get 获取 SP 中的键值对

@Nullable
    public String getString(String key, @Nullable String defValue) {synchronized (mLock) { 锁判断
            awaitLoadedLocked(); // 期待机制
            String v = (String)mMap.get(key); // 从键值对中获取数据
            return v != null ? v : defValue;
        }
    }
 private void awaitLoadedLocked() {
        .......
        while (!mLoaded) { // 在加载数据结束的时候,值为 true
            try {
            // 线程期待
                mLock.wait();} catch (InterruptedException unused) {}}
    }

如果数据没有加载结束(也就是说 mLoaded=false),此时将线程期待;

4、putXXX 以及 apply 源码


public Editor edit() {
        // 跟 getXXX 原理一样
        synchronized (mLock) {awaitLoadedLocked();
        }
        // 返回 EditorImp 对象
        return new EditorImpl();}
 public Editor putBoolean(String key, boolean value) {synchronized (mLock) {mModified.put(key, value);
           return this;
         }
 }
       public void apply() {final long startTime = System.currentTimeMillis();
            // 依据名字能够晓得:提交数据到内存
            final MemoryCommitResult mcr = commitToMemory();
           ........
// 提交数据到磁盘中
            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
            // 重点:调用 listener
            notifyListeners(mcr);
        }
  • 先执行了 commitToMemory,提交数据到内存;而后提交数据到磁盘中;
  • 紧接着调用了 listener;

5、commitToMemory


private MemoryCommitResult commitToMemory() {
            long memoryStateGeneration;
            List<String> keysModified = null;
            Set<OnSharedPreferenceChangeListener> listeners = null;
            // 写到磁盘的数据汇合
            Map<String, Object> mapToWriteToDisk;
            synchronized (SharedPreferencesImpl.this.mLock) {if (mDiskWritesInFlight > 0) {mMap = new HashMap<String, Object>(mMap);
                }
                // 赋值此时缓存汇合给 mapToWriteToDisk 
                mapToWriteToDisk = mMap;
                .......
                synchronized (mLock) {
                    boolean changesMade = false;
                    // 重点:是否清空数据
                    if (mClear) {if (!mMap.isEmpty()) {
                            changesMade = true;
                            // 清空缓存中键值对信息
                            mMap.clear();}
                        mClear = false;
                    }
                    // 循环 mModified,将 mModified 中的数据更新到 mMap 中
                    for (Map.Entry<String, Object> e : mModified.entrySet()) {String k = e.getKey();
                        Object v = e.getValue();
                        // "this" is the magic value for a removal mutation. In addition,
                        // setting a value to "null" for a given key is specified to be
                        // equivalent to calling remove on that key.
                        if (v == this || v == null) {if (!mMap.containsKey(k)) {continue;}
                            mMap.remove(k);
                        } else {if (mMap.containsKey(k)) {Object existingValue = mMap.get(k);
                                if (existingValue != null && existingValue.equals(v)) {continue;}
                            }
                            // 留神:此时把键值对信息写入到了缓存汇合中
                            mMap.put(k, v);
                        }
.........
                    }
                    // 清空长期汇合
                    mModified.clear();
                   ......
                }
            }
            return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
                    mapToWriteToDisk);
        }
  • mModified 就是咱们本次要更新增加的键值对汇合;
  • mClear 是咱们调用 clear() 办法的时候赋值的;
  • 大抵流程就是:首先判断是否须要清空内存数据,而后循环 mModified 汇合,增加更新数据到内存的键值对汇合中;

6、commit 办法


public boolean commit() {
            .......
            // 更新数据到内存
            MemoryCommitResult mcr = commitToMemory();
            // 更新数据到磁盘
            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null /* sync write on this thread okay */);
            try {
            // 期待:期待磁盘更新数据实现
                mcr.writtenToDiskLatch.await();} catch (InterruptedException e) {return false;} finally {if (DEBUG) {Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                            + "committed after" + (System.currentTimeMillis() - startTime)
                            + "ms");
                }
            }
            // 执行 listener 回调
            notifyListeners(mcr);
            return mcr.writeToDiskResult;
        }
  • 首先 apply 没有返回值,commit 有返回值;
  • 其实 apply 执行回调是和数据写入磁盘并行执行的,而 commit 办法执行回调是期待磁盘写入数据实现之后;

二、QueuedWork 详解

1、QueuedWork

QueuedWork 这个类,因为 sp 的初始化之后就是应用,后面看到,无论是 apply 还是 commit 办法都是通过 QueuedWork 来实现的;

QueuedWork 是一个治理类,顾名思义,其中有一个队列,对所有入队的 work 进行治理调度;

其中最重要的就是有一个 HandlerThread

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

2、入队 queue

// 如果是 commit,则不能 delay,如果是 apply,则能够 delay
    public static void queue(Runnable work, boolean shouldDelay) {Handler handler = getHandler();
        synchronized (sLock) {sWork.add(work);
            if (shouldDelay && sCanDelay) {
                // 默认 delay 的工夫是 100ms
                handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
            } else {handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
            }
        }
    }

3、音讯的解决

private static class QueuedWorkHandler extends Handler {
        static final int MSG_RUN = 1;
        QueuedWorkHandler(Looper looper) {super(looper);
        }
        public void handleMessage(Message msg) {if (msg.what == MSG_RUN) {processPendingWork();
            }
        }
    }
    private static void processPendingWork() {synchronized (sProcessingWork) {
            LinkedList<Runnable> work;
            synchronized (sLock) {work = (LinkedList<Runnable>) sWork.clone();
                sWork.clear();
                getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
            }
            if (work.size() > 0) {for (Runnable w : work) {w.run();
                }
            }
        }
    }
  • 能够看到,调度非常简单,外部有一个 sWork,须要执行的时候遍历所有的 runnable 执行;
  • 对于 apply 操作,会有肯定的提早再去执行 work,然而对于 commit 操作,则会马上触发调度,而且并不仅仅是调度 commit 传过来的那个工作,而是马上就调度队列中所有的 work;

4、waitToFinish

零碎中很多中央会期待 sp 的写入文件实现,期待形式是通过调用 QueuedWork.waitToFinish();

public static void waitToFinish() {Handler handler = getHandler();
        synchronized (sLock) {
            // 移除所有音讯,间接开始调度所有 work
            if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {handler.removeMessages(QueuedWorkHandler.MSG_RUN);
            }
            sCanDelay = false;
        }
        StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
        try {
            // 如果是 waitToFinish 调用过去,则马上执行所有的 work
            processPendingWork();} finally {StrictMode.setThreadPolicy(oldPolicy);
        }
        try {
            // 在所有的 work 执行结束之后,还须要执行 Finisher
            // 后面在 apply 的时候有一步是 QueuedWork.addFinisher(awaitCommit);
            // 其中的实现是期待 sp 文件的写入实现
            // 如果没有通过 msg 去调度而是通过 waitToFinish,则那个 runnable 就会在这里被执行
            while (true) {
                Runnable finisher;
                synchronized (sLock) {finisher = sFinishers.poll();
                }
                if (finisher == null) {break;}
                finisher.run();}
        } finally {sCanDelay = true;}
        ...
    }

零碎中对于四大组件的解决逻辑都在 ActivityThread 中实现,在 service/activity 的生命周期的执行中都会期待 sp 的写入实现,正是通过调用 QueuedWork.waitToFinish(),确保 app 的数据正确的写入到 disk;

5、sp 应用的倡议

  • 对数据实时性要求不高,尽量应用 apply
  • 如果业务要求必须数据胜利写入,应用 commit
  • 缩小 sp 操作频次,尽量一次 commit 把所有的数据都写入结束
  • 能够适当思考不要在主线程拜访 sp
  • 写入 sp 的数据尽量轻量级

    总结:

SharedPreferences 的自身实现就是分为两步,一步是内存,一部是磁盘,而主线程又依赖 SharedPreferences 的写入,所以可能当 io 成为瓶颈的时候,App 会因为 SharedPreferences 变的卡顿,重大状况下会 ANR,总结下来有以下几点:

  • 寄存在 xml 文件中的数据会被装在到内存中,所以获取数据很快
  • apply 是异步操作,提交数据到内存,并不会马上提交到磁盘
  • commit 是同步操作,会期待数据写入到磁盘,并返回后果
  • 如果有同一个线程屡次 commit,则前面的要期待后面执行完结
  • 如果多个线程对同一个 sp 并发 commit,前面的所有工作会进入到 QueuedWork 中排队执行,且都要等第一个执行结束
退出移动版