阅读视图

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

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,或者关注我的新浪微博,感谢你的来访,下期再见!

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,也可以关注我的新浪微博,下期再见!

 

❌