SharedPreferences.apply()为什么没有返回结果

这是别人在面试过程中遇到的一个问题, SharedPreferences的apply()为什么没有返回结果.根据官方文档我们已经知道commit()是有返回结果的, apply()是没有返回结果的, 且官方推荐使用apply(),这个结论好像谁都知道. 为什么要这么设计呢?直接看源码

加载SP

    @Override
    public SharedPreferences getSharedPreferences(String name, int mode) {
        // At least one application in the world actually passes in a null
        // name.  This happened to work because when we generated the file name
        // we would stringify it to "null.xml".  Nice.
        if (mPackageInfo.getApplicationInfo().targetSdkVersion <
                Build.VERSION_CODES.KITKAT) {
            if (name == null) {
                name = "null";
            }
        }

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


    @Override
    public SharedPreferences getSharedPreferences(File file, int mode) {
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
            //获取当前包名对应的所有SP
            final ArrayMap cache = getSharedPreferencesCacheLocked();
            sp = cache.get(file);
            if (sp == null) {
                checkMode(mode); 
                //Android O在解锁前不能访问应用内部存储
                if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) {
                    if (isCredentialProtectedStorage()
                            && !getSystemService(UserManager.class)
                                    .isUserUnlockingOrUnlocked(UserHandle.myUserId())) {
                        throw new IllegalStateException("SharedPreferences in credential encrypted "
                                + "storage are not available until after user is unlocked");
                    }
                }
                //创建SP
                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) {
            // If somebody else (some other process) changed the prefs
            // file behind our back, we reload it. This has been the
            // historical (if undocumented) behavior.
            sp.startReloadIfChangedUnexpectedly();
        }
        return sp;
    }

getSharedPreferencesCacheLocked()方法使用ArrayMap保存当前包名对应的所有SP, sSharedPrefsCache实例是static修饰的,一直存在于内存中

    /**
     * Map from package name, to preference name, to cached preferences.
     */
    @GuardedBy("ContextImpl.class")
    private static ArrayMap> sSharedPrefsCache;


    @GuardedBy("ContextImpl.class")
    private ArrayMap getSharedPreferencesCacheLocked() {
        if (sSharedPrefsCache == null) {
            sSharedPrefsCache = new ArrayMap<>();
        }

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

        return packagePrefs;
    }

SP的构造方法里,调用startLoadFromDisk()方法,可以看到开了一个子线程去加载

    SharedPreferencesImpl(File file, int mode) {
        mFile = file;   //SP文件路径
        mBackupFile = makeBackupFile(file); //备份SP文件
        mMode = mode;
        mLoaded = false; //是否加载完成
        mMap = null;  //存放数据的Map
        startLoadFromDisk(); 
    }
    
    private void startLoadFromDisk() {
        synchronized (mLock) {
            mLoaded = false;
        }
        new Thread("SharedPreferencesImpl-load") {
            public void run() {   
                loadFromDisk(); //新开了一个子线程去加载磁盘上的SP
            }
        }.start();
    }
    private void loadFromDisk() {
        synchronized (mLock) {
            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 {
                    //读取SP的xml文件,以的形式保存到map里
                    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;  //到这里加载完成了
            if (map != null) {
                mMap = map;  //保存从SP中读取的map集合
                mStatTimestamp = stat.st_mtime; 
                mStatSize = stat.st_size;
            } else {
                mMap = new HashMap<>();
            }
            //这是用到notifyAll(), 肯定是和wait()成对出现的
            mLock.notifyAll();
        }
    }

这里使用了notifyAll(), 他和wait()肯定是成对出现的
查看源码,在SP的写入操作edit(), putXXX()和读取操作getXXX()时,都调用了awaitLoadedLocked()方法, 里面调用了wait()方法阻塞并释放了锁,等待子线程的loadFromDisk()完成后再继续自己的读写操作

    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.
            //这里没看懂,loadFromDisk()已经在子线程中了,还需要BlockGuard()检测??
            BlockGuard.getThreadPolicy().onReadFromDisk();
        }
        while (!mLoaded) { //如果load没有完成,将会阻塞在这里
            try {
                mLock.wait();  //释放了mLock锁,等待notifyAll()将唤醒
            } catch (InterruptedException unused) {
            }
        }
    }

读取

getString()为例,mMap就是上一步从SP的xml解析出来的,这里要注意了, mMap并非是实时磁盘上的结果. 这里要解释一下

SP写入的操作分为两步: 1. 写入内存 2. 写入磁盘
所以当你commit()或者apply()提交的时候, 在写入内存后, mMap就会发生改变,这时候你就可以通过getXXX来获取你要的结果, 而不用等待写入磁盘完成(写入磁盘是IO操作,在子线程完成的)
其实很好解释,在应用存活期间直接可以读取内存里的数据,并不需要等待写入磁盘完成. 在应用重启后,把持久化到磁盘里的内容写入内存,继续操作内存里的数据.

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

写入

写入分为commitapply

commit

        @Override
        public boolean commit() {
            long startTime = 0;

            if (DEBUG) {
                startTime = System.currentTimeMillis();
            }
             //1.提交到内存
            MemoryCommitResult mcr = commitToMemory();
            //2.加入写磁盘的任务队列;(这里有句注释,在当前线程同步写OK,等会看怎么实现的)
            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");
                }
            }
            notifyListeners(mcr);
            return mcr.writeToDiskResult;  //返回commit是否成功,apply是没有这一句的
        }
  1. commitToMemory()提交到内存
        private MemoryCommitResult commitToMemory() {
            long memoryStateGeneration;
            List keysModified = null;
            Set listeners = null;
            Map mapToWriteToDisk;

            synchronized (SharedPreferencesImpl.this.mLock) {
                // We optimistically don't make a deep copy until
                // a memory commit comes in when we're already
                // writing to disk.
                if (mDiskWritesInFlight > 0) {
                    // We can't modify our mMap as a currently
                    // in-flight write owns it.  Clone it before
                    // modifying it.
                    // noinspection unchecked
                    mMap = new HashMap(mMap); //进行深拷贝,为啥是深拷贝,因为SP保存的是基本数据类型和String
                }
                mapToWriteToDisk = mMap; //保存map的拷贝,可以理解为磁盘快照
                mDiskWritesInFlight++;  //写磁盘的任务+1

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

                synchronized (mEditorLock) {
                    boolean changesMade = false;
                    //如果调用了clear方法,就清空这个硬盘快照
                    if (mClear) {
                        if (!mapToWriteToDisk.isEmpty()) {
                            changesMade = true;
                            mapToWriteToDisk.clear();
                        }
                        mClear = false;
                    }

                    for (Map.Entry 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.
                        // 调用remove方法里传的mModified.put(key, this); 或者put方法里传的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); //覆盖原值
                        }
                        //修改完成标记 = true
                        changesMade = true;
                        if (hasListeners) {
                            keysModified.add(k); //保存修改了哪些key的值
                        }
                    }
                    //清空待修改内容
                    mModified.clear();
                    //修改一次计数器就+1,这里用的是long类型,防止次数过多越界
                    if (changesMade) {
                        mCurrentMemoryStateGeneration++;
                    }

                    memoryStateGeneration = mCurrentMemoryStateGeneration;
                }
            }
            return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
                    mapToWriteToDisk);
        }

每次提交到内存时,调用mLock锁, 深拷贝mMapmapToWriteToDisk中, 把putString,putInt等等创建的待修改的值mModified的值写入到mapToWriteToDisk, 使用mDiskWritesInFlight来统计写磁盘的任务数量

  1. enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable))加入写磁盘的任务队列
    注意第二个参数commit()的时候传的是null, apply()的时候传了一个postWriteRunnable
    private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
        //判断是否是同步提交 (commit同步,apply异步)
        final boolean isFromSyncCommit = (postWriteRunnable == null);
        //构建写入磁盘的任务writeToDiskRunnable 
        final Runnable writeToDiskRunnable = new Runnable() {
                @Override
                public void run() {
                    synchronized (mWritingToDiskLock) {
                        //3. 写入磁盘的操作
                        writeToFile(mcr, isFromSyncCommit); 
                    }
                    synchronized (mLock) {
                        mDiskWritesInFlight--;  //写入完成后,磁盘任务-1
                    }
                    if (postWriteRunnable != null) {
                        postWriteRunnable.run();  //调用apply()传入的postRunnable
                    }
                }
            };

        // Typical #commit() path with fewer allocations, doing a write on
        // the current thread.
        //如果是同步提交 且只有一个任务,直接调用run()方法在当前线程执行不用加入任务队列了
        if (isFromSyncCommit) {
            boolean wasEmpty = false;
            synchronized (mLock) {
                wasEmpty = mDiskWritesInFlight == 1;
            }
            if (wasEmpty) {
                writeToDiskRunnable.run();
                return;
            }
        }
        //如果不是同步提交,或者有多个任务,就加入任务队列
        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
    }

这里创建了写入磁盘的任务writeToDiskRunnable 并加入到了任务队列中, 写入完成后写磁盘任务减1mDiskWritesInFlight--, 查看QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit)方法

    public static void queue(Runnable work, boolean shouldDelay) {
        Handler handler = getHandler();

        synchronized (sLock) {
            sWork.add(work);
            //apply()的延迟100ms延迟
            if (shouldDelay && sCanDelay) {
                handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
            } else { //commit()的马上执行
                handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
            }
        }
    }

这里调用getHandler()创建了HandlerThread在子线程来串行的执行任务, 这里的shouldDelay == !isFromSyncCommit.结合前面enqueue的代码可以发现使用commit()的时候马上执行, apply()写入会有100ms的延迟. 这个延迟具有一定的优化作用,在writeToFile中会说明

  1. writeToFile(mcr, isFromSyncCommit) 写入磁盘的操作
  • 在看写操作前,先来看看apply()commit()是怎么处理写任务的队列的
    他们都是通过CountDownLatch来实现的

CountDownLatch是JDK提供的一个并发编程类,是通过一个计数器来实现的,计数器的初始值为线程的数量。每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。

每个等待被写入的MemoryCommitResult里都维护了一个计数器值为1的CountDownLatchnew CountDownLatch(1), 当我们调用mcr.writtenToDiskLatch.await()时,实际把线程阻塞在这里,写入完成后又调用了MemoryCommitResultsetDiskWriteResult()方法把计数器置为0,解除阻塞

  private static class MemoryCommitResult {
        final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);
        void setDiskWriteResult(boolean wasWritten, boolean result) {
            this.wasWritten = wasWritten;
            writeToDiskResult = result;
            writtenToDiskLatch.countDown();
        }
  }

不同的是commit()apply()阻塞的线程不同
查看mcr.writtenToDiskLatch.await()调用的位置. 在commit()enqueueDiskWrite()加入队列后,阻塞的是commit所在的线程

        @Override
        public boolean commit() {
            long startTime = 0;

            if (DEBUG) {
                startTime = System.currentTimeMillis();
            }

            MemoryCommitResult mcr = commitToMemory();

            SharedPreferencesImpl.this.enqueueDiskWrite(
                mcr, null /* sync write on this thread okay */);
            try {
                mcr.writtenToDiskLatch.await(); //这里await()发生在操作commit的线程
            } catch (InterruptedException e) {
                return false;
            } finally {
                if (DEBUG) {
                    Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                            + " committed after " + (System.currentTimeMillis() - startTime)
                            + " ms");
                }
            }
            notifyListeners(mcr);
            return mcr.writeToDiskResult;
        }

apply()中, mcr.writtenToDiskLatch.await()Runnable awaitCommit的run()方法中,这个runnable又是在enqueue的Runnable postWriteRunnable中被执行的,所以阻塞发生在队伍队列执行的线程HandlerThread

        @Override
        public void apply() {
            final long startTime = System.currentTimeMillis();

            final MemoryCommitResult mcr = commitToMemory();
            final Runnable awaitCommit = new Runnable() {
                    @Override
                    public void run() {
                        try {  //这里的runnable在下面的postWriteRunnable中被执行
                            mcr.writtenToDiskLatch.await(); 
                        } catch (InterruptedException ignored) {
                        }

                        if (DEBUG && mcr.wasWritten) {
                            Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                                    + " applied after " + (System.currentTimeMillis() - startTime)
                                    + " ms");
                        }
                    }
                };

            QueuedWork.addFinisher(awaitCommit);
            //postWriteRunnable在enqueue()后被任务队列调用
            Runnable postWriteRunnable = new Runnable() {
                    @Override
                    public void run() {
                        awaitCommit.run();
                        QueuedWork.removeFinisher(awaitCommit);
                    }
                };

            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

            // Okay to notify the listeners before it's hit disk
            // because the listeners should always get the same
            // SharedPreferences instance back, which has the
            // changes reflected in memory.
            notifyListeners(mcr);
        }
  • 写入之前的判断
// Only need to write if the disk state is older than this commit
if (mDiskStateGeneration < mcr.memoryStateGeneration) {
      if (isFromSyncCommit) {
           needsWrite = true;
      } else {
           synchronized (mLock) {
           // No need to persist intermediate states. Just wait for the latest state to be persisted.   
           if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
                  needsWrite = true;
           }
      }
}

needsWirte是用来决定是否写入的:

  1. 先对比磁盘版本和内存版本mDiskStateGeneration < mcr.memoryStateGeneration,磁盘版本小于内存版本才写入

  2. 对比当前要写入的内存版本和全局的内存版本mCurrentMemoryStateGeneration == mcr.memoryStateGeneration才写入.

  • 文件备份的作用
    在初始化loadFromDisk()和写入磁盘writeToFile()中都用到了文件备份mBackupFile,大概逻辑就是: 每次写入成功就会删除备份; 如果备份没有被删除,说明上次写入失败了,这时直接把mBackupFile覆盖到mFile里作为正式数据

apply

apply()的流程和commit()一样: 写入内存, 加入任务队列, 写入磁盘
看看不同点

  • 上面介绍commit()已经提到了apply()在执行任务队列的任务时,commit()的阻塞发生在调用他的线程中(一般是主线程),而apply()阻塞在工作线程中,执行完了才执行下一个.
        @Override
        public void apply() {
            final long startTime = System.currentTimeMillis();

            final MemoryCommitResult mcr = commitToMemory();
            final Runnable awaitCommit = new Runnable() {
                    @Override
                    public void run() {
                        try {
                            mcr.writtenToDiskLatch.await();
                        } catch (InterruptedException ignored) {
                        }
                    }
                };

            QueuedWork.addFinisher(awaitCommit);

            Runnable postWriteRunnable = new Runnable() {
                    @Override
                    public void run() {
                        awaitCommit.run();
                        QueuedWork.removeFinisher(awaitCommit);
                    }
                };

            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

            // Okay to notify the listeners before it's hit disk
            // because the listeners should always get the same
            // SharedPreferences instance back, which has the
            // changes reflected in memory.
            notifyListeners(mcr);
        }
  • 加入任务队列中,和commit相比多了一步操作QueuedWork.addFinisher(awaitCommit);apply()的任务正常写入时,又移除QueuedWork.removeFinisher(awaitCommit);
    这里的finiisher队列相当于缓存,写入前添加写入后删除. 存在这样一种情况当页面即将被销毁的时候工作队列sWork中的内容还没被完全写入,可以到 ActivityThread 中搜一下 QueuedWork.waitToFinish,会发现在 Activity/Service stop 的时候调用了QueuedWork.waitToFinish()来取出addFinisher加入的任务,保证在页面退出前SP全部写入
public final class ActivityThread extends ClientTransactionHandler {
    private void handleServiceArgs(ServiceArgsData data) {
          QueuedWork.waitToFinish();
    }
}
  • 为什么apply()加入队列的任务任务执行要加100ms的延迟handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);

因为apply()对应的就是异步的情况,主线程不会像commit()一样阻塞来等待结果.
但是当频繁apply的时候,如果前后的 apply 间隔小于 100 毫秒,由于mCurrentMemoryStateGeneration是实时更新到内存的, 那么这个条件判断只在最后的写任务会为 true,从而避免了过多的无用的IO操作

总结

  • SP由ContextImpl中的private static ArrayMap> sSharedPrefsCache;维护,他是静态的一直存在于内存中
  • 不管是commit()还是apply()写入, 都是分为两步的:1.写内存 2.写磁盘
    在写内存完成后mMap就会发生改变,这时我们就能通过getXXX来获得结果而不需要等待写磁盘完成.
  • 写磁盘都是通过系统 QueueWork工作队列完成的,他内部维护了一个HandlerThread来执行每次写入内存后的结果作为任务. 不同的是commit()会阻塞住当前的线程,直到写入完成解除阻塞,所以当前线程能感知到写入是否完成的时机,也就是有返回值. 而apply()阻塞的是工作的线程HandlerThread,解除阻塞也在HandlerThread,当前线程是无法得知什么时候结束的.所以apply()无法返回结果. 这也符合文档里的说法: commit()是同步执行的,队列里的每个任务要等待上个任务写入磁盘成功解除阻塞. 而apply()不会阻塞当前线程,且不是每个写内存的结果都会被写入磁盘的,apply()通过延迟100ms执行和对比最新的内存写入版本来过滤掉不必要的写磁盘任务.
  • apply()由于是异步的,在Activity以及 Service 处理 onStoponStartCommand 时,执行 QueuedWork.waitToFinish() 等待所有的等待锁释放,他里面维护了一个sFinishers队列LinkedList sFinishers = new LinkedList<>(),会通过while循环不停的取出队列头部的任务直到任务执行完成为止.这有可能会引发ANR问题.更详细的请参照apply 引起的 ANR 问题

总结

commit()通过阻塞你当前线程,来拿到HandlerThread内的磁盘IO结果作为返回值.
apply()的写磁盘操作是完全异步的, 你当前线程可以继续处理你的事情不用在这里阻塞等待结果.现在使用commit会有提示使用apply的提示. 这是因为:

  1. 不管是commit还是apply,你使用getXXX的时候使用的内存里的结果, 和是否写入磁盘成功没有任何关系
  2. 单次的磁盘持久化成功对于 下次启动应用使用 没有任何影响,谁能保证你存在磁盘上的东西不会被修改呢不会被删除呢, 我们每次getXXX前, 还是要做判断是否存在的

你可能感兴趣的:(SharedPreferences.apply()为什么没有返回结果)