Android开发Android进阶之路

Android高手笔记 - 存储优化

2020-12-31  本文已影响0人  Android开发架构师

好文推荐
作者:今阳
转载地址:https://juejin.cn/post/6911882407880687623

存储基础

存储分区:

定义:
分类

存储安全

权限控制
数据加密

常见存储方法

关键要素

存储方法

1. SharedPreferences
//MMKV的使用:

//1\. 添加依赖
implementation 'com.tencent:mmkv-static:1.2.7'
//2\. 在Application中初始化
String rootDir = MMKV.initialize(this);
LjyLogUtil.d("mmkv root: " + rootDir);
//3\. 使用
private void testMMKV() {
    MMKV kv = MMKV.defaultMMKV();

    kv.encode("bool", true);
    boolean bValue = kv.decodeBool("bool");

    kv.encode("int", Integer.MIN_VALUE);
    int iValue = kv.decodeInt("int");

    kv.encode("string", "Hello from mmkv");
    String str = kv.decodeString("string");
}
//4\. 如果业务需要多进程访问,那么在初始化的时候加上标志位 MMKV.MULTI_PROCESS_MODE
MMKV mmkv = MMKV.mmkvWithID("InterProcessKV", MMKV.MULTI_PROCESS_MODE);
mmkv.encode("bool", true);
//5\. SharedPreferences 迁移
private void testImportSharedPreferences() {
    //SharedPreferences preferences = getSharedPreferences("myData", MODE_PRIVATE);
    MMKV preferences = MMKV.mmkvWithID("myData");
    // 迁移旧数据
    {
        SharedPreferences old_man = getSharedPreferences("myData", MODE_PRIVATE);
        preferences.importFromSharedPreferences(old_man);
        old_man.edit().clear().commit();
    }
    // 跟以前用法一样
    SharedPreferences.Editor editor = preferences.edit();
    editor.putBoolean("bool", true);
    editor.putInt("int", Integer.MIN_VALUE);
    editor.putLong("long", Long.MAX_VALUE);
    editor.putFloat("float", -3.14f);
    editor.putString("string", "hello, imported");
    HashSet<String> set = new HashSet<String>();
    set.add("W"); set.add("e"); set.add("C"); set.add("h"); set.add("a"); set.add("t");
    editor.putStringSet("string-set", set);
    // 无需调用 commit()
    //editor.commit();
}
2. ContentProvider

对象的序列化

1. Serializable

2. Externalizable

3. Parcelable

存在两个问题
  1. 系统版本的兼容性
    • 我们无法保证所有 Android 版本的Parcel.cpp实现都完全一致。如果不同系统版本实现有所差异,或者有厂商修改了实现,可能会存在问题。
  2. 数据前后兼容性
    • Parcelable 并没有版本管理的设计,如果我们类的版本出现升级,写入的顺序及字段类型的兼容都需要格外注意,这也带来了很大的维护成本。
Serializable与Parcelable区别

4. Serial(推荐)

//1\. 将一个对象序列化为byte[]
final Serial serial = new ByteBufferSerial();
final byte[] serializedData = serial.toByteArray(object, ExampleObject.SERIALIZER)
//2\. 将对象从byte[]反序列化为object
final ExampleObject object = serial.fromByteArray(serializedData, ExampleObject.SERIALIZER)
//目前库中默认提供的序列化实现类是ByteBufferSerial,它的产物是byte[]。使用者也可以自行更换实现类,不用拘泥于byte[]。

//3\. 定义Serializer
//需要给每个被序列化的对象单独定义一个Serializer
//Serializers中需要给每个field明确的定义write和read操作,对于有继承关系的序列化类,需要被递归的进行定义
//Serializers是无状态的,所以我们可以将其写为object的内部类,并通过 SERIALIZER 作为名称来访问它
public static class ExampleObject {

    public static final ObjectSerializer<ExampleObject> SERIALIZER = new ExampleObjectSerializer();

    public final int num;
    public final SubObject obj;

    public ExampleObject(int num, @NotNull SubObject obj) {
        this.num = num;
        this.obj = obj;
    }

    ...

    private static final class ExampleObjectSerializer extends ObjectSerializer<ExampleObject> {
        @Override
        protected void serializeObject(@NotNull SerializationContext context, @NotNull SerializerOutput output,
                @NotNull ExampleObject object) throws IOException {
            output
                .writeInt(object.num) // first field
                .writeObject(object.obj, SubObject.SERIALIZER); // second field
        }

        @Override
        @NotNull
        protected ExampleObject deserializeObject(@NotNull SerializationContext context, @NotNull SerializerInput input,
                int versionNumber) throws IOException, ClassNotFoundException {
            final int num = input.readInt(); // first field
            final SubObject obj = input.readObject(SubObject.SERIALIZER); // second field
            return new ExampleObject(num, obj);
        }
    }
}
//这个内部类和 parcelable 中的 Parcelable.Creator 极为相似,都是按顺序对变量进行读写操作。
public static final Parcelable.Creator<Person> CREATOR = new Creator<Person>() {

    @Override
    public Person createFromParcel(Parcel source) {
        Person person = new Person();
        person.mName = source.readString();
        person.mSex = source.readString();
        person.mAge = source.readInt();
        return person;
    }

    //供反序列化本类数组时调用的方法
    @Override
    public Person[] newArray(int size) {
        return new Person[size];
    }
};

//4.BuilderSerializer
//通过builder模式构建的类或是有多个构造方法的类,可以使用 BuilderSerializer 来做序列化。
//只需要继承 BuilderSerializer ,并实现 createBuilder 方法(仅return当前class的builder即可)
//和 deserializeToBuilder 方法(在这个方法中可以得到builder对象,这里将那些反序列化完毕的参数重新设置给builder)
public static class ExampleObject {
    ...

    public ExampleObject(@NotNull Builder builder) {
        this.num = builder.mNum;
        this.obj = builder.mObj;
    }

    ...

    public static class Builder extends ModelBuilder<ExampleObject> {
        ...
    }

    private static final class ExampleObjectSerializer extends BuilderSerializer<ExampleObject, Builder> {
        @Override
        @NotNull
        protected Builder createBuilder() {
            return new Builder();
        }

        @Override
        protected void serializeObject(@NotNull SerializationContext context, @NotNull SerializerOutput output,
                @NotNull ExampleObject object) throws IOException {
            output.writeInt(object.num)
                .writeObject(object.obj, SubObject.SERIALIZER);
        }

         @Override
        protected void deserializeToBuilder(@NotNull SerializationContext context, @NotNull SerializerInput input,
                @NotNull Builder builder, int versionNumber) throws IOException, ClassNotFoundException {
            builder.setNum(input.readInt())
                .setObj(input.readObject(SubObject.SERIALIZER));
        }
    }
}
//5\. 通过版本号这个字段来处理新老版本的问题
final Serializer<ExampleObject> SERIALIZER = new ExampleObjectSerializer(1);
...

@Override
@NotNull
protected ExampleObject deserializeObject(@NotNull SerializationContext context, @NotNull SerializerInput input, int versionNumber) throws IOException, ClassNotFoundException {
    final int num = input.readInt();
    final SubObject obj = input.readObject(SubObject.SERIALIZER);
    final String name;
    if (versionNumber < 1) {
        name = DEFAULT_NAME;
    } else {
        name = input.readString();
    }
    return new ExampleObject(num, obj, name);
}
//6\. 简单参数的序列化
//像 Integer 、 String 、 Size、Rect 等对象本身就十分简单,所以无需进行版本控制。
//而使用 ObjectSerializer 会让这些对象添加2-3字节的信息。
//所以,当不需要版本控制的时候,使用 ValueSerializer 是一个最佳选择:
public static final Serializer<Boolean> BOOLEAN = new ValueSerializer<Boolean>() {
    @Override
    protected void serializeValue(@NotNull SerializationContext context, @NotNull SerializerOutput output, @NotNull Boolean object) throws IOException {
        output.writeBoolean(object);
    }

    @NotNull
    @Override
    protected Boolean deserializeValue(@NotNull SerializationContext context, @NotNull SerializerInput input) throws IOException {
        return input.readBoolean();
    }
};

数据的序列化

1. JSON

优点
json库

2. Protocol Buffers

数据库

ORM

ORM 框架带来的问题
  1. ORM框架使用非常简单,但是以牺牲部分执行效率为代价的;
  2. 让开发者思维固化,最后可能连简单的 SQL 语句都不会写了;

WCDB

//1\. 添加依赖(通过 Maven 接入)
 implementation 'com.tencent.wcdb:wcdb-android:1.0.0'

//2\. 选择接入的 CPU 架构
android {
    defaultConfig {
        ndk {
            // 只接入 armeabi-v7a 和 x86 架构
            abiFilters 'armeabi-v7a', 'x86'
        }
    }
}

//3\. 迁移到 WCDB
//WCDB Android 使用与 Android SDK SQLite 框架几乎一样的接口,
//如果你的 APP 之前使用 Android SDK 的数据库接口,
//只需要将 import 里的 android.database.* 改为 com.tencent.wcdb.*,
//以及 android.database.sqlite.* 改为 com.tencent.wcdb.database.* 即可。 
//若之前使用 SQLCipher Android Binding,也需要对应修改 import。

//4\. 从非加密数据库迁移到加密数据库
//需要使用 SQL 函数 sqlcipher_export() 进行迁移
ATTACH 'old_database' AS old;
SELECT sqlcipher_export('main', 'old');   -- 从 'old' 导入到 'main'
DETACH old;

//5\. 从 SQLCipher Android 迁移
//关键改动点为 密码转换为byte[] 以及 传入SQLiteCipherSpec描述加密方式
String passphrase = "passphrase";
SQLiteCipherSpec cipher = new SQLiteCipherSpec()  // 加密描述对象
    .setPageSize(1024)        // SQLCipher 默认 Page size 为 1024
    .setSQLCipherVersion(3);  // 1,2,3 分别对应 1.x, 2.x, 3.x 创建的 SQLCipher 数据库
    // 如以前使用过其他PRAGMA,可添加其他选项
SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(
    "path/to/database",     // DB 路径
    passphrase.getBytes(),  // WCDB 密码参数类型为 byte[]
    cipher,                 // 上面创建的加密描述对象
    null,                   // CursorFactory
    null                    // DatabaseErrorHandler
    // SQLiteDatabaseHook 参数去掉了,在cipher里指定参数可达到同样目的
);

//6\. 更好的与 Android Jetpack 的组件互动
//WCDB 现在已经正式介入 Room 以提供 ORM 以及数据绑定的功能,并能与 Android Jetpack 其他组件互动。
//6.1 在接入 Room 的基础上,gradle 里加上 WCDB 的 room 组件
dependencies {
    implementation 'com.tencent.wcdb:room:1.0.8'  // 代替 room-runtime,同时也不需要再引用 wcdb-android
    annotationProcessor 'android.arch.persistence.room:compiler:1.1.1' // compiler 需要用 room 的
}
//6.2 代码里面,打开 RoomDatabase 时,指定 WCDBOpenHelperFactory 作为 openFactory
SQLiteCipherSpec cipherSpec = new SQLiteCipherSpec()  // 指定加密方式,使用默认加密可以省略
        .setPageSize(4096)
        .setKDFIteration(64000);

WCDBOpenHelperFactory factory = new WCDBOpenHelperFactory()
        .passphrase("passphrase".getBytes())  // 指定加密DB密钥,非加密DB去掉此行
        .cipherSpec(cipherSpec)               // 指定加密方式,使用默认加密可以省略
        .writeAheadLoggingEnabled(true)       // 打开WAL以及读写并发,可以省略让Room决定是否要打开
        .asyncCheckpointEnabled(true);        // 打开异步Checkpoint优化,不需要可以省略

AppDatabase db = Room.databaseBuilder(this, AppDatabase.class, "app-db")
                //.allowMainThreadQueries()   // 允许主线程执行DB操作,一般不推荐
                .openHelperFactory(factory)   // 重要:使用WCDB打开Room
                .build();
// 其他使用与 Room 一样

//7\. WCDB 在初始化的时候可以指定连接池的大小
public static SQLiteDatabase openDatabase (String path, 
                    SQLiteDatabase.CursorFactory factory, 
                    int flags, 
                    DatabaseErrorHandler errorHandler, 
                    int poolSize)

进程与线程并发

多进程并发
多线程并发
PRAGMA SQLITE_THREADSAFE = 2
PRAGMA schema.journal_mode = WAL
...
} catch (SQLiteDatabaseLockedException e) {
    if (sqliteLockedExceptionTimes < (tryTimes - 1)) {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e1) {
        }
    }
    sqliteLockedExceptionTimes++;
}
- 总的来说通过连接池与 WAL 模式,我们可以很大程度上增加 SQLite 的读写并发,大大减少由于并发导致的等待耗时,建议大家在应用中可以尝试开启。

优化

索引优化
BETWEEN:myfiedl索引无法生效
SELECT * FROM mytable WHERE myfield BETWEEN 10 and 20;
转换成:myfiedl索引可以生效
SELECT * FROM mytable WHERE myfield >= 10 AND myfield <= 20;
页大小与缓存大小
其他优化

损坏与恢复

加密与安全

全文搜索

监控

sqlite> EXPLAIN QUERY PLAN SELECT * FROM t1 WHERE a=1 AND b>2;
QUERY PLAN
|--SEARCH TABLE t1 USING INDEX i2 (a=? AND b>?)
Matrix SQLiteLint
//1\. 添加依赖
debugImplementation "com.tencent.matrix:matrix-sqlite-lint-android-sdk:${MATRIX_VERSION}"
releaseImplementation "com.tencent.matrix:matrix-sqlite-lint-android-sdk-no-op:${MATRIX_VERSION}"
//2\. Application的onCreate中调用下面方法
private void prepareSQLiteLint() {
    SQLiteLintPlugin plugin = (SQLiteLintPlugin) Matrix.with().getPluginByClass(SQLiteLintPlugin.class);
    if (plugin == null) {
        return;
    }
    plugin.addConcernedDB(new SQLiteLintConfig.ConcernDb(getWritableDatabase())
            .setWhiteListXml(R.xml.sqlite_lint_whitelist)
            .enableAllCheckers());
}

如果还想了解更多Android 相关的更多知识点,可以点进我的GitHub项目中:https://github.com/733gh/GH-Android-Review-master自行查看,里面记录了许多的Android 知识点。

上一篇 下一篇

猜你喜欢

热点阅读