codeegginterviewgroup / CodeEggDailyInterview

码个蛋每日面试题
395 stars 55 forks source link

SharedPreferences 的加载实现是在子线程,但是为什么说 getX(key) 操作可能还会阻塞主线程呢? #169

Open kukyxs opened 4 years ago

kukyxs commented 4 years ago
//getSharedPreferences 获取 SP 的核心源码
class ContextImpl extends Context {
    //Context会把对应 SP 缓存起来
    private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
    ......
    @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) {
                //第一次或者新进程中时缓存没有就新 new 一个对应实例
                sp = new SharedPreferencesImpl(file, mode);
                cache.put(file, sp);
                return sp;
            }
        }
        ......
        return sp;
    }
    ......
}

接着看看 SharedPreferencesImpl 实例化及通过 SharedPreferences 获取一个指定 key 的值的核心源码部分

//一个File对应的一个SP实例
final class SharedPreferencesImpl implements SharedPreferences {
    //用来存储该SP的所有Key-Value对 
    private Map<String, Object> mMap;
    SharedPreferencesImpl(File file, int mode) {
        ......
        //构造方法从磁盘加载SP文件
        startLoadFromDisk();
    }
    private void startLoadFromDisk() {
        ......
        //启动一个名字为SharedPreferencesImpl-load的线程从磁盘读文件
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                loadFromDisk();
            }
        }.start();
    }
    //子线程中读取文件解析成key-value对Map
    private void loadFromDisk() {
        ......
        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 (Exception e) {
                    Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
                } finally {
                    IoUtils.closeQuietly(str);
                }
            }
        } catch (ErrnoException e) {
            /* ignore */
        }
        synchronized (mLock) {
            mLoaded = true;
            if (map != null) {
                mMap = map;
                mStatTimestamp = stat.st_mtime;
                mStatSize = stat.st_size;
            } else {
                mMap = new HashMap<>();
            }
            //子线程读取SP文件到mMap成功后通过notify通知释放阻塞锁
            mLock.notifyAll();
        }
    }
    //通过SharedPreferences对象获取指定key的值
    //(一般与SharedPreferences获取在一个线程,主线程)
    public int getInt(String key, int defValue) {
        synchronized (mLock) {
            //阻塞等待SP读取到内存后再get
            awaitLoadedLocked();
            Integer v = (Integer)mMap.get(key);
            return v != null ? v : defValue;
        }
    }
    private void awaitLoadedLocked() {
        ......
        while (!mLoaded) {
            try {
                //阻塞等待SP读取到内存完成
                mLock.wait();
            } catch (InterruptedException unused) {
            }
        }
    }
    ......
}

所以说 SharedPreferences 读取 Map 虽然在子线程,但是其 getX(key) 系列方法想要调用的前提是 SharedPreferences 子线程已经读取完成,否则就会阻塞,所以 SharedPreferences 中如果存储太大内容或者太多内容导致 XML 解析等变慢就会导致后面的 getX(key) 阻塞主线程,从而导致主线程卡顿,所以说 SharedPreferences 是轻量级的持久化工具。