【Android性能优化面试题】 SharedPreference原理,commit与apply的区别是什么?使用时需要有哪些注意?

SharedPreference原理,commit与apply的区别是什么?使用时需要有哪些注意?

这道题想考察什么?

1、对SharedPreference原理的掌握;

2、项目开发中对使用的技术是否有深入了解,掌握其隐患,而不仅仅只是调API;

考察的知识点

IO、并发编程、数据序列化、数据持久化与性能优化

考生如何回答

SharedPreferences作为Android系统的轻量级数据存储方式之一,能够比较方便的存取一些简单的Key-Value数据。先来对SP按照使用流程进行梳理:

SharedPreferences保存的文件为XML数据,其内容如下:

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="key1">value1</string>
    <int name="key2">value2</string>
    <!-- ...... -->
</map>
初始化

首先SharedPreference本身是一个接口,在Android当中的实现类为:android.app.SharedPreferencesImplcontext.getSharedPreferences(String name, int mode)获得一个SP实例的时候,会以传入的name作为文件名,创建**/data/data/<程序包名>/shared_prefs/{name}.xml** 对应的File对象,同时以此File对象,执行SharedPreferencesImpl构造方法并且将加入到缓存中。

class ContextImpl extends Context {
    
    

    //使用全局的静态Map对象来保存SharedPreferencesImpl的所有实现
    private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
    //文件名和文件存储的Map对象
    private ArrayMap<String, File> mSharedPrefsPaths;

    //...省略代码
    @Override
    public SharedPreferences getSharedPreferences(String name, int mode) {
    
    
        //...省略代码
        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) {
    
    
        checkMode(mode);
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
    
    
            final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
            sp = cache.get(file);
            if (sp == null) {
    
    
                //同样的如果缓存中找不到sp,那么创建一个
                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.
            //如果小于3.0版本或者多进程,将重新加载刷新数据
            sp.startReloadIfChangedUnexpectedly();
        }
        return sp;
    }

    private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
    
    
        if (sSharedPrefsCache == null) {
    
    
            sSharedPrefsCache = new ArrayMap<>();
        }
        //根据包名来获取
        final String packageName = getPackageName();
        ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
        if (packagePrefs == null) {
    
    
            //没有的话新建一个放入进去
            packagePrefs = new ArrayMap<>();
            sSharedPrefsCache.put(packageName, packagePrefs);
        }

        return packagePrefs;
    }
    //...省略代码
}

SharedPreferencesImpl的构造方法为:

SharedPreferencesImpl(File file, int mode) {
    
    
        mFile = file;
        mBackupFile = makeBackupFile(file);//备份文件,用于备份和恢复内容
        mMode = mode;
        mLoaded = false;
        mMap = null;//内存缓存
        startLoadFromDisk();//从磁盘加载存储的内容
    }

构造方法里面创建了mMap的内存缓存,后面存放及获取值的时候都是从这个缓存里面维护的, startLoadFromDisk()就是从文件读取存储内容赋值给mMap的,看看其实现:

private void startLoadFromDisk() {
    
    
        synchronized (this) {
    
    
            mLoaded = false;
        }
        new Thread("SharedPreferencesImpl-load") {
    
    
            public void run() {
    
    
                loadFromDisk();//在新的线程中进行内容的读取
            }
        }.start();
    }

 private void loadFromDisk() {
    
    
         synchronized (mLock) {
    
    
            if (mLoaded) {
    
    
                return;//如果正在加载就return
            }
            if (mBackupFile.exists()) {
    
    //如果备份文件存在就使用备份文件
                mFile.delete();
                mBackupFile.renameTo(mFile);
            }
        }

        // Debugging
        if (mFile.exists() && !mFile.canRead()) {
    
    
            Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");
        }
        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);//xml解析并赋值给map
                } 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;//将内容再次赋值给mMap对象,这样mMap将负责在内存中的数据维护
                mStatTimestamp = stat.st_mtim;
                mStatSize = stat.st_size;
            } else {
    
    //如果没有内容的话,就直接创建一个Map对象
                mMap = new HashMap<>();
            }
            mLock.notifyAll();
        }
    }

所以SP在初始化时,其实就是进行利用IO对一个XML文件进行读取,IO操作本身时耗时操作(为什么IO是耗时操作?),因此需要开启子线程。在子线程中需要对读取到的内容进行XML解析,将解析到的数据保存在Map集合当中。

读取数据

在初始化完成之后,获得了mMap集合,读取数据则可以直接从此map集合中获取,以获取Int型数据为例:

@Override
public int getInt(String key, int defValue) {
    
    
	synchronized (mLock) {
    
    
		awaitLoadedLocked();
		Integer v = (Integer)mMap.get(key);
		return v != null ? v : defValue;
	}
}

@GuardedBy("mLock")
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();
	}
	while (!mLoaded) {
    
    
		try {
    
    
			mLock.wait();
         } catch (InterruptedException unused) {
    
    
         }
    }
    if (mThrowable != null) {
    
    
		throw new IllegalStateException(mThrowable);
    }
}

因为初始化是在IO子线程中进行,因此我们需要考虑多线程并发问题,在getInt与初始化时均有使用同一对象:mLock进行了加锁的操作。并且我们注意到在IO子线程中,读取文件代码时未加锁,为了防止在其他线程中调用getInt获取值时进入synchronized代码块,但是mMap还没有赋值,在synchronized代码块中还进行了wait()等待。那么此处如若SP在进行初始化时,我们在主线程读取数据,读取数据需要等待sp的初始化完成。因此我们应该避免使用SP保存复杂、大量的数据。

写入数据

保存新数据或者更新原本的Key需要使用SharedPreferences.edit():可以返回一个Editor对象,而Editor的接口实现类为EditorImpl

 @Override
    public Editor edit() {
    
    
        // TODO: remove the need to call awaitLoadedLocked() when
        // requesting an editor.  will require some work on the
        // Editor, but then we should be able to do:
        //
        //      context.getSharedPreferences(..).edit().putString(..).apply()
        //
        // ... all without blocking.
        synchronized (mLock) {
    
    
            awaitLoadedLocked();
        }

        return new EditorImpl();
    }

此处我们需要小心的是避免频繁的调用edit()方法,因为每次调用都会产生一个EditorImpl对象,存在内存抖动的风险。

在获得Editor对象后,使用putXX方法进行新数据的写入:

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

在写入时,putXX仅仅只是在内存中使用Map集合mModified记录新数据。最终需要将数据持久化到文件中,需要执行commit或者apply方法。

commit提交
public boolean commit() {
    
    
           
            //将内容先提交到mMap中
            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;
        }
apply提交
 public void apply() {
    
    
            final long startTime = System.currentTimeMillis();
            //将数据同步到内存缓存
            final MemoryCommitResult mcr = commitToMemory();
            final Runnable awaitCommit = new Runnable() {
    
    
                    public void run() {
    
    
                        try {
    
    
                            //等待文件写入完成
                            mcr.writtenToDiskLatch.await();
                        } catch (InterruptedException ignored) {
    
    
                        }

                        if (DEBUG && mcr.wasWritten) {
    
    
                            Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                                    + " applied after " + (System.currentTimeMillis() - startTime)
                                    + " ms");
                        }
                    }
                };
		   
            QueuedWork.addFinisher(awaitCommit);
            //该任务在文件写入完毕后才执行
            Runnable postWriteRunnable = new Runnable() {
    
    
                    public void run() {
    
    
                        awaitCommit.run();
                        QueuedWork.removeFinisher(awaitCommit);
                    }
                };
            //写入文件
            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

            // Okay to notify the listeners before it's hit disk
            // because the listeners should always get the same
            // SharedPreferences instance back, which has the
            // changes reflected in memory.
            notifyListeners(mcr);
        }

从源码中能够发现,其实无论是 SP 的 commit 还是 apply 最终都会调用 enqueueDiskWrite 方法,区别是 commit 方法调用传递的第二个参数为 null。enqueueDiskWrite方法内部也是根据第二个参数来区分 commit 和 apply 的,如果是 commit 则会同步的执行 writeToFile,apply 则会将 writeToFile 加入到一个任务队列中异步的执行:

private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
    
    
        //根据postWriteRunnable参数来判断是同步提交还是异步提交
        final boolean isFromSyncCommit = (postWriteRunnable == null);

        final Runnable writeToDiskRunnable = new Runnable() {
    
    
                public void run() {
    
    
                    synchronized (mWritingToDiskLock) {
    
    
                    //这是真正的写入文件的方法
                        writeToFile(mcr, isFromSyncCommit);
                    }
                    synchronized (mLock) {
    
    
                        mDiskWritesInFlight--;
                    }
                    if (postWriteRunnable != null) {
    
    
                        postWriteRunnable.run();
                    }
                }
            };

        if (isFromSyncCommit) {
    
    
            boolean wasEmpty = false;
            synchronized (mLock) {
    
    
                wasEmpty = mDiskWritesInFlight == 1;
            }
            if (wasEmpty) {
    
    
                //直接在当前线程运行同步执行 writeToFile
                writeToDiskRunnable.run();
                return;
            }
        }
        //将 writeToFile 加入到一个任务队列中异步的执行
        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

writeToFile 执行完成会释放等待锁,之后会回调传递进来的第二个参数 Runnable 的 run 方法,并将 QueuedWork 中的这个等待任务移除。

SP 调用 apply 方法,会创建一个等待锁放到 QueuedWork(QueuedWork.addFinisher(awaitCommit)) 中,并将真正数据持久化封装成一个任务放到异步队列中执行,任务执行结束会释放锁。而Activity onPuase、onStop 以及 Service 处理 onStop,onStartCommand 等情况下,就会执行 QueuedWork.waitToFinish() 等待所有的等待锁释放。 因此apply 调用次数过多也会容易引起 ANR 问题。

//ActivityThread 
@Override
    public void handlePauseActivity(IBinder token, boolean finished, boolean userLeaving,
            int configChanges, PendingTransactionActions pendingActions, String reason) {
    
    
        ActivityClientRecord r = mActivities.get(token);
        if (r != null) {
    
    
            if (userLeaving) {
    
    
                performUserLeavingActivity(r);
            }

            r.activity.mConfigChangeFlags |= configChanges;
            performPauseActivity(r, finished, reason, pendingActions);

            // Make sure any pending writes are now committed.
            if (r.isPreHoneycomb()) {
    
    
                //sp中的apply未完成,则会有awaitCommit在QueuedWork中
                QueuedWork.waitToFinish();
            }
            mSomeActivitiesChanged = true;
        }
    }
commit与apply的区别
  1. apply没有返回值,因此无法获知提交结果,而commit返回boolean表明修改是否提交成功;
  2. apply使用异步真正提交到文件, 而commit是同步的提交,因此需要等待正在处理的commit保存到文件后才会执行后续的代码。
使用注意事项

1、getXXX() 方法可能会导致主线程阻塞

根据上文中初始化得知,getXXX需要等待初始化完成,因此如果SP数据量过多,需要更多的时间,此时调用getXXX会等待初始化完成。

2、SP 不能保证类型安全

因为使用相同的 key 进行操作的时候,putXXX 方法可以使用不同类型的数据覆盖掉相同的 key。

3、SP 加载的数据会一直留在内存中

初始化时缓存在mMap中,占用内存。

4、避免频繁调用editor与commit方法

每次editor都会创建EditorImpl对象,而每次commit则会将所有数据进行序列化并且使用IO(写文件)。

5、apply方法也可能ANR

因为apply会调用QueuedWork.addFinisher(awaitCommit),在Activity执行onStop时会需要等待apply完成执行完awaitCommit才能继续往后执行。

最后

我整理了一套Android面试题合集,除了以上面试题,还包含【Java 基础、集合、多线程、虚拟机、反射、泛型、并发编程、Android四大组件、异步任务和消息机制、UI绘制、性能调优、SDN、第三方框架、设计模式、Kotlin、计算机网络、系统启动流程、Dart、Flutter、算法和数据结构、NDK、H.264、H.265.音频编解码、FFmpeg、OpenMax、OpenCV、OpenGL ES
在这里插入图片描述

有需要的朋友可以扫描下方二维码,免费领取全部面试题+答案解析!!!

猜你喜欢

转载自blog.csdn.net/datian1234/article/details/134960550
今日推荐