距离上一期的每周一轮子已通过去了好久了,离开的这段时间,去创业作了产品经理的工做,而后项目都失败了,如今重启开始新的技术之路,前段时间在面试,因此对于基础知识点进行了从新的整理,因此结合着面试的内容,将对Android中的第三方框架还有FrameWork层的内容进行更系统的一个整理。开始第一篇,准备先从一个简单的入手,咱们最多见的Android中的最多见的一种数据持久化方式,SharedPreferenced,经过SharePreference咱们能够以键值对的形式来进行数据的存取。按照常规书写惯例先从写法入手。android
面经快速进入通道
[快手,字节跳动,百度,美团Offer之旅(Android面经分享)](https://juejin.im/post/5e9102...
)面试
SharedPreferences preferences = getSharedPreferences("name", MODE_PRIVATE); preferences.getString("name", ""); preferences.edit().putString("name", null).apply(); preferences.edit().putString("name", null).commit();
上述是SharedPreferences的一个实现方式,经过指定名称和类型来获取一个SharedPreference,对于类型后面会展开来说,而后经过get方法能够根据键值来获取相应存取的值,对于数据的写入,经过edit方法后调用相应数据类型的put方法来添加数据,最后调用apply和commit方法来提交数据。那么接下来,咱们来跟进一下看SharedPreferences是如何实现读写操做的,还有不一样的类型的SharedPreference的差别性在哪里。数据库
下面是SharedPreferences支持的MODE缓存
只能够被当前建立的应用读取或者共享userid的应用多线程
其它应用能够进行读app
其它应用能够读写框架
上述是SharedPreferences实现的常见三种MODEide
安装在设备中的每个Android包文件(.apk)都会被分配到一个属于本身的统一的Linux用户ID,而且为它建立一个沙箱,以防止影响其余应用程序(或者其余应用程序影响它)。用户ID 在应用程序安装到设备中时被分配,而且在这个设备中保持它的永久性。经过Shared User id,拥有同一个User id的多个APK能够配置成运行在同一个进程中.因此默认就是能够互相访问任意数据. 也能够配置成运行成不一样的进程,同时能够访问其余APK的数据目录下的数据库和文件.就像访问本程序的数据同样.函数
在context中,咱们能够经过调用getSharePreferenced方法来获取SharePreferences,那么咱们先来看一下其具体实现。post
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);
首先根据名称从SharedPrefsPaths中进行查找,SharedPrefsPaths是一个ArrayMap,经过咱们传递的名称做为键,磁盘存储文件File做为值,当SharedPrefsPaths为null的时候,咱们建立一个,而后从中根据name来获取值,得不到值的时候,调用getSharedPreferencesPath来建立File,而后将其存入到SharedPrefsPaths之中,最后再调用getSharedPreferences根据File和Mode来获取SharePreference
SharePrefsPaths是一个ArrayMap经过name做为key,经过File做为value,当找不到File的时候,就会根据name在指定的文件夹下建立一个名为name.xml的文件。而后将其缓存起来。
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; } } .... return sp; }
SharedPreferences的实际获取是经过一个以File为键,以SharedPreferencesImpl为值的ArrayMap中存放的,当Cache中查找不到的时候,则会从新建立。整个SharedPreferences的核心实现就是在SharedPreferencesImpl之中。下面,咱们来看一下SharedPreferencesImpl的具体实现。
SharedPreferencesImpl(File file, int mode) { mFile = file; mBackupFile = makeBackupFile(file); mMode = mode; mLoaded = false; mMap = null; mThrowable = null; startLoadFromDisk(); } private void startLoadFromDisk() { synchronized (mLock) { mLoaded = false; } new Thread("SharedPreferencesImpl-load") { public void run() { loadFromDisk(); } }.start(); }
在其构造函数中调用startLoadFromDisk来进行数据的加载,此处实现是经过新开线程来实现的。loadFromDisk的核心实现代码以下。
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); }
经过对于xml的解析获得一个Map,后面就能够经过name对Map进行查询便可获得相应的值。至此,咱们已经知道了如何从SharedPreferences进行数据的读取数值了,从本地磁盘读取数值到内存之中的Map,咱们查找的时候首先进行Map查找就能够了。当有修改的时候回写到磁盘之中。那么接下来,咱们来看一下数据应该如何写回。
对于SharedPreferenced值的写入,这里咱们先从commit开始。分析commit以前,咱们先看一下edit是如何实现的。
public Editor edit() { synchronized (mLock) { awaitLoadedLocked(); } return new EditorImpl(); }
首先当调用edit方法来进行写操做的时候,会获取到读锁,来等待读操做完成,由于读操做是在一个子线程中进行,所以须要经过await来进行等待,返回了一个EditorImpl实例,对于其中的相关修改操做,其内部有一个Map来存放要写入和要修改的数据。后面咱们调用commit和apply的时候,会先进行内存中数据的修改,而后再进行本地文件的修改。接下来,先看一下commit方法。
@Override public boolean commit() { long startTime = 0; if (DEBUG) { startTime = System.currentTimeMillis(); } MemoryCommitResult mcr = commitToMemory(); SharedPreferencesImpl.this.enqueueDiskWrite( mcr, null /* sync write on this thread okay */); try { mcr.writtenToDiskLatch.await(); } catch (InterruptedException e) { return false; } finally { } notifyListeners(mcr); return mcr.writeToDiskResult; }
commit方法中首先调用了commitToMemory方法,而后调用了enqueueDiskWrite方法来进行数据写入到磁盘。
private MemoryCommitResult commitToMemory() { long memoryStateGeneration; List<String> keysModified = null; Set<OnSharedPreferenceChangeListener> listeners = null; Map<String, Object> mapToWriteToDisk; synchronized (SharedPreferencesImpl.this.mLock) { if (mDiskWritesInFlight > 0) { mMap = new HashMap<String, Object>(mMap); } mapToWriteToDisk = mMap; mDiskWritesInFlight++; boolean hasListeners = mListeners.size() > 0; if (hasListeners) { keysModified = new ArrayList<String>(); listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet()); } synchronized (mEditorLock) { boolean changesMade = false; if (mClear) { if (!mapToWriteToDisk.isEmpty()) { changesMade = true; mapToWriteToDisk.clear(); } mClear = false; } for (Map.Entry<String, Object> e : mModified.entrySet()) { String k = e.getKey(); Object v = e.getValue(); // "this" is the magic value for a removal mutation. In addition, // setting a value to "null" for a given key is specified to be // equivalent to calling remove on that key. if (v == this || v == null) { if (!mapToWriteToDisk.containsKey(k)) { continue; } mapToWriteToDisk.remove(k); } else { 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(); if (changesMade) { mCurrentMemoryStateGeneration++; } memoryStateGeneration = mCurrentMemoryStateGeneration; } } return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners, mapToWriteToDisk); }
commitToMemory的实现是将记录的修改的Map,同步修改到最开始从本地加载数据Map中。enqueueDiskWrite方法的实现中核心在于一个runnable,runnable封装了文件写入的封装,当commit调用时,将在当前线程执行,当调用apply的时候则会在在一个子线程的HandlerThread中执行。
final Runnable writeToDiskRunnable = new Runnable() { @Override public void run() { synchronized (mWritingToDiskLock) { writeToFile(mcr, isFromSyncCommit); } synchronized (mLock) { mDiskWritesInFlight--; } if (postWriteRunnable != null) { postWriteRunnable.run(); } } };
apply的实现中首先将修改写到内存之中,而后再写入到磁盘,commit是在当前线程直接写入,而对于apply则是经过一个HandlerThread来实现写入。对于其中的排队实现,经过的是CountDownLatch来实现的,能够对其进行await阻塞等待,当调用其countDown方法的时候,就会将其写入到磁盘之中。CountDownLatch能够用来进行多个任务的执行等待,若是有一个任务想在三个任务执行完成以后再执行,那么就能够经过CountDownLatch来进行。既然apply是经过一个独立的线程来执行的,那么它会不会阻塞主线程呢?答案是会的,在QueueWork中的waitToFinish方法,该方法会在Activity的onPause的时候被调用,会将其中队列的任务所有执行完成。所以其也是会阻塞主线程的执行。
分析完上面的SharedPreferences的读写过程,首先有一个疑问就是若是咱们在进行本地文件向内存中装载的时候,再进行文件的写入应该怎么处理?
synchronized (mLock) { awaitLoadedLocked(); } return new EditorImpl();
在返回EditorImpl实现的时候,首先调用了awaitLoadedLocked,经过该方法实现对于从本地磁盘读锁的等待。只有当本地的文件已经加载到内存之中,才会进行后面的相关写操做。
对于本地磁盘文件的操做上,为了防止在写的过程当中发生异常,因此在写入的时候,会先将当前文件作一个备份,而后再进行写操做,若是写成功了,则将备份文件删除,当下次进行读写的时候若是判断到有备份文件,则能够认为上次文件的写入是失败的,读取数据的时候则从备份文件中读取,而后将备份文件重命名。这里在文件操做上借助与备份文件作转化防止数据写入出错的设计仍是挺巧妙的。
SharePreference是支持进行多线程读写的,能够进行多线程下的读写操做,不会出现数据错乱的问题,内部经过锁来进行控制。对于同一个进程,其中只存在一个SharePreference实例。
SharePreference是不支持多进程的,由于对于磁盘中数据的加载只会进行一次,所以当一个进程对数据进行修改以后,是没法体如今另外一个进程之中的。
在数据写入完成,经过FileUtils的setPermission来根据Mode为当前文件设置权限,而后在文件读的时候也会进行相应的判断,对于文件的读写权限将会根据写入时写入的mode做为判断依据。
经过对于SharedPreferences的分析,能够看出其大体实现上为在本地磁盘经过xml的方式进行文件的存储,当咱们获取一个SharedPreferences的实例的时候,开启线程将本地磁盘的数据读取到内存之中,经过一个Map来存放,而后对其修改的时候,会建立一个新的Map来进行当前修改数据的存取,调用commit和apply的时候,将修改的数据写回内存还有磁盘,同时根据设置的MODE,进行相应的判断。