zoukankan      html  css  js  c++  java
  • Android-SharedPreferences源码学习与最佳实践

    最近有个任务是要做应用启动时间优化,然后记录系统启动的各个步骤所占用的时间,发现有一个方法是操作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):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    /**
       * Map from package name, to preference name, to cached preferences.
       */
    private static ArrayMap<string, arraymap<string,="" sharedpreferencesimpl="">> sSharedPrefs;//在内存的一份缓存
     
    @Override
    public 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;
        }</string,></string,></string,></string,>

    getSharedPreferences()做的事情很简单,一目了然,我们重点看下SharedPreferencesImpl.java这个类:
    SharedPreferencesImpl.java(https://github.com/android/platform_frameworks_base/blob/master/core/java/android/app/SharedPreferencesImpl.java)
    首先是构造函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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-v
    private final Object mWritingToDiskLock = new Object();//写硬盘文件时候加锁
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    //从硬盘加载
    private void startLoadFromDisk() {
            synchronized (this) {//先把状态置为未加载
                mLoaded = false;
            }
            new Thread("SharedPreferencesImpl-load") {//开了一个线程,异步加载
                public void run() {
                    synchronized (SharedPreferencesImpl.this) {
                        loadFromDiskLocked();//由SharedPreferencesImpl.this锁保护
                    }
                }
            }.start();
        }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    //从硬盘加载
    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();//唤醒等待线程
        }
    </string,>

    然后我们随便看一个读请求:

    1
    2
    3
    4
    5
    6
    7
    public int getInt(String key, int defValue) {
           synchronized (this) {//还是得首先获取this锁
               awaitLoadedLocked(); //这一步完成以后,说明肯定已经加载完了
               Integer v = (Integer)mMap.get(key);//直接从内存读取
               return v != null ? v : defValue;
           }
       }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    //等待数据加载完成
     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来做的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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,它是一个内部类
        }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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;
                }
            }
    }
    </string,>

    看一下commit:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    // Returns true if any changes were made
    private 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;
    }</string,></onsharedpreferencechangelistener></string></string,>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    //这是随后的写硬盘
     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都干了些什么:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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();//这个是写完以后的回调
                }
            }
        };

    下面是真正要写硬盘了:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    // 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);
        }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public static boolean sync(FileOutputStream stream) {
            try {
                if (stream != null) {
                    stream.getFD().sync();//强制写硬盘
                }
                return true;
            } catch (IOException e) {
            }
            return false;
    }

    这里面还有一个跟commit长得很像的方法叫apply():

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    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的。
    参考:
    http://stackoverflow.com/questions/19148282/read-speed-of-sharedpreferences
    http://stackoverflow.com/questions/12567077/is-sharedpreferences-access-time-consuming

    原文:http://www.2cto.com/kf/201312/268547.html

  • 相关阅读:
    java设计模式
    漏桶算法工具类
    http请求requestUtils
    去掉字符串中特殊符号造成的空格
    java 分布式id生成算法
    java枚举
    java 32个Java面试必考点
    配置tomcat下war包可以自压缩
    tomcat (选号)公司tomcat无页面解决
    docker 12 docker容器数据卷
  • 原文地址:https://www.cnblogs.com/veins/p/4269479.html
Copyright © 2011-2022 走看看