Android-SharedPreferences源码学习与最佳实践

来源:互联网 发布:次声波软件手机版 编辑:程序博客网 时间:2024/05/22 12:07

最近有个任务是要做应用启动时间优化,然后记录系统启动的各个步骤所占用的时间,发现有一个方法是操作SharedPreferences的,里面仅仅是读了2个key,然后更新一下值,然后再写回去,耗时竟然在500ms以上(应用初次安装的时候),感到非常吃惊。以前只是隐约的知道SharedPreferences是跟硬盘上的一个xml文件对应的,具体的实现还真没研究过,下面我们就来看看SharedPreferences到底是个什么玩意,为什么效率会这么低?

SharedPreferences是存放在ContextImpl里面的,所以先看写ContextImpl这个类:

ContextImpl.java(https://github.com/android/platform_frameworks_base/blob/master/core/java/android/app/ContextImpl.java):

 /**   * Map from package name, to preference name, to cached preferences.   */private static ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>> sSharedPrefs;//在内存的一份缓存@Overridepublic SharedPreferences getSharedPreferences(String name, int mode) {        SharedPreferencesImpl sp;        synchronized (ContextImpl.class) {//同步的            if (sSharedPrefs == null) {                sSharedPrefs = new ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>>();            }            final String packageName = getPackageName();            ArrayMap<String, SharedPreferencesImpl> packagePrefs = sSharedPrefs.get(packageName);            if (packagePrefs == null) {                packagePrefs = new ArrayMap<String, SharedPreferencesImpl>();                sSharedPrefs.put(packageName, packagePrefs);            }            // At least one application in the world actually passes in a null            // name.  This happened to work because when we generated the file name            // we would stringify it to "null.xml".  Nice.            if (mPackageInfo.getApplicationInfo().targetSdkVersion <                    Build.VERSION_CODES.KITKAT) {                if (name == null) {                    name = "null";                }            }            sp = packagePrefs.get(name);            if (sp == null) {                File prefsFile = getSharedPrefsFile(name);//这里是找到文件                sp = new SharedPreferencesImpl(prefsFile, mode);//在这里会做初始化,从硬盘加载数据                packagePrefs.put(name, 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;    }
getSharedPreferences()做的事情很简单,一目了然,我们重点看下SharedPreferencesImpl.java这个类:
SharedPreferencesImpl.java(https://github.com/android/platform_frameworks_base/blob/master/core/java/android/app/SharedPreferencesImpl.java)
首先是构造函数:

SharedPreferencesImpl(File file, int mode) {        mFile = file;//这个是硬盘上的文件        mBackupFile = makeBackupFile(file);//这个是备份文件,当mFile出现crash的时候,会使用mBackupFile来替换        mMode = mode;//这个是打开方式        mLoaded = false;//这个是一个标志位,文件是否加载完成,因为文件的加载是一个异步的过程        mMap = null;//保存数据用        startLoadFromDisk();//开始从硬盘异步加载}//还两个很重要的成员:private int mDiskWritesInFlight = 0;  //有多少批次没有commit到disk的写操作,每个批次可能会对应多个k-vprivate final Object mWritingToDiskLock = new Object();//写硬盘文件时候加锁//从硬盘加载private void startLoadFromDisk() {        synchronized (this) {//先把状态置为未加载            mLoaded = false;        }        new Thread("SharedPreferencesImpl-load") {//开了一个线程,异步加载            public void run() {                synchronized (SharedPreferencesImpl.this) {                    loadFromDiskLocked();//由SharedPreferencesImpl.this锁保护                }            }        }.start();    }//从硬盘加载private void loadFromDiskLocked() {        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 map = null;        StructStat stat = null;        try {            stat = Libcore.os.stat(mFile.getPath());            if (mFile.canRead()) {                BufferedInputStream str = null;                try {                    str = new BufferedInputStream(new FileInputStream(mFile), 16*1024);//从硬盘把数据读出来                    map = XmlUtils.readMapXml(str);//做xml解析                } catch (XmlPullParserException e) {                    Log.w(TAG, "getSharedPreferences", e);                } catch (FileNotFoundException e) {                    Log.w(TAG, "getSharedPreferences", e);                } catch (IOException e) {                    Log.w(TAG, "getSharedPreferences", e);                } finally {                    IoUtils.closeQuietly(str);                }            }        } catch (ErrnoException e) {        }        mLoaded = true;//设置标志位,已经加载完成        if (map != null) {            mMap = map;  //保存到mMap            mStatTimestamp = stat.st_mtime;//记录文件的时间戳            mStatSize = stat.st_size;//记录文件的大小        } else {            mMap = new HashMap<String, Object>();        }        notifyAll();//唤醒等待线程    }
然后我们随便看一个读请求:

public int getInt(String key, int defValue) {        synchronized (this) {//还是得首先获取this锁            awaitLoadedLocked(); //这一步完成以后,说明肯定已经加载完了            Integer v = (Integer)mMap.get(key);//直接从内存读取            return v != null ? v : defValue;        }    }//等待数据加载完成 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 {                wait();//等            } catch (InterruptedException unused) {            }        }    }
看一下写操作,写是通过Editor来做的:

public Editor edit() {// TODO: remove the need to call awaitLoadedLocked() when        // requesting an editor.  will require some work on the        // Editor, but then we should be able to do:        //        //      context.getSharedPreferences(..).edit().putString(..).apply()        //        // ... all without blocking.//注释很有意思,获取edit的时候,可以把这个同步去掉,但是如果去掉就需要在Editor上做一些工作(???)。//但是,好处是context.getSharedPreferences(..).edit().putString(..).apply()整个过程都不阻塞        synchronized (this) {//还是先等待加载完成            awaitLoadedLocked();        }        return new EditorImpl();//返回一个EditorImpl,它是一个内部类    }public final class EditorImpl implements Editor {//写操作暂时会把数据放在这里面        private final Map<String, Object> mModified = Maps.newHashMap();//由this锁保护//是否要清空所有的preferences private boolean mClear = false;public Editor putInt(String key, int value) {            synchronized (this) {//首先获取this锁                mModified.put(key, value);//并不是直接修改mMap,而是放到mModified里面                return this;            }        }}
看一下commit:

public boolean commit() {    MemoryCommitResult mcr = commitToMemory(); //首先提交到内存    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null /* sync write on this thread okay */);//然后提交到硬盘    try {        mcr.writtenToDiskLatch.await();//等待写硬盘完成    } catch (InterruptedException e) {        return false;    }    notifyListeners(mcr);    return mcr.writeToDiskResult;}commitToMemory()这个方法主要是用来更新内存缓存的mMap: // Returns true if any changes were madeprivate MemoryCommitResult commitToMemory() {    MemoryCommitResult mcr = new MemoryCommitResult();    synchronized (SharedPreferencesImpl.this) { //加SharedPreferencesImpl锁,写内存的时候不允许读        // We optimistically don't make a deep copy until a memory commit comes in when we're already writing to disk.        if (mDiskWritesInFlight > 0) {//如果存在没有提交的写, mDiskWritesInFlight是SharedPreferences的成员变量             // We can't modify our mMap as a currently in-flight write owns it.  Clone it before modifying it.            // noinspection unchecked            mMap = new HashMap<String, Object>(mMap);//clone一个mMap,没明白!        }        mcr.mapToWriteToDisk = mMap;        mDiskWritesInFlight++;//批次数目加1        boolean hasListeners = mListeners.size() > 0;        if (hasListeners) {            mcr.keysModified = new ArrayList<String>();            mcr.listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());        }        synchronized (this) {//对当前的Editor加锁            if (mClear) {//只有当调用了clear()才会把这个值置为true                if (!mMap.isEmpty()) {//如果mMap不是空                    mcr.changesMade = true;                    mMap.clear();//清空mMap。mMap里面存的是整个的Preferences                }                mClear = false;            }            for (Map.Entry<String, Object> e : mModified.entrySet()) {//遍历所有要commit的entry                String k = e.getKey();                Object v = e.getValue();                if (v == this) {  // magic value for a removal mutation                    if (!mMap.containsKey(k)) {                        continue;                    }                    mMap.remove(k);                } else {                    boolean isSame = false;                    if (mMap.containsKey(k)) {                        Object existingValue = mMap.get(k);                        if (existingValue != null && existingValue.equals(v)) {                            continue;                        }                    }                    mMap.put(k, v);//这里是往里面放,因为最外层有对SharedPreferencesImpl.this加锁,写是没问题的                }                mcr.changesMade = true;                if (hasListeners) {                    mcr.keysModified.add(k);                }            }            mModified.clear();//清空editor        }    }    return mcr;}//这是随后的写硬盘 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();                    }                }            };        final boolean isFromSyncCommit = (postWriteRunnable == null);//如果是commit,postWriteRunnable是null        // Typical #commit() path with fewer allocations, doing a write on        // the current thread.        if (isFromSyncCommit) {//如果是调用的commit            boolean wasEmpty = false;            synchronized (SharedPreferencesImpl.this) {                wasEmpty = mDiskWritesInFlight == 1;//如果只有一个批次等待写入            }            if (wasEmpty) {                writeToDiskRunnable.run();//不用另起线程,直接在当前线程执行,很nice的优化!                return;            }        }//如果不是调用的commit,会走下面的分支//如或有多个批次等待写入,另起线程来写,从方法名可以看出来也是串行的写,写文件本来就应该串行!        QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);    }看下writeToDiskRunnable都干了些什么:final Runnable writeToDiskRunnable = new Runnable() {//这是工作在另一个线程        public void run() {            synchronized (mWritingToDiskLock) {//mWritingToDiskLock是SharedPreferencesImpl的成员变量,保证单线程写文件,       //不能用this锁是因为editor上可能会存在多个commit或者apply       //也不能用SharedPreferences锁,因为会阻塞读,不错!                writeToFile(mcr);//写到文件            }            synchronized (SharedPreferencesImpl.this) {                mDiskWritesInFlight--;//批次减1            }            if (postWriteRunnable != null) {                postWriteRunnable.run();//这个是写完以后的回调            }        }    };//下面是真正要写硬盘了    // Note: must hold mWritingToDiskLock    private void writeToFile(MemoryCommitResult mcr) {        // Rename the current file so it may be used as a backup during the next read        if (mFile.exists()) {            if (!mcr.changesMade) {//如果没有修改,直接返回                // If the file already exists, but no changes were                // made to the underlying map, it's wasteful to                // re-write the file.  Return as if we wrote it                // out.                mcr.setDiskWriteResult(true);                return;            }            if (!mBackupFile.exists()) {//先备份                if (!mFile.renameTo(mBackupFile)) {                    Log.e(TAG, "Couldn't rename file " + mFile                          + " to backup file " + mBackupFile);                    mcr.setDiskWriteResult(false);                    return;                }            } else {//删除重建                mFile.delete();            }        }        // Attempt to write the file, delete the backup and return true as atomically as        // possible.  If any exception occurs, delete the new file; next time we will restore        // from the backup.        try {            FileOutputStream str = createFileOutputStream(mFile);            if (str == null) {                mcr.setDiskWriteResult(false);                return;            }            XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);            FileUtils.sync(str);//强制写到硬盘            str.close();            ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);            try {                final StructStat stat = Libcore.os.stat(mFile.getPath());                synchronized (this) {                    mStatTimestamp = stat.st_mtime;//更新文件时间戳                    mStatSize = stat.st_size;//更新文件大小                }            } catch (ErrnoException e) {                // Do nothing            }            // Writing was successful, delete the backup file if there is one.            mBackupFile.delete();            mcr.setDiskWriteResult(true);            return;        } catch (XmlPullParserException e) {            Log.w(TAG, "writeToFile: Got exception:", e);        } catch (IOException e) {            Log.w(TAG, "writeToFile: Got exception:", e);        }        // Clean up an unsuccessfully written file        if (mFile.exists()) {            if (!mFile.delete()) {                Log.e(TAG, "Couldn't clean up partially-written file " + mFile);            }        }        mcr.setDiskWriteResult(false);    }public static boolean sync(FileOutputStream stream) {        try {            if (stream != null) {                stream.getFD().sync();//强制写硬盘            }            return true;        } catch (IOException e) {        }        return false;}
这里面还有一个跟commit长得很像的方法叫apply():

 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);    Runnable postWriteRunnable = new Runnable() {            public void run() {                awaitCommit.run();                QueuedWork.remove(awaitCommit);            }        };    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);//这个地方传递的postWriteRunnable不再是null    // 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()这个方法了,因为参数postWriteRunnable不是null,最终会执行:
QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
这是在单独的线程上做写硬盘的操作,写完以后会回调postWriteRunnable,等待写硬盘完成!


从上面的代码可以得出以下结论:
(1)SharedPreferences在第一次加载的时候,会从硬盘异步的读文件,然后会在内存做缓存。
(2)SharedPreferences的读都是读的内存缓存。
(3)如果是commmit()写,是先把数据更新到内存,然后同步到硬盘,整个过程是在同一个线程中同步来做的。
(4)如果是apply()写,首先也是写到内存,但是会另起一个线程异步的来写硬盘。因为我们在读的时候,是直接从内存读取的,因此,用apply()而不是commit()会提高性能。
(5)如果有多个key要写入,不要每次都commit或者apply,因为这里面会存在很多的加锁操作,更高效的使用方式是这样:editor.putInt("","").putString("","").putBoolean("","").apply();并且所有的putXXX()的结尾都会返回this,方便链式编程。
(6)这里面有三级的锁:SharedPreferences,Editor, mWritingToDiskLock。
mWritingToDiskLock是对应硬盘上的文件,Editor是保护mModified的,SharedPreferences是保护mMap的。
(7)假如应用会启动多个进程,并且在Application.onCreate()里面有读写SharedPreferences,一定要注意,SharedPreferences的文件名要加上进程名以区分,否则就会出现多个进程同时操作硬盘上同一个文件,应用就会crash!最典型的就是在AndroidManifest.xml这里配置了service,并且给service加上了android:process这个属性的时候。
参考:
http://stackoverflow.com/questions/19148282/read-speed-of-sharedpreferences
http://stackoverflow.com/questions/12567077/is-sharedpreferences-access-time-consuming
http://blog.csdn.net/hudashi/article/details/7913847

写了个帮助类,源码下载:http://download.csdn.net/download/goldenfish1919/6792707

apply只有在api level大于9的时候才可以使用:

Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD;

public class SharedPreferencesCompat {private static boolean mUseApply = Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD;/** * 如果找到则使用apply执行,否则使用commit *  * @param editor */public static void apply(final SharedPreferences.Editor editor) {if(mUseApply){editor.apply();}else{new AsyncTask<Void, Void, Void>() {@Overrideprotected Void doInBackground(Void... params) {editor.commit();return null;}}.execute(null, null, null);}}}





0 0
原创粉丝点击