Android 原生ExoPlayer 解析

1.简述与应用范围

ExpPlayer是一个开源的,App等级的媒体API,它的开源项目包含了library和示例。

ExoPlayer相较于MediaPlayer有很多优点:

1. 支持基于http的移动流媒体协议,包括DASH,HSL,Smooth Stream。同时也支持文件流和udp流等。2. 支持更多媒体封装格式,包括mp4,mp3,Webm,aac,mkv,mpeg-ts。3. 支持DRM(Digital Right Management 数字版权管理)。4. 支持HD高清播放。5. 支持自定义和拓展使用场景。

2.上层调用方式

(本节说明重点为demo。)

简单来说,上层调用方式基本为:

PlayerActivity -> DemoPlayer -> ExoPlayer PlayerActivity -> RendererBuilder -> ExtractorRendererBuilder 

类图为:

(demo类图)

其中PlayerActivity面向UI层,一方面控制了播放器DemoPlayer,一方面选择了Renderer。

这里的Renderer指定了数据源格式、解码方式和缓冲区大小等。(说明,这里的缓冲区大小指RollingSampleBuffer的大小,不会影响进入播放的速度,只会影响缓存数据的最大值)

ExoPlayer则是媒体API接口。

DemoPlayer中直接封装了ExoPlayer和相关回调接口,负责播放器的逻辑控制和传入SurfaceView等操作,而非播放器的内部原理。

这里通过时序图来说明Demo中几个类的调用和封装方式。

(demo时序图)

3.代码结构

简单来说,代码结构是这样:

ExoPlayer ->ExoPlayerImpl -> ExoPlayerImplInternal -> TrackRenderer MediaCodecVideoTrackRenderer & MediaCodecAudioTrackRenderer ->  MediaCodecTrackRenderer -> SampleSourceTrackRenderer -> SampleSource,SampleSourceReader ExtractorSampleSource -> DataSource & Extractor & Loader 

(ExoPlayerImplInternal类图)

这里,ExoPlayer为接口。ExoPlayerImpl为实现,实现的一些详细步骤在ExoPlayerImplInternal中。后者用Handler消息机制进行异步通信,必要时会阻塞。

TrackRenderer是渲染器接口。

MediaCodecTrackRenderer中加入了MediaCodec(Android硬解码)。这里能看出,ExoPlayer用的是硬解,并且要求4.1以上Android系统。

SampleSourceTrackRenderer中调用了SampleSource,SampleSourceReader接口。SampleSource在这里指的是解封装后的媒体数据。

ExtractorSampleSource相当于一个核心控制器,它实现了SampleSource和SampleSourceReader接口。它通过实际的控制线程Loader,把从某DataSource即数据源中传过来的原始数据,传递给某Extractor来解封装。原始数据解析成SampleSource后,储存在RollingSampleBuffer即环形缓冲区中。

MediaCodecTrackRenderer会间接通过ExtractorSampleSource间接从RollingSampleBuffer中读取数据并渲染成画面,显示到SurfaceView中。

最后的过程有些复杂,流程图如下所示:

(ExtractorSampleSource流程图)

4.代码原理

1.ExoPlayer -> ExoPlayerImpl -> ExoPlayerImplInternal

(类图)

通过以下这段ExoPlayerImpl的构造方法代码,可以看出来ExoPlayerImpl中持有一个ExoPlayerImplInternal对象来控制播放器。创建ExoPlayerImplInternal对象时传入了一个eventHandler对象,把底层的错误信息和状态改变信息传递给上层处理。

ExoPlayerImpl类中构造方法:eventHandler = new Handler() {@Overridepublic void handleMessage(Message msg) {ExoPlayerImpl.this.handleEvent(msg);}
};
internalPlayer = new ExoPlayerImplInternal(eventHandler, playWhenReady, selectedTrackIndices,minBufferMs, minRebufferMs);

具体的功能性代码块,都在ExoPlayerImplInternal中实现。

状态改变信息和错误信息会通过eventHandler传上来进行处理。

ExoPlayerImpl类:// Not private so it can be called from an inner class without going through
// a thunk method.
/* package */ void handleEvent(Message msg) {switch (msg.what) {case ExoPlayerImplInternal.MSG_PREPARED: {System.arraycopy(msg.obj, 0, trackFormats, 0, trackFormats.length);playbackState = msg.arg1;for (Listener listener : listeners) {listener.onPlayerStateChanged(playWhenReady, playbackState);}break;}case ExoPlayerImplInternal.MSG_STATE_CHANGED: {playbackState = msg.arg1;for (Listener listener : listeners) {listener.onPlayerStateChanged(playWhenReady, playbackState);}break;}case ExoPlayerImplInternal.MSG_SET_PLAY_WHEN_READY_ACK: {pendingPlayWhenReadyAcks--;if (pendingPlayWhenReadyAcks == 0) {for (Listener listener : listeners) {listener.onPlayWhenReadyCommitted();}}break;}case ExoPlayerImplInternal.MSG_ERROR: {ExoPlaybackException exception = (ExoPlaybackException) msg.obj;for (Listener listener : listeners) {listener.onPlayerError(exception);}break;}}
}

这里的listeners是一个CopyOnWriteArrayList,里面的对象都是Listener,这里用的是一个观察者模式,用于给上层监听回调消息。上层即DemoPlayer或是EventLogger都在这里注册或注销监听。

2.ExoPlayerImplInternal -> TrackRenderer -> SampleSource,SampleSourceReader -> ExtractorSampleSource

(流程图)

1)ExoPlayerImplInternal中消息机制

    ExoPlayerImplInternal类中构造方法:internalPlaybackThread = new PriorityHandlerThread(getClass().getSimpleName() + ":Handler",Process.THREAD_PRIORITY_AUDIO);internalPlaybackThread.start();handler = new Handler(internalPlaybackThread.getLooper(), this);

ExoPlayerImplInternal实现了Handler.Callback接口:

ExoPlayerImplInternal类:@Override
public boolean handleMessage(Message msg) {try {switch (msg.what) {case MSG_PREPARE: {prepareInternal((TrackRenderer[]) msg.obj);return true;}case MSG_INCREMENTAL_PREPARE: {incrementalPrepareInternal();return true;}case MSG_SET_PLAY_WHEN_READY: {setPlayWhenReadyInternal(msg.arg1 != 0);return true;}case MSG_DO_SOME_WORK: {doSomeWork();return true;}case MSG_SEEK_TO: {seekToInternal(Util.getLong(msg.arg1, msg.arg2));return true;}case MSG_STOP: {stopInternal();return true;}case MSG_RELEASE: {releaseInternal();return true;}case MSG_CUSTOM: {sendMessageInternal(msg.arg1, msg.obj);return true;}case MSG_SET_RENDERER_SELECTED_TRACK: {setRendererSelectedTrackInternal(msg.arg1, msg.arg2);return true;}default:return false;}} catch (ExoPlaybackException e) {Log.e(TAG, "Internal track renderer error.", e);eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget();stopInternal();return true;} catch (RuntimeException e) {Log.e(TAG, "Internal runtime error.", e);eventHandler.obtainMessage(MSG_ERROR, new ExoPlaybackException(e, true)).sendToTarget();stopInternal();return true;}
}

通过这段代码,可以看出来,在ExoPlayerImplInternal内部是通过消息来控制播放器逻辑(控制TrackRenderer)。

2)doSomeWork分析及作用

ExoPlayerImplInternal类:private void doSomeWork() throws ExoPlaybackException {TraceUtil.beginSection("doSomeWork");long operationStartTimeMs = SystemClock.elapsedRealtime();long bufferedPositionUs = durationUs != TrackRenderer.UNKNOWN_TIME_US ? durationUs : Long.MAX_VALUE;boolean allRenderersEnded = true;boolean allRenderersReadyOrEnded = true;updatePositionUs();// 笔记:更新positionUsfor (int i = 0; i < enabledRenderers.size(); i++) {TrackRenderer renderer = enabledRenderers.get(i);// TODO: Each renderer should return the maximum delay before which// it wishes to be// invoked again. The minimum of these values should then be used as// the delay before the next// invocation of this method.// 笔记:这里调用了renderer的doSomeWork方法并传入了positionUs,//      elapsedRealtimeUs是个独立的系统时间参考renderer.doSomeWork(positionUs, elapsedRealtimeUs);allRenderersEnded = allRenderersEnded && renderer.isEnded();// Determine whether the renderer is ready (or ended). If it's not,// throw an error that's// preventing the renderer from making progress, if such an error// exists.boolean rendererReadyOrEnded = rendererReadyOrEnded(renderer);if (!rendererReadyOrEnded) {renderer.maybeThrowError();}allRenderersReadyOrEnded = allRenderersReadyOrEnded && rendererReadyOrEnded;if (bufferedPositionUs == TrackRenderer.UNKNOWN_TIME_US) {// We've already encountered a track for which the buffered// position is unknown. Hence the// media buffer position unknown regardless of the buffered// position of this track.} else {long rendererDurationUs = renderer.getDurationUs();long rendererBufferedPositionUs = renderer.getBufferedPositionUs();if (rendererBufferedPositionUs == TrackRenderer.UNKNOWN_TIME_US) {bufferedPositionUs = TrackRenderer.UNKNOWN_TIME_US;} else if (rendererBufferedPositionUs == TrackRenderer.END_OF_TRACK_US|| (rendererDurationUs != TrackRenderer.UNKNOWN_TIME_US&& rendererDurationUs != TrackRenderer.MATCH_LONGEST_US&& rendererBufferedPositionUs >= rendererDurationUs)) {// This track is fully buffered.} else {bufferedPositionUs = Math.min(bufferedPositionUs, rendererBufferedPositionUs);}}}// 笔记:更新缓冲位置,主要用于上层回调this.bufferedPositionUs = bufferedPositionUs;// 笔记:根据durationUs和positionUs来判断状态和开关渲染器(Renderer)if (allRenderersEnded && (durationUs == TrackRenderer.UNKNOWN_TIME_US || durationUs <= positionUs)) {setState(ExoPlayer.STATE_ENDED);stopRenderers();} else if (state == ExoPlayer.STATE_BUFFERING && allRenderersReadyOrEnded) {setState(ExoPlayer.STATE_READY);if (playWhenReady) {startRenderers();}} else if (state == ExoPlayer.STATE_READY && !allRenderersReadyOrEnded) {rebuffering = playWhenReady;setState(ExoPlayer.STATE_BUFFERING);stopRenderers();}// 笔记:准备再次调用doSomeworkhandler.removeMessages(MSG_DO_SOME_WORK);if ((playWhenReady && state == ExoPlayer.STATE_READY) || state == ExoPlayer.STATE_BUFFERING) {scheduleNextOperation(MSG_DO_SOME_WORK, operationStartTimeMs, RENDERING_INTERVAL_MS);} else if (!enabledRenderers.isEmpty()) {scheduleNextOperation(MSG_DO_SOME_WORK, operationStartTimeMs, IDLE_INTERVAL_MS);}TraceUtil.endSection();
}private void scheduleNextOperation(int operationType, long thisOperationStartTimeMs, long intervalMs) {long nextOperationStartTimeMs = thisOperationStartTimeMs + intervalMs;long nextOperationDelayMs = nextOperationStartTimeMs - SystemClock.elapsedRealtime();if (nextOperationDelayMs <= 0) {handler.sendEmptyMessage(operationType);} else {handler.sendEmptyMessageDelayed(operationType, nextOperationDelayMs);}
}// 笔记:通过上层传入的eventHandler把状态改变信息传递给上层
private void setState(int state) {if (this.state != state) {this.state = state;eventHandler.obtainMessage(MSG_STATE_CHANGED, state, 0).sendToTarget();}
}

doSomeWork方法是在播放器执行完prepare后执行的。是在准备动作都完成后,具体控制播放器开始渲染画面的方法。 
在以上代码中我们可以看出来,这里完成的主要动作有:

1. 更新positionUs(以及elapsedRealtimeUs)
2. renderer.doSomeWork
3. 把播放状态回调上层
4. 定时执行下一次doSomeWork

3)updataPositionUs和renderer.doSomeWork分析

positionUs指的是实际渲染位置。

ExoPlayerImplInternal类:private void updatePositionUs() {if (rendererMediaClock != null && enabledRenderers.contains(rendererMediaClockSource)&& !rendererMediaClockSource.isEnded()) {positionUs = rendererMediaClock.getPositionUs();standaloneMediaClock.setPositionUs(positionUs);} else {positionUs = standaloneMediaClock.getPositionUs();}elapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000;
}

通过这段在ExoPlayerImplInternal类中的代码,我们看出,这有两个分支,第一个分支主要是用于有音频的情况下,音频时间可以作为整体参考时间,来调整positionUs。第二个分支是没有音频的情况下,用系统独立时钟作为整体参考时间,来调整positionUs。

MediaCodecTrackRenderer类:@Override
protected void doSomeWork(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {// 笔记:判断是否应该继续缓冲sourceState = continueBufferingSource(positionUs)? (sourceState == SOURCE_STATE_NOT_READY ? SOURCE_STATE_READY : sourceState) : SOURCE_STATE_NOT_READY;// 笔记:判断解码是否连续,如果不连续,则重启解码器checkForDiscontinuity(positionUs);if (format == null) {// 笔记:读取格式readFormat(positionUs);}if (codec == null && shouldInitCodec()) {// 笔记:当有格式无解码器时,开启解码器maybeInitCodec();}if (codec != null) {TraceUtil.beginSection("drainAndFeed");// 笔记:如果解码器中可以输出缓冲,则会返回true,否则返回falsewhile (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {}// 笔记:如果解码器还可以输入原始帧,则返回true,否则返回false,第二个参数代表是否首次执行if (feedInputBuffer(positionUs, true)) {while (feedInputBuffer(positionUs, false)) {}}TraceUtil.endSection();}codecCounters.ensureUpdated();
}

positionUs传递给了drainOutputBuffer方法和feedInputBuffer方法。用于调整播放时间,和获取缓冲帧。

drainOutputBuffer方法调用到了processOutputBuffer方法,这里处理缓冲帧。这个方法在MediaCodecTrackRenderer类中是个抽象方法,具体实现在MediaCodecVideoTrackRenderer和MediaCodecAudioTrackRenderer类中。

MediaCodecVideoTrackRenderer类:// 笔记:返回true意味着输出的缓冲帧已经被渲染,false意味着尚未被渲染
@Override
protected boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, MediaCodec codec, ByteBuffer buffer,MediaCodec.BufferInfo bufferInfo, int bufferIndex, boolean shouldSkip) {if (shouldSkip) {skipOutputBuffer(codec, bufferIndex);return true;}if (!renderedFirstFrame) {if (Util.SDK_INT >= 21) {renderOutputBufferV21(codec, bufferIndex, System.nanoTime());} else {renderOutputBuffer(codec, bufferIndex);}return true;}if (getState() != TrackRenderer.STATE_STARTED) {return false;}// Compute how many microseconds it is until the buffer's presentation// time.long elapsedSinceStartOfLoopUs = (SystemClock.elapsedRealtime() * 1000) - elapsedRealtimeUs;long earlyUs = bufferInfo.presentationTimeUs - positionUs - elapsedSinceStartOfLoopUs;// Compute the buffer's desired release time in nanoseconds.long systemTimeNs = System.nanoTime();long unadjustedFrameReleaseTimeNs = systemTimeNs + (earlyUs * 1000);// Apply a timestamp adjustment, if there is one.long adjustedReleaseTimeNs = frameReleaseTimeHelper.adjustReleaseTime(bufferInfo.presentationTimeUs,unadjustedFrameReleaseTimeNs);earlyUs = (adjustedReleaseTimeNs - systemTimeNs) / 1000;// 笔记:以上是通过positionUs(实际渲染位置),elapsedRealtimeUs(独立时钟位置),//      bufferInfo.presentationTimeUs(缓冲帧位置)得出缓冲位置和播放位置之间的时间差值。// 笔记:如果渲染位置在此缓冲帧位置后面30ms,则弃掉此帧if (earlyUs < -30000) {// We're more than 30ms late rendering the frame.dropOutputBuffer(codec, bufferIndex);return true;}if (Util.SDK_INT >= 21) {// 笔记:如果系统api在21以上,则可以在framework层控制渲染速度// Let the underlying framework time the release.// 笔记:如果渲染位置在缓冲帧位置50毫秒之前,就return false。否则则渲染。if (earlyUs < 50000) {renderOutputBufferV21(codec, bufferIndex, adjustedReleaseTimeNs);return true;}} else {// 笔记:如果系统api在21以下,我们需要自己控制渲染速度// We need to time the release ourselves.if (earlyUs < 30000) {// 笔记:如果渲染位置和缓冲帧位置之差在30毫秒和11毫秒之间,则推迟至少1毫秒再渲染。//      如果在11毫秒以内,则直接渲染。if (earlyUs > 11000) {// We're a little too early to render the frame. Sleep until// the frame can be rendered.// Note: The 11ms threshold was chosen fairly arbitrarily.try {// Subtracting 10000 rather than 11000 ensures the sleep// time will be at least 1ms.Thread.sleep((earlyUs - 10000) / 1000);} catch (InterruptedException e) {Thread.currentThread().interrupt();}}renderOutputBuffer(codec, bufferIndex);return true;}}// We're either not playing, or it's not time to render the frame yet.// 笔记:return false的意思是,我们既不播放,而且也不渲染这帧。return false;
}

在renderOutputBuffer中,

codec.releaseOutputBuffer(bufferIndex, true);

通过releaseOutputBuffer方法把相关帧播放到surface中。

以上是通过positionUs调整缓冲时间以及播放缓冲帧的代码。

在feedInputBuffer中,

result = readSource(positionUs, formatHolder, sampleHolder, false);

通过readSource,调用到了ExtractorSampleSource中的readData方法,从rollingBuffer中取到了数据。

这是通过positionUs获取缓冲帧的代码。

通过这些代码可以分析出,如果positionUs获取错误的话,那么会直接影响到播放流程中从缓冲区获取数据和解码器渲染数据等功能。

3.ExtractorSampleSource -> DataSource & Extractor & Loader

(类图)

1)ExtractingLoadable分析

ExtractingLoadable是一个ExtractorSampleSource中的内部类。它实现了Loadable接口。Loadable接口应用于Loader,后者是一个异步线程。在这里主要用于从DataSource数据源中获取数据放进RollingSampleBuffer即缓冲区中。

/*** Loads the media stream and extracts sample data from it.*/
private static class ExtractingLoadable implements Loadable {private final Uri uri;private final DataSource dataSource;private final ExtractorHolder extractorHolder;private final Allocator allocator;private final int requestedBufferSize;private final PositionHolder positionHolder;private volatile boolean loadCanceled;private boolean pendingExtractorSeek;public ExtractingLoadable(Uri uri, DataSource dataSource, ExtractorHolder extractorHolder, Allocator allocator,int requestedBufferSize, long position) {this.uri = Assertions.checkNotNull(uri);this.dataSource = Assertions.checkNotNull(dataSource);this.extractorHolder = Assertions.checkNotNull(extractorHolder);this.allocator = Assertions.checkNotNull(allocator);this.requestedBufferSize = requestedBufferSize;positionHolder = new PositionHolder();positionHolder.position = position;pendingExtractorSeek = true;}// 笔记:用于控制线程的关闭@Overridepublic void cancelLoad() {loadCanceled = true;}@Overridepublic boolean isLoadCanceled() {return loadCanceled;}@Overridepublic void load() throws IOException, InterruptedException {int result = Extractor.RESULT_CONTINUE;while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {ExtractorInput input = null;try {long position = positionHolder.position;// 笔记:开打数据源,这里C.LENGTH_UNBOUNDED值为-1long length = dataSource.open(new DataSpec(uri, position, C.LENGTH_UNBOUNDED, null));if (length != C.LENGTH_UNBOUNDED) {length += position;}// 笔记:这里的ExtractorInput是一个对于数据源、读取位置、读取长度的封装//      用于向Extractor输入数据input = new DefaultExtractorInput(dataSource, position, length);// 笔记:通过数据选择正确的Extractor即文件封装拆解器Extractor extractor = extractorHolder.selectExtractor(input);if (pendingExtractorSeek) {extractor.seek();pendingExtractorSeek = false;}// 笔记:这个循环用于从Extractor中不断读取数据,放进RollingSampleBuffer中while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {allocator.blockWhileTotalBytesAllocatedExceeds(requestedBufferSize);result = extractor.read(input, positionHolder);// TODO: Implement throttling to stop us from buffering// data too often.}} finally {if (result == Extractor.RESULT_SEEK) {result = Extractor.RESULT_CONTINUE;} else if (input != null) {positionHolder.position = input.getPosition();}// 笔记:关闭数据源dataSource.close();}}}}

我们可以看出,线程中进行的主要动作是

1. dataSource.open,即打开数据源
2. Extractor extractor = extractorHolder.selectExtractor(input),选择正确的文件封装拆解器
3. result = extractor.read(input, positionHolder),从数据源中读取数据
4. dataSource.close,关闭数据源

2)ExtractorHolder分析

ExtractorHolder也是一个ExtractorSampleSource中的内部类。它主要负责持有Extractor。

    ExtractorHolder类:public Extractor selectExtractor(ExtractorInput input)throws UnrecognizedInputFormatException, IOException, InterruptedException {if (extractor != null) {return extractor;}for (Extractor extractor : extractors) {try {// 笔记:一旦识别到正确的解析器,则会返回trueif (extractor.sniff(input)) {this.extractor = extractor;break;}} catch (EOFException e) {// Do nothing.}input.resetPeekPosition();}if (extractor == null) {throw new UnrecognizedInputFormatException(extractors);}// 笔记:这里调用了extractor.init即初始化extractor.init(extractorOutput);return extractor;}

3)Extractor分析

Extractor是个接口,表示文件封装解析器。里面主要有四个方法:

void init(ExtractorOutput output);boolean sniff(ExtractorInput input) throws IOException, InterruptedException;int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException;void seek();

read方法是阻塞的。每次调用read只会获取一小部分数据。

同时这里定义了三个read方法的特殊返回值:

RESULT_CONTINUE = 0; //表示需要继续读取数据RESULT_SEEK = 1; //表示需要重新定位数据RESULT_END_OF_INPUT = C.RESULT_END_OF_INPUT; //表示已经读取结束

通过Extractor的实现类我们可以找到,当调用read方法时,都会调到trackOutput.sampleData方法。这个方法表示输出解封装后的帧。具体就是把解封装的帧存入RollingSampleBuffer中,在TrackOutput的实现类DefaultTrackOutput中的如下代码可以印证这一点:

@Override
public void sampleData(ParsableByteArray buffer, int length) {rollingBuffer.appendData(buffer, length);
}

具体的文件解封装这里不做细节分析。

4.其他

5.相关性补充

ijkplayer中Android部分:

ijkplayer是bilibili推出的同时支持ios和Android,硬解和软解的开源播放器框架。其中,在Android代码中,硬解部分应用了ExoPlayer,软解部分应用了ffmepg和sdl。

ijkplayer的demo中,调用方式是这样的:

VideoActivity -> IjkVideoView -> IMediaPlayer -> AbstractMediaPlayer AbstractMediaPlayer -> IjkExoMediaPlayer -> DemoPlayer -> ExoPlayer AbstractMediaPlayer -> IjkMediaPlayer -> ijkplayer_jni.c -> ijkplayer.c -> Ff_ffplayer.c 
原文链接:http://blog.csdn.net/henryjax/article/details/51313051

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/508579.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java8 Stream

概述 Java8 API中添加了一个新的抽象成为流Stream&#xff0c;可以以一种声明的方式处理数据。 Stream与java.io包中的InputStream和OutputStream是完全不同的概念。Java8中的Stream是对集合对象功能的增强&#xff0c;专注与对集合对象进行各种非常便利、高效的聚合操作&…

Java 注解原理

下面来看看Java中注解是如何实现的 创建注解类Inter: 创建测试类Test: 在程序第二句设置断点,可以看到: 可以看到,注解的实例是一个动态代理类的对象. 要想查看这个动态代理类,可以在代码中加 System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", …

myeclipse导入项目报错Target runtime Apache Tomcat v8.0 is not defined

今天想要查看一个现有的项目&#xff0c;本着最好使用原版本开发工具的奇怪想法&#xff0c;下载了一个myeclipse&#xff0c;在导入项目的时候报了一个错误 问题倒是挺明确的&#xff0c;就是项目需要的tomcat运行环境没有&#xff0c;有两种方法解决&#xff1a; 第一种方…

Android Studio查找功能(搜索功能)及快捷键

1、在当前窗口查找文本【CtrlF】 F3 向下查找关键字出现位置 ShiftF3 向上一个关键字出现位置 2、在当前工程内查找文本【CtrlShiftF】 先会弹出一个对话框&#xff0c;直接点击【find】&#xff0c;开始在整个工程内查找该字符串 查找结果如下&#xff1a; 3、查找类【C…

On the coexistence of transport protocols in data centers

论文信息&#xff1a;S. M. Irteza, A. Ahmed, S. Farrukh, B. N. Memon, and I. A. Qazi.On the coexistence of transport protocols in data centers. In Proceedings of IEEE ICC, 2014.数据中心传输协议的共存 摘要 云数据中心的出现直接导致了数据中心TCP&#xff08;D…

如何做科研20171206

昨日听董大一席话&#xff0c;感触颇多&#xff0c;今日在此进行记录。&#xff08;加粗字体为董大箴言&#xff09; ① 关于看论文 董大问我你最近看了什么论文&#xff0c;我说&#xff0c;论文的题目没有记下来&#xff0c;只记得主要讲了什么。我以前一直以为一篇论文的阅…

contiki cooja仿真

最近在做contiki平台上的一些cooja仿真的东西&#xff0c;发现现在网上能学到的东西实在是很有限&#xff0c;现在在这里将我最近学到的一些东西做一下总结。 一、 关于运行的一般步骤&#xff1a; https://www.zhihu.com/question/48708549/answer/139050874 知乎上这个问…

6大设计原则之单一职责原则

单一职责原则 如果有一个用户管理类,类图如下 我想,任谁也能看的出这个接口设计的有问题,用户的属性和用户的行为没有分开,应该把用户的信息抽取成一个业务对象,把用户的行为抽取成一个业务对象,按照这个思路对类图进行修正,如下图所示 其实,在实际使用中我们更倾向于使用两个…

6大设计原则之里氏替换原则

面对对象中的继承 优点如下: 代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性提高代码的重用性子类可以形如父类,但又异于父类提高代码的可扩展性,很多开源框架的扩展接口都是通过继承父类来实现的提高产品或项目的开放性 缺点如下: 继承是侵入性的.只要继承,就…

6大设计原则之接口隔离原则

接口隔离原则的定义 什么是接口. 实例接口,比如定义了一个Person类,然后 Person p new Pserson(); 产生一个实例,Person类就是 p 的接口类接口,就是Java中使用 interface 定义的接口 什么是隔离 隔离要求将接口尽量细化,同时接口中的方法尽量少. 接口隔离原则的实现 比如…

论文写作——origin画图

一 origin的安装 详见下面网址&#xff0c;内涵下载路径和破解方法。 http://www.ddooo.com/softdown/51005.htm 1. 下载origin 网址&#xff1a;https://thepcgo.com/origin-pro-8-0-free-download/ 2.下载破解相关压缩包 链接&#xff1a;https://pan.baidu.com/s/1LwA…

论文写作——texstudio+texlive

一 安装 安装见下方链接 https://jingyan.baidu.com/article/63f236287febc50208ab3deb.html 二 使用 1、选择模板 IEEE: https://ieeeauthorcenter.ieee.org/create-your-ieee-article/use-authoring-tools-and-ieee-article-templates/ieee-article-templates/ ACM:…

论文写作——如何作图(visio/ppt+Adobe Acrobat Pro)

前言 在论文中&#xff0c;基本上的图都要求是矢量图&#xff0c;就是即使放大也不会失真。 .eps是最常被要求用的&#xff0c;然而我使用了各种方法&#xff08;可以试试在线转换格式&#xff0c;很方便&#xff09;都没能成功转成打得开看得见的.eps格式&#xff0c;又懒得…

23种设计模式之单例模式

单例模式的定义 定义: 确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例. 单例模式的通用类图如下: 单例模式的优缺点 单例模式的优点: 由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁的创建、销毁时,而且创建和销毁时性能又无…

23种设计模式之工厂方法模式

工厂方法模式的定义 定义: 定义一个用于创建对象的接口,让子类决定实例化哪一个类. 工厂方法使一个类的实例化延迟到其子类 工厂方法模式的通用类图: 其中 Product 负责产品的共性,实现对事物最抽象的定义; Creator 为抽象创建类, 也就是抽象工厂, 具体如何创建产品类是由具体…

23中设计模式之抽象工厂模式

抽象工厂模式的定义 定义: 为创建一组相关或互相依赖的对象提供一个接口,而且无须制定它们的具体类 抽象工厂模式的实现 两个产品族, 其类图如下: 抽象产品类代码如下: 产品A的1级和2级类代码如下: 产品B与产品A类似 抽象工厂类 AbstractCreator 的职责是定义 每个工厂要实…

23种设计模式之模板方法模式

模板方法模式的定义 定义一个操作中的算法的框架,而将一些步骤延迟到子类中. 使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤. 通俗的讲,就是将子类相同的方法, 都放到其抽象父类中 类图如下: 其中, AbstractClass 叫抽象模板, 它的方法分为以下两类: 基…

23种设计模式之建造者模式

建造者模式的定义 建造者模式也叫生成器模式, 定义如下: 将一个复杂对象的构建与它的表示分离, 使得同样的构建过程可以创建不同的表示 类图如下: 在建造者模式中, 四个角色如下: Product 产品类: 通常是实现了模板方法模式, 也就是有模板方法和基本方法Builder 抽象建造者…

23种设计模式之原型模式

原型模式的定义 定义: 用原型实例指定创建对象的种类, 并且通过拷贝这些原型创建新的对象. 通俗的讲,就是不再使用new 来创建对象, 而改用 clone 方法来得到新的对象 原型模式的核心是一个 clone 方法, 通过该方法进行对象的拷贝, Java提供了一个Cloneable接口来标识这个对象…

课堂笔记——Data Mining(1)

一、Introduction …… 1、Major Issues in Data Mining User Interaction Presentation and visualization of data mining results : Efficiency and Scalability Diversity of data types: complex types of data; Mining dynamic, networked, and global data reposit…