7.6 ContentResolver openAssetFileDescriptor函数分析

通过对Cursor query的分析可知,客户端进程可像查询本地数据库那样,从目标CP进程获取信息。不过,这种方法也有其局限性:

客户端只能按照结果集的组织方式来获取数据,而结果集的组织方式是行列式的,即客户端须移动游标到指定行,才能获取自己感兴趣的列的值。在实际生活中,不是所有信息都能组织成行列的格式。

query查询得到的数据的数据量很有限。通过分析可知,用于承载数据的共享内存只有2MB大小。对于较大数据量的数据,通过query方式来获取显然不合适。

考虑到query的局限性,ContentProvider还支持另外一种更直接的数据传输方式,笔者称之为“文件流方式”。因为通过这种方式客户端将得到一个类似文件描述符的对象,然后在其上创建对应的输入或输出流对象。这样,客户端就可通过它们和CP进程交互数据了。

下面来分析这个功能是如何实现的。先向读者介绍客户端使用的函数,即CR的openAssetFileDescriptor。

7.6.1 openAssetFileDescriptor之客户端调用分析

这部分的代码如下:

[—>ContentResolver. java:openAssetFileDescriptor]


public final AssetFileDescriptor openAssetFileDescriptor(Uri uri,

String mode)throws FileNotFoundException{

//openAssetFileDescriptor是一个通用函数,它支持3种scheme类型的URI。见

//下文的解释

String scheme=uri.getScheme();

if(SCHEME_ANDROID_RESOURCE.equals(scheme)){

if(!"r".equals(mode)){

throw new FileNotFoundException("Can't write resources:"+uri);

}

//创建资源文件输入流

OpenResourceIdResult r=getResourceId(uri);

try{

return r.r.openRawResourceFd(r.id);

}……

}else if(SCHEME_FILE.equals(scheme)){

//创建普通文件输入流

ParcelFileDescriptor pfd=ParcelFileDescriptor.open(

new File(uri.getPath()),modeToMode(uri, mode));

return new AssetFileDescriptor(pfd,0,-1);

}else{

if("r".equals(mode)){

//我们重点分析这个函数,用于读取目标CP的数据

return openTypedAssetFileDescriptor(uri,"/",null);

}……//其他模式的支持,请读者学完本章后自行研究这部分内容

}

}


如以上代码所述,openAssetFileDescriptor是一个通用函数,它支持3种sheme类型的URI。

SCHEME_ANDROID_RESOURCE:字符串表达为android.resource。通过它可以读取APK包(其实就是一个压缩文件)中封装的资源。假设在应用进程的res/raw目录下存放一个test.ogg文件,最终生成的资源ID由R.raw.tet来表达,那么如果应用进程想要读取这个资源,创建的URI就是android.resource://com.package.name/R.raw.test。读者不妨试一试。

SCHEME_FILE:字符串表达为file。通过它可以读取普通文件。

除上述两种scheme之外的URI:这种资源背后到底对应的是什么数据需要由目标CP来解释。

接下来分析在第3种sheme类型下调用的openTypedAssetFileDescriptor函数。

1.openTypedAssetFileDescriptor函数分析

这部分的代码如下:

[—>ContentResolver.java:openTypedAssetFileDescriptor]


public final AssetFileDescriptor openTypedAssetFileDescriptor(Uri uri,

String mimeType, Bundle opts)throws FileNotFoundException{

//建立和目标CP进程交互的通道。读者还记得此处provider的真实类型吗?

//其真实类型是ContentProviderProxy

IContentProvider provider=acquireProvider(uri);

try{

//①调用远端CP的openTypedAssetFile函数,返回值的

//类型是AssetFileDescriptor。此处传递参数的值为:mimeType="/"

//opts=null

AssetFileDescriptor fd=provider.openTypedAssetFile(uri,

mimeType, opts);

……

//②创建一个ParcelFileDescriptor类型的变量

ParcelFileDescriptor pfd=new ParcelFileDescriptorInner(

fd.getParcelFileDescriptor(),provider);

provider=null;

//③又创建一个AssetFileDescriptor类型的变量作为返回值

return new AssetFileDescriptor(pfd, fd.getStartOffset(),

fd.getDeclaredLength());

}……

finally{

if(provider!=null){

releaseProvider(provider);

}

}

}


在以上代码中,阻碍我们思维的依然是新出现的类。先应解决它们,然后再分析代码中的关键调用。

2.FileDescriptor家族介绍

本节涉及的类家族图谱如图7-7所示。

图7-7中的内容比较简单,只需稍作介绍。

FileDescriptor类是Java的标准类,它是对文件描述符的封装。进程打开的每一个文件都有一个对应的文件描述符。在Native语言开发中,它用一个int型变量来表示。

7.6 ContentResolver openAssetFileDescriptor函数分析 - 图1

图 7-7 FileDescriptor家族图谱

文件描述符作为进程的本地资源,如想越过进程边界将其传递给其他进程,则需借助进程间共享技术。在Android平台上,设计者封装了一个ParcelFileDescriptor类。此类实现了Parcel接口,自然就支持了序列化和反序列化的功能。从图7-7可知,一个ParcelFileDescriptor通过mFileDescritpor指向一个文件描述符。

AssetFileDescriptor也实现了Parcel接口,其内部通过mFd成员变量指向一个ParcelFileDescriptor对象。从这里可看出,AssetFileDescritpor是对ParcelFileDescriptor类的进一步封装和扩展。实际上,根据SDK文档中对AssetFileDescritpor的描述可知,其作用在于从AssetManager(后续分析资源管理的时候会介绍它)中读取指定的资源数据。

提示 简单向读者介绍一下与AssetFileDescriptor相关的知识。它用于读取APK包中指定的资源数据。以前面提到的test.ogg为例,如果通过AssetFileDescriptor读取它,那么其mFd成员则指向一个ParcelFileDescriptor对象。且不管这个对象是否跨越了进程边界,它毕竟代表一个文件。假设这个文件是一个APK包,AssetFileDescriptor的mStartOffset变量用于指明test.ogg在这个APK包中的起始位置,比如100字节。而mLength用于指明test.ogg的长度,假设是1000字节。通过上面的介绍可知,该APK文件从100字节到1100字节这一段空间中存储的就是test.ogg的数据。这样,AssetFileDescriptor就能将test.ogg数据从APK包中读取出来了。

下面来看ContentProvider的openTypedAssetFile函数。