Android上的MediaPlayer播放底层框架已经经历了多次变动,从最早先的OpenCore到后来的StageFright再到现在的NuPlayerDriver,在工作开始接触Android的时候已经移除了OpenCore所以对OpenCore的了解仅仅停留在听说过,这些框架在演进过程中一般都是先两种框架并存,然后再在某个版本中将其移除,早先Android中使用的是Stagefright + NuPlayer并存的方式,其中前者负责播放本地的媒体文件,后者用于播放网络流媒体文件,但是在后来的Android L开始NuPlayeri渐渐开始替代了Stagefright,目前本地播放已经切换到NuPlayer上了,在Android N AOPS 源代码中甚至移除了Stagefright。在接下来的两篇博客中将会给大家介绍Stragefright播放框架以及Nuplayer播放框架。
今天这篇先带大家看下Stagefright播放框架。在介绍Stagefright和Nuplayer之前我们先来看下Android MediaPlayer播放框架的大体结构图:

整个结构可以分成如下几个部分:

  • MediaPlayer Java层,这是我们开发上层应用所接触的API层。代码位于 frameworks/base/media/java/android/media/MediaPlayer.java
  • MediaPlayer JNI 层,它用于将Java层和MediaPlayer的 native层连接起来。代码位于frameworks/base/media/jni/android_media_MediaPlayer.cpp
  • MediaPLayer Native层,它是MediaPlayer的本地代码这里包括了Stagefright + AwesomePlayer 以及Nuplayer + Nuplayer 两大播放框架,代码位于frameworks/av/
  • MediaPlayer HardWare层,这个涉及到一些硬编码部分

上面只是一个通用的结构,下面来个基于Stagefight的MediaPLayer框架的结构:

在最上层是应用层,这里包括Music,Video还可以是一些其他相关的应用,这些应用使用framwork层提供的上层API接口来访问和操作底层的播放框架,上面已经介绍过在Java层和native层之间通过jni层来进行映射,这些native层部件则通过IPC进程间通信方式来从Media Player Service获得请求的响应。Media Player Service 是在mediaserver创建的时候被创建出来的,它负责根据当前文件的mimetype创建合适的media player,并由对应的mediaplayer实例化一个合适的编解码器,获取和操作多媒体文件的内容,以及管理其间的缓存。下面是各个层的更为详细的介绍。
为了在后续的介绍中不被整个代码绕晕建议在这里先通过这些图对整个框架有大体的了解。后续的代码介绍的时候我会更偏向于细节介绍。

  • 应用层和framework层
    使用到MediaPlayer的应用很多,最常见的就是Music和Video,如果要了解这些应用的实现可以看下AOSP代码中的packages/apps,这些代码中用到了frameworks/base/media/所提供的MediaPlayer接口,这些接口都十分简单,我们只需要知道这些接口的具体功能就可以开发出一款功能较为齐全的Music,但是如果遇到一些难以解决的问题的时候就需要深入到native层的实现,从底层的实现来找到问题的根源,所以通过framework层代码可以更加深入得了解整个播放框架的机制。

  • Native Media Player 层:
    对于刚接触MediaPlayer框架的时候最怕的就是阅读这部分代码了,一旦播放流程走到JNI接口,再从JNI接口进入native层,整个请求的数据流和控制流都会变得复杂起来。并且这部分不像应用层那样调式起来方便,有大量的文档可以参阅,所以基本的学习方式只能通过打Log和阅读代码的注释。这部分将会再后续部分进行比较详细的讲解分析。

  • Media Player Service 部分:

从Native层发出的IPC请求将会由Media Player Service 部分进行处理.MediaPlayerService是在frameworks/av/media/mediaserver/main_mediaserver.cpp的main方法中初始化的,在main方法中还启动了多个Android系统服务比如AudioFlinger, CameraService等,实例化Media Player Service 子系统的工作包括MediaPlayerService对象的创建,以及内置底层Media PLayer播放框架工厂的注册,一旦 MediaPlayerService 服务启动,MediaPlayerService将会接受Native MediaPlayer 层的IPC请求,并且为每个操作media内容的请求实例化一个MediaPlayerService::Client对象, Client有一个createPlayer 的方法可以使用特定的工厂类为某个特定的类型创建一个本地media player,后续的发向native层的请求都会交给刚刚提到的native 层的 media palyer来处理,这里的media player指的是StagefrightPlayer或者Nuplayerdriver.但是我们这里先不讨论Nuplayerdriver。

AwesomePlayer是实现播放的底层操作者,它在StagefrightPlayer初始化的时候被创建,它负责将对应的音频视频和对应的解码器对应起来。这里涉及到了MediaExtractor,它会从媒体文件中抽取到有效的头信息。并返回对应的引用。在准备播放的时候AwesomePlayer通过OMXCodec来根据媒体文件类型创建解码器,解码器是驻留在OMX子系统上(OMX是OpenMAX在Android上面的实现),这些解码器主要用于处理内存缓冲,转化成原始数据格式,这部分的实现代码主要在frameworks/av/media/libstagefright/omx 以及frameworks/av/media/libstagefright/codecs 目录下, Stagefright Media Player和 OMX部件是通过IPC方式交互的.
AwesomePlayer最终会处理应用层发出的播放,暂停,停止等请求,这些请求往往和媒体类型有关联对于音频文件.AwesomePlayer 将会创建一个AudioPlayer来对文件进行处理,比如当前文件只有音频部分需要播放,这时候AwesomePlayer将会调用AudioPlayer::start()进行播放,一旦用户提交了其他新的请求AudioPlayer会使用MediaSource对象来和底层的OMX子系统进行交互。
对于视频文件,AwesomePlayer将会触发AwesomeRenderer的视频渲染能力,这也是通过MediaSource/OMXCodec 对象和 OMX子系统进行交互。并且AwesomePlayer负责音频和视频的同步工作,所以AwesomePlayer引入了一个时间队列机制TimedEventQueue.当一个入队的事件到期了,TimedEventQueue将会触发AwesomePlayer对应的回调方法从而保证音频和视频的同步. 这些是在 AwesomePlayer::onVideoEvent()方法中实现的, 在处理完当前帧后将会触发AwesomePlayer::postVideoEvent_l()来安排下一次的处理。整个框架的大体功能就是这样,但是到了细节的时候就复杂了。但是不怕,一点点分析就会看到整个框架的全貌了。一定要有耐心和细心,并且需要看多总结。好了这里就先介绍到这里,我们接下来细节部分放在下一篇博客中介绍。

应用层使用MediaPlayer的方式

在创建MediaPlayer的时候有如下的方式可以选择:

  • 将资源存放在raw目录下
  • 使用在线资源的Uri
  • 使用本地资源的Uri
  • 使用Content Provider

用法总结如下图所示:

MediaPlayer状态机机制

MediaPlayer的状态不是可以任意切换的,它的状态切换受到状态机器的约束:下面是MediaPlayer的状态机的切换图,如果违背了状态机切换规则则会抛出异常这些将会在后续源代码分析的时候进行介绍:

上图中每个椭圆代表一个状态点,共有Idle状态,End状态,Error状态,Initialized 状态,Prepared 状态,Started状态,Paused 状态,Stop 状态,PlaybackCompleted 状态九个状态:

  • Idle状态:MediaPlayer可以通过两种方式进入Idle状态:一种是使用new创建一个,另一种是调用 reset()方法,但是上述两种有细微的差别:在上述两种方式到达Idle状态的时候如果立即调用getCurrentPosition(), getDuration(),setLooping(boolean), setVolume(float, float), pause(), start(), stop(), seekTo(int), prepare() or prepareAsync()这些方法则会产生错误,但是前者不会触发OnErrorListener.onError(),后者则会触发OnErrorListener.onError()。还有个很重要的地方是:当使用new方式创建MediaPlayer实例的时候将处于Idle状态,但是如果使用create方法创建的实例的时候是处于Prepared状态。因此使用使用create方法创建实例后不需要调用prepare()方法。

  • End状态:当MediaPlayer实例调用release()方法后就处于End状态。一旦处于End状态MediaPlayer实例将不能再被使用并且不能再回到其他的状态。

  • Error状态:当出现不支持播放的格式,播放流超时,或者在MediaPlayer实例处于错误状态的时候调用prepare(), prepareAsync()或者setDataSource方法时会触发错误,并进入Error状态。这时候如果通过setOnErrorListener(android.media.MediaPlayer.OnErrorListener).设置了错误监听器则在发生上述错误的时候将会调用OnErrorListener.onError()方法。和End状态不同,处于Error状态的实例可以通过调用reset()回到空闲状态。

  • Initialized 状态:在Idle状态调用setDataSource将会到达Initialized状态。

  • Prepared 状态:在调用start()方法开始播放之前,需要进入Prepared 状态,可以通过两种方式到达这种状态,一种是调用prepare()方法,这种是同步的方法,只有在方法返回的时候才进入Prepared 状态,另一种是调用prepareAsync()使用异步的方式,这种方式是先让MediaPlayer实例进入Prepared状态并返回,然后内部的播放机制继续完成准备工作直到完成。不论哪种方式只要调用setOnPreparedListener方法.在完成后都会调用onPrepared()方法。

  • Started状态:在成功调用start()方法后MediaPlayer对象将会进入这个状态,这时候再调用start()方法将不会有任何影响。可以通过调用isPlaying()方法来查看是否处于Started状态。可以通过setOnBufferingUpdateListener(OnBufferingUpdateListener)注册对缓存区情况的监听。

  • Paused 状态:在Started状态可以调用pause()方法让其进入Paused 状态,在停止状态也可以调用start()方法回到Started状态,也可以调用stop()方法进入Stop状态。需要注意的是从Started状态状态到Paused 状态使用的是异步的方法,当pause()方法返回的时候需要等待一小段时间才能将状态更新到isPlaying()的返回值

  • Stop 状态:当MediaPlayer实例处于Started, Paused, Prepared 或者PlaybackCompleted状态的时候调用stop方法将会进入Stop 状态。在进入stop状态的时候就不能直接start,而必须通过调用prepare() 或者prepareAsync()方法进入Prepared 状态的时候才可以再次进入start状态。

  • PlaybackCompleted 状态:当播放完成的时候如果循环模式设为false,那么在播放结束的时候将会进入PlaybackCompleted 状态,如果这之前已经调用setOnCompletionListener(OnCompletionListener)注册监听器的话,将会调用OnCompletion.onCompletion()回调方法。在PlaybackCompleted 状态的时候可以调用start()方法重新开始播放歌曲。如果循环模式设为true则播放完成将不会进入PlaybackCompleted 状态而是继续留在Started状态。

  • 调整歌曲的播放进度,可以调用seekTo(int)方法来调整歌曲的播放进度,seekTo(int)方法会立刻返回。但是实际的seek操作会等待一会儿才会结束,如果调用了setOnSeekCompleteListener(OnSeekCompleteListener).在seek操作结束后将会调用OnSeekComplete.onSeekComplete()方法。可以在Start,Prepared, Paused 和 PlaybackCompleted 状态调用seekTo(int)方法调整歌曲的播放进度。

音频视频文件帧信息的获取

上面分析的是非音频视频图像文件的扫描流程,在接下来的章节将介绍多媒体文件信息的获取,接下来的部分以MP3格式的音频文件作为分析对象进行介绍:
对于MP3格式大家可以在网上找下,这里就不展开介绍了。

音频视频文件的TAG解析流程分析

在doScanFile中会执行对应的判断,如果当前扫描文件为音频或者视频文件则调用processFile方法进行处理。

public Uri doScanFile(String path, String mimeType, long lastModified,long fileSize, boolean isDirectory, boolean scanAlways, boolean noMedia) {

try {
FileEntry entry = beginFile(path, mimeType, lastModified,
fileSize, isDirectory, noMedia);
//......
if (entry != null && (entry.mLastModifiedChanged || scanAlways)) {
if (noMedia) {
//.........
} else {
//........
// 如果是音频文件或者视频文件调用processFile抽取元数据
// we only extract metadata for audio and video files
if (isaudio || isvideo) {
processFile(path, mimeType, this);
}
//.........
result = endFile(entry, ringtones, notifications, alarms, music, podcasts);
}
}
} catch (RemoteException e) {
//...................
}
return result;
}

在doScanFile调用的processFile是native方法,
因此首先调用android_media_Mediascanner.cpp.中的android_media_MediaScanner_processFile方法,在该方法中就直接调用StageFrightMediaScanner的processFile方法。

static void
android_media_MediaScanner_processFile(
JNIEnv *env, jobject thiz, jstring path,
jstring mimeType, jobject client)
{
MediaScanner *mp = getNativeScanner_l(env, thiz);
//........
//获取MimeType
const char *mimeTypeStr = (mimeType ? env->GetStringUTFChars(mimeType, NULL) : NULL);
//...........
MyMediaScannerClient myClient(env, client);
//调用StageFrightMediaScanner的processFile方法
MediaScanResult result = mp->processFile(pathStr, mimeTypeStr, myClient);
//..........
}

我们先来看下getNativeScanner_l:

static MediaScanner *getNativeScanner_l(JNIEnv* env, jobject thiz) {
return (MediaScanner *) env->GetLongField(thiz, fields.context);
}

它是将fields.context转换为MediaScanner,fields.context这个是怎么来的还记得吧:

static void
android_media_MediaScanner_native_setup(JNIEnv *env, jobject thiz)
{
ALOGV("native_setup");
MediaScanner *mp = new StagefrightMediaScanner;
//..............
env->SetLongField(thiz, fields.context, (jlong)mp);
}

所以这里的getNativeScanner_l返回的是StagefrightMediaScanner
而在StagefrightMediaScanner的processFile方法中则直接调用MediaScannerClient类中的beginFile以及endFile方法还有StagefrightMediaScanner 的processFileInternal方法。

MediaScanResult StagefrightMediaScanner::processFile(
const char *path, const char *mimeType,
MediaScannerClient &client) {
ALOGV("processFile '%s'.", path);

client.setLocale(locale());
//client = MyMediaScannerClient
client.beginFile();
MediaScanResult result = processFileInternal(path, mimeType, client);
client.endFile();
return result;
}

我们先来看下MyMediaScannerClient::beginFile

public FileEntry beginFile(String path, String mimeType, long lastModified,
long fileSize, boolean isDirectory, boolean noMedia) {
mMimeType = mimeType;
mFileType = 0;
mFileSize = fileSize;
mIsDrm = false;

if (!isDirectory) {
if (!noMedia && isNoMediaFile(path)) {
noMedia = true;
}
mNoMedia = noMedia;

// try mimeType first, if it is specified
if (mimeType != null) {
mFileType = MediaFile.getFileTypeForMimeType(mimeType);
}

// if mimeType was not specified, compute file type based on file extension.
if (mFileType == 0) {
MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
if (mediaFileType != null) {
mFileType = mediaFileType.fileType;
if (mMimeType == null) {
mMimeType = mediaFileType.mimeType;
}
}
}

if (isDrmEnabled() && MediaFile.isDrmFileType(mFileType)) {
mFileType = getFileTypeFromDrm(path);
}
}

FileEntry entry = makeEntryFor(path);
// add some slack to avoid a rounding error
long delta = (entry != null) ? (lastModified - entry.mLastModified) : 0;
boolean wasModified = delta > 1 || delta < -1;
if (entry == null || wasModified) {
if (wasModified) {
entry.mLastModified = lastModified;
} else {
entry = new FileEntry(0, path, lastModified,
(isDirectory ? MtpConstants.FORMAT_ASSOCIATION : 0));
}
entry.mLastModifiedChanged = true;
}

if (mProcessPlaylists && MediaFile.isPlayListFileType(mFileType)) {
mPlayLists.add(entry);
// we don't process playlists in the main scan, so return null
return null;
}

// clear all the metadata
mArtist = null;
mAlbumArtist = null;
mAlbum = null;
mTitle = null;
mComposer = null;
mGenre = null;
mTrack = 0;
mYear = 0;
mDuration = 0;
mPath = path;
mLastModified = lastModified;
mWriter = null;
mCompilation = 0;
mWidth = 0;
mHeight = 0;

return entry;
}

从processFileInternal方法开始就开始获取音频视频内部的数据了,在processFileInternal中对文件的处理分如下几个部分:

  • 获取当前音频视频文件的扩展名,看是否是平台所支持的文件类型,如果为空或者不支持就返回MEDIA_SCAN_RESULT_SKIPPED。
  • 根据文件扩展名判断当前扫描的文件是否是DRM文件,如果是的话则开始获取DRM的信息。DRM信息是通过调用drmManagerClient->getMetadata(&tmp)方法来获取的,获取到的信息封装在DrmMetadata对象中。这部分将放在今后的DRM部分介绍。
  • 获取音频视频的TAG信息:
    这部分最重要的是如下两个方法:
    StagefrightMetadataRetriever ::setDataSource
    StagefrightMetadataRetriever ::extractMetadata

这部分代码是比较重要的所以我们将对下面的方法进行详细介绍:

MediaScanResult StagefrightMediaScanner::processFileInternal(
const char *path, const char * /* mimeType */,
MediaScannerClient &client) {

//判断当前文件是否有扩展名
const char *extension = strrchr(path, '.');
if (!extension) {
return MEDIA_SCAN_RESULT_SKIPPED;
}
//判断当前扩展名十分支持
if (!FileHasAcceptableExtension(extension)) {
return MEDIA_SCAN_RESULT_SKIPPED;
}
//创建媒体抽取器
sp<MediaMetadataRetriever> mRetriever(new MediaMetadataRetriever);
//打开文件
int fd = open(path, O_RDONLY | O_LARGEFILE);
status_t status;
if (fd < 0) {
// couldn't open it locally, maybe the media server can?
//打不开尝试从网络上获取
status = mRetriever->setDataSource(NULL /* httpService */, path);
} else {
//设置数据源
status = mRetriever->setDataSource(fd, 0, 0x7ffffffffffffffL);
close(fd);
}

//抽取媒体头数据
const char *value;
if ((value = mRetriever->extractMetadata(METADATA_KEY_MIMETYPE)) != NULL) {
status = client.setMimeType(value);
if (status) {
return MEDIA_SCAN_RESULT_ERROR;
}
}

struct KeyMap {
const char *tag;
int key;
};
static const KeyMap kKeyMap[] = {
{ "tracknumber", METADATA_KEY_CD_TRACK_NUMBER },
{ "discnumber", METADATA_KEY_DISC_NUMBER },
{ "album", METADATA_KEY_ALBUM },
{ "artist", METADATA_KEY_ARTIST },
{ "albumartist", METADATA_KEY_ALBUMARTIST },
{ "composer", METADATA_KEY_COMPOSER },
{ "genre", METADATA_KEY_GENRE },
{ "title", METADATA_KEY_TITLE },
{ "year", METADATA_KEY_YEAR },
{ "duration", METADATA_KEY_DURATION },
{ "writer", METADATA_KEY_WRITER },
{ "compilation", METADATA_KEY_COMPILATION },
{ "isdrm", METADATA_KEY_IS_DRM },
{ "width", METADATA_KEY_VIDEO_WIDTH },
{ "height", METADATA_KEY_VIDEO_HEIGHT },
};
static const size_t kNumEntries = sizeof(kKeyMap) / sizeof(kKeyMap[0]);
//按照kKeyMap表格的顺序从媒体文件中获取TAG并添加到client中
for (size_t i = 0; i < kNumEntries; ++i) {
const char *value;
if ((value = mRetriever->extractMetadata(kKeyMap[i].key)) != NULL) {
status = client.addStringTag(kKeyMap[i].tag, value);
if (status != OK) {
return MEDIA_SCAN_RESULT_ERROR;
}
}
}
return MEDIA_SCAN_RESULT_OK;
}

我们先来看下能够支持的媒体格式:

static bool FileHasAcceptableExtension(const char *extension) {
static const char *kValidExtensions[] = {
".mp3", ".mp4", ".m4a", ".3gp", ".3gpp", ".3g2", ".3gpp2",
".mpeg", ".ogg", ".mid", ".smf", ".imy", ".wma", ".aac",
".wav", ".amr", ".midi", ".xmf", ".rtttl", ".rtx", ".ota",
".mkv", ".mka", ".webm", ".ts", ".fl", ".flac", ".mxmf",
".avi", ".mpeg", ".mpg", ".awb", ".mpga"
};
static const size_t kNumValidExtensions =
sizeof(kValidExtensions) / sizeof(kValidExtensions[0]);

for (size_t i = 0; i < kNumValidExtensions; ++i) {
if (!strcasecmp(extension, kValidExtensions[i])) {
return true;
}
}
return false;
}

我们看下setDataSource,它首先会创建一个FileSource。

public native void setDataSource(FileDescriptor fd, long offset, long length)
throws IllegalArgumentException;
}

在setDataSource中我们创建一个FileSource并将其传递到MediaExtractor::Create中根据FileSource来创建出合适的MediaExtractor

status_t StagefrightMetadataRetriever::setDataSource(int fd, int64_t offset, int64_t length) {
fd = dup(fd);
ALOGV("setDataSource(%d, %" PRId64 ", %" PRId64 ")", fd, offset, length);
clearMetadata();
mSource = new FileSource(fd, offset, length);
status_t err;
//检查是否打开文件成功
if ((err = mSource->initCheck()) != OK) {
mSource.clear();
return err;
}
//将FileSource传入后根据FileSource的信息创建出合适的MediaExtractor
mExtractor = MediaExtractor::Create(mSource);
if (mExtractor == NULL) {
mSource.clear();
return UNKNOWN_ERROR;
}
return OK;
}

我们接下来看下是如何创建对应的MediaExtractor的,首先会先调用DataSource的sniff,在DataSource的sniff中会调用每个注册的Sniffers对其进行探测,来选出最匹配的。以mimetype形式返回。根据返回的mimetype创建MediaExtractor

// static
sp<MediaExtractor> MediaExtractor::Create(const sp<DataSource> &source, const char *mime) {
sp<AMessage> meta;
String8 tmp;
if (mime == NULL) {
float confidence;
//首先我们会调用DataSource的sniff
if (!source->sniff(&tmp, &confidence, &meta)) {
ALOGV("FAILED to autodetect media content.");
return NULL;
}

mime = tmp.string();
ALOGV("Autodetected media content as '%s' with confidence %.2f",
mime, confidence);
}

bool isDrm = false;
// DRM MIME type syntax is "drm+type+original" where
// type is "es_based" or "container_based" and
// original is the content's cleartext MIME type
if (!strncmp(mime, "drm+", 4)) {
const char *originalMime = strchr(mime+4, '+');
if (originalMime == NULL) {
// second + not found
return NULL;
}
++originalMime;
if (!strncmp(mime, "drm+es_based+", 13)) {
// DRMExtractor sets container metadata kKeyIsDRM to 1
return new DRMExtractor(source, originalMime);
} else if (!strncmp(mime, "drm+container_based+", 20)) {
mime = originalMime;
isDrm = true;
} else {
return NULL;
}
}

MediaExtractor *ret = NULL;
if (!strcasecmp(mime, MEDIA_MIMETYPE_CONTAINER_MPEG4)
|| !strcasecmp(mime, "audio/mp4")) {
ret = new MPEG4Extractor(source);
} else if (!strcasecmp(mime, MEDIA_MIMETYPE_AUDIO_MPEG)) {
ret = new MP3Extractor(source, meta);
} else if (!strcasecmp(mime, MEDIA_MIMETYPE_AUDIO_AMR_NB)
|| !strcasecmp(mime, MEDIA_MIMETYPE_AUDIO_AMR_WB)) {
ret = new AMRExtractor(source);
} else if (!strcasecmp(mime, MEDIA_MIMETYPE_AUDIO_FLAC)) {
ret = new FLACExtractor(source);
} else if (!strcasecmp(mime, MEDIA_MIMETYPE_CONTAINER_WAV)) {
ret = new WAVExtractor(source);
} else if (!strcasecmp(mime, MEDIA_MIMETYPE_CONTAINER_OGG)) {
ret = new OggExtractor(source);
} else if (!strcasecmp(mime, MEDIA_MIMETYPE_CONTAINER_MATROSKA)) {
ret = new MatroskaExtractor(source);
} else if (!strcasecmp(mime, MEDIA_MIMETYPE_CONTAINER_MPEG2TS)) {
ret = new MPEG2TSExtractor(source);
} else if (!strcasecmp(mime, MEDIA_MIMETYPE_CONTAINER_WVM)) {
// Return now. WVExtractor should not have the DrmFlag set in the block below.
return new WVMExtractor(source);
} else if (!strcasecmp(mime, MEDIA_MIMETYPE_AUDIO_AAC_ADTS)) {
ret = new AACExtractor(source, meta);
} else if (!strcasecmp(mime, MEDIA_MIMETYPE_CONTAINER_MPEG2PS)) {
ret = new MPEG2PSExtractor(source);
} else if (!strcasecmp(mime, MEDIA_MIMETYPE_AUDIO_MIDI)) {
ret = new MidiExtractor(source);
}
if (ret != NULL) {
if (isDrm) {
ret->setDrmFlag(true);
} else {
ret->setDrmFlag(false);
}
}
return ret;
}

下面是sniff方法。

bool DataSource::sniff(String8 *mimeType, float *confidence, sp<AMessage> *meta) {
*mimeType = "";
*confidence = 0.0f;
meta->clear();
{
Mutex::Autolock autoLock(gSnifferMutex);
if (!gSniffersRegistered) {
return false;
}
}
for (List<SnifferFunc>::iterator it = gSniffers.begin();
it != gSniffers.end(); ++it) {
String8 newMimeType;
float newConfidence;
sp<AMessage> newMeta;
//调用各种嗅探器的方法获取类型和可信度
if ((*it)(this, &newMimeType, &newConfidence, &newMeta)) {
if (newConfidence > *confidence) {
*mimeType = newMimeType;
*confidence = newConfidence;
*meta = newMeta;
}
}
}
return *confidence > 0.0;
}

这些sniff是在StagefrightMetadataRetriever创建的时候注册的:

StagefrightMetadataRetriever::StagefrightMetadataRetriever()
: mParsedMetaData(false),
mAlbumArt(NULL) {
//注册默认的音频格式嗅探器
DataSource::RegisterDefaultSniffers();
CHECK_EQ(mClient.connect(), (status_t)OK);
}

下面是注册的sniff

//static
void DataSource::RegisterDefaultSniffers() {
Mutex::Autolock autoLock(gSnifferMutex);
if (gSniffersRegistered) {
return;
}
RegisterSniffer_l(SniffMPEG4);
RegisterSniffer_l(SniffMatroska);
RegisterSniffer_l(SniffOgg);
RegisterSniffer_l(SniffWAV);
RegisterSniffer_l(SniffFLAC);
RegisterSniffer_l(SniffAMR);
RegisterSniffer_l(SniffMPEG2TS);
RegisterSniffer_l(SniffMP3);
RegisterSniffer_l(SniffAAC);
RegisterSniffer_l(SniffMPEG2PS);
RegisterSniffer_l(SniffWVM);
RegisterSniffer_l(SniffMidi);

char value[PROPERTY_VALUE_MAX];
if (property_get("drm.service.enabled", value, NULL)
&& (!strcmp(value, "1") || !strcasecmp(value, "true"))) {
RegisterSniffer_l(SniffDRM);
}
gSniffersRegistered = true;
}

那么sniff又是怎样完成文件类型判断的任务的呢?我们看到上面的RegisterDefaultSniffers我们以MP3为例子,那么使用的sniff为SniffMP3
这里我们只关注MP3的sniff–SniffMP3。SniffMP3首先会调用Resync方法对音乐内容帧的帧头进行重新重定位,它会在音乐内容帧内对帧头进行搜索:每次读入1024字节的内容到待检测缓冲区,再从缓冲区中每次读取4个字节32位进行匹配,如果找到匹配的话还会读入后续的连续3个帧的帧头数据进行检测。如果4次检测均成功的话将当前位置作为音乐内容帧的起始位置返回,检测过程是通过GetMPEGAudioFrameSize方法来完成的,如果当前待检测缓冲区内数据没有匹配的则再次读入数据到缓冲区直到检测的位置达到最大检测字节数128*1024字节为止。

bool SniffMP3(const sp<DataSource> &source, String8 *mimeType,
float *confidence, sp<AMessage> *meta) {
off64_t pos = 0;
off64_t post_id3_pos;
uint32_t header;
if (!Resync(source, 0, &pos, &post_id3_pos, &header)) {
return false;
}

*meta = new AMessage;
(*meta)->setInt64("offset", pos);
(*meta)->setInt32("header", header);
(*meta)->setInt64("post-id3-offset", post_id3_pos);

*mimeType = MEDIA_MIMETYPE_AUDIO_MPEG;
*confidence = 0.2f;

return true;
}

static bool Resync(
const sp<DataSource> &source, uint32_t match_header,
off64_t *inout_pos, off64_t *post_id3_pos, uint32_t *out_header) {
if (post_id3_pos != NULL) {
*post_id3_pos = 0;
}
//跳过标题帧的位置开始同步。
if (*inout_pos == 0) {
// Skip an optional ID3 header if syncing at the very beginning
// of the datasource.

for (;;) {
uint8_t id3header[10];
//读取头10个字节,如果不够10个字节表示存在错误返回false
if (source->readAt(*inout_pos, id3header, sizeof(id3header))
< (ssize_t)sizeof(id3header)) {
// If we can't even read these 10 bytes, we might as well bail
// out, even if there _were_ 10 bytes of valid mp3 audio data...
return false;
}
//如果头3个字节不是ID3退出该循环
if (memcmp("ID3", id3header, 3)) {
break;
}

// Skip the ID3v2 header.
// 获取标签帧的长度
size_t len =
((id3header[6] & 0x7f) << 21)
| ((id3header[7] & 0x7f) << 14)
| ((id3header[8] & 0x7f) << 7)
| (id3header[9] & 0x7f);

len += 10;

*inout_pos += len; // inout_pos 指向音乐内容区的首地址

ALOGV("skipped ID3 tag, new starting offset is %lld (0x%016llx)",
(long long)*inout_pos, (long long)*inout_pos);
}

if (post_id3_pos != NULL) {
*post_id3_pos = *inout_pos;
}
}

off64_t pos = *inout_pos;
bool valid = false;

const size_t kMaxReadBytes = 1024;
const size_t kMaxBytesChecked = 128 * 1024;
uint8_t buf[kMaxReadBytes];
ssize_t bytesToRead = kMaxReadBytes;
ssize_t totalBytesRead = 0;
ssize_t remainingBytes = 0;
bool reachEOS = false;
uint8_t *tmp = buf;

do {
//如果当前的扫描位置超过音乐内容区的kMaxBytesChecked(128K)就停止扫描
if (pos >= (off64_t)(*inout_pos + kMaxBytesChecked)) {
// Don't scan forever.
ALOGV("giving up at offset %lld", (long long)pos);
break;
}
//加载一个缓冲区的内容。
if (remainingBytes < 4) {
if (reachEOS) {
break;
} else {
memcpy(buf, tmp, remainingBytes);
bytesToRead = kMaxReadBytes - remainingBytes;

/*
* The next read position should start from the end of
* the last buffer, and thus should include the remaining
* bytes in the buffer.
*/
totalBytesRead = source->readAt(pos + remainingBytes,
buf + remainingBytes,
bytesToRead);
if (totalBytesRead <= 0) {
break;
}
reachEOS = (totalBytesRead != bytesToRead);
totalBytesRead += remainingBytes;
remainingBytes = totalBytesRead;
tmp = buf;
continue;
}
}
//从tmp缓冲区中取出头
uint32_t header = U32_AT(tmp);

if (match_header != 0 && (header & kMask) != (match_header & kMask)) {
//如果帧的格式校验失败则从当前其实位置下一个位置开始获取32字节
++pos;
++tmp;
--remainingBytes;
continue;
}

size_t frame_size;
int sample_rate, num_channels, bitrate;
if (!GetMPEGAudioFrameSize(
header, &frame_size,
&sample_rate, &num_channels, &bitrate)) {
++pos;
++tmp;
--remainingBytes;
continue;
}

ALOGV("found possible 1st frame at %lld (header = 0x%08x)", (long long)pos, header);

// We found what looks like a valid frame,
// now find its successors.

off64_t test_pos = pos + frame_size;

valid = true;
//再连续读取3三个音乐内容区的帧头进行检查,如果都正确就退出检测,将找到的音乐内容区域的位置以及音乐内容帧的帧头返回。
for (int j = 0; j < 3; ++j) {
uint8_t tmp[4];
if (source->readAt(test_pos, tmp, 4) < 4) {
valid = false;
break;
}

uint32_t test_header = U32_AT(tmp);

ALOGV("subsequent header is %08x", test_header);

if ((test_header & kMask) != (header & kMask)) {
valid = false;
break;
}

size_t test_frame_size;
if (!GetMPEGAudioFrameSize(
test_header, &test_frame_size)) {
valid = false;
break;
}

ALOGV("found subsequent frame #%d at %lld", j + 2, (long long)test_pos);

test_pos += test_frame_size;
}

if (valid) {
*inout_pos = pos;

if (out_header != NULL) {
*out_header = header;
}
} else {
ALOGV("no dice, no valid sequence of frames found.");
}

++pos;
++tmp;
--remainingBytes;
} while (!valid);

return valid;
}

在MediaExtractor::Create 方法中我们通过sniff来识别当前音频的文件类型,并根据返回的mimeType以及可行度来创建具体的Extractor ,我们这里以MP3文件为例,根据sniff判断后在MediaExtractor::Create中将会创建MP3Extractor对象,在MP3Extractor构造方法中,主要是从传入的header中获取采样频率,通道数,帧大小等数据,并将其存入MetaData对象中。

MP3Extractor::MP3Extractor(
const sp<DataSource> &source, const sp<AMessage> &meta)
: mInitCheck(NO_INIT),
mDataSource(source),
mFirstFramePos(-1),
mFixedHeader(0) {
off64_t pos = 0;
off64_t post_id3_pos;
uint32_t header;
bool success;

//下面这些值在SniffMP3中已经获取了
int64_t meta_offset;/*表示帧数据的偏移*/
uint32_t meta_header;/*表示帧头*/
int64_t meta_post_id3_offset;/*表示TAB帧的偏移*/
if (meta != NULL&& meta->findInt64("offset", &meta_offset)
&& meta->findInt32("header", (int32_t *)&meta_header)
&& meta->findInt64("post-id3-offset", &meta_post_id3_offset)) {
// The sniffer has already done all the hard work for us, simply
// accept its judgement.
pos = (off64_t)meta_offset;
header = meta_header;
post_id3_pos = (off64_t)meta_post_id3_offset;
success = true;
} else {
}

mFirstFramePos = pos;
mFixedHeader = header;
mMeta = new MetaData;

size_t frame_size;
int sample_rate;
int num_channels;
int bitrate;
GetMPEGAudioFrameSize(header, &frame_size, &sample_rate, &num_channels, &bitrate);

unsigned layer = 4 - ((header >> 17) & 3);

switch (layer) {
case 1:
mMeta->setCString(kKeyMIMEType, MEDIA_MIMETYPE_AUDIO_MPEG_LAYER_I);
break;
case 2:
mMeta->setCString(kKeyMIMEType, MEDIA_MIMETYPE_AUDIO_MPEG_LAYER_II);
break;
case 3:
mMeta->setCString(kKeyMIMEType, MEDIA_MIMETYPE_AUDIO_MPEG);
break;
default:
TRESPASS();
}

mMeta->setInt32(kKeySampleRate, sample_rate);
mMeta->setInt32(kKeyBitRate, bitrate * 1000);
mMeta->setInt32(kKeyChannelCount, num_channels);

int64_t durationUs;

if (durationUs >= 0) {
mMeta->setInt64(kKeyDuration, durationUs);
}

mInitCheck = OK;

// Get iTunes-style gapless info if present.
// When getting the id3 tag, skip the V1 tags to prevent the source cache
// from being iterated to the end of the file.
ID3 id3(mDataSource, true);
if (id3.isValid()) {
ID3::Iterator *com = new ID3::Iterator(id3, "COM");
if (com->done()) {
delete com;
com = new ID3::Iterator(id3, "COMM");
}
while(!com->done()) {
String8 commentdesc;
String8 commentvalue;
com->getString(&commentdesc, &commentvalue);
const char * desc = commentdesc.string();
const char * value = commentvalue.string();

// first 3 characters are the language, which we don't care about
if(strlen(desc) > 3 && strcmp(desc + 3, "iTunSMPB") == 0) {
int32_t delay, padding;
if (sscanf(value, " %*x %x %x %*x", &delay, &padding) == 2) {
mMeta->setInt32(kKeyEncoderDelay, delay);
mMeta->setInt32(kKeyEncoderPadding, padding);
}
break;
}
com->next();
}
delete com;
com = NULL;
}
}

到目前为止我们已经识别出了当前扫描音频文件的格式类型,并从对应的音乐内容数据帧头部(HEAD)获取到了采样率,比特率等信息,但是我们还有一部分非常重要的信息需要获取,那就是标签帧的信息,在那里记录者歌曲作者,专辑名,歌曲名,甚至专辑封面图片和内嵌歌词等信息,下面部分我们就重点介绍这些信息的获取过程。
我们再次回到processFileInternal方法,Tags文件的获取是在mRetriever->extractMetadata(kKeyMap[i].key)中完成的,
在extractMetadata方法传入的参数为要寻找的那个Tag的key,如果当前尚未对帧标签帧进行解析则先调用parseMetaData方法对Tag标签进行解析,如果已经解析过了则这时候就使用传人到keyCode 到mMetaData中进行查找,并返回的需要查找到那个Tag的值.

const char *StagefrightMetadataRetriever::extractMetadata(int keyCode) {
if (mExtractor == NULL) {
return NULL;
}
//调用parseMetaData()解析标签帧
if (!mParsedMetaData) {
parseMetaData();
mParsedMetaData = true;
}
ssize_t index = mMetaData.indexOfKey(keyCode);
if (index < 0) {
return NULL;
}
/将数据添加到mMetaData
return mMetaData.valueAt(index).string();
}
void StagefrightMetadataRetriever::parseMetaData() {
//获取MetaData
sp<MetaData> meta = mExtractor->getMetaData();

//TAG对应的map
struct Map {
int from;
int to;
const char *name;
};
static const Map kMap[] = {
{ kKeyMIMEType, METADATA_KEY_MIMETYPE, NULL },
{ kKeyCDTrackNumber, METADATA_KEY_CD_TRACK_NUMBER, "tracknumber" },
{ kKeyDiscNumber, METADATA_KEY_DISC_NUMBER, "discnumber" },
{ kKeyAlbum, METADATA_KEY_ALBUM, "album" },
{ kKeyArtist, METADATA_KEY_ARTIST, "artist" },
{ kKeyAlbumArtist, METADATA_KEY_ALBUMARTIST, "albumartist" },
{ kKeyAuthor, METADATA_KEY_AUTHOR, NULL },
{ kKeyComposer, METADATA_KEY_COMPOSER, "composer" },
{ kKeyDate, METADATA_KEY_DATE, NULL },
{ kKeyGenre, METADATA_KEY_GENRE, "genre" },
{ kKeyTitle, METADATA_KEY_TITLE, "title" },
{ kKeyYear, METADATA_KEY_YEAR, "year" },
{ kKeyWriter, METADATA_KEY_WRITER, "writer" },
{ kKeyCompilation, METADATA_KEY_COMPILATION, "compilation" },
{ kKeyLocation, METADATA_KEY_LOCATION, NULL },
};
static const size_t kNumMapEntries = sizeof(kMap) / sizeof(kMap[0]);
//创建判断字符编码检测器
CharacterEncodingDetector *detector = new CharacterEncodingDetector();
//将对应的TAG添加到detector中
for (size_t i = 0; i < kNumMapEntries; ++i) {
const char *value;
if (meta->findCString(kMap[i].from, &value)) {
if (kMap[i].name) {
// add to charset detector
//这里没有直接添加到mMetaData而是先添加到CharacterEncodingDetector的mNames,mValues中等待转换编码
detector->addTag(kMap[i].name, value);
} else {
// directly add to output list
mMetaData.add(kMap[i].to, String8(value));
}
}
}
/*
void CharacterEncodingDetector::addTag(const char *name, const char *value) {
mNames.push_back(name);
mValues.push_back(value);
}
×/
//开始转化编码,这部分放在后面进行介绍
detector->detectAndConvert();
int size = detector->size();
if (size) {
//将转换后的TAG放到mMetaData,这时候整个mMetaData存放的都是正确编码后的内容
for (int i = 0; i < size; i++) {
const char *name;
const char *value;
detector->getTag(i, &name, &value);
for (size_t j = 0; j < kNumMapEntries; ++j) {
if (kMap[j].name && !strcmp(kMap[j].name, name)) {
mMetaData.add(kMap[j].to, String8(value));
}
}
}
}
delete detector;

const void *data;
uint32_t type;
size_t dataSize;
if (meta->findData(kKeyAlbumArt, &type, &data, &dataSize)&& mAlbumArt == NULL) {
mAlbumArt = MediaAlbumArt::fromData(dataSize, data);
}

size_t numTracks = mExtractor->countTracks();

char tmp[32];
sprintf(tmp, "%zu", numTracks);

mMetaData.add(METADATA_KEY_NUM_TRACKS, String8(tmp));

float captureFps;
if (meta->findFloat(kKeyCaptureFramerate, &captureFps)) {
sprintf(tmp, "%f", captureFps);
mMetaData.add(METADATA_KEY_CAPTURE_FRAMERATE, String8(tmp));
}

bool hasAudio = false;
bool hasVideo = false;
int32_t videoWidth = -1;
int32_t videoHeight = -1;
int32_t audioBitrate = -1;
int32_t rotationAngle = -1;

// The overall duration is the duration of the longest track.
int64_t maxDurationUs = 0;
String8 timedTextLang;
for (size_t i = 0; i < numTracks; ++i) {
sp<MetaData> trackMeta = mExtractor->getTrackMetaData(i);
int64_t durationUs;
if (trackMeta->findInt64(kKeyDuration, &durationUs)) {
if (durationUs > maxDurationUs) {
maxDurationUs = durationUs;
}
}
const char *mime;
if (trackMeta->findCString(kKeyMIMEType, &mime)) {
if (!hasAudio && !strncasecmp("audio/", mime, 6)) {
hasAudio = true;

if (!trackMeta->findInt32(kKeyBitRate, &audioBitrate)) {
audioBitrate = -1;
}
} else if (!hasVideo && !strncasecmp("video/", mime, 6)) {
hasVideo = true;
CHECK(trackMeta->findInt32(kKeyWidth, &videoWidth));
CHECK(trackMeta->findInt32(kKeyHeight, &videoHeight));
if (!trackMeta->findInt32(kKeyRotation, &rotationAngle)) {
rotationAngle = 0;
}
} else if (!strcasecmp(mime, MEDIA_MIMETYPE_TEXT_3GPP)) {
const char *lang;
trackMeta->findCString(kKeyMediaLanguage, &lang);
timedTextLang.append(String8(lang));
timedTextLang.append(String8(":"));
}
}
}

// To save the language codes for all timed text tracks
// If multiple text tracks present, the format will look
// like "eng:chi"
if (!timedTextLang.isEmpty()) {
mMetaData.add(METADATA_KEY_TIMED_TEXT_LANGUAGES, timedTextLang);
}

// The duration value is a string representing the duration in ms.
sprintf(tmp, "%" PRId64, (maxDurationUs + 500) / 1000);
mMetaData.add(METADATA_KEY_DURATION, String8(tmp));

if (hasAudio) {
mMetaData.add(METADATA_KEY_HAS_AUDIO, String8("yes"));
}

if (hasVideo) {
mMetaData.add(METADATA_KEY_HAS_VIDEO, String8("yes"));

sprintf(tmp, "%d", videoWidth);
mMetaData.add(METADATA_KEY_VIDEO_WIDTH, String8(tmp));

sprintf(tmp, "%d", videoHeight);
mMetaData.add(METADATA_KEY_VIDEO_HEIGHT, String8(tmp));

sprintf(tmp, "%d", rotationAngle);
mMetaData.add(METADATA_KEY_VIDEO_ROTATION, String8(tmp));
}

if (numTracks == 1 && hasAudio && audioBitrate >= 0) {
sprintf(tmp, "%d", audioBitrate);
mMetaData.add(METADATA_KEY_BITRATE, String8(tmp));
} else {
off64_t sourceSize;
if (mSource->getSize(&sourceSize) == OK) {
int64_t avgBitRate = (int64_t)(sourceSize * 8E6 / maxDurationUs);

sprintf(tmp, "%" PRId64, avgBitRate);
mMetaData.add(METADATA_KEY_BITRATE, String8(tmp));
}
}

if (numTracks == 1) {
const char *fileMIME;
CHECK(meta->findCString(kKeyMIMEType, &fileMIME));

if (!strcasecmp(fileMIME, "video/x-matroska")) {
sp<MetaData> trackMeta = mExtractor->getTrackMetaData(0);
const char *trackMIME;
CHECK(trackMeta->findCString(kKeyMIMEType, &trackMIME));

if (!strncasecmp("audio/", trackMIME, 6)) {
// The matroska file only contains a single audio track,
// rewrite its mime type.
mMetaData.add(
METADATA_KEY_MIMETYPE, String8("audio/x-matroska"));
}
}
}

// To check whether the media file is drm-protected
if (mExtractor->getDrmFlag()) {
mMetaData.add(METADATA_KEY_IS_DRM, String8("1"));
}
}

从上面代码中可以看出完成关键工作就是从Meta中取出各个TAG,经过字符编码转换后添加到mMetaData中。

我们看到这里不要忽略了getMetaData,getMetaData方法中通过创建用于对存储在文件头的ID3V2标签帧和存储在文件尾部的ID3V1帧进行解析的ID3对象来完成标签帧到解析,这里解析的内容包括基本的歌曲信息标签以及歌词,专辑封面等信息。想要真正了解整个解析过程我们还得继续看下ID3类的构造方法和ID3::Iterator方法。

sp<MetaData> MP3Extractor::getMetaData() {
sp<MetaData> meta = new MetaData;

if (mInitCheck != OK) {
return meta;
}
//设置mimetype
meta->setCString(kKeyMIMEType, "audio/mpeg");
//创建id3
ID3 id3(mDataSource);

if (!id3.isValid()) {
return meta;
}

struct Map {
int key;
const char *tag1;
const char *tag2;
};
static const Map kMap[] = {
{ kKeyAlbum, "TALB", "TAL" },
{ kKeyArtist, "TPE1", "TP1" },
{ kKeyAlbumArtist, "TPE2", "TP2" },
{ kKeyComposer, "TCOM", "TCM" },
{ kKeyGenre, "TCON", "TCO" },
{ kKeyTitle, "TIT2", "TT2" },
{ kKeyYear, "TYE", "TYER" },
{ kKeyAuthor, "TXT", "TEXT" },
{ kKeyCDTrackNumber, "TRK", "TRCK" },
{ kKeyDiscNumber, "TPA", "TPOS" },
{ kKeyCompilation, "TCP", "TCMP" },
};
static const size_t kNumMapEntries = sizeof(kMap) / sizeof(kMap[0]);
//获取上面提到的TAG
for (size_t i = 0; i < kNumMapEntries; ++i) {
ID3::Iterator *it = new ID3::Iterator(id3, kMap[i].tag1);
if (it->done()) {
delete it;
it = new ID3::Iterator(id3, kMap[i].tag2);
}
if (it->done()) {
delete it;
continue;
}
String8 s;
it->getString(&s);
delete it;
meta->setCString(kMap[i].key, s);
}

size_t dataSize;
String8 mime;
//获取专辑封面
const void *data = id3.getAlbumArt(&dataSize, &mime);
if (data) {
meta->setData(kKeyAlbumArt, MetaData::TYPE_NONE, data, dataSize);
meta->setCString(kKeyAlbumArtMIME, mime.string());
}
return meta;
}

我们先看下ID3的构造方法,在ID3方法中,会先调用parseV2,如果parseV2返回false的话会调用parseV1,也就是说ID3会首先解析位于文件头到ID3V2标签帧,如果解析失败则会尝试解析ID3V1帧。

ID3::ID3(const sp<DataSource> &source, bool ignoreV1, off64_t offset)
: mIsValid(false),
mData(NULL),
mSize(0),
mFirstFrameOffset(0),
mVersion(ID3_UNKNOWN),
mRawSize(0) {
//解析ID3V2Tab内容
mIsValid = parseV2(source, offset);

if (!mIsValid && !ignoreV1) {
//解析ID3V1 TAB 内容
mIsValid = parseV1(source);
}
}

而在parseV2以及parseV1方法中实际上也还没开始解析,它们只是开辟个mData 空间将对应的TAG标签加载到mData中,后续的解析工作将会针对mData中的这些数据进行解析。

bool ID3::parseV2(const sp<DataSource> &source) {
struct id3_header {
//标签头10个字节
char id[3]; /*必须为"ID3"否则认为标签不存在*/
uint8_t version_major; /*版本号ID3V2.3就记录3*/
uint8_t version_minor; /*副版本号此版本记录为0*/
uint8_t flags; /*存放标志的字节,这个版本只定义了三位*/
uint8_t enc_size[4]; /*标签大小,不包括标签头的10个字节*/
};
id3_header header;
if (source->readAt(0, &header, sizeof(header)) != (ssize_t)sizeof(header)) {
return false;
}
/*必须为"ID3"否则认为标签不存在*/
if (memcmp(header.id, "ID3", 3)) {
return false;
}
//如果主版本号和副版本号都是0xff则表示错误
if (header.version_major == 0xff || header.version_minor == 0xff) {
return false;
}
if (header.version_major == 2) {
//如果主版本号为2
//............................................
} else if (header.version_major == 3) {
//如果主版本号为3
if (header.flags & 0x1f) {
// We only support the 3 high bits, if any of the lower bits are
// set, we cannot guarantee to understand the tag format.
return false;
}
} else if (header.version_major == 4) {
//如果主版本号为4
} else {
//如果主版本号不是上述的则直接返回false
return false;
}
//获取标签大小
size_t size;
if (!ParseSyncsafeInteger(header.enc_size, &size)) {
return false;
}
//static const size_t kMaxMetadataSize = 3 * 1024 * 1024;
//如果大小超过3M则退出
if (size > kMaxMetadataSize) {
return false;
}
//分配用于存放标签内容的空间
mData = (uint8_t *)malloc(size);
mSize = size;
mRawSize = mSize + sizeof(header);
//mRawSize 表示 标签帧大小加上 标签头大小10个字节
//读取标签帧数据
if (source->readAt(sizeof(header), mData, mSize) != (ssize_t)mSize) {
free(mData);
mData = NULL ;
return false;
}
//..........................................................
mFirstFrameOffset = 0;
//..........................................................
if (header.version_major == 2) {
mVersion = ID3_V2_2;
} else if (header.version_major == 3) {
mVersion = ID3_V2_3;
} else {
CHECK_EQ(header.version_major, 4);
mVersion = ID3_V2_4;
}
return true;
}
bool ID3::parseV1(const sp<DataSource> &source) {
const size_t V1_TAG_SIZE = 128;
off64_t size;
if (source->getSize(&size) != OK || size < (off64_t)V1_TAG_SIZE) {
return false;
}

//分配空间
mData = (uint8_t *)malloc(V1_TAG_SIZE);
//读取位于文件尾部128字节的V1标签帧
if (source->readAt(size - V1_TAG_SIZE, mData, V1_TAG_SIZE)
!= (ssize_t)V1_TAG_SIZE) {
free(mData);
mData = NULL;
return false;
}
if (memcmp("TAG", mData, 3)) {
free(mData);
mData = NULL;
return false;
}
mSize = V1_TAG_SIZE;
mFirstFrameOffset = 3;
if (mData[V1_TAG_SIZE - 3] != 0) {
mVersion = ID3_V1;
} else {
mVersion = ID3_V1_1;
}
return true;
}

真正的解析流程是从Iterator开始的,在Iterator中先是调用strdup(id)获取当前要获取的Tag到ID值放到mID中,接着调用findFrame找到对应到帧。

ID3::Iterator::Iterator(const ID3 &parent, const char *id)
: mParent(parent),
mID(NULL),
mOffset(mParent.mFirstFrameOffset),
mFrameData(NULL),
mFrameSize(0) {
if (id) {
mID = strdup(id);
}
//找到帧对应的位置
findFrame();
}

在findFrame中将当前ID与存储着全部标签帧数据的mData空间中的每个帧进行对比,如果找到帧标志等于当前ID的数据的时候就退出遍历循环,这时候mID就指向要寻找标签数据的位置。

void ID3::Iterator::findFrame() {
for (;;) {
mFrameData = NULL;
mFrameSize = 0;
if (mParent.mVersion == ID3_V2_2) {
//..................................................
} else if (mParent.mVersion == ID3_V2_3
|| mParent.mVersion == ID3_V2_4) {
//这是正常的MP3格式
if (mOffset + 10 > mParent.mSize) {
return;
}
//如果头四个字节为0000则返回
if (!memcmp(&mParent.mData[mOffset], "\0\0\0\0", 4)) {
return;
}
size_t baseSize;
if (mParent.mVersion == ID3_V2_4) {
//.......................................................................
} else {
//获取标签帧的内容大小
baseSize = U32_AT(&mParent.mData[mOffset + 4]);
}
//表示包含帧头的总共大小
mFrameSize = 10 + baseSize;
if (mOffset + mFrameSize > mParent.mSize) {
return;
}
//当前标签帧的内容数据
mFrameData = &mParent.mData[mOffset + 10];
if (!mID) {
break;
}
//将Tab 复制到id上,判断id是否等于mID如果不等则继续查找
char id[5];
memcpy(id, &mParent.mData[mOffset], 4);
id[4] = '\0';
//如果等于要找的ID 则退出循环,这时候mOffset 指向的是要查找帧的位置
if (!strcmp(id, mID)) {
break;
}
} else {
//.................................................
}
mOffset += mFrameSize;
}
}

获取专辑图片:
专辑图片的获取过程实际和获取其他TAG的方式是一样的,也是通过遍历存放ID3标签原始数据的mData空间,找到标签为”APIC”的数据,然后再将指向专辑图片的数据的首地址返回。

const void *
ID3::getAlbumArt(size_t *length, String8 *mime) const {
*length = 0;
mime->setTo("");
//APIC Attached picture 定位专辑图片的位置
Iterator it(*this,(mVersion == ID3_V2_3 || mVersion == ID3_V2_4) ? "APIC" : "PIC");
while (!it.done()) {
size_t size;
//指向专辑图片数据
const uint8_t *data = it.getData(&size);
if (mVersion == ID3_V2_3 || mVersion == ID3_V2_4) {
uint8_t encoding = data[0];
mime->setTo((const char *)&data[1]);
size_t mimeLen = strlen((const char *)&data[1]) + 1;
//返回图片的类型
uint8_t picType = data[1 + mimeLen];
//图片描述的内容长度
size_t descLen = StringSize(&data[2 + mimeLen], encoding);
//图片的实际长度
*length = size - 2 - mimeLen - descLen;
//返回图片的地址
return &data[2 + mimeLen + descLen];
} else {
//...........................................................
}
}
return NULL;
}

上述就是获取各个标签帧数据的流程,找出标签帧的值后就将可以将其取出存储到mMetaData中了。
我们再回到processFileInternal方法,看下接下来需要做哪些操作:它将会调用status = client.addStringTag(kKeyMap[i].tag, value);将该Tag传递到MediaScannerClient中进行处理,那么在MediaScannerClient中会做哪些处理呢?

status_t MediaScannerClient::addStringTag(const char* name, const char* value)
{
handleStringTag(name, value);
return OK;
}

在MediaScanner.java中将从底层传来的对应值赋给对应的成员变量。至此就完成了音频视频文件的Tag标签的获取过程。接下来和其他普通文件的处理方式一样,就是通过MediaProvider将其存储到数据库中。从而完成媒体扫描的整个过程。

public void handleStringTag(String name, String value) {
if (name.equalsIgnoreCase("title") || name.startsWith("title;")) {
// Don't trim() here, to preserve the special \001 character
// used to force sorting. The media provider will trim() before
// inserting the title in to the database.
mTitle = value;
} else if (name.equalsIgnoreCase("artist") || name.startsWith("artist;")) {
mArtist = value.trim();
} else if (name.equalsIgnoreCase("albumartist") || name.startsWith("albumartist;")
|| name.equalsIgnoreCase("band") || name.startsWith("band;")) {
mAlbumArtist = value.trim();
} else if (name.equalsIgnoreCase("album") || name.startsWith("album;")) {
mAlbum = value.trim();
} else if (name.equalsIgnoreCase("composer") || name.startsWith("composer;")) {
mComposer = value.trim();
} else if (mProcessGenres &&
(name.equalsIgnoreCase("genre") || name.startsWith("genre;"))) {
mGenre = getGenreName(value);
} else if (name.equalsIgnoreCase("year") || name.startsWith("year;")) {
mYear = parseSubstring(value, 0, 0);
} else if (name.equalsIgnoreCase("tracknumber") || name.startsWith("tracknumber;")) {
// track number might be of the form "2/12"
// we just read the number before the slash
int num = parseSubstring(value, 0, 0);
mTrack = (mTrack / 1000) * 1000 + num;
} else if (name.equalsIgnoreCase("discnumber") ||
name.equals("set") || name.startsWith("set;")) {
// set number might be of the form "1/3"
// we just read the number before the slash
int num = parseSubstring(value, 0, 0);
mTrack = (num * 1000) + (mTrack % 1000);
} else if (name.equalsIgnoreCase("duration")) {
mDuration = parseSubstring(value, 0, 0);
} else if (name.equalsIgnoreCase("writer") || name.startsWith("writer;")) {
mWriter = value.trim();
} else if (name.equalsIgnoreCase("compilation")) {
mCompilation = parseSubstring(value, 0, 0);
} else if (name.equalsIgnoreCase("isdrm")) {
mIsDrm = (parseSubstring(value, 0, 0) == 1);
} else if (name.equalsIgnoreCase("width")) {
mWidth = parseSubstring(value, 0, 0);
} else if (name.equalsIgnoreCase("height")) {
mHeight = parseSubstring(value, 0, 0);
} else {
//Log.v(TAG, "unknown tag: " + name + " (" + mProcessGenres + ")");
}
}

至此TAG的解析以及解码过程已经结束,我们最后还需要看下handleStringTag这个方法,这个方法中会将解码后的TAG如何做处理呢?
我们首先需要看下这个方法是如何从native层上升到Java层的,说到这个就必须考虑android_media_MediaScanner.cpp这个过渡类,在这个文件中我们可以找到handleStringTag这个方法,和其他一样在这里调用了CallVoidMethod方法进入到java层。这样java层中的handleStringTag就会被调用。

virtual status_t handleStringTag(const char* name, const char* value)
{
ALOGV("MediaScanner handleStringTag: name(%s) and value(%s)", name, value);
jstring nameStr, valueStr;
if ((nameStr = mEnv->NewStringUTF(name)) == NULL) {
mEnv->ExceptionClear();
return NO_MEMORY;
}
char *cleaned = NULL;
//如果不是正常的UTF-8则将其置为?
if (!isValidUtf8(value)) {
cleaned = strdup(value);
char *chp = cleaned;
char ch;
while ((ch = *chp)) {
if (ch & 0x80) {
*chp = '?';
}
chp++;
}
value = cleaned;
}
valueStr = mEnv->NewStringUTF(value);
free(cleaned);
if (valueStr == NULL) {
mEnv->DeleteLocalRef(nameStr);
mEnv->ExceptionClear();
return NO_MEMORY;
}
//调用JAVA层的handleStringTag方法。
mEnv->CallVoidMethod(mClient, mHandleStringTagMethodID, nameStr, valueStr);
mEnv->DeleteLocalRef(nameStr);
mEnv->DeleteLocalRef(valueStr);
return checkAndClearExceptionFromCallback(mEnv, "handleStringTag");
}

最后还是上个图吧,虽然流程简单但是还是很杂的,容易乱:
![](/Android-源码分析之MediaScanner-2.md/1.png)

附录:帧标识的含义

AENC    	Audio encryption
APIC Attached picture
COMM Comments
COMR Commercial
ENCR Encryption method registration
EQUA Equalization
ETCO Event timing codes
GEOB General encapsulated object
GRID Group identification registration
IPLS Involved people list
LINK Linked information
MCDI Music CD identifier
MLLT MPEGlocation lookup table
OWNE Ownership
PRIV Private
PCNT Playcounter
POPM Popularimeter
POSS Position synchronisation
RBUF Recommended buffer size
RVAD Relative volume adjustment
RVRB Reverb
SYLT Synchronized lyric/text
SYTC Synchronized tempo codes
TALB Album/Movie/Show title
TBPM BPM(beats per minute)
TCOM Composer
TCON Content type
TCOP Copyright message
TDAT Date
TDLY Playlist delay
TENC Encoded by
TEXT Lyricist/Text writer
TFLT Filetype
TIME Time
TIT1 Content group deion
TIT2 Title/songname/content deion
TIT3 Subtitle/Deion refinement
TKEY Initial key
TLAN Language(s)
TLEN Length
TMED Media type
TOAL Original album/movie/show title
TOFN Original filename
TOLY Original lyricist(s)/text writer(s)
TOPE Original artist(s)/performer(s)
TORY Original release year
TOWN Fileowner/licensee
TPE1 Leadperformer(s)/Soloist(s)
TPE2 Band/orchestra/accompaniment
TPE3 Conductor/performer refinement
TPE4 Interpreted, remixed, or otherwise modified by
TPOS Partof a set
TPUB Publisher
TRCK Track number/Position in set
TRDA Recording dates
TRSN Internet radio station name
TRSO Internet radio station owner
TSIZ Size
TSRC ISRC(international standard recording code)
TSSE Software/Hardware and settings used for encoding
TYER Year
TXXX Userdefined text information
UFID Unique file identifier
USER Terms of use
USLT Unsychronized lyric/text tranion
WCOM Commercial information
WCOP Copyright/Legal information
WOAF Official audio file webpage
WOAR Official artist/performer webpage
WOAS Official audio source webpage
WORS Official internet radio station homepage
WPAY Payment
WPUB Publishers official webpage
WXXX Userdefined URL link

MediaScanner媒体扫描器顾名思义就是用于扫描手机内部多媒体文件的信息,并将这些信息存储到数据库中的模块。我们今天就针对MediaScanner这个模块进行分析学习一下:

普通文件的扫描流程

扫描请求的接收者 MediaScannerReceiver

MediaScannerReceiver 继承自BroadcastReceiver它是通过静态注册的方式进行注册的:

<receiver android:name="MediaScannerReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MEDIA_MOUNTED" />
<data android:scheme="file" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MEDIA_UNMOUNTED" />
<data android:scheme="file" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MEDIA_SCANNER_SCAN_FILE" />
<data android:scheme="file" />
</intent-filter>
</receiver>

从上面的代码可以看出MediaScannerReceiver接受三种扫描请求广播:

  • Intent.ACTION_BOOT_COMPLETED:开机广播,在收到开机广播后会扫描内部和外部存储路径下的多媒体文件。
  • Intent.ACTION_MEDIA_MOUNTED:在某个外部存储器加载的时候。
  • Intent.ACTION_MEDIA_UNMOUNTED: 在某个外部存储器卸载的时候
  • Intent.ACTION_MEDIA_SCANNER_SCAN_FILE:在接受到对某个文件进行扫描的请求的时候调用scanFile方法进行扫描。

那每种广播到来的时候又会进行怎样的响应呢?

public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
final Uri uri = intent.getData();
//收到开机广播后扫描
if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
// Scan both internal and external storage
//扫描内部存储器中/system/media里面的内容,存放的是铃声等多媒体文件
scan(context, MediaProvider.INTERNAL_VOLUME);
//扫描手机的外部存储实际扫描的是/mnt/sdcard/
scan(context, MediaProvider.EXTERNAL_VOLUME);

} else {
if (uri.getScheme().equals("file")) {
// handle intents related to external storage
String path = uri.getPath();
String externalStoragePath = Environment.getExternalStorageDirectory().getPath();
String legacyPath = Environment.getLegacyExternalStorageDirectory().getPath();
try {
path = new File(path).getCanonicalPath();
} catch (IOException e) {
Log.e(TAG, "couldn't canonicalize " + path);
return;
}
if (path.startsWith(legacyPath)) {
path = externalStoragePath + path.substring(legacyPath.length());
}

Log.d(TAG, "action: " + action + " path: " + path);
//收到外部存储器加载完成广播的扫描
if (Intent.ACTION_MEDIA_MOUNTED.equals(action)) {
// scan whenever any volume is mounted
scan(context, MediaProvider.EXTERNAL_VOLUME);
//如果请求的是扫描路径,并且要扫描的属于外部存储路径则开始扫描
} else if (Intent.ACTION_MEDIA_SCANNER_SCAN_FILE.equals(action) &&
path != null && path.startsWith(externalStoragePath + "/")) {
scanFile(context, path);
}
}
}
}

我们看到整个扫描归结于scan和scanFile两个方法的调用。特别是scan方法既能够扫描外部存储又能扫描手机内存,唯一的区别是传入的参数不一样而已。我们接下来就来看下这部分的实现:

private void scan(Context context, String volume) {
Bundle args = new Bundle();
args.putString("volume", volume);
context.startService(
new Intent(context, MediaScannerService.class).putExtras(args));
}

private void scanFile(Context context, String path) {
Bundle args = new Bundle();
args.putString("filepath", path);
context.startService(
new Intent(context, MediaScannerService.class).putExtras(args));
}

很简单吧,就直接将路径传递给MediaScannerService就OK了,唯一的不同是携带数据的时候key不一样。

当调用startService后就会启动MediaScannerService,接下来就来看下MediaSannerService

扫描处理的后台处理者 MediaScannerService

在Scan和ScanFile启动首次MediaScannerService的时候MediaScannerService会调用onCreate方法创建这个服务,在onCreate中最重要的工作是启动一个线程用于扫描任务,因为Service运行在主线程中,如果直接在Service中执行扫描任务的话会阻塞主线程。

@Override
public void onCreate()
{
PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
StorageManager storageManager = (StorageManager)getSystemService(Context.STORAGE_SERVICE);
mExternalStoragePaths = storageManager.getVolumePaths();

// Start up the thread running the service. Note that we create a
// separate thread because the service normally runs in the process's
// main thread, which we don't want to block.
Thread thr = new Thread(null, this, "MediaScannerService");
thr.start();
}

那么在扫描线程thr中又是做了哪些工作呢?从run方法中可以看出在thr线程中主要是创建了一个ServiceHandler。

public void run()
{
// reduce priority below other background threads to avoid interfering
// with other services at boot time.
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND +Process.THREAD_PRIORITY_LESS_FAVORABLE);
Looper.prepare();

mServiceLooper = Looper.myLooper();
mServiceHandler = new ServiceHandler();
Looper.loop();
}

在之后调用startService的时候,MediaScannerService类中的onStartCommand方法将会被调用。在onStartCommand方法中向mServiceHandler发送扫描请求。在传递给mServiceHandler的消息中包含了从Intent中获取的extra信息,凭借着这个信息可以区分当前的扫描请求是扫描整个valum还是单个文件。

@Override
public int onStartCommand(Intent intent, int flags, int startId)
{
while (mServiceHandler == null) {
synchronized (this) {
try {
wait(100);
} catch (InterruptedException e) {
}
}
}

if (intent == null) {
Log.e(TAG, "Intent is null in onStartCommand: ",
new NullPointerException());
return Service.START_NOT_STICKY;
}

Message msg = mServiceHandler.obtainMessage();
msg.arg1 = startId;
msg.obj = intent.getExtras();
mServiceHandler.sendMessage(msg);

// Try again later if we are killed before we can finish scanning.
return Service.START_REDELIVER_INTENT;
}
private final class ServiceHandler extends Handler
{
@Override
public void handleMessage(Message msg)
{
Bundle arguments = (Bundle) msg.obj;
String filePath = arguments.getString("filepath");

try {
//这是扫描单个文件的情况
if (filePath != null) {
IBinder binder = arguments.getIBinder("listener");
IMediaScannerListener listener =
(binder == null ? null : IMediaScannerListener.Stub.asInterface(binder));
Uri uri = null;
try {
//调用scanFile方法
uri = scanFile(filePath, arguments.getString("mimetype"));
} catch (Exception e) {
Log.e(TAG, "Exception scanning file", e);
}
if (listener != null) {
listener.scanCompleted(filePath, uri);
}
//这是扫描一个目录的情况
} else {
String volume = arguments.getString("volume");
String[] directories = null;
if (MediaProvider.INTERNAL_VOLUME.equals(volume)) {
//这是扫描内部存储的情况
// scan internal media storage
directories = new String[] {
Environment.getRootDirectory() + "/media",
Environment.getOemDirectory() + "/media",
};
}
else if (MediaProvider.EXTERNAL_VOLUME.equals(volume)) {
// scan external storage volumes
//这是扫描外部存储的情况
directories = mExternalStoragePaths;
}

if (directories != null) {
if (false) Log.d(TAG, "start scanning volume " + volume + ": "
+ Arrays.toString(directories));
//调用scan方法
scan(directories, volume);
if (false) Log.d(TAG, "done scanning volume " + volume);
}
}
} catch (Exception e) {
Log.e(TAG, "Exception in handleMessage", e);
}
//扫描结束后退出服务
stopSelf(msg.arg1);
}
};

从上面代码中可以看出在mServiceHandler方法中,首先提取Intent中携带的信息,如果有携带关键字为filepath的数据则说明的是需要扫描的为文件,则调用scanFile方法对指定文件路径的文件进行扫描,如果携带的是关键字为volume的数据则说明当前扫描请求为扫描加载卷,调用的方法为scan方法。

接下来我们重点关注对存储器加载卷的扫描过程。对单个文件的扫描原理大致相同。

private void scan(String[] directories, String volumeName) {
Uri uri = Uri.parse("file://" + directories[0]);
// don't sleep while scanning
//获取唤醒锁,避免在扫描的时候手机发生睡眠
mWakeLock.acquire();

try {
ContentValues values = new ContentValues();
values.put(MediaStore.MEDIA_SCANNER_VOLUME, volumeName);
Uri scanUri = getContentResolver().insert(MediaStore.getMediaScannerUri(), values);

//发送ACTION_MEDIA_SCANNER_STARTED广播通知系统其他部件扫描即将开始
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_STARTED, uri));

try {
if (volumeName.equals(MediaProvider.EXTERNAL_VOLUME)) {
openDatabase(volumeName);
}
//创建MediaScanner开始扫描
MediaScanner scanner = createMediaScanner();
scanner.scanDirectories(directories, volumeName);
} catch (Exception e) {
Log.e(TAG, "exception in MediaScanner.scan()", e);
}

getContentResolver().delete(scanUri, null, null);

} finally {
//通知其他部件扫描结束
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_FINISHED, uri));
//释放唤醒锁
mWakeLock.release();
}
}

于扫描过程是一个耗时的过程,因此在开始扫描之前必须获得电源锁,避免在扫描过程中手机休眠,在扫描结束后释放电源锁。在扫描开始之前会向系统发送一个Intent.ACTION_MEDIA_SCANNER_STARTED的广播,通知系统的其他部件扫描过程的开始,其他部件可以通过监听这个广播。来采取对应的处理,同样在扫描结束后会向系统发送一个Intent.ACTION_MEDIA_SCANNER_FINISHED广播。而扫描任务是通过创建一个MediaScanner对象,并调用scanDirectories方法来实现的。

在应用层转了一圈后终于进入frameworks层了,我们看下frameworks层的MediaScanner类
在MediaScanner类中首先加载media_jni库,并调用native_init这个native方法。

static {
System.loadLibrary("media_jni");
native_init();
}
public MediaScanner(Context c) {
native_setup();
mContext = c;
mPackageName = c.getPackageName();
mBitmapOptions.inSampleSize = 1;
mBitmapOptions.inJustDecodeBounds = true;

setDefaultRingtoneFileNames();

mExternalStoragePath = Environment.getExternalStorageDirectory().getAbsolutePath();
mExternalIsEmulated = Environment.isExternalStorageEmulated();
//mClient.testGenreNameConverter();
}

在MediaScanner的构造方法中最重要的是调用了native_setup()方法,native_setup也是一个native方法。
接下来我们看下native_init和native_setup这两个native方法。

static void
android_media_MediaScanner_native_init(JNIEnv *env)
{
jclass clazz = env->FindClass(kClassMediaScanner);
if (clazz == NULL) {
return;
}
fields.context = env->GetFieldID(clazz, "mNativeContext", "J");
if (fields.context == NULL) {
return;
}
}

native_init负责获取MediaScanner中的mNativeContext变量并保存到fields.context
native_setup负责创建一个MediaScanner的子类StagefrightMediaScanner并将native_init获取到的上下文传给StagefrightMediaScanner对象。

static void
android_media_MediaScanner_native_setup(JNIEnv *env, jobject thiz)
{
MediaScanner *mp = new StagefrightMediaScanner;
if (mp == NULL) {
jniThrowException(env, kRunTimeException, "Out of memory");
return;
}
env->SetLongField(thiz, fields.context, (jlong)mp);
}

接着我们看下scanDirectories方法:

public void scanDirectories(String[] directories, String volumeName) {

//.......
//1.扫描前预处理
initialize(volumeName);
//2. 预扫描
prescan(null, true);
//.......
if (ENABLE_BULK_INSERTS) {
// create MediaInserter for bulk inserts
mMediaInserter = new MediaInserter(mMediaProvider, mPackageName, 500);
}

for (int i = 0; i < directories.length; i++) {
//3.扫描某个目录
processDirectory(directories[i], mClient);
}

if (ENABLE_BULK_INSERTS) {
// flush remaining inserts
mMediaInserter.flushAll();
mMediaInserter = null;
}
//4. 扫描后处理
postscan(directories);
//.......
}

上面我们过滤掉所有的无关代码,我们看到整个扫描过程分成如下四个部分:

  • initialize 扫描前预处理
  • prescan 预扫描
  • processDirectory扫描某个目录
  • postscan扫描后处理
  1. 扫描前预处理:
    在initialize中主要初始化各种Uri地址:包括音频,视频,图像,图像缩略图,视频缩略图,文件等Uri地址。如果当前扫描的是外部存储器则需要处理播放列表以及风格。
    private void initialize(String volumeName) {

    mMediaProvider = mContext.getContentResolver().acquireProvider("media");
    mAudioUri = Audio.Media.getContentUri(volumeName);
    mVideoUri = Video.Media.getContentUri(volumeName);
    mImagesUri = Images.Media.getContentUri(volumeName);
    mThumbsUri = Images.Thumbnails.getContentUri(volumeName);
    mFilesUri = Files.getContentUri(volumeName);
    mFilesUriNoNotify = mFilesUri.buildUpon().appendQueryParameter("nonotify", "1").build();
    if (!volumeName.equals("internal")) {
    // we only support playlists on external media
    mProcessPlaylists = true;
    mProcessGenres = true;
    mPlaylistsUri = Playlists.getContentUri(volumeName);
    mCaseInsensitivePaths = true;
    }
    }
  2. 预扫描处理:
    预扫描处理主要完成如下工作:
  • 建立mPlayLists,因为在主扫描过程中对于播放列表是放到后面处理的,所以要新建一个mPlayLists用于存储播放列表文件记录,在beginFile扫到播放列表的时候需要先将其存储到mPlayLists中,等到postscan阶段对其进行统一处理。
  • 扫描数据库并查找数据库中的每一项的本地文件是否存在,这里每次只扫描1000条数据,避免因为加载太多数据而出现卡顿现象,需要注意的是这里并不需要删除真实的文件这是因为如果文件确实消失了则不需要删除,如果是因为在扫描期间文件系统被卸载而后又挂载,这时候删除操作也是不需要的。如果当前项所对应的本地文件不存在,则看下是否是播放列表文件,如果不是播放列表文件则从数据库中删除。如果是播放列表类型则不删除,因为这有可能是由于播放列表信息被用户修改导致的。用户可以在播放器中删除它们。
  • 在清空缺乏实际文件的数据项后重新查询图片数目,音频,视频的数目
private void prescan(String filePath, boolean prescanFiles) throws RemoteException {
Cursor c = null;
String where = null;
String[] selectionArgs = null;
//建立mPlayLists
if (mPlayLists == null) {
mPlayLists = new ArrayList<FileEntry>();
} else {
mPlayLists.clear();
}
//通过上述处理mPlayLists将会被清空,
if (filePath != null) {
// query for only one file
where = MediaStore.Files.FileColumns._ID + ">?" +" AND " + Files.FileColumns.DATA + "=?";
selectionArgs = new String[] { "", filePath };
} else {
where = MediaStore.Files.FileColumns._ID + ">?";
selectionArgs = new String[] { "" };
}

// Tell the provider to not delete the file.
// If the file is truly gone the delete is unnecessary, and we want to avoid
// accidentally deleting files that are really there (this may happen if the
// filesystem is mounted and unmounted while the scanner is running).
//在这里告诉provider不要删除那些通过扫描发现不存在的文件,
为的是避免在扫描的时候文件系统卸载了,但是在以后的时候又挂载上了。
Uri.Builder builder = mFilesUri.buildUpon();
builder.appendQueryParameter(MediaStore.PARAM_DELETE_DATA, "false");
MediaBulkDeleter deleter = new MediaBulkDeleter(mMediaProvider, mPackageName,builder.build());

// Build the list of files from the content provider
try {
if (prescanFiles) {
// First read existing files from the files table.
// Because we'll be deleting entries for missing files as we go,
// we need to query the database in small batches, to avoid problems
// with CursorWindow positioning.
long lastId = Long.MIN_VALUE;
Uri limitUri = mFilesUri.buildUpon().appendQueryParameter("limit", "1000").build();
mWasEmptyPriorToScan = true;
while (true) {
selectionArgs[0] = "" + lastId;
if (c != null) {
c.close();
c = null;
}
c = mMediaProvider.query(mPackageName, limitUri, FILES_PRESCAN_PROJECTION,where, selectionArgs, MediaStore.Files.FileColumns._ID, null);
if (c == null) {
break;
}
int num = c.getCount();
if (num == 0) {
//如果为0表示已经查询完全部文件信息,退出循环
break;
}
mWasEmptyPriorToScan = false;
//开始获取这一批查询到的要扫描的路径等信息
while (c.moveToNext()) {
long rowId = c.getLong(FILES_PRESCAN_ID_COLUMN_INDEX);
String path = c.getString(FILES_PRESCAN_PATH_COLUMN_INDEX);
int format = c.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX);
long lastModified = c.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX);
lastId = rowId;
// Only consider entries with absolute path names.
// This allows storing URIs in the database without the
// media scanner removing them.
if (path != null && path.startsWith("/")) {
boolean exists = false;
try {
//查看数据库中对应项的本地文件是否存在
exists = Os.access(path, android.system.OsConstants.F_OK);
} catch (ErrnoException e1) {
}
if (!exists && !MtpConstants.isAbstractObject(format)) {
// do not delete missing playlists, since they may have been
// modified by the user.
// The user can delete them in the media player instead.
// instead, clear the path and lastModified fields in the row
MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);

//如果当前记录在数据库中存在,在本地中文件并不存在,
//并且丢失的文件类型不是播放列表则将记录从数据库中删除
//如果是播放列表类型则不删除
if (!MediaFile.isPlayListFileType(fileType)) {
deleter.delete(rowId);
if (path.toLowerCase(Locale.US).endsWith("/.nomedia")) {
deleter.flush();
String parent = new File(path).getParent();
mMediaProvider.call(mPackageName, MediaStore.UNHIDE_CALL,parent, null);
}
}
}
}
}
}
}
}
finally {
if (c != null) {
c.close();
}
deleter.flush();
}
// compute original size of images
mOriginalCount = 0;
c = mMediaProvider.query(mPackageName, mImagesUri, ID_PROJECTION, null, null, null, null);
if (c != null) {
mOriginalCount = c.getCount();
c.close();
}
}

通过预处理保证数据库的文件记录与本地的实际文件将一一对应。紧接着就进入了扫描阶段在scanDirectories方法中将会对每个需要扫描的目录调用processDirectory(directories[i], mClient);方法。processDirectory是一个nativie方法。
在JNI层的android_media_MediaScanner_processDirectory将会被调用,在该方法中将会调用native层的MediaScanner.cpp的processDirectory方法。

static void
android_media_MediaScanner_processDirectory(
JNIEnv *env, jobject thiz, jstring path, jobject client)
{
ALOGV("processDirectory");
//获得Native层的媒体扫描器
MediaScanner *mp = getNativeScanner_l(env, thiz);
//.......
//这里新建一个MyMediaScannerClient对象然后将其作为参数传递到processDirectory中
MyMediaScannerClient myClient(env, client);
MediaScanResult result = mp->processDirectory(pathStr, myClient);
//............
}
MediaScanResult MediaScanner::processDirectory(
const char *path, MediaScannerClient &client) {
//..........
client.setLocale(locale());
////调用doProcessDirectory处理
MediaScanResult result = doProcessDirectory(pathBuffer, pathRemaining, client, false);
//.....
return result;
}

我们继续看下native层MediaScannner是如何完成扫描工作的:在MediaScannner中的processDirectory 方法中会对每个扫描路径调用一次processDirectory方法,这里的path为外部存储的或者内部存储的路径

MediaScanResult MediaScanner::doProcessDirectory(
char *path, int pathRemaining, MediaScannerClient &client, bool noMedia) {

//.......
DIR* dir = opendir(path);
//...........
MediaScanResult result = MEDIA_SCAN_RESULT_OK;
while ((entry = readdir(dir))) {
if (doProcessDirectoryEntry(path, pathRemaining, client, noMedia, entry, fileSpot) == MEDIA_SCAN_RESULT_ERROR) {
result = MEDIA_SCAN_RESULT_ERROR;
break;
}
}
closedir(dir);
return result;
}

doProcessDirectory这个方法会对每个主路径下的子文件调用doProcessDirectoryEntry进行扫描。
在开始扫描之前需事先判断当前的文件类型,并根据不同的文件类型进行不同的处理:

  • 如果当前文件类型为目录则直接调用client.scanFile方法,将扫描的工作交给MyMediaScannerClient,紧接着调用doProcessDirectory对该目录下的子文件再进行递归扫描。
  • 如果当前文件类型为普通文件类型则直接调用client.scanFile方法,但是与文件类型为目录不同的是当文件类型为普通文件类型的时候调用client.scanFile的时候传入isDirectory参数为false。
MediaScanResult MediaScanner::doProcessDirectoryEntry(char *path, int pathRemaining, MediaScannerClient &client, bool noMedia,struct dirent* entry, char* fileSpot) {
struct stat statbuf;
const char* name = entry->d_name;
if (type == DT_DIR) {
// report the directory to the client
//如果是目录类型
if (stat(path, &statbuf) == 0) {
//调用client.scanFile扫描目录:这里的isDirectory = true
status_t status = client.scanFile(path, statbuf.st_mtime, 0,true /*isDirectory*/, childNoMedia);
if (status) {
return MEDIA_SCAN_RESULT_ERROR;
}
}
// and now process its contents
//在这个地方开始扫描该目录的子文件
strcat(fileSpot, "/");
MediaScanResult result = doProcessDirectory(path, pathRemaining - nameLength - 1, client, childNoMedia);
if (result == MEDIA_SCAN_RESULT_ERROR) {
return MEDIA_SCAN_RESULT_ERROR;
}
} else if (type == DT_REG) {
//如果是普通文件 这里的isDirectory = false
stat(path, &statbuf);
status_t status = client.scanFile(path, statbuf.st_mtime, statbuf.st_size,false /*isDirectory*/, noMedia);
if (status) {
return MEDIA_SCAN_RESULT_ERROR;
}
}
return MEDIA_SCAN_RESULT_OK;
}

不论怎样这里都是调用的client.scanFile只不过,传入的isDirectory不一样罢了。大家还记得client为MyMediaScannerClient吧,我们就来看下这个方法:

virtual status_t scanFile(const char* path, long long lastModified,
long long fileSize, bool isDirectory, bool noMedia)
{
//......
mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified,
fileSize, isDirectory, noMedia);
//......
}

在上述方法中主要调用的是mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified,fileSize, isDirectory, noMedia);这是调用哪个方法呢?要解决这个问题必须找到mScanFileMethodID

MyMediaScannerClient(JNIEnv *env, jobject client)
: mEnv(env),
mClient(env->NewGlobalRef(client)),
mScanFileMethodID(0),
mHandleStringTagMethodID(0),
mSetMimeTypeMethodID(0)
{
jclass mediaScannerClientInterface =
env->FindClass(kClassMediaScannerClient);
// static const char* const kClassMediaScannerClient =
"android/media/MediaScannerClient";
if (mediaScannerClientInterface == NULL) {
ALOGE("Class %s not found", kClassMediaScannerClient);
} else {
mScanFileMethodID = env->GetMethodID(
mediaScannerClientInterface,
"scanFile",
"(Ljava/lang/String;JJZZ)V");
mHandleStringTagMethodID = env->GetMethodID(
mediaScannerClientInterface,
"handleStringTag",
"(Ljava/lang/String;Ljava/lang/String;)V");
mSetMimeTypeMethodID = env->GetMethodID(
mediaScannerClientInterface,
"setMimeType",
"(Ljava/lang/String;)V");
}
}

从上面可以看出MyMediaScannerClient实例中的scanFile方法实际上是调用位于MediaScanner.java文件中的MyMediaScannerClient的ScanFile方法:

@Override
public void scanFile(String path, long lastModified, long fileSize,
boolean isDirectory, boolean noMedia) {
// This is the callback funtion from native codes.
// Log.v(TAG, "scanFile: "+path);
doScanFile(path, null, lastModified, fileSize, isDirectory, false, noMedia);
}

ScanFile方法很简单就直接调用doScanFile方法。在doScanFile方法中将会进入下一个重要的阶段—获取音频视频文件的TAG数据。但是在介绍这个重要阶段之前还是先介绍下beginFile。

public Uri doScanFile(String path, String mimeType, long lastModified,
long fileSize, boolean isDirectory, boolean scanAlways, boolean noMedia) {

//调用beginFile
FileEntry entry = beginFile(path, mimeType, lastModified,
fileSize, isDirectory, noMedia);

if (entry != null && (entry.mLastModifiedChanged || scanAlways)) {
if (noMedia) {

} else {
String lowpath = path.toLowerCase(Locale.ROOT);
boolean ringtones = (lowpath.indexOf(RINGTONES_DIR) > 0);
boolean notifications = (lowpath.indexOf(NOTIFICATIONS_DIR) > 0);
boolean alarms = (lowpath.indexOf(ALARMS_DIR) > 0);
boolean podcasts = (lowpath.indexOf(PODCAST_DIR) > 0);
boolean music = (lowpath.indexOf(MUSIC_DIR) > 0) ||
(!ringtones && !notifications && !alarms && !podcasts);
boolean isaudio = MediaFile.isAudioFileType(mFileType);
boolean isvideo = MediaFile.isVideoFileType(mFileType);
boolean isimage = MediaFile.isImageFileType(mFileType);

if (isaudio || isvideo || isimage) {
path = Environment.maybeTranslateEmulatedPathToInternal(new File(path))
.getAbsolutePath();
}

//如果为音频或者视频文件抽取元数据
// we only extract metadata for audio and video files
if (isaudio || isvideo) {
processFile(path, mimeType, this);
}
//如果是图像则调用processImageFile
if (isimage) {
processImageFile(path);
}
//调用endFile
result = endFile(entry, ringtones, notifications, alarms, music, podcasts);
}
}
return result;
}

beginFile故名思议就是在扫描前进行的一些处理:

  • 如果当前扫描的文件是普通文件则确定当前普通文件的文件类型。
  • 使用当前path参数到数据库中进行查询,这时候有两种情况,该文件的记录已经存在数据库中了,这时候使用path在数据库中找到对应的文件信息,并使用这些信息来创建一个FileEntry,如果当前的文件是新添加的文件,则在数据中并没有关于这个文件的信息,则new出一个新的FileEntry:
    entry = new FileEntry(0, path, lastModified,(isDirectory ? MtpConstants.FORMAT_ASSOCIATION : 0));
  • 如果当前文件是播放列表类型则将其先存储到mPlayLists中,在主扫描进程中先不做处理。
  • 清空元数据,为接下来的扫描做准备。
    public FileEntry beginFile(String path, String mimeType, long lastModified,long fileSize, boolean isDirectory, boolean noMedia) {
    mMimeType = mimeType;
    mFileType = 0;
    mFileSize = fileSize;
    mIsDrm = false;

    if (!isDirectory) {
    if (!noMedia && isNoMediaFile(path)) {
    noMedia = true;
    }
    mNoMedia = noMedia;

    // try mimeType first, if it is specified
    if (mimeType != null) {
    mFileType = MediaFile.getFileTypeForMimeType(mimeType);
    }
    // if mimeType was not specified, compute file type based on file extension.
    if (mFileType == 0) {
    MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
    if (mediaFileType != null) {
    mFileType = mediaFileType.fileType;
    if (mMimeType == null) {
    mMimeType = mediaFileType.mimeType;
    }
    }
    }
    if (isDrmEnabled() && MediaFile.isDrmFileType(mFileType)) {
    mFileType = getFileTypeFromDrm(path);
    }
    }
    //使用path查询数据库,在数据库中找到对应的文件信息,
    并使用这些信息来创建一个FileEntry
    FileEntry entry = makeEntryFor(path);
    // add some slack to avoid a rounding error
    long delta = (entry != null) ? (lastModified - entry.mLastModified) : 0;
    boolean wasModified = delta > 1 || delta < -1;
    if (entry == null || wasModified) {
    if (wasModified) {
    entry.mLastModified = lastModified;
    } else {
    entry = new FileEntry(0, path, lastModified,
    (isDirectory ? MtpConstants.FORMAT_ASSOCIATION : 0));
    }
    entry.mLastModifiedChanged = true;
    }
    //如果当前类型为播放列表类型
    if (mProcessPlaylists && MediaFile.isPlayListFileType(mFileType)) {
    //将数据存储到队列中在后续的部分处理,不在主扫描过程中处理
    mPlayLists.add(entry);
    // we don't process playlists in the main scan, so return null
    return null;
    }
    // clear all the metadata
    mArtist = null;
    mAlbumArtist = null;
    mAlbum = null;
    mTitle = null;
    mComposer = null;
    mGenre = null;
    mTrack = 0;
    mYear = 0;
    mDuration = 0;
    mPath = path;
    mLastModified = lastModified;
    mWriter = null;
    mCompilation = 0;
    mWidth = 0;
    mHeight = 0;
    return entry;
    }

如果当前文件不是音频视频或者图像文件则直接在doScanFile中调用endFile将信息存储到数据库中。我们看下endFile这个方法。
在endFile方法中做了如下工作:

  • 如果歌手信息不存在则使用专辑作者作为歌手信息.
  • 接着调用toValues方法将文件的基本信息添加到ContentValues中
  • 对toValues方法添加的基本信息进行修正。
  • 如果是新添加的音频文件则将传入的ringtones,notifications,alarms,music,podcasts添加到ContentValues中,如果是图片则从ExifInterface中获取图片的长度宽度,经纬度,时区信息。
  • 如果当前扫描的文件是新增的则将当前的ContentValues插入到数据库中,如果当前的文件的记录在数据库中本来就有的则调用update对数据库原先存在的项进行更新。
  • 更新Setting上的相关设置.
private Uri endFile(FileEntry entry, boolean ringtones, boolean notifications,
boolean alarms, boolean music, boolean podcasts)
throws RemoteException {
// update database
// use album artist if artist is missing
if (mArtist == null || mArtist.length() == 0) {
mArtist = mAlbumArtist;
}
ContentValues values = toValues();
String title = values.getAsString(MediaStore.MediaColumns.TITLE);
if (title == null || TextUtils.isEmpty(title.trim())) {
title = MediaFile.getFileTitle(values.getAsString(MediaStore.MediaColumns.DATA));
values.put(MediaStore.MediaColumns.TITLE, title);
}
String album = values.getAsString(Audio.Media.ALBUM);
if (MediaStore.UNKNOWN_STRING.equals(album)) {
album = values.getAsString(MediaStore.MediaColumns.DATA);
// extract last path segment before file name
int lastSlash = album.lastIndexOf('/');
if (lastSlash >= 0) {
int previousSlash = 0;
while (true) {
int idx = album.indexOf('/', previousSlash + 1);
if (idx < 0 || idx >= lastSlash) {
break;
}
previousSlash = idx;
}
if (previousSlash != 0) {
album = album.substring(previousSlash + 1, lastSlash);
values.put(Audio.Media.ALBUM, album);
}
}
}
long rowId = entry.mRowId;
if (MediaFile.isAudioFileType(mFileType) && (rowId == 0 || mMtpObjectHandle != 0)) {
// Only set these for new entries. For existing entries, they
// may have been modified later, and we want to keep the current
// values so that custom ringtones still show up in the ringtone
// picker.
values.put(Audio.Media.IS_RINGTONE, ringtones);
values.put(Audio.Media.IS_NOTIFICATION, notifications);
values.put(Audio.Media.IS_ALARM, alarms);
values.put(Audio.Media.IS_MUSIC, music);
values.put(Audio.Media.IS_PODCAST, podcasts);
} else if (mFileType == MediaFile.FILE_TYPE_JPEG && !mNoMedia) {
ExifInterface exif = null;
try {
exif = new ExifInterface(entry.mPath);
} catch (IOException ex) {
// exif is null
}
if (exif != null) {
float[] latlng = new float[2];
if (exif.getLatLong(latlng)) {
values.put(Images.Media.LATITUDE, latlng[0]);
values.put(Images.Media.LONGITUDE, latlng[1]);
}

long time = exif.getGpsDateTime();
if (time != -1) {
values.put(Images.Media.DATE_TAKEN, time);
} else {
// If no time zone information is available, we should consider using
// EXIF local time as taken time if the difference between file time
// and EXIF local time is not less than 1 Day, otherwise MediaProvider
// will use file time as taken time.
time = exif.getDateTime();
if (time != -1 && Math.abs(mLastModified * 1000 - time) >= 86400000) {
values.put(Images.Media.DATE_TAKEN, time);
}
}

int orientation = exif.getAttributeInt(
ExifInterface.TAG_ORIENTATION, -1);
if (orientation != -1) {
// We only recognize a subset of orientation tag values.
int degree;
switch(orientation) {
case ExifInterface.ORIENTATION_ROTATE_90:
degree = 90;
break;
case ExifInterface.ORIENTATION_ROTATE_180:
degree = 180;
break;
case ExifInterface.ORIENTATION_ROTATE_270:
degree = 270;
break;
default:
degree = 0;
break;
}
values.put(Images.Media.ORIENTATION, degree);
}
}
}

Uri tableUri = mFilesUri;
MediaInserter inserter = mMediaInserter;
if (!mNoMedia) {
if (MediaFile.isVideoFileType(mFileType)) {
tableUri = mVideoUri;
} else if (MediaFile.isImageFileType(mFileType)) {
tableUri = mImagesUri;
} else if (MediaFile.isAudioFileType(mFileType)) {
tableUri = mAudioUri;
}
}
Uri result = null;
boolean needToSetSettings = false;
if (rowId == 0) {
if (mMtpObjectHandle != 0) {
values.put(MediaStore.MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID, mMtpObjectHandle);
}
if (tableUri == mFilesUri) {
int format = entry.mFormat;
if (format == 0) {
format = MediaFile.getFormatCode(entry.mPath, mMimeType);
}
values.put(Files.FileColumns.FORMAT, format);
}
// Setting a flag in order not to use bulk insert for the file related with
// notifications, ringtones, and alarms, because the rowId of the inserted file is
// needed.
if (mWasEmptyPriorToScan) {
if (notifications && !mDefaultNotificationSet) {
if (TextUtils.isEmpty(mDefaultNotificationFilename) ||
doesPathHaveFilename(entry.mPath, mDefaultNotificationFilename)) {
needToSetSettings = true;
}
} else if (ringtones && !mDefaultRingtoneSet) {
if (TextUtils.isEmpty(mDefaultRingtoneFilename) ||
doesPathHaveFilename(entry.mPath, mDefaultRingtoneFilename)) {
needToSetSettings = true;
}
} else if (alarms && !mDefaultAlarmSet) {
if (TextUtils.isEmpty(mDefaultAlarmAlertFilename) ||
doesPathHaveFilename(entry.mPath, mDefaultAlarmAlertFilename)) {
needToSetSettings = true;
}
}
}

// New file, insert it.
// Directories need to be inserted before the files they contain, so they
// get priority when bulk inserting.
// If the rowId of the inserted file is needed, it gets inserted immediately,
// bypassing the bulk inserter.
if (inserter == null || needToSetSettings) {
if (inserter != null) {
inserter.flushAll();
}
result = mMediaProvider.insert(mPackageName, tableUri, values);
} else if (entry.mFormat == MtpConstants.FORMAT_ASSOCIATION) {
inserter.insertwithPriority(tableUri, values);
} else {
inserter.insert(tableUri, values);
}

if (result != null) {
rowId = ContentUris.parseId(result);
entry.mRowId = rowId;
}
} else {
// updated file
result = ContentUris.withAppendedId(tableUri, rowId);
// path should never change, and we want to avoid replacing mixed cased paths
// with squashed lower case paths
values.remove(MediaStore.MediaColumns.DATA);

int mediaType = 0;
if (!MediaScanner.isNoMediaPath(entry.mPath)) {
int fileType = MediaFile.getFileTypeForMimeType(mMimeType);
if (MediaFile.isAudioFileType(fileType)) {
mediaType = FileColumns.MEDIA_TYPE_AUDIO;
} else if (MediaFile.isVideoFileType(fileType)) {
mediaType = FileColumns.MEDIA_TYPE_VIDEO;
} else if (MediaFile.isImageFileType(fileType)) {
mediaType = FileColumns.MEDIA_TYPE_IMAGE;
} else if (MediaFile.isPlayListFileType(fileType)) {
mediaType = FileColumns.MEDIA_TYPE_PLAYLIST;
}
values.put(FileColumns.MEDIA_TYPE, mediaType);
}
mMediaProvider.update(mPackageName, result, values, null, null);
}

if(needToSetSettings) {
if (notifications) {
setSettingIfNotSet(Settings.System.NOTIFICATION_SOUND, tableUri, rowId);
mDefaultNotificationSet = true;
} else if (ringtones) {
setSettingIfNotSet(Settings.System.RINGTONE, tableUri, rowId);
mDefaultRingtoneSet = true;
} else if (alarms) {
setSettingIfNotSet(Settings.System.ALARM_ALERT, tableUri, rowId);
mDefaultAlarmSet = true;
}
}

return result;
}

到目前为止processDirectory方法的整个流程已经介绍完毕,如果是非视频音频图像文件则已经结束了整个扫描的过程,但是我们上面介绍的时候还忽略了播放列表文件。所以在下面的部分介绍播放列表文件,音频,视频,图像文件的处理。

播放列表的处理:

我们在介绍beginFile方法的时候说到在主扫描进程中并不对扫描到的播放列表进行处理,只是将其放到mPlayLists中。那么这些播放列表是什么时候处理呢?又将如何处理这些播放列表?在MediaScanner中播放列表文件的处理位于postscan方法中,这时候我们已经知道了存储器中有哪些多媒体文件。在processPlayLists方法中只处理最新的和上次扫描以后修改后的播放列表文件,通过获取播放列表的类型,再根据播放列表的类型调用不同的方法处理播放列表文件。

private void postscan(String[] directories) throws RemoteException {

// handle playlists last, after we know what media files are on the storage.
//如果当前扫描路径为外部存储路径则,则处理播放列表
if (mProcessPlaylists) {
//只处理最新的和上次扫描以后修改后的播放列表文件,
//获得最后的名字以及最后的修改时间,
//并根据播放列表的类型调用不同的方法处理播放列表文件
processPlayLists();
}

if (mOriginalCount == 0 && mImagesUri.equals(Images.Media.getContentUri("external")))
pruneDeadThumbnailFiles();

// allow GC to clean up
mPlayLists = null;
mMediaProvider = null;
}

processPlayLists方法中首先获取全部的音频文件,并使用全部的音频文件作为参数,传入processPlayList,这个参数在后续将会介绍,其实之所以传入全部音频文件是用于获取每个播放列表的Playlist Member(播放列表成员歌曲)

private void processPlayLists() throws RemoteException {
Iterator<FileEntry> iterator = mPlayLists.iterator();
Cursor fileList = null;
try {
// use the files uri and projection because we need the format column,
// but restrict the query to just audio files
/获取全部的音频文件
fileList = mMediaProvider.query(mPackageName, mFilesUri, FILES_PRESCAN_PROJECTION,
"media_type=2", null, null, null);
while (iterator.hasNext()) {
FileEntry entry = iterator.next();
// only process playlist files if they are new or have been modified since the last scan
// 只处理最新的和上次扫描以后修改后的播放列表文件
if (entry.mLastModifiedChanged) {
processPlayList(entry, fileList);
}
}
} catch (RemoteException e1) {
} finally {
if (fileList != null) {
fileList.close();
}
}
}

我们看下对每个播放列表是怎么处理的,在处理前必须确保有一个播放列表的名字,首先通过查找MediaStore.Audio.Playlists.NAME对应的值,看下是否存在,如果不存在则从MediaStore.MediaColumns.TITLE中获取,如果还是没有值,就再尝试从文件名中获取。如果当前的播放列表是新增的则往数据库中插入新的播放列表信息,并获取播放列表成员的Uri,如果当前播放列表存在的话就更新其内容并且删除原来的member Uri。最后获取播放列表文件的类型,根据不同的文件类型调用不同的方法调用不同的方法处理。

private void processPlayList(FileEntry entry, Cursor fileList) throws RemoteException {

String path = entry.mPath;
ContentValues values = new ContentValues();
int lastSlash = path.lastIndexOf('/');
if (lastSlash < 0) throw new IllegalArgumentException("bad path " + path);
Uri uri, membersUri;
//从FileEnty中获取rowId
long rowId = entry.mRowId;
// 确保有一个播放列表名字,首先查看MediaStore.Audio.Playlists.NAME
// 再查看MediaStore.MediaColumns.TITLE 如果还是为空则获取最后的.后面的字符
String name = values.getAsString(MediaStore.Audio.Playlists.NAME);
if (name == null) {
name = values.getAsString(MediaStore.MediaColumns.TITLE);
if (name == null) {
// 从文件中获取播放列表名字
int lastDot = path.lastIndexOf('.');
name = (lastDot < 0 ? path.substring(lastSlash + 1):
path.substring(lastSlash + 1, lastDot));
}
}
//将播放列表的名字和修改时间添加到数据库中
values.put(MediaStore.Audio.Playlists.NAME, name);
values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified);
if (rowId == 0) {
//如果不存在的话插入新的播放列表信息
values.put(MediaStore.Audio.Playlists.DATA, path);
//这时候value包含NAME DATA DATE_MODIFIED
uri = mMediaProvider.insert(mPackageName, mPlaylistsUri, values);
rowId = ContentUris.parseId(uri);
//获得插入的行id
//获得播放列表成员的Uri
membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY);
} else {
//如果存在的话就更新并且删除原来的member Uri
uri = ContentUris.withAppendedId(mPlaylistsUri, rowId);
mMediaProvider.update(mPackageName, uri, values, null, null);
membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY);
mMediaProvider.delete(mPackageName, membersUri, null, null);
}
//获取播放列表的目录
String playListDirectory = path.substring(0, lastSlash + 1);
//获取播放列表的文件类型
MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
//根据不同的文件类型调用不同的方法调用不同的方法处理
if (fileType == MediaFile.FILE_TYPE_M3U) {
processM3uPlayList(path, playListDirectory, membersUri, values, fileList);
} else if (fileType == MediaFile.FILE_TYPE_PLS) {
processPlsPlayList(path, playListDirectory, membersUri, values, fileList);
} else if (fileType == MediaFile.FILE_TYPE_WPL) {
processWplPlayList(path, playListDirectory, membersUri, values, fileList);
}
}

如果是M3U播放列表:则调用processM3uPlayList方法处理:
在processM3uPlayList方法打开播放列表文件,并读取播放列表内容,将读取到的播放列表成员记录添加到mPlsylist队列中调用processCachedPlaylist处理。

private void processM3uPlayList(String path, String playListDirectory, Uri uri,
ContentValues values, Cursor fileList) {
BufferedReader reader = null;
try {
File f = new File(path);
//判断播放列表文件是否存在,如果存在则调用cachePlaylistEntry
//读取后添加到mPlsylist队列中调用processCachedPlaylist处理
if (f.exists()) {
reader = new BufferedReader(new InputStreamReader(new FileInputStream(f)),
8192);
String line = reader.readLine();
//清除播放列表缓冲列表
mPlaylistEntries.clear();
while (line != null) {
// 忽略注释行
if (line.length() > 0 && line.charAt(0) != '#') {
//如果不是注释行,则表示为播放列表信息,则将其处理后添加到mPlsylist队列中
cachePlaylistEntry(line, playListDirectory);
}
line = reader.readLine();
}
processCachedPlaylist(fileList, values, uri);
}
} catch (IOException e) {
} finally {
try {
if (reader != null)
reader.close();
} catch (IOException e) {
}
}
}

如果是PLS格式的播放列表则将包含“File = ”开头的行,则将等号后面的内容读取出来(这些就是播放列表成员的记录)并存储到mPlsylist中,并调用processCachedPlaylist进行处理。

private void processPlsPlayList(String path, String playListDirectory, Uri uri,
ContentValues values, Cursor fileList) {
BufferedReader reader = null;
try {
File f = new File(path);
if (f.exists()) {
reader = new BufferedReader(
new InputStreamReader(new FileInputStream(f)), 8192);
String line = reader.readLine();
mPlaylistEntries.clear();
while (line != null) {
//如果是File开头的行则表示是PLS格式的播放列表信息
if (line.startsWith("File")) {
int equals = line.indexOf('=');
if (equals > 0) {
//将=后面的内容添加到mPlsylist中,调用processCachedPlaylist进行处理
cachePlaylistEntry(line.substring(equals + 1), playListDirectory);
}
}
line = reader.readLine();
}
processCachedPlaylist(fileList, values, uri);
}
} catch (IOException e) {
} finally {
try {
if (reader != null)
reader.close();
} catch (IOException e) {

}
}
}

如果是WPL格式的播放列表文件则使用Xml解析器对其进行解析。同样最后也是调用processCachedPlaylist对其中的播放列表进行处理。

private void processWplPlayList(String path, String playListDirectory, Uri uri,
ContentValues values, Cursor fileList) {
Xlog.i(TAG,"MediaScanner#processWplPlayList()");
FileInputStream fis = null;
try {
File f = new File(path);
if (f.exists()) {
fis = new FileInputStream(f);
mPlaylistEntries.clear();
Xml.parse(fis, Xml.findEncodingByName("UTF-8"),
new WplHandler(playListDirectory, uri, fileList).getContentHandler());
processCachedPlaylist(fileList, values, uri);
}
} catch (SAXException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fis != null)
fis.close();
} catch (IOException e) {
Xlog.e(TAG, "IOException in MediaScanner.processWplPlayList()", e);
}
}
}

cachePlaylistEntry负责讲从播放列表文件中读取到的播放列表行进行处理后添加到mPlaylistEntries中。

private void cachePlaylistEntry(String line, String playListDirectory) {
PlaylistEntry entry = new PlaylistEntry();
// 去除尾部的空白符
int entryLength = line.length();
while (entryLength > 0 && Character.isWhitespace(line.charAt(entryLength - 1))) entryLength--;
if (entryLength < 3) return;//如果小于三个字符则不合法
if (entryLength < line.length()) line = line.substring(0, entryLength);
//如果entryLength小于line.length()表示尾部有空格则删除尾部的空白符

//当前路径是否是绝对路径?
char ch1 = line.charAt(0);
boolean fullPath = (ch1 == '/' ||
(Character.isLetter(ch1) && line.charAt(1) == ':' && line.charAt(2) == '\\'));
// 如果是一个相对路径,则通过和playListDirectory进行合并构成全路径
if (!fullPath)
line = playListDirectory + line;
entry.path = line;
// 将全路径保存到mPlaylistEntries
mPlaylistEntries.add(entry);
}

processCachedPlaylist方法中将数据库中全部音频文件数据与存储在mPlaylistEntries的当前播放列表的成员歌曲记录进行匹配,匹配过程是调用matchEntries方法来完成的,它会使用全部音频文件与mPlaylistEntries中未匹配的项进行对比,如果路径相同则认为找到匹配的,如果不完全一致就通过对比路径的各个部分,寻找最匹配的那个。 如果mPlaylistEntries中全部项都匹配了,则返回true。最后将mPlaylistEntries中的成员作为当前播放列表的playlist member添加到数据库中。

private void processCachedPlaylist(Cursor fileList, ContentValues values, Uri playlistUri) {
fileList.moveToPosition(-1);
while (fileList.moveToNext()) {
long rowId = fileList.getLong(FILES_PRESCAN_ID_COLUMN_INDEX);
String data = fileList.getString(FILES_PRESCAN_PATH_COLUMN_INDEX);
// mPlaylistEntries中全部项都匹配了
if (matchEntries(rowId, data)) {
break;
}
}
int len = mPlaylistEntries.size();
int index = 0;
for (int i = 0; i < len; i++) {
PlaylistEntry entry = mPlaylistEntries.get(i);
if (entry.bestmatchlevel > 0) {
try {
//将mPlaylistEntries中的成员作为当前播放列表的playlist member
//添加到数据库中。
values.clear();
values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER,
Integer.valueOf(index));
values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID,
Long.valueOf(entry.bestmatchid));
mMediaProvider.insert(mPackageName, playlistUri, values);
index++;
} catch (RemoteException e) {
return;
}
}
}
mPlaylistEntries.clear();
}
private boolean matchEntries(long rowId, String data) {

int len = mPlaylistEntries.size();
boolean done = true;
for (int i = 0; i < len; i++) {
PlaylistEntry entry = mPlaylistEntries.get(i);
//如果entry.bestmatchlevel == Integer.MAX_VALUE 则表示已经被匹配了
//这种情况下将当前的与mPlaylistEntries的进行比较
if (entry.bestmatchlevel == Integer.MAX_VALUE) {
continue;
}
//开始比较的时候先将done置为false
done = false;
//data字段已经匹配不需要进行路径匹配了
if (data.equalsIgnoreCase(entry.path)) {
entry.bestmatchid = rowId;
entry.bestmatchlevel = Integer.MAX_VALUE;
continue;
}
//通过比对路径的每个字段寻找最比配的那个播放列表
int matchLength = matchPaths(data, entry.path);
if (matchLength > entry.bestmatchlevel) {
entry.bestmatchid = rowId;
entry.bestmatchlevel = matchLength;
}
}
return done;
}

总结以下,播放列表的处理是将播放列表文件进行解析,然后将播放列表文件中的成员添加到一个容器中,然后对容器进行遍历,并在数据库中对其进行匹配,如果匹配则添加到Playlist Member中

缩略图处理:

在MediaScanner中对缩略图处理也是位于Postscan方法中。在pruneDeadThumbnailFiles方法中先获取DCIM/thumbnails目录下的所有文件列表,在这个文件夹下存放的是所有的缩略图,而后再查看数据库中的视频和图像的缩略图信息,如果在数据库中存在该记录则表示该缩略图有用,从existingFiles中移除,到最后existingFiles中剩余的就是那些数据库中不需要的缩略图了,这些不需要的缩略图可以从DCIM/.thumbnails中删除,最后删除Mini 缩略图文件。

private void pruneDeadThumbnailFiles() {
HashSet<String> existingFiles = new HashSet<String>();
String directory = "/sdcard/DCIM/.thumbnails";
String [] files = (new File(directory)).list();
Cursor c = null;
if (files == null)
files = new String[0];

for (int i = 0; i < files.length; i++) {
String fullPathString = directory + "/" + files[i];
existingFiles.add(fullPathString);
}

try {
c = mMediaProvider.query(
mPackageName,
mThumbsUri,
new String [] { "_data" },
null,
null,
null, null);
Log.v(TAG, "pruneDeadThumbnailFiles... " + c);
if (c != null && c.moveToFirst()) {
do {
String fullPathString = c.getString(0);
existingFiles.remove(fullPathString);
} while (c.moveToNext());
}

for (String fileToDelete : existingFiles) {
if (false)
Log.v(TAG, "fileToDelete is " + fileToDelete);
try {
(new File(fileToDelete)).delete();
} catch (SecurityException ex) {
}
}

Log.v(TAG, "/pruneDeadThumbnailFiles... " + c);
} catch (RemoteException e) {
// We will soon be killed...
} finally {
if (c != null) {
c.close();
}
}
}
音频视频文件帧信息的获取

这部分比较复杂我们将放在下一篇博客中介绍

下面这俩张是根据Android框架揭秘改编,这本书很适合初学者快速了解整个系统的大体结构,很形象生动,看代码的时候可以对着上面的图进行阅读会加深理解。下面这两张图就当作这篇博客的后续写作纲要吧。这段时间有点忙。只能有空的时候构思。

在之前的博客中已经介绍过了Handler,我们知道主线其实就是UI界面的守护进程一样,只有它才能修改UI界面上的元素,工作线程如果要更新界面需要通过主线程的Handler来更新,后来为了更加方便我们在子线程中更新UI元素,Android引入了一个AsyncTask类,通过它可以很方便地在后台线程和UI线程之间进行切换。极大得简便了我们的日常开发。

public abstract class AsyncTask<Params, Progress, Result> 

AsyncTask是一个抽象类,如果要使用需要继承这个类,这里需要再提下这三个参数的含义:

  • Params 表示在启动AsyncTask时传入的参数在后台任务中可以使用作为输入数据。如果不需要任何参数则可以传入Void
  • Progress 用于在后台任务在执行时调用onProgressUpdate方法在界面上显示当前的进度。
  • Result 表示当前后台任务完成后的返回结果

具体的用法见Android进阶之多线程技术这篇博客,这里就不再作展开介绍了,我们接下来就以一个场景来展开介绍

class DownloadAsyncTask extends AsyncTask<Void, Integer, Boolean> {  

@Override
protected void onPreExecute() {
downloadProgressDialog.show();
}
@Override
protected Boolean doInBackground(Void... params) {
try {
while (true) {
int process = downloadTask();
publishProgress(process);
if (process >= 100) {
break;
}
}
} catch (Exception e) {
Log.e(TAG,"There is something error !");
return false;
}
return true;
}
@Override
protected void onProgressUpdate(Integer... values) {
downloadProgressDialog.setMessage("下载进度:" + values[0] + "%");
}

@Override
protected void onPostExecute(Boolean result) {
downloadProgressDialog.dismiss();
if (result) {
Toast.makeText(context, "下载成功", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(context, "下载失败", Toast.LENGTH_SHORT).show();
}
}
}

这是一个比较简单的模拟下载的示例,在downloadTask我们使用一个简单的sleep后更新返回值,我們先來看下AsycTask的构造方法:

public AsyncTask() {
mWorker = new WorkerRunnable<Params, Result>() {
public Result call() throws Exception {
mTaskInvoked.set(true);
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
//noinspection unchecked
Result result = doInBackground(mParams);
Binder.flushPendingCommands();
return postResult(result);
}
};

mFuture = new FutureTask<Result>(mWorker) {
@Override
protected void done() {
try {
postResultIfNotInvoked(get());
} catch (InterruptedException e) {
android.util.Log.w(LOG_TAG, e);
} catch (ExecutionException e) {
throw new RuntimeException("An error occurred while executing doInBackground()",
e.getCause());
} catch (CancellationException e) {
postResultIfNotInvoked(null);
}
}
};
}

这里创建了一个WorkerRunnable对象以及FutureTask对象,我们在继续深入代码之前先对这两个类有大概的了解下:
WorkerRunnable实现了Callable接口,是一个有返回值的线程任务。它的返回结果就是Result,FutureTask可以获取到执行结果并且可以取消执行任务,通过传入Runnable或者Callable的任务给FutureTask,直接调用其run方法或者放入线程池执行,之后可以在外部通过FutureTask的get方法异步获取执行结果,FutureTask非常适合用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。另外,FutureTask还可以确保即使调用了多次run方法,它都只会执行一次Runnable或者Callable任务,或者通过cancel取消FutureTask的执行等。从上面描述是否看到了AyncTask的雏形。如果对这两个类想要有更多的了解可以通过如下两篇博客来了解,也相当于该自己留个坑后续再深入了解。

http://blog.csdn.net/linchunquan/article/details/22382487
http://blog.csdn.net/jackchen95/article/details/13631761

好了我们继续:
上面讲到我们创建了一个WorkerRunnable对象后放到FutureTask中运行。那么整个初始化就结束了,接着就调用execute方法启动任务。

@MainThread
public final AsyncTask<Params, Progress, Result> execute(Params... params) {
return executeOnExecutor(sDefaultExecutor, params);
}

紧接着调用我们的executeOnExecutor方法。

 @MainThread
public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
Params... params) {
if (mStatus != Status.PENDING) {
switch (mStatus) {
case RUNNING:
throw new IllegalStateException("Cannot execute task:"
+ " the task is already running.");
case FINISHED:
throw new IllegalStateException("Cannot execute task:"
+ " the task has already been executed "
+ "(a task can be executed only once)");
}
}

mStatus = Status.RUNNING;

onPreExecute();

mWorker.mParams = params;
exec.execute(mFuture);

return this;
}

这里传入的exec参数为sDefaultExecutor,它的定义如下

private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;
public static final Executor SERIAL_EXECUTOR = new SerialExecutor();

也就是说它是一个SerialExecutor,看下代码中有如下注释:
说明这是一个串行的任务,每次只能执行一个,当当前的任务结束的时候才允许执行下一个。这也是很多时候提到的AyncTask的弊端。我门这里先不讲这些。继续我们的源码分析。

/**
* An {@link Executor} that executes tasks one at a time in serial
* order. This serialization is global to a particular process.
*/

executeOnExecutor中会先判断当前的状态这里需要注意的是AysnTask是一个单次执行的任务,一旦执行结束后再调用excute就会抛出错误,必须重新new一个。在状态为PENDING的情况下说明当前的AsyncTask尚未执行,这时候会将当前状态置为Status.RUNNING
并在调用onPreExecute后调用SerialExecutor的execute方法。

private static class SerialExecutor implements Executor {
final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
Runnable mActive;

public synchronized void execute(final Runnable r) {
mTasks.offer(new Runnable() {
public void run() {
try {
r.run();
} finally {
scheduleNext();
}
}
});
if (mActive == null) {
scheduleNext();
}
}

protected synchronized void scheduleNext() {
if ((mActive = mTasks.poll()) != null) {
THREAD_POOL_EXECUTOR.execute(mActive);
}
}
}

在SerialExecutor的execute方法中我们调用了FutureTask 的 run()方法(FutureTask 代码不在framework层中而是在libcore下)

public void run() {
if (state != NEW ||
!U.compareAndSwapObject(this, RUNNER, null, Thread.currentThread()))
return;
try {
Callable<V> c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
try {
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
setException(ex);
}
if (ran)
set(result);
}
} finally {
// runner must be non-null until state is settled to
// prevent concurrent calls to run()
runner = null;
// state must be re-read after nulling runner to prevent
// leaked interrupts
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}

在这里调用了

result = c.call()

也就是介绍构造函数时提到的WorkRunable,为了避免大家在这关键关头翻代码我直接再次贴在下面了。

mWorker = new WorkerRunnable<Params, Result>() {
public Result call() throws Exception {
mTaskInvoked.set(true);
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
//noinspection unchecked
Result result = doInBackground(mParams);
Binder.flushPendingCommands();
return postResult(result);
}

};

这里大家是不是看到了doInBackground方法?那么执行完结果怎么返回到主线程呢?我们注意到最后通过postResult来将结果返回。

private Result postResult(Result result) {
@SuppressWarnings("unchecked")
Message message = getHandler().obtainMessage(MESSAGE_POST_RESULT,
new AsyncTaskResult<Result>(this, result));
message.sendToTarget();
return result;
}

我们继续看下这个方法:这里我们看到了Handler的影子。它会通过getHandler获取到一个Handler,这个Handler实际上就是MainHandler,我们拭目以待:

private static Handler getHandler() {
synchronized (AsyncTask.class) {
if (sHandler == null) {
sHandler = new InternalHandler();
}
return sHandler;
}
}
private static class InternalHandler extends Handler {
public InternalHandler() {
super(Looper.getMainLooper());
}

@SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})
@Override
public void handleMessage(Message msg) {
AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;
switch (msg.what) {
case MESSAGE_POST_RESULT:
// There is only one result
result.mTask.finish(result.mData[0]);
break;
case MESSAGE_POST_PROGRESS:
result.mTask.onProgressUpdate(result.mData);
break;
}
}
}

看到了吧在创建InternalHandler的时候会将getMainLooper传入。通过这个Handler将MESSAGE_POST_RESULT消息发送给MainLooper,在消息循环中执行到这个消息的时候就调用InternalHandler的handleMessage,进而调用:

result.mTask.finish(result.mData[0]);

在紧接着就是调用finish方法,并将result作为参数传入:

private void finish(Result result) {
if (isCancelled()) {
onCancelled(result);
} else {
onPostExecute(result);
}
mStatus = Status.FINISHED;
}

在这里就调用了onPostExecute方法。

最后再看下publishProgress这个方法:

@WorkerThread
protected final void publishProgress(Progress... values) {
if (!isCancelled()) {
getHandler().obtainMessage(MESSAGE_POST_PROGRESS,
new AsyncTaskResult<Progress>(this, values)).sendToTarget();
}
}

其实如果上面的流程大家看得懂的话这里只要贴出代码就知道整个过程了:就是往MainLooper上发送MESSAGE_POST_PROGRESS消息InternalHandler收到消息后调用onProgressUpdate执行。

其实这里还涉及到SerialExecutor的串行执行的问题,这个将放在后面的博客中介绍。

上一篇我们介绍了Handler 我们知道了一般的工作线程在默认情况下是没有消息处理能力的,如果要处理消息需要通过如下的代码来给普通的线程创建一个Looper。

class NonMainThread extends Thread {
public Handler mHandler;
public void run() {
Looper.prepare();
mHandler = new Handler() {
public void handleMessage(Message msg) {
// process incoming messages here
}
};
Looper.loop();
}
}

这个看上去不是很复杂,但是Android为我们提供了更为简单的实现方式–HandlerThread
首先我们还是先来看下它的使用方式:

mHandlerThread = new HandlerThread("WorkThread");
mHandlerThread.start();
mThreadHandler = new Handler(mHandlerThread.getLooper()) {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
//。。。。。
}
};

我们接下来就来分析下这个过程:

首先在构造方法中接受一个参数为这个HandlerThread的名字,接着为我们的线程设置一个默认的优先级。

public HandlerThread(String name) {
super(name);
mPriority = Process.THREAD_PRIORITY_DEFAULT;
}

如果不想使用默认的优先级可以使用下面的构造方法:

public HandlerThread(String name, int priority) {
super(name);
mPriority = priority;
}

紧接着在调用start方法后,run方法将会运行:

@Override
public void run() {
mTid = Process.myTid();
Looper.prepare();
synchronized (this) {
mLooper = Looper.myLooper();
notifyAll();
}
Process.setThreadPriority(mPriority);
onLooperPrepared();
Looper.loop();
mTid = -1;
}

这段代码很熟悉吧,其实该博客开头提到的那个代码,这里需要注意的是onLooperPrepared()这个方法是空的,我们如果需要在Looper循环开始前做些其他处理可以覆写这个方法。

接下来我们就可以调用getLooper方法返回已经处于loop状态的Looper了,也有了MessageQueue,就可以创建Handler处理其他线程发过来的消息了,注意的是这里是为了让非UI线程拥有处理其他线程发送过来消息能力才使用HandlerThread的,如果只需要具有消息发送的能力就不需要了,这个一定要清楚。

public Looper getLooper() {
if (!isAlive()) {
return null;
}

// If the thread has been started, wait until the looper has been created.
synchronized (this) {
while (isAlive() && mLooper == null) {
try {
wait();
} catch (InterruptedException e) {
}
}
}
return mLooper;
}

我们首先看下我们的日常使用场景:
下面的例子中我们启动了一个线程在后台上作一些耗时间的操作,在操作结束后通过Handler向主线程发送一个消息更新文本,我们接下来就着这个情景对Handler源代码进行学习。

private Handler handler = new Handler() {

@Override
public void handleMessage(Message msg) {
if (msg.what == CHANGE_TEXT) {
textView.setText(String.valueOf(msg.obj));
}
super.handleMessage(msg);
}
};
new Thread() {
@Override
public void run() {
try {

//一些耗时的操作
Message msg = Message.obtain();
msg.what = CHANGE_TEXT;
msg.obj = "Hello Handle" ;
handler.sendMessage(msg);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();

首先是调用构造方法创建一个Handler对象,在构造方法的注释中我们看到一个比较有价值的提示:就是如果一个线程没有Looper那么它将不具备接受Message的能力,如果完这种线程中post消息那么将会有异常抛出。

/**
* Default constructor associates this handler with the {@link Looper} for the
* current thread.
*
* If this thread does not have a looper, this handler won't be able to receive messages
* so an exception is thrown.
*/
public Handler() {
this(null, false);
}

上面的构造方法实际上是调用了Handler的另一个构造方法:

public Handler(Callback callback, boolean async) {
if (FIND_POTENTIAL_LEAKS) {
final Class<? extends Handler> klass = getClass();
if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
(klass.getModifiers() & Modifier.STATIC) == 0) {
Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
klass.getCanonicalName());
}
}

mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread that has not called Looper.prepare()");
}
mQueue = mLooper.mQueue;
mCallback = callback;
mAsynchronous = async;
}

在这个方法的开始部分会先判断当前的Handler类是否是匿名内部类,或者静态内部类,成员内部类,如果是这些的话需要将Handler声明成static,这个在之前的内存优化的博客中已经提到,如果是非静态的那么将会持有外部类的引用,很有可能造成内存泄露。
紧接着调用

Looper.myLooper()

判断当前线程是否有Looper,这一点我们上面已经提到了如果没有Looper就没有处理消息的能力。那么什么时候才有Looper呢?
我们看下myLooper方法:

/**
* Return the Looper object associated with the current thread. Returns
* null if the calling thread is not associated with a Looper.
*/
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}

从注释上看我们可以很明显看出它是用于获取当前线程的Looper,那么这个Looper是什么时候设置的呢? 我们接下来看:

private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}

上面的代码中会先判断当前线程是否已经有Looper了,如果没有那么就添加一个,这就意味着一个线程至多只能有一个Looper。
那么我们上面的代码并没有调用这个prepare方法,为什么没有报错,原因在于主线程在创建的时候就已经随即创建了一个MainLooper,我们看源代码中prepare方法下面的prepareMainLooper方法:

public static void prepareMainLooper() {
prepare(false);
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has already been prepared.");
}
sMainLooper = myLooper();
}
}

它会新建一个Looper后将其赋给sMainLooper。

我们可以搜索下这个方法在哪里被调用,我们可以看到AcitivityThread.java中的main方法中有调用该方法:

public static void main(String[] args) {
//.......
Looper.prepareMainLooper();
ActivityThread thread = new ActivityThread();
thread.attach(false);
if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}
if (false) {
Looper.myLooper().setMessageLogging(new
LogPrinter(Log.DEBUG, "ActivityThread"));
}
// End of event ActivityThreadMain.
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");
}

在这里主线程创建了一个Looper并添加到sThreadLocal中。

发送Message:
Handler发送消息的方式有很多种我们以最简单的sendMessage(Message msg)来作为分析对象:

public final boolean sendMessage(Message msg){
return sendMessageDelayed(msg, 0);
}

这里会调用sendMessageDelayed(Message msg, long delayMillis)这个方法的第二个参数为延迟时间:

public final boolean sendMessageDelayed(Message msg, long delayMillis)
{
if (delayMillis < 0) {
delayMillis = 0;
}
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}

sendMessageAtTime 会调用enqueueMessage方法将消息入队

public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
MessageQueue queue = mQueue;
if (queue == null) {
RuntimeException e = new RuntimeException(
this + " sendMessageAtTime() called with no mQueue");
Log.w("Looper", e.getMessage(), e);
return false;
}
return enqueueMessage(queue, msg, uptimeMillis);
}
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
msg.target = this;
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
boolean enqueueMessage(Message msg, long when) {
if (msg.target == null) {
throw new IllegalArgumentException("Message must have a target.");
}
if (msg.isInUse()) {
throw new IllegalStateException(msg + " This message is already in use.");
}

synchronized (this) {
if (mQuitting) {
IllegalStateException e = new IllegalStateException(
msg.target + " sending message to a Handler on a dead thread");
Log.w(TAG, e.getMessage(), e);
msg.recycle();
return false;
}

msg.markInUse();
msg.when = when;
Message p = mMessages;
boolean needWake;
if (p == null || when == 0 || when < p.when) {
// New head, wake up the event queue if blocked.
msg.next = p;
mMessages = msg;
needWake = mBlocked;
} else {
// Inserted within the middle of the queue. Usually we don't have to wake
// up the event queue unless there is a barrier at the head of the queue
// and the message is the earliest asynchronous message in the queue.
needWake = mBlocked && p.target == null && msg.isAsynchronous();
Message prev;
for (;;) {
prev = p;
p = p.next;
if (p == null || when < p.when) {
break;
}
if (needWake && p.isAsynchronous()) {
needWake = false;
}
}
msg.next = p; // invariant: p == prev.next
prev.next = msg;
}
// We can assume mPtr != 0 because mQuitting is false.
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}

从上面代码中可以看出当收到一个Message后将会遍历整个消息队列,并将当前消息插入按照时间排序好的队列中。

那么消息已经进入队列了,那么什么时候被调用的呢?我们在看AcitivityThread.java中的main方法的时候除了prepareMainLooer外在最后调用了loop方法。这个其实就是一个消息循环,

public static void loop() {
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
final MessageQueue queue = me.mQueue;

// Make sure the identity of this thread is that of the local process,
// and keep track of what that identity token actually is.
Binder.clearCallingIdentity();
final long ident = Binder.clearCallingIdentity();

for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}

// This must be in a local variable, in case a UI event sets the logger
Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}

msg.target.dispatchMessage(msg);

if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}

// Make sure that during the course of dispatching the
// identity of the thread wasn't corrupted.
final long newIdent = Binder.clearCallingIdentity();
if (ident != newIdent) {
Log.wtf(TAG, "Thread identity changed from 0x"
+ Long.toHexString(ident) + " to 0x"
+ Long.toHexString(newIdent) + " while dispatching to "
+ msg.target.getClass().getName() + " "
+ msg.callback + " what=" + msg.what);
}

msg.recycleUnchecked();
}
}

上面的代码其实不难,很容易找到关键代码:

msg.target.dispatchMessage(msg);

msg.target 是什么?其实它就是往MessageQueue中post消息的那个Handler,也就是说Looper会遍历整个消息队列,找到消息的Target然后调用它的dispatchMessage,下面我们来看下dispatchMessage方法,在dispatchMessage方法中调用的是handleMessage这个回调方法,这就是我们写的那个业务逻辑。

public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}

读到这里可能细心的同学会留意到都没介绍队列中怎么决定某个消息何时处理,是的,这个其实是通过在loop中调用queue.next进行决定的。

Message next() {
// Return here if the message loop has already quit and been disposed.
// This can happen if the application tries to restart a looper after quit
// which is not supported.
final long ptr = mPtr;
if (ptr == 0) {
return null;
}

int pendingIdleHandlerCount = -1; // -1 only during first iteration
int nextPollTimeoutMillis = 0;
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}

nativePollOnce(ptr, nextPollTimeoutMillis);

synchronized (this) {
// Try to retrieve the next message. Return if found.
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
if (msg != null && msg.target == null) {
// Stalled by a barrier. Find the next asynchronous message in the queue.
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
if (msg != null) {
if (now < msg.when) {
// Next message is not ready. Set a timeout to wake up when it is ready.
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// Got a message.
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
return msg;
}
} else {
// No more messages.
nextPollTimeoutMillis = -1;
}

// Process the quit message now that all pending messages have been handled.
if (mQuitting) {
dispose();
return null;
}

// If first time idle, then get the number of idlers to run.
// Idle handles only run if the queue is empty or if the first message
// in the queue (possibly a barrier) is due to be handled in the future.
if (pendingIdleHandlerCount < 0
&& (mMessages == null || now < mMessages.when)) {
pendingIdleHandlerCount = mIdleHandlers.size();
}
if (pendingIdleHandlerCount <= 0) {
// No idle handlers to run. Loop and wait some more.
mBlocked = true;
continue;
}

if (mPendingIdleHandlers == null) {
mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
}
mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
}

// Run the idle handlers.
// We only ever reach this code block during the first iteration.
for (int i = 0; i < pendingIdleHandlerCount; i++) {
final IdleHandler idler = mPendingIdleHandlers[i];
mPendingIdleHandlers[i] = null; // release the reference to the handler

boolean keep = false;
try {
keep = idler.queueIdle();
} catch (Throwable t) {
Log.wtf(TAG, "IdleHandler threw exception", t);
}

if (!keep) {
synchronized (this) {
mIdleHandlers.remove(idler);
}
}
}

// Reset the idle handler count to 0 so we do not run them again.
pendingIdleHandlerCount = 0;

// While calling an idle handler, a new message could have been delivered
// so go back and look again for a pending message without waiting.
nextPollTimeoutMillis = 0;
}
}

这里的代码很长,但是其实关键的逻辑不难,它就是在一段时间内会去编历整个消息队列,查看Message中的when字段,并和当前的时间进行比对,如果达到要发送的时刻那么就返回这个Message给Looper。到此整个Handler源代码学习告一个小段落。我们来回顾下整个过程:

首先在AcitivityThread.java的main方法中会调用Looper.prepareMainLooper();创建一个MainLooper,然后调用Looper.loop();开启消息循环,这时候就可以允许Hanlder往主消息队列投递消息了,我们在主线程中创建Hanlder,在子线程中处理完耗时操作后就可以通过这个Handler完MainLooper中sentMessage,这些消息会通过调用enqueueMessage加入到消息队列中,注意这里的Message持有Handler的引用,这也是为什么需要将匿名内部类,静态内部类的Handler设置为static的原因了,否则Handler会持有外部类的引用,那么只要消息没及时处理,Handler所在的Activity或者Service一旦退出了就会造成内存泄露。
接下来在Looper.looper中会定期查看每个消息,如果时间到了就会调用消息所绑定Handler的dispatchMessage方法。在该方法中调用我们实现的handleMessage。

整个过程如下图所示:

再来站在全局的角度来一张图:

那么子线程可以有处理消息的能力吗,这个问题我们其实在上面已经说过了,默认情况是没有的,因为没有Looper,但是我们可以为一个子线程创建一个Looper这样就有了消息处理的能力了。

class NonMainThread extends Thread {
public Handler mHandler;
public void run() {
Looper.prepare();
mHandler = new Handler() {
public void handleMessage(Message msg) {
// process incoming messages here
}
};
Looper.loop();
}
}

在之前的博客已经介绍了ListView和Adapter了,但是之前篇文章只是着眼于基本的使用并没有讲到具体的背后的原理,这篇博客将会代大家过下ListView的源代码,让大家了解下整个原理:重点是ListView 缓存机制

ListView 缓存机制的实现
RecycleBin原理概述

在介绍RecycleBin原理之前我们先来介绍两个对象,ActiveView和ScrapView。我们知道ListView中包含两类子View,一类是可见的,显示在屏幕上的,这个就是ActiveView,另一类是不可见的这些被称为ScrapView(Scrap表示废弃的意思)ListView会把ScrapView删除的同时放入到RecycleBin中缓存起来。当我们滑动ListView的时候,就会导致有一部分元素进入屏幕,一些item从屏幕中移出,在移入的时候会从RecycleBin中取出一个ScrapView,将其作为convertView参数传递给Adapter的getView方法,从而达到View复用的目的,这样就不必在Adapter的getView方法中执行 LayoutInflater.inflate()方法了,从而大大提高整个性能,在编程的世界中有两个东西一直是矛盾的那就是时间和空间,而ListView中在这两者之间做了很好的平衡,个人觉得之所以可以做到这点还依赖于ListView item的一个特性,那就是每个item的布局是一致的只不过换了内容。

class RecycleBin {

//.....
/**
* Views that were on screen at the start of layout. This array is populated at the start of
* layout, and at the end of layout all view in mActiveViews are moved to mScrapViews.
* Views in mActiveViews represent a contiguous range of Views, with position of the first
* view store in mFirstActivePosition.
*/
private View[] mActiveViews = new View[0];

/**
* Unsorted views that can be used by the adapter as a convert view.
*/
private ArrayList<View>[] mScrapViews;
//.....
/**
* Fill ActiveViews with all of the children of the AbsListView.
*
* @param childCount The minimum number of views mActiveViews should hold
* @param firstActivePosition The position of the first view that will be stored in
* mActiveViews
*/
void fillActiveViews(int childCount, int firstActivePosition) {
if (mActiveViews.length < childCount) {
mActiveViews = new View[childCount];
}
mFirstActivePosition = firstActivePosition;

//noinspection MismatchedReadAndWriteOfArray
final View[] activeViews = mActiveViews;
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();
// Don't put header or footer views into the scrap heap
if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
// Note: We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in active views.
// However, we will NOT place them into scrap views.
activeViews[i] = child;
// Remember the position so that setupChild() doesn't reset state.
lp.scrappedFromPosition = firstActivePosition + i;
}
}
}

/**
* Get the view corresponding to the specified position. The view will be removed from
* mActiveViews if it is found.
*
* @param position The position to look up in mActiveViews
* @return The view if it is found, null otherwise
*/
View getActiveView(int position) {
int index = position - mFirstActivePosition;
final View[] activeViews = mActiveViews;
if (index >=0 && index < activeViews.length) {
final View match = activeViews[index];
activeViews[index] = null;
return match;
}
return null;
}

/**
* @return A view from the ScrapViews collection. These are unordered.
*/
View getScrapView(int position) {
final int whichScrap = mAdapter.getItemViewType(position);
if (whichScrap < 0) {
return null;
}
if (mViewTypeCount == 1) {
return retrieveFromScrap(mCurrentScrap, position);
} else if (whichScrap < mScrapViews.length) {
return retrieveFromScrap(mScrapViews[whichScrap], position);
}
return null;
}

/**
* Puts a view into the list of scrap views.
* <p>
* If the list data hasn't changed or the adapter has stable IDs, views
* with transient state will be preserved for later retrieval.
*
* @param scrap The view to add
* @param position The view's position within its parent
*/
void addScrapView(View scrap, int position) {
final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
if (lp == null) {
// Can't recycle, but we don't know anything about the view.
// Ignore it completely.
return;
}

lp.scrappedFromPosition = position;

// Remove but don't scrap header or footer views, or views that
// should otherwise not be recycled.
final int viewType = lp.viewType;
if (!shouldRecycleViewType(viewType)) {
// Can't recycle. If it's not a header or footer, which have
// special handling and should be ignored, then skip the scrap
// heap and we'll fully detach the view later.
if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
getSkippedScrap().add(scrap);
}
return;
}

scrap.dispatchStartTemporaryDetach();

// The the accessibility state of the view may change while temporary
// detached and we do not allow detached views to fire accessibility
// events. So we are announcing that the subtree changed giving a chance
// to clients holding on to a view in this subtree to refresh it.
notifyViewAccessibilityStateChangedIfNeeded(
AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);

// Don't scrap views that have transient state.
final boolean scrapHasTransientState = scrap.hasTransientState();
if (scrapHasTransientState) {
if (mAdapter != null && mAdapterHasStableIds) {
// If the adapter has stable IDs, we can reuse the view for
// the same data.
if (mTransientStateViewsById == null) {
mTransientStateViewsById = new LongSparseArray<>();
}
mTransientStateViewsById.put(lp.itemId, scrap);
} else if (!mDataChanged) {
// If the data hasn't changed, we can reuse the views at
// their old positions.
if (mTransientStateViews == null) {
mTransientStateViews = new SparseArray<>();
}
mTransientStateViews.put(position, scrap);
} else {
// Otherwise, we'll have to remove the view and start over.
getSkippedScrap().add(scrap);
}
} else {
if (mViewTypeCount == 1) {
mCurrentScrap.add(scrap);
} else {
mScrapViews[viewType].add(scrap);
}

if (mRecyclerListener != null) {
mRecyclerListener.onMovedToScrapHeap(scrap);
}
}
}
}

RecycleBin的代码不是很大但是全部在博客讲解每个细节一来自己也不是完全懂每个细节,而且太关注细节我们常常会陷入细节而看不到整个原理,所以在分析源代码的时候,第一次不要太纠结细节,等到后面有疑问或者遇到问题需要解决的时候在认真分析对应的细节。
好了言归正传,我们上面贴出了RecycleBin的关键代码,正如上面介绍的RecycleBin包含mActiveViews,以及mScrapViews这两个主要成员变量,这里再插入一个分析源代码的方法就是:在学习源代码的时候先看英文注释,一般Android代码中大部分都有比较详细的注释的,通过这些注释往往会快速了解这东西到底是干吗的,到底要不要继续深入看下去。

我们看下mActiveViews 的注释:

Views that were on screen at the start of layout. This array is populated at the start of layout, and at the end of layout all view in mActiveViews are moved to mScrapViews.Views in mActiveViews represent a contiguous range of Views, with position of the first view store in mFirstActivePosition.

大体的意思就是它用于存放的是在每次开始布局之前位于屏幕上的那些item View,在布局结束后所有存在于mActiveViews的item View都会移到mScrapViews,mActiveViews存放的内容是连续的从mFirstActivePosition位置开始到屏幕所能显示下的最大item数目。

那么mScrapViews 又是什么呢?

Unsorted views that can be used by the adapter as a convert view.

从注释中可以了解到它是一个无序排列的列表,这些视图可以被传入Adapter中作为一个convert view.

  • fillActiveViews 这个方法用于将AbsListView所有的子item 添加到ActiveViews中,它接收两个参数childCount表示屏幕显示的子item的数目,firstActivePosition表示屏幕上第一个item的位置。这个很简单。
  • getActiveView 这个方法将会传入位置参数,然后将会从mActiveViews中寻找并取出item View,如果找到在mActiveViews中对应的这个位置View将会被置为null
  • addScrapView 这个代码比较多,但是所处理的任务很简单就是将废弃的View添加到mScrapViews中,getScrapView相反就是从ScrapView中取出view。

大家看完上面的代码是不是和我有一样的感觉,这代码有啥营养?确实如果不结合ListView代码来分析,是完全看不出RecycleBin的原理的。我们在了解到RecycleBin中有什么之后皆来来就需要看它怎么在Listview中起到一个缓存的作用。

我们接下来就来讲解下ListView和RecycleBin如何交互实现缓存的:

主要关注如下两个情况:

  • ListView的item View 回收到 RecycleBin
  • 从RecycleBin中取出View作为ListView的item View

我们从绘制的角度出发,观察它是如何在绘制的过程中实现子View的缓存:

首先在分析代码之前我们先来了解下ListView的继承关系,这样有利于我们对代码的了解(说白了就是知道在子类找不到的时候,知道如何顺着继承关系找)

ListView-> AbsListView ->AdapterView -> ViewGroup
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
mInLayout = true;
final int childCount = getChildCount();
if (changed) {
for (int i = 0; i < childCount; i++) {
getChildAt(i).forceLayout();
}
mRecycler.markChildrenDirty();
}
layoutChildren();
mInLayout = false;
mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR;
// TODO: Move somewhere sane. This doesn't belong in onLayout().
if (mFastScroll != null) {
mFastScroll.onItemCountChanged(getChildCount(), mItemCount);
}
}

onlayout方法是在AbsListView中实现的,ListView中并没有实现这个方法,这是因为这部分代码属于GridView以及ListView通用的,所以将其放在父类上,我们知道在布局大小或者位置等发生变化的时候将会调用onLayout()方法,它会强制要求所有子item进行重绘。但是如果仔细看会发现layoutChildren()这个方法,我们接下来看下这个方法:

@Override
protected void layoutChildren() {
//......
try {
//......
final int childrenTop = mListPadding.top;
final int childrenBottom = mBottom - mTop - mListPadding.bottom;
final int childCount = getChildCount();
int index = 0;
int delta = 0;
View sel;
View oldSel = null;
View oldFirst = null;
View newSel = null;
// Remember stuff we will need down below
switch (mLayoutMode) {
//......
case LAYOUT_MOVE_SELECTION:
default:
// Remember the previously selected view
index = mSelectedPosition - mFirstPosition;
if (index >= 0 && index < childCount) {
oldSel = getChildAt(index);
}
// Remember the previous first child
oldFirst = getChildAt(0);
if (mNextSelectedPosition >= 0) {
delta = mNextSelectedPosition - mSelectedPosition;
}
// Caution: newSel might be null
newSel = getChildAt(index + delta);
}
boolean dataChanged = mDataChanged;
if (dataChanged) {
handleDataChanged();
}

//......

final int firstPosition = mFirstPosition;
final RecycleBin recycleBin = mRecycler;
if (dataChanged) {
for (int i = 0; i < childCount; i++) {
recycleBin.addScrapView(getChildAt(i), firstPosition+i);
}
} else {
recycleBin.fillActiveViews(childCount, firstPosition);
}

// Clear out old views
detachAllViewsFromParent();
recycleBin.removeSkippedScrap();

switch (mLayoutMode) {
//......
default:
if (childCount == 0) {
if (!mStackFromBottom) {
final int position = lookForSelectablePosition(0, true);
setSelectedPositionInt(position);
sel = fillFromTop(childrenTop);
} else {
final int position = lookForSelectablePosition(mItemCount - 1, false);
setSelectedPositionInt(position);
sel = fillUp(mItemCount - 1, childrenBottom);
}
} else {
if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
sel = fillSpecific(mSelectedPosition,
oldSel == null ? childrenTop : oldSel.getTop());
} else if (mFirstPosition < mItemCount) {
sel = fillSpecific(mFirstPosition,
oldFirst == null ? childrenTop : oldFirst.getTop());
} else {
sel = fillSpecific(0, childrenTop);
}
}
break;
}

// Flush any cached views that did not get reused above
recycleBin.scrapActiveViews();
//......
} finally {
//......
}
}

在介绍之前我们先看下如下代码片:

final int firstPosition = mFirstPosition;
final RecycleBin recycleBin = mRecycler;
if (dataChanged) {
for (int i = 0; i < childCount; i++) {
recycleBin.addScrapView(getChildAt(i), firstPosition+i);
}
} else {
recycleBin.fillActiveViews(childCount, firstPosition);
}

我们需要了解下dataChanged这个变量是怎么控制的,其实这个变量是在Adapter调用了notifyDataSetChanged方法,通知Adapter的数据源发生了变化,此时dataChanged变量就为true,这时候会将当前的所有可视item 通过RecycleBin的addScrapView方法将其放入RecycleBin的废弃List中,供后续复用。

为了让大家地更清楚我把那些无用的代码删除掉了,我们先从第一次layout的情形进行分析,第一次layout的时候布局上是没有子元素的,这时候dataChanged = false,childCount = 0所以fillActiveViews这个方法也是没有任何作用的。所以上面的整个关键代码如下:

if (childCount == 0) {
if (!mStackFromBottom) {
final int position = lookForSelectablePosition(0, true);
setSelectedPositionInt(position);
sel = fillFromTop(childrenTop);
} else {
final int position = lookForSelectablePosition(mItemCount - 1, false);
setSelectedPositionInt(position);
sel = fillUp(mItemCount - 1, childrenBottom);
}
}

上面代码就是根据实际的方向来调用fillFromTop还是fillUp这两个其实功能都差不多,区别只是在于方向而已,我们以fillFromTop来分析:

/**
* Fills the list from top to bottom, starting with mFirstPosition
*
* @param nextTop The location where the top of the first item should be
* drawn
*
* @return The view that is currently selected
*/
private View fillFromTop(int nextTop) {
mFirstPosition = Math.min(mFirstPosition, mSelectedPosition);
mFirstPosition = Math.min(mFirstPosition, mItemCount - 1);
if (mFirstPosition < 0) {
mFirstPosition = 0;
}
return fillDown(mFirstPosition, nextTop);
}

片段没实在的作用,实际的功能位于fillDown中。

private View fillDown(int pos, int nextTop) {
View selectedView = null;
int end = (mBottom - mTop);
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
end -= mListPadding.bottom;
}
while (nextTop < end && pos < mItemCount) {
// is this the selected item?
boolean selected = pos == mSelectedPosition;
View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);
nextTop = child.getBottom() + mDividerHeight;
if (selected) {
selectedView = child;
}
pos++;
}
setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1);
return selectedView;
}

在fillDown中会从屏幕的第一个元素开始,遍历填充每个元素,这里最关键的代码是makeAndAddView

private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
View child;
if (!mDataChanged) {
// Try to use an existing view for this position
child = mRecycler.getActiveView(position);
if (child != null) {
// Found it -- we're using an existing child
// This just needs to be positioned
setupChild(child, position, y, flow, childrenLeft, selected, true);
return child;
}
}
// Make a new view for this position, or convert an unused view if possible
child = obtainView(position, mIsScrap);
// This needs to be positioned and measured
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}

在makeAndAddView方法中尝试从从RecycleBin当中获取一个ActiveView,但是在第一次布局时候RecycleBin是空的,所以返回的是null,那么就直接调用obtainView方法创建或者尝试从ScropView中或其一个子View来复用,但是这时候ScropView数组也是空的,所以只能新建一个,这个就是在obtainView方法中实现的逻辑了,我们现在这里提下,紧接着就将获取到的子View通过setupChild添加到ListView中。

因此我们重点关注下obtainView:

View obtainView(int position, boolean[] isScrap) {
//.......
final View scrapView = mRecycler.getScrapView(position);
final View child = mAdapter.getView(position, scrapView, this);
if (scrapView != null) {
if (child != scrapView) {
// Failed to re-bind the data, return scrap to the heap.
mRecycler.addScrapView(scrapView, position);
} else {
isScrap[0] = true;
// Finish the temporary detach started in addScrapView().
child.dispatchFinishTemporaryDetach();
}
}
//.......
return child;
}

obtainView方法也是又臭又长但是我们只看关键的。它会调用getScrapView从mRecycler的ScrapView中获取可以复用的scrapView但是我们现在第一次什么都没有,所以scrapView为null。下面这个大家熟悉了把,

final View child = mAdapter.getView(position, scrapView, this);

什么不熟悉?这个就是调用Adapter的getView方法啊,第二个参数就是我们经常提到的convertView。想必看了下面的代码大家都会有印象吧:

public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
if (convertView == null) {
holder=new ViewHolder();
convertView = mInflater.inflate(R.layout.vlist2, null);
holder.img = (ImageView)convertView.findViewById(R.id.img);
holder.title = (TextView)convertView.findViewById(R.id.title);
holder.info = (TextView)convertView.findViewById(R.id.info);
holder.viewBtn =(Button)convertView.findViewById(R.id.view_btn);
convertView.setTag(holder);
}else {
holder = (ViewHolder)convertView.getTag();
}
holder.img.setBackgroundResource((Integer)mData.get(position).get("img"));
holder.title.setText((String)mData.get(position).get("title"));
holder.info.setText((String)mData.get(position).get("info"));
return convertView;
}

第一次layout的时候我们convertView为空那么就会inflate一个作为convertView并返回。makeAndAddView#setupChild就将这个convertView添加到布局中。

private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
boolean selected, boolean recycled) {
//......
if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter
&& p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
attachViewToParent(child, flowDown ? -1 : 0, p);
} else {
p.forceAdd = false;
if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
p.recycledHeaderFooter = true;
}
addViewInLayout(child, flowDown ? -1 : 0, p, true);
}
//......
}

setupChild方法很简单就是调用了addViewInLayout将convertView添加到ListView中。

接下来我们看下在经过第一次layout后,往后的布局和之前的布局有什么不一样的地方。
我们还是从layoutChildren开始,这里和上面的区别是由于childCount不为0所以fillActiveViews会将Child Item添加到ActiviteView数组中,
紧接着调用fillSpecific,这个会从指定的位置开始加载Child item。紧接着调用makeAndAddView,这时候makeAndAddView跑的逻辑就和之前不一样了,由于mDataChanged为false(假设当前数据集每变,也就是没有对数据集进行增删的操作)这次调用mRecycler.getActiveView的时候返回的就不是空了,因为前面我们调用了RecycleBin的fillActiveViews()方法来缓存ChildView。所以就不会再进入obtainView()方法,而是会直接调用setupChild()方法,这样就避免了重新inflate。
接下来看下setupChild(),由于我们在这之前调用了detachAllViewsFromParent所以子View应该调用attachViewToParent()方法。而不是前面提到的addViewInLayout方法。

好了我们看完上面代码可能觉得还是没有接触到最核心的缓存机制,最关键的部分是在滑动的时候如何缓存的,接下来我们看下这部分逻辑:
我们知道我们的交互事件都会被传递到onTouchEvent中,在ListView也不例外,onTouchEvent有很多事件,我们关注ACTION_MOVE,在这部分代码中我们重点关注trackMotionScroll方法:这个方法顾名思义就是跟踪滑动事件:这个代码也很长,我们还是从中关注重点。

boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
final int childCount = getChildCount();
if (childCount == 0) {
return true;
}

final int firstTop = getChildAt(0).getTop();
final int lastBottom = getChildAt(childCount - 1).getBottom();

final Rect listPadding = mListPadding;

// "effective padding" In this case is the amount of padding that affects
// how much space should not be filled by items. If we don't clip to padding
// there is no effective padding.
int effectivePaddingTop = 0;
int effectivePaddingBottom = 0;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
effectivePaddingTop = listPadding.top;
effectivePaddingBottom = listPadding.bottom;
}

// FIXME account for grid vertical spacing too?
final int spaceAbove = effectivePaddingTop - firstTop;
final int end = getHeight() - effectivePaddingBottom;
final int spaceBelow = lastBottom - end;

final int height = getHeight() - mPaddingBottom - mPaddingTop;
if (deltaY < 0) {
deltaY = Math.max(-(height - 1), deltaY);
} else {
deltaY = Math.min(height - 1, deltaY);
}

if (incrementalDeltaY < 0) {
incrementalDeltaY = Math.max(-(height - 1), incrementalDeltaY);
} else {
incrementalDeltaY = Math.min(height - 1, incrementalDeltaY);
}

final int firstPosition = mFirstPosition;

// Update our guesses for where the first and last views are
if (firstPosition == 0) {
mFirstPositionDistanceGuess = firstTop - listPadding.top;
} else {
mFirstPositionDistanceGuess += incrementalDeltaY;
}
if (firstPosition + childCount == mItemCount) {
mLastPositionDistanceGuess = lastBottom + listPadding.bottom;
} else {
mLastPositionDistanceGuess += incrementalDeltaY;
}

final boolean cannotScrollDown = (firstPosition == 0 &&
firstTop >= listPadding.top && incrementalDeltaY >= 0);
final boolean cannotScrollUp = (firstPosition + childCount == mItemCount &&
lastBottom <= getHeight() - listPadding.bottom && incrementalDeltaY <= 0);

if (cannotScrollDown || cannotScrollUp) {
return incrementalDeltaY != 0;
}

final boolean down = incrementalDeltaY < 0;

final boolean inTouchMode = isInTouchMode();
if (inTouchMode) {
hideSelector();
}

final int headerViewsCount = getHeaderViewsCount();
final int footerViewsStart = mItemCount - getFooterViewsCount();

int start = 0;
int count = 0;

if (down) {
int top = -incrementalDeltaY;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
top += listPadding.top;
}
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
if (child.getBottom() >= top) {
break;
} else {
count++;
int position = firstPosition + i;
if (position >= headerViewsCount && position < footerViewsStart) {
// The view will be rebound to new data, clear any
// system-managed transient state.
child.clearAccessibilityFocus();
mRecycler.addScrapView(child, position);
}
}
}
} else {
int bottom = getHeight() - incrementalDeltaY;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
bottom -= listPadding.bottom;
}
for (int i = childCount - 1; i >= 0; i--) {
final View child = getChildAt(i);
if (child.getTop() <= bottom) {
break;
} else {
start = i;
count++;
int position = firstPosition + i;
if (position >= headerViewsCount && position < footerViewsStart) {
// The view will be rebound to new data, clear any
// system-managed transient state.
child.clearAccessibilityFocus();
mRecycler.addScrapView(child, position);
}
}
}
}
mMotionViewNewTop = mMotionViewOriginalTop + deltaY;
mBlockLayoutRequests = true;

if (count > 0) {
detachViewsFromParent(start, count);
mRecycler.removeSkippedScrap();
}

// invalidate before moving the children to avoid unnecessary invalidate
// calls to bubble up from the children all the way to the top
if (!awakenScrollBars()) {
invalidate();
}

offsetChildrenTopAndBottom(incrementalDeltaY);

if (down) {
mFirstPosition += count;
}

final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
fillGap(down);
}

if (!inTouchMode && mSelectedPosition != INVALID_POSITION) {
final int childIndex = mSelectedPosition - mFirstPosition;
if (childIndex >= 0 && childIndex < getChildCount()) {
positionSelector(mSelectedPosition, getChildAt(childIndex));
}
} else if (mSelectorPosition != INVALID_POSITION) {
final int childIndex = mSelectorPosition - mFirstPosition;
if (childIndex >= 0 && childIndex < getChildCount()) {
positionSelector(INVALID_POSITION, getChildAt(childIndex));
}
} else {
mSelectorRect.setEmpty();
}
mBlockLayoutRequests = false;
invokeOnItemScrollListener();
return false;
}

这个方法传入两个参数deltaY表示从手指最初按下时的位置到当前手指所处的位置,incrementalDeltaY则表示相邻两次在Y方向上位置的改变量,incrementalDeltaY的正负值就可以判断我们当前的滑动方向了。(incrementalDeltaY小于0,表示向下滑动,大于0就是向上滑动)在这个部分逻辑中将会根据边缘作为判断依据如果子View的bottom值小于top值的时候,说明这个子View移出屏幕了,这时候就会调用RecycleBin的addScrapView()方法将这个View加入到废弃缓存当中,上面介绍了移出屏幕的情况,由于界面显示的子View是固定的所以有移出就有移入,我们接下来看下这部分逻辑:
如果ListView中最后一个View的底部已经移入了屏幕,或者ListView中第一个View的顶部移入了屏幕,就会调 用fillGap()方法,我们看下这个方法:

void fillGap(boolean down) {
final int count = getChildCount();
if (down) {
int paddingTop = 0;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
paddingTop = getListPaddingTop();
}
final int startOffset = count > 0 ? getChildAt(count - 1).getBottom() + mDividerHeight :
paddingTop;
fillDown(mFirstPosition + count, startOffset);
correctTooHigh(getChildCount());
} else {
int paddingBottom = 0;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
paddingBottom = getListPaddingBottom();
}
final int startOffset = count > 0 ? getChildAt(0).getTop() - mDividerHeight :
getHeight() - paddingBottom;
fillUp(mFirstPosition - 1, startOffset);
correctTooLow(getChildCount());
}
}

额,又回到之前的流程,在fillGap又调用了fillDown/fillUp,我们知道这两个方法会调用makeAndAddView,但是这时候的makeAndAddView流程又和上面不大一样了。这时候mRecycler.getActiveView返回的是null

private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
View child;
if (!mDataChanged) {
// Try to use an existing view for this position
child = mRecycler.getActiveView(position);
if (child != null) {
// Found it -- we're using an existing child
// This just needs to be positioned
setupChild(child, position, y, flow, childrenLeft, selected, true);
return child;
}
}
// Make a new view for this position, or convert an unused view if possible
child = obtainView(position, mIsScrap);
// This needs to be positioned and measured
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}

为什么是null我们再来看下getActiveView:

View getActiveView(int position) {
int index = position - mFirstActivePosition;
final View[] activeViews = mActiveViews;
if (index >=0 && index < activeViews.length) {
final View match = activeViews[index];
activeViews[index] = null;
return match;
}
return null;
}

由于之前这个方法已经被调用过了它会将activeViews[index]置为null所以还会调用obtainView,上面已经对该方法做了分析,它会调用getScrapView()方法来尝试从废弃缓存中获取一个View,如果没有的话则会inflate一个返回。

ListView的优化

上面介绍的只是ListView的缓存机制,了解了整个缓存机制后我们就可以充分利用convertView来判断是否inflate了,所以整个ListView只加载一屏的布局,之后滑动出来的item使用的是之前已经加载的布局的缓存,但是我们看下下面的getView我们使用了ViewHolder,还有set/getTag这是干啥用的?

public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
if (convertView == null) {
holder=new ViewHolder();
convertView = mInflater.inflate(R.layout.vlist2, null);
holder.img = (ImageView)convertView.findViewById(R.id.img);
holder.title = (TextView)convertView.findViewById(R.id.title);
holder.info = (TextView)convertView.findViewById(R.id.info);
holder.viewBtn =(Button)convertView.findViewById(R.id.view_btn);
convertView.setTag(holder);
}else {
holder = (ViewHolder)convertView.getTag();
}
holder.img.setBackgroundResource((Integer)mData.get(position).get("img"));
holder.title.setText((String)mData.get(position).get("title"));
holder.info.setText((String)mData.get(position).get("info"));
return convertView;
}

其实使用ViewHolder,还有set/getTag是为了节省findViewById的时间。如果不使用ViewHolder,每次getView的时候都需要得到一次子布局,而这也是很耗时并且耗资源的,如果使用了ViewHolder作为子布局的缓存,使用View的setTag方法将缓存与每个item绑定,则也可以省去了findViewById的时间(这里我个人的理解是将id与ViewHolder中的View绑定起来,如果有理解错误欢迎更正)
这里还需要注意一点我们一般需要将ViewHolder设置为静态的因为因为静态内部类,不持有外部类的引用,从而避免内存泄露。

总结如下:基本的优化方式

  • 使用ConvertView复用机制
  • 使用ViewHolder
  • 使用set/getTag

ListView Item带有图片的情况:

我们在开发的时候会遇到ListView中每个item都有一个Image的情况,这个在Music应用中十分常见,
所以我们一般对这些图像的加载使用图片缓存,并且在加载这些图片的时候使用异步加载,但是这也面临这空间和时间的平衡问题。
这种情况还有一种方法就是针对图片资源进行优化比如在图片解码的时候,降低像素颜色信息,去掉透明度等,或者在设计资源的时候尽量减小图片资源的尺寸。

为了避免不需要加载在我们还可以设置在滑动不加载图片滑动停止的时候加载图片
下面是对应的实现例子:
转载自 http://blog.csdn.net/yy1300326388/article/details/45153813

//定义当前listview是否在滑动状态
private boolean scrollState=false;
public void setScrollState(boolean scrollState) {
this.scrollState = scrollState;
}
//实体类
UserEnity userEnity=lists.get(position);
if (!scrollState){//如果当前不是滑动的状态,我们填充真数据
//填充数据
viewHolder.tv_name.setText(userEnity.getName());
//设置Tag中数据为空表示数据已填充
viewHolder.tv_name.setTag(null);
//加载图片
ImageLoader.getInstance().displayImage(img_url,viewHolder.iv_icon);
//设置tag为1表示已加载过数据
viewHolder.iv_icon.setTag("1");

}else{//如果当前是滑动的状态,我们填充假数据
viewHolder.tv_name.setText("加载中");
//将数据name保存在Tag当中
viewHolder.tv_name.setTag(userEnity.getName());
//将数据image_url保存在Tag当中
viewHolder.iv_icon.setTag(img_url);
//设置默认显示图片(最好是本地资源的图片)
viewHolder.iv_icon.setImageResource(R.mipmap.ic_launcher);

}
设置监听
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
switch (scrollState){
case AbsListView.OnScrollListener.SCROLL_STATE_IDLE://停止滚动
{
//设置为停止滚动
myAdapter.setScrollState(false);
//当前屏幕中listview的子项的个数
int count = view.getChildCount();
Log.e("MainActivity",count+"");

for (int i = 0; i < count; i++) {
//获取到item的name
TextView tv_name = (TextView) view.getChildAt(i).findViewById(R.id.main_item_tv_name);
//获取到item的头像
ImageView iv_show= (ImageView) view.getChildAt(i).findViewById(R.id.main_item_iv_icon);

if (tv_name.getTag() != null) { //非null说明需要加载数据
tv_name.setText(tv_name.getTag().toString());//直接从Tag中取出我们存储的数据name并且赋值
tv_name.setTag(null);//设置为已加载过数据
}

if (!iv_show.getTag().equals("1")){//!="1"说明需要加载数据
String image_url=iv_show.getTag().toString();//直接从Tag中取出我们存储的数据image——url
ImageLoader.getInstance().displayImage(image_url, iv_show);//显示图片
iv_show.setTag("1");//设置为已加载过数据
}
}
break;
}
case AbsListView.OnScrollListener.SCROLL_STATE_FLING://滚动做出了抛的动作
{
//设置为正在滚动
myAdapter.setScrollState(true);
break;
}
case AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL://正在滚动
{
//设置为正在滚动
myAdapter.setScrollState(true);
break;
}
}
}

局部更新:
http://blog.csdn.net/nupt123456789/article/details/39432781
http://www.cnblogs.com/liuling/p/2015-10-20-01.html

private void updateProgressPartly(int progress,int position){
int firstVisiblePosition = listview.getFirstVisiblePosition();
int lastVisiblePosition = listview.getLastVisiblePosition();
if(position>=firstVisiblePosition && position<=lastVisiblePosition){
View view = listview.getChildAt(position - firstVisiblePosition);
if(view.getTag() instanceof ViewHolder){
ViewHolder vh = (ViewHolder)view.getTag();
vh.pb.setProgress(progress);
}
}
}

总结一下:

其实ListView优化的最根本途径在于getView方法的优化,所以我们优化就需要将一切耗时的操作从getView中抽离,比如图片加载,网络数据加载,文件加载。
因此可以通过如下几种途径来优化:

  1. 启动线程来异步加载图片,在图片尚未加载完成之前先用空白图片占位
  2. 滑动时不加载图片,停止滑动时加载
  3. 使用缓存机制将其缓存到内存中(注意使用弱引用)
  4. 局部刷新
  5. Item中的控件宽高尽量写成固定的值或者math_parent,避免影响其他控件的位置导致重新绘制
  6. 减少布局层次,缩短绘制时间。慎用 layout_weight 类似属性,以便缩短布局的 Measure 时间。
  7. 使用RecyclerView,RecyclerView提供了原生的局部刷新功能
  8. 在加载的时候,为ImageView设置一个Tag,比如imageView.setTag(image_url),下一次再加载之前,首先获取Tag,比如imageUrl = imageView.getTag(),如果此时的地址和之前的地址一样,我们就不需要加载了,如果不一样,再加载。

最后是之前开发中遇到的两种ListView错位的问题:
下面是当时的参考解决方案,后续有空的时候再对这部分进行专门总结:
http://www.cnblogs.com/lesliefang/p/3619223.html
http://www.runoob.com/w3cnote/android-tutorial-listview-checkbox.html
http://www.trinea.cn/android/android-listview-display-error-image-when-scroll/

三种引用对比

引用类型 回收时期
强引用 当代码中显示标示这个对象不使用的时候
软引用 内存不足的时候
弱引用 不管内存是否充足,只要执行gc的时候就有可能被回收

通过上面我们可以看出除了强引用外其他两种引用在使用的时候都需要判断是否被回收。

软引用和弱引用的使用场景上的区别:

如果只是想避免OutOfMemory异常的发生,则可以使用软引用。如果对于应用的性能更在意,想尽快回收一些占用内存比较大的对象,则可以使用弱引用。
另外可以根据对象是否经常使用来判断选择软引用还是弱引用。如果该对象可能会经常使用的,就尽量用软引用。如果该对象不被使用的可能性更大些,就可以用弱引用。