当前位置:   article > 正文

Android性能优化之SharedPreference卡顿优化_sharepreference 优化

sharepreference 优化

下面的源码都是基于Android api 31

1、SharedPreference使用

val sharePref = getPreferences(Context.MODE_PRIVATE)
with(sharePref.edit())
{
putBoolean("isLogin", true)
    putInt("age", 18)
    apply()
}
val isLogin = sharePref.getBoolean("isLogin", false)
val age = sharePref.getInt("age", -1)
Log.d(TAG, "isLogin $isLogin age $age")
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
1、获取SharedPreferencesImpl

获取sharedpreferences实现类是ContextImpl,下面是ContextImpl中获取sharedpreferencesImpl的源码

public SharedPreferences getSharedPreferences(String name, int mode) {
   ......
    File file;
    synchronized (ContextImpl.class) {
        //如果mSharedPrefsPaths为null,先创建一个mSharedPrefsPaths的Map。sharePreferences存放的地址
        if (mSharedPrefsPaths == null) {
            mSharedPrefsPaths = new ArrayMap<>();
        }
        //获取sharedPrefs的file
        file = mSharedPrefsPaths.get(name);
        if (file == null) {
            //如果file为null,创建file
            file = getSharedPreferencesPath(name);
            mSharedPrefsPaths.put(name, file);
        }
    }
    //通过file获取sharepref
    return getSharedPreferences(file, mode);
}
 //通过名称来获取file
public File getSharedPreferencesPath(String name) {
    return makeFilename(getPreferencesDir(), name + ".xml");
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

在contextImpl中是先根据sharedpref的名称来获取对应的File文件,然后通过file来获取sharedprefImpl

public SharedPreferences getSharedPreferences(File file, int mode) {
    SharedPreferencesImpl sp;
    synchronized (ContextImpl.class) {
        //Map中存放file-spImpl
        final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
        //先从cache中获取,
        sp = cache.get(file);
        if (sp == null) {
            //创建一个spImpl
            sp = new SharedPreferencesImpl(file, mode);
            //放入到cache中
            cache.put(file, sp);
            return sp;
        }
    }
    if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
        getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
        //多进程,会执行reload
        sp.startReloadIfChangedUnexpectedly();
    }
    return sp;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

由上面代码可知获取sharedPreferencesImpl实例分为两步:

1、通过sp的name来获取存放sp的File。

2、通过file来获取sharedPreferencesImpl实例。

name和file,file和sharedPreferencesImpl在创建过一次后会以key-value的形式保存在map中,方便后面再次获取。所以并不是每次获取spImpl都会去new。

2、SharedPrefrence存储

sp在磁盘中是以xml文件的形式存放的,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<defaultMap>
    <entry>
        <key>key</key>
        <value>true</value>
    </entry>    <entry>
        <key>key</key>
        <value>Hello World!</value>
    </entry>
    
    <entry>
        <key>key</key>
        <value>5</value>
    </entry>
</defaultMap>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

3、SharedPreference源码

SharedPreference本身是一个接口实现类是SharedPreferencesImpl。SharedPreferencesImpl构造方法如下:

SharedPreferencesImpl(File file, int mode) {
    mFile = file;
    mBackupFile = makeBackupFile(file);
    mMode = mode;
    mLoaded = false;
    mMap = null;
    mThrowable = null;
    startLoadFromDisk();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

SharedPreferencesImpl的构建方法中会先通过传参中的file,创建一个mBackupFile,并且开始从磁盘中获取数据。

private void startLoadFromDisk() {
    synchronized (mLock) {
        mLoaded = false;
    }
    new Thread("SharedPreferencesImpl-load") {
        public void run() {
            loadFromDisk();
        }
    }.start();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

startLoadFromDisk()中会创建一个名称为SharedPreferencesImpl-load的线程来加载磁盘中的数据。并将加载后的数据以key-value的形式放在mMap中。

写入数据

public final class EditorImpl implements Editor {
    private final Object mEditorLock = new Object();

    @GuardedBy("mEditorLock")
    private final Map<String, Object> mModified = new HashMap<>();

    @GuardedBy("mEditorLock")
    private boolean mClear = false;

    @Override
    public Editor putString(String key, @Nullable String value) {
        synchronized (mEditorLock) {
            mModified.put(key, value);
            return this;
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

写入数据时并不是直接往磁盘中写,如上的putString,会先将数据方法到mModified的map中,然后在commit或者apply的时候再进行写磁盘操作。

1、commit()方法

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 {
        if (DEBUG) {
            Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                    + " committed after " + (System.currentTimeMillis() - startTime)
                    + " ms");
        }
    }
    notifyListeners(mcr);
    return mcr.writeToDiskResult;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

在commit方法中会创建一个mcr对象,然后执行enqueueDiskWrite,最后返回mcr.writeToDiskResult。

在这里插入图片描述

2、commitToMemory方法
private MemoryCommitResult commitToMemory() {
    long memoryStateGeneration;
    boolean keysCleared = false;
    List<String> keysModified = null;
    Set<OnSharedPreferenceChangeListener> listeners = null;
    Map<String, Object> mapToWriteToDisk;

    synchronized (SharedPreferencesImpl.this.mLock) {
        
        if (mDiskWritesInFlight > 0) {
            //先判断是不是有多个写磁盘的操作,如果有多个写磁盘的操作,先copy mMap的对象。
            mMap = new HashMap<String, Object>(mMap);
        }
        //把map的对象赋值给mapToWriteDisk,
        mapToWriteToDisk = mMap;
        mDiskWritesInFlight++;//mDiskWritesInFlight加1
        
        synchronized (mEditorLock) {
            boolean changesMade = false;
              //判断是否清除sp中的数据
            if (mClear) {
                if (!mapToWriteToDisk.isEmpty()) {
                    changesMade = true;
                    mapToWriteToDisk.clear();
                    //如果清除sp中的数据将mapToWirteToDisk的数据清空。
                }
                keysCleared = true;
                mClear = false;
            }
            //遍历mModified map
            for (Map.Entry<String, Object> e : mModified.entrySet()) {
                String k = e.getKey();
                Object v = e.getValue();
                //
                //当v==this,或者v为null时,也就是执行了remove()方法的
                if (v == this || v == null) {
                    if (!mapToWriteToDisk.containsKey(k)) {
                    //如果mapToWriteToDisk中不包含这个k,继续循环
                        continue;
                    }
                   //否则从mapToWriteToDisk中移除这个k-v
                    mapToWriteToDisk.remove(k);
                } else {
                    if (mapToWriteToDisk.containsKey(k)) {
              
                        Object existingValue = mapToWriteToDisk.get(k);
                        if (existingValue != null && existingValue.equals(v)) {
                            continue;
                        }
                        //如果mapTopWriteToDisk中的key-value和mModified中的相同不操作
                    }
                    //把k-v方法哦mapToWriteToDisk中
                    mapToWriteToDisk.put(k, v);
                }

                changesMade = true;
            }
            //清除mModified中的数据
            mModified.clear();
            
            if (changesMade) {
            //Generation加1
                mCurrentMemoryStateGeneration++;
            }

            memoryStateGeneration = mCurrentMemoryStateGeneration;
        }
        return new MemoryCommitResult(memoryStateGeneration, keysCleared, keysModified,
        listeners, mapToWriteToDisk);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70

对应的流程图如下

在这里插入图片描述

3、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) {
                    //减1
                    mDiskWritesInFlight--;
                }
                if (postWriteRunnable != null) {
                    //运行postWriteRunnable
                    postWriteRunnable.run();
                }
            }
        };


    if (isFromSyncCommit) {
        //如果通过commit提交的直接执行writeToDiskRunnable
        boolean wasEmpty = false;
        synchronized (mLock) {
            wasEmpty = mDiskWritesInFlight == 1;
        }
        if (wasEmpty) {
            writeToDiskRunnable.run();
            return;
        }
    }
    //先进入队列
    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

在这里插入图片描述

4、queue代码

由上面可知mDiskWritesInFlight>1,或者是通过sp.apply的方法调用的,会执行到queue方法中

public static void queue(Runnable work, boolean shouldDelay) {
    Handler handler = getHandler();

    synchronized (sLock) {
        sWork.add(work);//添加到work中
        //shouldDelay和sCanDelay为true时Handler发一个Delayed消息,否则发送一个非延迟消息
        if (shouldDelay && sCanDelay) {
         
            handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
        } else {
            handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

上面的handler是通过HandlerThread创建的sHandler,源码如下

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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

由上面流程可以知道,只有执行commit操作并且mDiskWritesInFlight == 1时才会在主线程中执行,卡住主线程,当mDiskWritesInFlight > 1或者apply的时候不是在主线程中的。

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();
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
5、apply()方法

在这里插入图片描述

public void apply() {
    final long startTime = System.currentTimeMillis();
    //创建mcr对象
    final MemoryCommitResult mcr = commitToMemory();
    final Runnable awaitCommit = new Runnable() {
            @Override
            public void run() {
                try {
                ///创建awaitCommit,用于等待写磁盘完成后调用
                    mcr.writtenToDiskLatch.await();
                } catch (InterruptedException ignored) {
                }
            }
        };
    //添加awaitCommit
    QueuedWork.addFinisher(awaitCommit);

    Runnable postWriteRunnable = new Runnable() {
            @Override
            public void run() {
                //执行awaitCommit
                awaitCommit.run();
                ///移除awaitCommit
                QueuedWork.removeFinisher(awaitCommit);
            }
        };
    //进入队列
    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

    notifyListeners(mcr);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

apply方法,首先创建了一个 awaitCommit 的 Runnable,然后加入到 QueuedWork 的 sPendingWorkFinishers 队列中,awaitCommit 中包含了一个等待锁,需要在真正对 SP 进行持久化的时候进行释放。

6、apply方法中为什么要QueuedWork.addFinisher(awaitCommit)?

先要了解QueuedWork.waitToFinish() 方法,QueuedWork.waitToFinish() 会等待所有的addFinisher的runnable执行完成后才会进行执行。在ActivityThread中下面的方法都会执行QueuedWork.waitToFinish()方法。

  1. handleServiceArgs -> Service.onStartCommand
  2. handleStopService -> Service.onDestroy
  3. handlePauseActivity -> Activity.onPause
  4. handleStopActivity -> Activity.onStop

上面的问题转换成了为什么ActivityThread中上面的方法为什么都要等待sharePref的apply方法执行完写磁盘后才能被调用。

应用可能被系统回收、被用户杀死,会Crash,这些都是不确定的。apply 提交的任务,是先放到队列中去依次执行,而不是立即执行,意味着可能该任务还没被执行,app就被杀死。所以为了尽可能保证数据能被持久化,就要找到一些重要的时机去卡住,来保证完成写入。

7、获取数据
public String getString(String key, @Nullable String defValue) {
    synchronized (mLock) {
        awaitLoadedLocked();
        String v = (String)mMap.get(key);
        return v != null ? v : defValue;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

获取数据时先要等数据从磁盘中加载完成,所以会先执行awaitLoadedLocked();方法,这里面的会通过mLoaded判断数据是否已经从磁盘中加载完成,如果没有加载完成则会等待。

private void awaitLoadedLocked() {
    while (!mLoaded) {
        try {
            mLock.wait();
        } catch (InterruptedException unused) {
        }
    }
    if (mThrowable != null) {
        throw new IllegalStateException(mThrowable);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

4、疑问

1、执行put不执行apply或者commit,再获取这个值能获取到吗?
val sharePref = getPreferences(Context.MODE_PRIVATE)
with(sharePref.edit())
{
putBoolean("isLogin", true)
    putInt("age", 18)
    val age = sharePref.getInt("age", -1)
    Log.d(TAG, "----before apply--- age $age")
    apply()
    val age1 = sharePref.getInt("age", -1)
    Log.d(TAG, "----after apply--- age $age1")
} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

打印的log如下,在执行apply之前获取到的age是默认值-1,执行apply后获取的值是set的值18.

 ----before apply--- age -1
 ----after apply--- age 18
  • 1
  • 2

5、SharedPreferences跨进程

在使用SharedPreference 时,有如下一些模式: MODE_PRIVATE 私有模式,这是最常见的模式,一般情况下都使用该模式。 MODE_WORLD_READABLE,MODE_WORLD_WRITEABLE ,文件开放读写权限,不安全,已经被废弃了,google建议使用FileProvider共享文件。 MODE_MULTI_PROCESS,跨进程模式,如果项目有多个进程使用同一个Preference,需要使用该模式,但是也已经废弃了。使用contentProvider可以实现sp的跨进程,

在这里插入图片描述

参考:https://www.jianshu.com/p/875d13458538

6、SharedPreferences ANR

使用sp的项目中经常会出现下面的ANR

"main" prio=5 tid=1 Waiting
  | group="main" sCount=1 dsCount=0 obj=0x73f50268 self=0xefc05400
  | sysTid=13726 nice=0 cgrp=default sched=0/0 handle=0xf30cf534
  | state=S schedstat=( 1450815451 10991027618 7548 ) utm=40 stm=105 core=0 HZ=100
  | stack=0xff1f4000-0xff1f6000 stackSize=8MB
  | held mutexes=
  at java.lang.Object.wait!(Object.java)
  - waiting on <0x04386712> (a java.lang.Object)
  at java.lang.Thread.parkFor$(Thread.java:2127)
  - locked <0x04386712> (a java.lang.Object)
  at sun.misc.Unsafe.park(Unsafe.java:325)
  at java.util.concurrent.locks.LockSupport.park(LockSupport.java:161)
  at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:840)
  at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireSharedInterruptibly(AbstractQueuedSynchronizer.java:994)
  at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireSharedInterruptibly(AbstractQueuedSynchronizer.java:1303)
  at java.util.concurrent.CountDownLatch.await(CountDownLatch.java:203)
  at android.app.SharedPreferencesImpl$EditorImpl$ 1. run(SharedPreferencesImpl.java: 366 )  at android.app.QueuedWork.waitToFinish(QueuedWork.java:88)
  at android.app.ActivityThread.handleServiceArgs(ActivityThread.java:3400)
  at android.app.ActivityThread.-wrap21(ActivityThread.java:-1)
  at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1632)
  at android.os.Handler.dispatchMessage(Handler.java:110)
  at android.os.Looper.loop(Looper.java:203)
  at android.app.ActivityThread.main(ActivityThread.java:6251)
  at java.lang.reflect.Method.invoke!(Method.java)
  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1063)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:924)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
1、sp引起ANR的原因

由上面日志可以看出main线程在等待锁导致的ANR,具体原因是ActivityThread.handleServiceArgs,时执行QueueWork.waitToFinish。ActivityThread中handleServiceArgs的代码如下:

private void handleServiceArgs(ServiceArgsData data) {
    Service s = mServices.get(data.token);
    if (s != null) {
        try {
            if (data.args != null) {
                data.args.setExtrasClassLoader(s.getClassLoader());
                data.args.prepareToEnterProcess();
            }
            int res;
            if (!data.taskRemoved) {
                res = s.onStartCommand(data.args, data.flags, data.startId);
            } else {
                s.onTaskRemoved(data.args);
                res = Service.START_TASK_REMOVED_COMPLETE;
            }
            //QueuedWork.waitToFinish
            QueuedWork.waitToFinish();

            try {
                ActivityManager.getService().serviceDoneExecuting(
                        data.token, SERVICE_DONE_EXECUTING_START, data.startId, res);
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
            }
        } catch (Exception e) {
           ......
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

如上代码在handleServiceArgs中执行了QueueWork.waitFinish(),waitFinish()的源码如下:

public static void waitToFinish() {
   ......
    try {
    //循环获取sFinishers队列中的任务,当任务为null时退出
        while (true) {
            Runnable finisher;
            synchronized (sLock) {
                finisher = sFinishers.poll();
            }
            if (finisher == null) {
                break;
            }
            //执行任务
            finisher.run();
        }
    } finally {
        sCanDelay = true;
    }

......
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

在handleServiceArgs时执行QueueWork.waitFinish(),会等待QueueWork中的任务执行完成。在SharedPreferencesImpl的apply方法中如下:

public void apply() {

    final MemoryCommitResult mcr = commitToMemory();
    final Runnable awaitCommit = new Runnable() {
            @Override
            public void run() {
                try {
                    mcr.writtenToDiskLatch.await();
                } catch (InterruptedException ignored) {
                }
            }
        };
    //添加await
    QueuedWork.addFinisher(awaitCommit);
    //写磁盘后再调用
    Runnable postWriteRunnable = new Runnable() {
            @Override
            public void run() {
                awaitCommit.run();
                QueuedWork.removeFinisher(awaitCommit);
            }
        };
    //添加到队列
    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

    notifyListeners(mcr);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

postWriteRunnable执行时会调用awaitCommit.run();这里会执行mcr.writtenToDiskLatch.await();等待写磁盘完成,再调用,所以QueuedWork.removeFinisher(awaitCommit);是在写磁盘完成后移除的。

ActivityThread 中,下面的方法中都调用了QueuedWork.waitToFinish() :

  1. handleServiceArgs -> Service.onStartCommand
  2. handleStopService -> Service.onDestroy
  3. handlePauseActivity -> Activity.onPause
  4. handleStopActivity -> Activity.onStop

以上四处都存在着 SP 的等待锁问题所导致的 ANR 风险。

2、解决方法
1、8.0及以下版本

由于sPendingWorkFinishers是QueuedWork的静态集合对象,而且ConcurrentLinkedQueue这个集合类是可以继承的,所以可以直接重新定义一个集合类继承自ConcurrentLinkedQueue,覆盖ConcurrentLinkedQueue集合在QueuedWork中暴露出来的接口,将poll接口的返回值改为固定返回null,用这个自定义的集合动态代理之前的集合,这个时候ActivityThread的H在处理消息的时候就再也不用执行等待行为了。
在这里插入图片描述

如下方式hook住QueueWork替换sPendingWorkFinishers为ConcurrentLinkedQueueProxy

public static void replaceQueueWorkPendingWorkFinishers() {
    Log.d(TAG, "start hook, time stamp = " + System.currentTimeMillis());

    ConcurrentLinkedQueue<Runnable> sPendingWorkFinishers = null;
    Field field = null;
    try {
        Class<?> atClass = Class.forName("android.app.QueuedWork");
        field = atClass.getDeclaredField("sPendingWorkFinishers");
        field.setAccessible(true);
        sPendingWorkFinishers = (ConcurrentLinkedQueue<Runnable>) field.get(null);
        if (sPendingWorkFinishers != null) {
            field.set(null, new ConcurrentLinkedQueueProxy<Runnable>(sPendingWorkFinishers));
            Log.d(TAG, "Below android 0,replaceQueueWorkPendingWorkFinishers success.");
        }
    } catch (Exception e) {
        // 出现异常
        try {
            if (sPendingWorkFinishers != null && field != null) {
                field.set(null, sPendingWorkFinishers);
            }
        } catch (Exception ex) {
            //ignore
        }
        Log.e(TAG, "Below android 0,,hook sPendingWorkFinishers fail.", e);
    }
    Log.d(TAG, "end hook, time stamp = " + System.currentTimeMillis());
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

7、SharedPreferences替代方案

MMKV

|len|key|len|value||len|key|len|value||len|key|len|value|...
  • 1

文件中key value紧密排布。另有一个crc文件。

MMKV写策略是,增量kv对象序列化后,直接append到内存末尾。这样同一个key会有新旧若干份数据,最新的数据在最后。

不断 append 的话,文件大小会增长得不可控。例如同一个 key 不断更新的话,是可能耗尽几百 M 甚至上 G 空间,而事实上整个 kv 文件就这一个 key,不到 1k 空间就存得下。这明显是不可取的。因此MMKV在性能和空间上做了折中:以内存 pagesize 为单位申请空间,在空间用尽之前都是 append 模式;当 append 到文件末尾时,进行文件重整、key 排重,尝试序列化保存排重结果;排重后空间还是不够用的话,将文件扩大一倍,直到空间足够。

而这个重写策略,就是直接将内存中的hash表取出kv逐个写入文件,直接丢弃原先的文件内容。

存在问题:

  • 原先一直没有改变的值也需要跟着一起重新写入。
  • 由于写入的是同一个文件路径,在这期间如果重写中断或者失败,就会导致内容丢失。
  • append内容的时候如果被中断,会导致脏数据。
  • 多进程情况下,一旦有进程更新文件,另一进程在访问KV的时候就必须要校验crc,如不一致则需重新载入kv来构建新的hash表。
  • 无类型信息。一方面,无法根据已有文件来追踪所存内容。另一方面,这将导致已经迁移至MMKV的数据后续无法再迁移到其他方案,因为无法取出每个值的完整类型信息。
  • 每新开一个kv仓库,都需要新增一个fd,如果该仓库要支持多进程,需要再增加一个fd,从而容易导致fd超限问题。

链接:https://github.com/Tencent/MMKV

参考

1、https://stackoverflow.com/questions/45358761/how-to-save-variable-value-even-if-app-is-destroyed

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/程序自动化专家/article/detail/60748
推荐阅读
相关标签
  

闽ICP备14008679号