《第一行代码》读书笔记(六)

wshunli
2018-06-03 / 0 评论 / 14 阅读 / 正在检测是否收录...

《第一行代码》读书笔记 -- 数据存储方案

《第一行代码》读书笔记(一)-- 平台架构 (第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、应用安装包内文件数据访问

安装包内数据文件有很多种,这里主要介绍两部分:assetsraw

这两部分都会原封不动地打包进 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

评论 (0)

取消