在Android系统中,不同的应用程序是不能直接读写对方的数据文件的,如果它们想共享数据的话,只能通过Content Provider组件来实现。那么,Content Provider组件又是如何突破应用程序边界权限控制来实现在不同的应用程序之间共享数据的呢?在前面的文章中,我们已经简要介绍过它是通过Binder进程间通信机制以及匿名共享内存机制来实现的,在本文中,我们将详细分析它的数据共享原理。
Android应用程序之间不能直接访问对方的数据文件的障碍在于每一个应用程序都有自己的用户ID,而每一个应用程序所创建的文件的读写权限都是只赋予给自己所属的用户,因此,就限制了应用程序之间相互读写数据的操作,关于Android应用程序的权限问题,具体可以参考前面一篇文章 Android应用程序组件Content Provider简要介绍和学习计划 。通过前面 Android进程间通信(IPC)机制Binder简要介绍和学习计划 等一系列文章的学习,我们知道,Binder进程间通信机制可以突破了以应用程序为边界的权限控制来实现在不同应用程序之间传输数据,而Content Provider组件在不同应用程序之间共享数据正是基于Binder进程间通信机制来实现的。虽然Binder进程间通信机制突破了以应用程序为边界的权限控制,但是它是安全可控的,因为数据的访问接口是由数据的所有者来提供的,换句话来说,就是数据提供方可以在接口层来实现安全控制,决定哪些数据是可以读,哪些数据可以写。虽然Content Provider组件本身也提供了读写权限控制,但是它的控制粒度是比较粗的,如果有需要,我们还是可以在接口访问层做更细粒度的权限控制以达到数据安全的目的。
Binder进程间通信机制虽然打通了应用程序之间共享数据的通道,但是还有一个问题需要解决,那就是数据要以什么来作来媒介来传输。我们知道,应用程序采用Binder进程间通信机制进行通信时,要传输的数据都是采用函数参数的形式进行的,对于一般的进程间调来来说,这是没有问题的,然而,对于应用程序之间的共享数据来说,它们的数据量可能是非常大的,如果还是简单的用函数参数的形式来传递,效率就会比较低下。通过前面 Android系统匿名共享内存Ashmem(Anonymous Shared Memory)简要介绍和学习计划 等一系列文章的学习,我们知道,在应用程序进程之间以匿名共享内存的方式来传输数据效率是非常高的,因为它们之间只需要传递一个文件描述符就可以了。因此,Content Provider组件在不同应用程序之间传输数据正是基于匿名共享内存机制来实现的。
在继续分析Content Provider组件在不同应用程序之间共享数据的原理之前,我们假设应用程序之间需要共享的数据是保存在数据库(SQLite)中的,因此,接下来的分析都是基于SQLite数据库游标(SQLiteCursor)来进行的。SQLiteCursor在共享数据的传输过程中发挥着重要的作用,因此,我们先来它和其它相关的类的关系图,如下所示:
首先在第三方应用程序这一侧,当它需要访问Content Provider中的数据时,它会在本进程中创建一个CursorWindow对象,它在内部创建了一块匿名共享内存,同时,它实现了Parcel接口,因此它可以在进程间传输。接下来第三方应用程序把这个CursorWindow对象(连同它内部的匿名共享内存文件描述符)通过Binder进程间调用传输到Content Provider这一侧。这个匿名共享内存文件描述符传输到Binder驱动程序的时候,Binder驱动程序就会在目标进程(即Content Provider所在的进程)中创建另一个匿名共享文件描述符,指向前面已经创建好的匿名共享内存,因此,就实现了在两个进程中共享同一块匿名内存,这个过程具体可以参考 Android系统匿名共享内存Ashmem(Anonymous Shared Memory)在进程间共享的原理分析 一文。
在Content Provider这一侧,利用在Binder驱动程序为它创建好的这个匿名共享内存文件描述符,在本进程中创建了一个CursorWindow对象。现在,Content Provider开始要从本地中从数据库中查询第三方应用程序想要获取的数据了。Content Provider首先会创建一个SQLiteCursor对象,即SQLite数据库游标对象,它继承了AbstractWindowedCursor类,后者又继承了AbstractCursor类,而AbstractCursor类又实现了CrossProcessCursor和Cursor接口。其中,最重要的是在AbstractWindowedCursor类中,有一个成员变量mWindow,它的类型为CursorWindow,这个成员变量是通过AbstractWindowedCursor的子类SQLiteCursor的setWindow成员函数来设置的。这个SQLiteCursor对象设置好了父类AbstractWindowedCursor类的mWindow成员变量之后,它就具有传输数据的能力了,因为这个mWindow对象内部包含一块匿名共享内存。此外,这个SQLiteCursor对象的内部有两个成员变量,一个是SQLite数据库对象mDatabase,另外一个是SQLite数据库查询对象mQuery。SQLite数据库查询对象mQuery的类型为SQLiteQuery,它继承了SQLiteProgram类,后者又继承了SQLiteClosable类。SQLiteProgram类代表一个数据库存查询计划,它的成员变量mCompiledSql包含了一个已经编译好的SQL查询语句,SQLiteCursor对象就是利用这个编译好的SQL查询语句来获得数据的,但是它并不是马上就去获取数据的,而是等到需要时才去获取。
那么,要等到什么时候才会需要获取数据呢?一般来说,如果第三方应用程序在请求Content Provider返回数据时,如果指定了要返回关于这些数据的元信息时,例如数据条目的数量,那么Content Provider在把这个SQLiteCursor对象返回给第三方应用程序之前,就会去获取数据,因为只有获取了数据之后,才知道数据条目的数量是多少。SQLiteCursor对象通过调用成员变量mQuery的fillWindow成员函数来把从SQLite数据库中查询得到的数据保存其父类AbstractWindowedCursor的成员变量mWindow中去,即保存到第三方应用程序创建的这块匿名共享内存中去。如果第三方应用程序在请求Content Provider返回数据时,没有指定要返回关于这些数据的元信息,那么,就要等到第三方应用程序首次调用这个从Content Provider处返回的SQLiteCursor对象的数据获取方法时,才会真正执行从数据库存中查询数据的操作,例如调用了SQLiteCursor对象的getCount或者moveToFirst成员函数时。这是一种数据懒加载机制,需要的时候才去加载,这样就提高了数据传输过程中的效率。
上面说到,Content Provider向第三方应用程序返回的数据实际上是一个SQLiteCursor对象,那么,这个SQLiteCursor对象是如何传输到第三方应用程序的呢?因为它本身并不是一个Binder对象,我们需要对它进行适配一下。首先,Content Provider会根据这个SQLiteCursor对象来创建一个CursorToBulkCursorAdaptor适配器对象,这个适配器对象是一个Binder对象,因此,它可以在进程间传输,同时,它实现了IBulkCursor接口。Content Provider接着就通过Binder进程间通信机制把这个CursorToBulkCursorAdaptor对象返回给第三方应用程序,第三方应用程序得到了这个CursorToBulkCursorAdaptor之后,再在本地创建一个BulkCursorToCursorAdaptor对象,这个BulkCursorToCursorAdaptor对象的继承结构和SQLiteCursor对象是一样的,不过,它没有设置父类AbstractWindowedCursor的mWindow成员变量,因此,它只可以通过它内部的CursorToBulkCursorAdaptor对象引用来访问匿名共享内存中的数据,即通过访问Content Provider这一侧的SQLiteCursor对象来访问共享数据。
以 Android应用程序组件Content Provider应用实例一文的例子来分析Content Provider在不同应用程序之间共享数据的原理。在Android应用程序组件Content Provider应用实例 这篇文章介绍的应用程序Article中,它的主窗口MainActivity是通过调用它的内部ArticlesAdapter对象的getArticleByPos成员函数来把ArticlesProvider中的文章信息条目一条一条地取回来显示在ListView中的,在这篇文章中,我们就从ArticlesAdapter类的getArticleByPos函数开始,一步一步地分析第三方应用程序Article从ArticlesProvider这个Content Provider中获取数据的过程。同样,我们先来看看这个过程的序列图,然后再详细分析每一个步骤:
Step 1. ArticlesAdapter.getArticleByPos
这个函数定义在前面一篇文章 Android应用程序组件Content Provider应用实例 介绍的应用程序Artilce源代码工程目录下,在文件为packages/experimental/Article/src/shy/luo/article/ArticlesAdapter.java中:
1. public class
2. ......
3.
4. private ContentResolver resolver = null;
5.
6. public
7. resolver = context.getContentResolver();
8. }
9.
10. ......
11.
12. public Article getArticleByPos(int
13. Uri uri = ContentUris.withAppendedId(Articles.CONTENT_POS_URI, pos);
14.
15. new
16. Articles.ID,
17. Articles.TITLE,
18. Articles.ABSTRACT,
19. Articles.URL
20. };
21.
22. null, null, Articles.DEFAULT_SORT_ORDER);
23. if
24. return null;
25. }
26.
27. int id = cursor.getInt(0);
28. 1);
29. 2);
30. 3);
31.
32. return new
33. }
34. }
这个函数通过应用程序上下文的ContentResolver接口resolver的query函数来获得与Articles.CONTENT_POS_URI这个URI对应的文章信息条目。常量Articles.CONTENT_POS_URI是在应用程序ArticlesProvider中定义的,它的值为“content://shy.luo.providers.articles/pos”,通过调用ContentUris.withAppendedId函数来在后面添加了一个整数,表示要获取指定位置的文章信息条目。这个位置是指ArticlesProvider这个Content Provider中的所有文章信息条目按照Articles.DEFAULT_SORT_ORDER来排序后得到的位置的,常量Articles.DEFAULT_SORT_ORDER也是在应用程序ArticlesProvider中定义的,它的值为“_id asc”,即按照文章信息的ID值从小到大来排列。
Step 2. ContentResolver.query
这个函数定义在frameworks/base/core/java/android/content/ContentResolver.java文件中:
1. public abstract class
2. ......
3.
4. public final
5. String selection, String[] selectionArgs, String sortOrder) {
6. IContentProvider provider = acquireProvider(uri);
7. if (provider == null) {
8. return null;
9. }
10. try
11. ......
12. Cursor qCursor = provider.query(uri, projection, selection, selectionArgs, sortOrder);
13. ......
14.
15. return new
16. catch
17. ......
18. catch(RuntimeException e) {
19. ......
20. }
21. }
22.
23. ......
24. }
这个函数首先通过调用acquireProvider函数来获得与参数uri对应的Content Provider接口,然后再通过这个接口的query函数来获得相应的数据。我们先来看看acquireProvider函数的实现,再回过头来分析这个Content Provider接口的query函数的实现。
Step 3. ContentResolver.acquireProvider
Step 4. ApplicationContentResolver.acquireProvider
Step 5. ActivityThread.acquireProvider
Step 6. ActivityThread.getProvider
从Step 3到Step 6是获取与上面Step 2中传进来的参数uri对应的Content Provider接口的过程。在前面一篇文章 Android应用程序组件Content Provider的启动过程源代码分析 中,我们已经详细介绍过这个过程了,这里不再详述。不过这里我们假设,这个Content Provider接口之前已经创建好了,因此,在Step 6的ActivityThread.getProvider函数中,通过getExistingProvider函数就把之前已经好的Content Provider接口返回来了。
回到Step 2中的ContentResolver.query函数中,它继续调用这个返回来的Content Provider接口来获取数据。从这篇文章 Android应用程序组件Content Provider的启动过程源代码分析 中,我们知道,这个Content Provider接口实际上是一个在ContentProvider类的内部所创建的一个Transport对象的远程接口。这个Transport类继承了ContentProviderNative类,是一个Binder对象的Stub类,因此,接下来就会进入到这个Binder对象的Proxy类ContentProviderProxy中执行query函数。