美文网首页
SharedPreferences存取数据流程分析

SharedPreferences存取数据流程分析

作者: 嗯哼嗯哼嗯哼嗯哼 | 来源:发表于2019-12-14 18:01 被阅读0次

    SharedPreferences存取数据流程分析

    SharedPreferencesImpl

    今天研究一下SharedPreferences存取数据的实现,在Android中SharedPreferences是一个接口,真正的实现类是SharedPreferencesImpl,下面就开始分析SharedPreferencesImpl,首先查看相关的属性

    private final File mFile;// 存放数据的文件
    private final File mBackupFile;// 更新数据时的备份文件
    private final int mMode; //sharedPreferences的模式
    private final Object mLock = new Object();// 锁
    private final Object mWritingToDiskLock = new Object(); // 写入磁盘的锁
    private Map<String, Object> mMap; // 存放的数据映射
    private int mDiskWritesInFlight = 0; // 排队写入磁盘的任务数
    private long mDiskStateGeneration; // 最后一次提交写入磁盘的state,自增的
    private long mCurrentMemoryStateGeneration; // 当前内存数据的state, 自增的
    private boolean mLoaded = false; // 是否已经在加载文件的数据
    

    下面在看下构造函数

    SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        mBackupFile = makeBackupFile(file); //构造出备份文件
        mMode = mode;
        mLoaded = false;//置为false
        mMap = null; 
        mThrowable = null;
        startLoadFromDisk(); //开始加载文件数据
    }
    
    //开个线程做加载数据的操作
    private void startLoadFromDisk() {
        synchronized (mLock) {
        // mLoaded置为false
            mLoaded = false;
        }
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                loadFromDisk();
            }
        }.start();
    }
    
    //构造个备份文件
    static File makeBackupFile(File prefsFile) {
        return new File(prefsFile.getPath() + ".bak");
    }
    
    

    下面再看下真正加载数据的方法,loadFromDisk()

    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<String, Object> map = null;
        StructStat stat = null;
        Throwable thrown = null;
        try {
            stat = Os.stat(mFile.getPath());
            if (mFile.canRead()) {
                BufferedInputStream str = null;
                try {
                // 构造存放文件的数据流,从mFile读数据
                    str = new BufferedInputStream(
                            new FileInputStream(mFile), 16 * 1024);
                            // 读取数据生成map
                    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) {
           ...
        } catch (Throwable t) {
            thrown = t;
        }
    
        synchronized (mLock) {
        // mLoaded置为true
            mLoaded = true;
            mThrowable = thrown;
    
            // It's important that we always signal waiters, even if we'll make
            // them fail with an exception. The try-finally is pretty wide, but
            // better safe than sorry.
            try {
            // 如果正常加载完成,没有异常
                if (thrown == null) {
                // 如果数据不为空
                    if (map != null) {
                        mMap = map;//把加载的数据赋值给mMap
                        mStatTimestamp = stat.st_mtim;
                        mStatSize = stat.st_size;
                    } else {
                    // 数据为空则初始化一个mMap
                        mMap = new HashMap<>();
                    }
                }
                // In case of a thrown exception, we retain the old map. That allows
                // any open editors to commit and store updates.
            } catch (Throwable t) {
                mThrowable = t;
            } finally {
            // 加载完成,唤醒等待mLock的线程
                mLock.notifyAll();
            }
        }
    }
    
    1. 从mFile中加载数据,这个过程只加载一次,如果已经加载过了,则不再加载
    2. 如果加载的过程中,发现备份文件还存在,那么就说明上次 新建/更新 数据时发生了异常,此时mFile文件的数据得不到保障,所以继续采用备份文件的数据,放弃上次的操作
    3. 如果mFile里面没有数据,则重新初始化mMap,如果有数据,则把读取出来的数据赋值给mMap
    4. 数据读取成功后,唤醒等待加载数据的线程

    因为SharedPreferences里面只有读取数据的方法,下面看下getInt()

    @Override
    public int getInt(String key, int defValue) {
        synchronized (mLock) {
            awaitLoadedLocked();//判断数据是否加载完成,没有加载完成,则阻塞等待
            Integer v = (Integer)mMap.get(key);//从mMap中去数据
            return v != null ? v : defValue;
        }
    }
    
    @GuardedBy("mLock")
    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.
            BlockGuard.getThreadPolicy().onReadFromDisk();
        }
        while (!mLoaded) {
            try {
                mLock.wait();
            } catch (InterruptedException unused) {
            }
        }
        if (mThrowable != null) {
            throw new IllegalStateException(mThrowable);
        }
    }
    
    1. 首先判断数据是否加载完成,如果没有加载完成,则阻塞当前线程。加载完成后会被加载数据线程唤醒
    2. 数据加载完成后,从mMap中去数据
    3. 如果mMap中有数据则返回,没有的话返回默认值

    其他的读取数据的方法也是类似的,还有另外一个方法需要注意下

    @Override
    public Map<String, ?> getAll() {
        synchronized (mLock) {
            awaitLoadedLocked();
            //noinspection unchecked
            return new HashMap<String, Object>(mMap);
        }
    }
    

    调用这个方法会返回当前shardePreferences的所有数据,但是一定不要更改里面的内容,因为如果更改了内容,那么SharedPreferences的数据一致性就得不到保证了。

    下面再分析下ShardePreferences的新增,更新和删除的方法,这些方法就需要EditorImpl 这个类来做

    EditorImpl

    private final Object mEditorLock = new Object();// 锁
    private final Map<String, Object> mModified = new HashMap<>(); // 更改的数据
    private boolean mClear = false; //是否清空数据
    

    下面看下putString(),其他的putXXX()类似

    @Override
    public Editor putString(String key, @Nullable String value) {
    // 加锁,保证线程安全,数据一致性
        synchronized (mEditorLock) {
            mModified.put(key, value);// 把值放入mModified
            return this;
        }
    }
    
    1. 添加或者更新数据时,都会加锁,保证线程安全,数据一致性。然后会把数据放入mModified

    添加完数据后还需要更新到文件和内存中,一般会调用commit()和apply()下面先看看apply()

    @Override
    public void apply() {
        final long startTime = System.currentTimeMillis();
        // 首先更新内存中的数据,
        final MemoryCommitResult mcr = commitToMemory();
        // 这是等待提交到磁盘的任务
        final Runnable awaitCommit = new Runnable() {
                @Override
                public void run() {
                    try {
                    // 如果写入磁盘任务未完成,则一直等待writtenToDiskLatch是CountDownLatch
                        mcr.writtenToDiskLatch.await();
                    } catch (InterruptedException ignored) {
                    }
    
                    if (DEBUG && mcr.wasWritten) {
                        Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                                + " applied after " + (System.currentTimeMillis() - startTime)
                                + " ms");
                    }
                }
            };
    
    // 把awaitCommit 添加QueuedWork的finisher的任务队列
        QueuedWork.addFinisher(awaitCommit);
    
    // 磁盘任务写完之后需要执行任务
        Runnable postWriteRunnable = new Runnable() {
                @Override
                public void run() {
                // 执行awaitCommit.run()
                    awaitCommit.run();
                    // 把awaitCommit 移除finisher队列
                    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);
    }
    

    上面的代码中引入了MemoryCommitResult,MemoryCommitResult就是用来记录更新的数据提交到内存中的结果,比较简单,会放在下面再看

    1. 首先把数据更新到内存中
    2. 构造两个任务,awaitCommit和postWriteRunnable
    3. 调用enqueueDiskWrite(),传入postWriteRunnable

    继续追踪enqueueDiskWrite

    private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
         // commit()方法传入的postWriteRunnable为null,而apply()传入的不为null
        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
                        postWriteRunnable.run();
                    }
                }
            };
    
        // Typical #commit() path with fewer allocations, doing a write on
        // the current thread.
        // 如果isFromSyncCommit为true
        if (isFromSyncCommit) {
            boolean wasEmpty = false;
            synchronized (mLock) {
                wasEmpty = mDiskWritesInFlight == 1;
            }
            // 如果提交的写入磁盘数据的任务数为1,那么就直接执行,不再交给QueuedWork
            if (wasEmpty) {
                writeToDiskRunnable.run();
                return;
            }
        }
        // 默认交给QueuedWork执行
        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
    }
    
    1. 给isFromSyncCommit赋值,commit()方法中postWriteRunnable,及isFromSyncCommit为true,而apply()为false
    2. 构建一个将数据写入磁盘的任务writeToDiskRunnable
    3. isFromSyncCommit为true,并且写入磁盘的任务数为1,那么就会直接在当前线程执行,否则提交给QueuedWork,放在子线程中执行

    上面有个关键的方法writeToFile()

    private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
        ...
    
        boolean fileExists = mFile.exists();
    
        ...
        if (fileExists) {
            boolean needsWrite = false;
    
            // Only need to write if the disk state is older than this commit
            //只有当前磁盘的state小于内存的state,才会执行写入磁盘的任务
            if (mDiskStateGeneration < mcr.memoryStateGeneration) {
                if (isFromSyncCommit) {
                    needsWrite = true;
                } else {
                    synchronized (mLock) {
                        // 如果不是同步写入的话,那么只有当前内存state等于mcr.memoryStateGeneration才会执行写入
                        if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
                            needsWrite = true;
                        }
                    }
                }
            }
            
            // 如果不需要写入,则返回
            if (!needsWrite) {
                mcr.setDiskWriteResult(false, true);
                return;
            }
    
            boolean backupFileExists = mBackupFile.exists();
            
              ....
              
              // 如果备份文件不存在
            if (!backupFileExists) {
            // 把mFile重命名为备份文件名,如果执行失败,则返回
                if (!mFile.renameTo(mBackupFile)) {
                    Log.e(TAG, "Couldn't rename file " + mFile
                          + " to backup file " + mBackupFile);
                    mcr.setDiskWriteResult(false, false);
                    return;
                }
            } else {
            // 如果备份文件已经存在了,则把mFile文件删除
                mFile.delete();
            }
        }
    
        // 尝试写入数据到mFile,如果写入成功则把备份文件删除;如果写入的过程中发生了异常,则把新文件删除,下次会从备份文件读取数据。这里就跟读取数据的时候关联上了
        try {
            FileOutputStream str = createFileOutputStream(mFile);
            
            // 如果数据流为null
            if (str == null) {
                mcr.setDiskWriteResult(false, false);
                return;
            }
            // 把mcr.mapToWriteToDisk数据写入文件,此时所有的数据包括更新/新增的数据都在mcr.mapToWriteToDisk里面,因为在commitToMemory处理,下面会分析commitToMemory
            XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
    
            writeTime = System.currentTimeMillis();
    
            FileUtils.sync(str);
    
            fsyncTime = System.currentTimeMillis();
    
            str.close();
            ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
    
            try {
                final StructStat stat = Os.stat(mFile.getPath());
                synchronized (mLock) {
                    mStatTimestamp = stat.st_mtim;
                    mStatSize = stat.st_size;
                }
            } catch (ErrnoException e) {
                // Do nothing
            }
    
            // 写入数据成功,删除备份文件
            mBackupFile.delete();
    
            // 更新磁盘的mDiskStateGeneration为mcr.memoryStateGeneration
            mDiskStateGeneration = mcr.memoryStateGeneration;
            ...
    
            mcr.setDiskWriteResult(true, true);
    
            mNumSync++;
    
            return;
        } catch (XmlPullParserException e) {
            Log.w(TAG, "writeToFile: Got exception:", e);
        } catch (IOException e) {
            Log.w(TAG, "writeToFile: Got exception:", e);
        }
    
        // 如果写入数据的过程中发生了异常,那么就删除mFile文件,下次加载时会直接读取备份文件
        if (mFile.exists()) {
            if (!mFile.delete()) {
                Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
            }
        }
        mcr.setDiskWriteResult(false, false);
    }
    

    上面是分析了写入磁盘缓存的方法,下面再看下写入内存缓存的方法commitToMemory()

    private MemoryCommitResult commitToMemory() {
        long memoryStateGeneration;
        List<String> keysModified = null;
        Set<OnSharedPreferenceChangeListener> listeners = null;
        Map<String, Object> mapToWriteToDisk;
    
        synchronized (SharedPreferencesImpl.this.mLock) {
            // 只会在调用了commit和apply方法时,才会执行内存缓存,把数据更新到内存
            if (mDiskWritesInFlight > 0) {
                // 如果当前正在向磁盘写入mMap,那么禁止修改mMap
                // 对当前的mMap进行clone,下面就只会操作这个mMap,因为hashmap不是线程安全,SharedPreference最重要的必须保证的就是数据一致性
                mMap = new HashMap<String, Object>(mMap);
            }
            mapToWriteToDisk = mMap;//把mMap赋值给mapToWriteToDisk
            mDiskWritesInFlight++;// 写入磁盘的任务数 加1
    
            boolean hasListeners = mListeners.size() > 0;
            if (hasListeners) {
                keysModified = new ArrayList<String>();
                listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
            }
    
            synchronized (mEditorLock) {
                boolean changesMade = false;
                // 如果调用了clear方法,clear标志就会为true
                if (mClear) {
                    if (!mapToWriteToDisk.isEmpty()) {
                        changesMade = true;
                        // 清楚数据,此时清楚的只是mMap里面的数据,并没有清除mModified,即更新后的数据没有清除
                        mapToWriteToDisk.clear();
                    }
                    mClear = false;
                }
        
                    // 遍历修改的数据
                for (Map.Entry<String, Object> e : mModified.entrySet()) {
                    String k = e.getKey();
                    Object v = e.getValue();
                    // 如果v == null 或者v== this,这里this就是EditorImpl对象,就代表需要移除当前k
                    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;
                            }
                        }
                        // 把k,v写入mapToWriteToDisk
                        mapToWriteToDisk.put(k, v);
                    }
    
                    changesMade = true;
                    if (hasListeners) {
                        keysModified.add(k);
                    }
                }
    
                 // 修改后的数据集清空,此时所有的数据,原来的数据和更新后的数据全部在mapToWriteToDisk里面
                mModified.clear();
                    
                    // 如果有更新
                if (changesMade) {
                    mCurrentMemoryStateGeneration++;
                }
                    // 更新memoryStateGeneration为mCurrentMemoryStateGeneration
                memoryStateGeneration = mCurrentMemoryStateGeneration;
            }
        }
        return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
                mapToWriteToDisk);
    }
    

    只有在调用了commit或者apply方法时,才会调用这个方法。这个方法比较简单,就是把mModified中的数据和原来的数据mMap做添加,更新,删除的操作。得到最新的数据mapToWriteToDisk,更新内存状态,最终构造成MemoryCommitResult返回

    上面是apply方法,下面再看commit(),跟apply基本一样

    @Override
    public boolean commit() {
        long startTime = 0;
    
        if (DEBUG) {
            startTime = System.currentTimeMillis();
        }
    
        MemoryCommitResult mcr = commitToMemory();//更新内存的映射
    
        // 执行硬盘缓存的任务,传入null,表示可以在当前线程执行写入磁盘的操作
        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;
    }
    

    分析完apply方法后commit()就很容易了,commit跟apply的区别就在与commit允许在当前线程执行写入磁盘的任务,并且不管写入磁盘是否在当前任务执行,commit都会阻塞当前线程,等待磁盘更新完成。

    MemoryCommitResult

    这个类比较简单,先看相关的属性

    final long memoryStateGeneration;// 当前的内存state
    @Nullable final List<String> keysModified;//做了改变的值的列表
    @Nullable final Set<OnSharedPreferenceChangeListener> listeners;
    final Map<String, Object> mapToWriteToDisk;// 存放此次内存更新后的数据
    final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);//用来等待磁盘写入完成
    

    只有一个关键方法

    void setDiskWriteResult(boolean wasWritten, boolean result) {
        this.wasWritten = wasWritten;
        writeToDiskResult = result;
        writtenToDiskLatch.countDown();// writtenToDiskLatch计数减一,唤醒等待此任务的线程
    }
    

    上面的方法是在设置最终磁盘更新任务的结果,并且将writtenToDiskLatch计数减一,这样等待磁盘更新任务的线程会被唤醒,继续执行。

    相关文章

      网友评论

          本文标题:SharedPreferences存取数据流程分析

          本文链接:https://www.haomeiwen.com/subject/hsqhnctx.html