来自:http://blog.csdn.net/myarrow/article/details/7066007

Android StagefrightPlayer

1. 对StagefrightPlayer的好奇

前面对StagefrightPlayer的创建流程已经分析清楚了,即在Android::createPlayer中根据url的type来创建不同的player. StagefrightPlayer是Android提供的,比较经典的一个Player。但个人觉得它不怎么样,还不如ffmpeg支持的codec和parser多。还有一个opencore,更是复杂无比的东东,它采用datapath的方式,类似于大家熟悉的GStreamer。不理解大家为什么会把简单的事情复杂化。

2. StagefrightPlayer是个什么东东?

仔细一看代码,它也是一个空壳公司,其中就一个员工给他干活,它就是AwesomePlayer *mPlayer,在创建StagefrightPlayer时,它就被创建了。StagefrighPlayer中的所有接口都是简单调用AwesomePlayer的对应接口来实现。所以它只是一个接口人,什么都不是。这个AwesomePlayer才是我们的研究重点。

3. AwesomePlayer有些什么东东?

它再神奇,不也就是实现AV播放吗?看看自己直接基于Driver的MyPlayer不也就1000多行代码就把TS播放玩得很爽了。但google为了其开放性,搞得一下子搞不明白。既然想跟着Android混饭吃,只好读它这一大堆没有什么文档和注释的代码了。

AVPlayer肯定具有以下模块:

1) 数据源(如TS流,MP4...)

2) Demux (音视频分离)

3) 音视频解码

4) 音频播放和视频播放

5) 音视频同步

6) 整个工作流程

AwesomePlayer就是把以上6位员工组织起来工作的老板,下面就对每一个问题进行一一分析。

下面先看看它的几位骨干员工:

[cpp] view plain copy print ?
  1. //Events
  2. sp<TimedEventQueue::Event>mVideoEvent;
  3. sp<TimedEventQueue::Event>mStreamDoneEvent;
  4. sp<TimedEventQueue::Event>mBufferingEvent;
  5. sp<TimedEventQueue::Event>mCheckAudioStatusEvent;
  6. sp<TimedEventQueue::Event>mVideoLagEvent;
  7. //AudioSourceandDecoder
  8. voidsetAudioSource(sp<MediaSource>source);
  9. status_tinitAudioDecoder();
  10. //VideoSourceandDecoder
  11. voidsetVideoSource(sp<MediaSource>source);
  12. status_tinitVideoDecoder(uint32_tflags=0);
  13. //Datasource
  14. sp<DataSource>mFileSource;
  15. //Videorender
  16. sp<MediaSource>mVideoTrack;
  17. sp<MediaSource>mVideoSource;
  18. sp<AwesomeRenderer>mVideoRenderer;
  19. //Audiorender
  20. sp<MediaSource>mAudioTrack;
  21. sp<MediaSource>mAudioSource;
  22. AudioPlayer*mAudioPlayer;

AwesomePlayer的启动工作

分类: Android Media 1572人阅读 评论(0) 收藏 举报

继前一篇文章AwesomePlayer的准备工作,本文主要描述当Java调用mp.start();时,AwesomePlayer做了些什么...

1.AwesomePlayer::play_l

其调用流程如下:

StagefrightPlayer::start->

AwesomePlayer::play->

AwesomePlayer::play_l

AwesomePlayer::play_l主要代码如下:

[cpp] view plain copy print ?
  1. status_tAwesomePlayer::play_l(){
  2. modifyFlags(SEEK_PREVIEW,CLEAR);
  3. modifyFlags(PLAYING,SET);
  4. modifyFlags(FIRST_FRAME,SET);
  5. //创建AudioPlayer
  6. if(mAudioSource!=NULL){
  7. if(mAudioPlayer==NULL){
  8. if(mAudioSink!=NULL){
  9. mAudioPlayer=newAudioPlayer(mAudioSink,this);
  10. mAudioPlayer->setSource(mAudioSource);
  11. mTimeSource=mAudioPlayer;
  12. //Iftherewasaseekrequestbeforeweeverstarted,
  13. //honortherequestnow.
  14. //Makesuretodothisbeforestartingtheaudioplayer
  15. //toavoidaracecondition.
  16. seekAudioIfNecessary_l();
  17. }
  18. }
  19. CHECK(!(mFlags&AUDIO_RUNNING));
  20. //如果只播放音频,则启动AudioPlayer
  21. if(mVideoSource==NULL){
  22. //Wedon'twanttopostanerrornotificationatthispoint,
  23. //theerrorreturnedfromMediaPlayer::start()willsuffice.
  24. status_terr=startAudioPlayer_l(
  25. false/*sendErrorNotification*/);
  26. if(err!=OK){
  27. deletemAudioPlayer;
  28. mAudioPlayer=NULL;
  29. modifyFlags((PLAYING|FIRST_FRAME),CLEAR);
  30. if(mDecryptHandle!=NULL){
  31. mDrmManagerClient->setPlaybackStatus(
  32. mDecryptHandle,Playback::STOP,0);
  33. }
  34. returnerr;
  35. }
  36. }
  37. }
  38. if(mTimeSource==NULL&&mAudioPlayer==NULL){
  39. mTimeSource=&mSystemTimeSource;
  40. }
  41. //启动视频回放
  42. if(mVideoSource!=NULL){
  43. //Kickoffvideoplayback
  44. postVideoEvent_l();
  45. if(mAudioSource!=NULL&&mVideoSource!=NULL){
  46. postVideoLagEvent_l();
  47. }
  48. }
  49. ...
  50. returnOK;
  51. }


1.1 创建AudioPlayer

创建AudioPlayer,创建之后,如果只播放音频,则调用AwesomePlayer::startAudioPlayer_l启动音频播放,在启动音频播放时,主要调用以下启动工作:

AudioPlayer::start->

mSource->start

mSource->read

mAudioSink->open

mAudioSink->start

1.2 启动视频回放

调用AwesomePlayer::postVideoEvent_l启动视频回放。此函数代码如下:

[cpp] view plain copy print ?
  1. voidAwesomePlayer::postVideoEvent_l(int64_tdelayUs){
  2. if(mVideoEventPending){
  3. return;
  4. }
  5. mVideoEventPending=true;
  6. mQueue.postEventWithDelay(mVideoEvent,delayUs<0?10000:delayUs);
  7. }


前面已经讲过,mQueue.postEventWithDelay发送一个事件到队列中,最终执行事件的fire函数。这些事件的初始化在AwesomePlayer::AwesomePlayer中进行。

[cpp] view plain copy print ?
  1. AwesomePlayer::AwesomePlayer()
  2. :mQueueStarted(false),
  3. mUIDValid(false),
  4. mTimeSource(NULL),
  5. mVideoRendererIsPreview(false),
  6. mAudioPlayer(NULL),
  7. mDisplayWidth(0),
  8. mDisplayHeight(0),
  9. mFlags(0),
  10. mExtractorFlags(0),
  11. mVideoBuffer(NULL),
  12. mDecryptHandle(NULL),
  13. mLastVideoTimeUs(-1),
  14. mTextPlayer(NULL){
  15. CHECK_EQ(mClient.connect(),(status_t)OK);
  16. DataSource::RegisterDefaultSniffers();
  17. mVideoEvent=newAwesomeEvent(this,&AwesomePlayer::onVideoEvent);
  18. mVideoEventPending=false;
  19. mStreamDoneEvent=newAwesomeEvent(this,&AwesomePlayer::onStreamDone);
  20. mStreamDoneEventPending=false;
  21. mBufferingEvent=newAwesomeEvent(this,&AwesomePlayer::onBufferingUpdate);
  22. mBufferingEventPending=false;
  23. mVideoLagEvent=newAwesomeEvent(this,&AwesomePlayer::onVideoLagUpdate);
  24. mVideoEventPending=false;
  25. mCheckAudioStatusEvent=newAwesomeEvent(
  26. this,&AwesomePlayer::onCheckAudioStatus);
  27. mAudioStatusEventPending=false;
  28. reset();
  29. }


现在明白了,对于mVideoEnent,最终将执行函数AwesomePlayer::onVideoEvent,一层套一层,再继续向下看看...

1.2.1 AwesomePlayer::onVideoEvent

相关简化代码如下:

[cpp] view plain copy print ?
  1. <SPANstyle="FONT-SIZE:10px">voidAwesomePlayer::postVideoEvent_l(int64_tdelayUs)
  2. {
  3. mQueue.postEventWithDelay(mVideoEvent,delayUs);
  4. }
  5. voidAwesomePlayer::onVideoEvent()
  6. {
  7. mVideoSource->read(&mVideoBuffer,&options);//获取解码后的YUV数据
  8. [CheckTimestamp]//进行AV同步
  9. mVideoRenderer->render(mVideoBuffer);//显示解码后的YUV数据
  10. postVideoEvent_l();//进行下一帧的显示
  11. }
  12. </SPAN>

1)调用OMXCodec::read创建mVideoBuffer

2)调用AwesomePlayer::initRenderer_l初始化mVideoRender

[cpp] view plain copy print ?
  1. if(USE_SURFACE_ALLOC//硬件解码
  2. &&!strncmp(component,"OMX.",4)
  3. &&strncmp(component,"OMX.google.",11)){
  4. //HardwaredecodersavoidtheCPUcolorconversionbydecoding
  5. //directlytoANativeBuffers,sowemustusearendererthat
  6. //justpushesthosebufferstotheANativeWindow.
  7. mVideoRenderer=
  8. newAwesomeNativeWindowRenderer(mNativeWindow,rotationDegrees);
  9. }else{//软件解码
  10. //Otherdecodersareinstantiatedlocallyandasaconsequence
  11. //allocatetheirbuffersinlocaladdressspace.Thisrenderer
  12. //thenperformsacolorconversionandcopytogetthedata
  13. //intotheANativeBuffer.
  14. mVideoRenderer=newAwesomeLocalRenderer(mNativeWindow,meta);
  15. }


3)调用AwesomePlayer::startAudioPlayer_l启动音频播放

4)然后再循环调用postVideoEvent_l来post mVideoEvent事件,以循环工作。

其主要对象及关系如下图所示:

2. AwesomePlayer数据流

AwesomePlayer的准备工作

分类: Android Media 2489人阅读 评论(3) 收藏 举报

1. 前提条件

本文以播放本地文件为例,且setDataSource时传入的是文件的url地址。

在Java中,若要播放一个本地文件,其代码如下:

MediaPlayer mp = new MediaPlayer();
mp.setDataSource(PATH_TO_FILE); ...... (1)
mp.prepareAsync(); ........................ (2)、(3)
当收到视频准备完毕,收到OnPreparedListener时
mp.start(); .......................... (4)

在AwesomePlayer中,则会看到相应的处理;

2. AwesomePlayer::setDataSource

为了能播放本地文件,需要通过AwesomePlayer::setDataSource来告诉AwesomePlayer播放url地址,AwesomePlayer也只是简单地把此url地址存入mUri和mStats.mURI中以备在prepare时使用。

3. AwesomePlayer::prepareAsync或AwesomePlayer::prepare

3.1 mQueue.start();

表面看是启动一个队例,实质上是创建了一个线程,此程入口函数为:TimedEventQueue::ThreadWrapper。真正的线程处理函数为:TimedEventQueue::threadEntry, 从TimedEventQueue::mQueue队列中读取事件,然后调用event->fire处理此事件。TimedEventQueue中的每一个事件都带有触发此事件的绝对时间,到时间之后才执行此事件的fire.

TimedEventQueue::Event的fire是一个纯虚函数,其实现由其派生类来实现,如在AwesomePlayer::prepareAsync_l中,创建了一个AwesomeEvent,然后通过mQueue.postEvent把事件发送到mQueue中,此时,fire函数为AwesomePlayer::onPrepareAsyncEvent.

3.2 AwesomePlayer::onPrepareAsyncEvent被执行

根据上面的描述,把事件发送到队列之后,队列线程将读取此线程的事件,然后执行event的fire. 3.1中事件的fire函数为AwesomePlayer::onPrepareAsyncEvent,其代码为:

[cpp] view plain copy print ?
  1. voidAwesomePlayer::onPrepareAsyncEvent(){
  2. Mutex::AutolockautoLock(mLock);
  3. ....
  4. if(mUri.size()>0){//获取mAudioTrack和mVideoTrack
  5. status_terr=finishSetDataSource_l();---3.2.1
  6. ...
  7. }
  8. if(mVideoTrack!=NULL&&mVideoSource==NULL){//获取mVideoSource
  9. status_terr=initVideoDecoder();---3.2.2
  10. ...
  11. }
  12. if(mAudioTrack!=NULL&&mAudioSource==NULL){//获取mAudioSource
  13. status_terr=initAudioDecoder();---3.2.3
  14. ...
  15. }
  16. modifyFlags(PREPARING_CONNECTED,SET);
  17. if(isStreamingHTTP()||mRTSPController!=NULL){
  18. postBufferingEvent_l();
  19. }else{
  20. finishAsyncPrepare_l();
  21. }
  22. }


3.2.1 finishSetDataSource_l

[cpp] view plain copy print ?
  1. {
  2. dataSource=DataSource::CreateFromURI(mUri.string(),...);(3.2.1.1)
  3. sp<MediaExtractor>extractor=
  4. MediaExtractor::Create(dataSource);.....(3.2.1.2)
  5. returnsetDataSource_l(extractor);.........................(3.2.1.3)
  6. }

3.2.1.1创建dataSource

a. 对于本地文件(http://,https://,rtsp://实现方式不一样)的实现方式如下:

dataSource = DataSource::CreateFromURI(mUri.string(), &mUriHeaders);

根据url创建dataSource,它实际上new了一个FileSource。当new FileSource时,它打开此文件:

mFd = open(filename, O_LARGEFILE | O_RDONLY);

b. 对于http://和https://,则new一个ChromiumHTTPDataSource,

这些类之间的派生关系如下图所示:

3.2.1.2 创建一个MediaExtractor

创建MediaExtractor::Create中创建真正的MediaExtractor,以下以MPEG2TSExtractor为例,它解析TS流,它也是一个空架子,它有传入的mDataSource给它读数据,并创建了一个mParser(ATSParser)来真正的数据解析。在此过程中产生的对象即拥有关系为:

MPEG2TSExtractor->ATSParser->ATSParser::Program->ATSParser::Stream->AnotherPacketSource

extractor = MediaExtractor::Create(dataSource);它解析source所指定的文件,并且根据其header来选择extractor(解析器)。其代码如下:

[cpp] view plain copy print ?
  1. sp<MediaExtractor>MediaExtractor::Create(
  2. constsp<DataSource>&source,constchar*mime){
  3. sp<AMessage>meta;
  4. String8tmp;
  5. if(mime==NULL){
  6. floatconfidence;
  7. if(!source->sniff(&tmp,&confidence,&meta)){
  8. returnNULL;
  9. }
  10. mime=tmp.string();
  11. }
  12. ...
  13. MediaExtractor*ret=NULL;
  14. if(!strcasecmp(mime,MEDIA_MIMETYPE_CONTAINER_MPEG4)
  15. ||!strcasecmp(mime,"audio/mp4")){
  16. ret=newMPEG4Extractor(source);
  17. }elseif(!strcasecmp(mime,MEDIA_MIMETYPE_AUDIO_MPEG)){
  18. ret=newMP3Extractor(source,meta);
  19. }elseif(!strcasecmp(mime,MEDIA_MIMETYPE_AUDIO_AMR_NB)
  20. ||!strcasecmp(mime,MEDIA_MIMETYPE_AUDIO_AMR_WB)){
  21. ret=newAMRExtractor(source);
  22. }elseif(!strcasecmp(mime,MEDIA_MIMETYPE_AUDIO_FLAC)){
  23. ret=newFLACExtractor(source);
  24. }elseif(!strcasecmp(mime,MEDIA_MIMETYPE_CONTAINER_WAV)){
  25. ret=newWAVExtractor(source);
  26. }elseif(!strcasecmp(mime,MEDIA_MIMETYPE_CONTAINER_OGG)){
  27. ret=newOggExtractor(source);
  28. }elseif(!strcasecmp(mime,MEDIA_MIMETYPE_CONTAINER_MATROSKA)){
  29. ret=newMatroskaExtractor(source);
  30. }elseif(!strcasecmp(mime,MEDIA_MIMETYPE_CONTAINER_MPEG2TS)){
  31. ret=newMPEG2TSExtractor(source);
  32. }elseif(!strcasecmp(mime,MEDIA_MIMETYPE_CONTAINER_AVI)){
  33. ret=newAVIExtractor(source);
  34. }elseif(!strcasecmp(mime,MEDIA_MIMETYPE_CONTAINER_WVM)){
  35. ret=newWVMExtractor(source);
  36. }elseif(!strcasecmp(mime,MEDIA_MIMETYPE_AUDIO_AAC_ADTS)){
  37. ret=newAACExtractor(source);
  38. }
  39. if(ret!=NULL){
  40. if(isDrm){
  41. ret->setDrmFlag(true);
  42. }else{
  43. ret->setDrmFlag(false);
  44. }
  45. }
  46. ...
  47. returnret;
  48. }

当然对于TS流,它将创建一个MPEG2TSExtractor并返回。

当执行new MPEG2TSExtractor(source)时:

1) 把传入的FileSource对象保存在MEPG2TSExtractor的mDataSource成没变量中

2) 创建一个ATSParser并保存在mParser中,它负责TS文件的解析,

3) 在feedMore中,通过mDataSource->readAt从文件读取数据,把读取的数据作为mParser->feedTSPacket的参数,它将分析PAT表(ATSParser::parseProgramAssociationTable)而找到并创建对应的Program,并把Program存入ATSParser::mPrograms中。每个Program有一个唯一的program_number和programMapPID.

扫盲一下,PAT中包含有所有PMT的PID,一个Program有一个对应的PMT,PMT中包含有Audio PID和Video PID.

ATSParser::Program::parseProgramMap中,它分析PMT表,并分别根据Audio和Video的PID,为他们分别创建一个Stream。然后把新创建的Stream保存在ATSParser::Program的mStreams成员变量中。

ATSParser::Stream::Stream构造函数中,它根据媒体类型,创建一个类型为ElementaryStreamQueue的对象mQueue;并创建一个类型为ABuffer的对象mBuffer(mBuffer = new ABuffer(192 * 1024);)用于保存数据 。

注:ATSParser::Stream::mSource<AnotherPacketSource>创建流程为:

MediaExtractor::Create->

MPEG2TSExtractor::MPEG2TSExtractor->

MPEG2TSExtractor::init->

MPEG2TSExtractor::feedMore->

ATSParser::feedTSPacket->

ATSParser::parseTS->

ATSParser::parsePID->

ATSParser::parseProgramAssociationTable

ATSParser::Program::parsePID->

ATSParser::Program::parseProgramMap

ATSParser::Stream::parse->

ATSParser::Stream::flush->

ATSParser::Stream::parsePES->

ATSParser::Stream::onPayloadData

以上source->sniff函数在DataSource::sniff中实现,这些sniff函数是通过DataSource::RegisterSniffer来进行注册的,如MEPG2TS的sniff函数为:SniffMPEG2TS,其代码如下:

[cpp] view plain copy print ?
  1. boolSniffMPEG2TS(
  2. constsp<DataSource>&source,String8*mimeType,float*confidence,
  3. sp<AMessage>*){
  4. for(inti=0;i<5;++i){
  5. charheader;
  6. if(source->readAt(kTSPacketSize*i,&header,1)!=1
  7. ||header!=0x47){
  8. returnfalse;
  9. }
  10. }
  11. *confidence=0.1f;
  12. mimeType->setTo(MEDIA_MIMETYPE_CONTAINER_MPEG2TS);
  13. returntrue;
  14. }

由此可见,这些sniff是根据文件开始的内容来识别各种file container. 比如wav文件通过其头中的RIFF或WAVE字符串来识别。注:在创建player时,是根据url中的相关信息来判断的,而不是文件的内容来判断

3.2.1.3 AwesomePlayer::setDataSource_l(extractor)

主要逻辑代码如下(当然此extractor实质为MPEG2TSExtractor对象):

[cpp] view plain copy print ?
  1. status_tAwesomePlayer::setDataSource_l(constsp<MediaExtractor>&extractor)
  2. {
  3. for(size_ti=0;i<extractor->countTracks();++i){
  4. ...
  5. if(!haveVideo&&!strncasecmp(mime,"video/",6))
  6. setVideoSource(extractor->getTrack(i));
  7. ...
  8. if(!haveAudio&&!strncasecmp(mime,"audio/",6))
  9. setAudioSource(extractor->getTrack(i));
  10. ...
  11. }
  12. }

先看看extractor->getTrack做了些什么?

它以MPEG2TSExtractor和AnotherPacketSource做为参数创建了一个MPEG2TSSource对象返回,然后AwesomePlayer把它保存在mVideoTrack或mAudioTrack中。

3.2.2 initVideoDecoder

主要代码如下:

[cpp] view plain copy print ?
  1. status_tAwesomePlayer::initVideoDecoder(uint32_tflags){
  2. if(mDecryptHandle!=NULL){
  3. flags|=OMXCodec::kEnableGrallocUsageProtected;
  4. }
  5. mVideoSource=OMXCodec::Create(//3.2.2.1
  6. mClient.interface(),mVideoTrack->getFormat(),
  7. false,//createEncoder
  8. mVideoTrack,
  9. NULL,flags,USE_SURFACE_ALLOC?mNativeWindow:NULL);
  10. if(mVideoSource!=NULL){
  11. int64_tdurationUs;
  12. if(mVideoTrack->getFormat()->findInt64(kKeyDuration,&durationUs)){
  13. Mutex::AutolockautoLock(mMiscStateLock);
  14. if(mDurationUs<0||durationUs>mDurationUs){
  15. mDurationUs=durationUs;
  16. }
  17. }
  18. status_terr=mVideoSource->start();//3.2.2.2
  19. if(err!=OK){
  20. mVideoSource.clear();
  21. returnerr;
  22. }
  23. }
  24. returnmVideoSource!=NULL?OK:UNKNOWN_ERROR;
  25. }

它主要做了两件事,1)创建一个OMXCodec对象,2)调用OMXCodec的start方法。注mClient.interface()返回为一个OMX对象。其创建流程如下:

AwesomePlayer::AwesomePlayer->

mClient.connect->

OMXClient::connect(获取OMX对象,并保存在mOMX)->

BpMediaPlayerService::getOMX->

BnMediaPlayerService::onTransact(GET_OMX)->

MediaPlayerService::getOMX


3.2.2.1 创建OMXCodec对象

从上面的代码中可以看出,其mVideoTrack参数为一个MPEG2TSSource对象。

1)从MPEG2TSSource的metadata中获取mime类型

2)调用OMXCodec::findMatchingCodecs从kDecoderInfo中寻找可以解此mime媒体类型的codec名,并放在matchingCodecs变量中

3)创建一个OMXCodecObserver对象

4)调用OMX::allocateNode函数,以codec名和OMXCodecObserver对象为参数,创建一个OMXNodeInstance对象,并把其makeNodeID的返回值保存在node(node_id)中。

5)以node,codec名,mime媒体类型,MPEG2TSSource对象为参数,创建一个OMXCodec对象,并把此OMXCodec对象保存在OMXCodecObserver::mTarget中

[cpp] view plain copy print ?
  1. OMXCodec::OMXCodec(
  2. constsp<IOMX>&omx,IOMX::node_idnode,
  3. uint32_tquirks,uint32_tflags,
  4. boolisEncoder,
  5. constchar*mime,
  6. constchar*componentName,
  7. constsp<MediaSource>&source,
  8. constsp<ANativeWindow>&nativeWindow)


6)调用OMXCodec::configureCodec并以MEPG2TSSource的MetaData为参数,对此Codec进行配置。

3.2.2.2 调用OMXCodec::start方法

1)它调用mSource->start,即调用MPEG2TSSource::start函数。

2)它又调用Impl->start,即AnotherPacketSource::start,真遗憾,这其中什么都没有做。只是return OK;就完事了。

3.2.3 initAudioDecoder

其流程基本上与initVideoDecoder类似。创建一个OMXCodec保存在mAudioSource中。

至此,AwesomePlayer的准备工作已经完成。其架构如下图所示:

更多相关文章

  1. android平台解析epub格式的书籍信息
  2. IntentService通过HandlerThread单独开启一个线程来处理所有Inte
  3. Android(安卓)学习笔记--android――listview总结
  4. 理解onMeasure
  5. Android动态设置edittext的hint属性显示的提示文字大小
  6. Android权威编程指南读书笔记(1-2章)
  7. android客户端程序访问服务器端webservice,几篇不错的文章!
  8. Android:使用ViewPager实现左右滑动切换图片 (简单版)
  9. 监控android binder size

随机推荐

  1. Android(安卓)调用H5界面(交互)
  2. java.lang.NullPointerException: Attemp
  3. linux下编译MTK android的环境搭建
  4. android项目迁移到androidX:类映射(android
  5. Android(安卓)Kotlin Activity笔记
  6. android创建文件夹以及向文件写入数据
  7. 内存优化三
  8. 【Android】ViewFlipper的使用
  9. android实现RecyclerView上拉加载,下拉更
  10. andorid底部菜单导航