本章介绍Android五种主要存储方式的用法,包括共享参数SharedPreferences、数据库SQLite、SD卡文件、App的全局内存,另外介绍重要组件之一的应用Application的基本概念与常见用法,以及四大组件之一的内容提供器ContentProvider的基本概念与常见用法。
共享参数SharedPreferences
本节介绍Android的键值对存储方式——共享参数SharedPreferences的使用方法,包括如何保存数据与读取数据,通过共享参数结合“登录App”项目实现记住密码功能。
共享参数的基本用法
SharedPreferences是Android的一个轻量级存储工具,采用的存储结构是Key-Value的键值对方式,类似于Java的Properties类,二者都是把Key-Value的键值对保存在配置文件中。不同的是Properties的文件内容是Key=Value这样的形式,而SharedPreferences的存储介质是符合XML规范的配置文件。保存SharedPreferences键值对信息的文件路径是/data/data/应用包名/shared_prefs/文件名.xml。下面是一个共享参数的XML文件示例:
基于XML格式的特点,SharedPreferences主要适用于如下场合:
(1)简单且孤立的数据。若是复杂且相互间有关的数据,则要保存在数据库中。
(2)文本形式的数据。若是二进制数据,则要保存在文件中。
(3)需要持久化存储的数据。在App退出后再次启动时,之前保存的数据仍然有效。
实际开发中,共享参数经常存储的数据有App的个性化配置信息、用户使用App的行为信息、临时需要保存的片段信息等。
SharedPreferences对数据的存储和读取操作类似于Map,也有put函数用于存储数据、get函数用于读取数据。在使用共享参数之前,要先调用getSharedPreferences函数声明文件名与操作模式,示例代码如下:
// 从share.xml中获取共享参数对象SharedPreferences shared = getSharedPreferences("share", MODE_PRIVATE);
getSharedPreferences方法的第一个参数是文件名,上面的share表示当前使用的共享参数文件名是share.xml;第二个参数是操作模式,一般都填MODE_PRIVATE,表示私有模式。
共享参数存储数据要借助于Editor类,示例代码如下:
SharedPreferences.Editor editor = shared.edit(); // 获得编辑器的对象editor.putString("name", "Mr Lee"); // 添加一个名叫name的字符串参数editor.putInt("age", 30); // 添加一个名叫age的整型参数editor.putBoolean("married", true); // 添加一个名叫married的布尔型参数editor.putFloat("weight", 100f); // 添加一个名叫weight的浮点数参数editor.commit(); // 提交编辑器中的修改
共享参数读取数据相对简单,直接使用对象即可完成数据读取方法的调用,注意get方法的第二个参数表示默认值,示例代码如下:
String name = shared.getString("name", ""); // 从共享参数中获得名叫name的字符串int age = shared.getInt("age", 0); // 从共享参数中获得名叫age的整型数boolean married = shared.getBoolean("married", false); // 从共享参数中获得名叫married的布尔数float weight = shared.getFloat("weight", 0); // 从共享参数中获得名叫weight的浮点数
数据库SQLite
本节介绍Android的数据库存储方式—— SQLite的使用方法,包括如何建表和删表、变更表结构以及对表数据进行增加、删除、修改、查询等操作,然后通过SQLite结合“登录App”项目改进记住密码功能。
SQLite是一个小巧的嵌入式数据库,使用方便、开发简单,手机上最早由iOS运用,后来Android也采用了SQLite。SQLite的多数SQL语法与Oracle一样,下面只列出不同的地方:
(1)建表时为避免重复操作,应加上IF NOT EXISTS关键词,例如CREATE TABLE IF NOT EXISTS table_name。
(2)删表时为避免重复操作,应加上IF EXISTS关键词,例如DROP TABLE IF EXISTS table_name。
(3)添加新列时使用ALTER TABLE table_name ADD COLUMN …,注意比Oracle多了一个COLUMN关键字。
(4)在SQLite中,ALTER语句每次只能添加一列,如果要添加多列,就只能分多次添加。
(5)SQLite支持整型INTEGER、字符串VARCHAR、浮点数FLOAT,但不支持布尔类型。布尔类型数要使用整型保存,如果直接保存布尔数据,在入库时SQLite就会自动将其转为0或1,0表示false,1表示true。
(6)SQLite建表时需要一个唯一标识字段,字段名为_id。每建一张新表都要例行公事加上该字段定义,具体属性定义为_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL。
(7)条件语句等号后面的字符串值要用单引号括起来,如果没用使用单引号括起来,在运行时就会报错。
SQLiteDatabase是SQLite的数据库管理类,开发者可以在活动页面代码或任何能取到Context的地方获取数据库实例,参考代码如下:
// 创建名叫test.db的数据库。数据库如果不存在就创建它,如果存在就打开它SQLiteDatabase db = openOrCreateDatabase(getFilesDir() + "/test.db", Context.MODE_PRIVATE, null);// 删除名叫test.db数据库// deleteDatabase(getFilesDir() + "/test.db");
SQLiteDatabase提供了若干操作数据表的API,常用的方法有3类,列举如下:
1. 管理类,用于数据库层面的操作。
- openDatabase:打开指定路径的数据库。
- isOpen:判断数据库是否已打开。
- close:关闭数据库。
- getVersion:获取数据库的版本号。
- setVersion:设置数据库的版本号。
2. 事务类,用于事务层面的操作。
- beginTransaction:开始事务。
- setTransactionSuccessful:设置事务的成功标志。
- endTransaction:结束事务。执行本方法时,系统会判断是否已执行setTransactionSuccessful,如果之前已设置就提交,如果没有设置就回滚。
3. 数据处理类,用于数据表层面的操作。
- execSQL:执行拼接好的SQL控制语句。一般用于建表、删表、变更表结构。
- delete:删除符合条件的记录。
- update:更新符合条件的记录。
- insert:插入一条记录。
- query:执行查询操作,返回结果集的游标。
- rawQuery:执行拼接好的SQL查询语句,返回结果集的游标。
数据库帮助器SQLiteOpenHelper
SQLiteDatabase存在局限性,例如必须小心、不能重复地打开数据库,处理数据库的升级很不方便。Android提供了一个辅助工具—— SQLiteOpenHelper,用于指导开发者进行SQLite的合理使用。
SQLiteOpenHelper的具体使用步骤如下:
步骤01 新建一个继承自SQLiteOpenHelper的数据库操作类,提示重写onCreate和onUpgrade两个方法。其中,onCreate方法只在第一次打开数据库时执行,在此可进行表结构创建的操作;onUpgrade方法在数据库版本升高时执行,因此可以在onUpgrade函数内部根据新旧版本号进行表结构变更处理。
步骤02 封装保证数据库安全的必要方法,包括获取单例对象、打开数据库连接、关闭数据库连接。
- 获取单例对象:确保App运行时数据库只被打开一次,避免重复打开引起错误。
- 打开数据库连接:SQLite有锁机制,即读锁和写锁的处理;故而数据库连接也分两种,读连接可调用SQLiteOpenHelper的getReadableDatabase方法获得,写连接可调用getWritableDatabase获得。
- 关闭数据库连接:数据库操作完毕后,应当调用SQLiteDatabase对象的close方法关闭连接。
步骤03 提供对表记录进行增加、删除、修改、查询的操作方法。
可被SQLite直接使用的数据结构是ContentValues类,类似于映射Map,提供put和get方法用来存取键值对。区别之处在于ContentValues的键只能是字符串,查看ContentValues的源码会发现其内部保存键值对的数据结构就是HashMap“private HashMap<String, Object>mValues;”。ContentValues主要用于记录增加和更新操作,即SQLiteDatabase的insert和update方法。
对于查询操作来说,使用的是另一个游标类Cursor。调用SQLiteDatabase的query和rawQuery方法时,返回的都是Cursor对象,因此获取查询结果要根据游标的指示一条一条遍历结果集合。Cursor的常用方法可分为3类,说明如下:
1. 游标控制类方法,用于指定游标的状态。
- close:关闭游标。
- isClosed:判断游标是否关闭。
- isFirst:判断游标是否在开头。
- isLast:判断游标是否在末尾。
2. 游标移动类方法,把游标移动到指定位置。
- moveToFirst:移动游标到开头。
- moveToLast:移动游标到末尾。
- moveToNext:移动游标到下一条记录。
- moveToPrevious:移动游标到上一条记录。
- move:往后移动游标若干条记录。
- moveToPosition:移动游标到指定位置的记录。
3. 获取记录类方法,可获取记录的数量、类型以及取值。
- getCount:获取结果记录的数量。
- getInt:获取指定字段的整型值。
- getFloat:获取指定字段的浮点数值。
- getString:获取指定字段的字符串值。
- getType:获取指定字段的字段类型。
SD卡文件操作
本节介绍Android的文件存储方式—— SD卡的用法,包括如何获取SD卡目录信息、公有存储空间与私有存储空间的区别、在SD卡上读写文本文件、在SD卡读写图片文件等功能。
SD卡的基本操作
手机的存储空间一般分为两块,一块用于内部存储,另一块用于外部存储(SD卡)。早期的SD卡是可插拔式的存储芯片,不过自己买的SD卡质量参差不齐,经常会影响App的正常运行,所以后来越来越多的手机把SD卡固化到手机内部,虽然拔不出来,但是Android仍然称之为外部存储。
获取手机上的SD卡信息通过Environment类实现,该类是App获取各种目录信息的工具,主要方法有以下7种。
-
getRootDirectory:获得系统根目录的路径。
-
getDataDirectory:获得系统数据目录的路径。
-
getDownloadCacheDirectory:获得下载缓存目录的路径。
-
getExternalStorageDirectory:获得外部存储(SD卡)的路径。
-
getExternalStorageState:获得SD卡的状态。
SD卡状态的具体取值说明见表4-1。
-
getStorageState:获得指定目录的状态。
-
getExternalStoragePublicDirectory:获得SD卡指定类型目录的路径。
目录类型的具体取值说明见表4-2。
为正常操作SD卡,需要在AndroidManifest.xml中声明SD卡的权限,具体代码如下:
公有存储空间与私有存储空间
本来在AndroidManifest.xml里面配置了存储空间的权限,代码就能正常读写SD卡的文件。可是Android从7.0开始加强了SD卡的权限管理,即使App声明了完整的SD卡操作权限,系统仍然默认禁止该App访问外部存储。
不过系统默认关闭存储其实只是关闭外部存储的公共空间,外部存储的私有空间依然可以正常读写。这是缘于Android把外部存储分成了两块区域,一块是所有应用均可访问的公共空间,另一块是只有应用自己才可访问的专享空间。之前讲过,内部存储保存着每个应用的安装目录,但是安装目录的空间是很紧张的,所以Android在SD卡的“Android/data”目录下给每个应用又单独建了一个文件目录,用于给应用保存自己需要处理的临时文件。这个给每个应用单独建立的文件目录,只有当前应用才能够读写文件,其他应用是不允许进行读写的,故而“Android/data”目录算是外部存储上的私有空间。这个私有空间本身已经做了访问权限控制,因此它不受系统禁止访问的影响,应用操作自己的文件目录就不成问题了。当然,因为私有的文件目录只有属主应用才能访问,所以一旦属主应用被用户卸载,那么对应的文件目录也会一起被清理掉。
既然外部存储分成了公共空间和私有空间两部分,这两部分空间的路径获取也就有所不同。获取公共空间的存储路径,调用的是Environment.getExternalStoragePublicDirectory方法;获取应用私有空间的存储路径,调用的是getExternalFilesDir方法。下面是分别获取两个空间路径的代码例子:
应用的私有空间路径位于“外部存储根目录/Android/data/应用包名/files/Download”这个目录之下。
文本文件读写
文本文件的读写一般借助于FileOutputStream和FileInputStream。其中,FileOutputStream用于写文件,FileInputStream用于读文件。文件输出输入流是Java语言的基础工具,这里不再赘述,直接给出具体的实现代码:
图片文件读写
Android的图片处理类是Bitmap,App读写Bitmap可以使用FileOutputStream和FileInputStream。不过在实际开发中,读写图片文件一般用性能更好的BufferedOutputStream和BufferedInputStream。
保存图片文件时用到Bitmap的compress方法,可指定图片类型和压缩质量;打开图片文件时使用BitmapFactory的decodeStream方法。读写图片的具体代码如下:
应用Application基础
本节介绍Android重要组件Application的基本概念和常见用法。首先说明Application的生命周期,接着利用Application的持久特性实现App内部全局内存中的数据保存和获取。
Application的生命周期
Application是Android的一大组件,在App运行过程中有且仅有一个Application对象贯穿整个生命周期。打开AndroidManifest.xml时会发现activity节点的上级正是application节点,只是默认的application节点没有指定name属性,不像activity节点默认指定name属性值为.MainActivity,让人知晓这个activity的入口代码是MainActivity.java。现在试试给application节点加上name属性,看看其庐山真面目。
(1)打开AndroidManifest.xml,给application节点加上name属性,表示application的入口代码是MainApplication.java。
android:name=".MainApplication"
(2)创建MainApplication类,该类继承自Application,可以重写的方法主要有以下4个。
- Create:在App启动时调用。
- onTerminate:在App退出时调用(按字面意思)。
- onLowMemory:在低内存时调用。
- onConfigurationChanged:在配置改变时调用,例如从竖屏变为横屏。
(3)运行App,同时开启日志的打印。但是只在一开始看到MainApplication的onCreate操作(先于Activity的onCreate),却始终无法看到它的onTerminate操作,无论是自行退出还是强行杀死App的进程,日志都不会打印onTerminate。想在App退出前做资源回收操作,那么千万不要放在onTerminate方法中。
利用Application操作全局变量
C/C++有全局变量,因为全局变量保存在内存中,所以操作全局变量就是操作内存,内存的读写速度远比读写数据库或读写文件快得多。全局的意思是其他代码都可以引用该变量,因此全局变量是共享数据和消息传递的好帮手。不过,Java没有全局变量的概念。与之比较接近的是类里面的静态成员变量,该变量可被外部直接引用,并且在不同地方引用的值是一样的(前提是在引用期间不能修改该变量的值),所以可以借助静态成员变量实现类似全局变量的功能。
前面花费很大功夫介绍Application的生命周期,目的是说明其生命周期覆盖App运行的全过程。不像短暂的Activity生命周期,只要进入别的页面,原页面就被停止或销毁。因此,通过利用Application的持久存在性可以在Application对象中保存全局变量。
适合在Application中保存的全局变量主要有下面3类数据:
(1)会频繁读取的信息,如用户名、手机号等。
(2)从网络上获取的临时数据,为节约流量、减少用户等待时间,想暂时放在内存中供下次使用,如logo、商品图片等。
(3)容易因频繁分配内存而导致内存泄漏的对象,如Handler对象等。
要想通过Application实现全局内存的读写,得完成以下3项工作:
(1)写一个继承自Application的类MainApplication。该类要采用单例模式,内部声明自身类的一个静态成员对象,在创建App时把自身赋值给这个静态对象,然后提供该静态对象的获取方法getInstance。
(2)在Activity中调用MainApplication的getInstance方法,获得MainApplication的一个静态对象,通过该对象访问MainApplication的公共变量和公共方法。
(3)不要忘了在AndroidManifest.xml中注册新定义的Application类名,即在application节点中增加android:name属性,值为.MainApplication。
App把注册信息保存到MainApplication的全局变量中,然后在另一个页面从MainApplication的全局变量中读取保存好的注册信息。
完成以上编码后,Activity页面代码即可直接通过MainApplication.getInstance().mInfoMap对全局变量进行增、删、改、查操作。
内容提供与处理
本节介绍Android四大组件之一的ContentProvider的基本概念和常见用法。首先说明如何使用内容提供器封装数据的外部访问接口;接着阐述如何通过内容解析器在外部查询和修改数据,以及使用内容操作器完成批量数据操作;然后说明内容观察器的应用场合,并演示如何借助内容观察器实现流量校准的功能。
内容提供器ContentProvider
Android号称提供了4大组件,分别是页面Activity、广播Broadcast、服务Service和内容提供器ContentProvider。其中内容提供器是跟数据存取有关的组件,完整的内容组件由内容提供器ContentProvider、内容解析器ContentResolver、内容观察器ContentObserver这三部分组成。
ContentProvider为App存取内部数据提供统一的外部接口,让不同的应用之间得以共享数据。像我们熟知的SQLite操作的是应用自身的内部数据库;文件的上传和下载操作的是后端服务器的文件;而ContentProvider操作的是本设备其他应用的内部数据,是一种中间层次的数据存储形式。
在实际编码中,ContentProvider只是一个服务端的数据存取接口,开发者需要在其基础上实现一个具体类,并重写以下相关数据库管理方法。
- onCreate:创建数据库并获得数据库连接。
- query:查询数据。
- insert:插入数据。
- update:更新数据。
- delete:删除数据。
- getType:获取数据类型。
这些方法看起来是不是很像SQLite?没错,ContentProvider作为中间接口,本身并不直接保存数据,而是通过SQLiteOpenHelper与SQLiteDatabase间接操作底层的SQLite。所以要想使用ContentProvider,首先得实现SQLite的数据表帮助类,然后由ContentProvider封装对外的接口。
既然内容提供器是四大组件之一,就得在AndroidManifest.xml中注册它的定义,并开放外部访问权限,注册代码如下:
注册完毕后就完成了服务端App的封装工作,接下来可由其他App进行数据存取。
内容解析器ContentResolver
前面提到了利用ContentProvider实现服务端App的数据封装,如果客户端App想访问对方的内部数据,就要通过内容解析器ContentResolver访问。内容解析器是客户端App操作服务端数据的工具,相对应的内容提供器是服务端的数据接口。要获取ContentResolver对象,在Activity代码中调用getContentResolver方法即可。
ContentResolver提供的方法与ContentProvider是一一对应的,比如query、insert、update、delete、getType等方法,连方法的参数类型都一模一样。其中,最常用的是query函数,调用该函数返回一个游标Cursor对象,这个游标与SQLite的游标是一样的。
下面是query方法的具体参数说明(依顺序排列)。
- uri:Uri类型,可以理解为本次操作的数据表路径。
- projection:字符串数组类型,指定将要查询的字段名称列表。
- selection:字符串类型,指定查询条件。
- selectionArgs:字符串数组类型,指定查询条件中的参数取值列表。
- sortOrder:字符串类型,指定排序条件。
内容观察器ContentObserver
ContentResolver获取数据采用的是主动查询方式,有查询就有数据,没查询就没数据。有时我们不但要获取以往的数据,还要实时获取新增的数据,最常见的业务场景是短信验证码。电商App经常在用户注册或付款时发送验证码短信,为了给用户省事,App通常会监控手机刚收到的验证码数字,并自动填入验证码输入框。这时就用到了内容观察器ContentObserver,给目标内容注册一个观察器,目标内容的数据一旦发生变化,观察器规定好的动作马上触发,从而执行开发者预先定义的代码。
总结一下在Content组件经常使用的系统URI,详细的URI取值说明见表4-3。
表4-3 常用的系统URI取值说明