因为最近要面试,于是打算整理整理一下Android的基础知识,由于之前本人已经学习过大概的Android基础知识,这里主要讲这四大组件、五大存储、六大布局、网络请求等这些内容,其他一些等有时间再整理,话不多说。
应用组件(官方解释,需科学上网)
应用组件是 Android 应用的基本构建基块。每个组件都是一个不同的点,系统可以通过它进入您的应用。 并非所有组件都是用户的实际入口点,有些组件相互依赖,但每个组件都以独立实体形式存在,并发挥特定作用 — 每个组件都是唯一的构建基块,有助于定义应用的总体行为。
共有四种不同的应用组件类型。每种类型都服务于不同的目的,并且具有定义组件的创建和销毁方式的不同生命周期。
一、什么是ContentProvider(内容提供者)?
从官方的解释中可以看到:
ContentProvider
内容提供程序管理一组共享的应用数据。您可以将数据存储在文件系统、SQLite 数据库、网络上或您的应用可以访问的任何其他永久性存储位置。 其他应用可以通过内容提供程序查询数据,甚至修改数据(如果内容提供程序允许)。 例如,Android 系统可提供管理用户联系人信息的内容提供程序。 因此,任何具有适当权限的应用都可以查询内容提供程序的某一部分(如
ContactsContract.Data
),以读取和写入有关特定人员的信息。内容提供程序也适用于读取和写入您的应用不共享的私有数据。 例如,记事本示例应用使用内容提供程序来保存笔记。
内容提供程序作为
ContentProvider
的子类实现,并且必须实现让其他应用能够执行事务的一组标准 API。 如需了解详细信息,请参阅内容提供程序开发者指南。
参考网站:http://www.cnblogs.com/pxsbest/p/5068482.html https://www.jianshu.com/p/ea8bc4aaf057
Android这个系统和其他的操作系统还不太一样,我们需要记住的是,数据在Android当中是私有的,当然这些数据包括文件数据和数据库数据 以及一些其他类型的数据。那这个时候有读者就会提出问题,难道两个程序之间就没有办法对于数据进行交换?Android这么优秀的系统不会让这种情况发生 的。解决这个问题主要靠ContentProvider。一个Content Provider类实现了一组标准的方法接口,从而能够让其他的应用保存或读取此Content Provider的各种数据类型。也就是说,一个程序可以通过实现一个Content Provider的抽象接口将自己的数据暴露出去。外界根本看不到,也不用看到这个应用暴露的数据在应用当中是如何存储的,或者是用数据库存储还是用文件 存储,还是通过网上获得,这些一切都不重要,重要的是外界可以通过这一套标准及统一的接口和程序里的数据打交道,可以读取程序的数据,也可以删除程序的数 据,当然,中间也会涉及一些权限的问题。
一个程序可以通过实现一个ContentProvider的抽象接口将自己的数据完全暴露出去,而且ContentProviders是以类似数据 库中表的方式将数据暴露,也就是说ContentProvider就像一个“数据库”。那么外界获取其提供的数据,也就应该与从数据库中获取数据的操作基 本一样,只不过是采用URI来表示外界需要访问的“数据库”。
Content Provider提供了一种多应用间数据共享的方式,比如:联系人信息可以被多个应用程序访问。
Content Provider是个实现了一组用于提供其他应用程序存取数据的标准方法的类。 应用程序可以在Content Provider中执行如下操作: 查询数据 修改数据 添加数据 删除数据
标准的Content Provider: Android提供了一些已经在系统中实现的标准Content Provider,比如联系人信息,图片库等等,你可以用这些Content Provider来访问设备上存储的联系人信息,图片等等。
1.定义
ContentProvider即内容提供者,是Android四大组件之一
2.作用
进程之间进行数据交互和共享,即跨进程通信
二、原理(Binder机制 跨进程通信)
1.Binder是什么?
- Binder的中文意思是“粘合剂”,意思是为粘合两个不同的进程
- Binder在不同场景下的定义不同,示意图如下:
接下来将从大角度 ---> 小角度 去分析Binder
- 先从机制、模型的角度去分析整个Binder跨进程通信机制的模型(其中会详细分析模型组成中的Binder驱动)
- 再从源码实现的角度分析Binder在Android中的具体实现
2.知识储备(即看懂原理需要的基础知识)
在讲解Binder前,我们要先了解一些Linux的基础知识:
2.1 进程空间划分
- 一个进程空间分为用户空间和内核空间(Kernel),即把进程内用户和内核隔离开来
- 用户空间和内核空间(Kernel)的区别:
a. 进程间,用户空间的数据不可共享,所以用户空间 = 不可共享空间
b. 进程间,内核空间的数据可以共享,所以内核空间 = 可共享空间(所有进程共用一个内核空间)
- 进程内,用户空间和内核空间(Kernel)进行交互需通过系统调用,主要通过函数:
a. copy_from_user() : 将用户空间的数据拷贝到内核空间
b. copy_to_user() : 将内核空间的数据拷贝到用户空间
示意图:
2.2 进程隔离和跨进程通信(IPC)
- 进程隔离
为了保证安全性和独立性,一个进程不能直接操作或者访问另一个进程,即Android的进程是相互之间互相独立、隔离的。
- 跨进程通信(IPC)
即进程间需进行数据交互、通信
- 跨进程通信的基本原理(示意图)
注:
- Binder的作用则是:连接两个进程,实现了mmap()系统调用,主要负责创建数据接收的缓存空间和管理数据接收缓存。
- 传统的跨进程通信需要拷贝数据2次,但Binder机制只需1次,主要是使用到了内存映射,由于篇幅较大,请看链接。
3.Binder跨进程通信机制模型(机制、模型的角度)
3.1模型原理图(Binder跨进程通信机制模型基于client - server模式)
3.2模型组成角色说明
- Binder驱动作用中的跨进程通信原理
- 跨进程通信核心原理
3.3模型原理步骤说明
额外说明:
特别说明1:client进程、server进程和service manager进程之间的交互都必须通过Binder驱动(使用open和ioctl文件操作函数),而非直接交互。
原因:
1.client进程、server进程和server manager进程属于进程空间的用户空间,不可进行进程间交互。
2.Binder驱动属于进程空间的内核空间,可进行进程之间和进程内交互。
示意图如下(虚线表示并非直接交互)
特别说明2:Binder驱动和service manager进程属于Android基础架构(即系统已经实现好了);而client进程和server进程属于Android应用层(需要开发者自己实现)。所以在进行跨进程通信时,开发者只需自定义client和server进程并显式使用上述三个步骤,最终借助Android的基础架构功能就可以完成进程间的通信。
特别说明3:Binder请求的线程管理
- server进程会创建很多线程来处理Binder请求
- Binder模型的线程管理采用Binder驱动的线程池,并由Binder驱动自身进行管理(而不是由server进程来管理的)
- 一个进程的Binder线程数默认最大是16,超过的请求会被阻塞等待空闲的Binder线程(所以,在进程之间通信时处理并发问题时,如果使用ContentProvider时,他的CRUD(增查改删)方法只能同时有16个线程同时工作)
4.Binder机制在Android中的具体实现原理(源码实现的角度)
- Binder机制在Android中的实现主要依靠Binder类,其实现了IBinder接口
- 实例说明:client进程需要调用server进程的加法函数(将整数a和b相加)即:
a.client进程需要传两个整数给server进程
b.server进程需要把相加后的结果返回给client进程
- 具体步骤:
步骤1:注册服务
- 过程描述:server进程通过Binder驱动向service manager进程
- 代码实现:server进程创建一个Binder对象
a.Binder实体是server进程在Binder驱动中的存在形式
b.该对象保存Server和ServerManager的信息(保存在内核空间中)
c.Binder驱动通过内核空间的BInder实体找到用户空间的Server对象
- 代码分析:
Binder binder = new Stub(); // 步骤1:创建Binder对象 ->>分析1 // 步骤2:创建 IInterface 接口类 的匿名类 // 创建前,需要预先定义 继承了IInterface 接口的接口 -->分析3 IInterface plus = new IPlus(){ // 确定Client进程需要调用的方法 public int add(int a,int b) { return a+b; } // 实现IInterface接口中唯一的方法 public IBinder asBinder(){ return null ; } }; // 步骤3 binder.attachInterface(plus,"add two int"); // 1. 将(add two int,plus)作为(key,value)对存入到Binder对象中的一个Map<String,IInterface>对象中 // 2. 之后,Binder对象 可根据add two int通过queryLocalIInterface()获得对应IInterface对象(即plus)的引用,可依靠该引用完成对请求方法的调用 // 分析完毕,跳出 <-- 分析1:Stub类 --> public class Stub extends Binder { // 继承自Binder类 ->>分析2 // 复写onTransact() @Override boolean onTransact(int code, Parcel data, Parcel reply, int flags){ // 具体逻辑等到步骤3再具体讲解,此处先跳过 switch (code) { case Stub.add: { data.enforceInterface("add two int"); int arg0 = data.readInt(); int arg1 = data.readInt(); int result = this.queryLocalIInterface("add two int") .add( arg0, arg1); reply.writeInt(result); return true; } } return super.onTransact(code, data, reply, flags); } // 回到上面的步骤1,继续看步骤2 <-- 分析2:Binder 类 --> public class Binder implement IBinder{ // Binder机制在Android中的实现主要依靠的是Binder类,其实现了IBinder接口 // IBinder接口:定义了远程操作对象的基本接口,代表了一种跨进程传输的能力 // 系统会为每个实现了IBinder接口的对象提供跨进程传输能力 // 即Binder类对象具备了跨进程传输的能力 void attachInterface(IInterface plus, String descriptor); // 作用: // 1. 将(descriptor,plus)作为(key,value)对存入到Binder对象中的一个Map<String,IInterface>对象中 // 2. 之后,Binder对象 可根据descriptor通过queryLocalIInterface()获得对应IInterface对象(即plus)的引用,可依靠该引用完成对请求方法的调用 IInterface queryLocalInterface(Stringdescriptor) ; // 作用:根据 参数 descriptor 查找相应的IInterface对象(即plus引用) boolean onTransact(int code, Parcel data, Parcel reply, int flags); // 定义:继承自IBinder接口的 // 作用:执行Client进程所请求的目标方法(子类需要复写) // 参数说明: // code:Client进程请求方法标识符。即Server进程根据该标识确定所请求的目标方法 // data:目标方法的参数。(Client进程传进来的,此处就是整数a和b) // reply:目标方法执行后的结果(返回给Client进程) // 注:运行在Server进程的Binder线程池中;当Client进程发起远程请求时,远程请求会要求系统底层执行回调该方法 final class BinderProxy implements IBinder { // 即Server进程创建的Binder对象的代理对象类 // 该类属于Binder的内部类 } // 回到分析1原处 } <-- 分析3:IInterface接口实现类 --> public interface IPlus extends IInterface { // 继承自IInterface接口->>分析4 // 定义需要实现的接口方法,即Client进程需要调用的方法 public int add(int a,int b); // 返回步骤2 } <-- 分析4:IInterface接口类 --> // 进程间通信定义的通用接口 // 通过定义接口,然后再服务端实现接口、客户端调用接口,就可实现跨进程通信。 public interface IInterface { // 只有一个方法:返回当前接口关联的 Binder 对象。 public IBinder asBinder(); } // 回到分析3原处
注册服务后,Binder驱动持有Server进程创建的Binder实体
步骤2:获取服务
- client进程使用某个service前(此处是相加函数),需通过Binder驱动向ServiceManager进程获取相应的Service信息
- 具体代码实现过程如下:
此时client进程与server进程已经建立了连接
步骤3:使用服务
client进程根据获取到的service信息(Binder代理对象),通过Binder驱动建立与该service所在server进程通信的链路,并开始使用服务。
- 过程描述
a.client进程 将参数(整数a和b)发送到server进程
b.server进程 根据client进程要求调用目标方法(即加法函数)
c.server进程 将目标方法的结果(即加法后的结果)返回给client进程
- 代码实现过程
步骤1:client进程将参数(整数a和b)发送到server进程
/ 1. Client进程 将需要传送的数据写入到Parcel对象中 // data = 数据 = 目标方法的参数(Client进程传进来的,此处就是整数a和b) + IInterface接口对象的标识符descriptor android.os.Parcel data = android.os.Parcel.obtain(); data.writeInt(a); data.writeInt(b); data.writeInterfaceToken("add two int");; // 方法对象标识符让Server进程在Binder对象中根据"add two int"通过queryLocalIInterface()查找相应的IInterface对象(即Server创建的plus),Client进程需要调用的相加方法就在该对象中 android.os.Parcel reply = android.os.Parcel.obtain(); // reply:目标方法执行后的结果(此处是相加后的结果) // 2. 通过 调用代理对象的transact() 将 上述数据发送到Binder驱动 binderproxy.transact(Stub.add, data, reply, 0) // 参数说明: // 1. Stub.add:目标方法的标识符(Client进程 和 Server进程 自身约定,可为任意) // 2. data :上述的Parcel对象 // 3. reply:返回结果 // 0:可不管 // 注:在发送数据后,Client进程的该线程会暂时被挂起 // 所以,若Server进程执行的耗时操作,请不要使用主线程,以防止ANR // 3. Binder驱动根据 代理对象 找到对应的真身Binder对象所在的Server 进程(系统自动执行) // 4. Binder驱动把 数据 发送到Server 进程中,并通知Server 进程执行解包(系统自动执行)
步骤2:server进程根据client进程要求调用目标方法(即加法函数)
// 1. 收到Binder驱动通知后,Server 进程通过回调Binder对象onTransact()进行数据解包 & 调用目标方法 public class Stub extends Binder { // 复写onTransact() @Override boolean onTransact(int code, Parcel data, Parcel reply, int flags){ // code即在transact()中约定的目标方法的标识符 switch (code) { case Stub.add: { // a. 解包Parcel中的数据 data.enforceInterface("add two int"); // a1. 解析目标方法对象的标识符 int arg0 = data.readInt(); int arg1 = data.readInt(); // a2. 获得目标方法的参数 // b. 根据"add two int"通过queryLocalIInterface()获取相应的IInterface对象(即Server创建的plus)的引用,通过该对象引用调用方法 int result = this.queryLocalIInterface("add two int") .add( arg0, arg1); // c. 将计算结果写入到reply reply.writeInt(result); return true; } } return super.onTransact(code, data, reply, flags); // 2. 将结算结果返回 到Binder驱动
步骤3:server进程将目标方法的结果(即加法后的结果)返回给client进程
// 1. Binder驱动根据 代理对象 沿原路 将结果返回 并通知Client进程获取返回结果 // 2. 通过代理对象 接收结果(之前被挂起的线程被唤醒) binderproxy.transact(Stub.ADD, data, reply, 0); reply.readException();; result = reply.readInt();
原理图与流程图总结步骤3的内容:
5.优点
对比Linux(Android基于Linux)上的其他进程通信方式(如管道、消息队列、共享内存、信号量、Socket),Binder机制的优点有:
6.总结
三、ContentProvider(内容提供者)的使用
1.统一资源标识符(URI)
- 定义:Uniform Resource Identifier(URI),统一资源标识符
- 作用:唯一标识ContentProvider和其中的数据 (外界进程通过URI找到对应的ContentProvider和其中的数据,再进行数据操作)
- 具体使用:URI分为系统预置和自定义,分别对应系统内置的数据(如通讯录、日程表等等)和自定义数据库
自定义URI示意图:
// 设置URI Uri uri = Uri.parse("content://com.carson.provider/User/1") // 上述URI指向的资源是:名为 `com.carson.provider`的`ContentProvider` 中表名 为`User` 中的 `id`为1的数据 // 特别注意:URI模式存在匹配通配符* & # // *:匹配任意长度的任何有效字符的字符串 // 以下的URI 表示 匹配provider的任何内容 content://com.example.app.provider/* // #:匹配任意长度的数字字符的字符串 // 以下的URI 表示 匹配provider中的table表的所有行 content://com.example.app.provider/table/#
2.MIME数据类型
- 作用指定某个拓展名的文件用某种应用程序来打开(如指定.html文件采用text应用程序打开、指定.pdf文件采用flash应用程序打开)
- 具体使用:
a.ContentProvider根据URI返回MIME类型
ContentProvider.geType(uri) ;
b.MIME类型组成
每种MIME类型由两部分组成 = 类型 + 子类型 (MIME类型是一个包含两部分的字符串)
text / html // 类型 = text、子类型 = html text/css text/xml application/pdf
c.MIME类型形式
MIME类型有2种形式:
// 形式1:单条记录 vnd.android.cursor.item/自定义 // 形式2:多条记录(集合) vnd.android.cursor.dir/自定义 // 注: // 1. vnd:表示父类型和子类型具有非标准的、特定的形式。 // 2. 父类型已固定好(即不能更改),只能区别是单条还是多条记录 // 3. 子类型可自定义
- 实例说明
<-- 单条记录 --> // 单个记录的MIME类型 vnd.android.cursor.item/vnd.yourcompanyname.contenttype // 若一个Uri如下 content://com.example.transportationprovider/trains/122 // 则ContentProvider会通过ContentProvider.geType(url)返回以下MIME类型 vnd.android.cursor.item/vnd.example.rail <-- 多条记录 --> // 多个记录的MIME类型 vnd.android.cursor.dir/vnd.yourcompanyname.contenttype // 若一个Uri如下 content://com.example.transportationprovider/trains // 则ContentProvider会通过ContentProvider.geType(url)返回以下MIME类型 vnd.android.cursor.dir/vnd.example.rail
- 注:在这里解释下getType的作用(很感谢该博客的作者的解释 https://www.cnblogs.com/dajiji/p/6324633.html)
参考网上的信息,getType的作用应该是这样的,以指定的两种方式开头,android可以顺利识别出这是单条数据还是多条数据,比如在上篇博客中,我们的查询结果是一个Cursor,我们可以根据getType方法中传进来的Uri判断出query方法要返回的Cursor中只有一条数据还是有多条数据,这个有什么用呢?如果我们在getType方法中返回一个null或者是返回一个自定义的android不能识别的MIME类型,那么当我们在query方法中返回Cursor的时候,系统要对Cursor进行分析,进而得出结论,知道该Cursor只有一条数据还是有多条数据,但是如果我们按照Google的建议,手动的返回了相应的MIME,那么系统就不会自己去分析了,这样可以提高一丢点的系统性能。
3.ContentProvider类
3.1组织数据方式
- ContentProvider主要以表格的形式组织数据(同时也支持文件数据,只不过是表格形式用得比较多)
- 每个表格中包含多张表,每张表包含行与列,分别对应记录与字段。(同数据库)
3.2主要方法
- 进程之间共享数据的本质是:增删查改数据9
- 所以ContentProvider的核心方法也主要是上述4个作用
<-- 4个核心方法 --> public Uri insert(Uri uri, ContentValues values) // 外部进程向 ContentProvider 中添加数据 public int delete(Uri uri, String selection, String[] selectionArgs) // 外部进程 删除 ContentProvider 中的数据 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) // 外部进程更新 ContentProvider 中的数据 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) // 外部应用 获取 ContentProvider 中的数据 // 注: // 1. 上述4个方法由外部进程回调,并运行在ContentProvider进程的Binder线程池中(不是主线程) // 2. 存在多线程并发访问,需要实现线程同步 // a. 若ContentProvider的数据存储方式是使用SQLite & 一个,则不需要,因为SQLite内部实现好了线程同步,若是多个SQLite则需要,因为SQL对象之间无法进行线程同步 // b. 若ContentProvider的数据存储方式是内存,则需要自己实现线程同步 <-- 2个其他方法 --> public boolean onCreate() // ContentProvider创建后 或 打开系统后其它进程第一次访问该ContentProvider时 由系统进行调用 // 注:运行在ContentProvider进程的主线程,故不能做耗时操作 public String getType(Uri uri) // 得到数据类型,即返回当前 Url 所代表数据的MIME类型
- Android为常见的数据(如通讯录、日程表等)提供了默认内置的ContentProvider
- 但也可以根据需求自定义ContentProvider,但上述6个方法必须重写
- ContentProvider类并不会直接与外部进程交互,而是通过ContentResolver(内容解析器)类
4.ContentResolver类
4.1 作用
统一管理不同ContentProvider之间的操作
- 通过URI即可操作不同的ContentProvider中的数据
- 外部进程通过ContentResolver类来与ContentProvider类进行交互
4.2 为什么要使用通过ContentResolver类来与ContentProvider类进行交互,而不是直接访问ContentProvider类?
- 一般来说,一款应用要使用多个ContentProvider,若需要了解每个ContentProvider的不同实现再完成数据交互,操作成本高而且难度大。
- 所以再ContentProvider类上加多了一个ContentResolver类对所有的ContentProvider进行统一管理。
4.3 具体使用
ContentResolver类提供了与ContentProvider类名字相同和作用相同的4个方法
// 外部进程向 ContentProvider 中添加数据 public Uri insert(Uri uri, ContentValues values) // 外部进程 删除 ContentProvider 中的数据 public int delete(Uri uri, String selection, String[] selectionArgs) // 外部进程更新 ContentProvider 中的数据 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) // 外部应用 获取 ContentProvider 中的数据 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
实例说明
// 使用ContentResolver前,需要先获取ContentResolver // 可通过在所有继承Context的类中 通过调用getContentResolver()来获得ContentResolver ContentResolver resolver = getContentResolver(); // 设置ContentProvider的URI Uri uri = Uri.parse("content://cn.scu.myprovider/user"); // 根据URI 操作 ContentProvider中的数据 // 此处是获取ContentProvider中 user表的所有记录 Cursor cursor = resolver.query(uri, null, null, null, "userid desc");
Android提供了3个用于辅助ContentProvider的工具类:
- ContentUris
- UriMatcher
- ContentObserver
5.ContentProvider的工具类
5.1 ContentUris
- 作用:操作URI
- 具体使用:核心方法有两个(withAppendedId()、parseId())
// withAppendedId()作用:向URI追加一个id Uri uri = Uri.parse("content://cn.scu.myprovider/user") Uri resultUri = ContentUris.withAppendedId(uri, 7); // 最终生成后的Uri为:content://cn.scu.myprovider/user/7 // parseId()作用:从URL中获取ID Uri uri = Uri.parse("content://cn.scu.myprovider/user/7") long personid = ContentUris.parseId(uri); //获取的结果为:7
5.2 UriMatcher
- 作用:
- 在ContentProvider中注册URI
- 根据URI匹配ContentProvider中对应的数据表
- 具体使用:
// 步骤1:初始化UriMatcher对象 UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH); //常量UriMatcher.NO_MATCH = 不匹配任何路径的返回码 // 即初始化时不匹配任何东西 // 步骤2:在ContentProvider 中注册URI(addURI()) int URI_CODE_a = 1; int URI_CODE_b = 2; matcher.addURI("cn.scu.myprovider", "user1", URI_CODE_a); matcher.addURI("cn.scu.myprovider", "user2", URI_CODE_b); // 若URI资源路径 = content://cn.scu.myprovider/user1 ,则返回注册码URI_CODE_a // 若URI资源路径 = content://cn.scu.myprovider/user2 ,则返回注册码URI_CODE_b // 步骤3:根据URI 匹配 URI_CODE,从而匹配ContentProvider中相应的资源(match()) @Override public String getType(Uri uri) { Uri uri = Uri.parse(" content://cn.scu.myprovider/user1"); switch(matcher.match(uri)){ // 根据URI匹配的返回码是URI_CODE_a // 即matcher.match(uri) == URI_CODE_a case URI_CODE_a: return tableNameUser1; // 如果根据URI匹配的返回码是URI_CODE_a,则返回ContentProvider中的名为tableNameUser1的表 case URI_CODE_b: return tableNameUser2; // 如果根据URI匹配的返回码是URI_CODE_b,则返回ContentProvider中的名为tableNameUser2的表 } }
5.3 ContentObserver
- 定义:内容观察者
- 作用:观察URI引起ContentProvider中的数据变化和通知外界(即访问该数据访问者,当ContentProvider中数据增删改时,就会触发ContentObserver类)
- 具体使用:
// 步骤1:注册内容观察者ContentObserver getContentResolver().registerContentObserver(uri); // 通过ContentResolver类进行注册,并指定需要观察的URI // 步骤2:当该URI的ContentProvider数据发生变化时,通知外界(即访问该ContentProvider数据的访问者) public class UserContentProvider extends ContentProvider { public Uri insert(Uri uri, ContentValues values) { db.insert("user", "userid", values); getContext().getContentResolver().notifyChange(uri, null); // 通知访问者 } } // 步骤3:解除观察者 getContentResolver().unregisterContentObserver(uri); // 同样需要通过ContentResolver类进行解除
四、实例说明
- 由于ContentProvider不仅常用与进程之间通信,同时也适用于进程内通信
- 所以本实例会采用ContentProvider讲解:
- 进程内通信
- 进程间通信
- 实例说明:采用的数据源是Android中的SQLite数据库
4.1 进程内通信
步骤说明:
- 创建数据库类
- 自定义 ContentProvider 类
- 注册 创建的 ContentProvider 类
- 进程内访问 ContentProvider 的数据
步骤1:创建数据库类
public class DBHelper extends SQLiteOpenHelper { private static final String CREATE_STUDENT = "create table if not exists Student(" + "id integer primary key autoincrement," + "name varchar(11)," + "age integer)"; private static final String CREATE_TEACHER = "create table if not exists Teacher(" + "id integer primary key autoincrement," + "name varchar(11)," + "age integer)"; public DBHelper(@Nullable Context context) { super(context, "person.db", null, 1); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL(CREATE_STUDENT); db.execSQL(CREATE_TEACHER); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { } }
步骤2:自定义 ContentProvider 类
public class MyContentProvider extends ContentProvider { Context mContext; DBHelper dbHelper; SQLiteDatabase sqLiteDatabase; public static final String AUTOHORITY = "com.peter.myprovider"; // 设置ContentProvider的唯一标识 public static final int Student_Code = 1; public static final int Teacher_Code = 2; // UriMatcher类使用:在ContentProvider 中注册URI private static final UriMatcher mMatcher; static { mMatcher = new UriMatcher(UriMatcher.NO_MATCH); // 初始化 mMatcher.addURI(AUTOHORITY, "Student", Student_Code); mMatcher.addURI(AUTOHORITY, "Teacher", Teacher_Code); // 若URI资源路径 = content://cn.scu.myprovider/user ,则返回注册码User_Code // 若URI资源路径 = content://cn.scu.myprovider/job ,则返回注册码Job_Code } @Override public boolean onCreate() { mContext = getContext(); dbHelper = new DBHelper(getContext()); sqLiteDatabase = dbHelper.getWritableDatabase(); sqLiteDatabase.execSQL("delete from Student"); sqLiteDatabase.execSQL("insert into Student(name,age) values(\"小明\",17)"); sqLiteDatabase.execSQL("insert into Student(name,age) values(\"小红\",18)"); sqLiteDatabase.execSQL("delete from Teacher"); sqLiteDatabase.execSQL("insert into Teacher(name,age) values(\"张老师\",54)"); sqLiteDatabase.execSQL("insert into Teacher(name,age) values(\"王老师\",47)"); return true; } @Override public Uri insert(@NonNull Uri uri, ContentValues values) { // 根据URI匹配 URI_CODE,从而匹配ContentProvider中相应的表名 // 该方法在最下面 String table = getTableName(uri); // 向该表添加数据 sqLiteDatabase.insert(table, null, values); // 当该URI的ContentProvider数据发生变化时,通知外界(即访问该ContentProvider数据的访问者) mContext.getContentResolver().notifyChange(uri, null); // // 通过ContentUris类从URL中获取ID // long personid = ContentUris.parseId(uri); // System.out.println(personid); return uri; } @Override public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { // 根据URI匹配 URI_CODE,从而匹配ContentProvider中相应的表名 // 该方法在最下面 String table = getTableName(uri); // // 通过ContentUris类从URL中获取ID // long personid = ContentUris.parseId(uri); // System.out.println(personid); // 查询数据 return sqLiteDatabase.query(table,projection,selection,selectionArgs,null,null,sortOrder,null); } @Override public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) { // TODO: Implement this to handle requests to update one or more rows. throw new UnsupportedOperationException("Not yet implemented"); } @Override public String getType(@NonNull Uri uri) { // TODO: Implement this to handle requests for the MIME type of the data // at the given URI. throw new UnsupportedOperationException("Not yet implemented"); } @Override public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) { // Implement this to handle requests to delete one or more rows. throw new UnsupportedOperationException("Not yet implemented"); } /** * 根据URI匹配 URI_CODE,从而匹配ContentProvider中相应的表名 */ private String getTableName(Uri uri) { String tableName = null; switch (mMatcher.match(uri)) { case Student_Code: tableName = "Student"; break; case Teacher_Code: tableName = "Teacher"; break; } return tableName; } }
步骤3:注册 创建的 ContentProvider类
<provider android:name=".MyContentProvider" android:authorities="com.peter.myprovider" android:enabled="true" android:exported="true"/>
步骤4:进程内访问 ContentProvider中的数据
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Uri studentUri = Uri.parse("content://com.peter.myprovider/Student"); Uri teacherUri = Uri.parse("content://com.peter.myprovider/Teacher"); ContentValues values1 = new ContentValues(); values1.put("name", "小天"); values1.put("age", 18); ContentResolver resolver1 = getContentResolver(); resolver1.insert(studentUri, values1); Cursor cursor1 = resolver1.query(studentUri, null, null, null, null); if (cursor1 != null) { while (cursor1.moveToNext()) { Log.d("AAAAAAAAAAAAAA1", cursor1.getInt(cursor1.getColumnIndex("id")) + cursor1.getString(cursor1.getColumnIndex("name")) + cursor1.getInt(cursor1.getColumnIndex("age"))); } cursor1.close(); } ContentValues values2 = new ContentValues(); values2.put("name", "刘老师"); values2.put("age", 38); ContentResolver resolver2 = getContentResolver(); resolver2.insert(teacherUri, values2); Cursor cursor2 = resolver2.query(teacherUri, null, null, null, null); if (cursor2 != null) { while (cursor2.moveToNext()) { Log.d("AAAAAAAAAAAAAA2", cursor2.getInt(cursor2.getColumnIndex("id")) + cursor2.getString(cursor2.getColumnIndex("name")) + cursor2.getInt(cursor2.getColumnIndex("age"))); } cursor2.close(); } } }
结果:
05-16 13:59:21.773 7400-7400/com.peterli.storagetypefour1 D/AAAAAAAAAAAAAA1: 7小明17 05-16 13:59:21.773 7400-7400/com.peterli.storagetypefour1 D/AAAAAAAAAAAAAA1: 8小红18 05-16 13:59:21.773 7400-7400/com.peterli.storagetypefour1 D/AAAAAAAAAAAAAA1: 9小天18 05-16 13:59:21.783 7400-7400/com.peterli.storagetypefour1 D/AAAAAAAAAAAAAA2: 7张老师54 05-16 13:59:21.783 7400-7400/com.peterli.storagetypefour1 D/AAAAAAAAAAAAAA2: 8王老师47 05-16 13:59:21.783 7400-7400/com.peterli.storagetypefour1 D/AAAAAAAAAAAAAA2: 9刘老师38
github地址:https://github.com/lipete/ContentProvider1
4.2 进程间通信
- 实例说明:本文需要创建2个进程,即创建两个工程,作用如下(相当于C/S架构)
进程1(Server)
使用步骤如下:
- 创建数据库类
- 自定义 ContentProvider类
- 注册 创建的 ContentProvider类
前2个步骤同上例相同,此处不作过多描述,此处主要讲解步骤3
步骤3:注册 创建的 ContentProvider类
<provider android:name="MyProvider" android:authorities="com.peterli.myprovider" // 声明外界进程可访问该Provider的权限(读 & 写) android:permission="com.peterli.PROVIDER" // 权限可细分为读 & 写的权限 // 外界需要声明同样的读 & 写的权限才可进行相应操作,否则会报错 // android:readPermisson = "com.peterli.Read" // android:writePermisson = "com.peterli.Write" // 设置此provider是否可以被其他进程使用 android:exported="true" /> // 声明本应用 可允许通信的权限 <permission android:name="com.peterli.PROVIDER" android:protectionLevel="normal"/> // 细分读 & 写权限如下,但本Demo直接采用全权限 // <permission android:name="com.peterli.Write" android:protectionLevel="normal"/> // <permission android:name="com.peterli.Read" android:protectionLevel="normal"/>
github地址:https://github.com/lipete/ContentProvider2
进程2(Client)
步骤1:声明可访问的权限
// 声明本应用可允许通信的权限(全权限) <uses-permission android:name="com.peterli.PROVIDER"/> // 细分读 & 写权限如下,但本Demo直接采用全权限 // <uses-permission android:name="com.peterli.Read"/> // <uses-permission android:name="com.peterli.Write"/> // 注:声明的权限必须与进程1中设置的权限对应
步骤2:访问 ContentProvider的类
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Uri uri1 = Uri.parse("content://com.peter.provider/Student"); Uri uri2 = Uri.parse("content://com.peter.provider/Teacher"); ContentValues values1 = new ContentValues(); values1.put("name", "小黑"); values1.put("age", 18); ContentResolver resolver1 = getContentResolver(); resolver1.insert(uri1, values1); ContentValues values2 = new ContentValues(); values2.put("name", "黄老师"); values2.put("age", 78); ContentResolver resolver2 = getContentResolver(); resolver2.insert(uri2, values2); Cursor cursor1 = resolver1.query(uri1, null, null, null, null); if (cursor1 != null) { while (cursor1.moveToNext()) { Log.d("AAAAAAAAAAA1", cursor1.getInt(cursor1.getColumnIndex("id")) + cursor1.getString(cursor1.getColumnIndex("name")) + cursor1.getInt(cursor1.getColumnIndex("age"))); } cursor1.close(); } Cursor cursor2 = resolver2.query(uri2, null, null, null, null); if (cursor2 != null) { while (cursor2.moveToNext()) { Log.d("AAAAAAAAAAA2", cursor2.getInt(cursor2.getColumnIndex("id")) + cursor2.getString(cursor2.getColumnIndex("name")) + cursor2.getInt(cursor2.getColumnIndex("age"))); } cursor2.close(); } } }
结果
05-16 14:05:30.483 8080-8080/com.peterli.client D/AAAAAAAAAAA1: 1小明17 05-16 14:05:30.483 8080-8080/com.peterli.client D/AAAAAAAAAAA1: 2小红18 05-16 14:05:30.483 8080-8080/com.peterli.client D/AAAAAAAAAAA1: 3小黑18 05-16 14:05:30.483 8080-8080/com.peterli.client D/AAAAAAAAAAA2: 1张老师54 05-16 14:05:30.483 8080-8080/com.peterli.client D/AAAAAAAAAAA2: 2王老师47 05-16 14:05:30.483 8080-8080/com.peterli.client D/AAAAAAAAAAA2: 3黄老师78
github地址:https://github.com/lipete/Client
五、ContentProvider(内容提供者)的优点
1.安全
ContentProvider为应用之间的数据交互提供了一个安全的环境:允许把自己的应用数据根据需求开放给其他应用进行增删查改,而不用担心因为开放数据库权限而带来的安全问题。
2.访问简单而且高效
对比与其他对外共享数据的方式,数据访问的方式会因数据存储的方式而不同:
- 采用 文件方式 对外共享数据,需要进行文件操作读写数据;
- 采用 SharedPreferences 共享数据,需要使用 SharedPreferences API 读写数据
这使得访问数据变得复杂并且难度大
- 但采用 ContentProvider 方式,其解耦了底层数据的存储方式,使得无论底层数据存储采用何种方式,外界对数据的访问方式都是统一的,这使得访问简单而且高效。(比如说:一开始数据存储方式采用SQLIte数据库,后来把数据库换成MongoDB,也不会对上层数据 ContentProvider 使用代码产生影响)
六、总结
本文来源:https://www.jianshu.com/p/ea8bc4aaf057 感谢作者的分享,我选择性地再整理了一次。