Android 13 - Media框架(22)- ACodec(四)

前面两节我们了解了 ACodec 的创建及配置流程,配置完成后 ACodec 进入了 LoadedState,这一节开始将会了解 ACodec 的启动过程。

调用 ACodec::initiateStart 方法发出的 kWhatStart 消息将有 LoadedState 状态来处理,这个方法会向 OMX 组件发送命令OMX_CommandStateSet ,将组件的状态设定为 OMX_StateIdle,之后将 ACodec 的状态切换到中间等待状态 LoadedToIdleState

void ACodec::LoadedState::onStart() {ALOGV("onStart");status_t err = mCodec->mOMXNode->sendCommand(OMX_CommandStateSet, OMX_StateIdle);if (err != OK) {mCodec->signalError(OMX_ErrorUndefined, makeNoSideEffectStatus(err));} else {mCodec->changeState(mCodec->mLoadedToIdleState);}
}

切换状态时调用 LoadedToIdleState 的 stateEntered 方法,来为 OMX 组件分配 buffer,这是很关键的一步。

void ACodec::LoadedToIdleState::stateEntered() {ALOGV("[%s] Now Loaded->Idle", mCodec->mComponentName.c_str());status_t err;if ((err = allocateBuffers()) != OK) {ALOGE("Failed to allocate buffers after transitioning to IDLE state ""(error 0x%08x)",err);mCodec->signalError(OMX_ErrorUndefined, makeNoSideEffectStatus(err));mCodec->mOMXNode->sendCommand(OMX_CommandStateSet, OMX_StateLoaded);if (mCodec->allYourBuffersAreBelongToUs(kPortIndexInput)) {mCodec->freeBuffersOnPort(kPortIndexInput);}if (mCodec->allYourBuffersAreBelongToUs(kPortIndexOutput)) {mCodec->freeBuffersOnPort(kPortIndexOutput);}mCodec->changeState(mCodec->mLoadedState);}
}

stateEntered 中主要调用了 allocateBuffers 方法,如果返回结果有问题,那么会将状态回滚到 LoadedState。

status_t ACodec::LoadedToIdleState::allocateBuffers() {status_t err = mCodec->allocateBuffersOnPort(kPortIndexInput);if (err != OK) {return err;}err = mCodec->allocateBuffersOnPort(kPortIndexOutput);if (err != OK) {return err;}mCodec->mCallback->onStartCompleted();return OK;
}

allocateBuffers 中将 buffer 分配完成后,就会调用 callback 通知 MediaCodec 完成阻塞调用了。我们上面说到将 OMX 组件状态设置为 OMX_StateIdle,这个状态下,OMX 组件处理这个消息时应该是处于一个阻塞的状态,阻塞是在等待上层 buffer 分配完成,一旦完成后就会向 ACodec 发送一条消息,表示事件处理完成了(buffer准备完成),这时候 ACodec 将会再向 OMX 组件发送状态设置命令,将组件状态设置为 OMX_StateExecuting,组件就正式开始工作了。

bool ACodec::LoadedToIdleState::onOMXEvent(OMX_EVENTTYPE event, OMX_U32 data1, OMX_U32 data2) {switch (event) {case OMX_EventCmdComplete:{status_t err = OK;if (data1 != (OMX_U32)OMX_CommandStateSet|| data2 != (OMX_U32)OMX_StateIdle) {ALOGE("Unexpected command completion in LoadedToIdleState: %s(%u) %s(%u)",asString((OMX_COMMANDTYPE)data1), data1,asString((OMX_STATETYPE)data2), data2);err = FAILED_TRANSACTION;}if (err == OK) {err = mCodec->mOMXNode->sendCommand(OMX_CommandStateSet, OMX_StateExecuting);}if (err != OK) {mCodec->signalError(OMX_ErrorUndefined, makeNoSideEffectStatus(err));} else {mCodec->changeState(mCodec->mIdleToExecutingState);}return true;}default:return BaseState::onOMXEvent(event, data1, data2);}
}

上面这段主要是要理解,OMX组件在处理 OMX_StateIdle 这条命令时会处在一个阻塞的状态

接下来我们就要一起看 buffer 是如何分配的,如果已经了解我们上一节看的 Port Mode,那么这部分还是很简单的。

代码比较长,我们把代码分成两部分来看:

  1. 有native window的情况下分配output buffer;

  2. 对 input buffer ,以及没有 native window 时的 output buffer 进行分配;

首先我们来看第一部分:

    // 1、在有native window的情况下分配output bufferif (mNativeWindow != NULL && portIndex == kPortIndexOutput) {if (storingMetadataInDecodedBuffers()) {err = allocateOutputMetadataBuffers();} else {err = allocateOutputBuffersFromNativeWindow();}}

我们在上一篇中了解到有native window 时,output mode会有两种情况,一种是 tunnel mode;另一种是 kPortModeDynamicANWBuffer,也就是所谓的 MetaData mode,这里我们先看这种模式。

1、allocateOutputMetadataBuffers

status_t ACodec::allocateOutputMetadataBuffers() {CHECK(storingMetadataInDecodedBuffers());// 1、调用方法获取 native window 可用的 output buffer 数量以及大小OMX_U32 bufferCount, bufferSize, minUndequeuedBuffers;status_t err = configureOutputBuffersFromNativeWindow(&bufferCount, &bufferSize, &minUndequeuedBuffers,mFlags & kFlagPreregisterMetadataBuffers /* preregister */);if (err != OK)return err;mNumUndequeuedBuffers = minUndequeuedBuffers;ALOGV("[%s] Allocating %u meta buffers on output port",mComponentName.c_str(), bufferCount);// 2、创建 对应数量的 BufferInfofor (OMX_U32 i = 0; i < bufferCount; i++) {BufferInfo info;info.mStatus = BufferInfo::OWNED_BY_NATIVE_WINDOW;info.mFenceFd = -1;info.mRenderInfo = NULL;info.mGraphicBuffer = NULL;info.mNewGraphicBuffer = false;info.mDequeuedAt = mDequeueCounter;// 3、创建一个 MediaCodecBufferinfo.mData = new MediaCodecBuffer(mOutputFormat, new ABuffer(bufferSize));// Initialize fence fd to -1 to avoid warning in freeBuffer().((VideoNativeMetadata *)info.mData->base())->nFenceFd = -1;info.mCodecData = info.mData;// 4、调用 useBuffer 让 OMX 组件使用一块空的 buffer,并且回传 IDerr = mOMXNode->useBuffer(kPortIndexOutput, OMXBuffer::sPreset, &info.mBufferID);mBuffers[kPortIndexOutput].push(info);ALOGV("[%s] allocated meta buffer with ID %u",mComponentName.c_str(), info.mBufferID);}// 5、计算需要提交的 output buffer 数量mMetadataBuffersToSubmit = bufferCount - minUndequeuedBuffers;return err;
}

这个方法大致做了以下5个事情:

  1. 获取 native window 可用的 output buffer 数量以及大小;
  2. 创建对应数量的 BufferInfo;
  3. 为 BufferInfo 中的 mData 字段分配空间;
  4. 调用 useBuffer 让 OMX 组件使用一块空的 buffer,并且回传 ID 并与当前的 BufferInfo 相绑定;
  5. 计算需要提交的 output buffer 数量;

1.1、configureOutputBuffersFromNativeWindow

status_t ACodec::configureOutputBuffersFromNativeWindow(OMX_U32 *bufferCount, OMX_U32 *bufferSize,OMX_U32 *minUndequeuedBuffers, bool preregister) {OMX_PARAM_PORTDEFINITIONTYPE def;InitOMXParams(&def);def.nPortIndex = kPortIndexOutput;// 获取 OMX 组件定义的 output port 的定义,定义中会有 output buffer 的数量status_t err = mOMXNode->getParameter(OMX_IndexParamPortDefinition, &def, sizeof(def));if (err == OK) {err = setupNativeWindowSizeFormatAndUsage(mNativeWindow.get(), &mNativeWindowUsageBits,preregister && !mTunneled /* reconnect */);}if (err != OK) {mNativeWindowUsageBits = 0;return err;}// 设置从 nativewindow 中获取buffer这个动作为阻塞的static_cast<Surface *>(mNativeWindow.get())->setDequeueTimeout(-1);// Exits here for tunneled video playback codecs -- i.e. skips native window// buffer allocation step as this is managed by the tunneled OMX omponent// itself and explicitly sets def.nBufferCountActual to 0.// 如果是 tunnel mode,那么端口的 buffer 数量为0,不需要从上层获取 output bufferif (mTunneled) {ALOGV("Tunneled Playback: skipping native window buffer allocation.");def.nBufferCountActual = 0;err = mOMXNode->setParameter(OMX_IndexParamPortDefinition, &def, sizeof(def));*minUndequeuedBuffers = 0;*bufferCount = 0;*bufferSize = 0;return err;}// 从 native window 获取最小的还未出队列的 buffer 数量*minUndequeuedBuffers = 0;err = mNativeWindow->query(mNativeWindow.get(), NATIVE_WINDOW_MIN_UNDEQUEUED_BUFFERS,(int *)minUndequeuedBuffers);if (err != 0) {ALOGE("NATIVE_WINDOW_MIN_UNDEQUEUED_BUFFERS query failed: %s (%d)",strerror(-err), -err);return err;}// 重新计算OMX 组件上 output buffer 的数量// FIXME: assume that surface is controlled by app (native window// returns the number for the case when surface is not controlled by app)// FIXME2: This means that minUndeqeueudBufs can be 1 larger than reported// For now, try to allocate 1 more buffer, but don't fail if unsuccessful// Use conservative allocation while also trying to reduce starvation//// 1. allocate at least nBufferCountMin + minUndequeuedBuffers - that is the//    minimum needed for the consumer to be able to work// 2. try to allocate two (2) additional buffers to reduce starvation from//    the consumer//    plus an extra buffer to account for incorrect minUndequeuedBufsfor (OMX_U32 extraBuffers = 2 + 1; /* condition inside loop */; extraBuffers--) {// 尝试将output buffer 的数量设置为端口所需的最小数量 +  nativewindow最小未出队列的buffer数量 + 3OMX_U32 newBufferCount =def.nBufferCountMin + *minUndequeuedBuffers + extraBuffers;def.nBufferCountActual = newBufferCount;err = mOMXNode->setParameter(OMX_IndexParamPortDefinition, &def, sizeof(def));if (err == OK) {*minUndequeuedBuffers += extraBuffers;break;}ALOGW("[%s] setting nBufferCountActual to %u failed: %d",mComponentName.c_str(), newBufferCount, err);/* exit condition */if (extraBuffers == 0) {return err;}}// 设置 native window 的buffer数量err = native_window_set_buffer_count(mNativeWindow.get(), def.nBufferCountActual);if (err != 0) {ALOGE("native_window_set_buffer_count failed: %s (%d)", strerror(-err),-err);return err;}// 设置 buffercount 和 buffersize*bufferCount = def.nBufferCountActual;*bufferSize =  def.nBufferSize;return err;
}

由于不了解 Graphic 相关的内容,所以这部分只能边看边猜,以下是我自己的理解,可能有误

  1. 计算output buffer 数量时首先会从 OMX 组件获取输出端口的配置,配置中定义有最小和最大需要的 buffer 数量;
  2. 将从 native window 中 deque buffer 这个动作设置为阻塞的;
  3. 如果是 tunnel mode,不会从上层获取 output buffer,buffer 的数量设置为0;
  4. 从 native window 获取最小的还未出队列的 buffer 数量;
  5. 重新计算OMX 组件上真实使用的 output buffer 的数量;
  6. 设置 native window 的 buffer数量为真实使用的buffer 的数量。

这里对 nBufferCountActual (真实使用的buffer数量)的计算比较令人疑惑,上面的代码中有一个循环,会尝试将 nBufferCountMin (最小 buffer 数量)+ minUndequeuedBuffers + extra 作为真实值,并且尝试设定给组件,只要这个值没有超过最大值就可以成功设定。

这里的 minUndequeuedBuffers 代表什么意思呢?上文中的 mMetadataBuffersToSubmit = bufferCount - minUndequeuedBuffers 又是代表什么意思呢?

我的理解是native window 有一套自己的buffer管理机制,并不是一开始解码就把所有的output buffer全部分配出来,会根据需求来管理需要的buffer数量。那么开始解码需要有多个块output buffer呢?

支持OMX运行所需要的最少的output buffer 数量为 nBufferCountMin,开始解码时就先分配这么多个(mMetadataBuffersToSubmit 个),如果需要更多再去剩余还未使用的buffer( minUndequeuedBuffers 个)中获取新的 buffer。

1.2、BufferInfo

接下来我们了解一下 BufferInfo,它是一个容器用于记录分配出的 input/output buffer/Handle/MetaData,并且记录下它们所携带的一些信息

    struct BufferInfo {enum Status {OWNED_BY_US,OWNED_BY_COMPONENT,OWNED_BY_UPSTREAM,OWNED_BY_DOWNSTREAM,OWNED_BY_NATIVE_WINDOW,UNRECOGNIZED,            // not a tracked buffer};static inline Status getSafeStatus(BufferInfo *info) {return info == NULL ? UNRECOGNIZED : info->mStatus;}IOMX::buffer_id mBufferID;Status mStatus;unsigned mDequeuedAt;sp<MediaCodecBuffer> mData;  // the client's buffer; if not using data conversion, this is// the codec buffer; otherwise, it is allocated separatelysp<RefBase> mMemRef;         // and a reference to the IMemory, so it does not go awaysp<MediaCodecBuffer> mCodecData;  // the codec's buffersp<RefBase> mCodecRef;            // and a reference to the IMemorysp<GraphicBuffer> mGraphicBuffer;bool mNewGraphicBuffer;int mFenceFd;FrameRenderTracker::Info *mRenderInfo;// The following field and 4 methods are used for debugging onlybool mIsReadFence;// Store |fenceFd| and set read/write flag. Log error, if there is already a fence stored.void setReadFence(int fenceFd, const char *dbg);void setWriteFence(int fenceFd, const char *dbg);// Log error, if the current fence is not a read/write fence.void checkReadFence(const char *dbg);void checkWriteFence(const char *dbg);};

我们首先要注意的是ACodec::BufferInfo 和 ACodecBufferChannel::BufferInfo 以及 MediaCodec::BufferInfo是三个不一样的内容,我们这里只了解 ACodec::BufferInfo。

  • Status:表示当前 buffer 的使用者
    • OWNED_BY_US:表示 buffer 使用者是 ACodec;
    • OWNED_BY_COMPONENT:表示把 buffer 交给 OMX 组件;
    • OWNED_BY_UPSTREAM:表示 input buffer 使用完成回传给上层(MediaCodec);
    • OWNED_BY_DOWNSTREAM:表示 output buffer 填充好回传给上层;
    • OWNED_BY_NATIVE_WINDOW:表示 buffer 使用者是 native window

ACodec::BufferInfo 中还记录有 buffer id,ACodec 和 OMXNode 进行 buffer 信息传递都是通过该 id 来完成,id 由 OMXNode 创建。

mGraphicBuffer 指向从 native window 中dequeue 出来的 buffer;mNewGraphicBuffer 表示 dequeue出的buffer 是否被使用;mFenceFd 记录了 graphic buffer fence;

ACodec::BufferInfo 中有两个 MediaCodecBuffer,它记录的是分配出的buffer或者handle

    const sp<AMessage> mMeta;sp<AMessage> mFormat;const sp<ABuffer> mBuffer;

MediaCodecBuffer 有3个成员,一个是 mMeta用于存储 buffer 的元信息,一个是 mFormat 用于存储解码数据携带的格式(例如crop),还有一个就是 mBuffer 用于存储真正的数据或句柄。

mDequeuedAt 用于记录 output buffer(native window buffer)被使用的次数。

1.3、useBuffer

再回到第一节allocateOutputMetadataBuffers当中,可以看到创建 bufferInfo 时,状态为 OWNED_BY_NATIVE_WINDOW,说明此时 buffer 还是在 native window 当中,并未真正被使用。

这里的 mData 指向一个 MediaCodecBuffer,MediaCodecBuffer 中的 mBuffer 的大小是之前获取到output buffer 的大小,后面的使用中又把这块 Buffer 强转为了 VideoNativeMetadata,所以我们就可以猜测,这里的 bufferSize 就是 VideoNativeMetadata 这个结构体的大小。

创建完 BufferInfo 之后,一般来说需要调用 useBuffer 把获取到的 graphic buffer 传递给 OMX,但是这里并没有真正 dequeue 出来,所以就设了一个空buffer(OMXBuffer::sPreset)。

这种情况下 BufferInfo 并不需要使用 mData 以及 mCodeData,但是为了统一,还是创建了一个 VideoNativeMetaData 对象。

最后分配出的 buffer 会统一存储到数组 mBuffer 当中。

1.4、allocateOutputBuffersFromNativeWindow

这里会简单了解 tunnel mode下有 allocateBuffersOnPort 的流程,这种情况下会调用到 allocateOutputBuffersFromNativeWindow,进入方法后首先还是会调用 configureOutputBuffersFromNativeWindow 计算output buffer 的数量,这里我们在上一节已经看过了,tunnel mode 会直接返回0,所以 allocateOutputBuffersFromNativeWindow 中会什么都不做,因此,这种情况下上层不会接触到 output buffer。

2、input buffer allocate

input buffer 和无 native window下 output buffer的分配流程是一致的,本质上是分配一块可以通过 HIDL 传递的共享内存。

但是如果是 secure mode,input buffer 就需要让 OMX 分配一个 buffer handle,再把handle传回来。

        OMX_PARAM_PORTDEFINITIONTYPE def;InitOMXParams(&def);def.nPortIndex = portIndex;err = mOMXNode->getParameter(OMX_IndexParamPortDefinition, &def, sizeof(def));if (err == OK) {const IOMX::PortMode &mode = mPortMode[portIndex];size_t bufSize = def.nBufferSize;// Always allocate VideoNativeMetadata if using ANWBuffer.// OMX might use gralloc source internally, but we don't share// metadata buffer with OMX, OMX has its own headers.if (mode == IOMX::kPortModeDynamicANWBuffer) {bufSize = sizeof(VideoNativeMetadata);} else if (mode == IOMX::kPortModeDynamicNativeHandle) {bufSize = sizeof(VideoNativeHandleMetadata);}size_t conversionBufferSize = 0;sp<DataConverter> converter = mConverter[portIndex];if (converter != NULL) {// here we assume conversions of max 4:1, so result fits in int32if (portIndex == kPortIndexInput) {conversionBufferSize = converter->sourceSize(bufSize);} else {conversionBufferSize = converter->targetSize(bufSize);}}size_t alignment = 32; // This is the value currently returned by// MemoryDealer::getAllocationAlignment().// TODO: Fix this when Treble has// MemoryHeap/MemoryDealer.ALOGV("[%s] Allocating %u buffers of size %zu (from %u using %s) on %s port",mComponentName.c_str(),def.nBufferCountActual, bufSize, def.nBufferSize, asString(mode),portIndex == kPortIndexInput ? "input" : "output");// verify buffer sizes to avoid overflow in align()if (bufSize == 0 || max(bufSize, conversionBufferSize) > kMaxCodecBufferSize) {ALOGE("b/22885421");return NO_MEMORY;}// don't modify bufSize as OMX may not expect it to increase after negotiationsize_t alignedSize = align(bufSize, alignment);size_t alignedConvSize = align(conversionBufferSize, alignment);if (def.nBufferCountActual > SIZE_MAX / (alignedSize + alignedConvSize)) {ALOGE("b/22885421");return NO_MEMORY;}if (mode != IOMX::kPortModePresetSecureBuffer) {mAllocator[portIndex] = TAllocator::getService("ashmem");if (mAllocator[portIndex] == nullptr) {ALOGE("hidl allocator on port %d is null",(int)portIndex);return NO_MEMORY;}// TODO: When Treble has MemoryHeap/MemoryDealer, we should// specify the heap size to be// def.nBufferCountActual * (alignedSize + alignedConvSize).}const sp<AMessage> &format =portIndex == kPortIndexInput ? mInputFormat : mOutputFormat;for (OMX_U32 i = 0; i < def.nBufferCountActual && err == OK; ++i) {hidl_memory hidlMemToken;sp<TMemory> hidlMem;sp<IMemory> mem;BufferInfo info;info.mStatus = BufferInfo::OWNED_BY_US;info.mFenceFd = -1;info.mRenderInfo = NULL;info.mGraphicBuffer = NULL;info.mNewGraphicBuffer = false;if (mode == IOMX::kPortModePresetSecureBuffer) {void *ptr = NULL;sp<NativeHandle> native_handle;err = mOMXNode->allocateSecureBuffer(portIndex, bufSize, &info.mBufferID,&ptr, &native_handle);info.mData = (native_handle == NULL)? new SecureBuffer(format, ptr, bufSize): new SecureBuffer(format, native_handle, bufSize);info.mCodecData = info.mData;} else {bool success;auto transStatus = mAllocator[portIndex]->allocate(bufSize,[&success, &hidlMemToken](bool s,hidl_memory const& m) {success = s;hidlMemToken = m;});if (!transStatus.isOk()) {ALOGE("hidl's AshmemAllocator failed at the ""transport: %s",transStatus.description().c_str());return NO_MEMORY;}if (!success) {return NO_MEMORY;}hidlMem = mapMemory(hidlMemToken);if (hidlMem == nullptr) {return NO_MEMORY;}err = mOMXNode->useBuffer(portIndex, hidlMemToken, &info.mBufferID);if (mode == IOMX::kPortModeDynamicANWBuffer) {VideoNativeMetadata* metaData = (VideoNativeMetadata*)((void*)hidlMem->getPointer());metaData->nFenceFd = -1;}info.mCodecData = new SharedMemoryBuffer(format, hidlMem);info.mCodecRef = hidlMem;// if we require conversion, allocate conversion buffer for client use;// otherwise, reuse codec bufferif (mConverter[portIndex] != NULL) {CHECK_GT(conversionBufferSize, (size_t)0);bool success;mAllocator[portIndex]->allocate(conversionBufferSize,[&success, &hidlMemToken](bool s,hidl_memory const& m) {success = s;hidlMemToken = m;});if (!success) {return NO_MEMORY;}hidlMem = mapMemory(hidlMemToken);if (hidlMem == nullptr) {return NO_MEMORY;}info.mData = new SharedMemoryBuffer(format, hidlMem);info.mMemRef = hidlMem;} else {info.mData = info.mCodecData;info.mMemRef = info.mCodecRef;}}mBuffers[portIndex].push(info);

套路还是一样的,首先获取 port def,拿到 buffer 数量,如果是secure mode,那么就调用 allocateSecureBuffer 分配 native_handle,再封装为 SecureBuffer

如果是普通buffer,那么就调用共享内存分配一块hidl_memory ,然后在ACodec层直接映射成为 TMemory,再封装为 SharedMemoryBuffer;除此之外,还要调用 useBuffer,将分配的共享内存传递给 OMX。

    std::vector<ACodecBufferChannel::BufferAndId> array(mBuffers[portIndex].size());for (size_t i = 0; i < mBuffers[portIndex].size(); ++i) {array[i] = {mBuffers[portIndex][i].mData, mBuffers[portIndex][i].mBufferID};}if (portIndex == kPortIndexInput) {mBufferChannel->setInputBufferArray(array);} else if (portIndex == kPortIndexOutput) {mBufferChannel->setOutputBufferArray(array);} else {TRESPASS();}

当一个端口的buffer分配完成后,会把所有创建出来的BufferInfo中的mData成员(MediaCodecBuffer)组织成一个 vector,并且交由 ACodecBufferChannel 管理。

到这里,buffer的分配就完成了,buffer 分配完成后,OMX组件收到所有的 buffer,结束 IDLE 状态,把状态设定完成的信息回传给 ACodec,ACodec 接收到之后会再下命令让 OMX 组件进入 OMX_StateExecuting 状态,同时 ACodec 进入 IdleToExecuting 状态。

3、IdleToExecutingState

进入 IdleToExecutingState 状态时并不会有什么动作,当 OMX 组件返回状态设定完成的消息时,ACodec 会先调用 ExecutingState 的 resume 方法,然后正式进入 ExecutingState,这部分我们下节再了解。

bool ACodec::IdleToExecutingState::onOMXEvent(OMX_EVENTTYPE event, OMX_U32 data1, OMX_U32 data2) {switch (event) {case OMX_EventCmdComplete:{if (data1 != (OMX_U32)OMX_CommandStateSet|| data2 != (OMX_U32)OMX_StateExecuting) {ALOGE("Unexpected command completion in IdleToExecutingState: %s(%u) %s(%u)",asString((OMX_COMMANDTYPE)data1), data1,asString((OMX_STATETYPE)data2), data2);mCodec->signalError(OMX_ErrorUndefined, FAILED_TRANSACTION);return true;}mCodec->mExecutingState->resume();mCodec->changeState(mCodec->mExecutingState);return true;}default:return BaseState::onOMXEvent(event, data1, data2);}
}

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

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

相关文章

【深度学习目标检测】八、基于yolov5的抽烟识别(python,深度学习)

YOLOv5是目标检测领域一种非常优秀的模型&#xff0c;其具有以下几个优势&#xff1a; 1. 高精度&#xff1a;YOLOv5相比于其前身YOLOv4&#xff0c;在目标检测精度上有了显著的提升。YOLOv5使用了一系列的改进&#xff0c;如更深的网络结构、更多的特征层和更高分辨率的输入图…

Git及Linux命令介绍

Git介绍 Git 命令如何工作 首先&#xff0c;必须确定我们的代码存储在哪里。常见的假设是只有两个位置 - 一个位于 Github 等远程服务器上&#xff0c;另一个位于我们的本地计算机上。然而&#xff0c;这并不完全准确。 Git 在我们的机器上维护了三个本地存储&#xff0c;这意…

Linux-----12、时间日期

# 时间日期 # 时区设置 在Linux (opens new window)系统中&#xff0c;默认使用的是UTC时间。 即使在安装系统的时候&#xff0c;选择的时区是亚洲上海&#xff0c;Linux默认的BIOS时间&#xff08;也称&#xff1a;硬件时间&#xff09;也是UTC时间 (opens new window)。 在…

关于C#反射概念,附带案例!

反射 C#中的反射是一种使程序在运行时能够动态地获取类型信息并调用其成员的技术。通过反射&#xff0c;程序可以在运行时进行类型的动态加载、创建对象、调用方法和属性&#xff0c;以及访问和修改字段等。反射可以使程序更加灵活&#xff0c;但也增加了一定的性能开销。 在C#…

90%的人学Python爬虫都干过这种事,别不承认!

可以说&#xff0c;我是因为想批量下载一个网站的图片&#xff0c;才开始学的python爬虫。当一张一张图片自动下载下来时&#xff0c;满满的成就感&#xff0c;也满满的罪恶感……哈哈哈&#xff01;&#xff01;&#xff01;窈窕淑女&#xff0c;君子好逑&#xff0c;这篇文章…

Android 大版本升级变更截图方法总结

Android 大版本升级变更截图方法总结 一、Android R (11) 平台二、Android S (12) 平台三、Android U (14) 平台 Android 原生的截屏功能是集成在 SystemUI 中&#xff0c;因此我们普通应用想要获取截图方法&#xff0c;就需要研读下 SystemUI 截屏部分的功能实现。 一、Androi…

Android 移动端编译 cityhash动态库

最近做项目&#xff0c; 硬件端 需要 用 cityhash 编译一个 动态库 提供给移动端使用&#xff0c;l 记录一下 编译过程 city .cpp // // Created by Administrator on 2023/12/12. // // Copyright (c) 2011 Google, Inc. // // Permission is hereby granted, free of charg…

java配置+J_IDEA配置+git配置+maven配置+基本语句

当前目录文件夹dir 进入文件夹cd 返回上一级cd.. 创建文件夹&#xff1a;mkdir 文件名删除文件夹&#xff1a;rd 文件夹名&#xff0c; 目录不为空不能直接删 rd /s 带子文件夹一起删 清屏cls 切换d盘才能进入 下载git地址&#xff1a; Git - Downloading Package (g…

使用youtube的api

如何使用youtube的data api https://console.cloud.google.com/apis/dashboard 到这个地方先启用api,找到YouTube Data API v3 这个api,启用它 然后创建凭据 去创建凭据,里面创建相应的客户端,web的需要填写redirect地址,就是回调用的.客户端不需要这个. 创建客户端不需要详…

一文读懂Allins-首个基于 AMM 的多链铭文资产交易协议

“Allins 是铭文赛道中基础设施类的代表&#xff0c;该协议致力于以 AMM 的方式推动铭文资产的流动性&#xff0c;并为铭文资产交易者提供更好的 UI/UX。” 2023年1月份后&#xff0c;比特币Ordinals协议的推出为铭文赛道的兴起奠定了基础。该协议以聪为单位将比特币划分&#…

第十一章 函数式编程

11.1 函数式编程与命令式编程 以函数式范式进行开发并不简单&#xff1b;关键在于习惯这种范式的机制。我们编写一个例子来说明 差异。 假设我们想打印一个数组中所有的元素。我们可以用命令式编程&#xff0c;声明的函数如下&#xff1a; var printArray function(a…

ipa分发平台绑定域名有什么优势

大家好我是咕噜签名分发可爱多。今天跟大家分享一下&#xff0c;为什么建议大家将自己的域名绑定到分发平台&#xff08;比如咕噜分发&#xff09;。 将自己的域名绑定分发平台有几个原因和优势&#xff1a; 1. 专业性和品牌建设&#xff1a; 使用自己的域名可以让您的在线存…

【PTA-C语言】实验一-顺序结构

如果代码存在问题&#xff0c;麻烦大家指正 ~ ~有帮助麻烦点个赞 ~ ~ 实验一-顺序结构 7-1 逆序的三位数&#xff08;分数 10&#xff09;7-2 求整数均值&#xff08;分数 10&#xff09;7-3 日期格式化&#xff08;分数 10&#xff09;7-4 混合类型数据格式化输入&#xff08;…

消息队列有哪些应用场景?

分布式系统不同模块之间的通信&#xff0c;除了远程服务调用以外&#xff0c;消息中间件是另外一个重要的手段&#xff0c;在各种互联网系统设计中&#xff0c;消息队列有着广泛的应用。从本文开始&#xff0c;专栏进入分布式消息的模块&#xff0c;将讨论消息队列使用中的高频…

Windows如何安装使用TortoiseSVN客户端并实现公网访问本地SVN Server

文章目录 前言1. TortoiseSVN 客户端下载安装2. 创建检出文件夹3. 创建与提交文件4. 公网访问测试 前言 TortoiseSVN是一个开源的版本控制系统&#xff0c;它与Apache Subversion&#xff08;SVN&#xff09;集成在一起&#xff0c;提供了一个用户友好的界面&#xff0c;方便用…

再谈低代码开发——值得所有程序设计和开发者重视的建议!

前几天看到关于“低代码开发”的话题&#xff0c;简单的谈了些自己的看法&#xff0c;也看了一些朋友们各抒己见的好文章&#xff0c;今天想结合我们实际使用的开发平台和大家再做些探讨。 在平台的简介中首先提出了这个大家一定很关心的问题&#xff1a; 一、“为什么使用低代…

三防平板电脑定制参数_三防移动平板终端方案

这是一款搭载了低功耗高性能CPU的三防平板电脑。采用联发科MT6771处理器&#xff0c;内置4GB64GB内存和八核处理器&#xff0c;提供出色的性能和运行速度。同时&#xff0c;它运行着最新的安卓Android 11.0系统&#xff0c;全屏支持和屏幕内容显示的优化使其更加方便和简单易用…

【华大】HC32F420JATB-LQ48学习资料及开发环境

1. 产品特点 ⚫ 84MHz Cortex-M4 32位CPU平台 ⚫ 128K 字节 FLASH 存储器&#xff0c;具有擦写保护功能 ⚫ 24K 字节 RAM 存储器 ⚫ 6 通道 DMAC ⚫多达 52 个通用 I/O 管脚 ⚫ 时钟、晶振 ‒ 外部高速晶振 8MHz~32MHz ‒ 内部高速时钟 22MHz~24MHz ‒ 内部低速时钟 3…

GoWin FPGA, GPIO--- startup1

一个Bank只能用一个电压&#xff0c;假如同一个Bank&#xff0c;在引脚里设置不同的电压&#xff0c;编译不过。 解释说明 2. 错误引脚限制 以上编译设置会导致编译错误。 Floor planner说明

猫粮哪个牌子好又安全?安全的主食冻干猫粮牌子推荐

由于猫咪是肉食动物&#xff0c;对蛋白质的需求很高&#xff0c;如果摄入的蛋白质不足&#xff0c;就会影响猫咪的成长。而冻干猫粮本身因为制作工艺的原因&#xff0c;能保留原有的营养成分和营养元素&#xff0c;所以冻干猫粮蛋白含量比较高&#xff0c;营养又高&#xff0c;…