SharedPreferences源码及使用优缺点分析

SharedPreferences是我们在开发中非常常用的,但是应该有很多小伙伴没有去了解过SharedPreferences的实现原理,以及SharedPreferences现在逐渐被谷歌淘汰了,这是为什么呢,让我们一起来研究一下。

1.SharedPreferences的使用

        SharedPreferences sp = context.getSharedPreferences("test", Context.MODE_PRIVATE);
        SharedPreferences.Editor editor = sp.edit();
        editor.putString("spKey", "1");
        editor.commit();
        
        SharedPreferences sp = context.getSharedPreferences("test2", Context.MODE_PRIVATE);
        SharedPreferences.Editor editor = sp.edit();
        editor.putString("spKey", "1");
        editor.apply();

以上是一段简单的SharedPreferences使用,但是细心的小伙伴会发现两段最后一行代码是有一点不一样的,我们来看一下commit的apply有什么区别。

2.SharedPreferences的提交数据时IO写数据解析

(1)我们先来看commit方法,SharedPreferences的实现类是SharedPreferencesImpl,我们看一下里面的commit方法的源码:

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

            if (DEBUG) {
                startTime = System.currentTimeMillis();
            }
            // 1.先提交到内存
            MemoryCommitResult mcr = commitToMemory();
            // 2.真的去IO写入
            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");
                }
            }
            // 3.通知回调
            notifyListeners(mcr);
            return mcr.writeToDiskResult;
        }

可以看出commit方法是同步的,也就是说如果在主线程使用commit方法的话在写如何如果很耗时可是会阻塞主线程的有可能到导致ANR,所以我们在使用commit的时候要注意或者尽量使用apply方法,apply是异步的,接下来我们就来看一下apply方法的源码。

(2)apply方法解析

        @Override
        public void apply() {
            final long startTime = System.currentTimeMillis();
            // 1.先提交到内存
            final MemoryCommitResult mcr = commitToMemory();
            // 2.创建写文件的runnable
            final Runnable awaitCommit = new Runnable() {
                    @Override
                    public void run() {
                        try {
                            mcr.writtenToDiskLatch.await();
                        } catch (InterruptedException ignored) {
                        }

                        if (DEBUG && mcr.wasWritten) {
                            Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                                    + " applied after " + (System.currentTimeMillis() - startTime)
                                    + " ms");
                        }
                    }
                };
            // 3.添加finisher用于在生命周期结束时判断是否有未执行完的任务
            QueuedWork.addFinisher(awaitCommit);
            // 4.执行写文件的runnable
            Runnable postWriteRunnable = new Runnable() {
                    @Override
                    public void run() {
                        awaitCommit.run();
                        QueuedWork.removeFinisher(awaitCommit);
                    }
                };
            // 5.异步进行IO写操作
            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);
        }

可能看完了还是一头雾水,哪里异步了这就需要我们去看enqueueDiskWrite方法的实现了。

(3)enqueueDiskWrite解析

private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
        // 1.判断是否是同步,commit调用时postWriteRunnable为null
        final boolean isFromSyncCommit = (postWriteRunnable == null);
        // 2.异步IO写的runnable
        final Runnable writeToDiskRunnable = new Runnable() {
                @Override
                public void run() {
                    synchronized (mWritingToDiskLock) {
                        writeToFile(mcr, isFromSyncCommit);
                    }
                    synchronized (mLock) {
                        mDiskWritesInFlight--;
                    }
                    if (postWriteRunnable != null) {
                        postWriteRunnable.run();
                    }
                }
            };

        // Typical #commit() path with fewer allocations, doing a write on
        // the current thread.
        // 3. 同步写的时候才执行
        if (isFromSyncCommit) {
            boolean wasEmpty = false;
            synchronized (mLock) {
                // 4.是否同步写成功了
                wasEmpty = mDiskWritesInFlight == 1;
            }
            // 5.写失败了就异步再去写
            if (wasEmpty) {
                writeToDiskRunnable.run();
                return;
            }
        }
        // 6.apply异步才执行,QueuedWork利用handler去异步执行runnable
        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
    }

看了上面apply一定会有一些疑问,那异步写的话我在写的过程中再次读取我新写的值还能拿到吗,其实是可以的,这就要看SharedPreferences的初始化以及上面代码中提到过的commitToMemory方法了。

3.SharedPreferences的初始化

SharedPreferences初始化的时候会先把sp文件中的数据一次性读出来,保存到内存中,下次再获取的时候就不会再进行IO操作了,直接读取内存即可,减少了IO操作优化了性能,初始化代码如下:

// 1.初始化impl
SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        mBackupFile = makeBackupFile(file);
        mMode = mode;
        mLoaded = false;
        mMap = null;
        mThrowable = null;
        startLoadFromDisk();
    }
    
    // 2.异步读取sp文件的数据
    private void startLoadFromDisk() {
        synchronized (mLock) {
            mLoaded = false;
        }
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                loadFromDisk();
            }
        }.start();
    }
    
    //3.读取sp xml文件的数据并存储到内存的map中(所以sp文件不宜存过大的数据导致占用内存!)
    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 {
                    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;
        }

        synchronized (mLock) {
            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;
                        mStatTimestamp = stat.st_mtim;
                        mStatSize = stat.st_size;
                    } else {
                        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.notifyAll();
            }
        }
    }

所以其实我们在使用sp的时候每次只有第一次初始化会涉及IO操作,初始化后读就直接从内存中读取了,例如getString的源码如下:

    public String getString(String key, @Nullable String defValue) {
        synchronized (mLock) {
            // 加锁等待从sp文件读取数据的过程,自旋等待
            awaitLoadedLocked();
            // 当IO操作结束后,从内存的map中直接取出
            String v = (String)mMap.get(key);
            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);
        }
    }

4.SharedPreferences的线程安全和进程安全

(1)线程安全
SharedPreferences是线程安全的,从上面分析的源码可以看出SharedPreferences在进行操作时都会加锁来保证线程安全,所以线程间是可以放心使用的。

(2)进程安全
SharedPreferences在进程间是不安全的,在跨进程频繁读写会有数据丢失的可能 那么,如何保证SharedPreferences进程的安全呢?

实现思路很多,比如使用文件锁,保证每次只有一个进程在访问这个文件;或者对于Android开发而言,ContentProvider作为官方倡导的跨进程组件,其它进程通过定制的ContentProvider用于访问SharedPreferences,同样可以保证SharedPreferences的进程安全等等。

5.文件损坏 & 备份机制

SharedPreferences再次迎来了新的挑战。
由于不可预知的原因(比如内核崩溃或者系统突然断电),xml文件的 写操作 异常中止,Android系统本身的文件系统虽然有很多保护措施,但依然会有数据丢失或者文件损坏的情况。 作为设计者,如何规避这样的问题呢?答案是对文件进行备份,SharedPreferences的写入操作正式执行之前,首先会对文件进行备份,将初始文件重命名为增加了一个.bak后缀的备份文件:

// 尝试写入文件
private void writeToFile(...) {
  if (!backupFileExists) {
      !mFile.renameTo(mBackupFile);
  }
}

这之后,尝试对文件进行写入操作,写入成功时,则将备份文件删除:

// 写入成功,立即删除存在的备份文件
// Writing was successful, delete the backup file if there is one.
mBackupFile.delete();

反之,若因异常情况(比如进程被杀)导致写入失败,进程再次启动后,若发现存在备份文件,则将备份文件重名为源文件,原本未完成写入的文件就直接丢弃:

// 从磁盘初始化加载时执行
private void loadFromDisk() {
    synchronized (mLock) {
        if (mBackupFile.exists()) {
            mFile.delete();
            mBackupFile.renameTo(mFile);
        }
    }
  }

现在,通过文件备份机制,我们能够保证数据只会丢失最后的更新,而之前成功保存的数据依然能够有效。

6.SharedPreferences不可避免的ANR

在上面代码分析中不知道你们有没有记得apply方法中QueuedWork.addFinisher(awaitCommit)这个调用,其实apply是异步安全的,发生ANR是由于Android的另一个机制问题。当Activity.onStop()以及Service处理onStop等相关方法时,则会执行 QueuedWork.waitToFinish()等待所有的等待锁释放,因此如果SharedPreferences一直没有完成更新任务,有可能会导致卡在主线程,最终超时导致ANR。

  • 什么情况下SharedPreferences会一直没有完成任务呢? 比如太频繁无节制的apply(),导致任务过多,这也侧面说明了SPUtils.putXXX()这种粗暴的设计的弊端。 这个在头条sp ANR解决方案 的文章中有剖析过这个问题,并且有解决方案,大概就是采用hook在onstop之前把未完成的任务清空,避免onstop时等待而导致的ANR,后面项目中会尝试一下这种方案,当前现在还有替代SharedPreferences的方案可以更好的满足我们的需求。

7.对SharedPreferences的总结和反思

SharedPreferences在我们世纪应用开发时用用非常广泛,所以我们一定要了解SharedPreferences的实现原理,避免踩坑,正确的使用SharedPreferences。
(1)SharedPreferences已经优化了很多,比如一次读取sp后,其他时候使用内存,这种设计思想我们在代码设计的时候也可以做一些参考,极大的减少了IO操作
(2)文件保护机制即使比较常用的bak方法
(3)使用注意事项:避免滥用SharedPreferences,在多次添加sp数据时避免多次commit或者apply。

8.SharedPreferences的替代者

MMKV和Jetpack DataStore是目前主流的替代者,一个是腾讯的开源一个是官方维护的,说明SharedPreferences已经渐渐退出历史舞台了,后面我们有机会再学习一下MMKV和Jetpack DataStore。

参考链接:https://juejin.im/post/6884505736836022280

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值