说一说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中获取,不必要读取文件了。

相关推荐
hunterandroid2 小时前
Android WebView JSBridge 治理实战:从线上白屏崩溃到协议化通信
android·前端
一个用户名i2 小时前
【Compose 系列】第 1 篇:认识 Compose,为什么要学它
android·android jetpack
极客猴子3 小时前
iPhone实时转写软件推荐:会议录音功能真实体验
android·人工智能·飞书
达令哥4 小时前
告别 ARouter!基于 Google 官方 Navigation 3 + KSP 打造 Compose 时代的双轨制路由框架
android·前端
YF02114 小时前
Android App启动与权限管控
android
嘟哩DuliDuli4 小时前
AI 短剧生成为什么要有角色库、场景库和镜头卡
android·人工智能·安全·ai·软件工程
NINO6 小时前
深度解析 Android ANR:从标准分析流程到 NativePollOnce 疑难破局
android
阿巴斯甜6 小时前
AppFunctions 完整介绍
android
蜡台6 小时前
Jetpack Compose 从入门到精通
android·前端·kotlin·compose·jepack
toooooop87 小时前
踩坑:PHP7.2导出Excel正常,升级7.3文件损坏需修复
android·excel