5. Android MultiMedia框架完全解析 - 再谈Playback框架及一些学习方法的讨论

本文主要是介绍5. Android MultiMedia框架完全解析 - 再谈Playback框架及一些学习方法的讨论,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

经过前面的分析,我们大概知道了一些概念,但是这个StageFright在整个playback的位置是什么样的:

Android Media这一块的知识杂乱,而且有深度,有广度。深度的话,从app->JNI->C++->底层的OMX驱动,广度的话,每个播放器在播放的时候,会执行seek操作,快进,快退,暂停等等操作,同时,需要涉及读取源文件(从网络读取,从文件读取),解码(软解,硬解),Render等到一系列的操作,光听我这么说,就感觉很乱。

如果一味的追求深度,从app开始追代码的执行,一路追下去,可能追到最后,我追的初衷是什么,我刚开始想追的是什么都会忘记。所以这里插一章,讲解广度的内容。

 

这里可以总结为,需要“一横一竖”的分析,横是指:NuPlayerDriver.cpp中需要实现什么功能,都有哪些接口函数,包括必须调用的接口和可选的调用接口,并包含对这些接口的简单分析。而“竖”指:某一个必须调用的接口函数自上而下的调用过程。

 

这个博客分析,就是按照“一横一竖”的原则穿插进行。虽然同样可能会引起混乱,但是学习不会是一蹴而就的,更不会简单花两天时间就会理解这么多的东西。

 

首先,还是以播放一个本地文件为例,我们需要做什么?

本地文件是什么?

这里就需要一些基础知识了,以一个mp4文件为例,它是原始yuv数据,经过MPEG4编码后,再封装成的文件格式。我们想要播放它,就需要反其道而行之:

在这个播放器流程图中,首先需要将一个mp4文件读取出来,然后通过Parser/Demux来解封装,再通过解码器将压缩过的数据解码成yuv,最后将这些画面Render出来。

这些步骤就是一个完整播放器的需要完成的操作。

 

我们下面需要接触的NuPlayer,也是一个完整的播放器,它需要做的工作同样也是上图中的工作,只不过,在上面每一个步骤中,再封装提炼出几个Wrapper, 我这里先放出来一个图(或许很多概念和名字都不熟悉),我们以后的分析,就会按照这个图一步一步分析,在分析的过程中,也就好判断,分析到哪个位置了。先来看看NuPlayer所在的位置:

下面一步一步放大,来看看NuPlayer的框架图:

看这个图中,出现了很多新概念,现在挑几个重要的解释一下:

(1)Source:数据源,数据的来源不一定都是本地文件,也可能是网上的各种协议如:http,rtsp等等。source的任务就是把数据源抽象出来,为下一个Demux模块提供稳定的数据流,而Demux不用关心数据到底是怎么来的。GenericSource,它是NuPlayer::Source的一个子类,主要功能有:多媒体文件的格式探测,文件读取和解析。

(2)Parser/Demux:解复用,视频文件一般情况下都是把音视频的ES流交织的通过某种规则放在一起。这种规则就是容器规则。现在有很多不同的容器格式。如ts、mp4、flv、mkv、avi、rmvb等等。demux的功能就是把音视频的ES流从容器中剥离出来,然后分别送到不同的解码器中。其实音频和视频本身就是2个独立的系统。容器把它们包在了一起。但是他们都是独立解码的,所以解码之前,需要把它分别 独立出来。demux就是干这活的,他为下一步decoder解码提供了数据流。

(3)Decoder:解码器,播放器的核心模块。分为音频和视频解码器。影像在录制后,原始的音视频都是占用大量空间,而且是冗余度较高的数据。因此,通常会在制作的时候就会进行某种压缩 ( 压缩技术就是将数据中的冗余信息去除数据之间的相关性 )。这就是我们熟知的音视频编码格式,包括MPEG1(VCD)\ MPEG2(DVD)\ MPEG4 \ H.264 等等。音视频解码器的作用就是把这些压缩了的数据还原成原始的音视频数据。当然,编码解码过程基本上都是有损的,解码器的作用就是把编码后的数据还原成原始数据。按照MediaPlayer的框架,一般是调用MediaCodec完成解码,另外还有,解码器初始化,解码以及和其他模块的交互,比如Source,Renderer等。

(4)Renderer(渲染器),从功能上看,Renderer主要有几个功能:音视频原始数据缓存操作,音频播放(到声卡),视频显示(到显卡),音视频同步,其他辅助播放控制的操作。

(5)NuPlayer是这个播放框架中联系Source,Decoder,Renderer的纽带。

 

 

通常我们的调用逻辑是,构造函数->setDataSource->SetVideoSurfaceTexture->prepare/prepareAsync->start->stop->reset->析构函数,按照实际需求还会调用pause、isPlaying、getDuration、getCurrentPosition、setLooping、seekTo等。

 

在NuPlayerDriver.cpp中,NuPlayerDriver是继承自MediaPlayerInterface,可以看作是NuPlayer的Wrapper,提供了一个状态转换机制。

这个结构体里面比较重要的几个变量是:

State mState; //播放器的状态标志

sp<ALooper> mLooper; //内部消息驱动机制

sp<NuPlayer> mPlayer; //真正的NuPlayer

 

先来看NuPlayerDriver的构造函数:

NuPlayerDriver::NuPlayerDriver(pid_t pid)
:mState(STATE_IDLE),
mLooper(new ALooper),
mLooper->start(false, /* runOnCallingThread */true,  /* canCallJava */PRIORITY_AUDIO);
mPlayer = new NuPlayer(pid);
mLooper->registerHandler(mPlayer);
mPlayer->setDriver(this);

构造函数中最主要的作用是创建ALooper和NuPlayer实例,并将它们关联起来。

 

1. setDataSource

首先MediaPlayerService调用setDataSource函数,分别执行下面几步操作:

1.1

player_type playerType = MediaPlayerFactory::getPlayerType(this,fd,offset,length);

1.2

sp<MediaPlayerBase> p = setDataSource_pre(playerType);

通过这两个函数调用,就会获取到需要创建的播放器类型playerType,最终通过MediaPlayerFactory创建出NuPlayerDriver。

 

1.3

setDataSource_post(p, p->setDataSource(fd, offset, length));

这里创建NuPlayerDriver,并调用它的setDataSource函数,由于NuPlayerDriver是NuPlayer的一个Wrapper,在它的构造函数中,已经将mPlayer设置为NuPlayer,在NuPlayerDriver::setDataSource函数中就会去调用mPlayer->setDataSourceAsync(fd, offset, length);最终就跳转到NuPlayer::setDataSourceAsync函数中。

void NuPlayer::setDataSourceAsync(int fd, int64_t offset, int64_t length) {sp<AMessage> msg = new AMessage(kWhatSetDataSource, this);sp<AMessage> notify = new AMessage(kWhatSourceNotify, this);sp<GenericSource> source =new GenericSource(notify, mUIDValid, mUID);status_t err = source->setDataSource(fd, offset, length);if (err != OK) {ALOGE("Failed to set data source!");source = NULL;}msg->setObject("source", source);msg->post();
}

先解释一个Source的含义,每个需要播放的音视频,都有一个来源,这个来源就称为Source。这个Source位于struct NuPlayer中,他们有以下几种:

struct GenericSource; //本地资源

struct HTTPLiveSource; //http资源

struct StreamingSource; //流媒体资源

struct RTSPSource; //RTSP资源

这几个Source都继承自NuPlayer::Source,它同样位于struct NuPlayer中。

struct Source;

在这里是本地播放,所以创建的是GenericSource对象,之后调用它的setDataSource函数。同时发送kWhatSetDataSource消息。在onMessageReceived函数中,可以看到对这个消息的处理,调用了NuPlayerDriver的notifySetDataSourceCompleted函数,来对状态进行了一些处理(将状态设置成STATE_UNPREPARED),并通过mCondition.broadcast();来广播出去,同时NuPlayerDriver::setDataSource函数中通过mCondition.wait(mLock)一直在等待mState的状态发生改变。当接收到这个广播后,这个NuPlayerDriver::setDataSource函数就知道setdatasource已经执行完毕,就继续向下执行。重点是GenericSource的构造函数和source->setDataSource函数。

 

在NuPlayer.h中,还有一个sp<Source> mSource,这个结构体是保存现在使用的是哪一种Source,也就是说,在执行完GenericSource的setDataSource函数后,通过msg->setObject("source", source);将创建的GenericSource通过msg发送过去,然后在接收函数NuPlayer::onMessageReceived中,会将这个source赋值给mSource,通过这个操作,mSource中就保存了当前所使用的Source。

 

首先看GenericSource的构造函数,它传入了一个notify到Source中,同时在构造函数中通过DataSource::RegisterDefaultSniffers();函数,把那些Sniff函数注册进去。

NuPlayer::GenericSource::GenericSource(const sp<AMessage> &notify,bool uidValid,uid_t uid): Source(notify),mAudioTimeUs(0),mAudioLastDequeueTimeUs(0),mVideoTimeUs(0),mVideoLastDequeueTimeUs(0),mFetchSubtitleDataGeneration(0),mFetchTimedTextDataGeneration(0),mDurationUs(-1ll),mAudioIsVorbis(false),mIsWidevine(false),mIsSecure(false),mIsStreaming(false),mUIDValid(uidValid),mUID(uid),mFd(-1),mDrmManagerClient(NULL),mBitrate(-1ll),mPollBufferingGeneration(0),mPendingReadBufferTypes(0),mBuffering(false),mPrepareBuffering(false),mPrevBufferPercentage(-1) {resetDataSource();DataSource::RegisterDefaultSniffers();mTextTrackType = TextTrackType_3GPP;
}

这里需要注意的是,Source(notify)是NuPlayer::Source类的构造函数,这个构造函数为它的私有成员sp<AMessage> mNotify 赋值。在这个NuPlayer::setDataSourceAsync函数中,这个notify为kWhatSourceNotify。

 

这个source->setDataSource(fd, offset, length)最终是执行了NuPlayer::GenericSource::setDataSource函数。

 

这个notify在NuPlayer::onMessageReceived函数中,会去调用onSourceNotify函数,我感觉是为source提供的一个Amessage,source里面有什么情况,都通过这个notify与NuPlayer来沟通。具体的实现可能得到研究source时再具体探究。

 

 

 

2. setVideoSurfaceTexture

同样,是从MediaPlayerService开始调用这个函数,在这个函数中,会去创建Surface,(这里简单解释Surface,SurfaceFlinger,BufferQueue的知识。)

首先会去通过getPlayer()来获取Player,之后调用p->setVideoSurfaceTexture(bufferProducer);函数跳转到NuPlayer::setVideoSurfaceTextureAsync函数中,在这个函数中,只是发送了kWhatSetVideoSurface消息,继续跳转到NuPlayer::onMessageReceived函数中去执行:

NuPlayer::onMessageReceived

--->case kWhatSetVideoSurface:

--->performSetSurface(surface);

--->setVideoScalingMode(mVideoScalingMode)

在执行完setVideoScalingMode(mVideoScalingMode)函数后,NuPlayer::performSetSurface函数会去执行NuPlayerDriver的driver->notifySetSurfaceComplete()函数,然后同样回去通过条件变量去广播,通知NuPlayerDriver setVideoSurfaceTexture操作完成了。

 

 

3. prepareAsync

MediaPlayerService::Client::prepareAsync()函数首先会调用到NuPlayerDriver里面的NuPlayerDriver::prepareAsync函数,然后会根据NuPlayer的mState状态,选择去调用NuPlayer的prepareAsync函数还是seekToAsync(0, true /* needNotify */)函数。

先来看NuPlayer::prepareAsync函数,这个函数的实现很简单:

void NuPlayer::prepareAsync() {(new AMessage(kWhatPrepare, this))->post();
}

同样在NuPlayer::onMessageReceived函数中的实现也很简单:

case kWhatPrepare:{mSource->prepareAsync();break;}

就这样跳转到NuPlayer::GenericSource::prepareAsync()函数中,同样也只是发送了一个kWhatPrepareAsync信息,最终跳转到NuPlayer::GenericSource::onPrepareAsync()函数中。

 

首先会进入mDataSource = new FileSource(mFd, mOffset, mLength);中,执行FileSource的构造函数,在这个构造函数中,就是通过open函数来打开对应的文件。

继续在NuPlayer::GenericSource::onPrepareAsync()函数中执行,进入initFromDataSource();函数,在这个函数中会有关于Widevine的判断,可以查看网上的资源,这是有关DRM相关的知识,链接如下:

http://blog.csdn.net/beautyfuel/article/details/56277988

 

对于本地播放,它会依次执行下面的语句:

3.1

mDataSource = new FileSource(mFd, mOffset, mLength);

会根据mFd等等参数来创建mDataSource,这个mDataSource会在接下面的extractor create函数中使用。

 

3.2

这个函数的注释很好:init extractor from data source,也解释了这个函数主要的功能:创建Extractor,并调用Extractor来完成一些对Video/Audio的解析等等。

status_t NuPlayer::GenericSource::initFromDataSource() {
---> extractor = MediaExtractor::Create(mDataSource,mimeType.isEmpty() ? NULL : mimeType.string());

在这个函数中会调用source->sniff函数,由于上面创建的mDataSource是FileSource,而FileSource继承自DataSource,FileSource中没有实现sniff函数,所以这里调用的是DataSource里面的sniff函数,即:DataSource::sniff函数,通过这个函数去检测出媒体的类型,检测出媒体类型后,在log上打印出这样的话:

Autodetected media content as 'video/avi' with confidence 0.21

继续在MediaExtractor::Create函数中执行,会根据检测出来的媒体类型创建Extractor:

new FslExtractor(source,mime);

通过这个FslExtractor,就能够把这个文件解析出来,比如我们经常说的metadata,就是在extractor里面解析出来的,后面也使用到了这些内容。

 

继续到NuPlayer::GenericSource::initFromDataSource函数中去执行:

---> mFileMeta = extractor->getMetaData();if (mFileMeta != NULL) {int64_t duration;if (mFileMeta->findInt64(kKeyDuration, &duration)) {mDurationUs = duration;}

获取Metadata和Duration的值。

    int32_t totalBitrate = 0;size_t numtracks = extractor->countTracks();for (size_t i = 0; i < numtracks; ++i) {sp<MediaSource> track = extractor->getTrack(i);sp<MetaData> meta = extractor->getTrackMetaData(i);const char *mime;CHECK(meta->findCString(kKeyMIMEType, &mime));if (!strncasecmp(mime, "audio/", 6)) {if (mAudioTrack.mSource == NULL) {mAudioTrack.mIndex = i;mAudioTrack.mSource = track;mAudioTrack.mPackets =new AnotherPacketSource(mAudioTrack.mSource->getFormat());if (!strcasecmp(mime, MEDIA_MIMETYPE_AUDIO_VORBIS)) {mAudioIsVorbis = true;} else {mAudioIsVorbis = false;}}} else if (!strncasecmp(mime, "video/", 6)) {if (mVideoTrack.mSource == NULL) {mVideoTrack.mIndex = i;mVideoTrack.mSource = track;mVideoTrack.mPackets =new AnotherPacketSource(mVideoTrack.mSource->getFormat());// check if the source requires secure buffersint32_t secure;if (meta->findInt32(kKeyRequiresSecureBuffers, &secure)&& secure) {mIsSecure = true;if (mUIDValid) {extractor->setUID(mUID);}}}}if (track != NULL) {mSources.push(track);int64_t durationUs;if (meta->findInt64(kKeyDuration, &durationUs)) {if (durationUs > mDurationUs) {mDurationUs = durationUs;}}int32_t bitrate;if (totalBitrate >= 0 && meta->findInt32(kKeyBitRate, &bitrate)) {totalBitrate += bitrate;} else {totalBitrate = -1;}}}mBitrate = totalBitrate;return OK;
}

这里应该是将一个码流解析成几个Track,mAudioTrack,mVideoTrack,mSubtitleTrack,mTimedTextTrack,这几个track都是从video码流中解析出来的,每个track都是一个sp<MediaSource>。

但是提取出来track后,仅仅用这个结构体还是不能够完成描述一个track的,于是对于每一个track使用struct Track来描述,同时struct Track中包含这个sp<MediaSource>指针,指向从video中提取出来的track。

Track结构体的定义如下:

    struct Track {size_t mIndex;sp<MediaSource> mSource;sp<AnotherPacketSource> mPackets;};

它包含一个mIndex,这个mIndex就是此track在video中是第几个track,sp<MediaSource>就指向从video中提取出来的track,AnotherPacketSource继承自MediaSource,暂时还不知道它的作用。

 

(之后是 extractor->getMetaData()函数,什么是MetaData,直译过来叫元数据,其实是在Extractor中,它会在extractor->countTracks()等等操作中,获取到的时长,artist等等信息都保存在metadata中,然后才能够获取到。这个函数只是进行了创建MetaData,并设置媒体类型这个简单的操作。

然后就是extractor->countTracks()操作了,这个函数看上去很简单,但是它的实现真的非常复杂。)

上面这段话是以前写的,最后这句有点问题,以前认为很复杂的事情,在FSL代码中做了简化,因为以前说的复杂是软件的解码,这些操作确实复杂,但是在FslExtractor中,只是通过

IParser->getNumTracks(parserHandle, &trackCnt);就得到了track的数目,这些操作是lib_avi_parser_arm11_elinux.3.0.so库中做的,只是提供给了外面一些IParser->类的API函数,所以在这里的代码也就不复杂了,都是lib_avi_parser_arm11_elinux.3.0.so库中内部实现的。

(在FslExtractor::Init()函数中,首先执行CreateParserInterface(),然后执行ParseFromParser(),在这个函数中执行的IParser->getNumTracks(parserHandle, &trackCnt)得到的track的数目,然后保存在mTracks.size(),当需要执行FslExtractor::getTrack时,只是把mTracks.size()这个值返回。)

 

3.3

跳出initFromDataSource函数继续在NuPlayer::GenericSource::onPrepareAsync()函数中执行,

    if (mVideoTrack.mSource != NULL) {sp<MetaData> meta = doGetFormatMeta(false /* audio */);sp<AMessage> msg = new AMessage;err = convertMetaDataToMessage(meta, &msg);if(err != OK) {notifyPreparedAndCleanup(err);return;}notifyVideoSizeChanged(msg);}

这一段函数的意思是去设置Video的Size,为什么这样做呢?因为之前已经通过FslExtracor来获取到Video流的framesize,这里就需要重新设置一下,首先是doGetFormatMeta()函数,这个函数的形参是一个bool值,如果为true,就说明MediaSource为audio,同理,如果为false,MediaSource就为video。

在这里就是video了,之后通过convertMetaDataToMessage(meta, &msg);来从metadata里面提取所需的数据,并将某些数据转换正AMessage。这个函数很复杂,定义在Utils.cpp中。

之后调用NuPlayer::Source::notifyVideoSizeChanged函数来通知NuPlayer。(这里GenericSource是NuPlayer结构体中的一项,所以可以直接调用NuPlayer中的函数。)通过发送kWhatVideoSizeChanged,最终调用到NuPlayer::updateVideoSize函数。

 

3.4

finishPrepareAsync();

这个函数中执行了两个比较重要的函数,首先是startSources(),然后是notifyPrepared();

在NuPlayer::GenericSource::startSources()函数中,会去分别调用:

mAudioTrack.mSource->start()和mVideoTrack.mSource->start()函数来启动。

 

然后通过driver->notifyPrepareCompleted(err)()函数来通知NuPlayerDriver Prepare操作完成。

 

 

4. start函数

同样由MediaPlayerService发送start指令,经过NuPlayerDriver简单的判断处理,在NuPlayer::start() 中只是发送了一个kWhatStart消息,最终成功调用到NuPlayer::onStart函数。

4.1

首先去做的第一件事就是mSource->start(),在本地播放时,mSource就是GenericSource,所以这里就会调用NuPlayer::GenericSource::start()函数,

void NuPlayer::GenericSource::start() {ALOGI("start");mStopRead = false;if (mAudioTrack.mSource != NULL) {postReadBuffer(MEDIA_TRACK_TYPE_AUDIO);}if (mVideoTrack.mSource != NULL) {postReadBuffer(MEDIA_TRACK_TYPE_VIDEO);}setDrmPlaybackStatusIfNeeded(Playback::START, getLastReadPosition() / 1000);mStarted = true;(new AMessage(kWhatStart, this))->post();
}

在这个函数中,首先通过postReadBuffer函数来启动音视频的buffer流动,然后发送kWhatStart消息。先来看看NuPlayer::GenericSource::postReadBuffer函数中做了什么,发现它内部发送了一个kWhatReadBuffer消息,继续追踪,只调用了一个函数onReadBuffer(msg),继续调用到readBuffer(trackType);函数,这个函数根据传入的track类型来选择最大的buffer数目,然后执行

err = track->mSource->read(&mbuf, &options);

这里有点晕,这里的track->mSource是什么?与这里的GenericSource有什么关系?再缕缕。

先来想想track是在哪里初始化的?

FslExtractor::Init()

---> FslExtractor::ParseFromParser()

在这个函数中,会根据IParser->createParser2等等函数,将库与外面的函数连接起来,通过IParser->getNumTracks函数得到这个文件中track的数目,然后在FslExtractor::ParseMediaFormat()函数中通过ParseAudio,ParseVideo,ParseText等等函数解析出来这些track,并通过mTracks.push();将这些track以mSource变量的形式添加到mTracks这个Vector中,当使用这些track的时候,需要通过FslExtractor::getTrack来获得。

所以这里的track->mSource就是指解析出来的这些track,然后调用的是read函数,那么这个read函数是在哪里实现的?

首先来看track->mSource,他是struct MediaSource类型的变量,但是在这个类型的头文件中,只看到了read函数是一个虚函数,也就是说,这个read函数是需要struct MediaSource的子类自己实现的函数,在FslExtractor.cpp文件中,核心结构体是FslExtractor,它继承自class FslExtractor : public MediaExtractor,这时候思考到,既然FSL打算自己去实现这个Extractor,那么理应也有这个MediaSource对应的子类,所以在FslExtractor.cpp文件中找到了 FslMediaSource这个结构体,它继承自MediaSource结构体,实现了FslMediaSource::read()函数。

最终在read函数中通过while循环来不停解析track数据:

while (mPendingFrames.empty()) {status_t err = mExtractor->GetNextSample(mSourceIndex,false);
}

4.2

    mRenderer = new Renderer(mAudioSink, notify, flags);mRendererLooper = new ALooper;mRendererLooper->setName("NuPlayerRenderer");mRendererLooper->start(false, false, ANDROID_PRIORITY_AUDIO);mRendererLooper->registerHandler(mRenderer);mRenderer->setPlaybackSettings(mPlaybackSettings);

进行Render相关的设置。

 

4.3

继续看postScanSources()函数,在这个函数中有对解码器相关的操作。

instantiateDecoder(false, &mVideoDecoder);
instantiateDecoder(true, &mAudioDecoder);

通过这两个函数,就分别初始化了音频和视频的解码器,稍微简单看看函数内部做了什么:

NuPlayer::instantiateDecoder(bool audio, sp<DecoderBase> *decoder)
*decoder = new Decoder(notify, mSource, mPID, mRenderer, mSurface, mCCDecoder);(*decoder)->init();
(*decoder)->configure(format);

这是大致整个NuPlayer的播放流程,对他们先有一个理性的认识,后面再具体分析分析他们。

这篇关于5. Android MultiMedia框架完全解析 - 再谈Playback框架及一些学习方法的讨论的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/304662

相关文章

Nginx设置连接超时并进行测试的方法步骤

《Nginx设置连接超时并进行测试的方法步骤》在高并发场景下,如果客户端与服务器的连接长时间未响应,会占用大量的系统资源,影响其他正常请求的处理效率,为了解决这个问题,可以通过设置Nginx的连接... 目录设置连接超时目的操作步骤测试连接超时测试方法:总结:设置连接超时目的设置客户端与服务器之间的连接

Java判断多个时间段是否重合的方法小结

《Java判断多个时间段是否重合的方法小结》这篇文章主要为大家详细介绍了Java中判断多个时间段是否重合的方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录判断多个时间段是否有间隔判断时间段集合是否与某时间段重合判断多个时间段是否有间隔实体类内容public class D

Python使用国内镜像加速pip安装的方法讲解

《Python使用国内镜像加速pip安装的方法讲解》在Python开发中,pip是一个非常重要的工具,用于安装和管理Python的第三方库,然而,在国内使用pip安装依赖时,往往会因为网络问题而导致速... 目录一、pip 工具简介1. 什么是 pip?2. 什么是 -i 参数?二、国内镜像源的选择三、如何

IDEA编译报错“java: 常量字符串过长”的原因及解决方法

《IDEA编译报错“java:常量字符串过长”的原因及解决方法》今天在开发过程中,由于尝试将一个文件的Base64字符串设置为常量,结果导致IDEA编译的时候出现了如下报错java:常量字符串过长,... 目录一、问题描述二、问题原因2.1 理论角度2.2 源码角度三、解决方案解决方案①:StringBui

Linux使用nload监控网络流量的方法

《Linux使用nload监控网络流量的方法》Linux中的nload命令是一个用于实时监控网络流量的工具,它提供了传入和传出流量的可视化表示,帮助用户一目了然地了解网络活动,本文给大家介绍了Linu... 目录简介安装示例用法基础用法指定网络接口限制显示特定流量类型指定刷新率设置流量速率的显示单位监控多个

Java覆盖第三方jar包中的某一个类的实现方法

《Java覆盖第三方jar包中的某一个类的实现方法》在我们日常的开发中,经常需要使用第三方的jar包,有时候我们会发现第三方的jar包中的某一个类有问题,或者我们需要定制化修改其中的逻辑,那么应该如何... 目录一、需求描述二、示例描述三、操作步骤四、验证结果五、实现原理一、需求描述需求描述如下:需要在

JavaScript中的reduce方法执行过程、使用场景及进阶用法

《JavaScript中的reduce方法执行过程、使用场景及进阶用法》:本文主要介绍JavaScript中的reduce方法执行过程、使用场景及进阶用法的相关资料,reduce是JavaScri... 目录1. 什么是reduce2. reduce语法2.1 语法2.2 参数说明3. reduce执行过程

C#中读取XML文件的四种常用方法

《C#中读取XML文件的四种常用方法》Xml是Internet环境中跨平台的,依赖于内容的技术,是当前处理结构化文档信息的有力工具,下面我们就来看看C#中读取XML文件的方法都有哪些吧... 目录XML简介格式C#读取XML文件方法使用XmlDocument使用XmlTextReader/XmlTextWr

Android 悬浮窗开发示例((动态权限请求 | 前台服务和通知 | 悬浮窗创建 )

《Android悬浮窗开发示例((动态权限请求|前台服务和通知|悬浮窗创建)》本文介绍了Android悬浮窗的实现效果,包括动态权限请求、前台服务和通知的使用,悬浮窗权限需要动态申请并引导... 目录一、悬浮窗 动态权限请求1、动态请求权限2、悬浮窗权限说明3、检查动态权限4、申请动态权限5、权限设置完毕后

C++初始化数组的几种常见方法(简单易懂)

《C++初始化数组的几种常见方法(简单易懂)》本文介绍了C++中数组的初始化方法,包括一维数组和二维数组的初始化,以及用new动态初始化数组,在C++11及以上版本中,还提供了使用std::array... 目录1、初始化一维数组1.1、使用列表初始化(推荐方式)1.2、初始化部分列表1.3、使用std::