Android 缓存设计与实现方式

4,143 阅读6分钟

这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战

Android 缓存设计与实现方式

在初级面试中经常遇到四大组件、五大存储、六大布局。该篇文章主要分析下Android中实现缓存的方式。

一、了解Android的存储方式

关于五大存储

大家一般期待的回答是下面这样的。

  1. SharedPreferences
  2. SQLite数据库
  3. ContentProvider(内容提供者)
  4. 文件存储
  5. 网络存储

还可以用另外一种方式区分,远端存储,和本地存储。

  • SharedPreferences

存储目录:/data/data/<packagename>/shared_prefs/ 轻量级存储,本地目录在,在正式发布的版本上,只有root才能看到。

什么是轻量? 对其环境依赖的越少,就越轻量,也就是减少耦合的意思。

  • SQLite

存储目录:/data/data/<package name>/databases/ SQLite 是一个微型数据库,效率高,特别适合存储结构化的数据。Android 中内置了SQLite数据库,它支持SQL语法,但在Android中一般不直接写SQL语句,而是使用第三方库操作对象,达到对数据库增删改查的目的,比如 GreenDao。

  • ContentProvider

ContentProvider 是Android存储数据的方案,底部仍然是使用SqLite。它根据Uri(Universal Resource Identifier)统一资源定位符 确定资源。可以多app使用,比如通讯录可以给多个应用获取。虽然日常开发中对 ContentProvider 使用不多,但和ContentProvider的接触却很频繁,比如 选择一张图片,我们首先拿到的却是Uri。

  • 文件存储

文件存储,分为内部存储和外部存储。

  1. 怎么区分内部存储和外部存储?

对用户而言可插拔的SD卡就是外部存储,焊接在手机内部的就是内部存储。

但对于开发者而言并不是这样。

在该应用目录下的存储是内部存储。

反之即使存在手机不可拆卸的存储卡上,也叫外部存储。

  1. 内部存储和外部存储有哪些不同?

(1) 在/data/data/package_name目录下,在debug环境下,可以通过AndroidStudioDevice File Explorer查询到该app的存储,如果需要查询其它app的这个目录,则需要Root。 (2) 内部存储在应用卸载的时候会被清除,而外部存储不会。 (3) 操作内部存储目录不需要申请存储权限,而外部存储需要存储权限。这点也能理解,操作自己的应用不需要过多干预提醒。

  • 网络存储

文件保存在服务器上,通过url操作服务器上的资源。比如apk的版本更新下载apk的场景。

二、拓展的存储方式

  1. GreenDao-操作对象即修改数据
  2. ACache-一个类轻松缓存
  3. ImageLoader/Glide-缓存图片

三、缓存设计

场景一

“我想缓存一张图片”

使用三级缓存。网络缓存(速度慢、费流量), 本地缓存,内存缓存。

首次网络加载时缓存到本地存储,缓存到内存; 当再次访问该图片时,

graph TD
再次加载该图片时 --> C{内存中是否存在}
C -->|存在| D[显示图片]
C -->|不存在| E{本地文件是否存在}
E -->|存在| X[显示图片]
E -->|不存在| F[请求网络]
F --> Y[显示图片]

场景二

“我想加载新闻内容”

使用ACache 缓存新闻内容,在下次加载时先请求本地的缓存,然后后台请求网络,请求网络数据成功后再去更新页面。

graph TD
A[开始] -->|start| B(加载新闻)
B --> C{本地缓存是否存在}
C -->|是| D[显示缓存的内容]
D --> E
C -->|否| E[后台请求网络数据]
E -->|拿到数据| F[显示/刷新页面]

由于新闻的数据不需要与服务器保持高度统一,所以这里我们将新闻数据放入ACache中,ACache 也可以轻松的设置过期时间。

场景三

“我想在app存一万条人员信息”

当有大量数据时,如果我们把数据用JSON存在SP 或者 文件里,对单条信息的操作却要读取整个文件,这样明显是不值得的。

再看看Sqlite的有点

"适合存储结构化的数据"

那么人员信息就应该依据ID、姓名、性别、年龄等字段在Sqlite中建表。

这样在对单个数据操作时就可以通过ID进行操作。

四、GreenDao实践

明确GreenDao的核心类含义

  • DaoMaster: DaoMaster保存数据库对象(SQLiteDatabase)并管理特定模式的DAO类(而不是对象)。它有静态方法来创建表或删除它们。它的内部类OpenHelper和DevOpenHelper是SQLiteOpenHelper实现,它们在SQLite数据库中创建模式。

  • DaoSession:管理特定模式的所有可用DAO对象,您可以使用其中一个getter方法获取该对象。DaoSession还提供了一些通用的持久性方法,如实体的插入,加载,更新,刷新和删除。

  • XXXDao:数据访问对象(DAO)持久存在并查询实体。对于每个实体,greenDAO生成DAO。它具有比DaoSession更多的持久性方法,例如:count,loadAll和insertInTx。

  • Entities :可持久化对象。通常, 实体对象代表一个数据库行使用标准 Java 属性(如一个POJO 或 JavaBean )。

实践

1. 创建好输入框按钮等界面

2. 然后添加配置信息配置信息最好根据github上的地址指引去做,因为会经常保持更新。

github.com/greenrobot/…

摘:

Add the following Gradle configuration to your Android project. In your root build.gradle file:

buildscript {
    repositories {
        jcenter()
        mavenCentral() // add repository
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.1.1'
        classpath 'org.greenrobot:greendao-gradle-plugin:3.2.2' // add plugin
    }
}

In your app modules app/build.gradle file:
apply plugin: 'com.android.application'
apply plugin: 'org.greenrobot.greendao' // apply plugin
 
dependencies {
    implementation 'org.greenrobot:greendao:3.2.2' // add library
}

然后在app/build.gradle 中的根目录添加greenDao的配置信息。

//数据库配置
greendao {
    //数据库版本号
    schemaVersion 1
    //自动生成代码所在的包名
    daoPackage 'com.dao.green.db'
    targetGenDir 'src/main/java'
}

3. 创建实体类

@Entity(nameInDb = "STUDENT") //添加该行,代表自动生成
public class Student {

    @Id(autoincrement = true)
    @Unique
    private Long id; //主键自增长,不可重复,作为不同记录对象的标识,传入参数对象时不要传入

    @Property(nameInDb = "name")
    private String name; //名字

    @Property(nameInDb = "mark")
    private String mark; //备注

创建完成后,编译之后会自动在相应目录下生成master、session、dao文件。

4. 在自定义的Application中初始化

GreenDaopublic class App extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        //打开数据库
        DbManager.getInstance().initDb(this);
    }
}

5. 重写MySqliteOpenHelper,防止数据库升级清空本地数据

public class MySqliteOpenHelper extends DaoMaster.DevOpenHelper {

    public MySqliteOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory) {
        super(context, name, factory);
    }

    /**
     * 需要在实体类加一个字段 或者 改变字段属性等 就需要版本更新来保存以前的数据了
     *
     * @param db
     * @param oldVersion
     * @param newVersion
     */
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        super.onUpgrade(db, oldVersion, newVersion);

        //这里添加要增加的字段
        MigrationHelper.migrate(db, new MigrationHelper.ReCreateAllTableListener() {
            @Override
            public void onCreateAllTables(Database db, boolean ifNotExists) {
                DaoMaster.createAllTables(db, ifNotExists);
            }

            @Override
            public void onDropAllTables(Database db, boolean ifExists) {
                DaoMaster.dropAllTables(db, ifExists);
            }
        }, StudentDao.class);

    }
}

MigrationHelper.java 来自github:github.com/yuweiguocn/…

6. 封装DbManager、DbHelperDbManager

/**
 * @author by T, Date on 2019-10-22.
 * note: DB的管理类
 */
public class DbManager {

    //数据库名称
    private static final String DATABASE_NAME = "student_data";
    private static DbManager instance;


    private DaoSession mDaoSession;
    private DaoMaster.DevOpenHelper mDevOpenHelper;
    private DaoMaster mDaoMaster;

    /**
     * 获取单例
     *
     * @return
     */
    public static DbManager getInstance() {
        if (instance == null) {
            synchronized (DbManager.class) {
                if (instance == null) {
                    instance = new DbManager();
                }
            }
        }
        return instance;
    }


    public void initDb(Context context) {
        mDevOpenHelper = new MySqliteOpenHelper(context, DATABASE_NAME, null);
        mDaoMaster = new DaoMaster(mDevOpenHelper.getWritableDatabase());
        mDaoSession = mDaoMaster.newSession();
        LogUtils.d("打开了数据库:" + mDevOpenHelper.getDatabaseName());
    }


    public DaoSession getDaoSession() {
        return mDaoSession;
    }


    /**
     * 关闭数据库 (思考:关闭数据库的场景在哪里)
     */
    public void closeDataBase() {
        if (mDevOpenHelper != null) {
            LogUtils.d("关闭了数据库" + mDevOpenHelper.getDatabaseName());
        }
        closeDaoSession();
        closeHelper();
    }


    private void closeDaoSession() {
        if (mDaoSession != null) {
            mDaoSession.clear();
            mDaoSession = null;
        }
    }

    private void closeHelper() {
        if (mDevOpenHelper != null) {
            mDevOpenHelper.close();
            mDevOpenHelper = null;
        }
    }
}

DbHelper

/**
 * @author by T, Date on 2019-10-22.
 * note: 进一步封装,对外直接使用该类
 */
public class DbHelper {


    /**
     * 插入一条学生信息
     *
     * @param student
     * @return
     */
    public static boolean insertStudentInfo(Student student) {
        if (student == null) return false;
        StudentDao studentDao = DbManager.getInstance().getDaoSession().getStudentDao();
        try {
            long index = studentDao.insertOrReplace(student);
            LogUtils.d("id:" + index);
            return true;
        } catch (Exception e) {
            return false;
        }

    }


    /**
     * 查询所有的学生
     *
     * @return
     */
    public static List<Student> queryAllStudent() {
        StudentDao studentDao = DbManager.getInstance().getDaoSession().getStudentDao();
        try {
            return studentDao.loadAll();
        } catch (Exception e) {
            return null;
        }

    }

}

7. Activity中的使用Activity中的两个点击方法

    /**
     * 保存数据
     *
     * @param view
     */
    public void Save(View view) {
        Student student = new Student();
        student.setName(etName.getText().toString());
        student.setMark(etMark.getText().toString());
        boolean flag = DbHelper.insertStudentInfo(student);
        LogUtils.d("插入是否成功:" + flag);
    }


    /**
     * 查询全部数据
     *
     * @param view
     */
    public void QueryAll(View view) {
        List<Student> listStudents = DbHelper.queryAllStudent();
        if (listStudents == null) {
            LogUtils.d("没有数据");
            return;
        }

        for (int i = 0; i < listStudents.size(); i++) {
            LogUtils.d("id:" + listStudents.get(i).getId() + "_name:" + listStudents.get(i).getName());
        }
    }