Android中的三种常用数据持久化技术

一共有三种技术,分别是文件、SharedPreferences和数据库。

1. 文件

Context类中提供了openFileOutputopenFileInput的方法,可以存储和读取指定的文件中的数据,默认文件存储在data/data/package name/files/目录下,故而只需要指定文件名即可。文件操作模式有MODE_PRIVATEMODE_APPEND两种可选。默认为第一个。

这种方式属于程序私有目录下存储和读取,故而不需要读写权限,永久存储,即使应用被卸载,存储的数据依然存在。

1.1 写入

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_test);

    Button button = findViewById(R.id.id_button);
    button.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            FileOutputStream outputStream = null;
            BufferedWriter bufferedWriter = null;
            try {
                outputStream = openFileOutput("text.md", MODE_PRIVATE);
                bufferedWriter = new BufferedWriter(new 
                					OutputStreamWriter(outputStream));
                bufferedWriter.write("This is the datas.");
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }finally {
                try {
                    assert bufferedWriter != null;
                    bufferedWriter.close();
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    });
}

1.2 读取

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_test);

    Button button = findViewById(R.id.id_button);
    button.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            FileInputStream input = null;
            BufferedReader bufferedReader = null;
            try {
                input = openFileInput("text.md");
                bufferedReader = new BufferedReader(new InputStreamReader(input));
                String s = bufferedReader.readLine();
                button.setText(s);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }finally {
                try {
                    bufferedReader.close();
                    input.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    });
}

上面案例中,为了操作字符方便,所以使用了包装流对象。
上面两个案例中,均使用的字符流。那么字符流和字节流有什么区别?
字符流是由Java虚拟机将字节流转换得到的,这个过程比较耗时,并且,如果我们不知道流的编码类型,就很容易出现乱码问题。如果音频、图片、视频等文件使用字节流比较好,而如果涉及到字符,使用字符流比较好。

这里衍生下:

1.3 在Java中获取键盘输入的两种方式

Java中键盘输入也是数据流的方式,通常有两种方式获得:

  1. Scanner
  2. BufferedReader
// 方式一:
Scanner input = new Scanner(System.in);
String s = input.nextLine();

// 方式二:
BufferedReader bf = new BufferedReader(new InputStreamReader(System.in));
try {
    String s1 = bf.readLine();
} catch (IOException e) {
    e.printStackTrace();
}

第二种方式需要自己处理异常,而第一种不需要。故而通常第一种方式比较常用。

2. SharedPreferences存储

SharedPreferences采用键值对的方式进行存储。同时,支持多种不同的数据类型存储。

SharedPreferences文件默认存储在data/data/package name/shared_prefs/目录下。操作模式目前只有MODE_PRIVATE一种模式。其值为0,表示只有当前程序才可以对这个文件进行读写。其余几种操作模式已经被废弃。默认是没有考虑同步互斥,它基于单个文件,不支持多进程。在getSharedPreferences的时候,会强制让SharedPreferences进行一次读取操作,从而保证数据是最新的。是轻量级的存储类,特别适合用于保存软件配置参数。

1. 优点:

  1. 轻量级,以键值对的方式进行存储,使用方便,易于理解;
  2. 采用的是xml文件形式存储在本地,程序卸载后会也会一并被清除,不会残留信息;

2. 缺点:

  1. 由于是对文件IO读取,因此在IO上的瓶颈是个大问题,因为在每次进行getSharedPreferencescommit时都要将数据从内存写入到文件中,或从文件中读取;

  2. 多线程场景下效率较低,在getSharedPreferences操作时,会锁定SharedPreferences对象,互斥其他操作,而当putcommit时,则会锁定Editor对象,使用写入锁进行互斥,在这种情况下,效率会降低

  3. 不支持跨进程通讯;

  4. 由于每次都会把整个文件加载到内存中,因此,如果SharedPreferences文件过大,或者在其中的键值对是大对象的json数据则会占用大量内存,读取较慢是一方面,同时也会引发程序频繁GC,导致的界面卡顿。

以上优缺点总结来自:https://blog.csdn.net/qq_24349189/article/details/99674662

Android种提供了两种方式来获取SharedPreferences对象:

  • Context类中的getSharedPreferences()方法;
  • Activity类中的getPreferences()方法;

SharedPreference 种提交数据有两种方式,分别是applycommit。下面是两种方式的差别:
SharedPreference 相关修改使用 apply 方法进行提交会先写入内存,然后异步写入磁盘commit
方法是直接写入磁盘。如果频繁操作的话 apply 的性能会优于 commitapply会将最后修改内容写入磁盘。但是如果希望立刻获取存储操作的结果,并据此做相应的其他操作,应当使用 commit

  1. commitapply虽然都是原子性操作,但是原子的操作不同,commit是原子提交到数据库,所以从提交数据到存在Disk中都是同步过程,中间不可打断。
  2. apply方法的原子操作是原子提交的内存中,而非数据库,所以在提交到内存中时不可打断,之后再异步提交数据到数据库中,因此也不会有相应的返回值。
  3. 所有commit提交是同步过程,效率会比apply异步提交的速度慢,但是apply没有返回值(commit会返回一个boolean值),永远无法知道存储是否失败。
  4. 在不关心提交结果是否成功的情况下,优先考虑apply方法。

以上四条总结来源:SharedPreferences中的commit和apply方法

2.1 使用ActivitygetPreferences方式:

// TestActivity.java
button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        SharedPreferences preferences = getPreferences(MODE_PRIVATE);
        SharedPreferences.Editor edit = preferences.edit();
        edit.putString("key", "value");
        edit.commit();
    }
});

可以发现,这里我并没有指定文件名,但是操作提交后:
在这里插入图片描述
不难发现使用ActivitygetPreferences来获取SharedPreference对象,不用指定xml文件名,但是会使用当前的Activity的类名作为当前SharedPreference的文件名。

2.2 使用Context类的getSharedPreferences()方法;

button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        SharedPreferences preferences = getApplicationContext().
                getSharedPreferences("weizu", MODE_PRIVATE);
        SharedPreferences.Editor edit = preferences.edit();
        edit.putString("key", "value");
        edit.commit();
    }
});

不用指定文件后缀名,会自动添加.xml后缀。效果一样。

2.3 读取数据

button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        SharedPreferences preferences = getApplicationContext().
                getSharedPreferences("weizu", MODE_PRIVATE);
        // key  defaultValue
        String key = preferences.getString("key", "123");
        button.setText(key);
    }
});

一共支持如下类型:
在这里插入图片描述
当需要清除所有数据的时候,可以使用editor.clear(),然后apply:

button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        SharedPreferences preferences = getApplicationContext().
                getSharedPreferences("weizu", MODE_PRIVATE);
        SharedPreferences.Editor edit = preferences.edit();
        edit.clear();
        edit.apply();
    }
});

对应的xml文件就会变成空文件,如:

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map />

3. SQLite数据库

3.1 创建数据库和数据库表

Android中为管理数据库提供了SQLiteOpenHelper,这个类是一个抽象类,我们使用需要继承它,然后重写里面的对应方法onCreateonUpgrade

public class MySQLiteOpenHelper extends SQLiteOpenHelper {
    private Context context;
    private String name;
    public MySQLiteOpenHelper(@Nullable Context context,
                              @Nullable String name,
                              @Nullable SQLiteDatabase.CursorFactory factory,
                              int version) {
        super(context, name, factory, version);
        this.context = context;
        this.name = name;
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        // 创建数据库表
        String sql = "create table Book (id integer primary key autoincrement, " +
                "name text, pages integer)";
        db.execSQL(sql);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    }
}

在上面构造器中我们需要传入上下文对象,数据库名字,游标工厂(SQLiteDatabase.CursorFactory)和版本。按照逻辑,我们来创建数据库:

button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        mySQLiteOpenHelper = new MySQLiteOpenHelper(getApplicationContext(),
                "BookDatabase.db", null, 1);
        // 调用getWritableDatabase开始创建,得到可写数据库对象
        SQLiteDatabase database = mySQLiteOpenHelper.getWritableDatabase();
    }
});

使用mySQLiteOpenHelper.getWritableDatabase()就可以创建数据库,或者mySQLiteOpenHelper.getReadableDatabase()。这两个方法都可以创建或者打开一个现有数据库。

我们运行下程序,为了查看数据库方便,安装下Database Navigator这个插件。
在这里插入图片描述
找到我们刚刚运行后的程序包下的数据库目录,:
![在这里插入图片描述]
然后将BookDatabase.db数据库导出,然后使用顶部出现的DB Browser来进行打开:
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
可以看见数据库和数据库表均自动创建成功。

数据库文件位于:data/data/package name/database/目录下。

上面的案例中,尝试了使用onCreate(SQLiteDatabase db)方法来创建数据库表格,注意到还有一个方法为onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)方法。

该方法主要用于数据库的升级,对数据库的管理工作起着比较重要的作用。具体应用场景为,当我们不是第一次创建数据库,也就是已经存在数据库,那么我们在我们定义的MySQLiteOpenHelper类中onCreate方法将不会执行。

那么,如果我们需要新增一个表格,就不能再onCreate方法中继续使用db.execSQL(sql);来创建。除非我们先将程序卸载,然后重新运行。这相当不方便,故而提供了onUpgrade方法。当我们提供的newVersion大于之前使用的版本号,这个方法就可以执行。

比如此时我们需要添加一个User表:

public class MySQLiteOpenHelper extends SQLiteOpenHelper {
    private Context context;
    private String name;
    private String bookSql = "create table Book (id integer primary key autoincrement, " +
            "name text, pages integer)";
    private String userSql = "create table User (name text, age integer)";

    public MySQLiteOpenHelper(@Nullable Context context,
                              @Nullable String name,
                              @Nullable SQLiteDatabase.CursorFactory factory,
                              int version) {
        super(context, name, factory, version);
        this.context = context;
        this.name = name;
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        // 创建数据库表
        db.execSQL(bookSql);
        db.execSQL(userSql);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        db.execSQL("drop table if exists Book");
        db.execSQL("drop table if exists User");
        onCreate(db); // 重新执行一下onCreate方法
    }
}

// Activity.java中修改版本号为3
button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        mySQLiteOpenHelper = new MySQLiteOpenHelper(getApplicationContext(),
                "BookDatabase.db", null, 3);
        SQLiteDatabase database = mySQLiteOpenHelper.getWritableDatabase();
    }
});

在这里插入图片描述
打印下日志:
在这里插入图片描述
版本号增大会执行onUpgrade方法。但是上面我们删除了数据表Book这中方式确实不好,所以我们通常在onUpgrade中进行版本判断,新增对应的数据库表即可。

3.2 增删改查(CRUD)操作

发现Database Inspector更加方便,且不用每次导出。
在这里插入图片描述

1. 增加

insert.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        ContentValues values = new ContentValues();
        values.put("name", "张三");
        values.put("pages", 234);
        db.insert("Book", null, values);
        values.clear();
        values.put("name", "李四");
        values.put("pages", 80);
        db.insert("Book", null, values);
    }
});

结果如上图。

2. 删除

delete.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        db.delete("Book", "pages > ?", new String[]{"100"});
    }
});

在这里插入图片描述

3. 修改

update.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        ContentValues values = new ContentValues();
        values.put("name", "王武");
        values.put("pages", 345);
        db.update("Book", values, "pages = ?", new String[]{"234"});
    }
});

在这里插入图片描述

4. 查询

query.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Cursor book = db.query("Book", new String[]{"id", "name", "pages"},
                null, null, null, null, null);
        if(book.moveToFirst()){
            do{
                int id = book.getInt(book.getColumnIndex("id"));
                String name = book.getString(book.getColumnIndex("name"));
                int pages = book.getInt(book.getColumnIndex("pages"));
                Log.d("TAG", "query: id=" + id + " name="+name+" pages="+pages);
            }while(book.moveToNext());
        }
        book.close();
    }
});

在这里插入图片描述

3.3 注

注意到db对象可以直接执行sql语句,故而我们可以使用sql语句来直接进行CRUD操作。

drop table if exists Book

insert into Book (name, pages) values(value1, value2)

select * from Book where name='张三'

update Book set name='战三' where pages > 100

delete from Book where pages > 129

3.4 SQLite的事务

SQLite中的操作默认是开启了事务的,比如前面我们的截图:

![在这里插入图片描述]
我们可以看见生成了一个对应的BookDatabase.db-journal文件,当使用CRUD的时候,是在这个临时文件上进行的,只有顺利完成了才会更新BookDatabase.db数据库,否则就会被回滚。

这个临时文件BookDatabase.db-journalAndroid中不会删除,当没有事务时候,大小为0,存在事务时候,使用该文件进行回滚。

但是,因为默认SQLite开启了事务处理,故而当进行批量处理的时候,大大影响性能。有两种处理方式:

  • 手动开启事务,批量操作,提交事务;
  • 使用db.complieStatement返回的SQLiteStatement进行sql语句操作;

比如:

button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        db.beginTransaction();
        ContentValues values = new ContentValues();
        for (int i = 0; i < datas.size(); i++) {
            Book book = datas.get(i);
            values.put("name", book.getName());
            values.put("pages", book.getPages());
            db.insert("Book", null, values);
            values.clear();
        }
        db.setTransactionSuccessful();
        db.endTransaction();
    }
});

3.5 SQLite的优化

  1. 因为默认支持事务,故而我们可以使用手动开启事务方式做批量操作;
  2. 及时关闭Cursor,避免内存泄露;
  3. 数据库的操作较多,可以使用子线程来执行;
  4. ContentValues初始化时候,如果可以预估容量,就直接填入预估容量。因为ContentValues底层使用HashMap来进行存储数据,故而当容量不够需要扩容操作,预估容量值可以减少不必要的扩容操作。

Thanks

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

梦否

文章对你有用?不妨打赏一毛两毛

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值