Pag的2D渲染执行流程

news2024/11/26 18:31:31

Pag的渲染

背景

根据Pag文章里面说的,Pag之前长时间使用的Skia库作为底层渲染引擎。但由于Skia库体积过大,为了保证通用型(比如兼容CPU渲染)做了很多额外的事情。所以Pag的工程师们自己实现了一套2D图形框架替换掉Skia,在github里面在一个叫 TGFX 的目录内。我猜测腾讯的工程师这里是致敬暴雪的bgfx吧,不过这两个库其实有本质区别:bgfx更多是统一底层图形API为目标的,而tgfx是一套用于2D的渲染库,核心目标是替换掉Skia。其目前主要用OpenGL API用GPU实现渲染,默认不支持CPU渲染(也能通过SwiftShader来支持),在绝大部分设备上GPU渲染都会更高效。其API也和Skia的API相似度很高,只是没有SK开头罢了。这些API对于做Android应用开发的同学来说再熟悉不过了。不过我还是整体理一下里面的核心概念吧:

概念描述
Bitmap位图,它是像素的集合,是内存里的色彩的表现和承载者,一般用CPU解码图片后就会保存为Bitmap,也可以通过CPU去操作Bitmap的像素。
Texture纹理,也代表一张图,在OpenGL内使用,可以简单的理解为在显存内的Bitmap,Pag里面的视频/素材 都是通过Texture渲染的。
Surface表面,其实更像是一个装画的地方。画画我们可以在纸上画,也可以在墙上画。
Paint画笔,可以自定义各种色彩,样式等,拿起什么样的比就画出什么样的画。
Canvas画布,可以看做一种渲染过程,比如draw/drawTexture/drawPath等。经历过怎样的渲染过程就会产出什么样的结果。比如我画了一个狮子,再画一个老虎,那最终就会呈现出虎狮

概要

为了了解全貌,我这边把一些重要的概念和整体的Pag模块画了一个图,大家可以看到TGFX在整个libpag内的位置:
Pag整体架构

接下来我们还是从情景来分析pag渲染的具体流程。

Pag渲染流程分析

之前我们介绍了Pag的基本结构组织,这里来详细讲一下渲染过程,也就是如下两个方法调用:

1. 设置进度
pagPlayer.setProgress(progress);
2. 执行渲染
pagPlayer.flush();

设置进度

这个应该都大致能猜到,肯定核心逻辑就是会去设置到目标合成里通过归一化的percent通知到RootComposition当前有进度更新,需要准备下一次渲染的素材了之类的逻辑。具体代码如下:

void PAGPlayer::setProgress(double percent) {
  LockGuard autoLock(rootLocker);
  auto pagComposition = stage->getRootComposition();
  if (pagComposition == nullptr) {
    return;
  }
  auto realProgress = percent;
  auto frameRate = pagComposition->frameRateInternal();
  if (_maxFrameRate < frameRate && _maxFrameRate > 0) {
    auto duration = pagComposition->durationInternal();
    auto totalFrames = TimeToFrame(duration, frameRate);
    auto numFrames = ceilf(totalFrames * _maxFrameRate / frameRate);
    // 首先计算在maxFrameRate的帧号,之后重新计算progress
    auto targetFrame = ProgressToFrame(realProgress, numFrames);
    realProgress = FrameToProgress(targetFrame, numFrames);
  }
  pagComposition->setProgressInternal(realProgress);
}

发现这里其实还有一个播放器的最大渲染帧率的转换逻辑,如果合成的帧率比PagPlayer的目标帧率还高,需要根据间隔去做采样为播放器的最大帧率。这个在低端设备上应该会对性能会有一些提升,能够更大程度的利用缓存。核心的pagComposition->setProgressInternal(realProgress);是实现在PagLayer内的,它会通过progress计算出当前应该播放的时间,然后调用gotoTimeAndNotifyChanged方法:

bool PAGLayer::gotoTimeAndNotifyChanged(int64_t targetTime) {
  auto changed = gotoTime(targetTime);
  if (changed) {
    notifyModified();
  }
  return changed;
}

顾名思义,先Seek到指定的时间,然后如果发现图层需要更新(核心是一个_stretchedFrameDuration,这个是在AE插件里面保存的值,如果是静态图之类的,会和duration相同,更新progress不需要改变内容),则会调用notifyModified去通知有更新,我们看看里面的实现:

void PAGLayer::notifyModified(bool contentChanged) {
  if (contentChanged) {
    contentVersion++;
  }
  auto parentLayer = getParentOrOwner();
  while (parentLayer) {
    parentLayer->contentVersion++;
    parentLayer = parentLayer->getParentOrOwner();
  }
}

可以看到首先把自己的contentVersion做了自增,然后如果有父图层以及父图层更新contentVersion,我们这里本身是根图层更新,不会涉及parent。下一次渲染的时候会对比这个contentVersion和上一次的是否一致,如果一致的话,就不需要重新绘制了,通过这个version来避免额外性能损耗。

渲染过程

渲染过程的流程主要是分为:

  1. 组织渲染 – 主要是通过调用Canvas的一些方法,把所有渲染相关的指令组织起来,保存到上下文内
  2. 执行渲染 – 把上面组织起来的所有渲染相关的指令flush到GPU内,完成一次渲染
  3. 上屏渲染 – 等待渲染操作完成,把渲染完成的Buffer上屏到显示设备上

我这里先画一个简单的类图,把里面涉及到的大致的概念呈现出来,让大家有一个简单的认识。把里面的一些主要概念的关系画出来,也包含了里面的一些功能说明。
渲染相关类图

组织渲染

Pag执行渲染的方法叫做flush,就是把当前设置的这些配置要给执行了。Flush的实现核心是调用了flushInternal方法:

bool PAGPlayer::flushInternal(BackendSemaphore* signalSemaphore) {
  ...
  prepareInternal();
  clock.mark("rendering");
  if (!pagSurface->draw(renderCache, lastGraphic, signalSemaphore, _autoClear)) {
    return false;
  }
  ...
  return true;
}

我们这里省略了大部分其他代码,直接看了两个最重要的prepareInternalpagSurface->draw()调用就可以了。
prepareInternal的实现如下:

void PAGPlayer::prepareInternal() {
  // 为了提升性能,预加载加载视频和图片
  renderCache->prepareLayers();
  // 通过contentVersion来判断stage是否有刷新,没有刷新的话就不用去重新渲染了
  if (contentVersion != stage->getContentVersion()) {
    // 更新当前的渲染内容版本号
    contentVersion = stage->getContentVersion();
    Recorder recorder = {};
    // 难道在这里就渲染了?并不是哦,只是组织图层到recorder里面
    stage->draw(&recorder);
    lastGraphic = recorder.makeGraphic();
  }
  if (lastGraphic) {
    lastGraphic->prepare(renderCache);
  }
}

可以看到这里在判断需要刷新stage的时候有个stage->draw(&recorder);调用,stage代码如下:

void PAGComposition::draw(Recorder* recorder) {
  ... // 这里省略缓存策略
  auto preComposeLayer = static_cast<PreComposeLayer*>(layer);
  auto composition = preComposeLayer->composition;
  ... // 这里省略位图或者视频策略,Clip和判空逻辑
  auto count = static_cast<int>(layers.size());
  // 遍历所有的图层,然后挨个调用DrawChildLayer
  for (int i = 0; i < count; i++) {
    auto& childLayer = layers[i];
    if (!childLayer->layerVisible) {
      continue;
    }
    DrawChildLayer(recorder, childLayer.get());
  }
  ... // 省略Clip逻辑
}

这个方法核心其实就是遍历所有的childLayer,然后挨个调用DrawChildLayer把每一层存入Recorder内,我们看看DrawChildLayer的代码:

void PAGComposition::DrawChildLayer(Recorder* recorder, PAGLayer* childLayer) {
  // 图层的特效Modifier
  auto filterModifier = childLayer->cacheFilters() ? nullptr : FilterModifier::Make(childLayer);
  // 多点追踪使用的,暂时可以不用理会
  auto trackMatte = TrackMatteRenderer::Make(childLayer);
  Transform extraTransform = {ToTGFX(childLayer->layerMatrix), childLayer->layerAlpha};

  LayerRenderer::DrawLayer(recorder, childLayer->layer,
                           childLayer->contentFrame + childLayer->layer->startTime, filterModifier,
                           trackMatte.get(), childLayer, &extraTransform);
}

看到这个代码有点晕了,开始有TGFX的影子了。这里核心是调用静态函数LayerRenderer::DrawLayer:

void LayerRenderer::DrawLayer(Recorder* recorder, Layer* layer, Frame layerFrame,
                              std::shared_ptr<FilterModifier> filterModifier,
                              TrackMatte* trackMatte, Content* layerContent,
                              Transform* extraTransform) {
  if (TransformIllegal(extraTransform) || TrackMatteIsEmpty(trackMatte)) {
    return;
  }
  auto contentFrame = layerFrame - layer->startTime;
  // 这里比较核心返回一个layoutCache,里面有包含各种图层类型的渲染内容缓存的实现。
  auto layerCache = LayerCache::Get(layer);
  if (!layerCache->contentVisible(contentFrame)) {
    return;
  }
  auto content = layerContent ? layerContent : layerCache->getContent(contentFrame);
  ... //省略alpha,Blend, 多点追踪逻辑
  auto saveCount = recorder->getSaveCount();
  ... // 省略其他Transferm,Mask等逻辑
  // 核心draw逻辑
  content->draw(recorder);
  recorder->restoreToCount(saveCount);
  ... // 省略多点追踪逻辑
  recorder->restore();
}

可以看到这里核心流程就是通过 auto layerCache = LayerCache::Get(layer); 创建/获取了一个LayerCache。然后执行LayerCache的content的draw方法到recorder对象内。看看LayerCache::Get的代码:

LayerCache* LayerCache::Get(Layer* layer) {
  std::lock_guard<std::mutex> autoLock(layer->locker);
  if (layer->cache == nullptr) {
    layer->cache = new LayerCache(layer);
  }
  return static_cast<LayerCache*>(layer->cache);
}
LayerCache::LayerCache(Layer* layer) : layer(layer) {
  switch (layer->type()) {
    case LayerType::Shape:
      contentCache = new ShapeContentCache(static_cast<ShapeLayer*>(layer));
      break;
    case LayerType::Text:
      contentCache = new TextContentCache(static_cast<TextLayer*>(layer));
      break;
    case LayerType::Solid:
      contentCache = new SolidContentCache(static_cast<SolidLayer*>(layer));
      break;
    case LayerType::Image:
      contentCache = new ImageContentCache(static_cast<ImageLayer*>(layer));
      break;
    case LayerType::PreCompose:
      contentCache = new PreComposeContentCache(static_cast<PreComposeLayer*>(layer));
      break;
    default:
      contentCache = new EmptyContentCache(layer);
      break;
  }
  contentCache->update();
  transformCache = new TransformCache(layer);
  if (!layer->masks.empty()) {
    maskCache = new MaskCache(layer);
  }
  updateStaticTimeRanges();
  maxScaleFactor = ToTGFX(layer->getMaxScaleFactor());
}

其实核心就是通过ContentCache类型分别创建GraphicContent类型。这个GraphicContent是一个用于具体实现不同类型的图形的结构了。有一点像Android里面的View。接下来调用了核心的content.draw(recorder)把需要渲染的GraphicsContent存入到我们的Recorder内。

void Recorder::drawGraphic(std::shared_ptr<Graphic> graphic) {
  auto content = Graphic::MakeCompose(std::move(graphic), matrix);
  if (content == nullptr) {
    return;
  }
  if (layerIndex == 0) {
    rootContents.push_back(content);
  } else {
    layerContents.push_back(content);
  }
}

从这里也说明了这些函数虽然叫做drawXXX但是在这一步并没有真正的执行任何渲染相关的指令。
接下来回到一开始的prepareInternal,我们已经分析了这里通过这个Recorder保存了渲染需要用的content。prepareInternal最后做的事情就是通过这个recorder再生成一个具体用于渲染的Graphic对象,并且再prepare这个Graphic:

  lastGraphic = recorder.makeGraphic();
  if (lastGraphic) {
    lastGraphic->prepare(renderCache);
  }

这个lastGraphic是一个LayerGraphic,而下一步lastGraphic->prepare才真正的是prepare要渲染的内容的。LayerGraphic是一个树形结构的包装对象,和Android里面的ViewGroup十分类似。其prepare也就是深度遍历这个渲染树,把所有的节点都prepare一遍。

void LayerGraphic::prepare(RenderCache* cache) const {
  for (auto& content : contents) {
    content->prepare(cache);
  }
}

针对不同的Content,会有不同的prepare过程。比如对图片的内容来说,就可以在这里去获取解码好的图片,如果没有解码好需要等待解码完成,视频也需要在这里等这个时间的帧解码好才能去做渲染。

渲染核心过程

组织渲染指令

准备工作看完了,我们这里开始来深入分析一下这个pagSurface->draw。先上代码,这里我加上了注释:

bool PAGSurface::draw(RenderCache* cache, std::shared_ptr<Graphic> graphic,
                      BackendSemaphore* signalSemaphore, bool autoClear) {
  // 之前提到过,这里的drawable代表的是一个抽象的GPU渲染实例(GPUDrawable/OffscreenDrawable等),并不是做具体绘制的实例。
  if (!drawable->prepareDevice()) {
    return false;
  }
  // 获取用于渲染的上下文,这里拿到的是GLContext
  auto context = lockContext();
  if (!context) {
    return false;
  }
  if (surface != nullptr && autoClear && contentVersion == cache->getContentVersion()) {
    unlockContext();
    return false;
  }
  // 如果Surface为空,则创建。这里的Surface是指的TGFX内的
  if (surface == nullptr) {
    surface = drawable->createSurface(context);
  }
  if (surface == nullptr) {
    unlockContext();
    return false;
  }
  contentVersion = cache->getContentVersion();
  cache->attachToContext(context);
  auto canvas = surface->getCanvas();
  if (autoClear) {
    canvas->clear();
  }
  if (graphic) {
    // 核心渲染的操作,会生成draw call
    graphic->draw(canvas, cache);
  }
  // 这里具体去执行draw call,把CPU内组合的渲染操作(Op最终同步到GPU内
  if (signalSemaphore == nullptr) {
    // 同步模式下直接flush操作
    surface->flush();
  } else {
    // 这里多了异步渲染模式下的同步锁操作。
    tgfx::GLSemaphore semaphore = {};
    surface->flush(&semaphore);
    signalSemaphore->initGL(semaphore.glSync);
  }
  cache->detachFromContext();
  drawable->setTimeStamp(pagPlayer->getTimeStampInternal());
  drawable->present(context);
  unlockContext();
  return true;
}

上面最核心的是两个调用。一个是生成DrawCall的

  if (graphic) {
    graphic->draw(canvas, cache);
  }

最终会把在prepareInternal里面生成的所有Content调用draw来生成这个content当前状态对应的OpenGL渲染指令集。我们这里用绘制一个Shape来举例:

void Shape::draw(tgfx::Canvas* canvas, RenderCache* renderCache) const {
  tgfx::Paint paint;
  auto snapshot = renderCache->getSnapshot(this);
  if (snapshot) {
    ...
    // 这里省略了使用缓存的逻辑 
    ...
  }
  paint.setShader(shader);
  canvas->drawPath(path, paint);
}

是不是和我们在Android里面的drawPath很像?这里的canvas的实现是GLCanvas:

void GLCanvas::fillPath(const Path& path, const Paint& paint) {
  ...
  // 注意这里,创建了一个GLDrawOp
  auto op = MakeSimplePathOp(path, glPaint, state->matrix);
  if (op) {
    draw(std::move(op), std::move(glPaint));
    return;
  }
  ...
  op = GLTriangulatingPathOp::Make(glPaint.color, tempPath, state->clip.getBounds(), localMatrix);
  if (op) {
    save();
    resetMatrix();
    draw(std::move(op), std::move(glPaint));
    restore();
    return;
  }
  ...
  drawMask(deviceBounds, mask->makeTexture(getContext()), std::move(glPaint));
}

我们这里省略了很多代码,基本可以看到流程就是创建GLDrawOp,在里面设置一堆参数,最后调用draw或者drawMask(最终也是调用到了draw),我们来看看这个draw方法:

void GLCanvas::draw(std::unique_ptr<GLDrawOp> op, GLPaint paint, bool aa) {
  if (drawContext == nullptr) {
    return;
  }
  // 设置抗锯齿类型
  auto aaType = AAType::None;
  if (static_cast<GLSurface*>(surface)->renderTarget->sampleCount() > 1) {
    aaType = AAType::MSAA;
  } else if (aa && !IsPixelAligned(op->bounds())) {
    aaType = AAType::Coverage;
  } else {
    const auto& matrix = state->matrix;
    auto rotation = std::round(RadiansToDegrees(atan2f(matrix.getSkewX(), matrix.getScaleX())));
    if (static_cast<int>(rotation) % 90 != 0) {
      aaType = AAType::Coverage;
    }
  }
  // 设置Mask
  auto masks = std::move(paint.coverageFragmentProcessors);
  Rect scissorRect = Rect::MakeEmpty();
  auto clipMask = getClipMask(op->bounds(), &scissorRect);
  if (clipMask) {
    masks.push_back(std::move(clipMask));
  }
  // 设置渲染区域
  op->setScissorRect(scissorRect);
  // 设置Blend图层叠加模式
  unsigned first;
  unsigned second;
  if (BlendAsCoeff(state->blendMode, &first, &second)) {
    op->setBlendFactors(std::make_pair(first, second));
  } else {
    op->setXferProcessor(PorterDuffXferProcessor::Make(state->blendMode));
    op->setRequireDstTexture(!GLCaps::Get(getContext())->frameBufferFetchSupport);
  }
  op->setAA(aaType);
  // 配置颜色
  op->setColors(std::move(paint.colorFragmentProcessors));
  op->setMasks(std::move(masks));
  // 添加到渲染队列
  drawContext->addOp(std::move(op));
}

基本的核心配置我已经在代码里给了注释。这个地方特别重要,属于渲染的核心流程,所以代码我没有省略。
这里设置了一些Paint的通用配置,然后加入到绘制的上下文drawContext->addOp(std::move(op));

void SurfaceDrawContext::addOp(std::unique_ptr<Op> op) {
  getOpsTask()->addOp(std::move(op));
}

很简单,其实就是把当前这一个Content的Op添加到opsTask列表里面了。说明这里其实还没有具体去执行这些OpenGL指令。值得注意的是这个getOpsTask里面有一个很重要的操作:

OpsTask* SurfaceDrawContext::getOpsTask() {
  if (opsTask == nullptr || opsTask->isClosed()) {
    replaceOpsTask();
  }
  return opsTask.get();
}

void SurfaceDrawContext::replaceOpsTask() {
  opsTask = surface->getContext()->drawingManager()->newOpsTask(surface);
}

这个opsTask是通过replaceOpsTask里面的surface->getContext()->drawingManager()->newOpsTask(surface);创建的。这样就会被DrawingManager所管理起来,之后再被统一执行。

执行渲染

上面基本就把准备渲染的opTask准备好了。其实渲染的准备工作是最重要的,执行渲染只是把前面存放的即将用于渲染的指令具体去执行罢了。

if (signalSemaphore == nullptr) {
    surface->flush();
  } else {
    tgfx::GLSemaphore semaphore = {};
    surface->flush(&semaphore);
    signalSemaphore->initGL(semaphore.glSync);
  }

很明显这里面的这个Surface->flush就是渲染具体执行的地方了。里面的代码很简单:

bool Surface::flush(Semaphore* signalSemaphore) {
  // 解析渲染的目标。因为Pag支持设置渲染到纹理上,不一定都是上屏,所以这里会手动插入一个配置Target的Task
  renderTarget->getContext()->drawingManager()->newTextureResolveRenderTask(this);
  // 这里把之前保存的opTask都执行了
  return renderTarget->getContext()->drawingManager()->flush(signalSemaphore);
}

我们看看这里核心调用的DrawingManager->flush具体是做哪些事:

bool DrawingManager::flush(Semaphore* signalSemaphore) {
  auto* gpu = context->gpu();
  closeAllTasks();
  activeOpsTask = nullptr;
  for (auto& task : tasks) {
    task->execute(gpu);
  }
  removeAllTasks();
  return context->caps()->semaphoreSupport && gpu->insertSemaphore(signalSemaphore);
}

可以看到这里的核心就是会把之前所有的GLCanvas执行的opTask都执行一遍,然后再清空这个tasks列表。这样就完成了渲染指令的具体执行了。

上屏渲染

在PagSurface的draw最后几段代码如下:

  drawable->setTimeStamp(pagPlayer->getTimeStampInternal());
  drawable->present(context);

一开始的时候提到过,Pag里面的Drawable其实是一个渲染设备的封装。这个present在最后会调用到OpenGL的swapbuffer函数,将前面的所有操作交换缓冲区实现上屏到具体的渲染Target。这里我们就不具体去分析了。

总结

今天这个文章里面的代码比较多了,有一点乱,而且这里只是一次渲染,还没有讲到Pag如何管理渲染的缓存,实现上一期讲的纵向分层,水平分块把性能提升的。如果要深入理解建议自己去Debug到Pag的流程里面才能真正的了解内部渲染的执行过程,能够学到更多的东西。

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

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

相关文章

GC Garbage Collectors

本质一、算法1、哪些是垃圾&#xff1f;引用计数法&#xff1a;reference countPython中使用了。个对象如果没有任何与之关联的引用&#xff0c;即他们的引用计数都不为 0&#xff0c;则说明对象不太可能再被用到&#xff0c;那么这个对象就是可回收对象。漏洞&#xff1a;循环…

C/C++每日一练(20230303)

目录 1. 字符串相乘 2. 单词拆分 II 3. 串联所有单词的子串 1. 字符串相乘 给定两个以字符串形式表示的非负整数 num1 和 num2&#xff0c;返回 num1 和 num2 的乘积&#xff0c;它们的乘积也表示为字符串形式。 示例 1: 输入: num1 "2", num2 "3"…

【Linux】PXE+Kickstart无人值守安装系统

文章目录前言一、简介二、配置DHCP三、TFTP四、SYSLinux服务程序五、vsftpd服务六、Kickstart应答文件七、自动安装系统八、总结前言 本文来记录下PXEKickstart无人值守安装系统。 当需要安装OS的主机数量较多时&#xff0c;我们不可能通过U盘或光盘给这一台台主机去安装操作系…

SpringCloud项目报错和解决方法记录

1、项目在IDEA中正常启动&#xff0c;打包后报错 背景 项目本地是没有 application.yml 配置文件的&#xff0c;而是把配置文件放在nacos上&#xff0c;本地只有一个 bootstrap.yml 来绑定nacos上的配置文件。 项目在IDEA上是可以正常启动运行的&#xff0c;然后我就准备打包…

Hadoop之hdfs查看fsimage和edits

/opt/module/hadoop-3.1.3/data/dfs/name/current (1) Fsimage文件: HDFS文件系统元数据的一个永久性的检查点&#xff0c;其中包含HDFS文件系统的所有目录和文件inode的序列化信息。 (2) Edits文件:存放HDFS文件系统的所有更新操作的路径&#xff0c;文件系统客户端执行的所有…

10个优质的基于Node.js的CMS 内容管理平台

冬尽今宵长❝hi, 大家好, 我是徐小夕,之前和大家分享了很多「低代码可视化」和「前端工程化」相关的话题, 今天继续和大家聊聊「CMS」系统.❞内容管理系统 (「CMS」) 使没有强大技术背景的人也能够轻松发布内容。我们可以使用 「CMS」 来管理我们的内容和交付。市面上有不同类型…

触摸屏如何远距离无线采集各从站的模拟量信号?

本方案是昆仑通态触摸屏与4台DTD433FC无线模拟量信号测试终端进行无线 MODBUS 通信的实现方法。本方案中昆仑通态触摸屏作为主站显示各从站的模拟量信号&#xff0c;传感器、DCS、PLC、智能仪表等4个设备作为Modbus从站输出模拟量信号。方案中采用无线模拟量信号测控终端DTD433…

厚积薄发百变求新 | 科士达100kW/125kW 超大功率模块UPS重磅发布

3月2日下午&#xff0c;“厚积薄发百变求新” 科士达100kW/125kW超大功率模块UPS新品发布会在云上顺利举办。会上&#xff0c;科士达正式推出100kW/125kW超大功率模块UPS&#xff0c;该产品功率密度较主流的50kW功率模块UPS提升了一倍&#xff0c;跨越了高密新高度。 本次发布会…

Python每日一练(20230303)

1. 两数之和 给定一个整数数组 nums 和一个整数目标值 target&#xff0c;请你在该数组中找出 和为目标值 的那 两个 整数&#xff0c;并返回它们的数组下标。 你可以假设每种输入只会对应一个答案。但是&#xff0c;数组中同一个元素在答案里不能重复出现。 你可以按任意顺…

详解单链表(内有精美图示哦)

全文目录引言链表链表的定义与结构链表的分类单链表的实现及对数据的操作单链表的创建与销毁创建销毁单链表的打印单链表的头插与头删头插头删单链表的尾插与尾删尾插尾删单链表的查找单链表在pos位置后插入/删除插入删除单链表在pos位置插入/删除插入删除总结引言 在上一篇文…

K8s:渐进式入门服务网格 Istio (一)

写在前面 分享一些 Istio 的学习笔记博文内容涉及&#xff1a; istio 下载安装一个 Demo 运行什么是 istio&#xff0c;服务网格等概念介绍istio 架构组成&#xff0c;应用场景等 理解不足小伙伴帮忙指正 对每个人而言&#xff0c;真正的职责只有一个&#xff1a;找到自我。然后…

一文吃透 Go 内置 RPC 原理

hello 大家好呀&#xff0c;我是小楼&#xff0c;这是系列文《Go底层原理剖析》的第三篇&#xff0c;依旧分析 Http 模块。我们今天来看 Go内置的 RPC。说起 RPC 大家想到的一般是框架&#xff0c;Go 作为编程语言竟然还内置了 RPC&#xff0c;着实让我有些吃鲸。 从一个 Demo …

原型模式学习

本文讲解一下原型模式的概念并通过一个案例来进行实现。 4、原型模式 通过new产生一个对象需要非常繁琐的数据准备或访问权限&#xff0c;则可以使用原型模式原型模式就是Java中的克隆技术&#xff0c;以某个对象为原型&#xff0c;复制出新的对象&#xff0c;新的对象具有原…

VS2019加载解决方案时不能自动打开之前的文档(回忆消失)

✏️作者&#xff1a;枫霜剑客 &#x1f4cb;系列专栏&#xff1a;C实战宝典 &#x1f332;上一篇: 错误error c3861 :“_T“:找不到标识符 逐梦编程&#xff0c;让中华屹立世界之巅。 简单的事情重复做,重复的事情用心做,用心的事情坚持做&#xff1b; 文章目录前言一、问题描…

借助ChatGPT爆火,股价暴涨又暴跌后,C3.ai仍面临巨大风险

来源&#xff1a;猛兽财经 作者&#xff1a;猛兽财经 C3.ai的股价 作为一家人工智能技术提供商&#xff0c;C3.ai&#xff08;AI&#xff09;的股价曾在2021年初随着炒作情绪的增加&#xff0c;达到了历史最高点&#xff0c;但自那以后其股价就下跌了90%&#xff0c;而且炒作情…

数据正确性验证(造数据篇)

变更记录 记录每次修订的内容&#xff0c;方便追溯。 多行文本单选作者日期完成文档V1.02023-02-27V1.1V1.2 1. 数据质量检测标准 1.1 背景&#xff1a;整理数据质量测试的维度 摘取自国标文档 以上是除了常规的软件质量模型外&#xff08;软件测试质量六大特性&#xff0c…

Mysql Nested-Loop Join算法和MRR

MySQL8之前仅支持一种join 算法—— nested loop&#xff0c;在 MySQL8 中推出了一种新的算法 hash join&#xff0c;比 nested loop 更加高效。&#xff08;后面有时间介绍这种join算法&#xff09; 1、mysql驱动表与被驱动表及join优化 先了解在join连接时哪个表是驱动表&a…

ChatGPT今日正式开放API服务中小企业

开放隐私计算 开放隐私计算开放隐私计算OpenMPC是国内第一个且影响力最大的隐私计算开放社区。社区秉承开放共享的精神&#xff0c;专注于隐私计算行业的研究与布道。社区致力于隐私计算技术的传播&#xff0c;愿成为中国 “隐私计算最后一公里的服务区”。183篇原创内容公众号…

不要以没时间来说测试用例写不好

工作当中,总会有人为自己的测试用例写得不够好去找各种理由,时间不够是我印象当中涉及到最多的,也是最反感。想写好测试用例&#xff0c;前提是测试分析和需求拆解做的足够好&#xff0c;通过xmind或者UML图把需求和开发设计提供的产品信息提炼出来。 我个人的提炼标准一般是&…

CSS——学成在线案例

&#x1f353;个人主页&#xff1a;bit.. &#x1f352;系列专栏&#xff1a;Linux(Ubuntu)入门必看 C语言刷题 数据结构与算法 HTML和CSS3 目录 1.案例准备工作 2.CSS属性书写顺序&#xff08;重点&#xff09; 3.页面布局整体思路 4.头部的制作​编辑 5.banner制作…