Content Provider总结

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自定义权限与使用

学习过程中的问题及答案

  1. ContentProvider 的query返回一个Cursor用于遍历数据,但是外部应该通过怎样的信息来遍历呢?

我们设计ContentProvider是为了给外部使用的,那外部要想知道我们的数据组织方式,一个是通过文档知道表名称、列名称、列的结构、值类型,以及每种数据对应的URI,直接通过给的字符串来拼接URI,然后基于文档说明来解析、存储数据和类型。一个是通过我们提供的SDK,根据SDK中的模型来组织URI,比如MediaStore这种。

  1. Cursor有根据index获取对应Column的数据接口,可以映射成不同的值类型,并且可以根据Column名称获取index,但是怎么知道这个Column的值类型是什么呢?除了外部文档这种弱信息以外,有没有强约束的方式?

Cusor提供了getType接口,返回Cursor类中标识的常量,比如FIELD_TYPE_BLOB表示二进制块数据。

  1. 根据URI获取MIME类型,这个URI可以是表?列?值?,这个MIME对外部而言,可以起到怎样的参考作用?

MIME类型是最初是用于邮件附件的文件类型识别的,后来被广泛地用于互联网数据交互的内容类型标识。它有一整套自己的约定,由标准机构来负责维护,其他软件厂商负责支持。但是由于网络发展太快,而机构维护太慢,导致这个约定中存在大量的事实标准,一般以x-类型来表示。实际上,MIME只是一个内容类型的指导,由交互双方共同遵守。因此,Android在ContentProvider中引用的MIME类型,不是原有的任何类型,而是自己扩展出来的。它的父类型有两种,一个是vnd.android.dirs,表示目录,一个是vnd.android.item,表示一条记录。vnd是vendor specified 厂商自定义的意思。它的子类型,由具体的Provider自行约定,只要能说明这是一个什么内容即可。所以对于表、列、值,你都可以提供或者不提供MIME类型,取决于你的需求和设计。

  1. 如果我共享的数据,是我自己个的,那么比如我提供了图片的路径,但是图片访问不了,提供MIME类型也无济于事。也就是说只能提供基础类型的数据,那还要MIME类型干嘛?参考MediaProvider的Data字段来解答。

有些情况下,以ContentProvider提供数据,是提供一种数据组织和检索服务,这些数据不一定就是私有的,比如MediaProvider,就提供了所有内外存中的多媒体文件维护服务。

那么,假如我们获取的字段,比如MediaStore.Audio.Media.DATA,它表示音频文件的绝对路径,指向了一个其他应用的私有路径。这时候,我们通过文件系统是无法直接访问的,要通过ContentResolver的openFileDiscriptor来访问,它会回调到ContentProvider openFile接口,并返回一个ParcelFileDescriptor对象,其底层是一个跨进程的pipe或者socket文件,用于对目标文件的读写操作。

因为ContentResolver对Uri的请求,是封装了Android权限机制的,所以可以满足隐私数据的安全需求。并且,如果你不想提供相关的数据,就不应该实现对应的Provider接口。类似一些系统的Provider,也提供了隐藏自己相关数据的不被扫描的方法。当然最好是放在自己的私有目录中。

  1. 我们通过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对象来查询。

  1. Android SQLite的使用,raw查询和QueryBuilder的区别和优劣,使用场景推荐?

  2. 如果我们不使用数据库作为存储工具,怎么使用Provider来对外暴露数据呢?

ContentProvider使用URI来标识数据,并要求返回Cursor对象用来遍历数据,使用数据库是最贴近设计的存储模式,如果不使用的话,也可以自己组织数据,并初始化到Curosr对象返回给客户端即可。比如可以使用MartrixCursor,也可以使用其它的。

  1. 在问题7中,Android提供了哪些Cursor,分别对应怎样的需求,有怎样的设计和利弊?

你可能感兴趣的:(Content Provider总结)