四、小心连线

|   | “过早优化是万恶之源。” |   | |   | --唐纳德·克努特 |

在前一章中,我们讨论了一个非常重要的概念:内容提供商。我们以循序渐进的方式取得进展,涵盖了一些基本问题,例如如何创建内容提供商,以及如何与内容提供商一起使用现有系统。我们还介绍了如何通过创建测试应用来访问我们创建的内容提供者。

在本章中,我们将探讨如何使用加载器,特别是一个名为游标加载器的加载器。我们将借助一个示例来了解如何与内容提供商异步交互。我们将讨论安卓数据库中的安全这个重要话题,以及我们如何确保安卓模型中的数据安全。最后但同样重要的是,我们还将看到一些代码片段,这些代码片段将涵盖诸如如何升级数据库以及如何将预加载的数据库与我们的应用一起交付等主题。

在本章中,我们将涵盖以下主题:

  • 用 CursorLoader 加载数据
  • 数据安全
  • 一般提示和库

用光标加载器加载数据

CursorLoader是装载机家族的一部分。在我们深入探讨解释如何使用CursorLoader的示例之前,我们将探讨一下加载器,以及为什么它在当前场景中很重要。

装载机

在蜂巢(API 级别 11)中引入的加载器 服务于异步服务活动或片段中的数据的目的。对加载器的需求来自于许多事情:调用主用户界面线程上各种耗时的方法来获取数据,这导致了一个笨重的用户界面,甚至在某些情况下,可怕的 ANR 盒。这在下面的截图中有所展示:

Loaders

例如,在 API 11 中被否决的managedQuery()方法是ContentResolver'squery()方法的包装器。

在前一章中,在强调如何从查询方法内部的内容提供者获取数据时,我们使用了getContentResolver.query()而不是managedQuery()。使用不推荐使用的方法会导致未来版本出现问题,应该避免使用。

加载器为非用户界面线程上的片段活动提供异步数据加载。加载器或加载器的子类在一个单独的线程中执行它们的工作,并将它们的结果传递给主线程。将调用从主线程中分离出来,并在主线程上发布结果,同时在单独的线程中工作,这确保了我们有一个响应的应用。

类型

在加载器时代之后,我们面临着一些问题,比如什么时候应该由于配置更改而重新创建一个活动,例如设备方向的旋转。我们必须担心数据,并在创建新实例时重新提取数据。但是对于加载器,我们不必担心所有这些,因为在设备配置更改后重新创建加载器并重新提取数据时,加载器会自动重新连接到最后一个加载器的光标。另一个好处是,加载器监控数据源,并在内容发生变化时提供新的结果。换句话说,加载器会自动更新,因此,不需要重新查询光标。在 Android 开发者网站http://developer . Android . com/training/articles/perf-anr . html上阅读更多关于保持 Android 应用响应并避免 应用不响应 ( ANR )消息的内容。

加载器 API 的总结

让我们看看由各种类和接口组成的加载器 API。在本节中,我们将研究加载器应用编程接口类/接口的实现方面:

|

类/接口

|

描述

| | --- | --- | | LoaderManager | 这是一个与活动或片段相关联的抽象类,用于管理加载器。虽然可以有一个或多个加载器实例,但是每个活动或片段只允许有一个LoaderManager实例。它负责处理活动或片段的生命周期,在运行长时间运行的任务时特别有用。 | | LoaderManager.LoaderCallbacks | 这是我们与LoaderManager交互必须实现的回调接口。 | | Loader | 这是加载程序的基类。这是一个执行数据异步加载的抽象类。我们可以实现自己的子类,而不是使用CursorLoader这样的子类。 | | AsyncTaskLoader | 这是一个抽象加载器,提供AsyncTask在后台执行工作,也就是在一个单独的线程上;但是,结果在主线程上传递。根据文档,建议子类化AsyncTaskLoader,而不是直接子类化Loader类。 | | CursorLoader | 这是AsyncTaskLoader的一个子类,它以非阻塞的方式在后台线程上查询ContentResolver,并返回一个游标。 |

使用光标加载器

加载器为我们提供了很多便利的特性;其中之一是,一旦我们的活动或片段实现了一个加载器,它就不用担心刷新数据了。加载器为我们监视数据源,反映任何变化,甚至执行新的加载;所有这些都是异步完成的。因此,我们不需要关心实现和管理线程,在后台线程上卸载查询,并在查询完成后检索结果。

加载程序可以处于以下三种不同状态中的任何一种:

  • 启动状态:一旦启动,装载机保持在该状态,直到停止或复位。它执行加载,监视任何更改,并将更改反映给侦听器。
  • 停止状态:这里,加载器继续监控变化,但不将结果传递给客户端。
  • 重置状态:在这种状态下,加载器释放它们所拥有的任何资源,并且不执行执行、加载或监控数据的过程。

我们现在将重新查看我们的个人联系人管理器应用,并进行相应的更改以在我们的应用中实现CursorLoaderCursorLoader,顾名思义,是一个查询ContentResolver并返回光标的加载器。这是AsyncTaskLoader的一个子类,在后台线程上执行光标查询,这样就不会阻塞应用的 UI。在图中,您可以看到加载器回调的各种方法,以及它们如何与CursorLoaderCursorAdapter通信。

Using CursorLoader

为了实现光标加载器,我们需要执行以下步骤:

  1. To begin with, we need to implement the LoaderManager.LoaderCallbacks<Cursor> interface:

    java public class ContactsMainActivity extends Activity implements OnClickListener, LoaderManager.LoaderCallbacks<Cursor> {…}

    然后,实现反映加载器不同状态的方法:onCreateLoader()onLoadFinished()onLoaderReset()

  2. To initiate a query, we will make a call to the LoaderManager.initLoader() method; this initializes the background framework:

    java getLoaderManager().initLoader(CUR_LOADER, null, this);

    CUR_LOADER值被传递给onCreateLoader()方法,该方法充当加载器的标识。对initloader()的呼叫调用onCreateLoader(),传递我们过去称为initloader()的 ID:

    java @Override public Loader<Cursor> onCreateLoader(int loaderID, Bundle bundle) { switch (loaderID) { case CUR_LOADER: return new CursorLoader(this, PersonalContactContract.CONTENT_URI, PersonalContactContract.PROJECTION_ALL, null, null, null ); default: return null; } }

  3. We use a switch case to take the loader based on its ID and return null for an invalid ID. We create a URI object contentUri and pass it as a parameter to the CursorLoader constructor. A point to note is that we can implement a cursor loader using either this constructor or an empty unspecified cursor loader, CursorLoader(Context context). Also, we can set values via methods such as setUri(Uri), setSelection(String), setSelectionArgs(String[]), setSortOrder(String), and setProjection(St``ring[]):

    java public CursorLoader (Context context, Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)

    以下是之前代码的参数:

    • context:这是父活动上下文。
    • uri:我们采用contentURI,使用content://方案,检索内容。它可以基于一个标识或目录。
    • projection:这是一个要返回的列列表,因为我们准备了列名。通过null将返回所有列。
    • selection:这被格式化为一个 SQL WHERE子句,不包括WHERE本身,充当一个声明返回哪些行的过滤器。
    • selectionArgs:我们可以在选择中包含问号,问号会被selectionArgs中绑定为字符串的值所代替,它们会按照选择的顺序出现。
    • sortOrder:这告诉我们如何对行进行排序,格式为一个 SQL ORDER BY子句。空值将使用默认的排序顺序。
    • onCreateLoader在后台启动查询,当查询完成后,光标加载器对象被传递到后台的框架,框架调用onLoadFinished(),在这里我们为适配器实例提供光标对象数据:

    java @Override public void onLoadFinished(Loader<Cursor> loader, Cursor data) { this.mAdapter.changeCursor(data); }

  4. 适配器是CursorAdapter的子类。我们有bindView()newView()方法,而不是传统的getView()方法,传统的getView()方法是通过扩展BaseAdapter得到的。我们在newView的视图对象中展开我们的 listview 行布局,在绑定视图中,我们执行类似于getView()方法的操作。我们定义布局元素,并将主题与相关数据相关联:

    ```java public class CustomCursorAdapter extends CursorAdapter { ... public void bindView(View view, Context arg1, Cursor cursor) { finalImageView contact_photo = (ImageView) view .findViewById(R.id.contact_photo); ... ... contact_email.setText(cursor.getString(cursor .getColumnIndexOrThrow(DatabaseConstants.TABLE_ROW_EMAIL))); setImage(cursor.getBlob(cursor .getColumnIndex(DatabaseConstants.TABLE_ROW_PHOTOID)), contact_photo); }

    @Override public View newView(Context arg0, Cursor arg1, ViewGroup arg2) { final View view = LayoutInflater.from(context).inflate( R.layout.contact_list_row, null, false); return view; } ... } ```

  5. 当光标加载器被重置时,该方法被调用。我们通过将null传递给changeCursor()方法来清除对光标的任何引用。每当与游标相关联的数据发生变化时,游标加载器都会在重新运行查询以清除任何过去的引用之前调用此方法,从而防止内存泄漏。一旦设置了onLoaderReset(),光标加载器将重新运行其查询:

    ```java @Override public void onLoaderReset(Loader loader) { this.mAdapter.changeCursor(null);

    }
    

    ```

  6. 现在,我们转向我们的内容提供商,在那里我们必须进行小的更改,以确保我们对数据库所做的任何更改都反映在我们应用的列表视图中:

    java cr.setNotificationUri(getContext().getContentResolver(),uri);

  7. 我们需要通过ContentProvider的查询方式中的光标在ContentResolver中注册observer。我们这样做是为了观察内容 URI 的任何变化,在我们的例子中,它可以是特定数据行或表的 URI:

    java getContext().getContentResolver().notifyChange(ur,null);

  8. insert()方法中,我们使用notifyChange()方法通知注册观察员一行已更新。默认情况下,CursorAdapter对象会收到此通知。因此,现在当我们通过在应用中插入新的联系人来添加新的数据行时,contentProviderinsert()方法通过调用被调用:

    java resolver.insert(PersonalContactContract.CONTENT_URI, prepareData(contact));

  9. 对于delete()update()方法也需要执行类似的操作,这两个方法都是留给读者的练习,因为大部分样板代码都存在。实现一个加载器很简单,当涉及到线程时,它可以让我们免去很多头痛的事情,强烈建议使用不和谐的用户界面来执行这项任务。

loadInBackground()是另一个重要的方法;这将返回一个加载操作的游标实例,并在工作线程上调用。理想情况下,loadInBackground ()不应该直接返回负载操作的结果,但是我们可以通过覆盖deliverResult(D)方法来实现。要取消,我们需要检查isLoadInBackgroundCanceled()的值,就像我们在AsyncTask的情况下一样,我们定期检查isCancelled()

数据安全

安全是镇上最新的流行语。安卓生态系统确保我们的数据库不会被窥视;然而,一个有根的设备会暴露我们的数据库,正如我们在第二章连接点中看到的。在一个根设备、一个模拟器和adb pull命令的帮助下,我们用 SQLite 管理器工具提取了数据库进行检查。另一个重要方面是内容提供商;我们在设置权限时需要小心。我们应该强制应用适当权限的过程,以便使用contract类通知用户应用对数据建立的控制。

内容提供者和权限

第 3 章共享即关怀中,我们在将提供者添加到清单部分简要介绍了权限主题。让我们对此详细阐述一下:

  1. 如前所述,在将内容提供商添加到清单的同时,我们还将添加我们的自定义权限。这将确保两件事,即停止应用中未经授权的操作,并通知用户权限:

    ```java <provider android:name="com.personalcontactmanager.provider.PersonalContactProvider" android:authorities="com.personalcontactmanager.provider" android:readPermission="com.personalcontactmanager.provider.read" android:exported="true" android:grantUriPermissions="true"

    ```

  2. 此外,我们将在清单中添加permissions标签,以指示其他应用需要的权限集:

    java <permission android:name="com.personalcontactmanager.provider.read" android:icon="@drawable/ic_launcher" android:label="Contact Manager" android:protectionLevel="normal" > </permission>

  3. Now, in the application in which we want to access the content provider we use the permission tag, in our case, Ch4-TestApp in code bundle:

    java <uses-permission android:name="com.personalcontactmanager.provider.read" />

    当用户安装这个应用时,他们将获得我们的自定义权限消息以及应用所需的其他权限。对于这一步,不要直接从 Eclipse 运行应用,而是导出一个 apk 并安装它:

    ContentProvider and permissions

如果您没有在应用中定义权限,并且应用试图访问内容提供商,它将获得SecurityException: Permission Denial消息。

如果我们创建的内容提供商不打算共享,我们需要将android:exported="true"属性更改为false。这将使我们的内容提供商安全,如果有人试图对其运行恶意查询,他们将遇到安全异常。

如果我们希望只在我们的应用之间共享数据,Android 提供了解决方案;我们可以使用android:protectionLevel并将权限设置为signature而不是normal。为此,应用(实现内容提供商的应用和想要访问内容提供商的应用)在导出时都必须用相同的密钥签名。这是因为额外的签名权限不需要用户确认。这不会让用户感到困惑,因为这是在内部完成的,也不会妨碍用户体验。

加密关键数据

我们已经讨论了其他应用对我们的数据库拥有什么样的访问权限,以及如何高效地共享我们的内容提供商,我们还简要讨论了为什么我们不应该相信该系统是万无一失的。最简单的方法是,敏感数据不会保存在设备上,而是保存在服务器上,它会使用令牌进行访问。如果您必须将数据存储在设备的数据库中,请使用加密。使用用户定义的密钥加密和解密敏感数据。

我们将探索一种使用加密数据库的方法,如果有人能够通过根目录或通过利用备份来提取它,则该数据库将不可读。如果有人试图使用 SQLite Manager 或其他一些工具阅读它,他们会收到一条友好的消息,如下面截图中所示;这是我们稍后将使用名为 SQLCipher 的库创建的数据库文件。

Encrypting critical data

SQLCipher 是 SQLite 的开源扩展,提供数据库文件的透明 256 位 AES 加密,正如他们的网站上提到的。部署 SQLCipher 非常容易。现在,我们将了解构建示例应用的步骤:

  1. 首先,我们将从http://sqlcipher.net/open-source下载必要的文件。在这里,他们列出了基于安卓的 SQLCipher 的社区版;下载它。
  2. 现在我们将在 eclipse 环境中创建一个新的安卓项目。
  3. Inside the downloaded folder, we will find the libs folder; inside it, are a set of jars that we will need to work with SQLCipher. We will also notice that folders are named as armeabi, armeabi-v7a, and x86, and all of these contain the .so files. If you are familiar with Android NDK, this will not seem new. The .so file is a shared object file, which is a component of dynamic libraries. For different architectures, we require different .so files, hence the three folders. If you are running an x86 emulator, you will need the x86 folder in your libs folder. For simplicity, we will copy all the folders to the libs folder. Copy the asset folder's content into our project's asset folder and navigate to the project's properties. It will look something like the following screenshot. You can also see these JAR files in the project's class path. The initial setup for this project is now complete.

    Encrypting critical data

    完成必要的设置部分后,让我们开始编写代码,制作一个小的测试应用:

    ```java public class MainActivity extends Activity { TextView showResult;

    @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); showResult = (TextView) findViewById(R.id.showResult); InitializeSQLCipher(); }

    private void InitializeSQLCipher() { SQLiteDatabase.loadLibs(this); File databaseFile = getDatabasePath("test.db"); databaseFile.mkdirs(); databaseFile.delete(); SQLiteDatabase database = SQLiteDatabase .openOrCreateDatabase(databaseFile, "test123", null); database.execSQL("create table t1(a, b)"); database.execSQL("insert into t1(a, b) values(?, ?)", new Object[] {"I am ", "Encrypted" }); }

    public void runQuery(View v) { File databaseFile = getDatabasePath("test.db"); SQLiteDatabase database = SQLiteDatabase.openOrCreateDatabase( databaseFile, "test123", null); String selection = "select * from t1"; Cursor c = database.rawQuery(selection, null); c.moveToFirst(); showResult.setText(c.getString(c.getColumnIndex("a")) + c.getString(c.getColumnIndex("b"))); } } ```

    前面的代码有两个主要方法:InitializeSQLCipher()runQuery()。在InitializeSQLCipher()内部,我们通过调用loadLibs()方法加载我们的.so库文件。

  4. Now we find the absolute path to the database and create a missing parent folder if any. With openOrCreateDatabase(), we will make a call to open an existing database or create one if the database is nonexistent. We will execute standard database calls to create a table with columns a and b and insert values in a row.

    现在我们将执行一个简单的查询,将这些值提取回runQuery()方法。您会注意到,除了加载库之外,我们使用的所有核心方法都非常标准,那么主要的变化在哪里呢?转到代码包中的Ch4-PersonalContactManager示例,注意我们使用的包:

    java import android.database.Cursor; import android.database.sqlite.SQLiteDatabase;

    我们有 SQLCipher 包:

    java import net.sqlcipher.Cursor; import net.sqlcipher.database.SQLiteDatabase;

实现简单,熟悉,容易实现。如果你把数据库拉出来,试着去读,你会发现错误信息,就像我们前面截图中显示的。用户会发现没有变化,甚至我们的应用的逻辑也保持不变。在截图中,您可以看到我们刚刚构建的加密数据库的应用屏幕:

Encrypting critical data

OAuth 是一个开放的授权标准。它代表资源所有者向客户端应用提供对服务器资源的安全委托访问。它为资源所有者指定了一个流程,授权第三方访问他们的服务器资源,而无需共享他们的凭据,如维基百科中所解释的那样;在 http://oauth.net/2/阅读更多关于 OAuth 的信息。

一般提示和库

我们将介绍一些一般的和不太一般的变通方法和实践,根据具体情况可以很好地加以利用。例如,在某些情况下,我们需要一个预先填充的值数据库,我们将在我们的安卓应用中使用或升级数据库,这看起来微不足道,但可能会破坏我们的应用。

升级数据库

第二章连接点中,我们使用onUpgrade()来展示数据库是如何更新的。如果我们回到例子,你会注意到它执行了一个Drop Table命令。这里将会发生的是,原来的表将会被丢弃,而一个新的表将会被调用onCreate()创建。这将导致现有数据的丢失,因此如果我们需要更改数据库,这是不合适的。onUpgrade()功能可以定义如下:

public void onUpgrade(SQLiteDatabase db, int oldVersion,int newVersion)
{
  String DROP_TABLE = "DROP TABLE IF EXISTS " + TABLE_NAME;
  db.execSQL(DROP_TABLE);
  onCreate(db);
}

另一个挑战是确定我们在这里使用的版本。用户可能正在运行应用的旧版本,因此我们必须记住应用的不同版本,以及这些版本是否会对数据库带来任何变化。对于新用户,我们不用担心,因为如果数据库不存在,就会调用onCreate()

为了确保我们有一个适当的升级,我们将使用我们的CustomSQLiteOpenHelper类中的DB_VERSION常量来告诉我们的onUpgrade()方法要采取的行动:

private static final int DB_VERSION = 1;

我们将DB_VERSION常量更改为3以反映升级:

private static final int DB_VERSION = 3;

构造函数将处理其余部分:

public CustomSQLiteOpenHelper(Context context) 
{
  super(context, DB_NAME, null, DB_VERSION);
}  

当超级类构造函数运行时,它将存储的 SQLite .db文件的DB_VERSION常数与我们作为参数传递的DB_VERSION进行比较,如果需要,调用onUpgrade()方法:

public void onUpgrade(SQLiteDatabase db, int oldVersion,int newVersion)
{
switch(oldVersion) {
   case 1: db.execSQL(DATABASE_CREATE_MAIN_TABLE);
   case 2: db.execSQL(DATABASE_CREATE_MAIN_TABLE);
   case 3: db.execSQL(DATABASE_CREATE_DEL_TABLE);
   }
}

在我们的onUpgrade()方法中,我们有一个开关盒来进行更改。请注意,我们不使用break语句,因为用户可以使用旧版本,并且可能没有更新应用,如前所述。例如,让我们考虑一个用户正在运行一个特定版本的应用DB_VERSION =1,他或她跳过了包含DB_VERSION =2的下一个更新,最终,一个新版本的应用DB_VERSION =3被发布。现在,我们有一个案例,用户仍然使用旧版本的应用,并且没有安装我们发布的新更新。所以,在这种情况下,当用户安装应用时,onUpgrade()方法会先执行case 1再转到case 2安装用户错过的更新;最后,用户将安装第三个版本的更新,确保所有数据库更改都得到反映。注意没有break语句。这是因为我们要运行switch语句获得值1的所有情况,以及 switch case 获得值2的最后两个语句。

或者,我们也可以使用if语句。这也将按照我们的意图进行,因为我们的测试DB_VERSION常数是1,它将满足条件并反映变化:

if (oldVersion<2) {db.execSQL(DATABASE_CREATE_MORE_TABLE); } 
if (oldVersion<3) {db.execSQL(DATABASE_CREATE_DEL_TABLE); }

数据库减去 SQL 语句

在本书的大部分内容中,我们四处寻找安卓和 SQLite 的角落。对一些人来说,写 T2 SQL 语句只是办公室里的另一天,而对一些人来说,这就像坐过山车一样。本节将介绍一个库,它使我们能够保存和检索 SQLite 数据库记录,而无需编写一条 SQL 语句。主动安卓 是安卓的主动记录式 SQLite 持久化。根据文档,每个数据库记录都用save()delete()等方法整齐地包装成一个类。我们将使用活动安卓文档中的示例,并基于它构建一个工作示例。让我们看看启动和运行它所需的步骤。

看一下官方网站http://www.activeandroid.com/,了解一下概况,从http://goo.gl/oW2kod下载文件。

下载文件后,在根文件夹上运行ant构建 JAR 文件。一旦你运行了ant,你会在dist文件夹中找到你的 JAR 文件。在 Eclipse 中,新建一个项目,将 JAR 文件添加到项目的libs文件夹中,然后将 JAR 文件添加到项目属性中的 Java 构建路径中。

主动安卓通过执行以下步骤来查找配置的一些全局设置:

  1. 我们将从创建一个类开始,扩展应用类:

    ```java public class MyApplication extends com.activeandroid.app.Application { @Override public void onCreate() { super.onCreate(); ActiveAndroid.initialize(this); }

    @Override public void onTerminate() { super.onTerminate(); ActiveAndroid.dispose(); }

    } ```

  2. 现在我们将这个应用类添加到我们的清单文件中,并添加对应于我们的应用的元数据:

    java <application android:name="com.active.android.MyApplication"> <meta-data android:name="AA_DB_NAME" android:value="test.db" /> <meta-data android:name="AA_DB_VERSION" android:value="1" /> ……….. </application>

  3. With this basic setup complete, we will now proceed on to creating our data model. The ActiveAndroid library supports annotation and we will use it in the following model classes:

    ```java // Category class

    @Table(name = "Categories") public class Category extends Model { @Column(name = "Name") public String name; }

    // Item class

    @Table(name = "Items") public class Item extends Model { // If name is omitted, then the field name is used. @Column(name = "Name") public String name;

    @Column(name = "Category") public Category category;

    public Item() { super(); }

    public Item(String name, Category category) { super(); this.name = name; this.category = category;
    } } ```

    如果您想探索注释并在项目中使用它们,并减少样板代码,您可以查看以下安卓库:安卓注释、Square 的匕首和黄油刀。

  4. To add a new category or item, we need to make a call to save(). In the code segment, we can see that an item object is created and associated with a particular category, and in the end, save() is called:

    java public void insert(View v) { Item testItem = new Item(); testItem.category = testCategory; testItem.name = editTextItem.getText().toString(); testItem.save(); }

    要删除一个项目,我们可以调用item.delete()。同样,为了获取值,我们也有相关的方法。以下是获取特定类别的所有数据的调用:

    java List<Item>getall = new Select().from(Item.class) .where("Category = ?", testCategory.getId()) .orderBy("Name ASC").execute();

在主动安卓中还有很多有待探索的地方。它们有模式迁移和类型序列化;除此之外,您可以通过将数据库放在asset文件夹中来运送预填充的数据库,并且您还可以使用内容提供商。简而言之,它是一个构建良好的库,适合于寻找与数据库通信和执行数据库操作的间接方法的人。它有助于以熟悉的 Java 方法形式访问数据库,而不是准备 SQL 语句来执行相同的操作。完整的示例代码捆绑在chapter 4代码包中。

使用预先填充的数据库发货

我们将建立一个数据库并将其放入我们的asset文件夹中,这是一个只读目录。在运行时,我们将检查数据库是否存在。如果没有,我们将把数据库从asset文件夹复制到/data/data/yourpackage/databases。在第二章连接点中,我们使用了一个叫做 SQLite 管理器的工具;看看这一章的第三张截图。我们现在将使用相同的工具来构建我们的数据库。如果您按照该部分中的说明提取数据库,或者查看该屏幕截图,您会注意到除了数据库表之外还有几个表:

Shipping with a prepopulated database

创建预填充的数据库的步骤如下:

  1. 为了建立一个预填充的数据库,我们需要创建一个名为android_metadata的表,该表与我们需要的表不同。使用 SQLite 管理器工具,我们将创建一个名为contact的新数据库,然后我们将创建android_metdata表:

    java CREATE TABLE "android_metadata"("locale" TEXT DEFAULT 'en_US')

  2. 我们将在表格中插入一行:

    java INSERT INTO "android_metadata" VALUES ('en_US')

  3. Now we will create the tables we require, in our case, contact_table using the SQL query we used in Chapter 2, Connecting the Dots. In the DatabaseManager class, we will just replace the constants with the actual values:

    java CREATE TABLE "contact_table" ("_id" integer primary key autoincrement not null,"contact_name" text not null,"contact_number" text not null,"contact_email" text not null,"photo_id" BLOB )

    如果还没有定义的话,需要将我们表的主标识字段重命名为_id。这有助于安卓识别在哪里绑定我们的表的标识字段。

  4. 让我们填充几行数据。我们可以通过运行Insert查询或使用工具手动输入值来实现。现在,将数据库文件复制到asset文件夹中。

  5. 现在,在我们原来的个人联系人管理器中,我们将修改我们的DatabaseManager类。好的一面是,这是我们唯一需要修改的类,系统的其余部分将按预期工作。
  6. 当应用运行并通过传递上下文创建一个新的DatabaseManager类时,我们将调用createDatabase(),首先检查数据库是否已经存在:

    java Private Boolean checkDataBase() { SQLiteDatabase checkDB = null; try { String myPath = DB_PATH + DB_NAME; checkDB = SQLiteDatabase.openDatabase(myPath, null, SQLiteDatabase.OPEN_READONLY); } catch (SQLiteException e) { // database doesn't exist yet. } if (checkDB != null) { checkDB.close(); } return checkDB != null ? true : false; }

  7. 如果没有,我们将创建一个空数据库,用我们的数据库替换,我们将复制到我们的asset文件夹中。从asset文件夹复制数据库后,我们将创建一个新的SQLiteDatabase对象:

    ```java private void copyDataBase() throws IOException { InputStream myInput = myContext.getAssets().open(DB_NAME); String outFileName = DB_PATH + DB_NAME; OutputStream myOutput = new FileOutputStream(outFileName); byte[] buffer = new byte[1024]; int length; while ((length = myInput.read(buffer)) > 0) { myOutput.write(buffer, 0, length); }

    myOutput.flush(); myOutput.close(); myInput.close(); } ```

另一点需要注意的是,我们的CustomSQLiteOpenHelper类的onCreate()方法将是空的,因为我们不是在创建数据库和表,而是在复制一个。示例代码捆绑在chapter 4代码包中。如果这个过程看起来繁琐,不用担心;安卓开发者社区为您提供了一个解决方案。SQLiteAssetHelper 是一个安卓库,它将使用应用的原始素材文件来帮助您管理数据库创建和版本管理。

要实现这一点,我们必须遵循几个简单的步骤:

  1. 将 JAR 文件复制到我们项目的libs文件夹中。
  2. 将库添加到 Java 构建路径。
  3. 将我们的压缩数据库文件复制到projectimg/databases/your_database.db.zipasset文件夹中。
  4. ZIP 文件应该只包含一个db文件。
  5. 我们将扩展SQLiteAssetHelper类,而不是扩展框架的SQLiteOpenHelper类。
  6. 他们还为你提供升级数据库文件的帮助,需要放在img/databases/<database_name>_upgrade_<from_version>-<to_version>.sql中。
  7. 图书馆、文件及其相应的样本可在http://goo.gl/8XSSmR找到。

总结

在本章中,我们讨论了大量高级主题,从加载器到数据安全。我们实现了游标加载器来理解加载器是如何为我们的应用工作的,我们深入研究了保护我们的数据库和理解权限的概念,同时将我们的内容提供者暴露给其他应用。我们还介绍了一些技巧,例如使用预填充的数据库发货、升级数据库而不破坏系统,以及使用数据库查询而不使用 SQL 命令。这绝不是我们用数据库和安卓能实现的唯一一套东西。这一章仅仅是向广阔的编程可能性迈进。