SharedPreferencesde实现细节

650 阅读6分钟

这是我参与更文挑战的第1天,活动详情查看: 更文挑战

1. ContextImpl作用

SharedPreferences是一个接口,具体实现类是SharedPreferencesImpl。在Android源码中该类属于隐藏类,因此具体的对象获取需要通过Context。Application、Service和Activity都间接继承自Context,通过装饰模式,具体的操作交给ContextImpl对象。ContextImpl提供了getSharedPreferences方法来获取一个SharedPreferences对象。 我们知道了SharedPreferences对象是通过ContextImpl获取的,那么App中是如何保证获取的对象是同一个呢? 其实每一个App中,ContextImpl的数量为Activity个数+Service个数+Application,为了确保SharedPreferencesd的唯一性,在ContextImpl中通过一个静态集合来存储SharedPreferences对象。

private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;

ArrayMap中通过包名来细分应用的SharedPreferences。然后通过SharedPreferences的文件地址来获取具体的SharedPreferences对象。具体的存储路径为:

/data/user/0/com.mdy.sp/shared_prefs/${name}.xml

2. 数据 load 与 get

SharedPreferences的具体实现交给了SharedPreferencesImpl类。在创建SharedPreferencesImpl对象时,会调用startLoadFromDisk方法加载磁盘数据到内存中,内部通过创建一个单独的线程来执行loadFromDisk操作。

private void loadFromDisk() {
        synchronized (mLock) {
            // mLoaded = true表示数据已经被加载过了,所以直接返回
            if (mLoaded) { 
                return; 
            }
            //mBackupFile表示备份文件,备份文件存在的话,则删除源文件并替换
            if (mBackupFile.exists()) {
                mFile.delete();
                mBackupFile.renameTo(mFile);
            }
        }
        //通过IO的方式读取磁盘的xml文件解析到map集合中
        Map<String, Object> map = null;
        StructStat stat = null;
        Throwable thrown = null;
        try {
            stat = Os.stat(mFile.getPath());
            if (mFile.canRead()) {
                BufferedInputStream str = null;
                try {
                    str = new BufferedInputStream(
                            new FileInputStream(mFile), 16 * 1024);
                    map = (Map<String, Object>) XmlUtils.readMapXml(str);
                } catch (Exception e) {
                    Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
                } finally {
                    IoUtils.closeQuietly(str);
                }
            }
        } catch (ErrnoException e) {
            // An errno exception means the stat failed. Treat as empty/non-existing by
            // ignoring.
        } catch (Throwable t) {
            thrown = t;
        }
        //解析成功的话,将mLoaded置为true,并将map引用地址赋值给mMap
        synchronized (mLock) {
            mLoaded = true;
            mThrowable = thrown;

            try {
                if (thrown == null) {
                    if (map != null) {
                        mMap = map;
                        mStatTimestamp = stat.st_mtim;
                        mStatSize = stat.st_size;
                    } else {
                        mMap = new HashMap<>();
                    }
                }
            } catch (Throwable t) {
                mThrowable = t;
            } finally {
                //唤醒主线程
                mLock.notifyAll();
            }
        }
    }

loadFromDisk方法加载数据并解析,最后将引用地址赋值给SharedPreferencesImpl的mMap。调用get系列的方法获取缓存值时,实际是从mMap中获取缓存值。

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

get系列方法中会调用awaitLoadedLocked判断磁盘的读取是否结束,根据mLoaded的布尔值来确认,true表示完成,否则调用 mLock.wait()方法暂停主线程。这里就和上述的loadFromDisk方法最后调用的notifyAll方法对应上了,磁盘读取结束唤醒主线程。

3. 数据 缓存

SharedPreferences中数据的缓存需要通过Editor的实现类EditorImpl来实现。通过对应的get方法来缓存数据并调用commit或者apply提交。

3.1 内存同步

SharedPreferences中数据存储到磁盘之前,会首先调用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);
                }
                //mMap引用赋值给mapToWriteToDisk
                mapToWriteToDisk = mMap;
                //mDiskWritesInFlight值+1
                mDiskWritesInFlight++;

                synchronized (mEditorLock) {
                    boolean changesMade = false;
                    for (Map.Entry<String, Object> e : mModified.entrySet()) {
                        String k = e.getKey();
                        Object v = e.getValue();
                        // value 属于Editor或者为null,直接抛弃
                        if (v == this || v == null) {
                            if (!mapToWriteToDisk.containsKey(k)) {
                                continue;
                            }
                            mapToWriteToDisk.remove(k);
                        } else {
                        //当存在key时,根据value值是否一致来判断是否添加
                            if (mapToWriteToDisk.containsKey(k)) {
                                Object existingValue = mapToWriteToDisk.get(k);
                                if (existingValue != null && existingValue.equals(v)) {
                                    continue;
                                }
                            }
                            mapToWriteToDisk.put(k, v);
                        }
                        changesMade = true;
                    }
                    //mCurrentMemoryStateGeneration值+1
                    if (changesMade) {
                        mCurrentMemoryStateGeneration++;
                    }
                    //memoryStateGeneration值后面会传递给MemoryCommitResult,并在磁盘写入时,用来判断是否有磁盘正在写入
                    memoryStateGeneration = mCurrentMemoryStateGeneration;
                }
            }
            return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
                    mapToWriteToDisk);
        }

commitToMemory方法还是比较简单的,就是将新增的数据添加到mMap集合中,并生成一个MemoryCommitResult对象,后面会通过writeToFile方法写入磁盘。

3.2 commit提交

对于commit方法和apply方法的具体区别在什么地方,可能大家都会说,commit是同步提交,apply是单独开了一个线程提交。到底怎么回事,先看一下源码:

public boolean commit() {
            long startTime = 0;

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

            MemoryCommitResult mcr = commitToMemory();

            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null);
            try {
                mcr.writtenToDiskLatch.await();
            } catch (InterruptedException e) {
                return false;
            } finally {
                
            }
            notifyListeners(mcr);
            return mcr.writeToDiskResult;
        }

commitToMemory方法上面已经分析了是写入内存并返回mcr对象。之后调用enqueueDiskWrite方法写入磁盘,然后调用 mcr.writtenToDiskLatch.await()方法等待写入完成,最后返回结果mcr.writeToDiskResult。这里注意到调用enqueueDiskWrite方法时传入的postWriteRunnable参数是一个null对象。

3.3 apply提交

apply方法首先也是调用commitToMemory方法将数据写入内存,返回一个mcr对象。后续会构造一个postWriteRunnable对象,然后调用enqueueDiskWrite时传递进去。根据上述commit方法,我们可以知道commit方法传入的postWriteRunnable是一个null对象,而apply传入的是一个具体的Runnable对象。

3.4 enqueueDiskWrite

enqueueDiskWrite方法中根据传递的postWriteRunnable参数是否为null来区分commit和apply。

private void enqueueDiskWrite(final MemoryCommitResult mcr,final Runnable postWriteRunnable) {
        // commit提交时postWriteRunnable==null
        final boolean isFromSyncCommit = (postWriteRunnable == null);

        final Runnable writeToDiskRunnable = new Runnable() {
                @Override
                public void run() {
                    synchronized (mWritingToDiskLock) {
                        //具体的写入操作
                        writeToFile(mcr, isFromSyncCommit);
                    }
                    synchronized (mLock) {
                        //写入结束mDiskWritesInFlight值-1
                        mDiskWritesInFlight--;
                    }
                    if (postWriteRunnable != null) {
                        postWriteRunnable.run();
                    }
                }
            };
        // isFromSyncCommit==true 表示commit提交
        if (isFromSyncCommit) {
            boolean wasEmpty = false;
            synchronized (mLock) {
                wasEmpty = mDiskWritesInFlight == 1;
            }
            //wasEmpty==true表示mDiskWritesInFlight==1
            if (wasEmpty) {
                writeToDiskRunnable.run();
                return;
            }
        }
        // wasEmpty == false,执行入队操作
        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
    }

isFromSyncCommit为true时,表示commit提交,会根据mDiskWritesInFlight值是否为1,来判断是否有磁盘写入操作。在前面调用commitToMemory方法写入内存时,会将该值+1。若是有磁盘正在写入,那么mDiskWritesInFlight的值必定>1,则会将commit提交的任务添加到QueuedWork的队列中。磁盘写入操作结束,会将mDiskWritesInFlight-1,因此可以得出一个结论:commit方法提交数据时,会根据是否有磁盘写入操作在执行来区分,若无则在主线程进行写入操作,有的话就先将任务添加到QueuedWork中,然后再HandlerThread中执行任务

isFromSyncCommit为false时,表示apply提交。apply方法提交的任务会直接添加到QueuedWork中。要注意到quene的最后一个参数,根据!isFromSyncCommit来获取。

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

        synchronized (sLock) {
           // 任务添加到sWork中
            sWork.add(work);

            if (shouldDelay && sCanDelay) {
                //apply调用延迟方法,100ms
                handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
            } else {
                //commit调用
                handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
            }
        }
    }

queue方法中handler获取的是HandlerThread中的Looper对象。当是apply提交时,执行的是延迟方法,commit提交则是立刻发送Message到QueuedWorkHandlerhandleMessage方法执行,最终都会走到processPendingWork方法。

private static void processPendingWork() {
        long startTime = 0;

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

        synchronized (sProcessingWork) {
            LinkedList<Runnable> work;

            synchronized (sLock) {
                work = (LinkedList<Runnable>) sWork.clone();
                sWork.clear();

                // Remove all msg-s as all work will be processed now
                getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
            }
            //执行队列中的任务
            if (work.size() > 0) {
                for (Runnable w : work) {
                    w.run();
                }

            }
        }
    }

processPendingWork方法就是用于执行sWork中添加的任务,也就是执行我们传递的writeToDiskRunnable对象,最终会走到内部的writeToFile方法去写入磁盘。

源码分析到这里,发现SharedPreferences貌似也没啥大问题,那么它的问题到底出现在哪里呢?在下面:

 public static void waitToFinish() {
  processPendingWork();
 }

QueuedWork中结识了该方法会在Acitivity执行OnPause、BroadcastReceiver的onReceive之后,具体在ActivityThread中调用该方法。内部调用processPendingWork方法,但此时要注意到,这是在主线程调用的,而且内部执行的是磁盘的写入操作,如果waitToFinish超时了,就会导致ANR。

4. 总结

  1. 不需要返回结果时尽量采用apply方式提交。每个SharedPreferences对应的xml文件应该尽可能的小,这样磁盘写入的才会更快。
  2. SharedPreferences支持多线程,看看内部这么多的synchronized就知道了,但不支持多进程。

5. SharedPreferences如何支持多进程?

首先明确以下两点:

  • SharedPreferences不支持多进程,在多进程的访问的情况下无法实现数据的同步。
  • 设置SharedPreferences的mode = Context.MODE_MULTI_PROCESS时。当每次调用getSharedPreferences方法时,都会调用startLoadFromDisk方法从磁盘加载数据到内存,它的缺点在于依然无法实现数据同步,同时该mode已经被废弃。

虽然不推荐使用SharedPreferences来实现进程间通信,但万一面试遇到了呢;所以我们可以使用ContentProvider 来实现。实现ContentProvider时,内部数据存储采用SharedPreferences来实现,通过ContentProvider的CRUD方法来操作SharedPreferences的方法。其他进程通过Uri来访问SharedPreferences并执行数据的CRUD。但是在其他进程使用该SharedPreferences对象可能需要我们多封装一层,达到和正常使用SharedPreferences相似的体验。