SharedPerefrence源码分析

       前言:SharedPreferences是android中用于持久化数据的方式之一,它以key-value的方式组织数据,最后以xml文件的方式保存在/data/data/<package name>/shared_prefs/目录下。SharedPreferences以key-value方式保存数据,key的类型为String,value支持的类型有:boolean,float,int,long,String,Set<String>。

       基本使用:

      使用SharedPreferences前,需要先获得一个SharedPreferences对象,有两种方式获得SharedPreferences的对象,一个是在Activity中声明的getPreferences(int mode),一个是在Context中声明的getSharedPreferences(String name, int mode)getPreferences(int mode)其实是把当前Activity的类名作为name参数,直接调用getSharedPreferences(String name, int mode)。这是getPreferences(int mode)的实现:

 public SharedPreferences getPreferences(int mode) {
         return getSharedPreferences(getLocalClassName(), mode);
  }
<span style="color: rgb(85, 85, 85); font-family: 'Trebuchet MS', Verdana, sans-serif; font-size: 1.25em; line-height: 1.4; background-color: rgb(255, 255, 255);">getSharedPreferences(String name, int mode)参数说明:</span>
   name  指定保存SharedPreferences的文件名
    mode
    * MODE_PRIVATE  默认使用这个标志(值为0),SharedPreferences文件只有应用自身可读
    * MODE_MULTI_PROCESS  Added in API level 11,当多个进程使用同一个SharedPreferences文件时,要指定这个标志(3.0之前默认设置了MODE_MULTI_PROCESS,3.0以后默认是关的)
    * MODE_WORLD_READABLE  让其他应用可读SharedPreferences文件,@deprecated in API level 17,不推荐使用
    * MODE_WORLD_WRITEABLE  让其他应用可写SharedPreferences文件,@deprecated in API level 17,不推荐使用

一,写SharedPreferences
步骤:1.获得SharedPreferences对象 2.获得Editor对象 3.调用putXXX方法 4.最后apply()提交数据
//代码:
private void writePreferences() {
    //如果不存在demo.xml,会新建一个
    SharedPreferences sharedPref = getSharedPreferences("demo",             Context.MODE_PRIVATE);
    //写入数据需要使用Editor
    SharedPreferences.Editor editor = sharedPref.edit();
    editor.putBoolean("key_demo_boolean", true)
            .putInt("key_demo_int", 1)
            .putLong("key_demo_long", 2)
            .putFloat("key_demo_float,", 3.0f)
            .putString("key_demo_string", "Hello, World!");
    Set<String> set = new HashSet<String>();
    set.add("hello");
    set.add("world");
    editor.putStringSet("key_demo_set", set);
    editor.apply();//最后提交更改
    //注意:apply()没有返回值,如果你关注是否提交成功,
    //可以使用commit()代替apply()。
    //但是apply()效率更高,一般情况下,推荐使用apply()来提交更改,
    //两者区别后面会提到。
}
执行后demo.xml的内容:
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <float name="key_demo_float," value="3.0" />
    <string name="key_demo_string">Hello, World!</string>
    <long name="key_demo_long" value="2" />
    <int name="key_demo_int" value="1" />
    <boolean name="key_demo_boolean" value="true" />
    <set name="key_demo_set">
        <string>hello</string>
        <string>world</string>
    </set>
</map>

二, 读SharedPreferences
步骤:1.获得SharedPreferences对象 2.调用getXXX方法 注意:需要为getXXX(key,defaultValue)提供一个defaultValue,当SharedPreferences文件中不存在指定的key时,会返回defaultValue
    SharedPreferences sharedPref=getSharedPreferences("demo", Context.MODE_PRIVATE);
    int defaultValue=0;
    int number=sharedPref.getInt("key_demo_int",defaultValue);

       要注意的是,SharedPreferences是接口,而真正的实现在 android.app.SharedPreferencesImpl 中。Context的 getSharedPreferences(String,int) 的实现在 android.app.ContextImpl 中。

三,apply()和commit()的区别

     使用SharedPreferences.Editor编辑好数据后,最后都需要调用apply()或commit()来提交更改。这两个方法做的事情都是先将你的更改提交到内存,然后写入硬盘(存储器),它们之间的区别在于:

  1. apply()方法没有返回值,而commit()的返回值是boolean,你可以从commit()的返回值中了解是否写入成功。
  2. 在将数据写入硬盘时,commit()方法会在当前线程等待写入完成的结果,而apply()则是开启另外一个线程等待,然后apply()方法本身直接返回。这样就导致调用commit()方法比apply()方法更耗时,因此如果你在UI线程调用commit(),可能会因为这点延迟造成界面短暂的卡顿。
注意: 那这个两个方法应该如何选择呢?如果你不关心提交的结果,选择apply()更好,它返回的速度更快。如果你需要知道提交的结果,并根据结果做后续处理,则应该选择有返回值的commit()方法。
下面是源代码分析:
    public void apply() {
           //提交更改到内存
           final MemoryCommitResult mcr = commitToMemory();
           //新建一个线程等待写入硬盘的结果
           final Runnable awaitCommit = new Runnable() {
                   public void run() {
                       try {
                           mcr.writtenToDiskLatch.await();
                       } catch (InterruptedException ignored) {
                       }
                   }
               };           
           QueuedWork.add(awaitCommit);

           //在enqueueDiskWrite()中被调用
           Runnable postWriteRunnable = new Runnable() {
                   public void run() {
                       awaitCommit.run();
                       QueuedWork.remove(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);//回调onSharedPreferenceChanged
    }

看commit()的代码,可以看到,commit()会在当前线程等待写入结束
  public boolean commit() {
                //提交更改到内存
                MemoryCommitResult mcr = commitToMemory();
                //将数据写入到硬盘
                SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null );
                try {
                    //在当前线程等待写入结束
                    mcr.writtenToDiskLatch.await();
                } catch (InterruptedException e) {
                    return false;
                }
                notifyListeners(mcr);//回调onSharedPreferenceChanged
                return mcr.writeToDiskResult;
        }

    apply()和commit()都会调用commitToMemory(),它的主要操作是更新保存SharedPreferences的Map,然后返回一个MemoryCommitResult,来看一下MemoryCommitResult的代码:
      private static class MemoryCommitResult {
                public boolean changesMade;  //标识SharedPreferences里的数据是否发生改变
                public List<String> keysModified;  // may be null
                public Set<OnSharedPreferenceChangeListener> listeners;  // may be null
                public Map<?, ?> mapToWriteToDisk;//存放要写入硬盘的数据
                //初始化count为1
                public final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);
                //标识写入硬盘是否成功,commit()的返回值来自这个
                public volatile boolean writeToDiskResult = false;

                public void setDiskWriteResult(boolean result) {
                    writeToDiskResult = result;
                    //会使count--,count值变为0,释放调用writtenToDiskLatch.await()的线程
                    writtenToDiskLatch.countDown();
                }
    }
提交到内存后,然后这里是将数据写入硬盘:
 private void enqueueDiskWrite(final MemoryCommitResult mcr,final Runnable postWriteRunnable) {
            //创建一个将数据写入硬盘的线程
            final Runnable writeToDiskRunnable = new Runnable() {
                public void run() {
                    synchronized (mWritingToDiskLock) {
                        //在这里,才将数据写入硬盘
                        writeToFile(mcr);
                    }
                    synchronized (SharedPreferencesImpl.this) {
                        mDiskWritesInFlight--;
                    }
                    if (postWriteRunnable != null) {
                        postWriteRunnable.run();
                    }
                }
            };

            //如果postWriteRunnable==null,说明是被commit()而不是apply()调用
            final boolean isFromSyncCommit = (postWriteRunnable == null);

            // Typical #commit() path with fewer allocations, doing a write on
            // the current thread.
            if (isFromSyncCommit) {
                boolean wasEmpty = false;
                synchronized (SharedPreferencesImpl.this) {
                    //每次commitToMemory()会使mDiskWritesInFlight++,写入硬盘完成后,mDiskWritesInFlight--
                    //如果mDiskWritesInFlight==1,说明当前只有一个commit()操作还没写入到硬盘
                    wasEmpty = mDiskWritesInFlight == 1;
                }
                if (wasEmpty) {
                    writeToDiskRunnable.run();
                    return;
                }
            }
           //singleThreadExecutor()会返回一个单线程的线程池,保证任务串行执行(当有多个提交未写入硬盘时)
            QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
    }
<span style="color: rgb(85, 85, 85); font-family: 'Trebuchet MS', Verdana, sans-serif; font-size: 1.5em; line-height: 1.43; background-color: rgb(255, 255, 255);">Set<String>更新无效问题</span>
<span style="color: rgb(85, 85, 85); font-family: 'Trebuchet MS', Verdana, sans-serif; font-size: 1.5em; line-height: 1.43; background-color: rgb(255, 255, 255);"><span style="color: rgb(51, 51, 51); font-family: 'Helvetica Neue', Helvetica, 'Segoe UI', Arial, freesans, sans-serif; font-size: 16px; line-height: 25.6000003814697px;">在一开始的写SharedPreferences的示例中,我们向demo.xml中写入了一个key为"key_demo_set"的Set<String>,现在我们想向里面追加一个"appendString"。如果你按下面的代码执行,你会发现,等你去读取SharedPreferences时,"appendString"确实被添加了,但是demo.xml中却没有"appendString"。也就是说,现在内存中的SharedPreferences和硬盘中是不一致的。原因在于,不管是apply()还是commit()方法,只有数据发生改变才会进行写入硬盘的操作。虽然set的内容改变了,但是它对应的引用并没有改变。而在SharedPreferences的实现中,判断数据是否发生改变,使用的是Object的equals方法。来看下面的示例代码:</span>
</span>
   //这样做不会使数据写入到硬盘
    private void updateSetDataFailed() {
            SharedPreferences sharedPref = getSharedPreferences("demo", Context.MODE_PRIVATE);
            SharedPreferences.Editor editor = sharedPref.edit();//写入数据需要使用Editor
            Set<String> set = sharedPref.getStringSet("key_demo_set", new HashSet<String>()/*获取失败才会使用这个,这个示例中,不会用到*/);
            set.add("appendString");
            editor.putStringSet("key_demo_set", set);
            editor.apply();
    }

正确的做法是创建一个新的Set对象,再添加更新数据。
   //这才是正确的写法
    private void updateSetDataSuccess() {
            SharedPreferences sharedPref = getSharedPreferences("demo", Context.MODE_PRIVATE);
            SharedPreferences.Editor editor = sharedPref.edit();//写入数据需要使用Editor
            Set<String> oldSet = sharedPref.getStringSet("key_demo_set", new HashSet<String>()/*获取失败才会使用这个,这个示例中,不会用到*/);
            //创建一个新的对象,并复制原来的数据(注意构造函数)
            Set<String> newSet=new HashSet<String>(oldSet);
            newSet.add("appendString");
            editor.putStringSet("key_demo_set", newSet);
            editor.apply();
    }

从源代码中找找原因。apply()和commit()都会先调用commitToMemory(),看下commitToMemory()的源代码,可以找到答案(看注释部分):
  // Returns true if any changes were made
    private MemoryCommitResult commitToMemory() {
            MemoryCommitResult mcr = new MemoryCommitResult();
            synchronized (SharedPreferencesImpl.this) {
                //省略.........
                synchronized (this) {
                    //省略.....
                    for (Map.Entry<String, Object> e : mModified.entrySet()) {
                        String k = e.getKey();
                        Object v = e.getValue();
                        if (v == this || v == null) {
                            //省略......
                        } else {
                            if (mMap.containsKey(k)) {
                                Object existingValue = mMap.get(k);
                                //使用equals方法判断是否发生改变,如果没有,continue
                                if (existingValue != null && existingValue.equals(v)) {
                                    continue;
                                }
                            }
                            mMap.put(k, v);
                        }
                        //标识数据是否发生改变,如果一直continue,changesMade不会被赋值为true
                        //changesMade在writeToFile()中被使用
                        mcr.changesMade = true;
                        //省略....
                    }
                    mModified.clear();
                }
            }
            return mcr;
    }
mcr.changesMade在writeToFile()方法中被使用,来看看相关的代码:
    private void writeToFile(MemoryCommitResult mcr) {        
            if (mFile.exists()) {
                if (!mcr.changesMade) {
                    mcr.setDiskWriteResult(true);
                    //可以看到,如果没有发生改变,直接return了,不会执行写数据到硬盘的代码
                    return;
                }
                //省略......
            }
            //省略..........
            //省略的代码为将数据写入硬盘的操作,有兴趣的可以看下源代码
    }
所以,当你想要对SharedPreferences里的Set数据做出修改时,记得创建先一个新的Set对象(包含原来的数据),然后再进行操作。

Android本地数据存储:Shared Preferences安全风险浅析 http://jaq.alibaba.com/blog.htm?id=56)

Android源码分析之SharedPreferences http://www.cnblogs.com/xiaoweiz/p/3733272.html

SharedPreferences | Android Developers http://developer.android.com/reference/android/content/SharedPreferences.html



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值