Android编程权威指南总结(四)

第十四章     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 方法类似。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值