原址

NuPlayer是谷歌新研发的。 
AwesomePlayer存在BUG,谷歌早已在android m 版本中弃用。

sp<MediaPlayerBase> MediaPlayerService::Client::createPlayer(player_type playerType)
{// determine if we have the right player typesp<MediaPlayerBase> p = mPlayer;if ((p != NULL) && (p->playerType() != playerType)) {ALOGV("delete player");p.clear();}if (p == NULL) {p = MediaPlayerFactory::createPlayer(playerType, this, notify, mPid);}if (p != NULL) {p->setUID(mUID);}return p;
}

NuPlayerDriver构造时,new NuPlayer

NuPlayerDriver::NuPlayerDriver(): mState(STATE_IDLE),mIsAsyncPrepare(false),mAsyncResult(UNKNOWN_ERROR),mSetSurfaceInProgress(false),mDurationUs(-1),mPositionUs(-1),mNumFramesTotal(0),mNumFramesDropped(0),mLooper(new ALooper),mPlayerFlags(0),mAtEOS(false),mStartupSeekTimeUs(-1) {mLooper->setName("NuPlayerDriver Looper");mLooper->start(false, /* runOnCallingThread */true,  /* canCallJava */PRIORITY_AUDIO);mPlayer = new NuPlayer;mLooper->registerHandler(mPlayer);mPlayer->setDriver(this);
}
struct NuPlayer : public AHandler {NuPlayer(pid_t pid);void setUID(uid_t uid);void setDriver(const wp<NuPlayerDriver> &driver);void setDataSourceAsync(const sp<IStreamSource> &source);void setDataSourceAsync(const sp<IMediaHTTPService> &httpService,const char *url,const KeyedVector<String8, String8> *headers);void setDataSourceAsync(int fd, int64_t offset, int64_t length);void setDataSourceAsync(const sp<DataSource> &source);void prepareAsync();void setVideoSurfaceTextureAsync(const sp<IGraphicBufferProducer> &bufferProducer);void setAudioSink(const sp<MediaPlayerBase::AudioSink> &sink);status_t setPlaybackSettings(const AudioPlaybackRate &rate);status_t getPlaybackSettings(AudioPlaybackRate *rate /* nonnull */);status_t setSyncSettings(const AVSyncSettings &sync, float videoFpsHint);status_t getSyncSettings(AVSyncSettings *sync /* nonnull */, float *videoFps /* nonnull */);void start();void pause();// Will notify the driver through "notifyResetComplete" once finished.void resetAsync();// Will notify the driver through "notifySeekComplete" once finished// and needNotify is true.void seekToAsync(int64_t seekTimeUs, bool needNotify = false);status_t setVideoScalingMode(int32_t mode);status_t getTrackInfo(Parcel* reply) const;status_t getSelectedTrack(int32_t type, Parcel* reply) const;status_t selectTrack(size_t trackIndex, bool select, int64_t timeUs);status_t getCurrentPosition(int64_t *mediaUs);void getStats(Vector<sp<AMessage> > *mTrackStats);sp<MetaData> getFileMeta();float getFrameRate();

当调用setDataSource时,

void NuPlayer::setDataSourceAsync(const sp<IStreamSource> &source) {sp<AMessage> msg = new AMessage(kWhatSetDataSource, this);sp<AMessage> notify = new AMessage(kWhatSourceNotify, this);msg->setObject("source", new StreamingSource(notify, source));msg->post();
}

当发送Message时

void NuPlayer::onMessageReceived(const sp<AMessage> &msg) {switch (msg->what()) {case kWhatSetDataSource:{ALOGV("kWhatSetDataSource");CHECK(mSource == NULL);status_t err = OK;sp<RefBase> obj;CHECK(msg->findObject("source", &obj));if (obj != NULL) {mSource = static_cast<Source *>(obj.get());} else {err = UNKNOWN_ERROR;}CHECK(mDriver != NULL);sp<NuPlayerDriver> driver = mDriver.promote();if (driver != NULL) {driver->notifySetDataSourceCompleted(err);}break;}
}

当setDataSource好了后,上层发送start开始播放流程以后,开始创建解码器

void NuPlayer::start() {(new AMessage(kWhatStart, this))->post();
}
void NuPlayer::onMessageReceived(const sp<AMessage> &msg) {switch (msg->what()) {case kWhatStart:{ALOGV("kWhatStart");if (mStarted) {// do not resume yet if the source is still bufferingif (!mPausedForBuffering) {onResume();}} else {onStart();}mPausedByClient = false;break;}}
}

接下来

void NuPlayer::onStart(int64_t startPositionUs) {if (!mSourceStarted) {mSourceStarted = true;mSource->start();}if (startPositionUs > 0) {performSeek(startPositionUs);if (mSource->getFormat(false /* audio */) == NULL) {return;}}mOffloadAudio = false;mAudioEOS = false;mVideoEOS = false;mStarted = true;uint32_t flags = 0;if (mSource->isRealTime()) {flags |= Renderer::FLAG_REAL_TIME;}sp<MetaData> audioMeta = mSource->getFormatMeta(true /* audio */);audio_stream_type_t streamType = AUDIO_STREAM_MUSIC;if (mAudioSink != NULL) {streamType = mAudioSink->getAudioStreamType();}sp<AMessage> videoFormat = mSource->getFormat(false /* audio */);mOffloadAudio =canOffloadStream(audioMeta, (videoFormat != NULL), mSource->isStreaming(), streamType);if (mOffloadAudio) {flags |= Renderer::FLAG_OFFLOAD_AUDIO;}sp<AMessage> notify = new AMessage(kWhatRendererNotify, this);++mRendererGeneration;notify->setInt32("generation", mRendererGeneration);mRenderer = new Renderer(mAudioSink, notify, flags);mRendererLooper = new ALooper;mRendererLooper->setName("NuPlayerRenderer");mRendererLooper->start(false, false, ANDROID_PRIORITY_AUDIO);mRendererLooper->registerHandler(mRenderer);status_t err = mRenderer->setPlaybackSettings(mPlaybackSettings);if (err != OK) {mSource->stop();mSourceStarted = false;notifyListener(MEDIA_ERROR, MEDIA_ERROR_UNKNOWN, err);return;}float rate = getFrameRate();if (rate > 0) {mRenderer->setVideoFrameRate(rate);}if (mVideoDecoder != NULL) {mVideoDecoder->setRenderer(mRenderer);}if (mAudioDecoder != NULL) {mAudioDecoder->setRenderer(mRenderer);}postScanSources();
}

在onStart函数最后,有一个postScanSources();实现如下:

void NuPlayer::postScanSources() {if (mScanSourcesPending) {return;}sp<AMessage> msg = new AMessage(kWhatScanSources, this);msg->setInt32("generation", mScanSourcesGeneration);msg->post();mScanSourcesPending = true;
}

会发一个Message,key是kWhatScanSources:

void NuPlayer::onMessageReceived(const sp<AMessage> &msg) {switch (msg->what()) {case kWhatScanSources:{int32_t generation;CHECK(msg->findInt32("generation", &generation));if (generation != mScanSourcesGeneration) {// Drop obsolete msg.break;}mScanSourcesPending = false;ALOGV("scanning sources haveAudio=%d, haveVideo=%d",mAudioDecoder != NULL, mVideoDecoder != NULL);bool mHadAnySourcesBefore =(mAudioDecoder != NULL) || (mVideoDecoder != NULL);// initialize video before audio because successful initialization of// video may change deep buffer mode of audio.if (mSurface != NULL) {instantiateDecoder(false, &mVideoDecoder);}// Don't try to re-open audio sink if there's an existing decoder.if (mAudioSink != NULL && mAudioDecoder == NULL) {instantiateDecoder(true, &mAudioDecoder);}if (!mHadAnySourcesBefore&& (mAudioDecoder != NULL || mVideoDecoder != NULL)) {// This is the first time we've found anything playable.if (mSourceFlags & Source::FLAG_DYNAMIC_DURATION) {schedulePollDuration();}}status_t err;if ((err = mSource->feedMoreTSData()) != OK) {if (mAudioDecoder == NULL && mVideoDecoder == NULL) {// We're not currently decoding anything (no audio or// video tracks found) and we just ran out of input data.if (err == ERROR_END_OF_STREAM) {notifyListener(MEDIA_PLAYBACK_COMPLETE, 0, 0);} else {notifyListener(MEDIA_ERROR, MEDIA_ERROR_UNKNOWN, err);}}break;}if ((mAudioDecoder == NULL && mAudioSink != NULL)|| (mVideoDecoder == NULL && mSurface != NULL)) {msg->post(100000ll);mScanSourcesPending = true;}break;}}
}

先初始化视频解码器,再初始化音频解码器。最后都会进入到instantiateDecoder,

status_t NuPlayer::instantiateDecoder(bool audio, sp<DecoderBase> *decoder) {// The audio decoder could be cleared by tear down. If still in shut down// process, no need to create a new audio decoder.if (*decoder != NULL || (audio && mFlushingAudio == SHUT_DOWN)) {return OK;}sp<AMessage> format = mSource->getFormat(audio);if (format == NULL) {return -EWOULDBLOCK;}format->setInt32("priority", 0 /* realtime */);if (!audio) {AString mime;CHECK(format->findString("mime", &mime));sp<AMessage> ccNotify = new AMessage(kWhatClosedCaptionNotify, this);if (mCCDecoder == NULL) {mCCDecoder = new CCDecoder(ccNotify);}if (mSourceFlags & Source::FLAG_SECURE) {format->setInt32("secure", true);}if (mSourceFlags & Source::FLAG_PROTECTED) {format->setInt32("protected", true);}float rate = getFrameRate();if (rate > 0) {format->setFloat("operating-rate", rate * mPlaybackSettings.mSpeed);}}if (audio) {sp<AMessage> notify = new AMessage(kWhatAudioNotify, this);++mAudioDecoderGeneration;notify->setInt32("generation", mAudioDecoderGeneration);determineAudioModeChange();if (mOffloadAudio) {const bool hasVideo = (mSource->getFormat(false /*audio */) != NULL);format->setInt32("has-video", hasVideo);*decoder = new DecoderPassThrough(notify, mSource, mRenderer);} else {*decoder = new Decoder(notify, mSource, mPID, mRenderer);}} else {sp<AMessage> notify = new AMessage(kWhatVideoNotify, this);++mVideoDecoderGeneration;notify->setInt32("generation", mVideoDecoderGeneration);*decoder = new Decoder(notify, mSource, mPID, mRenderer, mSurface, mCCDecoder);// enable FRC if high-quality AV sync is requested, even if not// directly queuing to display, as this will even improve textureview// playback.{char value[PROPERTY_VALUE_MAX];if (property_get("persist.sys.media.avsync", value, NULL) &&(!strcmp("1", value) || !strcasecmp("true", value))) {format->setInt32("auto-frc", 1);}}}(*decoder)->init();(*decoder)->configure(format);// allocate buffers to decrypt widevine source buffersif (!audio && (mSourceFlags & Source::FLAG_SECURE)) {Vector<sp<ABuffer> > inputBufs;CHECK_EQ((*decoder)->getInputBuffers(&inputBufs), (status_t)OK);Vector<MediaBuffer *> mediaBufs;for (size_t i = 0; i < inputBufs.size(); i++) {const sp<ABuffer> &buffer = inputBufs[i];MediaBuffer *mbuf = new MediaBuffer(buffer->data(), buffer->size());mediaBufs.push(mbuf);}status_t err = mSource->setBuffers(audio, mediaBufs);if (err != OK) {for (size_t i = 0; i < mediaBufs.size(); ++i) {mediaBufs[i]->release();}mediaBufs.clear();ALOGE("Secure source didn't support secure mediaBufs.");return err;}}return OK;
}

开始*decoder = new Decoder(notify, mSource, mPID, mRenderer, mSurface, mCCDecoder); 然后(*decoder)->configure(format);这个Decoder结构体是定义在NuPlayerDecoder.h,看下对应configure实现。 
在NuPlayerDecoder.cpp

void NuPlayer::Decoder::onConfigure(const sp<AMessage> &format) {CHECK(mCodec == NULL);mFormatChangePending = false;mTimeChangePending = false;++mBufferGeneration;AString mime;CHECK(format->findString("mime", &mime));mIsAudio = !strncasecmp("audio/", mime.c_str(), 6);mIsVideoAVC = !strcasecmp(MEDIA_MIMETYPE_VIDEO_AVC, mime.c_str());mComponentName = mime;mComponentName.append(" decoder");ALOGV("[%s] onConfigure (surface=%p)", mComponentName.c_str(), mSurface.get());mCodec = MediaCodec::CreateByType(mCodecLooper, mime.c_str(), false /* encoder */, NULL /* err */, mPid);int32_t secure = 0;if (format->findInt32("secure", &secure) && secure != 0) {if (mCodec != NULL) {mCodec->getName(&mComponentName);mComponentName.append(".secure");mCodec->release();ALOGI("[%s] creating", mComponentName.c_str());mCodec = MediaCodec::CreateByComponentName(mCodecLooper, mComponentName.c_str(), NULL /* err */, mPid);}}if (mCodec == NULL) {ALOGE("Failed to create %s%s decoder",(secure ? "secure " : ""), mime.c_str());handleError(UNKNOWN_ERROR);return;}mIsSecure = secure;mCodec->getName(&mComponentName);status_t err;if (mSurface != NULL) {// disconnect from surface as MediaCodec will reconnecterr = native_window_api_disconnect(mSurface.get(), NATIVE_WINDOW_API_MEDIA);// We treat this as a warning, as this is a preparatory step.// Codec will try to connect to the surface, which is where// any error signaling will occur.ALOGW_IF(err != OK, "failed to disconnect from surface: %d", err);}err = mCodec->configure(format, mSurface, NULL /* crypto */, 0 /* flags */);if (err != OK) {ALOGE("Failed to configure %s decoder (err=%d)", mComponentName.c_str(), err);mCodec->release();mCodec.clear();handleError(err);return;}rememberCodecSpecificData(format);// the following should work in configured stateCHECK_EQ((status_t)OK, mCodec->getOutputFormat(&mOutputFormat));CHECK_EQ((status_t)OK, mCodec->getInputFormat(&mInputFormat));mStats->setString("mime", mime.c_str());mStats->setString("component-name", mComponentName.c_str());if (!mIsAudio) {int32_t width, height;if (mOutputFormat->findInt32("width", &width)&& mOutputFormat->findInt32("height", &height)) {mStats->setInt32("width", width);mStats->setInt32("height", height);}}sp<AMessage> reply = new AMessage(kWhatCodecNotify, this);mCodec->setCallback(reply);err = mCodec->start();if (err != OK) {ALOGE("Failed to start %s decoder (err=%d)", mComponentName.c_str(), err);mCodec->release();mCodec.clear();handleError(err);return;}releaseAndResetMediaBuffers();mPaused = false;mResumePending = false;
}
  • 其中通过CreateByType创建MediaCodec,位于MediaCodec.cpp
// static
sp<MediaCodec> MediaCodec::CreateByType(const sp<ALooper> &looper, const char *mime, bool encoder, status_t *err, pid_t pid) {sp<MediaCodec> codec = new MediaCodec(looper, pid);const status_t ret = codec->init(mime, true /* nameIsType */, encoder);if (err != NULL) {*err = ret;}return ret == OK ? codec : NULL; // NULL deallocates codec.
}// static
sp<MediaCodec> MediaCodec::CreateByComponentName(const sp<ALooper> &looper, const char *name, status_t *err, pid_t pid) {sp<MediaCodec> codec = new MediaCodec(looper, pid);const status_t ret = codec->init(name, false /* nameIsType */, false /* encoder */);if (err != NULL) {*err = ret;}return ret == OK ? codec : NULL; // NULL deallocates codec.
}

调用init函数,位于MediaCodec.cpp

status_t MediaCodec::init(const AString &name, bool nameIsType, bool encoder) {mResourceManagerService->init();// save init parameters for resetmInitName = name;mInitNameIsType = nameIsType;mInitIsEncoder = encoder;// Current video decoders do not return from OMX_FillThisBuffer// quickly, violating the OpenMAX specs, until that is remedied// we need to invest in an extra looper to free the main event// queue.if (nameIsType || !strncasecmp(name.c_str(), "omx.", 4)) {mCodec = new ACodec;} else if (!nameIsType&& !strncasecmp(name.c_str(), "android.filter.", 15)) {mCodec = new MediaFilter;} else {return NAME_NOT_FOUND;}...mLooper->registerHandler(this);mCodec->setNotificationMessage(new AMessage(kWhatCodecNotify, this));sp<AMessage> msg = new AMessage(kWhatInit, this);

MediaCodec.cpp中onMessageReceived函数

void MediaCodec::onMessageReceived(const sp<AMessage> &msg) {switch (msg->what()) {case kWhatInit:{sp<AReplyToken> replyID;CHECK(msg->senderAwaitsResponse(&replyID));if (mState != UNINITIALIZED) {PostReplyWithError(replyID, INVALID_OPERATION);break;}mReplyID = replyID;setState(INITIALIZING);AString name;CHECK(msg->findString("name", &name));int32_t nameIsType;int32_t encoder = false;CHECK(msg->findInt32("nameIsType", &nameIsType));if (nameIsType) {CHECK(msg->findInt32("encoder", &encoder));}sp<AMessage> format = new AMessage;if (nameIsType) {format->setString("mime", name.c_str());format->setInt32("encoder", encoder);} else {format->setString("componentName", name.c_str());}mCodec->initiateAllocateComponent(format);break;}}
}

最后来到ACodec中,initiateAllocateComponent

void ACodec::initiateAllocateComponent(const sp<AMessage> &msg) {msg->setWhat(kWhatAllocateComponent);msg->setTarget(this);msg->post();
}

消息接收

bool ACodec::UninitializedState::onMessageReceived(const sp<AMessage> &msg) {bool handled = false;switch (msg->what()) {case ACodec::kWhatSetup:{onSetup(msg);handled = true;break;}case ACodec::kWhatAllocateComponent:{onAllocateComponent(msg);handled = true;break;}}
}

初始化组件

bool ACodec::UninitializedState::onAllocateComponent(const sp<AMessage> &msg) {ALOGV("onAllocateComponent");CHECK(mCodec->mNode == 0);OMXClient client;if (client.connect() != OK) {mCodec->signalError(OMX_ErrorUndefined, NO_INIT);return false;}sp<IOMX> omx = client.interface();sp<AMessage> notify = new AMessage(kWhatOMXDied, mCodec);mDeathNotifier = new DeathNotifier(notify);if (IInterface::asBinder(omx)->linkToDeath(mDeathNotifier) != OK) {// This was a local binder, if it dies so do we, we won't care// about any notifications in the afterlife.mDeathNotifier.clear();}Vector<OMXCodec::CodecNameAndQuirks> matchingCodecs;AString mime;AString componentName;uint32_t quirks = 0;int32_t encoder = false;if (msg->findString("componentName", &componentName)) {ssize_t index = matchingCodecs.add();OMXCodec::CodecNameAndQuirks *entry = &matchingCodecs.editItemAt(index);entry->mName = String8(componentName.c_str());if (!OMXCodec::findCodecQuirks(componentName.c_str(), &entry->mQuirks)) {entry->mQuirks = 0;}} else {CHECK(msg->findString("mime", &mime));if (!msg->findInt32("encoder", &encoder)) {encoder = false;}OMXCodec::findMatchingCodecs(mime.c_str(),encoder, // createEncoderNULL,  // matchComponentName0,     // flags&matchingCodecs);}sp<CodecObserver> observer = new CodecObserver;IOMX::node_id node = 0;status_t err = NAME_NOT_FOUND;for (size_t matchIndex = 0; matchIndex < matchingCodecs.size();++matchIndex) {componentName = matchingCodecs.itemAt(matchIndex).mName.string();quirks = matchingCodecs.itemAt(matchIndex).mQuirks;pid_t tid = gettid();int prevPriority = androidGetThreadPriority(tid);androidSetThreadPriority(tid, ANDROID_PRIORITY_FOREGROUND);err = omx->allocateNode(componentName.c_str(), observer, &node);androidSetThreadPriority(tid, prevPriority);if (err == OK) {break;} else {ALOGW("Allocating component '%s' failed, try next one.", componentName.c_str());}node = 0;}if (node == 0) {if (!mime.empty()) {ALOGE("Unable to instantiate a %scoder for type '%s' with err %#x.",encoder ? "en" : "de", mime.c_str(), err);} else {ALOGE("Unable to instantiate codec '%s' with err %#x.", componentName.c_str(), err);}mCodec->signalError((OMX_ERRORTYPE)err, makeNoSideEffectStatus(err));return false;}notify = new AMessage(kWhatOMXMessageList, mCodec);observer->setNotificationMessage(notify);mCodec->mComponentName = componentName;mCodec->mRenderTracker.setComponentName(componentName);mCodec->mFlags = 0;if (componentName.endsWith(".secure")) {mCodec->mFlags |= kFlagIsSecure;mCodec->mFlags |= kFlagIsGrallocUsageProtected;mCodec->mFlags |= kFlagPushBlankBuffersToNativeWindowOnShutdown;}mCodec->mQuirks = quirks;mCodec->mOMX = omx;mCodec->mNode = node;{sp<AMessage> notify = mCodec->mNotify->dup();notify->setInt32("what", CodecBase::kWhatComponentAllocated);notify->setString("componentName", mCodec->mComponentName.c_str());notify->post();}mCodec->changeState(mCodec->mLoadedState);return true;
}

Android Multimedia框架总结(二十八)NuPlayer到OMX过程相关推荐

  1. (转载)Android项目实战(二十八):使用Zxing实现二维码及优化实例

    Android项目实战(二十八):使用Zxing实现二维码及优化实例 作者:听着music睡 字体:[增加 减小] 类型:转载 时间:2016-11-21 我要评论 这篇文章主要介绍了Android项 ...

  2. Android开发系列(二十八):使用SubMenu创建选项菜单

    大部分手机上边都会有一个"MENU"键,在一个应用安装到手机上之后,能够通过"MENU"显示该应用关联的菜单. 可是,从Android 3.0開始,Androi ...

  3. Android开发笔记(二十八)利用Application实现内存读写

    全局变量 C/C++有所谓的全局变量,因为全局变量保存在内存中,所以操作全局变量就是操作内存,其速度远比操作数据库或者操作文件快得多,而且工程里的任何代码都可以引用全局变量,因此很多时候全局变量是共享 ...

  4. Android学习记录(二十八)--Android apache httpclients的使用。

    1.历史原因: Android当前不在支持APACHE的一套内容,开始推自己的网络请求库,基本等同于okhttp.但是非常令人失望的是,这个库目前看支持是不全的,对于网络鉴权,只支持BASE的网络鉴权 ...

  5. Android 天气APP(二十八)地图搜索定位

    还是比较简单的,然后进入到MapWeatherActivity ImageView ivSearch;//搜索图标 @BindView(R.id.ed_search) EditText edSearc ...

  6. Android开发-自定义View-AndroidStudio(二十八)缩放的View

    转载请注明出处: http://blog.csdn.net/iwanghang/ 觉得博文有用,请点赞,请评论,请关注,谢谢!~ 继续上一篇博文,移动的View,我们来一下自定义View如何缩放: 老 ...

  7. Android多媒体框架(二)Codec初始化及Omx组件创建

    Codec创建流程 Android提供给应用编解码的接口为MediaCodec.我们这里从NuPlayerDecoder开始分析,一是为了衔接之前将的MediaPlayer-NuPlayer流程,二是 ...

  8. Android 天气APP(二十九)壁纸设置、图片查看、图片保存

    上一篇:Android 天气APP(二十八)地图搜索定位 效果图 开发流程 一.前情提要 二.正式开发 1. 列表数据填充 2. 浮动按钮的交互 3. 其他优化 4. 运行效果图 三.文末 一.前情提 ...

  9. 关闭数字健康 android 魅族,数字体验 篇二十八:精雕细刻,只为给魅友更好的选择,魅族16s Pro体验分享...

    数字体验 篇二十八:精雕细刻,只为给魅友更好的选择,魅族16s Pro体验分享 2019-09-06 17:31:22 14点赞 10收藏 15评论 当我还一直在称赞魅族16s所拥有的舒适手感表现时, ...

  10. RecyclerView完全解析,让你从此爱上它(二十八)

    RecyclerView完全解析,让你从此爱上它(二十八) 2015-11-20      0 个评论   来源: 专注移动开发,项目管理.jiangqqlmj   收藏   我要投稿 (一).前言: ...

最新文章

  1. oracle将213变成123,oracle 转换函数
  2. python软件加密、固定机器使用_如何用Python进行最常见的加密操作?(附最新400集Python教程)...
  3. ios程序内发送邮件的代码
  4. Linux下rgmii接口与fpga相连,FPGA控制RGMII接口PHY芯片88E1512网络通信
  5. C++ primer 11章关联容器
  6. GY编辑平台产品总结
  7. Kotlin协程简介(一) Hello,coroutines!
  8. mysql 高级映射_MyBatis是一个支持普通SQL查询,存储过程和高级映射的优秀持久层框架...
  9. flask + websocket
  10. 印地语freeCodeCamp YouTube频道+不和谐聊天现已上线
  11. 北斗卫星导航系统基础篇之(二)
  12. Win7专业版密码忘了使用U深度启动U盘清除登录密码
  13. python qqbot实现qq聊天机器人_Python QQBot库的QQ聊天机器人
  14. 胧月初音未来计算机,胧月(流星P所作歌曲《胧月》)_百度百科
  15. “不仅仅是土豆”精神
  16. ctfshow 做题 MISC入门 模块 21~30
  17. eclipse安装tomcat时只有locahost,不显示server name
  18. Mock.js + RAP 使用介绍
  19. scanner练习:BMI计算
  20. 反素数java_【Java自学】 反素数

热门文章

  1. python 文本处理操作
  2. os10.10上versions崩溃的问题解决
  3. 产品需求文档写作方法(一)写前准备+梳理需求
  4. 善于从错误中总结,而且还要持之以恒地达到目标
  5. Heaps 高性能游戏引擎
  6. OpenEphyra学习笔记1
  7. Warning:Null pointer access: The variable addStrings can only be null at this location
  8. linux怎么加块硬盘,如何给linux添加一块硬盘
  9. UcOS-II 和linux比较
  10. pandas的两种数据类型:Series和DataFrame