Content Provider总结
什么是ContentProvider
作为Android四大组件之一的ContentProvider,其设计的目的是为了为了提供夸应用的数据共享解决方案。
Android的安全机制中的访问控制,采用了Linux的DAC机制,将安装在设备上的应用,视作是Android系统的一个用户,除此之外还有root、system、sdcard等用户来托管系统的敏感数据。在5.1版本以后,还引入了SeLinux的MAC机制,但依然只用于限制系统资源的访问。
系统在应用安装的时候系统会为它分配一个唯一的UID。应用程序有自己的包名和签名,通过包名来标识身份,通过签名来验证身份,相同的签名属于同一个用户组(推测)。应用在持久化数据的时候,可以选择数据的访问权限,比如SharedPreference的私有和共享类型。但是有一些路径,比如数据库和缓存目录是强制私有的。这样可以防止应用,通过文件系统的访问,而获取到其他应用的私有数据。
而在实际使用当中,往往会遇到应用间共享数据的场景,比如系统设置的共享,联系人数据的共享。Android系统设计的主旨就是组件化,使得应用间可以互相调用组件,实现功能的复用。那么对于数据共享的需求,就诞生了ContentProvider这个组件。
数据共享的问题
所有的数据交互,都会有一些共同的问题:
- 数据交互通道
- 数据传输格式
- 数据类型的转换和还原
- 数据结构的转换和还原
我们用HTTP协议来举例,说明它是如何解决这些问题的。
HTTP的数据交互实现
- 数据交互通道
HTTP协议是网络传输协议,它的数据交互通道即是网络。当然,我们知道现行的网络结构是分层的,不同的层级有自己的一个或者一组协议负责实现对应的功能,为上层提供数据通道。对于HTTP而言,它的直接数据通道即是TCP协议通道。
- 数据传输格式
TCP提供的数据通道,是一个二进制的数据传输通道,实际上这是由IP协议来实现的,TCP协议只是负责建立和维护可靠连接。
- 数据类型的转换和还原
HTTP是文本传输协议,因此它的数据类型转换和还原,即字符集的二进制编解码。HTTP报文,使用的是ASCII编码,为了支持其他字符的传输,它允许在头部指明实际使用的字符编码格式。因此存在其他字符集对ASCII的双向转换,也叫编、解码。
- 数据结构的转换和还原
HTTP数据结构,即HTTP报文的数据结构,也叫做HTTP协议的格式。它包括请求头、请求体、响应头、响应体等定义,由客户端和服务器的软件进行解析,比如浏览器、Apache等。其中,请求头的格式固定,而请求体只作为数据容器,具体结构由通信双方自行协商。
这样,HTTP就定义了一个完整的数据交互方式,可以由其他软件广泛的实现和使用了。
ContentProvider的数据交互模式
相对于HTTP协议来说,ContentProvider的数据交互场景是大不相同的。
它不需要在网络上传输,而是在同一台机器上进行数据交换,实际上,它是使用Android 特有的进程通信机制Binder来实现的。它的数据传输,实际上就是内存拷贝。由于内存到数据的转换,都是由JVM实现的,它的数据类型还原,就是内存数据到Java对象的转换,不需要额外的控制信息来解决差异性问题(协议版本、字符集、内容类型等等),因此在数据类型的支持上更加地简单、直接和丰富(http只有文本类型)。
介于这种数据交换的灵活性,ContentProvider并不需要复杂的控制协议,因此在“头部信息”中,只使用了URI作为数据表示。但是在数据部分,却做了强制要求。它参考关系数据库的设计,使用数据表来组织数据,并且要求实现者提供基于URI的CRUD和类型查询接口。具体来说,它的各部分实现是这样的:
- 数据交互通道
Binder驱动,内存拷贝
- 数据传输格式
内存二进制数据
- 数据类型的转换和还原
内存数据到数据类型的转换
- 数据结构的转换和还原
表结构的使用
接下来,我们通过一个demo来体验一把。
动手实现
Demo设计
我们实现一个叫做User的数据结构,并通过Users表来进行数据共享。
User的结构如下
字段 | 类型 | 约束 | 说明 | 选项 | 默认值 |
---|---|---|---|---|---|
id | int | NoneNull | id | ||
name | string | NoneNull | 名字 | ||
male | boolean | NoneNull | 性别 | false,女,true男 | false |
phone | string | 手机号码 |
这部分略,可以参考:
Android:关于ContentProvider的知识都在这里了!
Android自定义权限与使用
学习过程中的问题及答案
- ContentProvider 的query返回一个Cursor用于遍历数据,但是外部应该通过怎样的信息来遍历呢?
我们设计ContentProvider是为了给外部使用的,那外部要想知道我们的数据组织方式,一个是通过文档知道表名称、列名称、列的结构、值类型,以及每种数据对应的URI,直接通过给的字符串来拼接URI,然后基于文档说明来解析、存储数据和类型。一个是通过我们提供的SDK,根据SDK中的模型来组织URI,比如MediaStore这种。
- Cursor有根据index获取对应Column的数据接口,可以映射成不同的值类型,并且可以根据Column名称获取index,但是怎么知道这个Column的值类型是什么呢?除了外部文档这种弱信息以外,有没有强约束的方式?
Cusor提供了getType接口,返回Cursor类中标识的常量,比如FIELD_TYPE_BLOB表示二进制块数据。
- 根据URI获取MIME类型,这个URI可以是表?列?值?,这个MIME对外部而言,可以起到怎样的参考作用?
MIME类型是最初是用于邮件附件的文件类型识别的,后来被广泛地用于互联网数据交互的内容类型标识。它有一整套自己的约定,由标准机构来负责维护,其他软件厂商负责支持。但是由于网络发展太快,而机构维护太慢,导致这个约定中存在大量的事实标准,一般以x-类型来表示。实际上,MIME只是一个内容类型的指导,由交互双方共同遵守。因此,Android在ContentProvider中引用的MIME类型,不是原有的任何类型,而是自己扩展出来的。它的父类型有两种,一个是vnd.android.dirs,表示目录,一个是vnd.android.item,表示一条记录。vnd是vendor specified 厂商自定义的意思。它的子类型,由具体的Provider自行约定,只要能说明这是一个什么内容即可。所以对于表、列、值,你都可以提供或者不提供MIME类型,取决于你的需求和设计。
- 如果我共享的数据,是我自己个的,那么比如我提供了图片的路径,但是图片访问不了,提供MIME类型也无济于事。也就是说只能提供基础类型的数据,那还要MIME类型干嘛?参考MediaProvider的Data字段来解答。
有些情况下,以ContentProvider提供数据,是提供一种数据组织和检索服务,这些数据不一定就是私有的,比如MediaProvider,就提供了所有内外存中的多媒体文件维护服务。
那么,假如我们获取的字段,比如MediaStore.Audio.Media.DATA,它表示音频文件的绝对路径,指向了一个其他应用的私有路径。这时候,我们通过文件系统是无法直接访问的,要通过ContentResolver的openFileDiscriptor来访问,它会回调到ContentProvider openFile接口,并返回一个ParcelFileDescriptor对象,其底层是一个跨进程的pipe或者socket文件,用于对目标文件的读写操作。
因为ContentResolver对Uri的请求,是封装了Android权限机制的,所以可以满足隐私数据的安全需求。并且,如果你不想提供相关的数据,就不应该实现对应的Provider接口。类似一些系统的Provider,也提供了隐藏自己相关数据的不被扫描的方法。当然最好是放在自己的私有目录中。
- 我们通过URI来对外暴露数据,并要求相应的权限申请,通常在存储端是采用数据库来实现的。那么,怎样转换URI到数据库的查询呢,如何组织才比较合理?
在问题3中,我们提到了MIME类型,Android提供了两种大类,目录和数据项。所以在URI上,也只需要提供两种类型的匹配即可。目录对应到数据表,而数据项对应到记录,在ContentProvider中是根据BaseColuem的ID来查询的,要求用于Provider的数据表,一定要有ID这列。
同时Android为我们设计一个一个URIMatcher类,方便我们进行URI匹配。它可以根据初始化设置,匹配URI并转成匹配码,并且支持字符串和数字的统配。我们可以参考下MdediaProvider里的实现:
URI_MATCHER.addURI("media", "*/images/media", IMAGES_MEDIA);
URI_MATCHER.addURI("media", "*/images/media/#", IMAGES_MEDIA_ID);
URI_MATCHER.addURI("media", "*/images/thumbnails", IMAGES_THUMBNAILS);
URI_MATCHER.addURI("media", "*/images/thumbnails/#", IMAGES_THUMBNAILS_ID);
以上是它的初始化代码,"media"是authority, * 代表字符通配, # 代表数字通配。具体的可以看UriMatcher的addURI方法实现。
其次,在查询参数的传递上,方法调用只为我们提供了几个参数,比如groupby、haviing、limit子句是没有的,但是Uri是可以携带参数的,如果需要,我们可以使用URI暴露对外的能力,并封装到SQLiteQueryBuilder里,传递给SQLiteDatabase对象来查询。
Android SQLite的使用,raw查询和QueryBuilder的区别和优劣,使用场景推荐?
如果我们不使用数据库作为存储工具,怎么使用Provider来对外暴露数据呢?
ContentProvider使用URI来标识数据,并要求返回Cursor对象用来遍历数据,使用数据库是最贴近设计的存储模式,如果不使用的话,也可以自己组织数据,并初始化到Curosr对象返回给客户端即可。比如可以使用MartrixCursor,也可以使用其它的。
- 在问题7中,Android提供了哪些Cursor,分别对应怎样的需求,有怎样的设计和利弊?