Android 持久化技术(一)之SharedPreferences

因为在本身练手的App项目中使用了SharedPreferences技术,因此对其进行必定探究。java

首先咱们先总结一下,Android的数据持久化方式:SharedPrefences、SQLite、文件储存、ContentProvider、网络储存。其他四种,留后再进一步探究。android

SharedPreferences

老规矩看看类注释是怎么介绍的
image.png缓存

由Context.getSharedPreferences方法放回的,能够访问和修改参数数据的接口。对于任一一组数据,都有一个全部客户机共享的该类实例。对参数数据的修改,必须经过Editor对象,该对象确保了参数数值的一致性和控制客户端将数值提交到储存。由各类get方法获得的对象必须是不可变得对象。
该类保证了强一致性,可是不支持跨进程使用。安全

从这段注释中,咱们不难发现:SharedPreferences须要经过Context建立,该类与Editor对象密切相关,在应用内能够数据共享。网络

咱们在SharedPreferences类中往下寻找,就找到Editor接口并发

/**
     * Interface used for modifying values in a {@link SharedPreferences}
     * object.  All changes you make in an editor are batched, and not copied
     * back to the original {@link SharedPreferences} until you call {@link #commit}
     * or {@link #apply}
     */
    public interface Editor {
      
        Editor putString(String key, @Nullable String value);
      
        Editor putLong(String key, long value);
       
        Editor putFloat(String key, float value);
       
        Editor putBoolean(String key, boolean value);

        Editor remove(String key);
        
        Editor clear();

        boolean commit();

        void apply();
    }

从这个Editor接口中,咱们能够获得几个信息。首先SharedPreferences只能储存4类数据,String,Long,Float,Boolean;其次SharedPreferences是使用key-value键值对的方式进行储存的;最后,有两种提交方式apply(),commit()。app

  • 从前两个信息不难推出,SharedPreferences只是一种轻量级的储存方式,因此最好不要使用这个去储存一些过大,或者复杂数据类型的数据。
  • apply()和commit()的区别:异步

    • 从上面能够看出,commit是有返回值的,而apply是没有放回值的。当咱们须要知道一个数据是否修改为功时,就须要调用commit方法。
    • commit是直接将修改的数据同步提交到硬件硬盘,会阻塞调用它的线程。apply则是将修改数据原子提交到内存,然后异步真正提交到硬件硬盘,因此后面调用apply会直接覆盖前面的数据,使用apply会提升效率。
    • apply方法不会提示任何失败的提示。因为在一个进程中,sharedPreference是单实例,通常不会出现并发冲突,若是对提交的结果不关心的话,建议使用apply,固然须要确保提交成功且有后续操做的话,仍是须要用commit的。
    • 此外SharedPreferences有一个接口,能够实现对键值变化的监听。
  • 若是须要储存复杂数据(图片或对象)时,就须要对将其转化为Base64编码。

如何使用SharedPreferences?

一、获取SharedPreferences

ContextImpl.getSharePreferences() 该类在AS上不显示。根据当前应用名称获取ArrayMap(存储sp容器),并根据文件名获取SharedPreferencesImpl对象(实现SharedPreferences接口)。
  • 缓存未命中, 才构造SharedPreferences对象,也就是说,屡次调用getSharedPreferences方法并不会对性能形成多大影响,由于又缓存机制
  • SharedPreferences对象的建立过程是线程安全的,由于使用了synchronize`关键字
  • 若是命中了缓存,而且参数mode使用了Context.MODE_MULTI_PROCESS,那么将会调用sp.startReloadIfChangedUnexpectedly()方法,在startReloadIfChangedUnexpectedly方法中,会判断是否由其余进程修改过这个文件,若是有,会从新从磁盘中读取文件加载数据
class ContextImpl extends Context {
    //静态存储类,缓存全部应用的SP容器,该容器key对应应用名称,value则为每一个应用存储全部sp的容器
    private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
    
     @Override
    public SharedPreferences getSharedPreferences(String name, int mode) {
        ......
        
        // 根据 名字获取相对应的文件名。
        // 若是没有则直接新建一个
        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) {
        // 从ArrayMap中获取到应用储存的value
            final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
            // 从当前的Map中获取一个,若是没有则直接新建一个而且放回
            sp = cache.get(file);
            if (sp == null) {
                checkMode(mode);
                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 = 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;
    }
    
}
sharedPreferences的对象实例 sharedPreferencesImpl类

用SP存储的静态变量键值数据在内存中是一直存在(文件存储),经过SharedPreferencesImpl构造器开启一个线程对文件进行读取。SharedPreferencesImpl主要是对文件进行操做。ide

  • 将传进来的参数file以及mode分别保存在mFile以及mMode
  • 建立一个.bak备份文件,当用户写入失败的时候会根据这个备份文件进行恢复工做
  • 将存放键值对的mMap初始化为null
  • 调用startLoadFromDisk()方法加载数据
startLoadFromDisk()
  • 若是有备份文件,直接使用备份文件进行回滚
  • 第一次调用getSharedPreferences方法的时候,会从磁盘中加载数据,而数据的加载时经过开启一个子线程调用loadFromDisk方法进行异步读取的
  • 将解析获得的键值对数据保存在mMap
  • 将文件的修改时间戳以及大小分别保存在mStatTimestamp以及mStatSize中(保存这两个值有什么用呢?咱们在分析getSharedPreferences方法时说过,若是有其余进程修改了文件,而且modeMODE_MULTI_PROCESS,将会判断从新加载文件。如何判断文件是否被其余进程修改过,没错,根据文件修改时间以及文件大小便可知道)
  • 调用notifyAll()方法通知唤醒其余等待线程,数据已经加载完毕
SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        // 文件备份
        mBackupFile = makeBackupFile(file);
        mMode = mode;
        mLoaded = false;
        mMap = null;
        mThrowable = null;
        
        
        // 开启一个线程读取文件
        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()) {
                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);
                    //使用XmlUtils工具类读取xml文件数据
                    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;/若是有数据,将数据已经赋值给类成员变量mMap(将从文件读取的数据赋值给mMap)
                        mStatTimestamp = stat.st_mtim;
                        mStatSize = stat.st_size;
                    } else {
                    //没有数据直接建立一个hashmap对象
                        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();
            }
        }
    }

二、获取数据

sharedPreferencesImpl重写了SharedPreferences的方法,基本上结构都一致,这里拿String类举例。工具

  • getXxx方法是线程安全的,由于使用了synchronize关键字
  • getXxx方法是直接操做内存的,直接从内存中的mMap中根据传入的key读取value
  • getXxx方法有可能会卡在awaitLoadedLocked方法,从而致使线程阻塞等待(何时会出现这种阻塞现象呢?前面咱们分析过,第一次调用getSharedPreferences方法时,会建立一个线程去异步加载数据,那么假如在调用完getSharedPreferences方法以后当即调用getXxx方法,此时的mLoaded颇有可能为false,这就会致使awaiteLoadedLocked方法阻塞等待,直到loadFromDisk方法加载完数据而且调用notifyAll来唤醒全部等待线程
public String getString(String key, @Nullable String defValue) {
        synchronized (mLock) {
        //此处会阻塞当前线程,直到文件加载完毕,第一次使用的时候可能会阻塞主线程
            awaitLoadedLocked();
        //从类成员变量mMap中直接读取数据,没有直接返回默认值
            String v = (String)mMap.get(key);
            return v != null ? v : defValue;
        }
    }

三、提交数据

3.1 获取editor对象
public Editor edit() {
       synchronized (mLock) {
           awaitLoadedLocked();//若是文件未加载完毕,会一直阻塞当前线程,直到加载完成为止
       }
       return new EditorImpl();
   }
3.2 对数据进行修改
  • SharedPreferences的写操做是线程安全的,由于使用了synchronize关键字
  • 对键值对数据的增删记录保存在mModified中,而并非直接对SharedPreferences.mMap进行操做(mModified会在commit/apply方法中起到同步内存SharedPreferences.mMap以及磁盘数据的做用)
public final class EditorImpl implements Editor {
    //先存储在Editor的map中
    private final Map<String, Object> mModified = new HashMap<>();
    
   //各类修改方法依旧相似
    public Editor putString(String key, @Nullable String value) {
            synchronized (mEditorLock) {
                mModified.put(key, value);
                return this;
            }
        }
    ...
 }
3.3 提交数据
  • commit()方法
public boolean commit() {
            long startTime = 0;

            if (DEBUG) {
                startTime = System.currentTimeMillis();
            }
            //第一步  commitToMemory方法能够理解为对SP中的mMap对象同步到最新数据状态
            //mcr对象就是最终须要写入磁盘的mMap
            MemoryCommitResult mcr = commitToMemory();
            
            //第二步 写文件;注意第二个参数为null,写文件操做会运行在当前线程
            //当前只有一个commit线程时。会直接在当前线程执行
            //若是是UI线程 则可能会形成阻塞
            //会判断有无 备份文件,必定要有备份文件,防止写入错误
            //将mcr写入磁盘
            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主要区别就是apply的写文件操做会在一个线程中执行,不会阻塞UI线程
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) {
                        }

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

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

注意点

  • SharedPreferences是线程安全的,可是不是进程安全的。
  • SharedPreferences 不要存放特别大的数据

    • 第一次加载时,须要将整个SP加载到内存当中,若是过于大,会致使阻塞,甚至会致使 ANR
    • 每次apply或者commit,都会把所有的数据一次性写入磁盘, 因此 SP 文件不该该过大, 影响总体性能
    • SharedPreference的文件存储性能与文件大小相关,咱们不要将毫无关联的配置项保存在同一个文件中,同时考虑将频繁修改的条目单独隔离出来
  • 不适宜存储JSON等特殊符号不少的数据
  • 全部的getXxx都是从内存中取的数据,数据来源于SharedPreferences.mMap
  • apply同步回写(commitToMemory())内存SharedPreferences.mMap,而后把异步回写磁盘的任务放到一个单线程的线程池队列中等待调度。apply不须要等待写入磁盘完成,而是立刻返回
  • ommit同步回写(commitToMemory())内存SharedPreferences.mMap,而后若是mDiskWritesInFlight(此时须要将数据写入磁盘,但还未处理或未处理完成的次数)的值等于1,那么直接在调用commit的线程执行回写磁盘的操做,不然把异步回写磁盘的任务放到一个单线程的线程池队列中等待调度。commit会阻塞调用线程,知道写入磁盘完成才返回
  • MODE_MULTI_PROCESS是在每次getSharedPreferences时检查磁盘上配置文件上次修改时间和文件大小,一旦全部修改则会从新从磁盘加载文件,因此并不能保证多进程数据的实时同步
  • 屡次edit屡次commit/apply

    • 屡次edit会产生不少editor对象
    • 屡次apply和commit App的stop方法会等待写完为止
相关文章
相关标签/搜索