「砰砰砰、砰砰砰、砰砰砰」git
「大師,大師,江湖救急啊」github
「不知少俠,着急讓老夫出關所爲什麼事?」性能優化
「大師以前授與個人iOS性能優化(初級)和iOS性能優化(中級),我已熟悉研讀多日,且勤學苦練,至今已能解決大部分滑動卡頓問題。」bash
「少俠,果真聰慧過人」異步
「可是,最近依然遇到了問題,小師妹想作一個相似於微博主頁的頁面,有不少feed,每一個feed裏面,有話題,連接、圖片、表情、圓角頭像等,這麼多元素雜在一塊兒,縱然我使出畢生所學,卻依然會有卡頓,達不到小師妹對流暢性的要求,因此非常苦惱,懇求大師指點。」async
「原來是這樣,老夫這就來助你突破瓶頸,更上一層樓。」oop
在iOS性能優化(初級)和iOS性能優化(中級)中,爲了屏幕流暢咱們作了不少,也取得了不錯的成果。但不管怎麼作,最後的繪製是提交給系統的,系統默認是在主線程作這一切,當須要繪製的元素過多,過於頻繁,那麼依然會形成卡頓。post
那麼咱們可不能夠像處理複雜數據同樣,把繪製過程放在後臺線程執行呢?性能
很高興,答案是能夠的。學習
iOS裏面的視圖UIView
中有一個CALayer *layer
的屬性,UIView
的內容,實際上是layer
顯示的,layer
中有一個屬性id contents
,contents
的內容就是要顯示的具體內容,大多數狀況下,contents
的值是一張圖片。咱們經常使用的不管是 UILabel
仍是 UIImageView
裏面顯示的內容,其實都是繪製在一張畫布上,繪製完成從畫布中導出圖片,再把圖片賦值給layer.contents
就完成了顯示。
異步繪製,就是異步在畫布上繪製內容。
Talk is cheap. Show me the code
首先來新建一個AsyncLabel類,而後重寫- (void)displayLayer:(CALayer *)layer
方法,在其中進行異步繪製。
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface AsyncLabel : UIView
//設置文字內容
@property(nonatomic, copy) NSString *text;
//設置字體
@property(nonatomic, strong) UIFont *font;
@end
NS_ASSUME_NONNULL_END
複製代碼
#import "AsyncLabel.h"
#import <CoreText/CoreText.h>
@implementation AsyncLabel
- (void)displayLayer:(CALayer *)layer
{
NSLog(@"是否是主線程 %d", [[NSThread currentThread] isMainThread]);
//輸出 1 表明是主線程
//異步繪製,因此咱們在使用了全局子隊列,實際使用中,最好自創隊列
dispatch_async(dispatch_get_global_queue(0, 0), ^{
__block CGSize size = CGSizeZero;
__block CGFloat scale = 1.0;
dispatch_sync(dispatch_get_main_queue(), ^{
size = self.bounds.size;
scale = [UIScreen mainScreen].scale;
});
UIGraphicsBeginImageContextWithOptions(size, NO, scale);
CGContextRef context = UIGraphicsGetCurrentContext();
[self draw:context size:size];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
dispatch_async(dispatch_get_main_queue(), ^{
self.layer.contents = (__bridge id)(image.CGImage);
});
});
}
@end
複製代碼
- (void)draw:(CGContextRef)context size:(CGSize)size
{
//將座標系上下翻轉。由於底層座標系和UIKit的座標系原點位置不一樣。
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, size.height);
CGContextScaleCTM(context, 1.0,-1.0);
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(0, 0, size.width, size.height));
//設置內容
NSMutableAttributedString * attString = [[NSMutableAttributedString alloc] initWithString:self.text];
//設置字體
[attString addAttribute:NSFontAttributeName value:self.font range:NSMakeRange(0, self.text.length)];
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attString);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attString.length), path, NULL);
//把frame繪製到context裏
CTFrameDraw(frame, context);
}
複製代碼
這樣就完成了一個簡單的繪製。在- (void)displayLayer:(CALayer *)layer
方法中,在異步線程裏,建立一個畫布並把繪製的結果在主線程中傳給layer.contents
。
繪製過程使用了CoreText
,這裏只簡單的把文字繪製上去,實際使用過程當中,根據須要可能會有不少的地方須要設置,還請少俠自行學習CoreText
。
調用一下看一下結果:
AsyncLabel *label = [[AsyncLabel alloc] initWithFrame:CGRectMake(50, 200, [UIScreen mainScreen].bounds.size.width - 2 * 50, 100)];
label.backgroundColor = [UIColor lightGrayColor];
label.text = @"今天是個好日子啊,心想的事兒都能成,今天是個好日子啊,啊,安心,太平";
label.font = [UIFont systemFontOfSize:20];
[self.view addSubview:label];
[label.layer setNeedsDisplay];
複製代碼
顯示效果達到。
「多謝大師指點,大師一番操做,讓我茅塞頓開。」
上面的操做是很是常規的操做,在實際使用中還有幾個問題須要解決:
下面老夫來給少俠介紹一種,全新的解決方式,刷新常規想法,且封裝優秀。
它的主要處理流程以下:
observer
,它的優先級要比系統的CATransaction
要低,保證系統先作完必須的工做。runloop
會在observer
須要的時機通知統一處理。layer.contents
。大概瞭解了原理,咱們來使用一下YYAsyncLayer
刪除以前在AsyncLabel.m
中使用原始方式異步繪製的代碼加入下列代碼
- (void)setText:(NSString *)text {
_text = text.copy;
[[YYTransaction transactionWithTarget:self selector:@selector(contentsNeedUpdated)] commit];
}
- (void)setFont:(UIFont *)font {
_font = font;
[[YYTransaction transactionWithTarget:self selector:@selector(contentsNeedUpdated)] commit];
}
- (void)layoutSubviews {
[super layoutSubviews];
[[YYTransaction transactionWithTarget:self selector:@selector(contentsNeedUpdated)] commit];
}
- (void)contentsNeedUpdated {
// do update
[self.layer setNeedsDisplay];
}
複製代碼
這一些代碼,執行了處理流程中的1、2,註冊了observer,並收集了要統一處理的操做。
+ (Class)layerClass
{
return [YYAsyncLayer class];
}
- (YYAsyncLayerDisplayTask *)newAsyncDisplayTask {
YYAsyncLayerDisplayTask *task = [YYAsyncLayerDisplayTask new];
task.willDisplay = ^(CALayer *layer) {
//...
};
task.display = ^(CGContextRef context, CGSize size, BOOL(^isCancelled)(void)) {
if (isCancelled()) {
return;
}
if (!self.text.length) {
return;
}
[self draw:context size:size];
};
task.didDisplay = ^(CALayer *layer, BOOL finished) {
if (finished) {
// finished
} else {
// cancelled
}
};
return task;
}
複製代碼
這些代碼實現了流程中的3,異步繪製,並提供給使用者willDisplay
、display
、didDisplay
幾個block。
有一點須要注意,必須重寫+ (Class)layerClass
,纔會進入自定義的subLayer執行方法。至關於打UIView的layer,從默認layer指到subLayer。
上述招式,老夫只是簡單演示,但少俠遇到的事要比老夫複雜的多。少俠天資聰慧,切不可傲嬌,還需好生練習並配合runloop
、CoreText
使用,方能得心應手。快去答覆小師妹去罷。