Android SharedPreferences源码都不会,怎么通过面试?

SP整体认知

sp结构.png

SharedPreferences的流程是非常简单的,在ContextImpl中初始化,在SharedPreferenceImpl实现读写数据。先大概看一下sp的整体结构,先不用记住,后面还会说的,大概知道有这么几个类就行。

  • SharedPreferences是个接口,它内部还有2个接口Editor和OnSharedPreferenceChangeListener,其中Editor是写入数据的,就是那个put()方法,OnSharedPreferenceChangeListener是在sp文件改变的时候回调的接口,此外SharedPreferences内部还有获取数据的方法,也就是get()方法。
  • SharedPreferenceImpl是SharedPreferences的实现类,EditorImpl是Editor的实现类,它是SharedPreferenceImpl的内部类。
  • 小结一下,SharedPreferenceImpl是重中之重,初始化、读写都在这里完成。

sp文件

SharedPreferences实际上是一个xml文件,存储位置在

/data/data/应用包名/shared_prefs/xx.xml

sp文件内容.png

SharedPreferences基本用法

val sp: SharedPreferences = context.getSharedPreferences("zx", Context.MODE_PRIVATE)
        val editor = sp.edit()
        editor.putString("key0", "11")
        editor.commit() //同步提交
        editor.apply() //异步提交
        val result = sp.getString("key0", "")      

SharedPreferences有多种获取方式,但是最终都是通过ContextImpl的getSharedPreferences()方法获取的,这一步也叫初始化,后面还有获取、提交数据,很简单吧,一共就3个部分,下面就来具体分析一下。

sp存储初认识

class ContextImpl extends Context {
    private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
  
    private ArrayMap<String, File> mSharedPrefsPaths;
}

class SharedPreferencesImpl implements SharedPreferences {
    private Map<String, Object> mMap;
}

class EditorImpl implements Editor {
    private final Map<String, Object> mModified = new HashMap<>();
}
  • 从文件上说,SharedPreferences是xml文件,每个xml文件都对应一个存储键值对的map。
  • 从内存上说,每个xml文件都会被加载进内存缓存起来,这样避免了频繁的I/O,提升了性能。所以先来了解一下缓存相关的几个map。
    mSharedPrefsPaths:保存了sp文件名-xml文件的映射关系
    sSharedPrefsCache:保存了xml文件对应的操作它的SharedPreferencesImpl的对象,据查源码,它内部只保存了一个键值对,key就是app的包名。
    mMap:保存了xml文件的内容
    mModified:调用put()方法时,临时保存修改内容,在调用commit/apply之后保存到mMap中,并且写进xml文件里。

好了,有个整体认识之后,开始源码解析。

获取SharePreferences

##ContextImpl
public SharedPreferences getSharedPreferences(String name, int mode) {
     //...省略一些检测代码

        File file;
        synchronized (ContextImpl.class) {
            if (mSharedPrefsPaths == null) {
                mSharedPrefsPaths = new ArrayMap<>();
            }
            //先从缓存获取xml文件
            file = mSharedPrefsPaths.get(name);
            if (file == null) {
                //创建name.xml文件
                file = getSharedPreferencesPath(name);
                //放入缓存
                mSharedPrefsPaths.put(name, file);
            }
        }
        return getSharedPreferences(file, mode);
    }

    //在指定目录创建name.xml文件
    public File getSharedPreferencesPath(String name) {
        return makeFilename(getPreferencesDir(), name + ".xml");
    }
##ContextImpl
public SharedPreferences getSharedPreferences(File file, int mode) {
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
            //从缓存sSharedPrefsCache获取
            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;
    }

//从缓存sSharedPrefsCache读取SharedPreferencesImpl实例
 private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
        if (sSharedPrefsCache == null) {
            sSharedPrefsCache = new ArrayMap<>();
        }

        final String packageName = getPackageName();
        //从缓存sSharedPrefsCache中查看是否有对应包名下的SharedPreferences
        ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
        if (packagePrefs == null) {
          //没有的话创建一个空的map,然后放入缓存
            packagePrefs = new ArrayMap<>();
            sSharedPrefsCache.put(packageName, packagePrefs);
        }

        return packagePrefs;
    }

getSharedPreferences()就是获取SharedPreferencesImpl对象,也就是真正实现sp的类,先从缓存sSharedPrefsCache获取,没有的话就new一个并且放入缓存。再来看一下ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache对象,key为包名,值是一个ArrayMap,值其实是在getSharedPreferences(File file, int mode)创建的,sSharedPrefsCache的size最多为1,值ArrayMap<File, SharedPreferencesImpl>存储的是xml文件-sp实现类SharedPreferencesImpl映射,我们知道xml是可以有多个的,所以sSharedPrefsCache的值可以有多个。

小结:在ContextImpl中通过缓存获取或者创建了SharedPreferencesImpl对象,它是sp真正的核心类。

核心类SharedPreferencesImpl

它实现了SharedPreferences接口,sp的初始化、获取、写入数据都是在这里完成的。

SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        mBackupFile = makeBackupFile(file);
        mMode = mode;
        mLoaded = false; //是否加载xml文件成功,默认false
        mMap = null;
        mThrowable = null;
        //关键是这里
        startLoadFromDisk();
    }

    
    private void startLoadFromDisk() {
       
        synchronized (mLock) {
            mLoaded = false;
        }
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                //开启子线程,从磁盘加载、解析xml文件
                loadFromDisk();
            }
        }.start();
    }
 private void loadFromDisk() {
        synchronized (mLock) {
            //如果加载过了直接返回
            if (mLoaded) {
                return;
            }
          
               //解析xml后,需要缓存到内存中,用map来保存
                Map<String, Object> map = null;
                BufferedInputStream str = null;
                try {
                    str = new BufferedInputStream(
                            new FileInputStream(mFile), 16 * 1024);
                    map = (Map<String, Object>) XmlUtils.readMapXml(str);
                } catch (Exception e) {
                    Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
                } finally {
                    IoUtils.closeQuietly(str);
                }
            }

      ...
      //加载成功之后,设置一下标识位,并且唤醒线程
      synchronized (mLock) {
            mLoaded = true;
            try {
                if (thrown == null) {
                    if (map != null) {
                         //这个就是之前提到过的SharedPreferencesImpl中的成员变量mMap  ,这里给它复值
                        mMap = map;
                    } else {
                        mMap = new HashMap<>();
                    }
                }
            } catch (Throwable t) {
                mThrowable = t;
            } finally {
                //这里很关键,加载完成之后唤醒等待阻塞的线程
                mLock.notifyAll();
            }
        }
    }

这里省略了一些无关的判断,这里的逻辑比较简单,通过XmlUtils工具类去读取xml文件内容,其实就是用了XmlParser解析,从xml文件取到数据之后就缓存到mMap中,后面就可以直接从内存读取了,这样就提高了读取效率。

读取数据

##SharedPreferencesImpl
public String getString(String key, @Nullable String defValue) {
        //这里之所以要加锁,是因为awaitLoadedLocked()里面用了wait()等待阻塞,wait必须要加锁才能用
        synchronized (mLock) {
            //如果xml文件没有加载完成,就一直等待阻塞
            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();
        }

        //循环等待xml文件加载完成,加载完成之后会唤醒阻塞
        while (!mLoaded) {
            try {
                mLock.wait();
            } catch (InterruptedException unused) {
            }
        }
        if (mThrowable != null) {
            throw new IllegalStateException(mThrowable);
        }
    }

这里以getString()为例,读取的时候会先加锁,如果xml文件没有加载完成,就一直等待阻塞;如果加载完成会自动唤醒阻塞线程,并且从内存获取数据。

修改数据

修改数据其实分为2部分,临时存放修改数据(put)和提交修改数据(commit、apply)。

临时存放数据Editor

通常向SharedPreferences存放数据的时候,是通过如下方式完成的

 val editor: SharedPreferences.Editor = sp.edit()
 editor.putString("key0", "11")

首先调用SharedPreferences的edit()方法获取Editor对象,然后调用put()方法,注意:Editor是一个接口,它的实现类是EditorImpl。

##SharedPreferencesImpl
@Override
    public Editor edit() {
       //阻塞等待,直到xml加载完成
        synchronized (mLock) {
            awaitLoadedLocked();
        }

        return new EditorImpl();
    }

class EditorImpl implements Editor {
        private final Object mEditorLock = new Object();
        //临时存储要提交数据的map
        private final Map<String, Object> mModified = new HashMap<>();

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

这里代码很简单,创建Editor对象之前会先判断xml文件是否加载完成,每次edit()调用都是创建一个新的对象。put()方法是线程安全的,它只是把数据存放到一个临时的Map集合,并没有提交到内存缓存和磁盘。

提交修改数据:commit()和apply()

 public boolean commit() {
            //将Editor修改的内容提交到内存缓存mMap
            MemoryCommitResult mcr = commitToMemory();
            //把数据写入磁盘文件
            SharedPreferencesImpl.this.enqueueDiskWrite(
                mcr, null /* sync write on this thread okay */);
            try {
                //CountDownLatch阻塞等待
                mcr.writtenToDiskLatch.await();
            } catch (InterruptedException e) {
                return false;
            } 
            //回调通知
            notifyListeners(mcr);
            //写入磁盘文件是否成功
            return mcr.writeToDiskResult;
        }


public void apply() {
           //修改内存缓存mMap
            final MemoryCommitResult mcr = commitToMemory();
            //等待写入文件完成的任务
            final Runnable awaitCommit = new Runnable() {
                    @Override
                    public void run() {
                        try {
                            //阻塞等待写入文件完成,否则阻塞在这
                            //利用CountDownLatch来等待任务的完成
//后面执行enqueueDiskWrite写入文件成功后会把writtenToDiskLatch多线程计数器减1, 这样的话下面的阻塞代码就可以通过了.
                            mcr.writtenToDiskLatch.await();
                        } catch (InterruptedException ignored) {
                        }
                    }
                };
            //QueuedWork是用来确保SharedPrefenced的写操作在Activity 销毁前执行完的一个全局队列. 
            //QueuedWork里面的队列是通过LinkedList实现的,LinkedList不仅可以做链表,也可以做队列
            //添加到全局的工作队列中
            QueuedWork.addFinisher(awaitCommit);

          //这个任务是等待磁盘写入完成,然后从队列中移除任务
            Runnable postWriteRunnable = new Runnable() {
                    @Override
                    public void run() {
                        //执行阻塞任务
                        awaitCommit.run();
                        //阻塞完成之后,从队列中移除任务
                        QueuedWork.removeFinisher(awaitCommit);
                    }
                };
            //异步执行磁盘文件写入,注意这里和commit不同的是postWriteRunnable不为空
            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

       
            notifyListeners(mcr);
        }

commit()方法很简单,就3步,写入内存缓存、写入磁盘文件、返回写入文件结果。apply的流程和commit差不多,只是apply没有返回值。二者都会调用enqueueDiskWrite写入文件,commit的postWriteRunnable参数为null,apply是有值的。

private MemoryCommitResult commitToMemory() {
      ...只保留关键代码
          //mModified就是之前edit().put()来存放的数据
          for (Map.Entry<String, Object> e : mModified.entrySet()) {
                        String k = e.getKey();
                        Object v = e.getValue();
                    // 如果执行了remove,则v对应的this,将这些key/value从mMap移除
                        if (v == this || v == null) {
                            if (!mapToWriteToDisk.containsKey(k)) {
                                continue;
                            }
                            mapToWriteToDisk.remove(k);
                        } else {
                            //将mModified中要修改的数据写到内存缓存mMap中
                            if (mapToWriteToDisk.containsKey(k)) {
                                Object existingValue = mapToWriteToDisk.get(k);
                                if (existingValue != null && existingValue.equals(v)) {
                                    continue;
                                }
                            }
                            mapToWriteToDisk.put(k, v);
                        }

                        changesMade = true;
                        if (hasListeners) {
                            keysModified.add(k);
                        }
                    }
                //清空该次修改的记录
                    mModified.clear();

      return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
                    mapToWriteToDisk);
}

commitToMemory就是将mModified中的修改写入到内存缓存中,接着看一下写入文件方法enqueueDiskWrite()

##SharedPreferencesImpl
private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
        //commit和apply的区别,commit 的postWriteRunnable参数为null,而apply是有值的
        final boolean isFromSyncCommit = (postWriteRunnable == null);

        //写入磁盘文件的任务
        final Runnable writeToDiskRunnable = new Runnable() {
                @Override
                public void run() {
                    synchronized (mWritingToDiskLock) {
                        //就是将MemoryCommitResult的mapToWriteToDisk写入到文件,其实就是mMap的内容
                        // 写入文件完成之后,会调用writtenToDiskLatch.countDown()将计数器-1,这样就不会阻塞了
                        writeToFile(mcr, isFromSyncCommit);
                    }
                    synchronized (mLock) {
                        mDiskWritesInFlight--;
                    }
                    //apply才执行
                    if (postWriteRunnable != null) {
                        // 写文件成功后则执行移除全局队列中的任务的任务.
                        // 此时waitCommit 任务就不会阻塞了, 因为writtenToDiskLatch==0 了.
                        // 不阻塞 QueuedWork.remove(awaitCommit); 就会被调用, 也就是说该任务执行完了
                      // 会将该任务从队列中移除
                        postWriteRunnable.run();
                    }
                }
            };

       //commit才执行,即在UI线程写入文件
        if (isFromSyncCommit) {
            boolean wasEmpty = false;
            synchronized (mLock) {
                // mDiskWritesInFlight 会在commitToMemory() 方法中进行+1 操作
                wasEmpty = mDiskWritesInFlight == 1;
            }
            if (wasEmpty) {
                // 在当前线程执行写任务
                //这里是直接调用Runnable的run方法,就是普通的方法调用,而不是开启子线程,懂了吧
                writeToDiskRunnable.run();
                return;
            }
        }
        
        //apply会调用这个,里面会通过HandlerThread去执行writeToDiskRunnable
        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
    }

小结:apply的逻辑稍微复杂了一点,大家仔细看。
apply是异步操作,每次调用apply会把写入任务放在一个LinkedList实现的队列中,然后在QueuedWork中通过一个HandlerThread串行的执行写入文件的任务。排队是通过CountDownLatch来实现的,它其实是一个多线程计数器,调用它的await可以阻塞等待写入文件的子线程(HandlerThread)完成,在写入文件完成之后会调用它的countDown()将计数器-1,这样writtenToDiskLatch==0就不会阻塞等待了,然后再移除队列中的该任务。

##QueuedWork
private static final LinkedList<Runnable> sWork = new LinkedList<>();
/** If new work can be delayed or not */
private static boolean sCanDelay = true;

public static void queue(Runnable work, boolean shouldDelay) {
        //获取子线程的Handler,通过HandlerThread创建的
        Handler handler = getHandler();

        synchronized (sLock) {
            //Runnable加入list排队
            sWork.add(work);
            //apply传入的shouldDelay为true
            if (shouldDelay && sCanDelay) {
   //apply执行这句,默认延迟100ms执行任务             handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
            } else {
                handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
            }
        }
    }

//懒加载创建子线程的Handler,后面的写入文件的任务都在子线程完成
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;
        }
    }

private static class QueuedWorkHandler extends Handler {
        static final int MSG_RUN = 1;

        QueuedWorkHandler(Looper looper) {
            super(looper);
        }

        public void handleMessage(Message msg) {
            if (msg.what == MSG_RUN) {
                //处理排队的写文件任务
                processPendingWork();
            }
        }
    }

这里代码量不少,但是逻辑却很简单,把apply提交的任务加到一个LinkedList中,然后开启子线程去串行的执行任务。

private static void processPendingWork() {
       synchronized (sProcessingWork) {
            LinkedList<Runnable> work;

            synchronized (sLock) {
                work = (LinkedList<Runnable>) sWork.clone();
                sWork.clear();

                // Remove all msg-s as all work will be processed now
                getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
            }
            //太简单了,串行处理排队的写文件任务
            //注意:这里已经在HandlerThread这个子线程中了
            if (work.size() > 0) {
                for (Runnable w : work) {
                    w.run();
                }
            }
        }
    }

好了,到这里SharedPreferences源码就分析完了。

总结

  • 1、初始化:通过XmlUtils这个工具类读取、解析xml文件,并且把数据加载到内存缓存中,这样避免了频繁的 I/O,提升了读取数据的效率;
  • 2、写操作:通过Editor把数据存放到临时map集合,当调用commit()/apply()的时候,再把数据分别提交到内存和文件;
  • 3、读操作:需要先阻塞等待加载文件完成,然后再从内存中读取数据;第一次会比较慢,以后就很快;
  • 4、commit()有返回值,是在主线程写入文件;apply()没有返回值,在子线程写入文件。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,242评论 5 461
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,138评论 2 372
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,412评论 0 323
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,380评论 1 266
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,221评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,203评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,607评论 3 384
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,292评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,581评论 1 292
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,650评论 2 311
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,405评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,268评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,646评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,942评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,226评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,601评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,795评论 2 335