美文网首页timing技术团队
iOS页面流畅度优化 代码解析

iOS页面流畅度优化 代码解析

作者: 曾柏超 | 来源:发表于2018-10-20 10:27 被阅读99次

    微博 Demo 性能优化技巧
    预排版
    预渲染
    异步绘制
    全局并发控制
    更高效的异步图片加载
    其他可以改进的地方
    如何评测界面的流畅度

    高效布局文本和图片

    YYText框架

    YYText框架根据文字的内容、字体大小 去计算文本行高的思想是很值得学习的 ,在页面布局复杂的时候 比masonry 好用很多,能够大幅度减少卡顿。 YYText框架 主要是 利用了 YYTextContainer TextLinePositionModifier 这几个处理文本的类。
    比如下面的代码

    _textLabel.textLayout = feed.layout.textLayout;
    

    YYControl

    YYControl是更轻量级 代替UIImageView 的控件

    #import <UIKit/UIKit.h>
    #import "YYKit.h"
    
    @interface YYControl : UIView
    @property (nonatomic, strong) UIImage *image;
    @property (nonatomic, copy) void (^touchBlock)(YYControl *view, YYGestureRecognizerState state, NSSet *touches, UIEvent *event);
    @property (nonatomic, copy) void (^longPressBlock)(YYControl *view, CGPoint point);
    @end
    
    

    CALayer (YYWebImage)

    YYWebImage 是CALayer 高效异步下载图片的分类

    下面开始详细介绍使用方法

    1.手动布局
    在网络请求获取到数据的时候去计算布局layout

    
              for (WBStatus *status in item.statuses) {
                    WBStatusLayout *layout = [[WBStatusLayout alloc] initWithStatus:status style:WBLayoutStyleTimeline];
                    [_layouts addObject:layout];
                }
    
    

    2.在layout中 通过具体的数据内容去计算各种文本和图片的位置布局

    
    @implementation WBStatusLayout
    
    - (instancetype)initWithStatus:(WBStatus *)status style:(WBLayoutStyle)style {
        if (!status || !status.user) return nil;
        self = [super init];
        _status = status;
        _style = style;
        [self layout];
        return self;
    }
    
    - (void)_layout {
        
        _marginTop = kWBCellTopMargin;
        _titleHeight = 0;
        _profileHeight = 0;
        _textHeight = 0;
        _retweetHeight = 0;
        _retweetTextHeight = 0;
        _retweetPicHeight = 0;
        _retweetCardHeight = 0;
        _picHeight = 0;
        _cardHeight = 0;
        _toolbarHeight = kWBCellToolbarHeight;
        _marginBottom = kWBCellToolbarBottomMargin;
        
        
        // 文本排版,计算布局
        [self _layoutTitle];
        [self _layoutProfile];
        [self _layoutRetweet];
        if (_retweetHeight == 0) {
            [self _layoutPics];
            if (_picHeight == 0) {
                [self _layoutCard];
            }
        }
        [self _layoutText];
        [self _layoutTag];
        [self _layoutToolbar];
        
        // 计算高度
        _height = 0;
        _height += _marginTop;
        _height += _titleHeight;
        _height += _profileHeight;
        _height += _textHeight;
        if (_retweetHeight > 0) {
            _height += _retweetHeight;
        } else if (_picHeight > 0) {
            _height += _picHeight;
        } else if (_cardHeight > 0) {
            _height += _cardHeight;
        }
        if (_tagHeight > 0) {
            _height += _tagHeight;
        } else {
            if (_picHeight > 0 || _cardHeight > 0) {
                _height += kWBCellPadding;
            }
        }
        _height += _toolbarHeight;
        _height += _marginBottom;
    }
    
    /// 文本
    - (void)_layoutText {
        _textHeight = 0;
        _textLayout = nil;
        
        NSMutableAttributedString *text = [self _textWithStatus:_status
                                                      isRetweet:NO
                                                       fontSize:kWBCellTextFontSize
                                                      textColor:kWBCellTextNormalColor];
        if (text.length == 0) return;
        
        WBTextLinePositionModifier *modifier = [WBTextLinePositionModifier new];
        modifier.font = [UIFont fontWithName:@"Heiti SC" size:kWBCellTextFontSize];
        modifier.paddingTop = kWBCellPaddingText;
        modifier.paddingBottom = kWBCellPaddingText;
        
        YYTextContainer *container = [YYTextContainer new];
        container.size = CGSizeMake(kWBCellContentWidth, HUGE);
        container.linePositionModifier = modifier;
        
        _textLayout = [YYTextLayout layoutWithContainer:container text:text];
        if (!_textLayout) return;
        
        _textHeight = [modifier heightForLineCount:_textLayout.rowCount];
    }
    
    - (void)_layoutPicsWithStatus:(WBStatus *)status isRetweet:(BOOL)isRetweet {
        if (isRetweet) {
            _retweetPicSize = CGSizeZero;
            _retweetPicHeight = 0;
        } else {
            _picSize = CGSizeZero;
            _picHeight = 0;
        }
        if (status.pics.count == 0) return;
        
        CGSize picSize = CGSizeZero;
        CGFloat picHeight = 0;
        
        CGFloat len1_3 = (kWBCellContentWidth + kWBCellPaddingPic) / 3 - kWBCellPaddingPic;
        len1_3 = CGFloatPixelRound(len1_3);
        switch (status.pics.count) {
            case 1: {
                WBPicture *pic = _status.pics.firstObject;
                WBPictureMetadata *bmiddle = pic.bmiddle;
                if (pic.keepSize || bmiddle.width < 1 || bmiddle.height < 1) {
                    CGFloat maxLen = kWBCellContentWidth / 2.0;
                    maxLen = CGFloatPixelRound(maxLen);
                    picSize = CGSizeMake(maxLen, maxLen);
                    picHeight = maxLen;
                } else {
                    CGFloat maxLen = len1_3 * 2 + kWBCellPaddingPic;
                    if (bmiddle.width < bmiddle.height) {
                        picSize.width = (float)bmiddle.width / (float)bmiddle.height * maxLen;
                        picSize.height = maxLen;
                    } else {
                        picSize.width = maxLen;
                        picSize.height = (float)bmiddle.height / (float)bmiddle.width * maxLen;
                    }
                    picSize = CGSizePixelRound(picSize);
                    picHeight = picSize.height;
                }
            } break;
            case 2: case 3: {
                picSize = CGSizeMake(len1_3, len1_3);
                picHeight = len1_3;
            } break;
            case 4: case 5: case 6: {
                picSize = CGSizeMake(len1_3, len1_3);
                picHeight = len1_3 * 2 + kWBCellPaddingPic;
            } break;
            default: { // 7, 8, 9
                picSize = CGSizeMake(len1_3, len1_3);
                picHeight = len1_3 * 3 + kWBCellPaddingPic * 2;
            } break;
        }
        
        if (isRetweet) {
            _retweetPicSize = picSize;
            _retweetPicHeight = picHeight;
        } else {
            _picSize = picSize;
            _picHeight = picHeight;
        }
    }
    
    
    1. WBStatusView 初始化的创建
    @implementation WBStatusView {
        BOOL _touchRetweetView;
    }
    
    - (instancetype)initWithFrame:(CGRect)frame {
        if (frame.size.width == 0 && frame.size.height == 0) {
            frame.size.width = kScreenWidth;
            frame.size.height = 1;
        }
        self = [super initWithFrame:frame];
        self.backgroundColor = [UIColor clearColor];
        self.exclusiveTouch = YES;
        @weakify(self);
        
        _contentView = [UIView new];
        _contentView.width = kScreenWidth;
        _contentView.height = 1;
        _contentView.backgroundColor = [UIColor whiteColor];
    
    
        _profileView = [WBStatusProfileView new];
        [_contentView addSubview:_profileView];
        
    
    
        
        _textLabel = [YYLabel new];
        _textLabel.left = kWBCellPadding;
        _textLabel.width = kWBCellContentWidth;
        _textLabel.textVerticalAlignment = YYTextVerticalAlignmentTop;
        _textLabel.displaysAsynchronously = YES;
        _textLabel.ignoreCommonProperties = YES;
        _textLabel.fadeOnAsynchronouslyDisplay = NO;
        _textLabel.fadeOnHighlight = NO;
        _textLabel.highlightTapAction = ^(UIView *containerView, NSAttributedString *text, NSRange range, CGRect rect) {
            if ([weak_self.cell.delegate respondsToSelector:@selector(cell:didClickInLabel:textRange:)]) {
                [weak_self.cell.delegate cell:weak_self.cell didClickInLabel:(YYLabel *)containerView textRange:range];
            }
        };
        [_contentView addSubview:_textLabel];
        
        NSMutableArray *picViews = [NSMutableArray new];
        for (int i = 0; i < 9; i++) {
            YYControl *imageView = [YYControl new];
            imageView.size = CGSizeMake(100, 100);
            imageView.hidden = YES;
            imageView.clipsToBounds = YES;
            imageView.backgroundColor = kWBCellHighlightColor;
            imageView.exclusiveTouch = YES;
            imageView.touchBlock = ^(YYControl *view, YYGestureRecognizerState state, NSSet *touches, UIEvent *event) {
                if (![weak_self.cell.delegate respondsToSelector:@selector(cell:didClickImageAtIndex:)]) return;
                if (state == YYGestureRecognizerStateEnded) {
                    UITouch *touch = touches.anyObject;
                    CGPoint p = [touch locationInView:view];
                    if (CGRectContainsPoint(view.bounds, p)) {
                        [weak_self.cell.delegate cell:weak_self.cell didClickImageAtIndex:i];
                    }
                }
            };
            
            UIView *badge = [UIImageView new];
            badge.userInteractionEnabled = NO;
            badge.contentMode = UIViewContentModeScaleAspectFit;
            badge.size = CGSizeMake(56 / 2, 36 / 2);
            badge.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleLeftMargin;
            badge.right = imageView.width;
            badge.bottom = imageView.height;
            badge.hidden = YES;
            [imageView addSubview:badge];
            
            [picViews addObject:imageView];
            [_contentView addSubview:imageView];
        }
        _picViews = picViews;
        
      
        
        return self;
    }
    
    1. 通过计算好的layout 去摆放各种文字 图片控件
    - (void)setLayout:(WBStatusLayout *)layout {
        _layout = layout;
        
        self.height = layout.height;
        _contentView.top = layout.marginTop;
        _contentView.height = layout.height - layout.marginTop - layout.marginBottom;
        
        _textLabel.top = top;
        _textLabel.height = layout.textHeight;
        _textLabel.textLayout = layout.textLayout;
        top += layout.textHeight;
        
        //优先级是 转发->图片->卡片
        if (layout.retweetHeight > 0) {
            _retweetBackgroundView.top = top;
            _retweetBackgroundView.height = layout.retweetHeight;
            _retweetBackgroundView.hidden = NO;
            
            _retweetTextLabel.top = top;
            _retweetTextLabel.height = layout.retweetTextHeight;
            _retweetTextLabel.textLayout = layout.retweetTextLayout;
            _retweetTextLabel.hidden = NO;
            
            if (layout.retweetPicHeight > 0) {
              //布局图片
                [self _setImageViewWithTop:_retweetTextLabel.bottom isRetweet:YES];
            } else {
                [self _hideImageViews];
                if (layout.retweetCardHeight > 0) {
                    _cardView.top = _retweetTextLabel.bottom;
                    _cardView.hidden = NO;
                    [_cardView setWithLayout:layout isRetweet:YES];
                }
            }
        } else if (layout.picHeight > 0) {
            [self _setImageViewWithTop:top isRetweet:NO];
        } else if (layout.cardHeight > 0) {
            _cardView.top = top;
            _cardView.hidden = NO;
            [_cardView setWithLayout:layout isRetweet:NO];
        }
        
        if (layout.tagHeight > 0) {
            _tagView.hidden = NO;
            [_tagView setWithLayout:layout];
            _tagView.centerY = _contentView.height - kWBCellToolbarHeight - layout.tagHeight / 2;
        } else {
            _tagView.hidden = YES;
        }
        
    
    }
    
    - (void)_hideImageViews {
        for (UIImageView *imageView in _picViews) {
            imageView.hidden = YES;
        }
    }
    
    //布局图片
    - (void)_setImageViewWithTop:(CGFloat)imageTop isRetweet:(BOOL)isRetweet {
        CGSize picSize = isRetweet ? _layout.retweetPicSize : _layout.picSize;
        NSArray *pics = isRetweet ? _layout.status.retweetedStatus.pics : _layout.status.pics;
        int picsCount = (int)pics.count;
        
        for (int i = 0; i < 9; i++) {
            UIView *imageView = _picViews[i];
            if (i >= picsCount) {
                [imageView.layer cancelCurrentImageRequest];
                imageView.hidden = YES;
            } else {
                CGPoint origin = {0};
                switch (picsCount) {
                    case 1: {
                        origin.x = kWBCellPadding;
                        origin.y = imageTop;
                    } break;
                    case 4: {
                        origin.x = kWBCellPadding + (i % 2) * (picSize.width + kWBCellPaddingPic);
                        origin.y = imageTop + (int)(i / 2) * (picSize.height + kWBCellPaddingPic);
                    } break;
                    default: {
                        origin.x = kWBCellPadding + (i % 3) * (picSize.width + kWBCellPaddingPic);
                        origin.y = imageTop + (int)(i / 3) * (picSize.height + kWBCellPaddingPic);
                    } break;
                }
                imageView.frame = (CGRect){.origin = origin, .size = picSize};
                imageView.hidden = NO;
                [imageView.layer removeAnimationForKey:@"contents"];
                WBPicture *pic = pics[i];
                
                UIView *badge = imageView.subviews.firstObject;
                switch (pic.largest.badgeType) {
                    case WBPictureBadgeTypeNone: {
                        if (badge.layer.contents) {
                            badge.layer.contents = nil;
                            badge.hidden = YES;
                        }
                    } break;
                    case WBPictureBadgeTypeLong: {
                        badge.layer.contents = (__bridge id)([WBStatusHelper imageNamed:@"timeline_image_longimage"].CGImage);
                        badge.hidden = NO;
                    } break;
                    case WBPictureBadgeTypeGIF: {
                        badge.layer.contents = (__bridge id)([WBStatusHelper imageNamed:@"timeline_image_gif"].CGImage);
                        badge.hidden = NO;
                    } break;
                }
                
                @weakify(imageView);
                [imageView.layer setImageWithURL:pic.bmiddle.url
                                     placeholder:nil
                                         options:YYWebImageOptionAvoidSetImage
                                      completion:^(UIImage *image, NSURL *url, YYWebImageFromType from, YYWebImageStage stage, NSError *error) {
                    @strongify(imageView);
                    if (!imageView) return;
                    if (image && stage == YYWebImageStageFinished) {
                        int width = pic.bmiddle.width;
                        int height = pic.bmiddle.height;
                        CGFloat scale = (height / width) / (imageView.height / imageView.width);
                        if (scale < 0.99 || isnan(scale)) { // 宽图把左右两边裁掉
                            imageView.contentMode = UIViewContentModeScaleAspectFill;
                            imageView.layer.contentsRect = CGRectMake(0, 0, 1, 1);
                        } else { // 高图只保留顶部
                            imageView.contentMode = UIViewContentModeScaleToFill;
                            imageView.layer.contentsRect = CGRectMake(0, 0, 1, (float)width / height);
                        }
                        ((YYControl *)imageView).image = image;
                        if (from != YYWebImageFromMemoryCacheFast) {
                            CATransition *transition = [CATransition animation];
                            transition.duration = 0.15;
                            transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
                            transition.type = kCATransitionFade;
                            [imageView.layer addAnimation:transition forKey:@"contents"];
                        }
                    }
                }];
            }
        }
    }
    
    

    YYTextContainer TextLinePositionModifier 如何计算文本布局的内部实现大家可以自己学习一下

    参考来源

    https://blog.ibireme.com/2015/11/12/smooth_user_interfaces_for_ios/

    相关文章

      网友评论

        本文标题:iOS页面流畅度优化 代码解析

        本文链接:https://www.haomeiwen.com/subject/cjmpzftx.html