《第一行代码》读书笔记 -- 数据存储方案
《第一行代码》读书笔记(一)-- 平台架构 (第1章)
《第一行代码》读书笔记(二)-- 应用组件之 Activity (第2、4章)
《第一行代码》读书笔记(三)-- 应用组件之 Service (第10章)
《第一行代码》读书笔记(四)-- 应用组件之 BroadcastReceiver (第5章)
《第一行代码》读书笔记(五)-- 应用组件之 ContentProvider (第7章)
《第一行代码》读书笔记(六)-- 数据存储方案 (第6章)
《第一行代码》读书笔记(七)-- 多媒体资源 (第8章)
《第一行代码》读书笔记(八)-- 网络编程 (第9章)
第6章 数据存储全方案
Android 系统中主要提供了 3 种方式:文件存储、SharedPreference 以及数据库存储。
文件存储
对于文件存储,Android 系统不对数据进行格式化处理,适合一些简单的文本数据或者二进制数据。
1、将数据保存到文件中
String FILENAME = "hello_file";
String string = "hello world!";
FileOutputStream fos = openFileOutput(FILENAME, Context.MODE_PRIVATE);
fos.write(string.getBytes());
fos.close();
// 异常捕捉 ==>
FileOutputStream fos = null;
try {
fos = openFileOutput(FILENAME, Context.MODE_PRIVATE);
fos.write(string.getBytes());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
if (fos != null) {
fos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
书上是这样写的,主要为了提高性能:
String FILENAME = "hello_file";
String string = "hello world!";
FileOutputStream fileOutputStream;
BufferedWriter writer = null;
try {
fileOutputStream = openFileOutput(FILENAME, Context.MODE_PRIVATE);
writer = new BufferedWriter(new OutputStreamWriter(fileOutputStream));
writer.write(string);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (writer != null) {
writer.close();
}
} catch (IOException e) {
e.printStackTrace();
}
其中 MODE_PRIVATE 表示新创建文件,已经存在的文件会覆盖掉;
如果使用 MODE_APPEND 表示向存在的文件中追加内容,如果文件不存在会新建。
还有 MODE_WORLD_READABLE 和 MODE_WORLD_WRITEABLE 已经过时了。
2、从文件中读取数据
int SIZE = 4096;
byte[] buf = new byte[SIZE];
FileInputStream fis = null;
try {
fis = openFileInput(FILENAME);
int len = fis.read(buf);
while (len != -1) {
System.out.println(new String(buf, 0, len));
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fis != null) {
fis.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
书上是这样写的,也是为了提高性能:
FileInputStream fileInputStream;
BufferedReader reader = null;
StringBuilder content = new StringBuilder();
try {
fileInputStream = openFileInput(FILENAME);
reader = new BufferedReader(new InputStreamReader(fileInputStream));
String line;
while ((line = reader.readLine() )!= null) {
content.append(line);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (reader != null) {
reader.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
System.out.println(content.toString());
3、Android 系统数据存储位置
对于 Android 设备可分为 内部存储 和 外部存储。
3.1、内部存储
内部存储默认应用私有,其他 应用(和用户) 不能访问这些文件。卸载应用系统会自动删除。
应用的内部存储空间一般会有 files 和 cache 两个文件夹,分别代表永久存储和缓存数据。
File Context.getFilesDir()
方法 /data/user/0/com.wshunli.store.demo/files
File Context.getCacheDir()
方法 /data/user/0/com.wshunli.store.demo/cache
其中 0 代表不同用户,Android 6.0 以前不存在。
文件在 /data/data/com.wshunli.store.demo/ 目录下可以看到。
3.2、外部存储
外部存储可能是可移除的存储介质(例如 SD 卡)或内部(不可移除)存储,用户可以删除。
使用外部存储前应 检查介质可用性 :
/* Checks if external storage is available for read and write */
public boolean isExternalStorageWritable() {
String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state)) {
return true;
}
return false;
}
/* Checks if external storage is available to at least read */
public boolean isExternalStorageReadable() {
String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state) ||
Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
return true;
}
return false;
}
这部分比较特殊,也可以分为两种,可以保存与其他应用共享的文件,也可以保存应用私有文件(类似于内部存储)。
(1)对于外部存储的 私有 文件夹,一般也会有 files 和 cache 两个文件夹,4.4 及以后读写不再需要权限。
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="18" />
访问私有文件夹主要有两种方法:
File Context.getExternalFilesDir()
方法 /storage/emulated/0/Android/data/com.wshunli.store.demo/files
File Context.getExternalCacheDir()
方法 /storage/emulated/0/Android/data/com.wshunli.store.demo/cache
其中 getExternalFilesDir() 方法需要传入目录类型。传入 null
表示获取根目录。
目录类型 包括以下几种: DIRECTORY_MUSIC, DIRECTORY_PODCASTS, DIRECTORY_RINGTONES, DIRECTORY_ALARMS, DIRECTORY_NOTIFICATIONS, DIRECTORY_PICTURES, DIRECTORY_MOVIES.
https://developer.android.com/reference/android/os/Environment#DIRECTORY_MUSIC
有时,已分配某个内部存储器 分区 用作外部存储的设备可能还提供了 SD 卡槽。
在 Android 4.3 以前 Context.getExternalFilesDir() 只能获取内部分区的访问权限;
从 Android 4.4 开始 Context.getExternalFilesDirs() 可以同时访问两个位置,及内部分区和 SD 卡。
对于 Android 4.3 或者更低版本使用 ContextCompat.getExternalFilesDirs() 方法有同样的效果,可以同时访问两个位置。
(2)对于外部存储的 共享 文件夹,必须申请读写权限,而且 6.0 以后要申请运行时权限。
Environment.getExternalStorageDirectory()
方法返回值 /storage/emulated/0
Android 7.0 提供简化的 API 来访问常见的外部存储目录。
https://developer.android.com/training/articles/scoped-directory-access
Environment.getExternalStoragePublicDirectory()
返回设备上的 “公共” 位置。
同样需要传入目录类型,例如:
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM);
返回相机文件所在目录 /storage/emulated/0/DCIM
在外部文件目录中包含名为 .nomedia
的空文件(注意文件名中的点前缀),可在媒体扫描程序中隐藏文件。
4、应用安装包内文件数据访问
安装包内数据文件有很多种,这里主要介绍两部分:assets 和 raw 。
这两部分都会原封不动地打包进 apk 安装包,并不会编译成 二进制文件。
assets
文件夹在 app/src/main/assets/
允许创建目录结构。
AssetManager assetManager = getResources().getAssets();
assetManager.open(FILENAME);
raw
文件夹在 app/src/main/res/raw/
不允许创建目录结构,但会在 R.java 中自动进行资源标识。
InputStream inputStream = getResources().openRawResource(R.id.data);
SharedPreferences 存储
SharedPreferences 主要用于保存检索原始数据类型的永久性键值对。
1、使用 SharedPreferences 存储数据首先应 获取 SharedPreferences 对象,主要有三种方法:
(1)Context 类中 的 getSharedPreferences 方法,按照文件名称识别 preferences 。
SharedPreferences getSharedPreferences (String name, int mode)
SharedPreferences sharedPreferences = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
其中 PREFS_NAME 表示 preferences 文件名称,在 /data/data/com.wshunli.store.demo/shared_prefs/ 路径下 。
MODE_PRIVATE 表示操作模式,目前只有这一种模式可选,其他 MODE_WORLD_READABLE, MODE_WORLD_WRITEABLE , MODE_MULTI_PROCESS 已经废弃。
(2)Activity 类中的 getPreferences 方法,按照 Activity 识别 preferencs ,仅用于本 Activity 。
SharedPreferences getPreferences (int mode)
SharedPreferences preferences = getPreferences(MODE_PRIVATE);
仅指定 操作模式 即可。
(3)PreferenceManager 类中的 getDefaultSharedPreferences 静态方法,以应用包名作为前缀识别。
SharedPreferences getDefaultSharedPreferences (Context context)
SharedPreferences defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
2、获取 SharedPreferences 对象后就可以 存储和检索 数据了。
对于 数据存储 需要使用 SharedPreferences.Editor 接口。
public static final String PREFS_NAME = "wshunli";
SharedPreferences sharedPreferences = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString("website", "wshunli.com");
editor.putBoolean("is", true);
editor.apply();
我们可以在 shared_prefs 文件夹下发现 wshunli.xml
文件:
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="website">wshunli.com</string>
<boolean name="is" value="true" />
</map>
前面三种方式在 xml 文件命名不太一样,内容是一样的:
/data/data/com.wshunli.store.demo/shared_prefs/wshunli.xml
/data/data/com.wshunli.store.demo/shared_prefs/SPActivity.xml
/data/data/com.wshunli.store.demo/shared_prefs/com.wshunli.store.demo_preferences.xml
SharedPreferences.Editor 提交数据有两种方法,apply() 和 commit() ,两者主要区别有两点:
(1) commit() 有返回值,apply() 没有返回值。apply() 失败了是不会报错的。
(2) apply() 写入文件的操作是 异步 的,会把 Runnable 放到线程池中执行,而 commit() 的写入文件的操作是在当前线程 同步 执行的。
因此当两者都可以使用的时候还是推荐使用 apply() ,因为 apply() 写入文件操作是异步执行的,不会占用主线程资源。
3、从 SharedPreferences 中读取数据
直接使用 SharedPreferences 对象实例方法即可。
String getString (String key, String defValue)
String website = sharedPreferences.getString("website", "");
其他方法还有:
Map<String, ?> getAll () // 获取所有键值对
boolean contains (String key) // 是否包含某键值数据
数据库存储
Android 提供了对 SQLite 数据库的完全支持。
也有很多其他数据库可以使用,比如 Realm 、ObjectBox 等等。
一些数据库 ORM 框架也要学习,比如 GreenDao 、LitePal 等等。
这里只介绍 SQLite 数据库。
1、创建数据库
在 Android 中操纵 SQLite 数据库可以使用 SQLiteOpenHelper 类。
public class MSQLiteOpenHelper extends SQLiteOpenHelper {
public MSQLiteOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
this(context, name, factory, version, null);
}
public MSQLiteOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version, DatabaseErrorHandler errorHandler) {
super(context, name, factory, version, errorHandler);
}
@Override
public void onCreate(SQLiteDatabase db) {
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
其中构造函数:context 即上下文;name 为数据库名称;factory 为自定义 Cursor ,一般传入 null ;version 为数据库版本号,用于数据库升级。
然后调用 SQLiteOpenHelper 对象实例的 getReadableDatabase() 或者 getWritableDatabase() 即可创建数据库。
MSQLiteOpenHelper dbHelper = new MSQLiteOpenHelper(this, "book.db", null, 1);
dbHelper.getWritableDatabase();
数据库文件存储位置:/data/data/com.wshunli.sqlite.demo/databases/book.db
2、添加表结构
前面只是创建了空数据库,下面添加表。
使用 SQLiteDatabase 对象的 execSQL(String sql) 方法。
public class MSQLiteOpenHelper extends SQLiteOpenHelper {
public static final String CREATE_BOOK = "create table Book ("
+ "id integer primary key autoincrement, "
+ "author text, "
+ "price real, "
+ "pages integer, "
+ "name text)";
private Context context;
public MSQLiteOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
this(context, name, factory, version,null);
}
public MSQLiteOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version, DatabaseErrorHandler errorHandler) {
super(context, name, factory, version, errorHandler);
this.context = context;
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_BOOK);
Toast.makeText(context, "数据写入成功", Toast.LENGTH_LONG).show();
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
3、升级数据库
在添加数据库表时,非常有用。其实就是实现 onUpgrade 方法。
public class MSQLiteOpenHelper extends SQLiteOpenHelper {
public static final String CREATE_BOOK = "create table Book ("
+ "id integer primary key autoincrement, "
+ "author text, "
+ "price real, "
+ "pages integer, "
+ "name text)";
public static final String CREATE_CATEGORY = "create table Category ("
+ "id integer primary key autoincrement, "
+ "category_name text, "
+ "category_code integer)";
private Context context;
public MSQLiteOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
this(context, name, factory, version,null);
}
public MSQLiteOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version, DatabaseErrorHandler errorHandler) {
super(context, name, factory, version, errorHandler);
this.context = context;
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_BOOK);
db.execSQL(CREATE_CATEGORY);
Toast.makeText(context, "数据写入成功", Toast.LENGTH_LONG).show();
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("drop table if exists Book");
db.execSQL("drop table if exists Category");
onCreate(db);
}
}
这时我们在构造函数中 版本号大于 1 即可。
dbHelper = new MSQLiteOpenHelper(this, "book.db", null, 2);
如果在 onCreate 方法里直接添加 db.execSQL(CREATE_CATEGORY);
语句是不会执行的。
4、添加数据
对数据库的操作无非就是 CRUD :C 添加、R 查询、U 更新、D 删除。
添加数据使用 SQLiteDatabase 对象的 insert 方法。
long insert(String table, String nullColumnHack, ContentValues values)
SQLiteDatabase db = dbHelper.getWritableDatabase();
ContentValues values = new ContentValues();
// 开始组装第一条数据
values.put("name", "The Da Vinci Code");
values.put("author", "Dan Brown");
values.put("pages", 454);
values.put("price", 16.96);
db.insert("Book", null, values); // 插入第一条数据
values.clear();
// 开始组装第二条数据
values.put("name", "The Lost Symbol");
values.put("author", "Dan Brown");
values.put("pages", 510);
values.put("price", 19.95);
db.insert("Book", null, values); // 插入第二条数据
其中第二个参数 nullColumnHack 用于未指定添加数据的情形,一般直接传入 null 即可。
5、更新数据
更新数据使用 SQLiteDatabase 对象的 update 方法。
int update(String table, ContentValues values, String whereClause, String[] whereArgs)
SQLiteDatabase db = dbHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put("price", 10.99);
db.update("Book", values, "name = ?", new String[] { "The Da Vinci Code" });
其中 whereClause 、 whereArgs 用于约束数据第几行或者某几行,不指定则更新所有行。
6、删除数据
删除数据使用 SQLiteDatabase 对象的 delete 方法。
int delete(String table, String whereClause, String[] whereArgs)
SQLiteDatabase db = dbHelper.getWritableDatabase();
db.delete("Book", "pages > ?", new String[] { "500" });
其中 whereClause 、 whereArgs 用于约束数据第几行或者某几行,不指定则删除所有行。
7、查询数据
查询数据使用 SQLiteDatabase 对象的 query 方法。
Cursor query(String table, String[] columns, // 指定表名、列名
String selection, String[] selectionArgs,
String groupBy, String having,
String orderBy)
其中 selection 、 selectionArgs 用于约束数据第几行或者某几行,不指定则查询所有行。
然后 groupBy 指定 group by 的列,having 对 group by 结果进一步约束。
最后 orderBy 指定排序方式。
SQLiteDatabase db = dbHelper.getWritableDatabase();
// 查询Book表中所有的数据
Cursor cursor = db.query("Book", null, null, null, null, null, null);
if (cursor.moveToFirst()) {
do {
// 遍历Cursor对象,取出数据并打印
String name = cursor.getString(cursor.getColumnIndex("name"));
String author = cursor.getString(cursor.getColumnIndex("author"));
int pages = cursor.getInt(cursor.getColumnIndex("pages"));
double price = cursor.getDouble(cursor.getColumnIndex("price"));
Log.d("MainActivity", "book name is " + name);
Log.d("MainActivity", "book author is " + author);
Log.d("MainActivity", "book pages is " + pages);
Log.d("MainActivity", "book price is " + price);
} while (cursor.moveToNext());
}
cursor.close();
8、使用 SQL 语句操纵数据库
使用 SQLiteDatabase 对象的 execSQL 方法,还是挺麻烦的,可以使用一些 ORM 框架。
参考资料
1、存储选项 | Android Developers
https://developer.android.com/guide/topics/data/data-storage
2、FileInputStream读取文件数据的两种方式 - CSDN博客
https://blog.csdn.net/a909301740/article/details/52574602
3、全面的Android文件目录解析和获取方法(包含对6.0系统的说明) - CSDN博客
https://blog.csdn.net/zhangbuzhangbu/article/details/23257873
4、android 目录/data/data/ 跟 /data/user/0/ 差别 - V2EX
https://www.v2ex.com/t/259080
5、提供资源 | Android Developers
https://developer.android.com/guide/topics/resources/providing-resources
6、Android数据存储之Assets、Raw - CSDN博客
https://blog.csdn.net/sjm19901003/article/details/47026503
7、SharePreferences源码分析(commit与apply的区别以及原理) - CSDN博客
https://blog.csdn.net/Double2hao/article/details/53871640
8、Room,Realm,,ObjectBox 你选择哪个? - 泡在网上的日子
http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2017/0926/8551.html
9、【Android 数据库框架总结,总有一个适合你!】 - CSDN博客
https://blog.csdn.net/da_caoyuan/article/details/61414626
10、android基础---->SQLite数据库的使用 - huhx - 博客园
http://www.cnblogs.com/huhx/p/sqliteDatabase.html
评论 (0)