
注:以下内容基于Android API Version 27(Android 8.1)Linux Kernel 3.18.0
概述
ContentProvider
是一种Android数据共享机制,无论其内部数据以什么样的方式组织,对外都是提供统一的SQL式的接口。
ContentProvider
基于binder
进行进程间通信,具有较高的安全性。由于其使用共享内存传输数据,也因此具备较高的传输效率。
ContentProvider的查找和启动
调用流程
Provider总是依附于一个进程,如果不特别指定,Provider属于App主进程,如果指定,就运行于指定进程,Provider会在自己所在进程启动时启动,由于App主进程一般是第一个运行,所以属于主进程的Provider会最先被启动,子进程的Provider只有等到Provider被调用时或者子进程由于其他原因被启动时启动。当Provider所在进程未启动时,AMS
总是先启动App进程,然后由进程自己启动所包含的Provider。
我们假设ContentProvider由其他App提供。
调用端App在使用ContentProvider前首先要获取ContentProvider。ContentProvider使用authority
唯一标识,通过ContentResolver
调用acquireProvider(authority)
,acquireProvider
经过ContentImpl
调到ActivityThread
,ActivityThread
首先通过一个map
查找是否已经install
过这个Provider,如果install
过就直接将之返回给调用者,如果没有install
过就调用AMS
的getContentProvider
,AMS
首先查找这个Provider是否被publish
过,如果publish
过就直接返回,否则通过PMS
找到Provider所在的App。
接下来就区分App进程是否启动。
A,如果发现目标App进程未启动,就创建一个ContentProviderRecord
对象然后调用其wait
方法阻塞当前执行流程,并将ContentProviderRecord
记录到一个map
中,接着启动目标App进程,目标App进程启动后调用AMS
的attachApplication
,AMS
找到App的所有运行于当前进程的Provider,如果某个Provider没有对应的ContentProviderRecord
就创建一个,保存在map
中,然后通过ApplicationThead
回调App端的bindApplication
, 将要启动的所有Provider传给目标App进程, App进程在bindApplication
流程里启动传来的Provider(通过反射创建Provider对象,调用其onCreate
方法),并将启动后的Provider对象记录在App进程中的一个map
方便下次查找,然后调用AMS
的publishContentProviders
将所有启动的Provider发送给AMS
。AMS
收到Provider后将Provider记录起来,并根据Provider从之前的map
中找到ContentProviderRecord
然后依次调用这些ContentProviderRecord
的notifyAll
方法解除前面对获取Provider执行流程的阻塞,这样获取Provider的流程可以继续执行,并将刚刚发布Provider返回给调用端。
B,如果目标App进程已启动,正常情况下App在进程启动过程(无论是主进程还是子进程)走到bindApplication
一步时会将所有AMS发来的本进程的Provider都启动起来并记录和发布给AMS
。所以AMS
在getContentProvider
里会查找到要获取的Provider,就直接返回了。如果因为某种异常情况目标App没有启动自己的Provider并发送给AMS
,AMS
这时会发现目标App进程已启动,但是自己没有记录这个Provider,这时会调用目标App进程的ApplicationThread
的scheduleInstallProvider
,请求目标进程install
这个Provider,目标进程install
后就会将Provider发送给AMS
,AMS
就可以恢复getContentProvider
调用将结果返回给调用端App了。
调用端App收到AMS
的返回结果后(acquireProvider
返回),调用ActivityThread
的installProvider
将Provider记录到本地的一个map
中,下次再调用acquireProvider
就直接返回。
假设调用者和所调用的ContentProvider在同一个App中
如果ContentProvider和调用进程不属于同一个进程,则流程和上述调用者和被调用者不在一个App相同,如果属于同一个进程,由于调用者进程启动时就已经将Provider启动并缓存在了ActivityThread
的一个map
中,所以acquireProvider
会直接返回缓存的Provider对象而不经过AMS
。
ContentProvider进程间传递
ContentProvider跨进程传输实际上传输的是ContentProvider.Transport
对象,Transport
是一个binder本地对象(BBinder
),实现了IContentProvider
接口,IContentProvider
包含了ContentProvider的所有操作,query
、insert
等。调用端进程拿到的是ContentProviderProxy
对象,通过跨进程调用对端的接口。
ContentProvider的接口调用
ContentProvider主要支持增删改查4种SQL调用,和一个支持调用自定义方法的call
方法。
ContentProvider一般是使用ContentResolver
进行访问,ContentResolver
提供了对ContentProvider访问的包装。
ContentProvider所提供的接口中只有query
是基于共享内存的,其他都是直接使用binder的入参出参进行数据传递。
query调用原理
ContentResolver
的query
实际调用的是ContentProvider的query
,而ContentProvider的query
是跨进程调用的,在调用者进程这边,实际通过ContentProviderProxy
将query
调用转发给了其所包装的binder远程对象。而在Provider提供者进程这边,ContentProvider继承自ContentProviderNative
,ContentProviderNative
是一个Binder本地对象,ContentProviderNative
收到query
的binder调用后会调用用户自定义的ContentProvider的query
方法,query
方法的返回值是一个Cursor
,Cursor
是一个接口,Provider进程会将用户返回的Cursor
对象装成一个实现了IBulkCursor
接口的CursorToBulkCursorAdaptor
对象,CursorToBulkCursorAdaptor
是一个Binder本地对象,支持跨进程传递和调用,在构造CursorToBulkCursorAdaptor
过程中Provider进程为Cursor
分配了一个CursorWindow
,CursorWindow
本质上是一个buffer,SQLite查询得到的数据保存在CursorWindow
的buffer中,CursorWindow
基于MemoryBase
实现,也就是说CursorWindow
实际上是基于一片共享内存,此时Provider进程会执行查询语句并将查询结果保存在这个CursorWindow
。接着Provider进程将CursorWindow
和CursorToBulkCursorAdaptor
写到Parcel
中回传给调用者进程。(CursorWindow
实际上写到Parcel
的是共享内存的fd
),调用者进程收到返回结果后通过共享内存fd
构造了自己的CursorWindow
(此CursorWindow
和Provider进程的CursorWindow
指向的是同一片内存),同时构造了一个BulkCursorToCursorAdaptor
对象用来包装对CursorWindow
和远程的IBulkCursor
的访问。BulkCursorToCursorAdaptor
实现了Cursor
接口,所以对于调用者来说拿到的是一个Cursor
,至于这个Cursor
是跨进程调用的还是本地调用对调用方是透明的。
总结:
在ContentProvider传递过程中,AMS
是一个中间人的角色,它负责从Provider提供者进程获取Provider,然后传给Provider调用者进程,后续交互就是提供者和调用者两个进程之间的事情了。
query
方法和其他方法的最大不同是它基于共享内存,因此它可以满足大批量数据传输并且保持高效的需求。
注意
- ContentProvider先于
Application.onCreate
执行,后于Application
的构造方法和Application.attachBaseContext
执行。 ContentResolver.acquireProvider
的执行是耗时的,最坏情况下它要经过:跨进程调用AMS->AMS启动目标进程->目标进程执行Application
初始化->目标进程启动Provider->目标进程跨进程请求AMS
->AMS
跨进程返回Provider。因此要避免在主线程调用。- 可以通过调用
call
方法来调用ContentProvider的自定义方法,call
有一个字符串用来标识方法名。入参出参通过Bundle
传递。