• 说一说SharedPreferences中的线程同步和加载时机
  • 发布于 1个月前
  • 63 热度
    0 评论
前言
这两个问题也是面试时偶尔会问到的,所以今天一起来看看源码,看看细节上是如何处理的。由于SharedPreferences是接口,另外context是抽象类,所以要想看源码,需要在它们的实现类中,在android-sdk\sources\android-xxx\android\app下,分别是SharedPreferencesImpl.java和ContextImpl.java。

线程问题
SharedPreferences是线程同步的,这点在SharedPreferencesImpl下对应的get、put等函数中都可以看到,第一步就是请求同步锁,如:
@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,源码如下:
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();
                }
            }
        };
     // 堆代码 duidaima.com
    // 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,所以执行
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
这个函数源码:
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中):
@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中获取,不必要读取文件了。
用户评论