第一点是我们在ArticlesProvider类的内部中定义了一个DBHelper类,它继承于SQLiteOpenHelper类,它用是用辅助我们操作数据库的。使用这个DBHelper类来辅助操作数据库的好处是只有当我们第一次对数据库时行操作时,系统才会执行打开数据库文件的操作。拿我们这个例子来说,只有第三方应用程序第一次调用query、insert、update或者delete函数来操作数据库时,我们才会真正去打开相应的数据库文件。这样在onCreate函数里,就不用执行打开数据库的操作,因为这是一个耗时的操作,而在onCreate函数中,要避免执行这些耗时的操作。
第二点是设置URI匹配规则。因为我们是根据URI来操作数据库的,不同的URI对应不同的操作,所以我们首先要定义好URI匹配规则,这样,当我们获得一个URI时,就能快速地判断出要如何去操作数据库。设置URI匹配规则的代码如下所示:
1. private static final UriMatcher uriMatcher;
2. static {
3. new UriMatcher(UriMatcher.NO_MATCH);
4. "item", Articles.ITEM);
5. "item/#", Articles.ITEM_ID);
6. "pos/#", Articles.ITEM_POS);
7. }
在创建UriMatcher对象uriMatcher时,我们传给构造函数的参数为UriMatcher.NO_MATCH,它表示当uriMatcher不能匹配指定的URI时,就返回代码UriMatcher.NO_MATCH。接下来增加了三个匹配规则,分别是content://shy.luo.providers.articles/item、content://shy.luo.providers.articles/item/#和content://shy.luo.providers.articles/pos/#,它们的匹配码分别是Articles.ITEM、Articles.ITEM_ID和Articles.ITEM_POS,其中,符号#表示匹配任何数字。
第三点是SQLiteQueryBuilder的使用。在query函数中,我们使用SQLiteQueryBuilder来辅助数据库查询操作,使用这个类的好处是我们可以不把数据库表的字段暴露出来,而是提供别名给第三方应用程序使用,这样就可以把数据库表内部设计隐藏起来,方便后续扩展和维护。列别名到真实列名的映射是由下面这个HashMap成员变量来实现的:
1. private static final HashMap<String, String> articleProjectionMap;
2. static {
3. new HashMap<String, String>();
4. articleProjectionMap.put(Articles.ID, Articles.ID);
5. articleProjectionMap.put(Articles.TITLE, Articles.TITLE);
6. articleProjectionMap.put(Articles.ABSTRACT, Articles.ABSTRACT);
7. articleProjectionMap.put(Articles.URL, Articles.URL);
8. }
在上面的put函数中,第一个参数表示列的别名,第二个参数表示列的真实名称。在这个例子中,我们把列的别名和和真实名称都设置成一样的。
第四点是数据更新机制的使用。执行insert、update和delete三个函数时,都会导致数据库中的数据发生变化,所以这时候要通过调用ContentResolver接口的notifyChange函数来通知那些注册了监控特定URI的ContentObserver对象,使得它们可以相应地执行一些处理,例如更新数据在界面上的显示。在query函数中,最终返回给调用者的是一个Cursor,调用者获得这个Cursor以后,可以通过它的deleteRow或者commitUpdates来执行一些更新数据库的操作,这时候也要通知那些注册了相应的URI的ContentObserver来作相应的处理,因此,这里在返回Cursor之前,要通过Cursor类的setNotificationUri函数来把当前上下文的ContentResolver对象保存到Curosr里面去,以便当通过这个Cursor来改变数据库中的数据时,可以通知相应的ContentObserver来处理。不过这种用法已经过时了,即不建议通过这个Cursor来改变数据库的数据,要把Cursor中的数据看作是只读数据。这里调用Cursor类的setNotificationUri函数还有另外一个作用,我们注意到它的第二个参数uri,对应的是Cursor中的内容,当把这个uri传给Cursor时,Cursor就会注册自己的ContentObserver来监控这个uri对应的数据的变化。一旦这个uri对应的数据发生变化,这个Cursor对应的数据就不是再新的了,这时候就需要采取一些操作来更新内容了。
第五点我们实现了ContentProvider的call函数。这个函数是一个未公开的函数,第三方应用程序只有Android源代码环境下开发,才能使用这个函数。设计这个函数的目的是什么呢?我们知道,当我们需要从Content Provider中获得数据时,一般都是要通过调用它的query函数来获得的,而这个函数将数据放在Cursor中来返回给调用者。以前面一篇文章 Android应用程序组件Content Provider简要介绍和学习计划 中,我们提到,Content Provider传给第三方应用程序的数据,是通过匿名共享内存来传输的。当要传输的数据量大的时候,使用匿名共享内存来传输数据是有好处的,它可以减入数据的拷贝,提高传输效率。但是,当要传输的数据量小时,使用匿名共享内存来作为媒介就有点用牛刀来杀鸡的味道了,因为匿名共享内存并不是免费的午餐,系统创建和匿名共享内存也是有开销的。因此,Content Provider提供了call函数来让第三方应用程序来获取一些自定义数据,这些数据一般都比较小,例如,只是传输一个整数,这样就可以用较小的代价来达到相同的数据传输的目的。
至此,ArticlesProvider的源代码就分析完了,下面我们还要在AndroidManifest.xml文件中配置这个ArticlesProvider类才能正常使用。AndroidManifest.xml文件的内容如下所示:
1. <manifest xmlns:android="http://schemas.android.com/apk/res/android"
2. package="shy.luo.providers.articles">
3. <application android:process="shy.luo.process.article"
4. android:label="@string/app_label"
5. android:icon="@drawable/app_icon">
6. <provider android:name="ArticlesProvider"
7. android:authorities="shy.luo.providers.articles"
8. android:label="@string/provider_label"
9. android:multiprocess="false">
10. </provider>
11. </application>
12.
1. </
manifest
>
在配置Content Provider的时候,最重要的就是要指定它的authorities属性了,只有配置了这个属性,第三方应用程序才能通过它来找到这个Content Provider。这要需要注意的,这里配置的authorities属性的值是和我们前面在Articles.java文件中定义的AUTHORITY常量的值是一致的。另外一个属性multiprocess是一个布尔值,它表示这个Content Provider是否可以在每个客户进程中创建一个实例,这样做的目的是为了减少进程间通信的开销。这里我们为了减少不必要的内存开销,把属性multiprocess的值设置为false,使得系统只能有一个Content Provider实例存在,它运行在自己的进程中。在这个配置文件里面,我们还可以设置这个Content Provider的访问权限,这里我们为了简单起见,就不设置权限了。有关Content Provider的访问权限的设置,可以参考官方文档 http://developer.android.com/guide/topics/manifest/provider-element.html 。
这个应用程序使用到的字符串资源定义在res/values/strings.xml文件中,它的内容如下所示:
1. <?xml version="1.0" encoding="utf-8"?>
2. <resources>
3. <string name="app_label">Articles Storage</string>
4. <string name="provider_label">Articles</string>
5.
1. </
resources
>
由于Content Provider类型的应用程序是没有用户界面的,因此,我们不需要在res/layout目录下为程序准备界面配置文件。
程序的编译脚本Android.mk的内容如下所示:
1. LOCAL_PATH:= $(call my-dir)
2. include $(CLEAR_VARS)
3.
4. LOCAL_MODULE_TAGS := optional
5.
6. LOCAL_SRC_FILES := $(call all-subdir-java-files)
7.
8. LOCAL_PACKAGE_NAME := ArticlesProvider
9.
10. include $(BUILD_PACKAGE)
下面我们就可以参照 如何单独编译Android源代码中的模块 一文来编译和打包这个应用程序了:
1. USER-NAME@MACHINE-NAME:~/Android$ mmm packages/experimental/ArticlesProvider
2. USER-NAME@MACHINE-NAME:~/Android$ make snod
这样,打包好的Android系统镜像文件system.img就包含我们这里所创建的ArticlesProvider应用程序了。
前面说过,在Articles.java文件中定义的常量是要给第三方应用程序使用的,那么我们是不是直接把这个源文件交给第三方呢?这样就显得太不专业了,第三方拿到这个文件后,还必须要放在shy/luo/providers/articles目录下或者要把这个Articles类所在的package改掉才能正常使用。正确的做法是把编译好的Articles.java文件打包成一个jar文件交给第三方使用。编译ArticlesProvider这个应用程序成功后,生成的中间文件放在out/target/common/obj/APPS/ArticlesProvider_intermediates目录下,我们进入到这个目录中,然后执后下面的命令把Articles.class文件提取出来:
1. USER-NAME@MACHINE-NAME:~/Android/out/target/common/obj/APPS/ArticlesProvider_intermediates$ jar -xvf classes.jar shy/luo/providers/articles/Articles.class
然后再单独打包这个Articles.class文件:
1. USER-NAME@MACHINE-NAME:~/Android/out/target/common/obj/APPS/ArticlesProvider_intermediates$ jar -cvf ArticlesProvider.jar ./shy
这样,我们得到的ArticlesProvider.jar文件就包含了Articles.java这个文件中定义的常量了,第三方拿到这个文件后,就可以开发自己的应用程序来访问我们在ArticlesProvider这个Content Provider中保存的数据了。接下来我们就介绍调用这个ArticlesProvider来获取数据的第三方应用程序Article。 2. Article应用程序的实现
首先是参照前面的ArticlesProvider工程,在packages/experimental目录下建立工程文件目录Article。这个应用程序的作用是用来管理ArticlesProvider应用程序中保存的文章信息的,因此,它需要获得相应的Content Provider接口来访问ArticlesProvider中的数据。我们首先在工程目录Article下面创建一个libs目录,把上面得到的ArticlesProvider.jar放在libs目录下,后面我们在编译脚本的时候,再把它引用到工程上来。下面我们就开始分析这个应用程序的实现。
这个应用程序的主界面MainActivity包含了一个ListView控件,用来显示从ArticlesProvider中得到的文章信息条目,在这个主界面上,可以浏览、增加、删除和更新文章信息。当需要增加、删除或者更新文章信息时,就会跳到另外一个界面ArticleActivity中去执行具体的操作。为了方便开发,我们把每一个文章信息条目封装成了一个Article类,并且把与ArticlesProvider进交互的操作都通过ArticlesAdapter类来实现。下面介绍每一个类的具本实现。
下面是Article类的实现,它实现在src/shy/luo/Article.java文件中:
1. package
2.
3. public class
4. private int
5. private
6. private
7. private
8.
9. public Article(int
10. this.id = id;
11. this.title = title;
12. this.abs = abs;
13. this.url = url;
14. }
15.
16. public void setId(int
17. this.id = id;
18. }
19.
20. public int
21. return this.id;
22. }
23.
24. public void
25. this.title = title;
26. }
27.
28. public
29. return this.title;
30. }
31.
32. public void
33. this.abs = abs;
34. }
35.
36. public
37. return this.abs;
38. }
39.
40. public void
41. this.url = url;
42. }
43.
44. public
45. return this.url;
46. }
47. }