阅读视图

发现新文章,点击刷新页面。

PAG动效框架源码笔记 (五)渲染流程

转载请注明出处:http://www.olinone.com/

前言

上一章介绍了TGFX渲染框架的大致结构,本章基于OpenGL介绍TGFX绘制Texture纹理详细的渲染流程

绘制Texture纹理,渲染引擎主要包括两个流程:GLSL着色器代码的装载及数据对象的绑定操作

着色器代码(GLSL)

渲染一个纹理,TGFX 需要构建顶点着色器(Vertex Shader)和片段着色器(Frament Shader)两个着色器,其代码分别如下

#version 100
precision mediump float;
uniform vec4 tgfx_RTAdjust; // 坐标系映射矩阵
uniform mat3 uCoordTransformMatrix_0_Stage0; 

attribute vec2 aPosition;
attribute vec2 localCoord;
attribute vec4 inColor;

varying vec2 vTransformedCoords_0_Stage0;
varying vec4 vColor_Stage0;

void main() {
    // Geometry Processor QuadPerEdgeAAGeometryProcessor
    // 矩阵变化,比如缩放、偏移,更适合GPU并行计算
    vTransformedCoords_0_Stage0 = (uCoordTransformMatrix_0_Stage0 * vec3(localCoord, 1)).xy;
    vColor_Stage0 = inColor;
    // 坐标系转化
    gl_Position = vec4(aPosition.xy * tgfx_RTAdjust.xz + tgfx_RTAdjust.yw, 0, 1); 
}

顶点着色器需要计算每个顶点在渲染坐标系中的坐标,同时将纹理数据输出给片段着色器

为了优化计算性能,TGFX没有在CPU阶段处理矩阵变化和坐标映射,而是交由GPU来处理( GPU更适合矩阵计算);同时,由GPU处理坐标系映射可以更灵活适配不同平台不同坐标系

#version 100
precision mediump float;
uniform mat3 uMat3ColorConversion_Stage1; // 颜色空间转化矩阵
uniform vec2 uAlphaStart_Stage1; // Alpha区域偏移量
uniform sampler2D uTextureSampler_0_Stage1;
uniform sampler2D uTextureSampler_1_Stage1;

varying highp vec2 vTransformedCoords_0_Stage0;
varying highp vec4 vColor_Stage0;

void main() {
    vec4 outputColor_Stage0;
    vec4 outputCoverage_Stage0;
    { // Stage 0 QuadPerEdgeAAGeometryProcessor
        outputCoverage_Stage0 = vec4(1.0);
        outputColor_Stage0 = vColor_Stage0;
    }
    vec4 output_Stage1;
    { // Stage 1 XfermodeFragmentProcessor - dst
        vec4 child;
        {
            // Child Index 0 (mangle: _c0): YUVTextureEffect
            // yuv取值,光栅化后每个点坐标都不一样
            vec3 yuv;
            yuv.x = texture2D(uTextureSampler_0_Stage1, vTransformedCoords_0_Stage0).rrra.r;
            yuv.yz = texture2D(uTextureSampler_1_Stage1, vTransformedCoords_0_Stage0).rgrg.ra;
            yuv.x -= (16.0 / 255.0);
            yuv.yz -= vec2(0.5, 0.5);
            // yuv数据转rgb
            vec3 rgb = clamp(uMat3ColorConversion_Stage1 * yuv, 0.0, 1.0); 
            // 通过RGB颜色区域偏移计算Alpha区域,比如左rgb右alpha,整体+0.5
            vec2 alphaVertexColor = vTransformedCoords_0_Stage0 + uAlphaStart_Stage1; 
            float yuv_a = texture2D(uTextureSampler_0_Stage1, alphaVertexColor).rrra.r;
            // 为避免因压缩误差、精度等原因造成不透明变成部分透明(比如255变成254),
            // 下面进行了减1.0/255.0的精度修正。
            yuv_a = (yuv_a - 16.0/255.0) / (219.0/255.0 - 1.0/255.0);
            yuv_a = clamp(yuv_a, 0.0, 1.0); 
            child = vec4(rgb * yuv_a, yuv_a) * vec4(1.0);
        }
        // Compose Xfer Mode: DstIn
        output_Stage1 = child * outputColor_Stage0.a; // blend混合模式
    }
    { // Xfer Processor EmptyXferProcessor
        gl_FragColor = output_Stage1 * outputCoverage_Stage0;
    }
}

片段着色器决定了光栅化后每个点像素的最终颜色,TGFX需要处理纹理RGBA计算、Mask蒙版遮罩以及多图层的blend混合计算等

渲染流程

1、任务创建

void Canvas::drawImage(std::shared_ptr<Image> image, const Paint* paint) {
 	// Mipmap纹理映射处理
  auto mipMapMode = image->hasMipmaps() ? tgfx::MipMapMode::Linear : tgfx::MipMapMode::None;
  tgfx::SamplingOptions sampling(tgfx::FilterMode::Linear, mipMapMode);
  drawImage(std::move(image), sampling, paint);
}

void Canvas::drawImage(std::shared_ptr<Image> image, SamplingOptions sampling, const Paint* paint) {
  ...
  // 记录Canvas当前状态
  auto oldMatrix = getMatrix();
  ...
  // 绘制image(包含解码后的texture纹理)
  drawImage(std::move(image), sampling, paint);
  // 还原Canvas上下文
  setMatrix(oldMatrix);
}

void Canvas::drawImage(std::shared_ptr<Image> image, SamplingOptions sampling, const Paint& paint) {
  ...
  // 纹理处理,序列帧左rgb右alpha数据
  auto processor = image->asFragmentProcessor(getContext(), surface->options()->flags(), sampling);
  ...
  // 创建画笔Paint
  if (!PaintToGLPaintWithImage(getContext(), surface->options()->flags(), paint, state->alpha, std::move(processor), image->isAlphaOnly(), &glPaint)) {
    return;
  }
  // 创建矩形填充绘制Operation
  auto op = FillRectOp::Make(glPaint.color, localBounds, state->matrix);
  // 绑定Paint到Op上,提交绘制Task
  draw(std::move(op), std::move(glPaint), true);
}

// 创建Paint
static bool PaintToGLPaint(Context* context, uint32_t surfaceFlags, const Paint& paint, float alpha, std::unique_ptr<FragmentProcessor> shaderProcessor, GpuPaint* glPaint) {
  ...
  // 绘制串行Pipeline
  // 纹理
  shaderFP = shader->asFragmentProcessor(args);
  if (shaderFP) {
    glPaint->colorFragmentProcessors.emplace_back(std::move(shaderFP));
  } 
  // 滤镜
  if (auto colorFilter = paint.getColorFilter()) {
    if (auto processor = colorFilter->asFragmentProcessor()) {
      glPaint->colorFragmentProcessors.emplace_back(std::move(processor));
    } 
  }
  // 蒙版遮罩
  if (auto maskFilter = paint.getMaskFilter()) {
    if (auto processor = maskFilter->asFragmentProcessor(args)) {
      glPaint->coverageFragmentProcessors.emplace_back(std::move(processor));
    }
  }
  return true;
}

// 绑定Paint到绘制Operation中,提交到绘制OperationQueue
void Canvas::draw(std::unique_ptr<DrawOp> op, GpuPaint paint, bool aa) {
  ...
  // Canvas裁切
  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);
  BlendModeCoeff first;
  BlendModeCoeff second;
  // blend混合模式
  if (BlendModeAsCoeff(state->blendMode, &first, &second)) {
    op->setBlendFactors(std::make_pair(first, second));
  } else {
    op->setXferProcessor(PorterDuffXferProcessor::Make(state->blendMode));
    op->setRequireDstTexture(!getContext()->caps()->frameBufferFetchSupport);
  }
  op->setAA(aaType);
  // 纹理图层
  op->setColors(std::move(paint.colorFragmentProcessors));
  // 蒙版图层
  op->setMasks(std::move(masks));
  surface->aboutToDraw(false);
  // 加入到绘制队列中
  drawContext->addOp(std::move(op));
}

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

2、Flush绘制

bool DrawingManager::flush(Semaphore* signalSemaphore) {
  ...
  // 遍历执行
  std::for_each(tasks.begin(), tasks.end(), [gpu](std::shared_ptr<RenderTask>& task) { task->execute(gpu); });
  return context->caps()->semaphoreSupport && gpu->insertSemaphore(signalSemaphore);
}

bool OpsTask::execute(Gpu* gpu) {
  // 先prepare
  std::for_each(ops.begin(), ops.end(), [gpu](auto& op) { op->prepare(gpu); });
  // 再execute
  opsRenderPass->begin();
  auto tempOps = std::move(ops);
  for (auto& op : tempOps) {
    op->execute(opsRenderPass);
  }
  opsRenderPass->end();
  // 提交
  gpu->submit(opsRenderPass);
  return true;
}

3、绘制预处理

// 顶点着色器数据构造
void FillRectOp::onPrepare(Gpu* gpu) {
  // 数据构造(CPU),包含画布、纹理以及RGB区域数据
  auto data = vertices();
  // 绑定数据到GPU
  vertexBuffer = GpuBuffer::Make(gpu->context(), BufferType::Vertex, data.data(), data.size() * sizeof(float));
  // 自定义绘制顺序index
  if (aa == AAType::Coverage) {
    indexBuffer = gpu->context()->resourceProvider()->aaQuadIndexBuffer();
  } else {
    indexBuffer = gpu->context()->resourceProvider()->nonAAQuadIndexBuffer();
  }
}

std::shared_ptr<GpuBuffer> GpuBuffer::Make(Context* context, BufferType bufferType, const void* buffer, size_t size) {
  ...
  auto glBuffer = std::static_pointer_cast<GLBuffer>(context->resourceCache()->findScratchResource(scratchKey));
	...
	// GPU数据绑定
  gl->bindBuffer(target, glBuffer->_bufferID);
  // GPU数据赋值
  gl->bufferData(target, static_cast<GLsizeiptr>(size), buffer, GL_STATIC_DRAW);
  return glBuffer;
}

4、绘制执行

void FillRectOp::onExecute(OpsRenderPass* opsRenderPass) {
  // 着色器代码定义
  auto info = createProgram(opsRenderPass, QuadPerEdgeAAGeometryProcessor::Make(opsRenderPass->renderTarget()->width(), opsRenderPass->renderTarget()->height(), aa, !colors.empty()));
  // 着色器代码装载及数据绑定
  opsRenderPass->bindPipelineAndScissorClip(info, scissorRect());
  // 绑定顶点及自定义绘制顺序数据
  opsRenderPass->bindBuffers(indexBuffer, vertexBuffer);
  if (needsIndexBuffer()) {
    // 自定义顺序绘制
    opsRenderPass->drawIndexed(PrimitiveType::Triangles, 0, static_cast<int>(rects.size()) * numIndicesPerQuad);
  } else {
    // 默认顺序绘制
    opsRenderPass->draw(PrimitiveType::TriangleStrip, 0, 4);
  }
}

// 着色器代码生成,包括顶点和片段着色器代码
ProgramInfo DrawOp::createProgram(OpsRenderPass* opsRenderPass,
                                  std::unique_ptr<GeometryProcessor> gp) {
  auto numColorProcessors = _colors.size();
  // 片段着色器函数代码Pipeline组装
  std::vector<std::unique_ptr<FragmentProcessor>> fragmentProcessors = {};
  fragmentProcessors.resize(numColorProcessors + _masks.size());
  // 纹理
  std::move(_colors.begin(), _colors.end(), fragmentProcessors.begin());
  // 蒙版
  std::move(_masks.begin(), _masks.end(),
            fragmentProcessors.begin() + static_cast<int>(numColorProcessors));
	...
  ProgramInfo info;
  // blend模式
  info.blendFactors = _blendFactors;
  info.pipeline = std::make_unique<Pipeline>(std::move(fragmentProcessors), numColorProcessors, std::move(_xferProcessor), dstTexture, dstTextureOffset, &swizzle);
  info.pipeline->setRequiresBarrier(dstTexture != nullptr && dstTexture == opsRenderPass->renderTargetTexture());
  // 顶点着色器函数代码
  info.geometryProcessor = std::move(gp);
  return info;
}

// 着色器代码装载,包括编译、链接及Uniform数据绑定
bool GLOpsRenderPass::onBindPipelineAndScissorClip(const ProgramInfo& info, const Rect& drawBounds) {
  GLProgramCreator creator(info.geometryProcessor.get(), info.pipeline.get());
  // Program函数创建,先缓存,没有再新建
  _program = static_cast<GLProgram*>(_context->programCache()->getProgram(&creator));
  auto glRT = static_cast<GLRenderTarget*>(_renderTarget.get());
  auto* program = static_cast<GLProgram*>(_program);
  // 绑定函数
  gl->useProgram(program->programID());
  gl->bindFramebuffer(GL_FRAMEBUFFER, glRT->getFrameBufferID());
  gl->viewport(0, 0, glRT->width(), glRT->height());
  // GL裁切
  UpdateScissor(_context, drawBounds);
  // GL混合模式
  UpdateBlend(_context, info.blendFactors);
	// 绑定数据,包括Uniform参数和纹理数据
  program->updateUniformsAndTextureBindings(glRT, *info.geometryProcessor, *info.pipeline);
  return true;
}

// 创建Program函数
std::unique_ptr<GLProgram> GLProgramBuilder::CreateProgram(Context* context, const GeometryProcessor* geometryProcessor, const Pipeline* pipeline) {
  GLProgramBuilder builder(context, geometryProcessor, pipeline);
  if (!builder.emitAndInstallProcessors()) {
    return nullptr;
  }
  return builder.finalize();
}

bool ProgramBuilder::emitAndInstallProcessors() {
  // 生成顶点着色器代码
  emitAndInstallGeoProc(&inputColor, &inputCoverage);
  // 生成片段着色器代码
  emitAndInstallFragProcessors(&inputColor, &inputCoverage);
  // 图层叠加混合代码
  emitAndInstallXferProc(inputColor, inputCoverage);
  emitFSOutputSwizzle();
  return checkSamplerCounts();
}

std::unique_ptr<GLProgram> GLProgramBuilder::finalize() {
 	...
 	// Vertex Shader代码
  auto vertex = vertexShaderBuilder()->shaderString();
  // Frament Shader代码
  auto fragment = fragmentShaderBuilder()->shaderString();
  // 创建Program,编译、链接
  auto programID = CreateGLProgram(context, vertex, fragment);
  // GPU顶点着色器参数绑定
  computeCountsAndStrides(programID);
  // 获取Program Uniform位置
  resolveProgramResourceLocations(programID);
  return createProgram(programID);
}

std::unique_ptr<GLProgram> GLProgramBuilder::createProgram(unsigned programID) {
  auto program = new GLProgram(context, uniformHandles, programID, _uniformHandler.uniforms, std::move(glGeometryProcessor), std::move(xferProcessor), std::move(fragmentProcessors), attributes, vertexStride);
  // GPU Uniform参数绑定
  program->setupSamplerUniforms(_uniformHandler.samplers);
  return std::unique_ptr<GLProgram>(program);
}

// 提交绘制
void GLOpsRenderPass::draw(const std::function<void()>& func) {
	// GPU顶点着色器参数赋值
  gl->bindBuffer(GL_ARRAY_BUFFER, std::static_pointer_cast<GLBuffer>(_vertexBuffer)->bufferID());
  auto* program = static_cast<GLProgram*>(_program);
  for (const auto& attribute : program->vertexAttributes()) {
    const AttribLayout& layout = GetAttribLayout(attribute.gpuType);
    gl->vertexAttribPointer(static_cast<unsigned>(attribute.location), layout.count, layout.type, layout.normalized, program->vertexStride(), reinterpret_cast<void*>(attribute.offset));
    gl->enableVertexAttribArray(static_cast<unsigned>(attribute.location));
  }
  // 绘制
  func();
	...
}

总结

为了支持多平台不同对象(纹理、图形等)绘制,TGFX抽象封装了一套完整的GLSL代码生成模版,各平台继承模版父类负责逻辑实现,后续可以针对iOS平台提供Metal绘制实现

class ProgramCreator {
 public:
  virtual ~ProgramCreator() = default;
  virtual void computeUniqueKey(Context* context, BytesKey* uniqueKey) const = 0;
  virtual std::unique_ptr<Program> createProgram(Context* context) const = 0;
};

class GLProgramCreator : public ProgramCreator {
 public:
  GLProgramCreator(const GeometryProcessor* geometryProcessor, const Pipeline* pipeline);
  void computeUniqueKey(Context* context, BytesKey* uniqueKey) const override;
  std::unique_ptr<Program> createProgram(Context* context) const override;
};

std::unique_ptr<Program> GLProgramCreator::createProgram(Context* context) const {
  return GLProgramBuilder::CreateProgram(context, geometryProcessor, pipeline);
}

至此, PAG动效框架源码就全部讲解完成

框架中还有大量本文未提到的内容,比如滤镜、文本、图形绘制等等,有兴趣的同学建议阅读源码

相信未来行业内会有大量类似的解决方案,比如基于MP4方案的多图层绘制框架等

PAG动效框架源码笔记 (四)渲染框架

转载请注明出处:http://www.olinone.com/

前言

PAG采用自研TGFX特效渲染引擎,抽象分离了接口及平台实现类,可以扩展支持多种图形渲染库,比如OpenGL、Metal等

TGFX引擎是如何实现纹理绘制?本文基于OpenGL图形库分析讲解TGFX渲染框架分层及详细架构设计。开始之前,先提一个问题:

绘制一个Texture纹理对象,一般需要经历哪些过程?

渲染流程

通常情况下,绘制一个Texture纹理对象到目标Layer上,可以抽象为以下几个阶段:

1. 获取上下文:通过EGL获取Context绘制上下文,提供与渲染设备交互的能力,比如缓冲区交换、Canvas及Paint交互等

2. 定义着色器:基于OpenGL的着色器语言(GLSL)编写着色器代码,编写自定义顶点着色器和片段着色器代码,编译、链接加载和使用它们

3. 绑定数据源:基于渲染坐标系几何计算绑定顶点数据,加载并绑定纹理对象给GPU,设置渲染目标、混合模式等

4. 渲染执行:提交渲染命令给渲染线程,转化为底层图形API调用、并执行实际的渲染操作

img

关于OpenGL完整的渲染流程,网上有比较多的资料介绍,在此不再赘述,有兴趣的同学可以参考 OpenGL ES Pipeline

框架层级

TGFX框架大致可分为三大块:

1. Drawable上下文:基于EGL创建OpenGL上下文,提供与渲染设备交互的能力

2. Canvas接口:定义画布Canvas及画笔Paint,对外提供渲染接口、记录渲染状态以及创建绘制任务等

3. DrawOp执行:定义并装载着色器函数,绑定数据源,执行实际渲染操作

为了支持多平台,TGFX定义了一套完整的框架基类,实现框架与平台的物理隔离,比如矩阵对象Matrix、坐标Rect等,应用上层负责平台对象与TFGX对象的映射转化

- (void)setMatrix:(CGAffineTransform)value {
  pag::Matrix matrix = {};
  matrix.setAffine(value.a, value.b, value.c, value.d, value.tx, value.ty);
  _pagLayer->setMatrix(matrix);
}

Drawable上下文

PAG通过抽象Drawable对象,封装了绘制所需的上下文,其主要包括以下几个对象

1. Device(设备):作为硬件设备层,负责与渲染设备交互,比如创建维护EAGLContext等

2. Window(窗口):拥有一个Surface,负责图形库与绘制目标的绑定,比如将OpenGL的RenderBuffer绑定到CAEAGLLayer上;

3. Surface(表面):创建canvas画布提供可绘制区域,对外提供flush绘制接口;当窗口尺寸发生变化时,surface会创建新的canvas

4. Canvas(画布):作为实际可绘制区域,提供绘制api,进行实际的绘图操作,比如绘制一个image或者shape等

详细代码如下:

1、Device创建Context

std::shared_ptr<GLDevice> GLDevice::Make(void* sharedContext) {
  if (eaglShareContext != nil) {
    eaglContext = [[EAGLContext alloc] initWithAPI:[eaglShareContext API]
                                        sharegroup:[eaglShareContext sharegroup]];
  } else {
    // 创建Context
    eaglContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
    if (eaglContext == nil) {
      eaglContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
    }
  }
  auto device = EAGLDevice::Wrap(eaglContext, false);
  return device;
}

std::shared_ptr<EAGLDevice> EAGLDevice::Wrap(EAGLContext* eaglContext, bool isAdopted) {
  auto oldEAGLContext = [[EAGLContext currentContext] retain];
  if (oldEAGLContext != eaglContext) {
    auto result = [EAGLContext setCurrentContext:eaglContext];
    if (!result) {
      return nullptr;
    }
  }
  auto device = std::shared_ptr<EAGLDevice>(new EAGLDevice(eaglContext),
                                            EAGLDevice::NotifyReferenceReachedZero);
  if (oldEAGLContext != eaglContext) {
    [EAGLContext setCurrentContext:oldEAGLContext];
  }
  return device;
}

// 获取Context
bool EAGLDevice::makeCurrent(bool force) {
  oldContext = [[EAGLContext currentContext] retain];
  if (oldContext == _eaglContext) {
    return true;
  }
  if (![EAGLContext setCurrentContext:_eaglContext]) {
    oldContext = nil;
    return false;
  }
  return true;
}

2、Window创建Surface,绑定RenderBuffer

std::shared_ptr<Surface> EAGLWindow::onCreateSurface(Context* context) {
  auto gl = GLFunctions::Get(context);
  ...
  gl->genFramebuffers(1, &frameBufferID);
  gl->bindFramebuffer(GL_FRAMEBUFFER, frameBufferID);
  gl->genRenderbuffers(1, &colorBuffer);
  gl->bindRenderbuffer(GL_RENDERBUFFER, colorBuffer);
  gl->framebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, colorBuffer);
  auto eaglContext = static_cast<EAGLDevice*>(context->device())->eaglContext();
  // 绑定到CAEAGLLayer上
  [eaglContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:layer];
  ...
  GLFrameBufferInfo glInfo = {};
  glInfo.id = frameBufferID;
  glInfo.format = GL_RGBA8;
  BackendRenderTarget renderTarget = {glInfo, static_cast<int>(width), static_cast<int>(height)};
  // 创建Surface
  return Surface::MakeFrom(context, renderTarget, ImageOrigin::BottomLeft);
}

// 通过renderTarget持有context、frameBufferID及Size
std::shared_ptr<Surface> Surface::MakeFrom(Context* context,
                                           const BackendRenderTarget& renderTarget,
                                           ImageOrigin origin, const SurfaceOptions* options) {
  auto rt = RenderTarget::MakeFrom(context, renderTarget, origin);
  return MakeFrom(std::move(rt), options);
}

3、Surface创建Canvas及flush绘制

Canvas* Surface::getCanvas() {
  // 尺寸变化时会清空并重新创建canvas
  if (canvas == nullptr) {
    canvas = new Canvas(this);
  }
  return canvas;
}

bool Surface::flush(BackendSemaphore* signalSemaphore) {
  auto semaphore = Semaphore::Wrap(signalSemaphore);
  // drawingManager创建tasks,装载绘制pipiline
  renderTarget->getContext()->drawingManager()->newTextureResolveRenderTask(this);
  auto result = renderTarget->getContext()->drawingManager()->flush(semaphore.get());
  return result;
}

4、渲染流程

bool PAGSurface::draw(RenderCache* cache, std::shared_ptr<Graphic> graphic,
                      BackendSemaphore* signalSemaphore, bool autoClear) {
  // 获取context上下文                    
  auto context = lockContext(true);
  // 获取surface
  auto surface = drawable->getSurface(context);
  // 通过canvas画布
  auto canvas = surface->getCanvas();
  // 执行实际绘制
  onDraw(graphic, surface, cache);
  // 调用flush
  surface->flush();
  // glfinish
  context->submit();
  // 绑定GL_RENDERBUFFER
  drawable->present(context);
  // 释放context上下文
  unlockContext();
  return true;
}

Canvas接口

Canvas API主要包括画布操作及对象绘制两大类:

画布操作包括Matrix矩阵变化、Blend融合模式、画布裁切等设置,通过对canvasState画布状态的操作实现绘制上下文的切换

对象绘制包括Path、Shape、Image以及Glyph等对象的绘制,结合Paint画笔实现纹理、文本、图形、蒙版等多种形式的绘制及渲染

class Canvas {
 	// 画布操作
  void setMatrix(const Matrix& matrix);
  void setAlpha(float newAlpha);
  void setBlendMode(BlendMode blendMode);

  // 绘制API
  void drawRect(const Rect& rect, const Paint& paint);
  void drawPath(const Path& path, const Paint& paint);
  void drawShape(std::shared_ptr<Shape> shape, const Paint& paint);
  void drawImage(std::shared_ptr<Image> image, const Matrix& matrix, const Paint* paint = nullptr);
  void drawGlyphs(const GlyphID glyphIDs[], const Point positions[], size_t glyphCount,
                  const Font& font, const Paint& paint);
};

// CanvasState记录当前画布的状态,包括Alph、blend模式、变化矩阵等
struct CanvasState {
  float alpha = 1.0f;
  BlendMode blendMode = BlendMode::SrcOver;
  Matrix matrix = Matrix::I();
  Path clip = {};
  uint32_t clipID = kDefaultClipID;
};

// 通过save及restore实现绘制状态的切换
void Canvas::save() {
  auto canvasState = std::make_shared<CanvasState>();
  *canvasState = *state;
  savedStateList.push_back(canvasState);
}

void Canvas::restore() {
  if (savedStateList.empty()) {
    return;
  }
  state = savedStateList.back();
  savedStateList.pop_back();
}

DrawOp执行

DrawOp负责实际的绘制逻辑,比如OpenGL着色器函数的创建装配、顶点及纹理数据的创建及绑定等

TGFX抽象了FillRectOp矩形绘制Op,可以覆盖绝大多数场景的绘制需求

当然,其还支持其它类型的绘制Op,比如ClearOp清屏、TriangulatingPathOp三角图形绘制Op等

class DrawOp : public Op {
  // DrawOp通过Pipiline实现多个_colors纹理对象及_masks蒙版的绘制
  std::vector<std::unique_ptr<FragmentProcessor>> _colors;
  std::vector<std::unique_ptr<FragmentProcessor>> _masks;
};

// 矩形实际绘制执行者
class FillRectOp : public DrawOp {
  FillRectOp(std::optional<Color> color, const Rect& rect, const Matrix& viewMatrix,
             const Matrix& localMatrix);
  void onPrepare(Gpu* gpu) override;
  void onExecute(OpsRenderPass* opsRenderPass) override;
};

总结

本文结合OpenGL讲解了TGFX渲染引擎的大概框架结构,让各位有了一个初步认知

接下来将结合image纹理绘制介绍TGFX渲染引擎详细的绘制渲染流程,欢迎大家关注点赞!

PAG动效框架源码笔记 (三)播放流程

转载请注明出处:http://www.olinone.com/

PAG框架支持单PAGView同时渲染多个PAGFile,相较于渲染单一文件,框架首先需要解决多文件渲染同步问题

1、多文件渲染帧率同步

多文件有着不同的帧率(FPS) ,为了实现同一容器渲染不同帧率文件,传统的基于定时器模式(定时回调,间隔与帧率同步)的回调已无法满足该场景

2、多文件渲染进度同步

多文件渲染需要避免多图层渲染进度不一致问题;为了保证播放流畅度,通常会在子线程解码视频帧,当渲染多视频图层时,如何保证多解码线程下的帧同步?

3、多文件播放区间控制

PAG支持File自定义显示区间,每个File文件时长也不一致,如何控制不同文件的播放区间?

播放进度

 

 

 

 

 

PAG没有采用类似于CMTime帧数的方式记录播放进度,而是通过时长百分比记录各层级播放进度,从而实现不同帧率(FPS)文件的进度同步

bool PAGComposition::gotoTime(int64_t layerTime) {
  auto changed = PAGLayer::gotoTime(layerTime);
  auto compositionOffset =
      // 相对起始时间
      static_cast<PreComposeLayer*>(layer)->compositionStartTime - layer->startTime + startFrame;
  auto compositionOffsetTime =
      static_cast<Frame>(floor(compositionOffset * 1000000.0 / frameRateInternal()));
  for (auto& layer : layers) {
    // 各图层记录各自的播放进度
    if (layer->gotoTime(layerTime - compositionOffsetTime)) {
      changed = true;
    }
  }
  return changed;
}
​
bool PAGLayer::gotoTime(int64_t layerTime) {
  ...
  // 使用各自帧率转化成对应帧
  auto layerFrame = TimeToFrame(layerTime, frameRateInternal());
  auto oldContentFrame = contentFrame;
  contentFrame = layerFrame - startFrame;
  ...
  return changed;
}

采用时长百分比记录进度会导致flush绘制重复帧,为了解决性能问题,PAG引入了LayerCache角色,当重复绘制同一帧时可以直接使用缓存数据

Content* PAGLayer::getContent() {
  return layerCache->getContent(contentFrame);
}

PAG信号源PAGValueAnimator通过绝对时间差值计算播放进度,多个PAGView共用一个全局信号触发器

// 获取绝对时间戳
static int64_t GetCurrentTimeUS() {
  static auto START_TIME = std::chrono::high_resolution_clock::now();
  auto now = std::chrono::high_resolution_clock::now();
  auto ns = std::chrono::duration_cast<std::chrono::nanoseconds>(now - START_TIME);
  return static_cast<int64_t>(ns.count() * 1e-3);
}
​
- (void)start {
  ...
  // PAG支持重复播放
  if (repeatedTimes >= (repeatCount + 1)) {
    repeatedTimes = 0;
  }
  self.animatorId = [PAGValueAnimator AddAnimator:self];
  // startTime不是最初开始时间,每次暂停恢复播放后会重新记录
  startTime = GetCurrentTimeUS() - playTime % duration - repeatedTimes * duration;
  animatedFraction = static_cast<double>(playTime) / duration;
  ...
}
​
- (void)onAnimationFrame:(int64_t)timestamp {
  auto count = (timestamp - startTime) / duration;
  if (repeatCount >= 0 && count > repeatCount) {
    // 播放结束
    playTime = duration;
    animatedFraction = 1.0;
    ...
  } else {
    // 当次播放时间戳
    playTime = (timestamp - startTime) % duration;
    animatedFraction = static_cast<double>(playTime) / duration;
    ...
}

播放流程

1、信号触发

- (void)onAnimationUpdate {
  // 触发更新
  [self updateView];
}
​
- (BOOL)flush {
  ...
  // 更新播放进度
  [pagPlayer setProgress:[valueAnimator getAnimatedFraction]];
  // 触发刷新
  result = [pagPlayer flush];
  ...
}

2、更新播放进度

void PAGPlayer::setProgress(double percent) {
  // 获取渲染图层,递归更新各图层播放进度
  auto pagComposition = stage->getRootComposition();
  pagComposition->setProgressInternal(realProgress);
}
​
void PAGLayer::setProgressInternal(double percent) {
  // 各图层起始播放时间不同,转化为全局整体播放时间戳
  gotoTimeAndNotifyChanged(startTimeInternal() + ProgressToTime(percent, durationInternal()));
}

3、触发渲染

bool PAGPlayer::flushInternal(BackendSemaphore* signalSemaphore) {
  // 图层预处理,生成播放时间戳对应的图形模型,模型转化参考上一章层级视图讲解
  prepareInternal();
  // 绘制图层对象
  if (!pagSurface->draw(renderCache, lastGraphic, signalSemaphore, _autoClear)) {
    return false;
  }
  ...
}
​
void PAGSurface::onDraw(std::shared_ptr<Graphic> graphic, std::shared_ptr<tgfx::Surface> target, RenderCache* cache) {
  auto canvas = target->getCanvas();
  if (graphic) {
    // 预处理,比如视频帧解码
    graphic->prepare(cache);
    // 渲染到画布Canvas
    graphic->draw(canvas, cache);
  }
}

4、图形解码(以视频帧为例)

// 预处理解码
void RenderCache::prepareSequenceImage(std::shared_ptr<SequenceInfo> sequence, Frame targetFrame) {
  // 解码队列,每个资源对应一个解码队列
  auto queue = getSequenceImageQueue(sequence, targetFrame);
  if (queue != nullptr) {
    queue->prepare(targetFrame);
  }
}
​
void SequenceImageQueue::prepare(Frame targetFrame) {
  // 获取当前帧对应的image数据,开始解码(比如AVC解码)
  auto image = sequence->makeFrameImage(reader, targetFrame);
  preparedImage = image->makeDecoded();
  preparedFrame = targetFrame;
}
​
// 创建异步解码任务
AsyncSource::AsyncSource(UniqueKey uniqueKey, std::shared_ptr<ImageGenerator> imageGenerator, bool mipMapped) {
  ...
  imageTask = ImageGeneratorTask::MakeFrom(generator, tryHardware);
}
​
ImageGeneratorTask::ImageGeneratorTask(std::shared_ptr<ImageGenerator> generator, bool tryHardware) : imageGenerator(std::move(generator)) {
  // 解码函数
  task = Task::Run([=] { imageBuffer = imageGenerator->makeBuffer(tryHardware); });
}
​
std::shared_ptr<tgfx::ImageBuffer> VideoReader::onMakeBuffer(Frame targetFrame) {
  auto targetTime = FrameToTime(targetFrame, frameRate);
  ... 
  // 解码
  auto sampleTime = demuxer->getSampleTimeAt(targetTime);
  auto success = decodeFrame(sampleTime);
  lastBuffer = videoDecoder->onRenderFrame();
  return lastBuffer;
}

5、渲染图形

void draw(tgfx::Canvas* canvas, RenderCache* cache) const override {
    ...
    // 获取解码图形
    auto image = proxy->getImage(cache);
    canvas->drawImage(std::move(image));
}
​
std::shared_ptr<tgfx::Image> SequenceImageQueue::getImage(Frame targetFrame) {
  // 目标帧已解码直接返回
  if (targetFrame == preparedFrame) {
    currentImage = preparedImage;
    return currentImage;
  }
  // 同步等待解码后的数据
  auto image = sequence->makeFrameImage(reader, targetFrame);
  currentImage = image->makeDecoded();
  return currentImage;
}
​
std::shared_ptr<ImageBuffer> ImageGeneratorTask::getBuffer() const {
  // 等待直到解码完成
  task->wait();
  return imageBuffer;
}
​
void Task::wait() {
  std::unique_lock<std::mutex> autoLock(locker);
  if (!_executing) {
    return;
  }
  // 等待解码锁释放
  condition.wait(autoLock);
}

总结

为了优化播放体验,PAG使用了多种性能优化策略,包括提前预解码、渲染帧复用、GPU优化等等

void RenderCache::prepareLayers() {
// 提前 500ms 开始解码
int64_t timeDistance = DECODING_VISIBLE_DISTANCE;
auto layerDistances = stage->findNearlyVisibleLayersIn(timeDistance);
for (auto& item : layerDistances) {
  for (auto pagLayer : item.second) {
    if (pagLayer->layerType() == LayerType::PreCompose) {
      preparePreComposeLayer(static_cast<PreComposeLayer*>(pagLayer->layer));
    } else if (pagLayer->layerType() == LayerType::Image) {
      prepareImageLayer(static_cast<PAGImageLayer*>(pagLayer));
    }
  }
}
}

PAG应用框架层主要负责上层业务逻辑处理,包括文件视频解码、播放流程控制以及生成渲染引擎所需要的数据源等,接下来将结合OpenGL讲解TGFX渲染引擎部分

PAG动效框架源码笔记 (二)层级视图

转载请注明出处:http://www.olinone.com/

如上章所言,特效播放主要包括应用逻辑处理和图形渲染两个阶段,其中,逻辑处理又可以看做模型对象的定义与流转

模型分层

PAG框架模型大致可以分为三部分:

Alt text

1、组件(PAGCompositon)

PAG框架支持多文件多图层渲染,PAGCompositon组件可以同时容纳多个PAGFile文件,每个PAGFile文件又可以包含多个Layer组件

PAGFile解析自File源文件,其中Layer组件可以支持元素替换,比如可以替换PAGImageLayer中PAGImage资源对象,从而实现自定义融合元素

2、图层(PAGStage)

PAGStage承载了完整的组件图层,记录了每个组件及其资源对象的映射关系,比如PAGImage及其关联的PAGImageLayer对象,实现通过资源对象查找对应Layer对象的能力

此外,PAGStage通过SequenceCache还缓存了资源对象与Graphic视图对象的映射关系,类似于渲染缓存的职责

3、图形(LayerGraphic)

组件模块加上时间戳,就生成对应时刻的图形对象Graphic,比如纹理图形Picture,或者文本图形Text

LayerGraphic作为图形模型的容器,继承自ComposeGraphic对象,包含当前播放时刻所有的图像对象,每一个图形对象可以通过装饰器添加裁切或者蒙版等多种处理效果

源码浅析

1、File文件解析

// 文件二进制解析
std::shared_ptr<File> File::Load(const void* bytes, size_t length, const std::string& filePath, const std::string&) {
file = Codec::Decode(bytes, static_cast<uint32_t>(length), filePath);
...
return file;
}
​
// File对象初始化
File::File(std::vector<Composition*> compositionList, std::vector<pag::ImageBytes*> imageList) : images(std::move(imageList)), compositions(std::move(compositionList)) {
// 每一个File文件都有对应的mainComposition对象,组件元信息其实都存在compositon里面
mainComposition = compositions.back();
rootLayer = PreComposeLayer::Wrap(mainComposition).release();
...
}

2、PAGFile构造

// 通过File构造PAGFile
std::shared_ptr<PAGFile> PAGFile::MakeFrom(std::shared_ptr<File> file) {
  // 解析File构造组件模型
  auto pagLayer = BuildPAGLayer(file, file->getRootLayer());
  pagLayer->gotoTime(0);
  auto pagFile = std::static_pointer_cast<PAGFile>(pagLayer);
  ...
  return pagFile;
}

// 每一个组件对象其实都持有原始File对象
std::shared_ptr<PAGLayer> PAGFile::BuildPAGLayer(std::shared_ptr<File> file, Layer* layer) {
  PAGLayer* pagLayer;
  switch (layer->type()) {
    case LayerType::Text: {
      pagLayer = new PAGTextLayer(file, static_cast<TextLayer*>(layer));
    } break;
    case LayerType::Image: {
      pagLayer = new PAGImageLayer(file, static_cast<ImageLayer*>(layer));
    } break;
    case LayerType::PreCompose: {
      ...
      if (composition->type() == CompositionType::Vector) {
        auto& layers = static_cast<VectorComposition*>(composition)->layers;
        // 遍历组件列表
        for (int i = static_cast<int>(layers.size()) - 1; i >= 0; i--) {
          auto childLayer = layers[i];
          auto childPAGLayer = BuildPAGLayer(file, childLayer);
					...
        }
      }
    } break;
  }
}

3、PAGStage图层填充

// PAGComposition可以容纳多个PAGFile
- (PAGComposition *)makeComposition {
    PAGComposition* compostion = [PAGComposition Make:self.view.bounds.size];
    PAGFile* file = [PAGFile Load:[[NSBundle mainBundle] pathForResource:@"data-TimeStretch" ofType:@"pag"]];
    // 可以替换PAGImage资源对象,底层其实是操作PAGImageLayer
    [file replaceImage:0 data:[PAGImage FromPath:[[NSBundle mainBundle] pathForResource:@"test" ofType:@"png"]]];
    [compostion addLayer:file];
    
    file = [PAGFile Load:[[NSBundle mainBundle] pathForResource:@"data_video" ofType:@"pag"]];
    [compostion addLayer:file atIndex:0];
    return compostion;
}

void PAGPlayer::setComposition(std::shared_ptr<PAGComposition> newComposition) {
  ...
  pagComposition = newComposition;
  if (pagComposition) {
    // 填充容器
    stage->doAddLayer(pagComposition, 0);
  }
}

// 建立索引缓存
void PAGStage::addReference(PAGLayer* pagLayer) {
  addToReferenceMap(pagLayer->uniqueID(), pagLayer);
  addToReferenceMap(pagLayer->layer->uniqueID, pagLayer);
  if (pagLayer->layerType() == LayerType::PreCompose) {
    auto composition = static_cast<PreComposeLayer*>(pagLayer->layer)->composition;
    addToReferenceMap(composition->uniqueID, pagLayer);
  } else if (pagLayer->layerType() == LayerType::Image) {
    auto imageBytes = static_cast<ImageLayer*>(pagLayer->layer)->imageBytes;
    addToReferenceMap(imageBytes->uniqueID, pagLayer);
    auto pagImage = static_cast<PAGImageLayer*>(pagLayer)->getPAGImage();
    if (pagImage != nullptr) {
      addReference(pagImage.get(), pagLayer);
    }
  }
	...
}

// 可以通过资源ID查找渲染缓存
std::shared_ptr<Graphic> PAGStage::getSequenceGraphic(Composition* composition,
                                                      Frame compositionFrame) {
	...
  SequenceCache cache = {};
  cache.graphic = RenderSequenceComposition(composition, compositionFrame);
  cache.compositionFrame = compositionFrame;
  sequenceCache[composition->uniqueID] = cache;
  return cache.graphic;
}

4、Graphic图形生成

// 播放进度
void PAGPlayer::setProgress(double percent) {
  auto pagComposition = stage->getRootComposition();
  ...
  pagComposition->setProgressInternal(realProgress);
}

// 生成图形
void PAGPlayer::prepareInternal() {
  renderCache->beginFrame();
  auto result = updateStageSize();
  if (result && contentVersion != stage->getContentVersion()) {
    contentVersion = stage->getContentVersion();
    Recorder recorder = {};
    // 通过recorder记录每个可绘制组件Layer
    stage->draw(&recorder);
    // 导出所有图形对象
    lastGraphic = recorder.makeGraphic();
  }
}

// recorder类似于二叉树,记录了每个Layer组件当前时刻对应的Graphic
void PAGComposition::draw(Recorder* recorder) {
  ...
  auto composition = preComposeLayer->composition;
  if (composition->type() == CompositionType::Bitmap ||
      composition->type() == CompositionType::Video) {
    auto layerFrame = layer->startTime + contentFrame;
    auto compositionFrame = preComposeLayer->getCompositionFrame(layerFrame);
    auto graphic = stage->getSequenceGraphic(composition, compositionFrame);
    recorder->drawGraphic(graphic);
  }
	...
  if (hasClip()) {
    // 裁切装饰器
    recorder->saveClip(0, 0, static_cast<float>(_width), static_cast<float>(_height));
  }
  // 堆栈模式处理每个视图及其子视图,保证每个视图及其子视图渲染环境一致性,比如matrix变化等
  for (int i = 0; i < count; i++) {
    DrawChildLayer(recorder, childLayer.get());
  }
  if (hasClip()) {
    recorder->restore();
  }
}

总结

为了支持多文件多图层渲染,PAG框架设计了一套完整的框架模型,其复杂的对象继承关系,加深了代码阅读理解难度,在理解其设计思路后,才能知其然知其所以然


写在文后:

本文并未提及File文件解析过程,主要原因在于我们项目中特效使用MP4文件实现,因此暂未细研其技术细节,后续有机会再单开细聊

PAG官方暂未开源文件生成源码,如果只是针对传统的礼物特效场景,可以尝试使用MP4作为替换容器

^-^

PAG动效框架源码笔记 (一)概览

转载请注明出处:http://www.olinone.com/

前言

PAG特效框架是腾讯开源的一套完整的动效工作流解决方案,相较于传统特效渲染框架,其支持更丰富的组合样式,网上已有详细介绍,在此不再赘述

PAG框架是如何实现特效渲染?本文结合预合成视频序列帧特效,通过分析其源码来展示其完整的渲染流程。开始之前,先问大家一个问题:

渲染一个视频类型的PAG特效文件,一般需要经历哪些流程?

渲染流程

通常情况下,加载一个PAG文件,到最终展示到屏幕上,一般会经过以下几个阶段:

1. 文件解码:加载PAG源文件,解析数据流,转化为可识别的数据模型

2. 获取帧数据:获取播放时间戳对应的视频帧数据,比如H264编码数据

3. 视频解码:解码视频帧数据,获取解码后对应的纹理数据,并交给GPU以供给渲染

4. 几何阶段(GPU):渲染顶点矩阵计算,齐次空间坐标转换等

5. 光栅化渲染(GPU):片元着色,深度测试之后,与帧缓冲混合后,最终渲染到屏幕上

前三个阶段可以看做应用程序阶段(CPU),后两个阶段为渲染阶段(GPU)

渲染渲染阶段,以OpenGL为例,通过API 创建用于在 GPU 上运行的 shader,然后将通过 CPU 获取到的图形点信息传入给 GPU 中的 Shader ,在 Vertex Shader 中通过矩阵变换,将顶点坐标从模型坐标系转换到世界坐标系,再到观察坐标系,最后投影到屏幕坐标系中,计算出显示屏幕上各顶点的坐标

然后,通过光栅化得到所有像素点信息,并在 Fragment Shader 中计算出所有像素点颜色。最后,将得到的像素信息进行depth/stencil test、blend,得到最终的图像并显示到屏幕上

在了解完渲染流程后,大家可以再思考一个问题:

设计一个类似于PAG动效框架,如何分层?

框架层级

通常情况下,一个动效框架大致可以分为四层:

1. 应用框架层:负责应用程序阶段的逻辑处理,包括文件、视频解码,播放处理等

2. 渲染引擎层:对外提供图形渲染服务,负责图形对象的绘制渲染工作

3. 图形接口层:位于应用程序和驱动程序之间,提供GPU驱动的标准接口,常见的如OpenGL

4. 硬件接口层:桥接图形接口与Native硬件平台视窗系统,比如EGL

PAG框架

浏览PAG iOS项目目录,可以看到其大致包括两部分:

一、src目录

PAG应用程序框架目录,处于应用程序阶段,包括PAG文件的解码、播放处理等

各目录职责如下图所示:

二、tgfx目录

特效渲染引擎包括渲染流程处理、GPU对象管理等,对外提供图形渲染服务

据网友描述,该渲染引擎名有可能参考自 bgfx 框架,有兴趣的同学也可以学习参考

该部分代码位于tgfx目录下,主要包括以下几个模块::

总结

PAG特效框架的应用程序框架和渲染引擎分别承担着不同的职责,通过合理的设计和组织解决了实现复杂动效的难题

本章以整体视角介绍了一个渲染框架应有的流程以及PAG工程目录概况,让各位读者特别新人有一个整体认知,接下来结合项目源码,分析PAG框架代码设计思路及详细的渲染流程


写在文后:

许久未更新博客,新建微信公众号 olinoneDev,欢迎大家关注交流~

第一次尝试 ChatGPT 辅助写作,以下是借助ChatGPT生成的PAG项目目录介绍,因版权问题放到最后

SRC

1.codec 此文件夹存放了PAG特效文件解码相关的代码,包括PAG文件格式定义,解码工具类,解码器类等。其中,PAG文件格式定义包括PAG动画文件头信息、帧动画信息、图片信息、特效元素信息等,这些信息都是通过解码器读取后转化为可识别的数据结构。

2.player 此文件夹存放了PAG动效播放相关的代码,包括PAG动效视图、播放管理器等。PAG动效视图负责接收PAG特效文件数据并进行视频解码、渲染等操作,播放管理器则负责对PAG动效进行播放控制,如开始、暂停、停止等操作。

3.renderer 此文件夹存放了PAG动效的渲染处理相关的代码,包括PAG渲染视图、图像渲染工具类、图形对象管理器等。PAG渲染视图负责处理渲染流程,如初始化渲染环境、读取PAG数据、创建纹理、绘制等操作,图像渲染工具类负责提供图像的渲染操作,如镜像、旋转等操作,图形对象管理器则负责管理图形对象,包括坐标对象、颜色对象、纹理对象等。

TGFX

1.gpu 此文件夹存放了与GPU相关的代码,主要负责提供GPU对象和操作函数,包括纹理、着色器、缓冲区、渲染管线等。通过使用OpenGL ES技术,可以通过这些GPU对象和操作函数来实现图形渲染。

2.render 此文件夹存放了渲染引擎相关的代码,包括渲染器、顶点格式、顶点缓冲、纹理贴图等。渲染器负责维护所有渲染状态,包括渲染参数、纹理贴图参数、混合模式等,并提供绘制顶点缓冲数据的方法。

3.util 此文件夹存放了辅助工具类的代码,包括矩阵计算、颜色处理、线性插值、画线算法等。这些辅助工具类提供了一些通用的图像处理函数,可用于协助完成图形渲染。

iOS弹幕解决方案——HJDanmaku 2.0发布

转载请注明出处:http://www.olinone.com/

Hi,好久不见,HJDanmaku 1.0版本发布已经过去两年之久,直播行业的快速崛起催生了直播弹幕的迫切需求,高并发、大流量、实时性的特性和以往视频弹幕的场景都大有不同,为了满足新的直播业务场景,HJDanmaku2.0正式发布!

流畅度

相较于1.0版本, HJDanmaku2.0采用全新的异步渲染引擎,98%的计算工作转移到子线程执行,避免了主线程的卡顿延时。同时,参考离屏渲染技术,将组装弹幕和渲染弹幕分布在两个独立线程异步执行,确保了弹幕渲染的流畅性

NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
    NSArray <HJDanmakuAgent *> *danmakuAgents = [self.danmakuSource fetchDanmakuAgentsForTime:(HJDanmakuTime){HJMaxTime(time), time.interval}];
    dispatch_async(_renderQueue, ^{
        if (danmakuAgents.count > 0) {
            [self.danmakuQueuePool insertObjects:danmakuAgents atIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, danmakuAgents.count)]];
        }
    });
}];
[self.sourceQueue cancelAllOperations];
[self.sourceQueue addOperation:operation];

将组装弹幕的过程拆分为独立的子线程任务,统一由NSOperationQueue单执行队列管理,有效的降低CPU的使用率,提升系统运行稳定性。此外,在2.0版本中,使用CADisplayLink替换定时器NSTimer,与屏幕刷新频率保持一致,可以避免NSTimer由于线程阻塞导致的刷新延时

高并发

直播与传统视频最大区别在于其实时性,短时间大量的弹幕发送对底层渲染引擎是个不小的挑战。为了解决这个问题,HJDanmaku2.0引入数据源Source的思想,将弹幕接收与组装的过程分开,可以针对直播、视频场景实现差异化的处理方案。视频场景对时间精确度要求较高,涉及到弹幕的时间排序,同时,播放进度回放也需要数据源保存所有的弹幕数据。直播场景则比较单一,播放完可以立刻释放,避免内存的过度消耗

u_int interval = 100;
NSMutableArray *danmakuAgents = [NSMutableArray arrayWithCapacity:interval];
NSUInteger lastIndex = danmakus.count - 1;
[danmakus enumerateObjectsUsingBlock:^(HJDanmakuModel *danmaku, NSUInteger idx, BOOL *stop) {
    HJDanmakuAgent *agent = [[HJDanmakuAgent alloc] initWithDanmakuModel:danmaku];
    [danmakuAgents addObject:agent];
    if (idx == lastIndex || danmakuAgents.count % interval == 0) {
        OSSpinLockLock(&_spinLock);
        [self.danmakuAgents addObjectsFromArray:danmakuAgents];
        OSSpinLockUnlock(&_spinLock);
        [danmakuAgents removeAllObjects];
     }
}];

通过拆分入库数据分布添加可以避免线程锁的长时间占有,提升系统的稳定性和流畅度

精确度

与1.0版本不同,新版本通过toleranceCount维度判断弹幕是否过期,默认允许最大2秒误差。弹幕刷新频率为0.5秒,即每个弹幕有效等待次数为2/0.5 = 4次,超过4次没有渲染将自动丢弃

- (void)removeExpiredDanmakusForTime:(HJDanmakuTime)time {
    [self.danmakuQueuePool enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(HJDanmakuAgent *danmakuAgent, NSUInteger idx, BOOL *stop) {
        danmakuAgent.toleranceCount --;
        if (danmakuAgent.toleranceCount <= 0) {
            [self.danmakuQueuePool removeObjectAtIndex:idx];
        }
    }];
}

弹幕冗余度的设计使得弹幕显示更加平均,优化了弹幕显示效果,但是会降低弹幕显示的精确度,特别对于视频场景,相对于1.0版本有所下降,如果你对精确度要求较高,可以降低tolerance冗余值

碰撞检测

与1.0相同,HJDanmaku2.0仍然使用系统动画的方式提供弹幕动画支持,但是碰撞检测方式略有不同

- (BOOL)checkLRIsWillHitWithPreDanmaku:(HJDanmakuAgent *)preDanmakuAgent danmaku:(HJDanmakuAgent *)danmakuAgent {
    CGFloat width = CGRectGetWidth(self.bounds);
    CGFloat preDanmakuSpeed = (width + preDanmakuAgent.size.width) / self.configuration.duration;
    if (preDanmakuSpeed * (self.configuration.duration - preDanmakuAgent.remainingTime) < preDanmakuAgent.size.width) {
        return YES;
    }
    CGFloat curDanmakuSpeed = (width + danmakuAgent.size.width) / self.configuration.duration;
    if (curDanmakuSpeed * preDanmakuAgent.remainingTime > width) {
        return YES;
    }
    return NO;
}

HJDanmaku2.0中,碰撞检测不再以弹幕时间点为参考维度,渲染的弹幕拥有剩余时间属性,通过剩余时间与速度的关系即可判断两者之间是否碰撞。同时,2.0版本只在添加弹幕和恢复动画时为弹幕视图添加动画,其它时间不再校验

手势

运动视图系统默认无法响应手势交互事件,整个点击事件交由全局统一处理。HJDanmakuCell定义属性selectionStyle控制弹幕能否点击,默认HJDanmakuCellSelectionStyleNone,即不能点击

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    self.selectDanmakuAgent = nil;
    HJDanmakuAgent *danmakuAgent = [self danmakuAgentAtPoint:point];
    if (danmakuAgent) {
        if (danmakuAgent.danmakuCell.selectionStyle == HJDanmakuCellSelectionStyleDefault) {
            self.selectDanmakuAgent = danmakuAgent;
            return self;
        }
        CGPoint cellPoint = [self convertPoint:point toView:danmakuAgent.danmakuCell];
        return [danmakuAgent.danmakuCell hitTest:cellPoint withEvent:event];
    }
    return [super hitTest:point withEvent:event];
}

视图整体响应链参考以上代码,当收到点击事情时,优先判断弹幕cell是否响应,如果响应则交由弹幕cell处理,否则交由全局统一处理

总结

时隔两年,HJDanmaku2.0在性能、并发以及定制型方面都有较大的提升,以iphone6设备测试为例,CPU整体使用率稳定在5%左右,大并发100条/秒弹幕的持续输入,FPS可以维持在55帧以上

目前暂时支持OC,swift版本正在开发中,如果你有意贡献swift代码,可以与我联系~

当然,如果你喜欢,可以为本项目点点赞


写在文后:

新建了一个iOS开发QQ交流群(首页右上角入群),欢迎广大iOS开发朋友一同交流学习。当然,你也可以Follow本人GitHub,或者关注我的新浪微博,感谢你的来访,下期再见!

是的,最近很忙!

时光飞逝,转眼又到年末,过去一个多月,主要做了几件事:川藏之旅,辞了阿里,来到新东家—— 比心陪练!

川藏行

中秋国庆期间,陪夫人走了一趟川藏线,这场旅行足足半月之久,为了放空自己,特意选择青藏铁路出行,沿途风景很美,美的让人心醉!

秋天的九寨格外美,喜欢湛蓝天空下红绿相交的美丽,更喜欢大山深处有人家的淳朴与善良

相较于九寨的色彩斑斓,大美西藏似乎更多了一分壮美。都说西藏是离天堂最近的地方,站在通往纳木错的路中央,留下了这张印证我足记的照片,远离城市的嘈杂,生活原本可以不同~

2

天空之城,大美于行!

阿里,再见

是的,终于还是离开了阿里,一个生活了快三年的地方!无需过多的理由,走了就是走了~

两年前拖着全部家当从武汉迁往上海的场景,依旧历历在目,仿佛就在昨天,而明天就是我离开阿里一个月的纪念日。阿里有句古话:“一年香,三年醇,五年陈”,而我终究没能迈过“三年醇”

怎么说了,非常感谢阿里,感谢曾经的天天,感谢前公司所有的同事、朋友以及小伙伴们。路走对了就不怕远,而我只能陪你们到这里,真心祝福你们!

伴随阿里星球音乐的停服,天天动听终究还是落幕。诺干年后,又有多少朋友记得曾经的天天动听?

谨以此纪念曾经的天天动听,纪念我即将逝去的青春!

比心,我来了

第一次听说比心是在国庆节后的一个上午,当时,大树同学找到我,邀请我加入他们团队,抱着体验的心态下载比心陪练,打开APP的一瞬间,我似乎找到了未来

这是一家用心做产品的公司,当然,我个人也比较追崇工匠精神。物欲横流的今天,能够静心做产品的初创公司实属难得,很荣幸能够加入这个大家庭

期待

凌晨1点23分,夜是如此的安静,而我是如此的清醒

上海,一个从来不缺乏梦想的国际化大都市。在这里,年轻的人们为了各自的梦想疲于奔波。 太多的青春热血在这里挥洒,也有太多的辛酸泪水在这里述说

正如蔡崇达在《皮囊》中所言:“北京不只是他想要求医的地方,还是他为自己开出的最后的药方”。或许,我们都生了同一种病,我也是一个来看病的人!

2016年12月8日记于上海

Protocol协议分发器

转载请注明出处:http://www.olinone.com/

Hi,本期跟大家聊聊协议分发,何为协议分发?协议分发可以简单理解为将协议代理交给多个对象实现!

Protocol协议代理在开发中应用频繁,开发者经常会遇到一个问题——事件的连续传递。比如,为了隔离封装,开发者可能经常会把tableview的delegate或者datesource抽离出独立的对象,而其它对象(比如VC)需要获取某些delegate事件时,只能通过事件的二次传递。有没有更简单的方法了?协议分发器正好可以派上用场

话不多说,先上干货:HJProtocolDispatcher是一个协议实现分发器,通过该工具能够轻易实现将协议事件分发给多个实现者。比如最常见的tableview的delegate协议,通过HJProtocolDispatcher,能够非常容易的分发给多个对象,具体可参考Demo

self.tableView.delegate = AOProtocolDispatcher(UITableViewDelegate, self, self.delegateSource);

原理解析

原理并不复杂, 协议分发器Dispatcher并不实现Protocol协议,其只需将对应的Protocol事件分发给不同的实现者Implemertor。如何实现分发?

熟悉类Class响应链的童鞋都知道,NSObject对象主要通过以下函数响应未实现的Selector函数调用

- (id)forwardingTargetForSelector:(SEL)aSelector OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
- (void)forwardInvocation:(NSInvocation *)anInvocation OBJC_SWIFT_UNAVAILABLE("");
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

因此,协议分发器Dispatcher可以在该函数中将Protocol中Selector的调用传递给实现者Implemertor,由实现者Implemertor实现具体的Selector函数即可

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL aSelector = anInvocation.selector;
    if (!ProtocolContainSel(self.prococol, aSelector)) {
        [super forwardInvocation:anInvocation];
        return;
    }
    
    for (ImplemertorContext *implemertorContext in self.implemertors) {
        if ([implemertorContext.implemertor respondsToSelector:aSelector]) {
            [anInvocation invokeWithTarget:implemertorContext.implemertor];
        }
    }
}

设计关键

如何做到只对Protocol中Selector函数的调用做分发是设计的关键,系统提供有函数

objc_method_description protocol_getMethodDescription(Protocol *p, SEL aSel, BOOL isRequiredMethod, BOOL isInstanceMethod)

通过以下方法即可判断Selector是否属于某一Protocol

struct objc_method_description MethodDescriptionForSELInProtocol(Protocol *protocol, SEL sel) {
    struct objc_method_description description = protocol_getMethodDescription(protocol, sel, YES, YES);
    if (description.types) {
        return description;
    }
    description = protocol_getMethodDescription(protocol, sel, NO, YES);
    if (description.types) {
        return description;
    }
    return (struct objc_method_description){NULL, NULL};
}

BOOL ProtocolContainSel(Protocol *protocol, SEL sel) {
    return MethodDescriptionForSELInProtocol(protocol, sel).types ? YES: NO;
}

注意事项

协议分发器使用需要了解如何处理带有返回值的函数 ,比如

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section

我们知道,iOS中,函数执行返回的结果存在于寄存器R0中,后执行的会覆盖先执行的结果。因此,当遇到有返回结果的函数时,返回结果以后执行的函数返回结果为最终值,以Demo为例

self.tableView.delegate = AOProtocolDispatcher(UITableViewDelegate, self, self.delegateSource);

TableView的DataSource以后面的self.delegateSource中实现函数返回的结果为准

备注

开发完本项目后发现网上已有朋友实现了协议分发器AOMultiproxier,因此,技术版权属于原作者,本文只做宣传,特此说明!


写在文后:

看时间大家可能也发现,项目代码提交已有一段时间, 为啥这么久才更新此文章?其实,最近完成了一次长途旅行——川藏行,算作给自己的一次心灵洗涤吧!

如果你喜欢本文章,Demo欢迎你的点赞

新建了一个iOS开发QQ交流群(首页右上角入群),欢迎广大iOS开发朋友一同交流学习。当然,你也可以Follow本人GitHub,或者关注我的新浪微博,感谢你的来访,下期再见!

ReactNative源码笔记——你知道几条?

转载请注明出处:http://www.olinone.com/

ReactNative是Facebook开源的一种实现移动跨平台开发的解决方案,目前在业界得到广泛应用,这里有非常详细的中文使用指南。本文主要分享RN源码中一些值得大家学习或者借鉴的代码或者编写技巧等,供大家学习参考

整个RN库包含10多个工程,有兴趣的童鞋可以下载源码查看具体细节,在此不再展开

宏定义巧用

整个ReactNative源码工程中用到了大量的宏定义,包括RCT_EXTERN、RCT_NOT_IMPLEMENTED、RCT_EXPORT_METHOD以及RCT_EXPORT_MODULE等申明宏或者功能宏。通过宏定义的方式,可以非常方便嵌入功能代码或者逻辑实现,重用代码的同时又保持了代码的整洁性

比如,ProtocolKit工程中,作者通过宏定义@defs将Protocol接口巧妙的实现在.h文件中,代码简介明了,又不失功能完整性。当然,RN工程中,RCT_NOT_IMPLEMENTED宏也有相似作用,实际项目中各位也可以尝试通过宏定义实现一些常用功能模块

关于iOS宏定义的文章有很多,在此推荐两篇非常不错的文章:RAC中必须要知道的宏ios宏的使用和技巧

环境变量

iOS开发中,各位对#ifdef DEBUG应该非常熟悉,通过判断该条件,可以区别当前运行环境是Debug环境还是Release环境。比如Release环境下通过重定义NSLog以屏蔽所有日志输出

#ifdef DEBUG  
#define NSLog(...) NSLog(__VA_ARGS__)  
#else  
#define NSLog(...) {}  
#endif

进一步,是否可以考虑只在联机调试环境下输出日志?此时就涉及联机调试环境的判断,环境变量正好可以解决该问题

Xcode可以在不同环境下自定义环境变量Environment Variables,通过在运行环境Run中自定义变量CI_USE_PACKAGER,此时便可在项目代码中通过getenv()函数判断当前运行环境

if (getenv("CI_USE_PACKAGER")) {
  // to do...
}

被忽略的硬键盘

相较于软键盘文字符号的输入,对于APP来说,硬键盘的应用开发似乎很容易被忽视,毕竟,通常情况下,硬键盘输入只会出现在模拟器环境下

iOS7以后,系统定义有硬键盘响应交互类UIKeyCommand,通过UIKeyCommand,APP能够监听硬键盘的特定输入响应,比如Command+D等,当然,前提是APP需要首先监听该输入命令

UIKeyCommand的使用非常简单,当需要在特定场景触发某一事件,但又不想影响界面显示的时候,不妨试试UIKeyCommand,具体使用可以看看这篇文章

_cmd

iOS官方文档中,_cmd表示当前方法的selector,你可以通过下面代码打印输出当前函数名

NSLog(@"Current method: %@", NSStringFromSelector(_cmd));

当然,实际项目中,你也可以这样使用

NSNumber *rootTag = objc_getAssociatedObject(self, _cmd) ?: @1;
objc_setAssociatedObject(self, _cmd, @(rootTag.integerValue + 10), OBJC_ASSOCIATION_RETAIN_NONATOMIC);

瞧,是不是有点意思!

kCFNull

相对于nil NSNull而言,kCFNull笔者接触较少,kCFNull可以理解为NSNull单例对象

id null1 = (id)kCFNull;
id null2 = [NSNull null];

打印地址

null1=(NSNull *)0x10426eaf0
null2=(NSNull *)0x10426eaf0

从上面测试结果可以看出它们其实指向同一地址, 可以简单理解为 kCFNull === [NSNull null]

文本阴影NSShadow

APP开发中,程序猿可能经常需要在图片或视频上显示文字,由于背景颜色跟文字颜色相近,导致文字看不清,比如时下火热的直播弹幕显示,为了确保文字显示清晰,开发者一般会配上阴影或者文字描边

给文本添加阴影描边,系统提供有NSShadow类,可以这样使用

NSShadow *shadow = [NSShadow new];
shadow.shadowOffset = CGSizeZero;
shadow.shadowBlurRadius = 5.0f;
shadow.shadowColor = [UIColor colorWithWhite:0.0f alpha:0.3f];
NSAttributedString *attString = [[NSAttributedString alloc] initWithString:@"www.olinone.com" attributes:@{NSShadowAttributeName: shadow, NSForegroundColorAttributeName: [UIColor whiteColor]}];
lbl.attributedText = attString;

实际效果是这样的,shadowBlurRadius值越小,文本描边越清晰

主线程判断

判断当前执行线程是否为主线程的方法有很多,比如

[NSThread isMainThread]
pthread_main_np

在RN中,它是这样的

BOOL RCTIsMainQueue() {
  static void *mainQueueKey = &mainQueueKey;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    dispatch_queue_set_specific(dispatch_get_main_queue(), mainQueueKey, mainQueueKey, NULL);
  });
  return dispatch_get_specific(mainQueueKey) == mainQueueKey;
}

当然,由于无法查看NSThread内部实现机制,暂时无法了解孰优孰劣,不过,[NSThread isMainThread]貌似足矣!

volatile不简单

百科中,是这样描述它的:就像大家更熟悉的const一样,volatile是一个类型修饰符,它是被设计用来修饰被不同线程访问和修改的变量。作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值

简单说,被volatile修饰的变量是多线程安全的,其次,不会因为编译器优化导致读值出错。关于编译器编译优化可以看看这篇文章

iOS开发中确保多线程安全的方法有很多,原子操作、线程锁、单线程执行等等,本人也写过相关文章iOS开发多线程同步

在RN中,通过volatile修饰符,巧妙实现了多线程取消操作

__block volatile uint32_t cancelled = 0;
if (!cancelled) {
   // to do...
}
OSAtomicOr32Barrier(1, &cancelled);

通过原子性操作访问被volatile修饰的cancelled对象即可保障函数只执行一次。想想大家熟悉的单例dispatch_once_t,现在让你设计单例对象,你又会如何设计了?

+ (instancetype)sharedInstance {
  static RCTWebSocketManager *sharedInstance = nil;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    sharedInstance = [self new];
  });
  return sharedInstance;
}

结构体Struct

说起Struct,不知各位对它印象如何?大学C课本中学过?NSObject类class原型貌似有讲?

struct iOSDev {
    NSString *nickName;
};

OC中一个简单的结构体,在Swift中,Struct也可以这样写

struct iOSDev {
    var nickName : String
    func getBusinessCard() -> String {
        return "\(nickName),幽默的iOS开发者!"
    }
};

let iOSOlinone = iOSDev(nickName: "olinone")
print(iOSOlinone.getBusinessCard())

getBusinessCard为结构体函数,是不是感觉很方便!其实OC中也可以这样写

struct iOSDev {
    NSString *nickName;
    NSString *getBusinessCard() {
        return [NSString stringWithFormat:@"%@,幽默的iOS开发者!", nickName];
    }
};

iOSDev iosDev = iOSDev{@"olinone"};
NSLog(@"%@", iosDev.getBusinessCard());

当然,为Struct添加函数并不是C语言特性,而是C++特性,因此,为了编译通过,你需要将.m文件修改成.mm文件

Struct有其使用的特殊场景,相较于Class,合理的使用Struct可以使代码更加整洁。同时,为了适应Swift中Struct强大特性,可以试着在OC项目中尝试Struct

最后,给大家来个段子吧:

话说一美女要在两个男人之间做选择,一个年纪大,长的丑,是个千万富翁,另一个年轻,帅气,iOS开发程序猿。 她对他们说,我会给你们一人一张纸条,写着我愿意的那张就是我的选择。 富翁打开纸条,看见上面写着我愿意,于是搂着她,坐上豪车高兴的走了。 年轻的小伙很伤心,打开纸条看见上面写着:“等我一个月~”  ^o^
 

写在文后:

有些童鞋可能经常会问一个问题,感觉自己技术遇到瓶颈,如何才能进一步提升自己技术能力?其实这个问题,本人也是摸石头过河,不过有一点可以确定,那就是保持一颗不断进取的心吧

新建了一个iOS开发QQ交流群(首页右上角入群),欢迎广大iOS开发朋友一同交流学习。当然,你也可以Follow本人GitHub,或者关注我的新浪微博,感谢你的来访,下期再见!

MVVM奇葩说

转载请注明出处:http://www.olinone.com/

一直想聊聊这个话题,也有朋友跟我留言,让我讲讲MVVM,只可惜一直没整明白,不敢轻易下笔。针对MVVM,网上有很多不错的文章,比如MVVM介绍被误解的 MVC 和被神化的 MVVM以及Look at MVVM from a different perspective等等

文章前我想先提几个问题

  1. MVVM到底是什么?它和MVC有什么区别?
  2. MVVM中VM到底是个什么角色?它和Controller或者Manager有什么区别?
  3. ViewController在MVVM中扮演怎样角色?Api数据请求放在哪里?数据流向如何?

MVVM简介

关于MVVM,相信大家或多或少都有了解。引用MVVM介绍文中一图

受MVC或MVP架构的影响,对MVVM最初印象以为这是一个以ViewModel为核心,处理View和Model的开发架构。于是乎在原有MVC的基础上,创建了一个所谓的ViewModel对象,然后把ViewController中的代码移到ViewModel中,在ViewModel里面处理View以及Model的所有逻辑。毕竟大家都在说MVVM可以为ViewController瘦身,这ViewController就剩创建ViewModel的代码,嗯,够瘦身,这就是MVVM!

慢慢的,发现有什么地方不对,哪里不对?第一想法就是ViewController的定位,View?不是,Controller?也不是!毕竟它就创建ViewModel,好像与View、Model也没啥关系。抛开ViewController不谈,突然发现这样的ViewModel、Model以及View不就是MVC,一个以ViewModel为中心的MVC!

错在哪里

核心问题就在于对ViewModel角色的定位不清!基于MVVM设计思路,ViewModel存在目的在于抽离ViewController中展示业务逻辑,而不是替代ViewController,其它视图操作业务等还是应该放在ViewController中实现

既然不负责视图操作逻辑,ViewModel中就不应该存在任何View对象,更不应该存在Push/Present等视图跳转逻辑。因此,ViewModel中绝不应该存在任何视图操作相关的代码

@interface ViewModel : NSObject

// viewmodel中切不可存在view对象,更不该出现push或者present代码
- (instancetype)initWithTableView:(UITableView *)tableView;

@end

ViewModel做啥

很简单,处理视图展示逻辑,ViewModel负责将数据业务层提供的数据转化为界面展示所需的VO。其与View一一对应,没有View就没有ViewModel

比如,数据业务层传递一个含有性别属性sex的DO对象,0表示男, 1表示女。ViewModel的职责就是将其转化为展示层可显示的VO对象

self.personVO.sex = personDO.sex == 0 ? @"男": @"女";

ViewModel和View一起组成DDD(Model-Driven Design)领域驱动架构体系中的Presentation展示层。在iOS中,数据流向可以表示为ViewModel->ViewController->View,ViewController负责连接VO及其对应的View对象

领域驱动设计

领域驱动设计(DDD)对于安卓童鞋可能非常熟悉,有兴趣的童鞋可以参考这篇文章,本文不做过多讲解,借用其描述介绍几个名词

  • VO(View Object):视图对象,用于展示层,它的作用是把某个指定页面(或组件)的所有数据封装起来
  • DO(Domain Object):领域对象,就是从现实世界中抽象出来的有形或无形的业务实体
  • PO(Persistent Object):持久化对象,它跟持久层(通常是关系型数据库)的数据结构形成一一对应的映射关系,如果持久层是关系型数据库,那么,数据表中的每个字段(或若干个)就对应PO的一个(或若干个)属性
  • Domain:领域驱动层,是用户与数据库交互的核心中转站,控制用户数据收集,控制请求转向等

MVVM架构中,ViewModel连接视图View和数据业务Model层,而Domain和Data数据持久层共同组成整个Model层。完整结构如图所示

Model并不表示Model

MVVM架构中的M,并不表示Model对象,而是表示整个数据业务层,对应DDD架构中的Domain层以及数据Data层。业务开发中,一般考虑Api或者DB对象,极少考虑Domain层设计,也不会区分DO或者PO对象。笼统定义Model ,将其传递给展示层ViewModel,久而久之,Model对象承载的信息越来越多,更有甚者,在Model中处理业务逻辑,导致项目维护成本增加,代码中出现if..else的概率也会越来越大

@interface PersonModel : NSObject

@property (nonatomic, assign) NSInteger sex;
@property (nonatomic, readonly) NSString *sexDescription;

@end

@implementation PersonModel

// model中不应该存在业务逻辑代码
- (NSString *)sexDescription {
    return self.sex == 0 ? @"男": @"女";
}

@end

当然,Domain层并不是必须的,实际开发中,需要根据具体复杂度和需求来决定。比如只是纯粹的请求展示界面,设计过多的层次结构反而会增加项目的维护成本。同时,Domain层不应该存在任何状态变量!

Data数据层

ViewModel负责展示层逻辑,而Data层则对应数据层逻辑,一般以Manager或者Service身份存在,数据来源主要包括Api、DB或者Cache等。Data数据层操作对象主要为PO持久化对象,对象一旦创建,原则上不可修改

Data数据层具有独立可测试性,其不依赖视图层而存在。切记不可在Data层操作任何视图对象!

@implementation PersonDBAccess

// Data层不应该存在任何视图相关代码
- (NSArray *)fetchPersonModels {
    [SVProgressHUD showWithStatus:@"加载中。。。"];
}

@end

MVVM奇葩说

文章到此,想必各位对MVVM架构已经有了大致了解。对比安卓童鞋对MVP架构的钟爱,iOS童鞋也许更加青睐MVVM,拌上ReactiveCocoa或者RxSwift,这道菜可以做的更加绚烂多彩!当然,正如唐巧在文中所言:ReactiveCocoa 和 MVVM 不应该被神化,我们需要保持的是一个拥抱变化的心,以及理性分析的态度。在新技术的面前,不盲从,也不守旧,一切的决策都应该建立在认真分析的基础上,这样才能应对技术的变化!


写在文后:

为期两天的SwiftCon已经落幕,虽然离我只有两站地铁,只可惜依然没能前往聆听各位大师的技术分享,特别还有我同事兼朋友刘冠杉的个人首秀。虽然,现在网上也出现了各种不好的声音,但是我仍然相信,大多数分享者还是为此付出了不少心血,值得我们为之鼓掌!

个人技术的成长不在一朝一夕,而是长年累月的付出积累沉淀的结果!Swift3.0不久即将发布,而我个人也会逐渐转移☞对Swift语言的学习。与此同时,随着公司ReactNative项目的上马,我也会加强对RN技术的专研和学习中

感谢你的来访,你可以Follow我的个人GitHub,也可以关注我的新浪微博,下期再见!

 

❌