background

As the project gets more complex, so does the page, which ultimately leads to performance optimization issues. Performance optimization is also more, we study the optimization point of the project VVeboTableView to learn optimization ideas.

In the end, the project optimization was just a goal. Page fluency reached 60FPS. The project performed well even on low-end devices through optimization, so let’s delve into how it did it.

However, the optimization is generally not early optimization of the big aspects, it is best to stabilize the project, specific for a specific complex did not reach 60FPS page optimization.

The road to explore

Views are not loaded during sliding

First is realized VVeboTableView UIScrollViewDelegate several methods, the key of these methods is the – (void) scrollViewWillEndDragging: (scrollView UIScrollView *) WithVelocity (CGPoint) Velocity targetContentOffset (inout CGPoint *)targetContentOffset This method, the moment we raise our hands, All uITableView cells that need to be loaded on the current page are calculated by the position of the finger at this time. I’m going to add 3 above and 3 below the screen.

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{ [needLoadArr removeAllObjects]; } // Load on demand - If the target row differs from the current row by more than the specified number of rows, only specify 3 rows before and after the target scroll range to load. - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset{ NSIndexPath *ip = [self indexPathForRowAtPoint:CGPointMake(0, targetContentOffset->y)]; NSIndexPath *cip = [[self indexPathsForVisibleRows] firstObject]; NSInteger skipCount = 8; if (labs(cip.row-ip.row)>skipCount) { NSArray *temp = [self indexPathsForRowsInRect:CGRectMake(0, targetContentOffset->y, self.width, self.height)]; NSMutableArray *arr = [NSMutableArray arrayWithArray:temp]; if (velocity.y<0) { NSIndexPath *indexPath = [temp lastObject]; if (indexPath.row+3<datas.count) { [arr addObject:[NSIndexPath indexPathForRow:indexPath.row+1 inSection:0]]; [arr addObject:[NSIndexPath indexPathForRow:indexPath.row+2 inSection:0]]; [arr addObject:[NSIndexPath indexPathForRow:indexPath.row+3 inSection:0]]; } } else { NSIndexPath *indexPath = [temp firstObject]; if (indexPath.row>3) { [arr addObject:[NSIndexPath indexPathForRow:indexPath.row-3 inSection:0]]; [arr addObject:[NSIndexPath indexPathForRow:indexPath.row-2 inSection:0]]; [arr addObject:[NSIndexPath indexPathForRow:indexPath.row-1 inSection:0]]; } } [needLoadArr addObjectsFromArray:arr]; } } - (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView{ scrollToToping = YES; return YES; } - (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView{ NSLog(@"scrollViewDidEndScrollingAnimation\n");  scrollToToping = NO; [self loadContent]; } - (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView{ scrollToToping = NO; [self loadContent]; }Copy the code

The UITableView calls the [self drawCell:cell withIndexPath:indexPath] method to load the cell.

- (void)drawCell:(VVeboTableViewCell *)cell withIndexPath:(NSIndexPath *)indexPath{
    NSDictionary *data = [datas objectAtIndex:indexPath.row];
    cell.selectionStyle = UITableViewCellSelectionStyleNone;
    [cell clear];
    cell.data = data;
    if (needLoadArr.count>0&&[needLoadArr indexOfObject:indexPath]==NSNotFound) {
        [cell clear];
        return;
    }
    if (scrollToToping) {
        return;
    }
    [cell draw];
}
Copy the code

This method checks whether the cell needs to be drawn, and returns blank if not. So we’ll see some blank pages as we swipe

Reduce the level and number of views

- (void)draw{if (drawed) {return; } NSInteger flag = drawColorFlag; drawed = YES; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ CGRect rect = [_data[@"frame"] CGRectValue]; UIGraphicsBeginImageContextWithOptions(rect.size, YES, 0); CGContextRef context = UIGraphicsGetCurrentContext(); [[UIColor colorWithRed:250/255.0 green:250/255.0 blue:250/255.0 alpha:1] set]; [UIColor colorWithRed:250/255.0 green:250/255.0 blue:250/255.0 alpha:1] set]; CGContextFillRect(context, rect); If ([_data valueForKey:@"subData"]) {[[UIColor colorWithRed:243/255.0 green:243/255.0 blue:243/255.0 alpha:1] set]; if ([_data valueForKey:@"subData"]) {[[UIColor colorWithRed:243/255.0 green:243/255.0 blue:243/255.0 alpha:1] set]; CGRect subFrame = [_data[@"subData"][@"frame"] CGRectValue]; CGContextFillRect(context, subFrame); [[UIColor colorWithRed:200/255.0 green:200/255.0 blue:200/255.0 alpha:1] set]; CGContextFillRect(context, CGRectMake(0, subFrame.origin.y, rect.size.width, .5)); } { float leftX = SIZE_GAP_LEFT+SIZE_AVATAR+SIZE_GAP_BIG; float x = leftX; float y = (SIZE_AVATAR-(SIZE_FONT_NAME+SIZE_FONT_SUBTITLE+6))/2-2+SIZE_GAP_TOP+SIZE_GAP_SMALL-5; [_data[@"name"] drawInContext:context withPosition:CGPointMake(x, Y) andFont:FontWithSize(SIZE_FONT_NAME) andTextColor:[UIColor colorWithRed:106/255.0 green:140/255.0 blue:181/255.0 alpha:1] andHeight:rect.size.height]; y += SIZE_FONT_NAME+5; float fromX = leftX; float size = [UIScreen screenWidth]-leftX; NSString *from = [NSString stringWithFormat:@"%@ %@", _data[@"time"], _data[@"from"]]; [from drawInContext:context withPosition:CGPointMake(fromX, Y) andFont:FontWithSize(SIZE_FONT_SUBTITLE) andTextColor:[UIColor colorWithRed:178/255.0 green:178/255.0 blue:178/255.0 alpha:1] andHeight:rect.size.height andWidth:size]; } { CGRect countRect = CGRectMake(0, rect.size.height-30, [UIScreen screenWidth], 30); [[UIColor colorWithRed:250/255.0 green:250/255.0 blue:250/255.0 alpha:1] set]; [UIColor colorWithRed:250/255.0 green:250/255.0 blue:250/255.0 alpha:1] set]; CGContextFillRect(context, countRect); float alpha = 1; float x = [UIScreen screenWidth]-SIZE_GAP_LEFT-10; NSString *comments = _data[@"comments"]; if (comments) { CGSize size = [comments sizeWithConstrainedToSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX) fromFont:FontWithSize(SIZE_FONT_SUBTITLE) lineSpace:5]; x -= size.width; [comments drawInContext:context withPosition:CGPointMake(x, Y) andFont:FontWithSize(12) andTextColor:[UIColor colorWithRed:178/255.0 green:178/255.0 Alpha blue: 178/255.0:1] andHeight: the rect. Size. Height]; [[UIImage imageNamed:@"t_comments.png"] drawInRect:CGRectMake(x-5, 10.5+countRect.origin. 9) blendMode:kCGBlendModeNormal alpha:alpha]; commentsRect = CGRectMake(x-5, self.height-50, [UIScreen screenWidth]-x+5, 50); x -= 20; } NSString *reposts = _data[@"reposts"]; if (reposts) { CGSize size = [reposts sizeWithConstrainedToSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX) fromFont:FontWithSize(SIZE_FONT_SUBTITLE) lineSpace:5]; x -= MAX(size.width, 5)+SIZE_GAP_BIG; [reposts drawInContext:context withPosition:CGPointMake(x, Y) andFont:FontWithSize(12) andTextColor:[UIColor colorWithRed:178/255.0 green:178/255.0 Alpha blue: 178/255.0:1] andHeight: the rect. Size. Height]; [[UIImage imageNamed:@"t_repost.png"] drawInRect:CGRectMake(x-5, 11+countRect.origin.y, 10, 9) blendMode:kCGBlendModeNormal alpha:alpha]; repostsRect = CGRectMake(x-5, self.height-50, commentsRect.origin.x-x, 50); x -= 20; } [@ "• • •" drawInContext: context withPosition: CGPointMake (SIZE_GAP_LEFT, Y) andFont:FontWithSize(11) andTextColor:[UIColor colorWithRed:178/255.0 green:178/255.0 Alpha blue: 178/255.0:. 5] andHeight: the rect. Size. Height]; If ([_data valueForKey:@"subData"]) {[[UIColor colorWithRed:200/255.0 green:200/255.0 blue:200/255.0 alpha:1] set]; if ([_data valueForKey:@"subData"]) {[[UIColor colorWithRed:200/255.0 green:200/255.0 blue:200/255.0 alpha:1] CGContextFillRect(context, CGRectMake(0, rect.size. Height-30.5, rect.size. Width,.5)); CGContextFillRect(context, CGRectMake(0, rect.size. } } UIImage *temp = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); dispatch_async(dispatch_get_main_queue(), ^{ if (flag==drawColorFlag) { postBGView.frame = rect; postBGView.image = nil; postBGView.image = temp; }}); }); [self drawText]; [self loadThumb]; }Copy the code

From the above code, we can see that the main content such as name, comment icon and quantity, retweet icon and quantity etc. are all drawn on a single image and then set on the UIImageView

By asynchronously drawing the view, the main thread is set

In the code above we see that the drawing process is done in the global concurrent queue and is eventually set back to the main thread, as it must be done

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ ... dispatch_async(dispatch_get_main_queue(), ^{ if (flag==drawColorFlag) { postBGView.frame = rect; postBGView.image = nil; postBGView.image = temp; }}); } [self drawText]; [self loadThumb];Copy the code

Next, let me look at the implementation of the [self drawText] method.

- (void)drawText{ if (label==nil||detailLabel==nil) { [self addLabel]; } label.frame = [_data[@"textRect"] CGRectValue]; [label setText:_data[@"text"]]; if ([_data valueForKey:@"subData"]) { detailLabel.frame = [[_data valueForKey:@"subData"][@"textRect"] CGRectValue]; [detailLabel setText:[_data valueForKey:@"subData"][@"text"]]; detailLabel.hidden = NO; }}Copy the code

This method uses a custom Label, draws content asynchronously in the setText method, and the underlying implementation uses CoreText for better performance

I’ve simplified the code a little bit

- (void)setText:(NSString *)text{ .... dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSString *temp = text; _text = text; CGSize size = self.frame.size; size.height += 10; UIGraphicsBeginImageContextWithOptions(size, ! [self.backgroundColor isEqual:[UIColor clearColor]], 0); CGContextRef context = UIGraphicsGetCurrentContext(); if (context==NULL) { return; } if (! [self.backgroundColor isEqual:[UIColor clearColor]]) { [self.backgroundColor set]; CGContextFillRect(context, CGRectMake(0, 0, size.width, size.height)); } CGContextSetTextMatrix(context,CGAffineTransformIdentity); CGContextTranslateCTM(context,0,size.height); CGContextScaleCTM (context, 1.0, 1.0); //Determine default text color UIColor* textColor = self.textColor; //Set line height, font, color and break mode CGFloat minimumLineHeight = self.font.pointSize,maximumLineHeight = minimumLineHeight, linespace = self.lineSpace; CTFontRef font = CTFontCreateWithName((__bridge CFStringRef)self.font.fontName, self.font.pointSize,NULL); CTLineBreakMode lineBreakMode = kCTLineBreakByWordWrapping; CTTextAlignment alignment = CTTextAlignmentFromUITextAlignment(self.textAlignment); //Apply paragraph settings CTParagraphStyleRef style = CTParagraphStyleCreate((CTParagraphStyleSetting[6]){ {kCTParagraphStyleSpecifierAlignment, sizeof(alignment), &alignment}, {kCTParagraphStyleSpecifierMinimumLineHeight,sizeof(minimumLineHeight),&minimumLineHeight}, {kCTParagraphStyleSpecifierMaximumLineHeight,sizeof(maximumLineHeight),&maximumLineHeight}, {kCTParagraphStyleSpecifierMaximumLineSpacing, sizeof(linespace), &linespace}, {kCTParagraphStyleSpecifierMinimumLineSpacing, sizeof(linespace), &linespace}, {kCTParagraphStyleSpecifierLineBreakMode,sizeof(CTLineBreakMode),&lineBreakMode} },6); NSDictionary* attributes = [NSDictionary dictionaryWithObjectsAndKeys:(__bridge id)font,(NSString*)kCTFontAttributeName,  textColor.CGColor,kCTForegroundColorAttributeName, style,kCTParagraphStyleAttributeName, nil]; //Create attributed string, with applied syntax highlighting NSMutableAttributedString *attributedStr = [[NSMutableAttributedString alloc] initWithString:text attributes:attributes]; CFAttributedStringRef attributedString = (__bridge CFAttributedStringRef)[self highlightText:attributedStr]; //Draw the frame CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributedString); CGRect rect = CGRectMake(0, 5,(size.width),(size.height-5)); if ([temp isEqualToString:text]) { [self drawFramesetter:framesetter attributedString:attributedStr textRange:CFRangeMake(0, text.length) inRect:rect context:context]; CGContextSetTextMatrix(context,CGAffineTransformIdentity); CGContextTranslateCTM(context,0,size.height); CGContextScaleCTM (context, 1.0, 1.0); UIImage *screenShotimage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); dispatch_async(dispatch_get_main_queue(), ^{ CFRelease(font); CFRelease(framesetter); CFRelease(style); [[attributedStr mutableString] setString:@""]; if (drawFlag==flag) { if (isHighlight) { .... } else { if ([temp isEqualToString:text]) { ... highlightImageView.image = nil; labelImageView.image = nil; labelImageView.image = screenShotimage; } } // [self debugDraw]; // Draw a touchable region}}); }}); }Copy the code

This implementation works with – (void)drawInContext (CGContextRef) Context withPosition (CGPoint)p andFont (UIFont *)font andTextColor (UIColor) *)color andHeight:(float)height the implementation of this method is similar

Next is [self loadThumb]; The key to this method is to use SDWebimage to load images, because this framework has been optimized for loading images for me. (Unzip the image asynchronously and draw the image onto the canvas so that the unzip data does not reside in memory)

conclusion

The authors did achieve significant performance improvements through the above three points. However, I encountered two bugs in the process of research:

  • There is a memory leak in the code, mainly due to one placeCTParagraphStyleRef styleThis object is not released
  • When I swipe up hard with one hand, the page stops loading data. Actually, it’s because of the surveillance- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffsetIn this method leads to

On the basis of the above three optimization points, we can also make an obvious optimization is to cache the drawing results with NSCache, there is no need to draw every time. Add this and you’ll get an even bigger performance boost

Besides the last point, there are four points in total. Except for the first point, which some projects will not accept, the other three points and SDWebimage optimization methods can basically solve a large part of the performance problems as long as we use them appropriately in the project

My blog

FlyOceanFish