第十四章 SQLite数据库
Android设备上的应用都有一个沙盒目录。将文件保存在沙盒中,可阻止其他应用甚至是设备用户的访问和窥探。(当然,如果设备被root的话,用户就可以为所欲为)。
应用的沙盒目录是/data/data/[应用的包名称],例如,CriminalIntent 应用的沙盒目录是:/data/data/com.bignerdranch.android.criminalintent。
需要保存大量数据时,大多数应用都不会使用类似txt这样的普通文件。原因很简单:假设将数据写入了这样的文件,在仅需要修改数据标题的时候,就得首先读取整个文件的内容,完成修改后再全部保存,如果数据量大的话将会非常麻烦并且耗时耗资源。
这时候,我们可以使用SQLite。SQLite是类似于MySQL和PostgreSQL的开源关系型数据库。不同于其他数据库的是,SQLite使用单个文件存储数据,读写数据时使用SQLite库。Android标准库包含SQLite库以及配套的一些Java辅助类。
一、定义 schema
创建数据库前,首先要清楚存储什么样的数据。定义schema的方式有很多,如何选择因人而异。但所有方式都有一个共同的目标:“不要重复造轮子。”这也是人人都应遵守的编程规则:多花时间思考复用代码的编写和调用,避免在应用中到处使用重复代码。使用数据库也是如此。
首先创建一个定义Schema的Java类,命名为CrimeDbSchema,同时在新建类对话框中输入包名 database.CrimeDbSchema。这样就可以将CrimeDbSchema.java文件放入专门的database包中,实现数据库相关代码的统一组织和归类。
在CrimeDbSchema类中,再定义一个描述数据表的CrimeTable内部类,这个内部类唯一的作用就是定义描述数据表元素的String常量,比如数据库表名和数据库表各个字段。这样创建给修改字段名称或新增表元素带来方便。
二、创建初始数据库
openOrCreateDatabase(......) 和 databaseList() 是Android提供的Context底层方法,用来打开数据库文件并将其转换为SQLiteDatabase实例。
实践中,建议遵循以下步骤:
- 确认目标数据库是否存在
- 如果不存在,首先创建数据库,然后创建数据表并初始化数据
- 如果存在,打开并确认 CrimeDbSchema 是否为最新(项目后续版本可能有改动)
- 如果是旧版本,就先升级到最新版本
以上工作可借助Android的 SQLiteOpenHelper 类处理。
调用 getWritableDatabase() 方法时,CrimeBaseHelper 会做如下的工作:
- 打开 /data/data/com.bignerdranch.android.criminalintent/databases/crimeBase.db 数据库;如果不存在,就先创建 crimeBase.db数据库文件。
- 如果是首次创建数据库,就调用 onCreate(SQLiteDatabase) 方法,然后保存最新的版本号。
- 如果已经创建过数据库,就先检查它的版本号。如果 CrimeBaseHelper 中的版本号更高,就调用 onUpgrade(SQLiteDatabase,int,int)方法升级。
最后做个总结:onCreate(SQLiteDatabase)方法负责创建初始数据库;onUpgrade(SQLiteDatabase,int,int)方法负责与升级相关的工作。
如果使用模拟器或者是已 root 的设备,可直接看到已创建的数据库文件。
三、修改 CrimeLab 类
四、写入数据库
1、使用 ContentValues
负责处理数据库写入和更新操作的辅助类是 ContentValues。它是一个键值存储类,类似于 HashMap 和 Bundle。不同的是,ContentValues 只能用于处理SQLite数据。
2、插入和更新记录
(1)插入记录
insert(String,String,ContentValues)方法的第一个和第三个参数很重要,第二个很少用到。传入的第一个参数是数据表名,第三个是要写入的是数据。
第二个参数称为 nullColumnHack 。它有什么用途呢?
假设你想调用 insert(......) 方法,但传入了 ContentValues 空值。这时,SQLite不干了,insert(......) 方法调用只能以失败告终。然而,如果能以 uuid 值作为 nullColumnHack 传入的话,SQLite就可以忽略 ContentValues 空值,而且还会自动传入一个带 uuid 且值为null的 ContentValues,然后 insert(......) 方法就能成功调用并插入一条新纪录。
(2)更新记录
update(String,ContentValues,String,String[ ])方法类似于 insert(......) 方法,向其传入要更新的数据表名和为表记录准备的 ContentValues,然而,与 insert(......) 方法不同的是,你要确定该更新那些记录。具体的做法是:创建 where 语句(第三个参数),然后指定 where 语句中的参数值(String[ ] 数组参数)。
mDatabase.update(CrimeTable.NAME, values, CrimeTable.Cols.UUID + " = ?",
new String[] { uuidString });
问题来了,为什么不直接在where语句中放入uuidString呢?这可比使用 ? 然后传入String[ ] 简单多了!
主要因为,很多时候,String本身会包含SQL代码。如果将它直接放到query语句中,这些代码可能会改变query语句的含义,甚至会修改数据库资料。这实际就是SQL脚本注入,其危害相当严重。而使用 ? 的话,就不用关心String包含什么,代码执行的效果肯定就是我们想要的。
五、读取数据库
读取数据库需要用到 SQLiteDatabase.query(......) 方法。
1、使用CursorWrapper
Cursor是个神奇的表数据处理工具,其功能就是封装数据表中的原始字段值。从Cursor获取数据的代码大致如下:
String uuidString = cursor.getString(cursor.getColumnIndex(CrimeTable.Cols.UUID));
String title = cursor.getString(cursor.getColumnIndex(CrimeTable.Cols.TITLE));
long date = cursor.getLong(cursor.getColumnIndex(CrimeTable.Cols.DATE));
int isSolved = cursor.getInt(cursor.getColumnIndex(CrimeTable.Cols.SOLVED));
每从Cursor中取出一条记录,以上代码都要重复写一次,这样不符合代码复用的原则。所以我们可以创建可复用的专用Cursor子类。创建Cursor子类最简单的方法是使用CursorWrapper,用CursorWrapper封装Cursor对象,然后再添加有用的扩展方法。
2、创建模型层对象
数据库cursor之所以被称为cursor,是因为它内部就像有根手指一样,总是指向查询的某个元素。因此,要从cursor中取出数据,首先要调用 moveToFirst() 方法移动虚拟手指指向第一个元素;读取行记录之后,再调用 moveToNext() 方法,读取下一行记录,直到 isAfterLast() 说没有数据可取为止。
最后,别忘了调用Cursor的 close() 方法关闭它,否则会出错,轻则应用报错,重则应用崩溃。
六、深入学习:数据库高级主题介绍
很多高级应用都需要高级数据库使用支持。为了提高开发效率,人们常常会求助于ORM这样专业又复杂的工具。
开发复杂的数据库应用时,难免会需要以下数据库元素:
- 字段类型。从技术实现上讲,SQLite的字段不需要指定数据类型。没有它们完全不影响数据库的使用;当然,如果能给出数据类型提示会更好。
- 索引。查询数据库字段时,字段不加索引会严重影响性能和效率。
- 外键。如果涉及多张表,关联数据需要外键约束。
数据库性能优化需要考虑的因素众多。
七、深入学习:应用上下文(getApplicationContext())
为什么有时候要用应用上下文而不直接用activity作为context?
关键在于考虑它们的生命周期。只要有activity在,Android就肯定创建了application对象,用户在应用的不同界面间导航时,各个activity时而存在时而消亡,但application对象不会受任何影响,可以说,它的生命周期要比任何activity都长。
我们创建一个单例的CrimeLab,CrimeLab代码中引用着Context对象。这个CrimeLab一旦创建就会一直存在,直至整个应用进程被销毁。显然,如果把activity作为Context对象保存的话,这个由CrimeLab一直引用着的activity肯定会免遭垃圾回收器的处理,即便用户跳转离开这个activity时也是如此。
为了避免资源浪费,我们使用应用上下文。这样,CrimeLab仍可以引用Context对象,而activity的生死也不用受它束缚了。
第十五章 隐式 Intent
Android系统中,可利用隐式Intent启动其他应用的Activity。在显式Intent中,我们指定要启动的activity类,操作系统会负责启动它。在隐式Intent中,我们只需要描述要完成的任务,操作系统就会找到合适的应用,并在其中启动相应的activity。
对于开发者来说,使用隐式Intent利用其他应用完成常见任务,远比自己编写代码从头到尾要容易的多。对于用户来说,他们也乐意在应用中调用自己熟悉或喜爱的应用。
一、添加按钮组件
二、添加信息至模型层
注意!如果设备上已安装该应用,当前应用中的数据库是不包含新添加的字段的,而且onCreate(SQLiteDatabase)方法也不会添加这个字段。这时,最容易的解决方法就是删除旧数据库,也就是将应用重新卸载安装。
三、使用格式化字符串
应用运行前,我们无法获知具体的细节。因此,必须使用带有占位符(可在应用运行时替换)的格式化字符串。下面是将要使用的格式化字符串:
<string name="crime_report">%1$s! The crime was discovered on %2$s. %3$s, and %4$s</string>
%1$s、%2$s等特殊字符串即为占位符,它们接受字符串参数。在代码中,我们将调用 getString(......) 方法,并传入格式化字符串资源ID以及另外四个字符串参数(与要替换的占位符顺序一致)。
四、使用隐式Intent
1、隐式intent的组成
(1)要执行的动作
通常以Intent类中的常量来表示。例如,要访问某个URL,可以使用 Intent.ACTION_VIEW;要发送邮件,可以使用 Intent.ACTION_SEND。
(2)待访问数据的位置
这可能是设备以外的资源,如某个网页的URL,也可能是指向某个文件的URI,或者是指向 ContentProvider 中某条记录的某个内容URI(content URI)。
(3)操作涉及的数据类型
这指的是 MIME 形式的数据类型,如 text/html 或 audio/mpeg3。如果一个 intent 包含数据位置,那么通常可以从中推测出数据的类型。
(4)可选类别
操作用于描述具体要做什么,而类别通常用来描述你打算何时、何地或者如何使用某个activity。例如,Android的 android.intent.category.LAUNCHER 类别表明,activity 应该展示在顶级应用启动器中;而 android.intent.category.INFO 类别表明,虽然activity向用户显示了包信息,但它不应该出现在启动器中。
一个查看某个网址简单隐式intent会包括一个 Intent.ACTION_VIEW 操作,以及某个具体URL网址的Uri数据。
基于以上信息,操作系统将启动适用的activity。(如果有多个应用适用,用户自己挑)
通过配置文件中的intent过滤器设置,activity会对外宣称自己是适合处理ACTION_VIEW的activity。例如,如果想开发一款浏览器应用,为了响应ACTION_VIEW操作,你可以在activity声明中包含以下intent过滤器:
<activity
android:name=".BrowserActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="http" android:host="www.bignerdranch.com" />
</intent-filter>
</activity>
必须在intent过滤器中明确的设置DEFAULT类别。action元素告诉操作系统,activity能够胜任指定任务。DEFAULT类别告诉操作系统(问谁可以做时),activity愿意处理某项业务。DEFAULT类别实际隐含于所有模式intent中(当然也有例外,24章)。
和显示intent一样,隐式intent也可以包含extra信息。不过,操作系统在寻找适用的activity时,不会使用附加在隐式intent上的任何extra。
注意,显式intent也可以使用隐式intent的操作和数据部分。这相当于要求特定的activity去做特定的事情。
2、发送消息
例子:消息是由字符串组成的文本信息,我们的任务是发送一段文本消息,因此隐式intent的操作是 ACTION_SEND,它不指向任何数据,也不包含任何类别,但会指定数据类型为 text/plain 。
Intent i = new Intent(Intent.ACTION_SEND);
i.setType("text/plain");
i.putExtra(Intent.EXTRA_TEXT,getCrimeReport());
i.putExtra(Intent.EXTRA_SUBJECT,getString(R.string.crime_report_suspect));
startActivity(i);
以上代码使用了一个接收字符串参数的Intent构造方法,我们传入的是一个定义操作的常量。取决于要创建的隐式intent类别,还有一些其他形式的构造方法可用,可以查阅Intent参考文档进一步了解。因为没有接收数据类型的构造方法可用,所以必须专门设置它。
消息内容和主题是作为extra附加在intent上的。注意,这些extra信息使用了Intent类中定义的常量。因此,任何响应该intent的activity都知道这些常量,自然也知道该如何使用它们的关联值。
使用隐式intent启动activity时,也可以创建每次都显示的activity选择器。和以前一样创建隐式intent后,调用以下Intent方法并传入创建的隐式intent以及用作选择器标题的字符串:
public static Intent createChooser(Intent target, String title)
然后,将 createChooser(......) 方法返回的intent传入 startActivity(......) 方法。
i = Intent.createChooser(i,getString(R.string.send_report));
3、获取联系人信息
创建另一个隐式intent,这个隐式intent将由操作以及数据获取位置组成。操作为 Intent.ACTION_PICK,联系人数据获取位置为 ContactsContract.contacts.CONTENT_URI。
(1)从联系人列表中获取联系人数据
很多应用都会共享联系人信息,因此Android提供了一个深度定制的API用于处理联系人信息:ContentProvider 类。该类的实例封装了联系人数据库并提供给其他应用使用。我们可以通过 ContentResolver 访问 ContentProvider。
(2)联系人信息使用权限
如何获得读取联系人数据库的权限呢?实际上,这是联系人应用将其权限临时赋予了我们。联系人应用有使用联系人数据库的全部权限。联系人应用返回包含在intent中的URI数据给父activity时,会添加一个Intent.FLAG_GRANT_READ_URI_PERMISSION 标志。该标志告诉Android,应用中的父activity可以使用联系人数据一次。这很有用,因为不需要访问整个联系人数据库,我们只需要访问其中一条联系人信息。
4、检查可响应任务的activity
如果操作系统找不到可用的activity,应用就会崩溃。解决方法是首先通过操作系统中的PackageManager类进行自检,在onCreateView(......)方法中实现检查:
PackageManager packageManager = getActivity().getPackageManager();
if (packageManager.resolveActivity(pickContact,
PackageManager.MATCH_DEFAULT_ONLY) == null) {
mSuspectButton.setEnabled(false);
}
Android设备上安装了哪些组件以及包括哪些activity,PackageManager类全都知道。调用 resolveActivity(Intent,int) 方法,可以找到匹配给定Intent任务的activity。flag标志 MATCH_DEFAULT_ONLY 限定只搜索带 CATAGORY_DEFAULT 标志的activity。这和 startActivity 方法类似。