likes
comments
collection
share

SharedPreference面试必会知识点及ANR

作者站长头像
站长
· 阅读数 5

[TOC]

概要

SharedPreferences(简称SP)是Android中很常用的数据存储方式,SP采用key-value(键值对)形式, 主要用于轻量级的数据存储, 尤其适合保存应用的配置参数

使用

SharedPreferences sharedPreferences = getSharedPreferences("setting", Context.MODE_PRIVATE);

Editor editor = sharedPreferences.edit();
editor.putString("current_city", "北京");
editor.commit();

/data/data/<package_name>/shared_prefs/setting.xml

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
   <string name="current_city">北京</string>
</map>

整体结构

SharedPreference面试必会知识点及ANR

关键点

  • 获取SP,没有文件会创建
  • 获取SP实际会创建SharedPreferencesImpl,会将文件内容加载到内存

获取SP

getSharedPreferences

关键点:

  • 没有xml的时候会创建文件
  • 返回的SP实际是SharedPreferencesImpl
  • SP文件打开后缓存位置
    • ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache
    • key是package_name
class ContextImpl extends Context {
    private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;

    private ArrayMap<String, File> mSharedPrefsPaths;

    public SharedPreferences getSharedPreferences(String name, int mode) {
        File file;
        synchronized (ContextImpl.class) {
            if (mSharedPrefsPaths == null) {
                mSharedPrefsPaths = new ArrayMap<>();
            }
            //先从mSharedPrefsPaths查询是否存在相应文件
            file = mSharedPrefsPaths.get(name);
            if (file == null) {
                //如果文件不存在, 则创建新的文件,文件的路径是/data/data/package name/shared_prefs/
                file = getSharedPreferencesPath(name);
                mSharedPrefsPaths.put(name, file);
            }
        }
        return getSharedPreferences(file, mode);
    }
}

public File getSharedPreferencesPath(String name) {
    return makeFilename(getPreferencesDir(), name + ".xml");
}

private File getPreferencesDir() {
    synchronized (mSync) {
        if (mPreferencesDir == null) {
            //创建目录
            mPreferencesDir = new File(getDataDir(), "shared_prefs");
        }
        return ensurePrivateDirExists(mPreferencesDir);
    }
} 

public SharedPreferences getSharedPreferences(File file, int mode) {
    checkMode(mode); 
    SharedPreferencesImpl sp;
    synchronized (ContextImpl.class) {
        final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
        sp = cache.get(file);
        if (sp == null) {
            //创建SharedPreferencesImpl
            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) {
        sp.startReloadIfChangedUnexpectedly();
    }
    return sp;
}

private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
    if (sSharedPrefsCache == null) {
        sSharedPrefsCache = new ArrayMap<>();
    }

    final String packageName = getPackageName();
    ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
    if (packagePrefs == null) {
        packagePrefs = new ArrayMap<>();
        sSharedPrefsCache.put(packageName, packagePrefs);
    }
    return packagePrefs;
}

一定会创建SharedPreferencesImpl

加载和初始化

  • SharedPreference加载文件使用异步方式,可利用此预加载
  • 将XML的KV读到一个map里,所以如果XML很大,会占用比较多的内存
    • 每次初始化会把所有kv全部加载到内存,提交时会把全部的数据更新到文件,所以整个文件不应该过大,否则影响性能。
    • 拆分:将使用频繁和非频繁、读频繁和写频繁的kv分开存储
SharedPreferencesImpl(File file, int mode) {
    mFile = file;
    //创建为.bak为后缀的备份文件
    mBackupFile = makeBackupFile(file);
    mMode = mode;
    mLoaded = false;
    mMap = null;
    startLoadFromDisk();
}

private void startLoadFromDisk() {
    synchronized (this) {
        mLoaded = false;
    }
    new Thread("SharedPreferencesImpl-load") {
        public void run() {
            loadFromDisk(); 
        }
    }.start();
}

private void loadFromDisk() {
    synchronized (SharedPreferencesImpl.this) {
        if (mLoaded) {
            return;
        }
        if (mBackupFile.exists()) {
            mFile.delete();
            mBackupFile.renameTo(mFile);
        }
    }

    Map map = null;
    StructStat stat = null;
    try {
        stat = Os.stat(mFile.getPath());
        if (mFile.canRead()) {
            BufferedInputStream str = null;
            try {
                str = new BufferedInputStream(new FileInputStream(mFile), 16*1024);
                map = XmlUtils.readMapXml(str);
            } catch (XmlPullParserException | IOException e) {
                ...
            } finally {
                IoUtils.closeQuietly(str);
            }
        }
    } catch (ErrnoException e) {
        ...
    }

    synchronized (SharedPreferencesImpl.this) {
        mLoaded = true;
        if (map != null) {
            mMap = map; //从文件读取的信息保存到mMap
            mStatTimestamp = stat.st_mtime; //更新修改时间
            mStatSize = stat.st_size; //更新文件大小
        } else {
            mMap = new HashMap<>();
        }
        notifyAll(); //唤醒处于等待状态的线程
    }
}

如果没有加载完,会等待一个锁,调用线程被阻塞

public String getString(String key, @Nullable String defValue) {
    synchronized (this) {
        //检查是否加载完成
        awaitLoadedLocked();
        String v = (String)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 {
            mLock.wait();
        } catch (InterruptedException unused) {
        }
    }
    if (mThrowable != null) {
        throw new IllegalStateException(mThrowable);
    }
}

可以看到实际上创建了mModified的map,用于记录要修改的内容

public Editor edit() {
    synchronized (this) {
        awaitLoadedLocked(); 
    }
    return new EditorImpl(); 
}

public final class EditorImpl implements Editor {
    private final Map<String, Object> mModified = Maps.newHashMap();
    private boolean mClear = false;

    //插入数据
    public Editor putString(String key, @Nullable String value) {
        synchronized (this) {
            //插入数据, 先暂存到mModified对象
            mModified.put(key, value);
            return this;
        }
    }
    //移除数据
    public Editor remove(String key) {
        synchronized (this) {
            mModified.put(key, this);
            return this;
        }
    }

    //清空全部数据
    public Editor clear() {
        synchronized (this) {
            mClear = true;
            return this;
        }
    }
}

落盘 commit apply

commit

关键点:

  • 将内存的KV的map和mModified进行merge

  • commit执行是isFromSyncCommit,直接将落盘的runnable run了起来。是同步的过程。

  • commit有返回值

  • 不要连续多次提交,应该获取一次获取edit(),然后多次执行putxxx()后批量提交,封装时尤其要注意

public boolean commit() {
    //将数据更新到内存
    MemoryCommitResult mcr = commitToMemory();
    //将内存数据同步到文件
    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null);
    try {
        //进入等待状态, 直到写入文件的操作完成
        mcr.writtenToDiskLatch.await();
    } catch (InterruptedException e) {
        return false;
    }
    //通知监听则, 并在主线程回调onSharedPreferenceChanged()方法
    notifyListeners(mcr);
    // 返回文件操作的结果数据
    return mcr.writeToDiskResult;
}

private MemoryCommitResult commitToMemory() {
    MemoryCommitResult mcr = new MemoryCommitResult();
    synchronized (SharedPreferencesImpl.this) {
        if (mDiskWritesInFlight > 0) {
            mMap = new HashMap<String, Object>(mMap);
        }
        //这里就是要写到磁盘的map
        mcr.mapToWriteToDisk = mMap;
        mDiskWritesInFlight++;

        //是否有监听key改变的监听者
        boolean hasListeners = mListeners.size() > 0;
        if (hasListeners) {
            mcr.keysModified = new ArrayList<String>();
            mcr.listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
        }

        synchronized (this) {
            //当mClear为true, 则直接清空mMap
            if (mClear) {
                if (!mMap.isEmpty()) {
                    mcr.changesMade = true;
                    mMap.clear();
                }
                mClear = false;
            }

            for (Map.Entry<String, Object> e : mModified.entrySet()) {
                String k = e.getKey();
                Object v = e.getValue();
                //注意此处的this是个特殊值, 用于移除相应的key操作.
                if (v == this || v == null) {
                    if (!mMap.containsKey(k)) {
                        continue;
                    }
                    mMap.remove(k);
                } else {
                    if (mMap.containsKey(k)) {
                        Object existingValue = mMap.get(k);
                        if (existingValue != null && existingValue.equals(v)) {
                            continue;
                        }
                    }
                    mMap.put(k, v);
                }

                mcr.changesMade = true; // changesMade代表数据是否有改变
                if (hasListeners) {
                    mcr.keysModified.add(k); //记录发生改变的key
                }
            }
            mModified.clear(); //清空EditorImpl中的mModified数据
        }
    }
    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--;
                }
                //此时postWriteRunnable为null不执行该方法
                if (postWriteRunnable != null) {
                    postWriteRunnable.run();
                }
            }
        };

    final boolean isFromSyncCommit = (postWriteRunnable == null);

    if (isFromSyncCommit) { //commit方法会进入该分支
        boolean wasEmpty = false;
        synchronized (SharedPreferencesImpl.this) {
            //commitToMemory过程会加1,则wasEmpty=true
            wasEmpty = mDiskWritesInFlight == 1;
        }
        if (wasEmpty) {
            //跳转到上面
            writeToDiskRunnable.run();
            return;
        }
    }
    //不执行该方法
    QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}

apply

实际使用HandlerThread去执行的

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);
}
QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
public static void queue(Runnable work, boolean shouldDelay) {
    Handler handler = getHandler();

    synchronized (sLock) {
        sWork.add(work);

        if (shouldDelay && sCanDelay) {
            handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
        } else {
            handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
        }
    }
}

private static Handler getHandler() {
    synchronized (sLock) {
        if (sHandler == null) {
            HandlerThread handlerThread = new HandlerThread("queued-work-looper",
                    Process.THREAD_PRIORITY_FOREGROUND);
            handlerThread.start();

            sHandler = new QueuedWorkHandler(handlerThread.getLooper());
        }
        return sHandler;
    }
}

该用哪一个

  • apply() 没有返回值而 commit() 返回 boolean 表明修改是否提交成功
  • commit() 是把内容同步提交到硬盘的,而 apply() 先立即把修改提交到内存,然后开启一个异步的线程提交到硬盘,并且如果提交失败,你不会收到任何通知。
  • get 操作都是线程安全的, 并且 get 仅仅是从内存中 (mMap) 获取数据

问题

SharedPreferences 是线程安全的吗

  • SharePreferences是线程安全的里面的方法有大量的synchronized来保障(效率低,很多synchronized),但不是进程安全

ANR

  • 第一次读取完毕之前 所有的get/set请求都会被卡住 等待读取完毕后再执行,所以第一次读取会有ANR风险。

  • 即使是apply,在没有落盘结束,也可能会ANR

SharedPreference面试必会知识点及ANR SharedPreference面试必会知识点及ANR SharedPreference面试必会知识点及ANR

总结

  • SharedPreference加载文件使用异步方式,可利用此预加载
  • 每次初始化会把所有kv全部加载到内存,提交时会把全部的数据更新到文件,所以整个文件不应该过大,否则影响性能。拆分原则:将使用频繁和非频繁的kv分开存储,将读频繁和写频繁的kv分开存储
  • 不要连续多次提交,应该获取一次获取edit(),然后多次执行putxxx()后批量提交,封装时尤其要注意
  • apply虽是异步提交,但会引起卡顿/ANR

SP的替代方案:MMKV

内存准备

通过 mmap 内存映射文件,提供一段可供随时写入的内存块,App 只管往里面写数据,由操作系统负责将内存回写到文件,不必担心 crash 导致数据丢失。

mmap回写磁盘时机

  • 内存不足
  • 进程退出
  • 调用msync和munmap
  • 一定延迟时间后

数据组织

数据序列化方面我们选用 protobuf 协议,pb 在性能和空间占用上都有不错的表现。考虑到我们要提供的是通用 kv 组件,key 可以限定是 string 字符串类型,value 则多种多样(int/bool/double 等)。要做到通用的话,考虑将 value 通过 protobuf 协议序列化成统一的内存块(buffer),然后就可以将这些 KV 对象序列化到内存中。

转载自:https://juejin.cn/post/6982742727528022052
评论
请登录