Android ContentProvider数据共享全解析

来源:互联网 发布:淘宝巧克力 编辑:程序博客网 时间:2024/05/19 19:31

     前两天处理了那个beam分享的问题单之后,回头来想想,蓝牙进程为什么能读取到文件并分享给另一个手机呢?Android手机本身是LInux系统,每个文件都有相应的读、写、执行权限的,如果权限不符合是无法访问该文件的,而在那个问题单的处理过程当中,我们可以看到,就是使用了Uri标识文件,然后给蓝牙进程赋予读权限,最终蓝牙进程肯定就是通过ContentProvider来实现文件的读取并共享的。问题单虽然处理完了,但是对于ContentProvider还是只了解面貌,底层到底是如何实现的,我们还是不清楚,也就是考虑到这个,所以本节课我们来深究一下ContentProvider的实现原理。

     之前也读过老罗关于ContentProvider的研究的博客了,写的非常好,非常细致,大家也可以学习一下,但是还是要说明一下,只是读别人的博客,自己不研究,不跟踪代码,那么学习还是非常肤浅的,基本上就非常模糊。好了,我们这节课呢实现很简单,就是通过访问图库中的图片,来学习一下ContentProvider的实现原理。客户端很简单,就一个方法,其他的调用界面我就不写了,大家可以自己写一下:

     我在这个方法和系统层framework当中都加了一些日志打印,从输出的日志当中,我们可以看到最后获取回来的blob数据是一个byte[]数组,存储的全部是十进制的整数,这些数组中的内容代码什么呢?

     
     我们可以把这些十进制的数据先转换为十六进制,然后再将十六进制的数据转换成字符串,就可以看到它到底是什么内容了。


     从转换成的字符串结果当中,很清楚的可以看到,它正是我们当前取回来的图片文件在手机上的存储路径。 

     这个方法当中的目的也非常明确,就是通过Resolver来查询图库中Media.EXTERNAL_CONTENT_URI匹配的所有的图片,然后在返回的结果中把数据读出来,看一下返回来的数据是什么,这样就完成了。那么根据这样的目的,我们也就把这节课分为三个步骤来讲:1、context.getContentResolver().query(uri, null, null, null, null)调用完成,返回一个Cursor对象;2、cursor.moveToNext()到底干了什么?;3、cursor.getBlob(cursor.getColumnIndex(Media.DATA))是如何把数据取回来的。后面两个步骤大家可能有疑问,这两步也要单独拿出来分析?先不着急,大家细心看完就知道了,其实这两步是非常重要的,数据的获取就是通过这两步来完成的。

     1、context.getContentResolver().query(uri, null, null, null, null)返回Cursor对象

     context.getContentResolver()是由ContextImpl实现的,它返回的就是成员变量mContentResolver,而mContentResolver是一个ApplicationContentResolver对象,是在ContextImpl定义的一个内部类,而query方法是由父类ContentResolver来实现的,注意query方法的修饰符为final,即此方法不允许子类自己实现,那么我们就来看一下query方法的实现:

     1.1、acquireUnstableProvider(uri)获取一个IContentProvider对象

     调用acquireUnstableProvider(uri)来获取一个Provider实例,获取的时候会先检查uri.getScheme(),如果Scheme字段不是content,则直接返回空了,Scheme字段正确后,然后获取uri.getAuthority(),这也就是我们自己实现一个ContentProvider时定义的Authority了,注意,这个字段必须是唯一的,是在一个手机上唯一的,如果有定义了两个Authority相同的ContentProvider,那么,后面那个apk是无法安装到手机上的,大家可以试一下。acquireUnstableProvider方法最终也是根据Authority字段来查询我们的目标Provider的。这个过程我们就不展开了,大体的步骤就是先在ActivityThread当中调用acquireExistingProvider去缓存当中查,查到了就直接返回;如果缓存中没有查到,那么就继续去AMS当中查,AMS当中也是先在缓存中查,缓存有,则直接返回给当前调用进程;如果缓存也没有,那就说明提供目标ContentProvider的进程还未启动,那么就先启动目标进程,启动完成后,将目标进程提供的该ContentProvider缓存起来,同时返回给当前调用进程。这个过程,大家可以参考老罗的博客:

     Android应用程序组件Content Provider的启动过程源代码分析

     我们这里呢,从第一张图的日志输出看到,对应Media.EXTERNAL_CONTENT_URI的ContentProvider是com.android.providers.media/.MediaProvider对象,而在调用方拿到的只是一个代理对象,对应的服务端实际上就是定义在ContentProvider的内部类Transport了。

     1.2、unstableProvider.query执行查询
     unstableProvider是一个IContentProvider对象,它的query方法是由ContentProviderProxy来实现的,我们来看一下它的实现:
     这个方法当中先创建了一个BulkCursorToCursorAdaptor对象,这个adaptor也正是我们要返回给上层调用者的。binder通信在执行QUERY_TRANSACTION传信时,如果查询成功,则会调用reply.writeInt(1)写入1,如果失败则reply.writeInt(0)写入0,用这个int值来区分查询是否成功。这里我们假设查询是成功的,那么就会执行BulkCursorDescriptor d = BulkCursorDescriptor.CREATOR.createFromParcel(reply)、adaptor.initialize(d),完成后,将adapter返回给调用层。关于这块的详细执行,大家也可以看老罗的博客:
    Android应用程序组件Content Provider在应用程序之间共享数据的原理分析
     我们还是按照代码执行流程先来看一下mRemote.transact(IContentProvider.QUERY_TRANSACTION, data, reply, 0)的执行过程,然后再回来看一下query方法中后面的逻辑。mRemote.transact()调用之后,就通过binder进程间通信到ContentProviderNative类的onTransact方法当中了,当前的code就是QUERY_TRANSACTION,在这个分支当中,主要的事情就是:1、将调用方的参数通过binder传递过来的data中取出来,然后用这些参数调用query方法,完成后会得到一个Cursor cursor对象;2、以这个cursor对象为参数,构造一个CursorToBulkCursorAdaptor,然后再调用BulkCursorDescriptor d = adaptor.getBulkCursorDescriptor(),将需要的数据都封装在d当中,最后通过binder把数据返回到调用方进程当中。这里需要提醒大家一下,执行到这里,已经是在ContentProvider的服务端了,相当于正在服务端查数据。我们先来看一下服务端query方法的实现,它的实现正是由我们1.1当中Transport来实现的,那我们就来看一下Transport类的query方法的实现:
     在这里一般我们的调用方都是有read权限的,if (enforceReadPermission(callingPkg, uri, null) != AppOpsManager.MODE_ALLOWED)判断为false,那么就是直接调用最后的ContentProvider.this.query(),查询完成的结果就直接返回了,这里的this就是我们在1.1当中日志中看到的真正执行查询方法的MediaProvider了。那么接下来,我们就来看一下MediaProvider类的query方法是如何实现的。这个query方法非常长,我们这里只贴出关键代码,其他的就省略了:
     在这个方法当中,我们可以看到好多匹配,有图库、音频、版本等等各种信息,在MediaProvider类的开始,我们也可以看到,定义了两个数据库:private static final String INTERNAL_DATABASE_NAME = "internal.db"、private static final String EXTERNAL_DATABASE_NAME = "external.db",我们的图片肯定就是保存在external.db数据库中了。不过保存的只是索引,可不是图片信息,一定要理解清楚哈。我们这里要查询的表就是images了,查询工作是调用SQLiteQueryBuilder类的query方法来完成的。SQLiteQueryBuilder类的query方法我们就不贴代码了,主要作了两件事:1、将外部传入的参数组合起来,构造好一条sql语句;2、调用SQLiteDatabase类的rawQueryWithFactory方法继续执行查询。SQLiteDatabase类的rawQueryWithFactory方法也是两件事:1、先构造一个SQLiteDirectCursorDriver对象;2、调用SQLiteDirectCursorDriver的query方法执行查询。SQLiteDirectCursorDriver类的整体代码如下:
     可以看到它的构造方法中没有什么实质性的动作,只是给成员变量赋值,query方法中,构造了一个SQLiteQuery对象,然后以它为参数,构造一个SQLiteCursor,最终返回的就是这个SQLiteCursor了。注意这里的参数factory,它是SQLiteQueryBuilder类的成员变量,在整个过程中,我们没有指定factory,所以此处它为空。SQLiteCursor的构造函数中也没有什么实质性的工作,这里我们就不展开了。
     我们可以看到,系统中管理所有图片都是在MediaProvider当中,它的代码在packages/providers/MediaProvider目录下,查看manifest文件,我们就可以看到它的包名。

     有了包名,我们看一下它在手机当中的数据信息的位置,使用adb shell dumpsys package "com.android.providers.media"命令。

     这里会显示当前进程的好多信息,那么我们就是想看一下它用来存储数据的数据库到底是什么样的,我们进入它的data目录,把它的进程保存的数据全部pull出来看一下。

     这里呢,我把当前MediaProvider进程下位置data/data/com.android.providers.media目录下的所有文件夹全部pull出来了,可以看到它和我们普通进程的数据存储目录结构是相同的。

     好了,兴奋的时刻来了,我们使用SQLiteSpy来打开它的external.db数据库文件,我靠,无语了,数据库是加密的。

     从以上的代码分析,我们就可以得知,在调用Cursor cursor = context.getContentResolver().query(uri, null, null, null, null)返回的结果cursor中其实还没有任何数据,它只是准备好了一个Cursor对象,我们可以加上代码来验证一下我们猜想,比如我们先不调用cursor.moveToNext(),就直接取它当中的blob数据,能不能取出来呢?

     可以从我们的验证看到,当前只是返回的cursor对象,它的mPos才刚初始化,当前值为-1,所以我们这时候取值,就直接异常崩溃了。
     回到我们的主题,那么到这,只是在MediaProvider对象的query方法执行完了,返回到上一层Transport当中,这里也是直接返回了,那么再往回一层,回到ContentProviderNative类的onTransact方法的case QUERY_TRANSACTION分支,返回回来的cursor不为空,而且它是一个SQLiteCursor对象,在这个方法当中,就用这个cursor对象构造一个CursorToBulkCursorAdaptor对象,它当中就是给一些成员变量赋值,然后调用createAndRegisterObserverProxyLocked将我们要监听的Observer对象注册进来,这些过程我们就不深究了,其中的注册Observer会在数据变化时通知我们。创建好CursorToBulkCursorAdaptor对象之后,然后调用adaptor.getBulkCursorDescriptor()获取一个BulkCursorDescriptor对象,最后把这个对象写入的binder返回的数据当中,再写入1表示cursor创建成功,到这里Provider服务端的工作就完成了。那么再往上返回一层,就到了ContentProviderProxy类的query方法的mRemote.transact()调用,这里的(reply.readInt() != 0)就为true了,那么就利用reply中的数据构造一个BulkCursorDescriptor,然后调用adaptor.initialize(d)就完成了,最后把这个adapter返回给上一层。再往上一层就是ContentResolve类的query方法中的unstableProvider.query()调用了。到这里呢,大家可以看一下系统层所作的工作,整个query过程,只是根据我们传入的参数在服务端构造了一个SQLiteCursor,但是还没有进行任何实质性的查询工作,同时通过binder进程间通信的服务端返回给Provider客户端的是一个CursorToBulkCursorAdaptor对象,Provider客户端在收到binder通信的返回数据后,构造了一个BulkCursorToCursorAdaptor,这两个对象是通过BulkCursorDescriptor联系起来的,在服务端调用adaptor.getBulkCursorDescriptor()把数据准备好,然后写入到binder的返回数据reply中;客户端就直接从reply中调用BulkCursorDescriptor.CREATOR.createFromParcel(reply)把数据取回来,然后进一步封装,这些细节一定要搞清楚。
     1.3、qCursor.getCount()
     从这句代码的备注“Force query execution.  Might fail and throw a runtime exception here.”上,我们先来理解一下,也就是系统强制先执行一次查询,其实大家看一下它的实现就知道了,这里相当于只是进行一下数据检查,它的实现非常简单,就是检查mBulkCursor成员变量是否为空,在1.2步骤的最后,已经调用adaptor.initialize(d)把binder返回给我们的数据保存下来了,所以这里的mBulkCursor也就不为空了。
     1.4、以1.2的结果为参数调用new CursorWrapperInner(),最终返回给调用者
     通过了1.3的关口检查,那么最后就是把数据封装成一个CursorWrapperInner最终返回给我们客户端了,到这句执行完成后,相当于我们在应用层中调用getContentResolver().query()方法才完成,作了这么多工作,才只完成了一句代码的逻辑,大家从这里也可以看到,ContentProvider系统是多么的复杂!!好了,继续我们的流程,CursorWrapperInner的构造方法也比较简单,就是把传入的参数保存在成员变量当中,就完成了。
     2、cursor.moveToNext()到底干了什么?
     现在我们该执行第二步了,这里的cursor就是应用层拿到的cursor了,是一个CursorWrapperInner对象,moveToNext()是由它的父类CursorWrapper来实现的。它当中是直接调用mCursor.moveToNext()来处理的,这里的成员变量cursor也就是在1.4步骤时构造CursorWrapperInner传入的第一个参数了,实际就是binder通信完成,在客户端自己构造的一个BulkCursorToCursorAdaptor对象,它的moveToNext()也是调用父类AbstractCursor来实现的,这里的实现非常简单,就是调用moveToPosition(mPos + 1),注意,mPos还进行任何赋值,所以此时它的值是-1,moveToPosition方法当中就作了两件事:1、计算mPos的位置,然后赋值;2、回调子类的onMove。这里的子类当然就是BulkCursorToCursorAdaptor了,我们来看一下它的onMove方法的实现:
     mWindow是从父类AbstractWindowedCursor继承下来的,它是一个重量级的对象,大家从后边的分析中就可以感觉到。在前面的query过程当中,因为还没有执行任何实质性的查询,所以此时第一次执行时,mWindow是为空的,那么就执行if分支,调用mBulkCursor.getWindow(newPosition)获取服务端的window对象,然后调用setWindow赋值给它的成员变量。mBulkCursor是从binder通信的服务端返回过来的一个CursorToBulkCursorAdaptor对象。在中间的查询过程,涉及到的各种对象太多了,大家如果没有自己分析源码,这时候肯定都已经乱了。
     在这里我们重点说一下,在Provider通信过程中,binder通信直接对应的两端的对象:客户端是BulkCursorToCursorAdaptor,服务端是CursorToBulkCursorAdaptor。
     往外退一层,应用层访问的Cursor实质是封装的一个CursorWrapperInner对象,服务端实际构造好的是一个SQLiteCursor对象。
     好了,稍微理一下,我们继续,那么就通过调用CursorToBulkCursorAdaptor对象的getWindow方法去获取一个Window,我们来看一下这个方法的实现:
     这个方法当中的mCursor定义为CrossProcessCursor,是一个接口,本质就是我们上面说的服务端往外退一层的SQLiteCursor了,那么第一次调用时,获取到的window肯定是空的了,就构造一个CursorWindow对象,然后赋值给成员变量。在这个方法当中,非常重要的两步:1、调用mFilledWindow = new CursorWindow(mProviderName)构造一个window;2、mCursor.fillWindow(position, window)。
     我们先来看一下CursorWindow对象的构造过程。它的构造方法中,给mStartPos赋值为0,表示起始位置,然后调用nativeCreate方法在native层创建一个CursorWindow,它是和Java层对应的,最后调用recordNewWindow将window在native层的对象缓存起来。在调用nativeCreate(mName, sCursorWindowSize)时,传入的第二个参数sCursorWindowSize就是指的我们要创建的匿名共享内存的大小了,它是定义如下:
     config_cursorWindowSize的定义是在frameworks/base/core/res/res/values/config.xml当中:
     2048*1024,也就是说我们能创建的匿名共享内存的上限就是2M。我们来看一下native层的CursorWindow的构造方法:
     在这里的代码,大家就非常清楚的看到我们Provider跨进程传递数据的真谛了,就是ashmem_create_region系统调用创建了一个匿名共享内存,然后通过它来实现数据的跨进程传递的。如果大家想学习匿名共享内存的相关知识,可以参考老罗的博客:
     Android系统匿名共享内存Ashmem(Anonymous Shared Memory)简要介绍和学习计划

     Android系统匿名共享内存Ashmem(Anonymous Shared Memory)驱动程序源代码分析

     Android系统匿名共享内存Ashmem(Anonymous Shared Memory)在进程间共享的原理分析

     老罗的博客写的非常细致,强力给大家推荐!!

     创建好了native层的匿名共享内存后,然后调用mCursor.fillWindow(position, window),这里的mCursor就是SQLiteCursor对象,但是它没有重写带两个参数的fillWindow方法,是调用父类AbstractCursor,而父类的实现中也非常简单,就是直接调用DatabaseUtils.cursorFillWindow(this, position, window)来处理的,我们来看一下它的实现:

     分析这个方法之前,我们要先明白传进来的参数,第一个cursor就是SQLiteCursor,position是0,因为我们当前是第一次调用,window就是我们才刚刚创建好的CursorWindow对象了。接下来看一下这个方法的实现,也就是数据库操作中的本质的东西了,查到的数据都会通过调用window.putLong()、window.putDouble()、window.putBlob()、window.putString()填充到window对象上,其他剩下的逻辑我们就不展开了,大家自己有兴趣的,可以分析一下。

     上面的fillWindow方法执行完成后,最终就把window对象返回到客户端BulkCursorToCursorAdaptor对象当中了,moveToNext的逻辑也就执行完了。

     3、cursor.getBlob(cursor.getColumnIndex(Media.DATA))

     这一步就是直接调用服务端CursorWindow对象查询完成后填充好的数据了,中间的过程我们就不看了,它的最终实现是在android_database_CursorWindow.cpp当中的nativeGetBlob方法,我们来看一下它的代码:

     在这里,我想看一下它里边的数据,就自己加了一些日志输出,但是代码全部加在这个文件当中,又怕后边忘了不好找,我们就自己写个类,专门来完成我们自己的意图。为了方便查看C++中的内容,我们自己在系统的jni目录下加一个文件,专门添加我们自己的代码,防止和framework当中的代码有干扰,好,我们现在在frameworks/base/core/jni目录下加一个头文件和一个源文件,分别命名为a_leui.h、a_leui.cpp,先加一个打印jbyteArray的方法,代码非常简单:


     好,添加完成后,我们在jni目录下mm,然后打编译成功的so文件替换掉手机中当前的so库,然后再次运行并打印日志。


     这里呢需要说一下,我们搞应用层的,对C++了解非常少,当然也有很多同事经常用到JNI,那就非常熟悉了,这里的日志打印当中的%s、%d、%p等等是个格式化输出符,在网上找到了一个非常详细的总结的博客,大家如果对这个不是很了解,可以学习一下:

     printf 格式化输出符号详细说明

      好了,这里也只是简单的引入一下,只要有这个点,就可以加很多逻辑了。那么我们这里反过来,在2步骤的最后,是通过DatabaseUtils.cursorFillWindow方法来把数据填充进去的,那我们就来看一下它是怎么填充的,取出来的过程也就理解了。我们要取的blob数据,最终是调用android_database_CursorWindow.cpp当中的nativePutBlob方法来完成的,我们来看一下它的实现:

     这里的重点就是调用native层中对应的CursorWindow类的putBlob方法来填充数据了,putBlob方法很简单,是直接调用putBlobOrString来完成的,putBlobOrString方法的实现如下:

     在CursorWindow内部还使用FieldSlot、RowSlot来组织管理数据的,因为本人C++基础薄弱,而且Linux系统基本上不懂,所以再深的就不展开了。有哪位清楚的话,也请指点我一下。

    这节课也就到这里了,从整个过程当中,我们可以看到,底层的实现非常的复杂,尤其是在第一步的时候,一句query调用,底层为我们作了大量的工作。也正是这种庞大的机制才能最终保证了我们使用ContentProvider数据的简易和方便!!

     好了,同学们,下课!!

0 0
原创粉丝点击