iOS 基于CoreText题型渲染引擎主题讨论

目录

前言

本次分享的主题是关于CoreText的相关应用实践,并非是纯分享CoreTextAPI的使用。CoreText如何使用,网上很多相关文章,官方也有相应的开发文档,在这里我只是做简单的铺垫。

本次分享的主要目的是将“文本排版”,“文本绘制”,“图片绘制”,“图文混排”等相关的知识点串联起来,形成体系。然后通过“题型渲染引擎”和“YYText”的相关实践,来系统化的思考未来开发中关于排版可能的解决方案。

一:排版的基础

1. UILabel,UITextField,UITextView,UIWebView的文本是如何排版并显示到屏幕上的?

我们平常使用最多的是UIKit里的UILabel,UITextField,UITextView,UIWebView,他们的文字排版是都基于CoreText这个核心库。CoreText是对富文本做排版,将排版后的数据,即字形和其位置,交给CoreGraphics进行绘制,绘制的结果是bitmap图,然后交由GPU渲染。

下面是iOS7之后的UIKit文本处理的架构图如下

text_kit_arch_2x
【图片来自官方文档】Text Kit Framework Position

2. Text纯文本的排版原理

排版的核心是把字符转换成字形,把字形排成行,把行排成段落。

  1. 字符与字形概念

    • 字符:每一个字符用唯一的Unicode表示
    • 字形:一个字符可以有很多个字形,也就是字的形状,包括,斜体,加粗都是不同的字形
    • 字体:字符与字形的颜射,一个字符可以有多种字形
    • 装饰:下划线,删除线,与字形没有关系,系统只是额外的加了一条线
    • 段落:段落与字形也没有关系,只是改变字形的位置,包括缩进,对齐,方向

    我们平时设置[UIFont fontWithSystem:]方法时,就是在字体里找所应的字符的形状。

  2. 字形描述集(Glyphs Metris)

    那么字形本身用哪些属性来描述呢?如下面两幅图

    glyphterms_2x
    【图片来自官方文档】Text Programming Guide for iOS
    textpg_intro_2x

    【图片来自官方文档】Text Programming Guide for iOS

  3. 字形描述解释
    BaseLine:一条假想的参照线,以此为基础进行字形的布局
    Origin:基线上最左侧的点。
    Ascent:上行高度,字形的最高到baseline的高度
    Descent:下行高度,字形的最低到baseline的高度
    leading:行间距
    Bounding Box:一个假想的边框,尽可能地容纳整个字形
    通过下面的一张图可以检验是否理解了字形描述的属性
    Xnip2019-03-23_15-34-29
    红色高度即为当前行高,绿线为baseline,绿色到红框上部分为当前行的最大Ascent,绿线到黄线为当前行的最大Desent,而黄框的高即为行间距。

  4. CoreTex对字形的处理过程
    布局引擎要用字体把字符转换成字形,然后通过字体规格信息微调大小,用段落样式把字形放到它所该在的位置。(暂时假设,字形是图片,那么,图片怎么显示到屏幕上的呢?)

3. 图片是怎样从加载到显示的?

字形我们假设是图片,那么图片又是如何从本地加载到显示到屏幕上的呢?
在这里我只是做个简短的描述,想要更为细致的研究,请参考
谈谈 iOS 中图片的解压缩
iOS 视图、动画渲染机制探究

图片显示过程如下:

  • 从磁盘加载一张图片,UIImage *image = [UIImage imageWithName:]这个时候图片并没有解压缩,UIImage里存的是未解压缩的NSData。
  • 然后将生成UIImage赋值给UIImageView
  • 接着一个隐士的 CATranscation,捕获UIImageView图层树的变化(Transactions是CoreAnimation的用于将多个layer tree操作批量化为渲染树的原子更新的机制)
  • 在主线程的下一个runloop到来时,Core Animation 框架准备要渲染的 layer 的各种属性数据,以及要做的动画的参数,准备传递給 render server。同时在这个阶段也会解压缩要渲染的 image。
  • CoreAnimation打包layer的信息以及需要做的动画的参数,通过IPC交给renderServer(动画和视图的渲染其实是在另外一个进程renderServer里进行,renderServer与GPU打交道)

4. 怎么实现图文混排呢?

我们知道了文本的排版,知道了图片的渲染,那么如何实现图文混合排版呢?在开始这个问题前,我们先想想目前有哪些已经实现了图文混排的控件。

  • UIWebView
    UIWebView的可以实现图片,文本的混合排版。整个的实现思路我猜测,是解析HTML文件,转换成富文本,然后用CoreText进行排版,然后将排版的结果绘制成位图图片,最后将图片放到视图CALayer的content上,由CoreAnimation交个RenderServer进行渲染。当然我说的这个流程是他的渲染一个简化的流程,苹果的实现还会对JS,网络等做处理,这就是一个比较大的话题了,不过也是一个非常有趣的主题。虽然UIWebView就能满足我们的需求,当考虑到H5加载速度慢,占内存,交换缓慢,绘制在主线程这些缺陷,影响用户体验。

  • UITextView
    UITextView的实现,也是基于TextKit实现,TextKit是对CoreText的一个封装。其实我们自己也可以通过CoreText来实现我们自己的TextView。YYText就是一个典范,我们在最后会谈谈YYText的源码。

现在我们知道了文字如何排版,图片如何显示,也知道了UIWebView,UITextView等的文本,图文混排都是基于CoreText的。那么我们怎么去实现文,图,输入框,其他自定义元素的混合排版呢?

下面我们看看CoreText的细节,看能不能从中找到实现的关键手段。

二:CoreText实现图文细节

1. CoreText的API

参见Core Text Programming Guide

主要类介绍
CTFramesetter:排版器
CTFrame:排版结果,我们操作绝大部分都是对里面的line,run,delegate做操作
CTLine:CTFrame里的代表一行
CTRun:CTFrame里带表相同属性的片段
CTRunDelegate:字符代理,供设置字形的一些属性。(哈哈,这里是不是跟我们前面一开始聊排版的字形相联系起来了,对的,这个代理就是供设置哪些字形的属性,当然前提是要那个属性可以被设置,查看好像只有Ascent,Descent,width提供代理callBack)

CoreText关键元素结构图如下

core_text_arch_2x

【图片来自官方文档】Core Text Programming Guide

排版流程图如下

绘制流程

总结下:CoreText整个的输入是富文本,输出是CTFrame。所以在将文本进行排版的前提是将排版的元素转换成富文本,特殊元素用特殊字符代替,计算好位置后在特殊元素放在特殊字符的位置就可以了,CTFrame绘制到指定的图片尚上下文(可以是在主线程,也可以是在子线程绘制)。

  • 生成对指定富文本的排版器CTFramesetter

    CTFramesetterRef setter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef) self.assembleAttributesString);
    
  • 生成排版结果CTFrame

    指定的路径范围里对指定的字符串的范围利用排版器setter进行排版,生成排版结果
    CTFrame

    CTFrameRef frame = CTFramesetterCreateFrame(setter, CFRangeMake(0,  self.assembleAttributesString.length), path, NULL);
    
  • CTFrame绘制到指定的图片上下文

    CGContextRef context = UIGraphicsGetCurrentContext();//取得当前的上下文
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);//去掉文本锯齿
    CGContextTranslateCTM(context, 0, self.bounds.size.height);//转换坐标系
    CGContextScaleCTM(context, 1, -1);
    CTFrameDraw(self.richTextData.frameRef, context);
    

以下是部分核心代码

//CoreRichTextData.m
- (instancetype)initWithSentenceArray:(NSArray *)sentenceArray {
    if(self = [super init]){
        _sentenceArray = sentenceArray;
        _assembleAttributesString = [[NSMutableAttributedString alloc]init];
        [self arrangementAssembleAttributesString];
    }
    return self;
}

- (void)setTextBounds:(CGRect)textBounds {
    _textBounds = textBounds;
    [self calculateImagePosition];
}

- (void)calculateImagePosition {
    //获取所有的列
    NSArray *lines = (NSArray *)CTFrameGetLines(self.frameRef);
    if (lines == 0) {
        return;
    }

    CGPoint lineOrigins[lines.count];
    CTFrameGetLineOrigins(self.frameRef, CFRangeMake(0, 0), lineOrigins);
    for (int i = 0; i<lines.count; i++) {
        CTLineRef line = (__bridge CTLineRef)lines[i];
        //获取每一行的runs
        NSArray *runs = (NSArray *)CTLineGetGlyphRuns(line);
        for (int j = 0; j< runs.count; j++) {
            CTRunRef run = (__bridge CTRunRef)runs[j];
            
            //每一个run的属性,从run中获取先前保存的信息
            NSDictionary *attributes = (NSDictionary *)CTRunGetAttributes(run);
            if(!attributes){
                continue;
            }
            
            //获取当前run的位置
            NSDictionary *extraData = (NSDictionary *)[attributes valueForKey:kCoreExtraDataAttributeTypeKey];
            if(extraData){
               id<CoreItemPotocal>  item = [extraData valueForKey:@"value"];//对象
                if (item == nil) {
                    continue;
                }
                if ([item isKindOfClass:[CoreTextItem class]]) {
                    NSLog(@"这是普通文本");
                }else if([item isKindOfClass:[CoreLinkItem class]]){//返回的坐标是uikit坐标(左上为起点)
                    NSLog(@"这是链接");
                }else if([item isKindOfClass:[CoreImageItem class]]){//返回的坐标是CoreText(左下为起点)
                    NSLog(@"这是图片");
                }
                CGFloat ascent;
                CGFloat desent;
                CGFloat width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &desent, NULL);
                CGFloat height = ascent + desent;
                
                CGFloat xOffset = lineOrigins[i].x + CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
                CGFloat yOffset = lineOrigins[i].y;
                
                //coreText坐标
                CGRect ctClickableFrame = CGRectMake(xOffset, yOffset, width, height);
                
                //将CoreText坐标转换为UIKit坐标
                CGRect uiKitClickableFrame = CGRectMake(xOffset, self.textBounds.size.height - yOffset - ascent, width, height);
                
                [item.uiKitFrames addObject:[NSValue valueWithCGRect:uiKitClickableFrame]];
                [item.runFrames addObject:[NSValue valueWithCGRect:ctClickableFrame]];
            }
        }
    }
}

- (void)arrangementAssembleAttributesString {
    for (int i = 0; i< self.sentenceArray.count; i++) {
        id<CoreItemPotocal>  item = self.sentenceArray[i];
        if([item conformsToProtocol:@protocol(CoreItemPotocal)] && item.attributesStr!=nil){
            [self.assembleAttributesString appendAttributedString:item.attributesStr];
        }
    }
}

- (CTFrameRef)frameRef {
    //排版的路径范围
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddEllipseInRect(path, &CGAffineTransformIdentity, self.textBounds);
    
    //创建排版器,
    CTFramesetterRef setter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef) self.assembleAttributesString);

    //在指定的路径里,排版哪些范围的字符
    CTFrameRef frame = CTFramesetterCreateFrame(setter, CFRangeMake(0, self.assembleAttributesString.length), path, NULL);
    return frame;
}

2. 需要解决的主要问题

下面我只是将要面临的问题的思路简单说明下,想一探究竟的可以参考我的一个在GitHub上的CoreText开源示例的CoreTextDemo,或者参考唐巧老师的基于CoreText的排版引擎代码里有对这些问题的实现。

  • 坐标转换
    UIKit的原点与CoreText的坐标原点不一样。需要做坐标转换。

  • 特殊元素(图片,自定义的view等)
    图片主要是在指定的位置正确显示,用一个空白符,指定其代理方法的callBack设置空白符的大小。遍历CTFrame找到这个空白符,将其相对应的图片绘制在这个空白位置。CGContextDrawImage(context, frame, imageItem.image.CGImage);

  • 自适应高度
    要能够实现自适应高度,像使用基本的UILable一样,能通过自动布局或者sizeToFit计算出高度。做的完善点还可以支持自动布局

三:基于CoreText的题型渲染引擎实践

1. 效果展示

Xnip2019-03-21_10-48-001Xnip2019-03-21_10-48-472

2. 整体架构

  • UML结构图

    题型渲染引擎的架构图如下,将要排版题目的相关数据放在TemplateRenderModel(主要承载要绘制的题目内容,编辑与非编辑状态,不同场景下的样式区分)里。TemplateRenderEngin会对TemplateRender进行解析成

题型渲染引擎结构图

  • 渲染流程图

未命名文件 -2-

  • TemplateRenderModel

    • TemplateRenderModel里的question

      {"type":"para_begin","style":"math_picture"}##{"type":"img","src":"https://     tikuqiniu.knowbox.cn/Fuh17in1YIvYokZpB4k3Wrvxdz4U","size":"big_image","id":1}       ##{"type":"para_end"}##{"type":"para_begin","style":"math_text"}        ##{"type":"blank","class":"fillin","size":"express","id":1}#-       #{"type":"blank","class":"fillin","size":"express","id":2}#=        #{"type":"blank","class":"fillin","size":"express","id":3}##{"type":"para_end"}#
      

      解释question字符串(不需要纠结为什么这么定义,只需要明白定义了些什么)

      type:定义是什么类型的元素,段落,blank,img,audio等
      style:定义段落样式,本地有样式plist文件的映射
      src:像图片,audio等带有媒体资源的源
      size:图片大小
      id:方便在同一个上下文里区分相同类型的不同元素
      class:规定元素所属的类

      对Html了解的同学会觉得非常像Html标记里的定义,这算是一种模仿定义吧。

    • TemplateRenderModel里的renderMode

      typedef NS_ENUM(NSUInteger, TemplateRenderMode) {
      kTemplateRenderModeEdit,
      kTemplateRenderModePreview,
      kTemplateRenderModeOptionPreview,//预览选项
      kTemplateRenderModeOptionEdit,  //答题选项
      kTemplateRenderModeEditUserAnswer, //编辑模式下填充用户答案
      };
      

      解释renderMode,渲染模式,题目既可以是可编辑的,也可以是预览或者用户填充完答案的。他们的显示内容是有差别的。

  • 字符串转富文本(数据解析过程)

    //1. 解析question字符串为一个个句子,即模型化
        NSArray<TextFlowParseSentenceItem *> *sentenceList = [TextFlowSentenceParser parseString:string error:&parseError];
    //2. 将句子转换成段落, 即模型化
            NSArray<TextFlowParagraphItem *> *textFlowParagraphs = [TextFlowParagraphParser paragraphsFromSentences:sentenceList rawString:string error:&paragraphParseError];
    //3.1 将句子转换成一个抽象的对象TemplateRenderEngineUnitItem
        将模型化后的句子进行标准化,标准化成一个可以操作的元素。这个元素标准化成三类,一般文本,纯属性文本,特殊元素。这个特殊元素就是前面提到的用数据填充后的模板。有音频的模板,图片的,其他任何自定元素的模板。这些被填充过后的模板知道了其自身的大小。并将模板的代理设置为TemplateRenderEngin,方便对模板的操作提供交互回调。
    
    //3.2 将整个段落转换成抽象的对象TemplateRenderEngineParagraphItem
        将模型化后的句子进行标准化,标准化成一个段落元素。这个段落元素包括<TemplateRenderEngineUnitItem *>units,和样式。
    
  • 创建View
    根据标准化的段落句子,将他们的模板所生成的view,添加到这个self.view上。TemplateRenderEngin的view就是渲染后的View。

  • 计算各元素即相应的view的位置
    将self.paragraphs里的TemplateRenderEngineParagraphItem,TemplateRenderEngineUnitItem转换成特殊字符,这些特殊字符代表占位。并将转换成的富文本,进行离屏绘制,生成bitmap,记录位置。
    将TemplateRenderEngineUnitItem的填充抽的模板的view进行重新设置位置。这块的内容就是利用到了上节所聊的CoreText的排版技术。

3. 可以借鉴的地方?可以优化的地方?

  • 好的地方

    1. 数据,样式,模板的相分离的设计
    2. 离屏绘制,即单独生成一个图形生下文进行绘制
    3. 绘制后bitmap图片的缓存
  • 待优化的地方

    1. 异步绘制优化性能?
    2. TemplateEngin里的数据解析应该与排版相分离。也就是引擎里实现分层并不是很清晰

四:YYText排版思路对比

1. YYText基于CoreText排版部分的思路

Xnip2019-03-25_00-47-22

2. 我们可以借鉴的地方?

  1. 异步绘制

YYLabel提供异步绘制功能通过一个异步绘制层YYTextAsyncLayer,将绘制的任务在子线程操作。绘制完成后后回调给YYLabel,在将特色元素放在指定的位置。

以下是YYLabel里的异步绘制任务调用

- (YYTextAsyncLayerDisplayTask *)newAsyncDisplayTask {
    
    // capture current context
    BOOL contentsNeedFade = _state.contentsNeedFade;
    NSAttributedString *text = _innerText;
    YYTextContainer *container = _innerContainer;
    YYTextVerticalAlignment verticalAlignment = _textVerticalAlignment;
    YYTextDebugOption *debug = _debugOption;
    NSMutableArray *attachmentViews = _attachmentViews;
    NSMutableArray *attachmentLayers = _attachmentLayers;
    BOOL layoutNeedUpdate = _state.layoutNeedUpdate;
    BOOL fadeForAsync = _displaysAsynchronously && _fadeOnAsynchronouslyDisplay;
    __block YYTextLayout *layout = (_state.showingHighlight && _highlightLayout) ? self._highlightLayout : self._innerLayout;
    __block YYTextLayout *shrinkLayout = nil;
    __block BOOL layoutUpdated = NO;
    if (layoutNeedUpdate) {
        text = text.copy;
        container = container.copy;
    }
    
    // create display task
    YYTextAsyncLayerDisplayTask *task = [YYTextAsyncLayerDisplayTask new];
    
    task.willDisplay = ^(CALayer *layer) {
        [layer removeAnimationForKey:@"contents"];
        
        // If the attachment is not in new layout, or we don't know the new layout currently,
        // the attachment should be removed.
        for (UIView *view in attachmentViews) {
            if (layoutNeedUpdate || ![layout.attachmentContentsSet containsObject:view]) {
                if (view.superview == self) {
                    [view removeFromSuperview];
                }
            }
        }
        for (CALayer *layer in attachmentLayers) {
            if (layoutNeedUpdate || ![layout.attachmentContentsSet containsObject:layer]) {
                if (layer.superlayer == self.layer) {
                    [layer removeFromSuperlayer];
                }
            }
        }
        [attachmentViews removeAllObjects];
        [attachmentLayers removeAllObjects];
    };

    task.display = ^(CGContextRef context, CGSize size, BOOL (^isCancelled)(void)) {
        if (isCancelled()) return;
        if (text.length == 0) return;
        
        YYTextLayout *drawLayout = layout;
        if (layoutNeedUpdate) {
            layout = [YYTextLayout layoutWithContainer:container text:text];
            shrinkLayout = [YYLabel _shrinkLayoutWithLayout:layout];
            if (isCancelled()) return;
            layoutUpdated = YES;
            drawLayout = shrinkLayout ? shrinkLayout : layout;
        }
        
        CGSize boundingSize = drawLayout.textBoundingSize;
        CGPoint point = CGPointZero;
        if (verticalAlignment == YYTextVerticalAlignmentCenter) {
            if (drawLayout.container.isVerticalForm) {
                point.x = -(size.width - boundingSize.width) * 0.5;
            } else {
                point.y = (size.height - boundingSize.height) * 0.5;
            }
        } else if (verticalAlignment == YYTextVerticalAlignmentBottom) {
            if (drawLayout.container.isVerticalForm) {
                point.x = -(size.width - boundingSize.width);
            } else {
                point.y = (size.height - boundingSize.height);
            }
        }
        point = YYTextCGPointPixelRound(point);
        [drawLayout drawInContext:context size:size point:point view:nil layer:nil debug:debug cancel:isCancelled];
    };

    task.didDisplay = ^(CALayer *layer, BOOL finished) {
        YYTextLayout *drawLayout = layout;
        if (layoutUpdated && shrinkLayout) {
            drawLayout = shrinkLayout;
        }
        if (!finished) {
            // If the display task is cancelled, we should clear the attachments.
            for (YYTextAttachment *a in drawLayout.attachments) {
                if ([a.content isKindOfClass:[UIView class]]) {
                    if (((UIView *)a.content).superview == layer.delegate) {
                        [((UIView *)a.content) removeFromSuperview];
                    }
                } else if ([a.content isKindOfClass:[CALayer class]]) {
                    if (((CALayer *)a.content).superlayer == layer) {
                        [((CALayer *)a.content) removeFromSuperlayer];
                    }
                }
            }
            return;
        }
        [layer removeAnimationForKey:@"contents"];
        
        __strong YYLabel *view = (YYLabel *)layer.delegate;
        if (!view) return;
        if (view->_state.layoutNeedUpdate && layoutUpdated) {
            view->_innerLayout = layout;
            view->_shrinkInnerLayout = shrinkLayout;
            view->_state.layoutNeedUpdate = NO;
        }
        
        CGSize size = layer.bounds.size;
        CGSize boundingSize = drawLayout.textBoundingSize;
        CGPoint point = CGPointZero;
        if (verticalAlignment == YYTextVerticalAlignmentCenter) {
            if (drawLayout.container.isVerticalForm) {
                point.x = -(size.width - boundingSize.width) * 0.5;
            } else {
                point.y = (size.height - boundingSize.height) * 0.5;
            }
        } else if (verticalAlignment == YYTextVerticalAlignmentBottom) {
            if (drawLayout.container.isVerticalForm) {
                point.x = -(size.width - boundingSize.width);
            } else {
                point.y = (size.height - boundingSize.height);
            }
        }
        point = YYTextCGPointPixelRound(point);
        [drawLayout drawInContext:nil size:size point:point view:view layer:layer debug:nil cancel:NULL];
        for (YYTextAttachment *a in drawLayout.attachments) {
            if ([a.content isKindOfClass:[UIView class]]) [attachmentViews addObject:a.content];
            else if ([a.content isKindOfClass:[CALayer class]]) [attachmentLayers addObject:a.content];
        }
        
        if (contentsNeedFade) {
            CATransition *transition = [CATransition animation];
            transition.duration = kHighlightFadeDuration;
            transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
            transition.type = kCATransitionFade;
            [layer addAnimation:transition forKey:@"contents"];
        } else if (fadeForAsync) {
            CATransition *transition = [CATransition animation];
            transition.duration = kAsyncFadeDuration;
            transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
            transition.type = kCATransitionFade;
            [layer addAnimation:transition forKey:@"contents"];
        }
    };
    
    return task;
}
  1. 将排版后数据抽象成YYTextLayout 将排版结构信息包装,方便处理。

3. 疑问,为什么不用YYText直接实现题型渲染,能不能满足需求呢?

题型渲染引擎包括的内容有题型的解析,排版,绘制。YYText包括了排版,绘制,所以题型解析的工作是逃不开的。而YYText是并不是专门做排版的,而是掺杂了TextView,label的特性的库。在一些支持一些特殊的排版问题时,是否能很好的支持,对于项目方的我们是存在很大的不确定性的。排版的机制并不是很难,我们自己做排版,虽然麻烦了点,但能掌握主动权,适应未来的需求。

五: 总结与参考文档

本篇文章主要探讨的是,基于CoreText排版库的应用实践。总体的流程是将富文本在指定的路径或者范围进行排版。对于自定的view或者其他元素,可以预先设置占符号,给定大小,排版完成后将view放在制定的位置即可。对于优化方面,有图片渲染后的内存缓存,异步绘制等。深入了解了文字如何排版,文字如何显示,图片如何显示,相信你对于UI布局,渲染性能方面的理解会提高一个很高的层次。

参考文档
Text Programming Guide for iOS
Core Text Programming Guide
Quartz 2D Programming Guide
Core Animation Programming Guide
谈谈 iOS 中图片的解压缩
iOS 视图、动画渲染机制探究
iOS

2019/3/15 posted in  iOS性能优化

Xcode里的Instruments的应用

TimerPofile

可以查看应用程序的那个方法耗时比较长,然后针对性优化

2018/12/22 posted in  iOS性能优化

xcode项目编译过程

在xcode的IDE工具很少关注整个的编译过程。
实际编译过程就是buildPhases的列表,一件一件执行这里面的工作。
比如

Check Pods Manifest.lock
runScript
Compile Sourse
Link Binary With Libraries
Copy Bundle Resources
Copy Pods Resources
RunScript
RunScript
Embed Pods Frameworks
  1. 先检查podfile的本地使用清单与Git后的清单,保证使用的是最小的pod清单

  2. runSript 执行自定义的脚本,比如提前下载编译需要的文件(这些大文件不好用git进行管理)

  3. Compile Source 编译.m文件,项目中的文件

  4. Link Binary 链接所有的静态库文件(包括.framework的静态库文件,.a的静态库文件)

  5. Copy Bundle Resources 拷贝资源文件到可执行文件目录

  6. Copy Pods Resources 拷贝Pods的资源文件到可执行的文件目录(如果pods库里包含boundle的资源文件,就会执行这个脚本,将其拷贝到可执行目录)

  7. RunScript 执行自定义脚本,在这里可以执行编译完成后的一些脚本。比如有bugTag的可以上传一些符号表。

当然执行自己的脚本方可以自己调整执行顺序。即是在编译前执行还是在编译后执行。

2018/9/25 posted in  iOS性能优化

iOS如何将像素显示到屏幕


绘制像素到屏幕

参考文章绘制像素到屏幕

参考文章iOS视图,动画渲染机制

本文将总结
1. 位图数据如何存储的?
2. 像素绘制到屏幕上需要经历的流程?
3. CPU的工作是什么?
4. GPU的工作是什么?
5. 离屏渲染的取舍?
6. 绘制与动画的关系?
7. 渲染性能优化的总结

一.像素

显示在屏幕上的是什么?
当像素映射到屏幕的时候,每一个像素均由三个颜色组件构成:红,绿,蓝,透明度。三个独立的颜色单元会根据给定的颜色显示到一个像素上。例如在iPhone5的显示屏上是1136 *640个像素。

1.1 像素在内存里的默认布局

  A   R   G   B   A   R   G   B   A   R   G   B  
| pixel 0       | pixel 1       | pixel 2   
  0   1   2   3   4   5   6   7   8   9   10  11 ...

1.2 二维数据

在实际使用像素数据时,有时会使用二位数据。就时颜色组件红,绿,蓝,alpha,每一个组件是一个数组。这样可以实现很好的对数据进行压缩或者其他处理。

二. 软件构成

Xnip2018-09-09_08-32-56

Display(显示屏)
GPU
GPU Driver
OpenGL
CoreGraphics
CoreAnimation
CoreImage

GPU是一个专门为图形并发计算而量身定做的处理单元。
CoreAnimation使用CoreGraphics来做渲染。CoreGraphics在CPU里进行运算出数据,形成位图包装成纹理交给OpenGl进行操作GPU进行渲染。

2.1 工作大致流程

GPU需要将每一个frame的纹理合成在一起。每一个纹理会占用VRAM,
CPU开始程序时,会让CPU从bundle加载一张PNG的图片并且解压它,这所有的事情都在CPU上。在显示文本时,会促进CoreText和CoreGrapic生成一个位图(coreText排版器最终也是要绘制到图片上下文),一旦准备好,它将会被作为一个纹理上传到GPU并显示出来。但滚动或者在屏幕上移动文本时,不管怎样,同样的纹理会能够复用,CPU只需要简单告诉GPU新的位置就可以,所有GPU可以重用存在的纹理,CPU并不需要重新渲染文本,并且位图也不需要重新上传到GPU。

三. 纹理合成

在简化的理解中,纹理相当于CoreAnimatio里的CALayer,纹理可以有位图内容,其实CALayer也是有位图内容的,对于每一个纹理,所有的纹理都以某种方式叠加在彼此的顶部。当两个纹理覆盖在一起时候,GPU要为所有像素做合成操作。

  • 不透明的合成
    在不透明时,即opaque=yes时,不用合成,直接取上面的纹理。这样就减少了合成的时间。(这也就是为什么在做性能优化时,减少层次关系,减少不必要的透明)

  • mask合成
    一个图层可以有一个和它相关联的 mask(蒙板),mask 是一个拥有 alpha 值的位图,当像素要和它下面包含的像素合并之前都会把 mask 应用到图层的像素上去。当你要设置一个图层的圆角半径时,你可以有效的在图层上面设置一个 mask。但是也可以指定任意一个蒙板。比如,一个字母 A 形状的 mask。最终只有在 mask 中显示出来的(即图层中的部分)才会被渲染出来。
    所谓的mask就是一mask的不透明区域,显示本身的内容。实际上mask也是一种合成。

3.1 离屏渲染

  • 帧缓冲区
    屏幕缓存区,在屏幕上

  • 屏幕外缓冲区
    屏幕外缓冲区

  • 哪些情况会默认会强制进行离屏渲染?
    CoreAnimation为了应用mask会强制进行屏幕外渲染。
    CoreAnimation设置圆角半径会进行屏幕外渲染
    CoreAnimation设置阴影也会出现屏幕外渲染
    设置层为光栅化layer.shouldRasterize = yes
    (特别说明下,rasterize是图层的光栅化,会造成离屏渲染)

  • 离屏渲染的性能取舍?
    一般情况下需要避免离屏渲染,因为这是很大的消耗。直接将图层合成到帧的缓冲区(屏幕上)比先创建屏幕外缓冲区,然后将屏幕外缓冲区内容般到帧缓冲区要廉价很多。但有时候需要渲染树很复杂,可以强制离屏渲染那些图层,这样就可以缓存合成的结果。性能就会有所提升。(当使用离屏渲染时,GPU第一次会混合所有图层到一个基于新的纹理的位图缓存上,然后使用这个纹理来绘制到屏幕上)。当对这个纹理进行移动,变形等操作时,可以使用这个位图缓存。这样这部分的合成将减少很多工作。其实这就是做动画的流畅的原因。所以要不要离屏渲染,要看有取舍,一般对于静态的不经常变更的可以使用离屏,增加缓存。对于经常变更的就最好不要使用离屏。这样会增加建立屏幕外缓冲区的时间,以及屏幕内与屏幕外的切换时间。

3.2离屏渲染检测

Instrument的CoreAnimationg工具,Color Offscreen-Rendered Yellow,是检测离屏渲染。Color Hits Green and Misses Red 选项,绿色代表无论何时一个屏幕外缓冲区被复用,而红色代表当缓冲区被重新创建。

四. CoreAnimation与CoreGraphics与OpenGLES

CoreAnimation利用CoreGraphics绘制,CoreGraphics利用OpenGLES实现绘制。。

OpenGLES做的就是将纹理合并,做些另外的操作,比如mask,阴影等。OpenGLES对这些有层次关系的纹理进行合成,而这些具体的操作是通过GPU来实现的(也就是通过OpenGLES来操作GPU,OpenGLES只是编程接口)。

CoreAnimation重要的任务是判断出哪些图层需要被重新绘制,绘制完成后会有生成bitmap,CoreAnimion里的图层有backsore,就是一这个bitmap;这个bitmap可以是读取的图片数据,也可以是利用CoreGrapics绘制的。无论是给的图片还是自己通过CrorCrapics绘制的最终,提交把这个位图数据交个生成的纹理(这个纹理与这个Layer相对应)。

五. CPU瓶颈与GPU瓶颈优化

要在1/60里完成渲染工作,CPU与GPU的总时间不能操作这个时间。否则就会出现掉帧。
在出现性能瓶颈时, 我们采用Instrument里的,OpenGL ES Driver instrument 进行查看。

六. CoreGraphics

CPU实现的是位图绘制,这个绘制过程是用CoreGraphics完成的,也就是通过CPU进行计算而来。我们自己画的线条,长方形,通过CPU计算,最终将数据形成与CGContext里。
当渲染系统准备好,它会调用视图图层的-display方法.此时,图层会装配它的后备存储。然后建立一个 Core Graphics 上下文(CGContextRef),将后备存储对应内存中的数据恢复出来,绘图会进入对应的内存区域,并使用CGContextRef 绘制

  • UIKit 版本的代码为何不传入一个上下文参数到方法中?
    这是因为当使用 UIKit 或者 AppKit 时,上下文是唯一的。UIkit 维护着一个上下文堆栈,UIKit 方法总是绘制到最顶层的上下文中。UIGraphicsGetCurrentContext() 来得到最顶层的上下文。你可以使用 UIGraphicsPushContext() 和 UIGraphicsPopContext() 在 UIKit 的堆栈中推进或取出上下文。

  • 自己创建一个位图上下文
    自己创建的CGContext,那么绘制的数据在这个自己创建的CGContext里,可以用这个CGGcontext形成位图或者图片。(这里可以自己绘制然后生成图片,也可以实现异步绘制,哈哈)

6.1 drawRect原理

当你调用 -setNeedsDisplay,UIKit 将会在这个视图的图层上调用 -setNeedsDisplay。这为图层设置了一个标识,标记为 dirty,但还显示原来的内容。它实际上没做任何工作,所以多次调用 -setNeedsDisplay并不会造成性能损失。当渲染系统准备好,它会调用视图图层的-display方法.此时,图层会装配它的后备存储。然后建立一个 Core Graphics 上下文(CGContextRef),将后备存储对应内存中的数据恢复出来,绘图会进入对应的内存区域,并使用 CGContextRef 绘制
当你使用 UIKit 的绘制方法,例如: UIRectFill() 或者 -[UIBezierPath fill] 代替你的 -drawRect: 方法,他们将会使用这个上下文。使用方法是,UIKit 将后备存储的 CGContextRef 推进他的 graphics context stack,也就是说,它会将那个上下文设置为当前的。因此 UIGraphicsGetCurrent() 将会返回那个对应的上下文。既然 UIKit 使用 UIGraphicsGetCurrent() 绘制方法,绘图将会进入到图层的后备存储。如果你想直接使用 Core Graphics 方法,你可以自己调用 UIGraphicsGetCurrent() 得到相同的上下文,并且将这个上下文传给 Core Graphics 方法。

总结下:检测是否需要重绘,需要重绘,若果实现了drawRect就会生成一个相应大小的后备存储。然后调用drawRect里的代码进行绘制。将数据写入上下文,幷形成新的后备存储。幷将新的后备存储交给GPU渲染。(这里如果不需要绘制,就不要重写drawRect方法了,这样就不会生成后没有必要的备存储对象了,以免造成性能的浪费),幷切每次出现重绘时,都会执行drawRect方法。造成时间和内存浪费。可以采用自己创建位图上下文,生成图片后,赋值给视图。这样可以避免不断绘制。

6.2 异步绘制

将一些耗时的工作,避过图片的获取,图片的解码等工作放到子线程中去做,形成图片后,在放到主线程里将图片放进去。

UIImageView *view; // assume we have this
NSOperationQueue *renderQueue; // assume we have this
CGSize size = view.bounds.size;
[renderQueue addOperationWithBlock:^(){
UIImage *image = [renderer renderInImageOfSize:size];
[[NSOperationQueue mainQueue] addOperationWithBlock:^(){
view.image = image;
}];
}];

6.3 图片解码

你需要知道在 GPU 内,一个 CALayer 在某种方式上和一个纹理类似。图层有一个后备存储,这便是被用来绘制到屏幕上的位图。
在给CALayer设置图片时,CoreAnimation会将其查看这个图片时否已经解码,没解码的化话进行解码(这里就是一个优化点,可以将解码的工作全部放到子线程里进行,哈哈)。
imageNamed:从 bundle 里加载会立马解压。一般的情况是在赋值给 UIImageView 的 image 或者 layer 的 contents 或者画到一个 core graphic context 里才会解压。

6.4 可变尺寸的图片

使用较小的图片好处
解码快
占用内存小

七. 完整的绘制与动画流程

动画在APP内部的4个阶段

  1. 布局:
    在这个阶段,程序设置 View/Layer 的层级信息,设置 layer 的属性,如 frame,background color 等等。

  2. 创建 backing image:在这个阶段程序会创建 layer 的 backing image,无论是通过 setContents 将一个 image 传給 layer,还是通过 drawRect:或 drawLayer:inContext:来画出来的。所以 drawRect:等函数是在这个阶段被调用的。

  3. 准备:在这个阶段,Core Animation 框架准备要渲染的 layer 的各种属性数据,以及要做的动画的参数,准备传递給 render server。同时在这个阶段也会解压要渲染的 image。(除了用 imageNamed:方法从 bundle 加载的 image 会立刻解压之外,其他的比如直接从硬盘读入,或者从网络上下载的 image 不会立刻解压,只有在真正要渲染的时候才会解压)。

  4. 提交:在这个阶段,Core Animation 打包 layer 的信息以及需要做的动画的参数,通过 IPC(inter-Process Communication)传递給 render server。

动画在APP外部的2个阶段

当这些数据到达 render server 后,会被反序列化成 render tree。然后 render server 会做下面的两件事:

  • 根据 layer 的各种属性(如果是动画的,会计算动画 layer 的属性的中间值),用 OpenGL 准备渲染。

  • 渲染这些可视的 layer 到屏幕。

如果做动画的话,最后的两个步骤会一直重复知道动画结束。

八. 渲染性能优化的总结

  • 隐藏的绘制
    UILabel将text画入backing image。也就是将文字搞成相对应的图片(文字最终都会是图片)。如果改了一个包含 text 的 view 的 frame 的话,text 会被重新绘制。

  • Rasterize
    当使用layer的shouldRasterize的时候,layer会被强制绘制到一个offscreen image上,并且会被缓存起来。这种方法可以在比较复杂的不会变化的图层上。

  • 离屏绘制
    使用 Rounded corner, layer masks, drop shadows 的效果可以使用 stretchable images。比如实现 rounded corner,可以将一个圆形的图片赋值于 layer 的 content 的属性。并且设置好 contentsCenter 和 contentScale 属性。

  • Blending
    如果一个 layer 被另一个 layer 完全遮盖,GPU 会做优化不渲染被遮盖的 layer,但是计算一个 layer 是否被另一个 layer 完全遮盖是很耗 cpu 的。将几个半透明的 layer 的 color 融合在一起也是很消耗的。

  • opaque
    减少透明,减少合成时间

  • drawRect
    没有必要不要在drawRect里实现。可以采用异步绘制。

  • 图片解码
    图片解码可以异步进行,不要在设置图片的时候解码

2018/9/7 posted in  iOS性能优化

iOS保持界面流畅的技巧(性能优化)

参考iOS 性能优化总结
参考微信读书 iOS 性能优化总结
参考iOS实时卡顿监控
参考移动端IM实践:iOS版微信界面卡顿监测方案
参考iOS保持界面流畅的技巧

本文是对上面几篇文章的总结。并与我自己的知识体系相融合,以达到一个自相融洽的整体。

卡顿原理

VSync信号到来时,系统图形服务会通过CADisplayLink等机制通知App,APP主线程开始在CPU中计算显示内容,比如视图的创建,布局计算,图片解码,文本绘制等。随后CPU会将计算好的内容提交到GPU,由GPU进行变换,合成,渲染。随后GPU会把渲染结果提交到帧缓冲区区,等待下一次VSync信号到来时显示到屏幕上。
由于垂直同步机制,若果在一个VSync时间内,CPU或者GPU没有完成内容的提交,则这个没有完成的提交不会显示到屏幕,在其提交后会到缓冲区,等待下一次机会在显示。那么这个下一次有可能是正常时间提交修改,则会将缓存区的内容覆盖。导致被覆盖的永远没机会显示了。就是掉帧了。
那么与runloop有什么关系呢?
Runloop的时间概念比1/60更小,也就是Runloop处理事物的时间远远比1/60要小。绝大部分时间是睡眠的。所以它们两个本身是没有关系的。绝大部分时候是主线程早就处理完毕所有显示数据,并提交到了渲染系统,渲染系统也完成了合成,也就是提交到了缓存区。只需要等待时钟的到来,将这个缓冲区的内容显示到屏幕上。

FPS的真正含义是,1s的时间真正有多少帧显示到了屏幕上。

卡顿监控思路

  1. 主线程卡顿监控
    通过子线程监测主线程的runloop,判断两个状态区域之间的耗时是否达到一定阈值。这两个状态就是kCFRunLoopBeforeSources与kCFRunLoopAfterWaiting

  2. 借助FPS监控
    CADisplayLink 可以将其看做一个定时器。定时器都与机器的硬件有关系。而这个定时器是有屏幕“垂直时钟”驱动。也就是与垂直时钟同步,1/60时间跳动一次。因为CADisplayLink的回调也要在线程里执行,将其加入到主线程的Runloop里。runloop的TimerSource就会触发runloop的“叫醒”,执行该执行的内容。若果不将CADisplayLink加入,当然就不会定固定时间“叫醒”。

CADislayLink的timestampe的两次差为1s之内的tick次数,即使FPS。

线程卡顿监控方案一的实现

iOS实时卡顿检测
这一方案的思路,就是从引起卡顿的本质来优化。引起卡顿,在kCFRunLoopBeforeSources通知后执行主队列里的代码,执行block的代码等。或则在kCFRunLoopAfterWaiting被唤起后,也会执行队列里的代码,block代码。

#import <CrashReporter/CrashReporter.h>

@interface PerformanceMonitor ()
{
    int timeoutCount;
    CFRunLoopObserverRef observer;
    
    @public
    dispatch_semaphore_t semaphore;
    CFRunLoopActivity activity;
}
@end

@implementation PerformanceMonitor

+ (instancetype)sharedInstance
{
    static id instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[self alloc] init];
    });
    return instance;
}

static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
    PerformanceMonitor *moniotr = (__bridge PerformanceMonitor*)info;
    
    moniotr->activity = activity;
    
    dispatch_semaphore_t semaphore = moniotr->semaphore;
    dispatch_semaphore_signal(semaphore);//没当状态发生变化的时候+1;这是在主线程执行
}

- (void)stop
{
    if (!observer)
        return;
    
    CFRunLoopRemoveObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    CFRelease(observer);
    observer = NULL;
}

- (void)start
{
    if (observer)
        return;
    
    // 信号
    semaphore = dispatch_semaphore_create(0);
    
    // 注册RunLoop状态观察
    CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
    observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                       kCFRunLoopAllActivities,
                                       YES,
                                       0,
                                       &runLoopObserverCallBack,
                                       &context);
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    
    //在子线程监控时长
    //在子线程不断的监控信号量,如果在50 ms里还没有超时,也就是在50ms状态没有发生该边的话。就说明在两个状态的时间间隔里有执行时长超过50ms的
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        while (YES)
        {
            long st = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));
            if (st != 0)
            {
                if (!observer)
                {
                    timeoutCount = 0;
                    semaphore = 0;
                    activity = 0;
                    return;
                }
                
                //若果超时的是kCFRunLoopBeforeSources 或者kCFRunLoopAfterWaiting 就更能说明问题
                //如果5此都是这样,说明有问题。几下主线程的调用堆栈。
                if (activity==kCFRunLoopBeforeSources || activity==kCFRunLoopAfterWaiting)
                {
                    if (++timeoutCount < 5)
                        continue;
                    
                    PLCrashReporterConfig *config = [[PLCrashReporterConfig alloc] initWithSignalHandlerType:PLCrashReporterSignalHandlerTypeBSD
                                                                                       symbolicationStrategy:PLCrashReporterSymbolicationStrategyAll];
                    PLCrashReporter *crashReporter = [[PLCrashReporter alloc] initWithConfiguration:config];
                    
                    NSData *data = [crashReporter generateLiveReport];
                    PLCrashReport *reporter = [[PLCrashReport alloc] initWithData:data error:NULL];
                    NSString *report = [PLCrashReportTextFormatter stringValueForCrashReport:reporter
                                                                              withTextFormat:PLCrashReportTextFormatiOS];
                    
                    NSLog(@"------------\n%@\n------------", report);
                }
            }
            timeoutCount = 0;
        }
    });
}

@end
2018/8/25 posted in  iOS性能优化

组件化系列(一) 组件化原理及方案


本系列文章是对大神的博客文章的研读与总结。我们公司从18年初开始组件化的开发的构建。为了理解组件开发的原理,找了相关资料做了对比分析。以下是我参考的文章
模块化与解耦
Casa的iOS应用架构谈 组件化方案
蘑菇街的开源
Casa的组件化方案开源

一:模块化与解耦

模块化与解耦

1.为什么模块化

因为在实际的开发中,项目业务较多,一个APP会有多个小组进行开发,比如我们公司的有数学组,语文组,英语组,商业组,等,出现的问题大多数情况下一个开发人员只关心我这个组的代码。这样在编译时实际上是编译整个项目,编译效率低。每个小组在同一个工程里增,删,改文件,xcode的工程文件会经常发生冲突(我们用git进行版本控制),合并代码时很痛苦。在整个项目查找自己忘记类名了的功能时,犹如大海捞针。有些基础模块核心模块需要专人维护,对基础的开发人员不开发,需要隔离基础库,也需要进行解耦。

这些理由已经足以说明进行模块化组件化的迫切了。

2. 模块设计原则

  1. 越底层的模块,应该越稳定,越抽象,越具有高度复用度。
  2. 不要让稳定的模块依赖不稳定的模块,减少依赖
  3. 提升模块的复用度,自完备性有时候要优于代码复用
  4. 每个模块只做好一件事情,不要让Common出现
  5. 按照你架构的层数从上到下依赖,不要出现下层模块依赖上层模块的现象,业务模块之间也尽量不要耦合

3. 模块解耦手段

要实现模块之间真正的解耦才算真正的模块化。
解耦目标:

在基于模块设计原则上让模块之间没有循环依赖,让业务模块之间解除依赖

公共模块可以通过架构设计来避免耦合业务。但业务模块之间还是会有耦合的。
业务模块之间的比如页面跳转,数据传递,怎么实现解耦不同业务模块之间的代码调用呢?

1. 面向接口调用(很妙)

直接引用有依赖的情况

//A 模块
- (void)getSomeDataFromB {
    B.getSomeData();
}
//B 模块
- (void)getSomeData {
    return self.data;
}
//接口
@protocol BService<NSObject>
- (void)getSomeData;
@end

//A 模块,只依赖接口(针对协议编程)
- (void)getSomeDataFromB {
    id b = findService(@protocol(BService));
    b.getSomeData;
}
//B模块,实现BService接口
@interface B:NSObject<BService>
- (void)getSomeData {
    return self.data;
}

这样就可以实现了既满足了模块之间的调用,也实现了解耦
优点

接口类似代码,可以非常灵活的定义函数和回调

缺点

1. 接口定义文件需要放在一个模块里以供依赖,但是这个模块不贡献代码。还好。
2. 使用麻烦,每个调用都需要定义一个service的接口,幷实现。

2. 面向自定义协议的调用

面向接口的调用的缺点导致幷不能满足所有的需求,也解耦的不彻底(对接口的依赖)。
终极手段就是通过定义一套自定义协议来实现模块间的通信,可以采用现成的协议如URL协议,简单,易于上手,这也是很多人采用url作为协议原因。可统一实现本地于远程的页面跳转,并且实现业务间的解耦。
要实现真正的解耦,采用注册机制。

3.利用运行时的反射机制

Object-C的反射机制是通过一个字符串找到一个类的类对象,即NSClassFromString();
要实现真正的解耦,可以采用通过反射机制获取类,在创建对象,实现跳转或者通信。这样就不用依赖”要跳转的类“了。

总结
以上的解耦方式是从模块化与解耦得到的启发。这些解耦方式无论是在平时开发中,还是要搭建组件化的框架都可以使用。具体的组件化框架我们将在下面对前人的组件化探索做下分析

二:模块拆分

  1. 基础库组件
    第三方库如AFNetWorking,SDWebImage等,还有一些工具也要从主库重剥离出来,形成自己的私有基础仓库。

  2. 服务组件

  3. 业务组件

https://blog.csdn.net/xinzhou201/article/details/51000807

  1. 拆分中遇到的问题 主工程与壳工程的pods版本的管理问题

三:蘑菇街组件化

蘑菇街的方案一

原理采用的是面向自定义协议的方式实现解耦。自定义协议采用的是现成的url协议,url协议成熟,方便,利用URL可以在这里做key值,方便的参数解析,与应用之间的调用相吻合。
嫌在这里看代码代码麻烦的话,可以下载我自己写的原理 Demo组件化原理分析

//Mediator3.h文件
typedef void(^componetBlock)(id param);
typedef id(^objectComponetBlock)(id param);

/*
 原理就是将url与Block进行映射
 url起到两个作用:一是作为key值与block进行映射,二是可以直接接参数就想普通url后面跟参数一样。
 
 特别说明:
 url传递参数受到一定的限制。比如对本地来说,需要传非常规的参数时,就办不到。
 这里我自己的Demo直接将参数放在了函数后面省去对url进行解析的操作。实际的开发中将这个参数从url里解析出来。如果要传递非常规的参数。也可以直接在后面添加有一个param。
 */

@interface Mediator3 : NSObject

+ (instancetype)shareInstance;

//指定相应的url的执行操作
//例如,只是简单的打开一个页面
- (void)registerURLPattern:(NSString *)urlPattern toHandler:(componetBlock)block;
//打开某个页面
- (void)openURL:(NSString *)url withParam:(id)param;


//指定相应rul的执行操作,并给一个返回值
//例如打开一个页面,或者在一个组件里面取值后返回来
- (void)registerURLPattern:(NSString *)urlPattern toObjectHandler:(objectComponetBlock)block;
//取得某个组件操作后的值
- (id)objectForURL:(NSString *)url withParam:(id)param;
//Mediator3.m文件
@interface Mediator3()
@property (nonatomic, strong)NSMutableDictionary *cache;
@end

@implementation Mediator3

- (instancetype)init {
    if(self = [super init]){
        _cache = [[NSMutableDictionary alloc]init];
    }
    return self;
}

+ (instancetype)shareInstance {
    static dispatch_once_t onceToken;
    static Mediator3 *mediator = nil;
    dispatch_once(&onceToken, ^{
        
        mediator = [[self alloc]init];
    });
    return mediator;
}

- (void)registerURLPattern:(NSString *)urlPattern toHandler:(componetBlock)block {
    [self.cache setObject:block forKey:urlPattern];
}

- (void)openURL:(NSString *)url withParam:(id)param {
    componetBlock block = [self.cache objectForKey:url];
    if(block){
        block(param);
    }
}

- (void)registerURLPattern:(NSString *)urlPattern toObjectHandler:(objectComponetBlock)block {
    [self.cache setObject:block forKey:urlPattern];
}

- (id)objectForURL:(NSString *)url withParam:(id)param{
    objectComponetBlock block = [self.cache objectForKey:url];
    if(block){
        return block(param);
    }
    return nil;
}

@end
//BookDetailViewController.m
@implementation BookDetailViewController

+ (void)load {
    //实现跳转功能
    [[Mediator3 shareInstance] registerURLPattern:@"weread://bookDetail" toHandler:^(id param) {
        NSDictionary *paramDict = (NSDictionary *)param;
        BookDetailViewController *bookDetailVC = [[BookDetailViewController alloc]initWithBookId:paramDict[@"bookId"]];
        UINavigationController *nav = (UINavigationController *) [UIApplication sharedApplication].keyWindow.rootViewController;
        [nav pushViewController:bookDetailVC animated:YES];
    }];
    
    
    //只是取值的操作
    [[Mediator3 shareInstance]registerURLPattern:@"weread://bookCount" toObjectHandler:^id(id param) {
        //执行一定的操作后
        return @"5";
    }];
    
//ReadingViewController.m
- (void)viewDidLoad {
 [super viewDidLoad];
 //注册url与block的映射,实现调转的操作
    [[Mediator3 shareInstance] openURL:@"weread://bookDetail" withParam:@{@"bookId":@"2"}];
    
    //注册url与block映射,实现只是取值的操作
    NSString *bookCount =  [[Mediator3 shareInstance] objectForURL:@"weread://bookCount" withParam:@""];
    NSLog(@"这是从某个组件取回来的值%@",bookCount);
    }

蘑菇街的方案二

实现原理:
面向接口调用,即新开了一个对象叫做ModuleManager,提供了一个registerClass:forProtocol:的方法,注册后,@protocol和Class进行配对。因此ModuleManager中就有了一个字典来记录这个配对。
当有涉及非常规参数的调用时,业务方就不会去使用[MGJRouter openURL:@"mgj://detail?id=404"]的方案了,转而采用ModuleManager的classForProtocol:方法。业务传入一个@protocol给ModuleManager,然后ModuleManager通过之前注册过的字典查找到对应的Class返回给业务方,然后业务方再自己执行alloc和init方法得到一个符合刚才传入@protocol的对象,然后再执行相应的逻辑。

这里的protocol统样起到两个作用,一是key值,另一个是起到定义调用接口的作用,可以定义任意类型的参数。

缺点
1. 被调用方与调用方,虽然不相互依赖,但都得依赖这个协议。这实际上是一种不彻底的解耦。

  1. 同url注册形式一样,都得维持注册表。

四:casa组件化

基于Mediator模式和Target-Action模式,中间采用了runtime来完成调用。这套组件化方案将远程应用调用和本地应用调用做了拆分,而且是由本地应用调用为远程应用调用提供服务

实现原理
[[CTMediator sharedInstance] performTarget:targetName action:actionName params:@{...}]向CTMediator发起跨组件调用,CTMediator根据获得的target和action信息,通过objective-C的runtime转化生成target实例以及对应的action选择子,然后最终调用到目标业务提供的逻辑,完成需求。

Xnip2018-09-04_16-42-52

下面是工程实践
下面是casa开源的实现的头文件。.m文件请自行下载

//casa开源的CTMeditor.h
@interface CTMediator : NSObject

+ (instancetype)sharedInstance;

// 远程App调用入口
- (id)performActionWithUrl:(NSURL *)url completion:(void(^)(NSDictionary *info))completion;
// 本地组件调用入口
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget;
- (void)releaseCachedTargetWithTargetName:(NSString *)targetName;

@end

//CTMeditor.m
略
采用运行时构建可执行的NSInvocation。在内部都给其添加了前缀
 NSString * targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
 NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
//Target_B.h
@interface Target_B : NSObject

- (UIViewController *)Action_DemoModuleBDetailViewController:(NSDictionary *)dict;

@end

//Target_B.m
#import "Target_B.h"
#import "DemoModuleBDetailViewController.h"
@implementation Target_B

- (UIViewController *)Action_DemoModuleBDetailViewController:(NSDictionary *)param {
    DemoModuleBDetailViewController *bDetailVC = [[DemoModuleBDetailViewController alloc]init];
    return bDetailVC;
}
@end
//ViewController.m
//这是调用方
#import "CTMediator+CTMediatorModuleAActions.h"
#import "CTMediator+CTMediatorModuleBActions.h"
- (void)viewDidLoad {
//直接调用
  if (indexPath.row == 6) {
        [[CTMediator sharedInstance] performTarget:@"InvalidTarget" action:@"InvalidAction" params:nil shouldCacheTarget:NO];
    }
    //通过分类调用
    if(indexPath.row == 7){
        UIViewController *vc =  [[CTMediator sharedInstance]CTMediator_viewControllerForModuleBDetail:@{}];
        [self.navigationController pushViewController:vc animated:YES];
    }
}
  1. 从.h文件里可以看到,外部的调用将远程与本地分开,内部实现时远程利用了本地(通过解析url,将url转换成了本地的调用)。

  2. Mediator分别对每一个模块有个一个分类,提供对外部的调用的列表。这些分类被需要调用的模块所依赖。也就是只需要依赖Mediator就可以了,是单向依赖。

  3. 为了更好的实现组件对外接口的管理。此种方案专门针对每个模块有一个Target_A类似的对外服务接口的实现。

  4. 用户调用都是通过对Mediator的分类,对固定的模块的类的名字的反射,来对Target_A的调用,当然就调用到了A的服务。

  5. 此种方式为了使代码方便管理,会为每个模块提供Target和一个对Mediator的分类。

  6. Mediator与其分类可以是单独一个repo,方便其他组件依赖。也就是其他组件只依赖于这个中间件。解耦与组件化就完成了。

五:总结

组件化就是在与解耦,解耦的方式大致就是上面提到的三种方法(也可能有其他办法,但至少现在我看到的最好实践就这三)。然后是基于各个原理的工程化实践。从工程实践来看casa的Mediator+target-action更胜一筹。思路清晰,调用统一,没有注册机制的维护,模块的服务的实现(Target)在同一个地方,不用耦合到真正的模块里。
多说一句,滴滴组件化,页面间的跳转采用openURL,页面在+(void)load方法里进行注册,ONERoute内部保存一份URL与Class的对应表。当调用openURL时,会查找到相应的类,然后生成相应的实例对象。
最后强烈建议大家认真读Casa的iOS应用架构谈 组件化方案

2018/8/25 posted in  iOS性能优化

iOS APP启动如何优化

参考文章
iOS App 启动性能优化

iOS APP启动如何优化

App总的启动时间 = main()之前加载装载时间 + didFinishLaunchingWithOptions()之后时间

  • didFinishLaunchingWithOptions之后
    在didFinishLaunching时间后,我们有SDK的初始化,视图控制器的创建。网络请求,配置等信息。可以将这个进行类,并分别时间进行配置

    • 日志,统计可以继续放在最先配置
    • 推送,即是通讯,项目配置
    • 其他sdk的初始化

    一点:日志,统计配置先启动,其次是推送,环境配置,项目配置,可以放在加载广告时进行处理,最后是一般SDK的配置,可以在第一个页面渲染完成后进行配置。

    二点:可以将网络请求放在didAppear里进行,并将数据解析渲染等操作,放在didAppear里进行。首页面给一个缓存的数据先显示出来。
    总的思路:1.didFinishLaunchingWithOptions里的配置操作 2.减少第一个页面的渲染时间。

  • main之前的加载时间

动态库加载越多,启动越慢(因为在启动时,需要将编译时依赖的动态库的函数真正绑定)
Objc类越多,启动越慢(因为要解析更多的二进制数据)
静态数据越多,启动越慢
+load越多,启动越慢

Total pre-main time: 1.7 seconds (100.0%)
         dylib loading time: 252.96 milliseconds (14.7%)
        rebase/binding time: 939.94 milliseconds (54.7%)
            ObjC setup time: 113.02 milliseconds (6.5%)
           initializer time: 410.92 milliseconds (23.9%)

优化手段
移出不需要的动态库
移出不需要用到的类
合并功能类似的类和扩展(一般不做优化,对程序员成本太高)
减少+load方法里执行耗时的操作,可以用dispatch_once来替代的只执行一次的方案
减少图片大小

2018/8/19 posted in  iOS性能优化