说一说SharedPreferences的线程同步和加载时机

前言

SharedPreferences是Android中经常使用的功能之一,不过很多时候对它内部的实现并没有太多关注,比如合适加载它的本地缓存到内存中?有没有线程同步的问题。这篇文章就几个平时不太关注的点来简单说一说

由于SharedPreferences是接口,另外context是抽象类,所以要想看源码,需要在它们的实现类中,在android-sdk\sources\android-xxx\android\app下,分别是SharedPreferencesImpl.java和ContextImpl.java。

线程问题

SharedPreferences是线程同步的,这点在SharedPreferencesImpl下对应的get、put等函数中都可以看到,第一步就是请求同步锁,如:

java 复制代码
@Override
public boolean getBoolean(String key, boolean defValue) {
    synchronized (mLock) {
        awaitLoadedLocked();
        Boolean v = (Boolean)mMap.get(key);
        return v != null ? v : defValue;
    }
}

@Override
public Editor putString(String key, @Nullable String value) {
    synchronized (mEditorLock) {
        mModified.put(key, value);
        return this;
    }
}

commit和apply

老生常谈了,commit会立刻写入内存和文件,所以有返回值,是否成功。而apply则先写入内存,然后异步写入文件,所以没有返回值。

如果不关心返回值,则最好使用apply。因为可以减少io操作,比如同时大量的修改,apply会先将修改封装并放入一个队列,然后通过handler发送一个消息,如果消息执行,则将队列中的事件处理。因为handler的消息并不一定及时执行,比如前面消息很多,或者ui操作比较多,所以会延迟集中处理,这样也节省了系统资源。

不论commit还是apply,都会执行enqueueDiskWrite,源码如下:

java 复制代码
private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) {
    final boolean isFromSyncCommit = (postWriteRunnable == null);

    final Runnable writeToDiskRunnable = new Runnable() {
            @Override
            public void run() {
                synchronized (mWritingToDiskLock) {
                    writeToFile(mcr, isFromSyncCommit);
                }
                synchronized (mLock) {
                    mDiskWritesInFlight--;
                }
                if (postWriteRunnable != null) {
                    postWriteRunnable.run();
                }
            }
        };

    // Typical #commit() path with fewer allocations, doing a write on
    // the current thread.
    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (mLock) {
            wasEmpty = mDiskWritesInFlight == 1;
        }
        if (wasEmpty) {
            writeToDiskRunnable.run();
            return;
        }
    }

    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

如果是apply,则isFromSyncCommit为false,所以执行

java 复制代码
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);

这个函数源码:

java 复制代码
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);
        }
    }
}

可以看到通过handler来处理。

加载时机

SharedPreferences本质上也是文件,那么何时读取这个文件的呢?

看context的getSharedPreferences函数(ContextImpl中):

java 复制代码
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
    // 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";
        }
    }

    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) {
        final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
        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;
}

可以看到,执行这个函数后,就先在cache中判断是否存在,如果不存在才会取读取文件。所以第一次执行这个函数时就是读取文件的时候(根据name)。后面再执行这个函数(相同name)就会从cache中获取,不必要读取文件了。

相关推荐
魑魅魍魉都是鬼10 分钟前
随缘玩 一: 代理模式
android·java·代理模式
auxor1 小时前
Android AMS拦截Activity启动
android
勤劳打代码1 小时前
曲径通幽 —— Android 息屏 TCP 连接管理
android·tcp/ip·flutter
恋猫de小郭2 小时前
Flutter 里的 Layer 解析,带你了解不一样角度下的 Flutter 渲染逻辑
android·前端·flutter
vivo高启强2 小时前
FD 泄露引发的AGP8 build 失败问题
android
用户2018792831672 小时前
Binder 事务失败(FAILED BINDER TRANSACTION)
android
柿蒂3 小时前
Android图片批量添加处理优化:从「30」秒缩短至「4.4」秒
android·android jetpack
拓端研究室4 小时前
专题:2025医药生物行业趋势与投融资研究报告|附90+份报告PDF、原数据表汇总下载
android·开发语言·kotlin
小鱼人爱编程4 小时前
当上组长一年里,我保住了俩下属
android·前端·后端
2501_916013744 小时前
移动端 WebView 调试实战,多平台行为差异排查与统一调试流程
android·ios·小程序·https·uni-app·iphone·webview