Android4.2.2的Stagefright维护编解码器的数据流

这里是他们自己的源代码阅读点滴总结属性,转请注明出处,谢谢。

欢迎和大家分享。qq:1037701636 email:gzzaigcn2012@gmail.com

Android源代码版本号Version:4.2.2; 硬件平台 全志A31

 

前沿:

在前面的博文中,基本提到的是stagefright相关的控制流,详细分析了android架构中的MediaExtractor、AwesomePlayer、StagefrightPlayer、OMXCodec等的创建。底层OMXNodinstance实例的创建。

分析了OMX最底层插件库、编解码器组件的架构以及怎样创建属于我们自己的OMX Plugin。

分析源代码架构的还有一个关键是数据流的分析,从这里開始。我们将对stagefright中的编解码缓存区进行分析:

1.

回到OMXCodec的创建过程的源代码:

status_t AwesomePlayer::initVideoDecoder(uint32_t flags) {
.......mVideoSource = OMXCodec::Create(mClient.interface(), mVideoTrack->getFormat(),//提取视频流的格式, mClient:BpOMX;mVideoTrack->getFormat()false, // createEncoder,不创建编码器falsemVideoTrack,NULL, flags, USE_SURFACE_ALLOC ? mNativeWindow : NULL);//创建一个解码器mVideoSourceif (mVideoSource != NULL) {int64_t durationUs;if (mVideoTrack->getFormat()->findInt64(kKeyDuration, &durationUs)) {Mutex::Autolock autoLock(mMiscStateLock);if (mDurationUs < 0 || durationUs > mDurationUs) {mDurationUs = durationUs;}}status_t err = mVideoSource->start();//启动解码器OMXCodec。完毕解码器的init初始化操作
.............
}

在Android4.2.2下Stagefright多媒体架构中的A31的OMX插件和Codec组件 博文我们对于OMXCodec::create已经做了具体的分析。这里来关注mVideoSource->start的相关功能,即OMXCodec::start的处理:

status_t OMXCodec::start(MetaData *meta) {Mutex::Autolock autoLock(mLock);
........return init();//进行初始化操作
}

这里调用init()的过程。将会进行buffer的申请操作。为兴许的流操作打下基础:

status_t OMXCodec::init() {// mLock is held.
.........err = allocateBuffers();//缓存区的分配if (err != (status_t)OK) {return err;}if (mQuirks & kRequiresLoadedToIdleAfterAllocation) {err = mOMX->sendCommand(mNode, OMX_CommandStateSet, OMX_StateIdle);CHECK_EQ(err, (status_t)OK);setState(LOADED_TO_IDLE);}
............
}

我们来看allocateBuffers的实现

 

2.关注allocateBuffersOnPort的实现

status_t OMXCodec::allocateBuffers() {status_t err = allocateBuffersOnPort(kPortIndexInput);//输入缓存input口分配if (err != OK) {return err;}return allocateBuffersOnPort(kPortIndexOutput);//输出缓存input口分配
}

这里分别将对输入和输出口进行Buffer的申请与分配。对于解码器,须要输入口来存储待解码的数据源,须要将解码后的数据源存储到输出口,而这也符合硬件的实现逻辑。

以输入缓存区分配为例展开分析:

status_t OMXCodec::allocateBuffersOnPort(OMX_U32 portIndex) {
.......OMX_PARAM_PORTDEFINITIONTYPE def;InitOMXParams(&def);def.nPortIndex = portIndex;//输入口err = mOMX->getParameter(mNode, OMX_IndexParamPortDefinition, &def, sizeof(def));//获取输入口參数到def
..........err = mOMX->allocateBuffer(mNode, portIndex, def.nBufferSize, &buffer,&info.mData);
........info.mBuffer = buffer;//获取相应的buffer_id。有保存有底层的buffer的相关信息info.mStatus = OWNED_BY_US;info.mMem = mem;info.mMediaBuffer = NULL;...........mPortBuffers[portIndex].push(info);//把当前的buffer恢复到mPortBuffers[2]中去

上述过程主要分为:

step1:先是获取底层解码器组件的当前的參数熟悉,一般这些參数都在建立OMX_Codec时完毕的初始配置,前一博文中已经提到过。

step2:进行allocateBuffer的处理,这个函数的调用终于交给底层的OMX组件来完毕,相关的实现将集成到A31的底层OMX编解码组件的处理流中进行分析。

step3:完毕对分配好的buffer信息info。维护在mPortBuffers[0]这个port中。

上述过程完毕了输入与输出的Buffer分配。为兴许解码操作buffer打下了基础。

 

3.mediaplay启动播放器

通过start的API调用。进入MediaplayerService::Client,再依次经过stagefrightplayer,AwesomePlayer。

触发play的videoevent的发生.

void AwesomePlayer::postVideoEvent_l(int64_t delayUs) {ATRACE_CALL();if (mVideoEventPending) {return;}mVideoEventPending = true;mQueue.postEventWithDelay(mVideoEvent, delayUs < 0 ? 10000 : delayUs);
}

依据前一博文的分析可知,该事件相应的处理函数为AwesomePlayer::onVideoEvent(),该部分代码量较大。提取核心内容read的处理进行分析:

   status_t err = mVideoSource->read(&mVideoBuffer, &options);//循环读数据实际的OMX_CODEC::read,读取到mVideoBuffer

read的核心是获取能够用于render的视频数据,这表明了read函数主要完毕了从视频源读取元数据,并调用解码器完毕解码生成可送显的数据。

 

4. read函数的实现

能够想象read函数的应该是一个比較复杂的过程。我们从OMX_Codec的read函数入手来分析:

status_t OMXCodec::read(MediaBuffer **buffer, const ReadOptions *options) {status_t err = OK;*buffer = NULL;Mutex::Autolock autoLock(mLock);drainInputBuffers();//buffer,填充数据源if (mState == EXECUTING) {// Otherwise mState == RECONFIGURING and this code will trigger// after the output port is reenabled.fillOutputBuffers();}}...........
}

read的核心逻辑总结为drainInputBuffers()和fillOutputBuffers(),我们对其依次进行深入的分析

 

5. drainInputBuffers()读取待解码的视频数据源到解码器的Inport

这里贴出其较为复杂的处理过程代码。主要分为下面3个部分进行分析:

(1)

bool OMXCodec::drainInputBuffer(BufferInfo *info) {
   if (mCodecSpecificDataIndex < mCodecSpecificData.size()) {CHECK(!(mFlags & kUseSecureInputBuffers));const CodecSpecificData *specific =mCodecSpecificData[mCodecSpecificDataIndex];size_t size = specific->mSize;if (!strcasecmp(MEDIA_MIMETYPE_VIDEO_AVC, mMIME)&& !(mQuirks & kWantsNALFragments)) {static const uint8_t kNALStartCode[4] ={ 0x00, 0x00, 0x00, 0x01 };CHECK(info->mSize >= specific->mSize + 4);size += 4;memcpy(info->mData, kNALStartCode, 4);memcpy((uint8_t *)info->mData + 4,specific->mData, specific->mSize);} else {CHECK(info->mSize >= specific->mSize);memcpy(info->mData, specific->mData, specific->mSize);//copy前面的数据字段}mNoMoreOutputData = false;CODEC_LOGV("calling emptyBuffer with codec specific data");status_t err = mOMX->emptyBuffer(mNode, info->mBuffer, 0, size,OMX_BUFFERFLAG_ENDOFFRAME | OMX_BUFFERFLAG_CODECCONFIG,0);//处理bufferCHECK_EQ(err, (status_t)OK);info->mStatus = OWNED_BY_COMPONENT;++mCodecSpecificDataIndex;return true;}
...............(1)

这部分的内容主要是提取一部分解码器字段,填充到info->mData的存储空间中去。这部分主要基于视频源的格式,如mp4等在创建OXMCodec病configureCodec时就完毕了这个mCodecSpecificData字段的加入,应该些解码须要的特殊字段吧。

是否须要要看其视频源的格式。获取完这个字段信息后就是正式读取视频源的数据了。

 

(2)

  for (;;) {MediaBuffer *srcBuffer;if (mSeekTimeUs >= 0) {if (mLeftOverBuffer) {mLeftOverBuffer->release();mLeftOverBuffer = NULL;}MediaSource::ReadOptions options;options.setSeekTo(mSeekTimeUs, mSeekMode);mSeekTimeUs = -1;mSeekMode = ReadOptions::SEEK_CLOSEST_SYNC;mBufferFilled.signal();err = mSource->read(&srcBuffer, &options);//读取视频源中的真实数据这里是MPEG4Source的readif (err == OK) {int64_t targetTimeUs;if (srcBuffer->meta_data()->findInt64(kKeyTargetTime, &targetTimeUs)&& targetTimeUs >= 0) {CODEC_LOGV("targetTimeUs = %lld us", targetTimeUs);mTargetTimeUs = targetTimeUs;} else {mTargetTimeUs = -1;}}} else if (mLeftOverBuffer) {srcBuffer = mLeftOverBuffer;mLeftOverBuffer = NULL;err = OK;} else {err = mSource->read(&srcBuffer);}if (err != OK) {signalEOS = true;mFinalStatus = err;mSignalledEOS = true;mBufferFilled.signal();break;}if (mFlags & kUseSecureInputBuffers) {info = findInputBufferByDataPointer(srcBuffer->data());CHECK(info != NULL);}size_t remainingBytes = info->mSize - offset;//buffer中剩余的能够存储视频数据的空间if (srcBuffer->range_length() > remainingBytes) {//当前读取的数据已经达到解码的数据量if (offset == 0) {CODEC_LOGE("Codec's input buffers are too small to accomodate ""buffer read from source (info->mSize = %d, srcLength = %d)",info->mSize, srcBuffer->range_length());srcBuffer->release();srcBuffer = NULL;setState(ERROR);return false;}mLeftOverBuffer = srcBuffer;//把没读取的buffer记录下来break;}bool releaseBuffer = true;if (mFlags & kStoreMetaDataInVideoBuffers) {releaseBuffer = false;info->mMediaBuffer = srcBuffer;}if (mFlags & kUseSecureInputBuffers) {// Data in "info" is already provided at this time.releaseBuffer = false;CHECK(info->mMediaBuffer == NULL);info->mMediaBuffer = srcBuffer;} else {CHECK(srcBuffer->data() != NULL) ;memcpy((uint8_t *)info->mData + offset,(const uint8_t *)srcBuffer->data()+ srcBuffer->range_offset(),srcBuffer->range_length());//copy数据源数据到输入缓存,数据容量srcBuffer->range_length()}int64_t lastBufferTimeUs;CHECK(srcBuffer->meta_data()->findInt64(kKeyTime, &lastBufferTimeUs));CHECK(lastBufferTimeUs >= 0);if (mIsEncoder && mIsVideo) {mDecodingTimeList.push_back(lastBufferTimeUs);}if (offset == 0) {timestampUs = lastBufferTimeUs;}offset += srcBuffer->range_length();//添加偏移量if (!strcasecmp(MEDIA_MIMETYPE_AUDIO_VORBIS, mMIME)) {CHECK(!(mQuirks & kSupportsMultipleFramesPerInputBuffer));CHECK_GE(info->mSize, offset + sizeof(int32_t));int32_t numPageSamples;if (!srcBuffer->meta_data()->findInt32(kKeyValidSamples, &numPageSamples)) {numPageSamples = -1;}memcpy((uint8_t *)info->mData + offset,&numPageSamples,sizeof(numPageSamples));offset += sizeof(numPageSamples);}if (releaseBuffer) {srcBuffer->release();srcBuffer = NULL;}++n;if (!(mQuirks & kSupportsMultipleFramesPerInputBuffer)) {break;}int64_t coalescedDurationUs = lastBufferTimeUs - timestampUs;if (coalescedDurationUs > 250000ll) {// Don't coalesce more than 250ms worth of encoded data at once.break;}}...........

该部分是提取视频源数据的关键,主要通过 err = mSource->read(&srcBuffer, &options)来完毕,mSource是在创建编解码器传入的,实际是一个相应于视频源格式的一个解析器MediaExtractor。比方在建立MP4的解析器MPEG4Extractor,通过新建一个new MPEG4Source。故终于这里调用的是MPEG4Source的read成员函数,事实上际也维护着整个待解码的原始视频流。

我们能够看大在read函数后。会将待解码的数据流以for循环依次读入究竟层的buffer空间中。仅仅有当满足当前读取的原始数据片段比底层的input口的buffer剩余空间小srcBuffer->range_length() > remainingBytes。那就能够继续读取,否则直接break后,去进行下一步操作。

或者假设一次待解码的数据时张是大于250ms也直接跳出。

这处理体现了处理的高效性。

终于视频原始数据存储在info->mData的底层输入空间中。

 

(3)

    err = mOMX->emptyBuffer(mNode, info->mBuffer, 0, offset,flags, timestampUs);

触发底层的解码器组件进行处理。这部分留在兴许对A31的底层编解码API操作时进行分析。

6.fillOutputBuffers对输出buffer口的填充,即实现解码过程:

void OMXCodec::fillOutputBuffers() {CHECK_EQ((int)mState, (int)EXECUTING);
...........Vector<BufferInfo> *buffers = &mPortBuffers[kPortIndexOutput];输出portfor (size_t i = 0; i < buffers->size(); ++i) {BufferInfo *info = &buffers->editItemAt(i);if (info->mStatus == OWNED_BY_US) {fillOutputBuffer(&buffers->editItemAt(i));}}
}
void OMXCodec::fillOutputBuffer(BufferInfo *info) {CHECK_EQ((int)info->mStatus, (int)OWNED_BY_US);if (mNoMoreOutputData) {CODEC_LOGV("There is no more output data available, not ""calling fillOutputBuffer");return;}CODEC_LOGV("Calling fillBuffer on buffer %p", info->mBuffer);status_t err = mOMX->fillBuffer(mNode, info->mBuffer);if (err != OK) {CODEC_LOGE("fillBuffer failed w/ error 0x%08x", err);setState(ERROR);return;}info->mStatus = OWNED_BY_COMPONENT;
}

从上面的代码看来,fillOutputBuffer的实现比drainInputBuffers简单了非常多。

但同样的是。两者终于都讲控制权交给底层的解码器来完毕。

 

7.等待解码数据被fill到outbuffer中,OMXCodecObserver完毕回调处理

等待解码完毕的这部分内容在read函数中通过下面函数来实现:

    while (mState != ERROR && !mNoMoreOutputData && mFilledBuffers.empty()) {if ((err = waitForBufferFilled_l()) != OK) {//进入等待buffer被填充return err;}}

上述表明,仅仅要mFilledBuffers为空则进入等待填充pthread_cond_timedwait。而这个线程被唤醒是通过底层的组件回调来完毕的。回调函数的注冊哎底层编解码器Node完毕的。实际终于的回调是交给OMXCodecObserver来完毕的:

struct OMXCodecObserver : public BnOMXObserver {OMXCodecObserver() {}void setCodec(const sp<OMXCodec> &target) {mTarget = target;}// from IOMXObservervirtual void onMessage(const omx_message &msg) {sp<OMXCodec> codec = mTarget.promote();if (codec.get() != NULL) {Mutex::Autolock autoLock(codec->mLock);codec->on_message(msg);//OMX_Codec的on_message处理codec.clear();}}

终于能够看到是由OMX_Codec->on_message来进行消息的处理。这部分的内容主要包含EMPTY_BUFFER_DONE和FILL_BUFFER_DONE两个message处理。对FILL_BUFFER_DONE完毕后的消息回调进行分析:

void OMXCodec::on_message(const omx_message &msg) {if (mState == ERROR) {/** only drop EVENT messages, EBD and FBD are still* processed for bookkeeping purposes*/if (msg.type == omx_message::EVENT) {ALOGW("Dropping OMX EVENT message - we're in ERROR state.");return;}}switch (msg.type) {                                                                                                                                         case omx_message::FILL_BUFFER_DONE://底层回调callback告知当前                        ..............mFilledBuffers.push_back(i);//当前的输出buffer信息维护在mFilledBuffersmBufferFilled.signal();//发出信息用于渲染

能够看到这里对read线程进行了唤醒。


8.提取一个可用的解码后的数据帧

    size_t index = *mFilledBuffers.begin();mFilledBuffers.erase(mFilledBuffers.begin());BufferInfo *info = &mPortBuffers[kPortIndexOutput].editItemAt(index);//从获取解码后的视频源CHECK_EQ((int)info->mStatus, (int)OWNED_BY_US);info->mStatus = OWNED_BY_CLIENT;info->mMediaBuffer->add_ref();//if (mSkipCutBuffer != NULL) {mSkipCutBuffer->submit(info->mMediaBuffer);}*buffer = info->mMediaBuffer;

获得了线程唤醒后的buffer,从这里获取到输出port相应的Bufferinfo。作为终于的BufferInfo信息返回给read函数

9

经过5、6、7、8的处理过程。read终于返回可用于显示的mVideoBuffer,接下去就是怎样送显的过程了。

能够看到以下的代码。将会创建一个渲染器mVideoRenderer来完毕这个解码后视频源的显示:

          

    if ((mNativeWindow != NULL)             && (mVideoRendererIsPreview || mVideoRenderer == NULL)) {//首次创建渲染器         mVideoRendererIsPreview = false;

        initRenderer_l();//初始化渲染器。新建一个AwesomeLocalRenderer     }

    if (mVideoRenderer != NULL) {         mSinceLastDropped++;         mVideoRenderer->render(mVideoBuffer);//启动渲染。即显示当前buffer         if (!mVideoRenderingStarted) {             mVideoRenderingStarted = true;             notifyListener_l(MEDIA_INFO, MEDIA_INFO_RENDERING_START);         }

    }

void AwesomePlayer::initRenderer_l() {ATRACE_CALL();if (mNativeWindow == NULL) {return;}sp<MetaData> meta = mVideoSource->getFormat();int32_t format;const char *component;int32_t decodedWidth, decodedHeight;CHECK(meta->findInt32(kKeyColorFormat, &format));CHECK(meta->findCString(kKeyDecoderComponent, &component));CHECK(meta->findInt32(kKeyWidth, &decodedWidth));CHECK(meta->findInt32(kKeyHeight, &decodedHeight));int32_t rotationDegrees;if (!mVideoTrack->getFormat()->findInt32(kKeyRotation, &rotationDegrees)) {rotationDegrees = 0;}mVideoRenderer.clear();// Must ensure that mVideoRenderer's destructor is actually executed// before creating a new one.IPCThreadState::self()->flushCommands();// Even if set scaling mode fails, we will continue anywaysetVideoScalingMode_l(mVideoScalingMode);if (USE_SURFACE_ALLOC&& !strncmp(component, "OMX.", 4)&& strncmp(component, "OMX.google.", 11)&& strcmp(component, "OMX.Nvidia.mpeg2v.decode")) {//使用硬件渲染器。除去上述的解码器// Hardware decoders avoid the CPU color conversion by decoding// directly to ANativeBuffers, so we must use a renderer that// just pushes those buffers to the ANativeWindow.mVideoRenderer =new AwesomeNativeWindowRenderer(mNativeWindow, rotationDegrees);//通常是使用硬件渲染机制} else {// Other decoders are instantiated locally and as a consequence// allocate their buffers in local address space.  This renderer// then performs a color conversion and copy to get the data// into the ANativeBuffer.mVideoRenderer = new AwesomeLocalRenderer(mNativeWindow, meta);}
}

能够看到这里有2个渲染器的创建分支,OMX和OMX.google说明底层的解码器用的是软解码。那么他渲染器也使用所谓的本地渲染器实际是软渲染器。故这里我们使用的是AwesomeNativeWindowRenderer渲染器,其结构例如以下所述:

struct AwesomeNativeWindowRenderer : public AwesomeRenderer {AwesomeNativeWindowRenderer(const sp<ANativeWindow> &nativeWindow,int32_t rotationDegrees): mNativeWindow(nativeWindow) {applyRotation(rotationDegrees);}virtual void render(MediaBuffer *buffer) {ATRACE_CALL();int64_t timeUs;CHECK(buffer->meta_data()->findInt64(kKeyTime, &timeUs));native_window_set_buffers_timestamp(mNativeWindow.get(), timeUs * 1000);status_t err = mNativeWindow->queueBuffer(mNativeWindow.get(), buffer->graphicBuffer().get(), -1);//直接使用queuebuffer进行渲染显示if (err != 0) {ALOGE("queueBuffer failed with error %s (%d)", strerror(-err),-err);return;}sp<MetaData> metaData = buffer->meta_data();metaData->setInt32(kKeyRendered, 1);}

不是非常复杂,仅仅是实现了AwesomeRenderer渲染接口render。终于调用这个函数来实现对buffer的显示。这里看到非常熟悉的queueBuffer,大家能够回看我的博文Android4.2.2 SurfaceFlinger之图形渲染queueBuffer实现和VSYNC的存在感 ,这是通过应用程序的本地窗体mNativeWindow(由于播放器videoview继承了sufaceview,surfaceview类会创建一个本地的surface,其继承了本地窗体类)将当前buffer提交给SurfaceFlinger服务进行显示。具体内容不在展开。

至此我们完毕了stagefright下的编解码的数据流的相关操作,程序上复杂主要体如今emptybuffer和fillbuffer为主。

当然由于能力有限。在非常多细节上也没有进行非常具体的分析。也希望大家多交流。多学习。

 




 


 

版权声明:本文博主原创文章。博客,未经同意不得转载。

转载于:https://www.cnblogs.com/hrhguanli/p/4827324.html

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

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

相关文章

PHP的安装

PHP的环境也是诸多服务器软件的必要因素之一&#xff0c;它是一个HTML内嵌式语言&#xff0c;在服务器端执行。由于PHP的开源高效化平台&#xff0c;所以搭建一个php环境是一个运维工程师必备的能力。现在lamp也有类似lnmp.org那种一键安装包&#xff0c;地址是http://yumlamp.…

android 横向铺满,Android开发全程记录(八)——设置ImageView显示的图片铺满全屏(适应魅族等不常见屏幕比例)...

为适应不同屏幕的手机&#xff0c;ImageView显示的图片可能不铺满屏幕&#xff0c;如果定高的话&#xff0c;两边可能会出现空白。魅族手机就会有这种情况&#xff0c;在其他手机里显示正常&#xff0c;在魅族手机里显示&#xff0c;图片左右两边会出现空白&#xff0c;为解决这…

tihs 关键字

//this关键词/*调用类中的属性 调用类中的方法或构造方法 调用当前对象&#xff0c;调用自己的方法&#xff0c;可以省略。 */ //http://blog.sina.com.cn/s/blog_71f6c1980100wtj4.html//this指当前对象自己public class Google{String s"hello";public Google(Stri…

良好的编程习惯

*************************************************** 更多精彩&#xff0c;欢迎进入&#xff1a;http://shop115376623.taobao.com *************************************************** 良好的编程习惯良好的习惯对于人的成长是非常重要的&#xff0c;良好的编程习惯对于我…

ntfs for mac使用注意事项有哪些?

2019独角兽企业重金招聘Python工程师标准>>> mac的用户有很多&#xff0c;一些用户朋友会发现自己的电脑是无法读写ntfs驱动器的。而ntfs驱动器又是一种常用的驱动器。面对这种情况我们可以选择用NTFS for Mac软件来帮助我们&#xff0c;它可以读写ntfs驱动器&#…

android音乐播放器文章,Android复习09【内容提供者、音乐播放器】

目 录PersonCpPersonCp.javainsert()ContentObserver音乐播放器1、添加读写权限1.1、动态权限授予(调用封装好的方法)2、获取音乐文件(MainActivity.java)2、Music.java(实体类)申请访问SD卡的权限设置适配器下拉刷新PersonCpPersonCp.javapackage cn.wangzg.personcp;import a…

程序员的业余项目

程序员的业余项目&#xff0c;我们也叫它 side project。 前几天&#xff0c;100offer 发起了一场活动叫 <寻找实干和坚持的技术力量>&#xff0c;他们是这么说的&#xff1a; 世界在被代码改变着&#xff0c;而我们在创造着代码。 仅仅是因为好玩&#xff0c;他开发了…

C语言的数组名和对数组名取地址

*************************************************** 更多精彩&#xff0c;欢迎进入&#xff1a;http://shop115376623.taobao.com *************************************************** 相信不少的C语言初学者都知道&#xff0c;数组名相当于指针&#xff0c;指向数组的首地…

小米 android 8,小米华为们谁最良心?10大手机厂商安卓8.0升级情况盘点

3月8日&#xff0c;谷歌放出了首个安卓9.0开发者预览版的固件包&#xff0c;不出意外的话&#xff0c;它的正式版会在今年正式亮相。但对广大安卓用户来说&#xff0c;想要立刻用上最新系统并非易事。目前来说&#xff0c;安卓碎片化问题依然严重&#xff0c;我们不妨现实点&am…

窥探Swift之数组安全索引与数组切片

在Swift中的数组和字典中下标是非常常见的&#xff0c;数组可以通过索引下标进行元素的查询&#xff0c;字典可以通过键下标来获取相应的值。在使用数组时&#xff0c;一个常见的致命错误就是数组越界。如果在你的应用程序中数组越界了&#xff0c;那么对不起&#xff0c;如果由…

大小端模式的快速判断方法

*************************************************** 更多精彩&#xff0c;欢迎进入&#xff1a;http://shop115376623.taobao.com *************************************************** 大小端的问题剖析&#xff1a; 嵌 入式系统开发者应该对Little-endian和Big-endian模…

【RAC】How to Proceed from Failed 11gR2 CRS Installation

Applies to: [ID 942166.1] Oracle Server – Enterprise Edition – Version: 11.2.0.1 to 11.2.0.2 – Release: 11.2 to 11.2 Generic UNIX Generic Linux Goal This goal of this note is to provide steps to proceed from failed 11gR2 Grid Infrastructure installat…

WinForm支持拖拽效果

有一个MSDN客户提问在WinForm中如何实现拖拽效果——比如在WinForm中有一个Button&#xff0c;我要实现的效果是拖拽这个Button到目标位置后生成一个该控件的副本。 其实这个操作主要分成三步走&#xff1a; 1&#xff09;确定被拖拽的对象&#xff1a; 这里是Button&#xff0…

win7 64位出现桌面右键鼠标显示忙碌

*************************************************** 更多精彩&#xff0c;欢迎进入&#xff1a;http://shop115376623.taobao.com *************************************************** 将下面绿色内容复制到txt文本中&#xff0c;然后另存为1.bat 双击运行即可 【针对64位…

android tee,Android 9.0的新增安全特性与TEE

Android P&#xff0c;预计将于 2018 年第三季度发布最终版本。特别是Android8.0以来&#xff0c;安全性是Android版本变更的一个重要因素。从安全性增强方面来看&#xff0c;本次Android9.0版本主要有以下几个方面&#xff1a;统一的指纹身份验证对话框Android P 中&#xff0…

哪些要素会让咱们呈现抑郁症的病症

依据最新研讨标明&#xff0c;一自个的性情怎样&#xff0c;本来是天然生成的&#xff0c;后天的日子&#xff0c;仅仅对咱们的性情进行批改&#xff0c;但在咱们潜意识中&#xff0c;违反自个性情的行动&#xff0c;会让咱们感到格外累&#xff0c;所以&#xff0c;不少人即是…

如何定义一个只能在堆上(栈上)生成对象的类?

在C中&#xff0c;类的对象建立分为两种&#xff0c;一种是静态建立&#xff0c;如A a&#xff1b;另一种是动态建立&#xff0c;如A* ptrnew A&#xff1b;这两种方式是有区别的。 静态建立一个类对象&#xff0c;是由编译器为对象在栈空间中分配内存&#xff0c;是通过直接移…

canny算子的理论分析

****************************************************************************************************************************************** 红&#xff1a;数字图像处理视频教程&#xff08;两部&#xff09; {中科院版36讲视频教程 电子科大版70讲视频教程&#x…

Android为spinner设置适配器,Android Spinner与适配器模式详解及实例代码

最近做项目对Android Spinner 使用&#xff0c;这里简单写个小例子&#xff0c;来测试如何使用。Spinner是一个下拉列表&#xff0c;往安卓界面中拖拽一个Spinner控件&#xff0c;在属性中设置Android:entries“array/spinner_data”其中spinner_data为在string中设置的数组。数…

web框架-Struts开始

问题&#xff1a; 为什么有structs 作为一种框架&#xff08;frameset&#xff09;可以与传统的mvc进行比较&#xff1f; MVC是一种模式数据处理、显示和数据输入分开&#xff0c;来规范开发&#xff0c;但是却又并不规范。可以这样想&#xff1a;有三家公司&#xff0c;他们对…