iOS 界面性能優化淺析

GitHub Repo:coderZsq.project.ios
Follow: coderZsq · GitHub
Blog: Castie! Note
Resume: coderzsq.github.io/coderZsq.pr…javascript

平常扯淡

emm~~~ 前段時間又出去看了看iOS市場的行情, 除非是BAT, TMD, 這類一線的知名企業, 其餘的只要努努力仍是能夠經過的, 這裏分享一些小技巧, 好比當HR,或者別人內推你的時候, 無論要求寫的多麼的天花亂墜, 千萬不要被這些東西給嚇到了, 你能夠先逆向一下對方的項目, 瞭解一下工程架構及代碼規範, 以及使用了哪些三方庫, 其實都是在面試前很好的對對方的初步瞭解, 知己知彼固然就增添了一份勝算. 固然如今對iOS的要求也是良莠不齊, 面試官能力和視野的高低也會無形的給你入職形成必定的困擾, 就好比有一次, 面試官問, 能詳細說說Swift麼?, 這種如此寬泛的命題, 你讓我用什麼角度來回答呢... 編譯器層面? 語言層面的寫時複製? 仍是語法層面的? 仍是WWDC的新特性? 挑了一方面講講, 就被說只有這些麼?, 能不能不要只說關鍵詞? 而後就是伴隨着的鄙視的眼神... 關鍵是個妹子面試官, 並且在我逆向其項目時發現代碼質量其實也就通常, 算了不吐槽了, 省得又要被說影響很差, 面試不過就瞎bb~~java

固然如今對於iOS的要求是比之前有了很大的提升, 只會用OC寫個tableview如今是幾乎很難找到工做了, 然而我除了公司的項目其餘時間基本不會碰OC的代碼, 因此當我要寫這個OC版的性能優化的時候, 好多語法都不太記得都須要現查... 徹底記不住API.node

固然在作性能優化的時候, 你仍是須要了解一些C系語言的底層知識, 這些雖然不必定可以在代碼上可以體現, 但對於理解和設計是有很是大的幫助的, 並且如今網上的講底層原理的博客實在是太多了, 都是你抄我我抄你的, 可能會有些許的水分, 建議仍是從runtime-723, GNUStep, 這類源碼來本身看會比較靠譜, 固然看這些代碼的時候可能會看不太懂C++的語法, 能夠去個人github-page上看看一些我整理的C++語法的總結, 應該會挺有幫助的.ios

準備工做

對於界面的性能優化, 簡單的說就是保持界面流暢不掉幀, 固然原理這種網上一搜一大把, 有空的話看看YYKit也就可以知曉個大概. 硬是要說原理的話, 就是當Vsync信號來臨的16.67msCPU作完排版, 繪製, 解碼, GPU避免離屏渲染之類的, 就會在Runloop下一次來臨的時候渲染到屏幕上.git

不說那麼多原理了, 理論永遠不如實踐來的有意義, 這也是所謂的貝葉斯算法隨時修正的策略. 咱們先寫一個測試數據來以備後面優化的須要.github

const http = require('http');

http.createServer((req, res) => {

    if (decodeURI(req.url) == "/fetchMockData") {
        console.log(req.url);

        let randomNumber = function (n) {
            var randomNumber = "";
            for (var i = 0; i < n; i++)
                randomNumber += Math.floor(Math.random() * 10);
            return randomNumber;
        }

        let randomString = function(len) {&emsp;&emsp;
           len = len || 32;&emsp;&emsp;
            var $chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678';&emsp;&emsp;
            var maxPos = $chars.length;&emsp;&emsp;
            var randomString = '';&emsp;&emsp;
            for (i = 0; i < len; i++) {&emsp;&emsp;&emsp;&emsp;
                randomString += $chars.charAt(Math.floor(Math.random() * maxPos));&emsp;&emsp;
            }&emsp;&emsp;
            return randomString;
        }

        let json = [];
        for (let i = 0; i < 100; i++) {
            let obj = {};
            let texts = [];
            let images = [];
            for (let i = 0; i < randomNumber(4); i++) {
                texts.push(randomString(randomNumber(1)));
            }
            for (let i = 0; i < 16; i++) {
                images.push("https://avatars3.githubusercontent.com/u/19483268?s=40&v=4");
            }
            obj.texts = texts;
            obj.images = images;
            json.push(obj);
        }
        res.writeHead(200, {
            'Content-Type': 'application/json'
        });

        res.end(JSON.stringify({
            data: json,
            status: 'success'
        }));
    } 

}).listen(8080);
console.log('listen port = 8080');
複製代碼

我就偷個懶用javascript起了個node服務器, 就是用來模擬測試數據用的, 固然若是你的機器沒有node環境的話, 建議你去自行安裝.web

完成以後, 咱們經過瀏覽器訪問就可以獲得測試數據, 測試數據分爲兩塊, 一塊是文字, 一塊是圖片的url, 因爲嫌麻煩, 直接就使用了我github的頭像.面試

預排版

對於界面流暢, 第一個想到的就是預排版了, 並且預排版的做用顯著, 原理也很簡單, 就是異步預先計算, 避免每次在layoutSubviews中, cell重用的時候進行重複計算. 誒... 又要被說只講關鍵詞了... 但原理就是那麼簡單, 還能講出個啥? 再說的細了代碼就寫出來了...算法

#import "Service.h"
#import <AFNetworking.h>

@implementation Service

- (void)fetchMockDataWithParam:(NSDictionary *)parameter completion:(RequestCompletionBlock)completion {
    
    AFHTTPSessionManager * manager = [AFHTTPSessionManager manager];
    [manager GET:@"http://localhost:8080/fetchMockData" parameters:parameter progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        if ([responseObject[@"status"] isEqualToString: @"success"]) {
            completion(responseObject[@"data"], nil);
        }
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        completion(nil, error);
    }];
}

@end
複製代碼

固然你須要請求一下剛纔的模擬數據, 注意localhosthttp的須要強制越權.json

#import "ViewModel.h"
#import "ComponentModel.h"
#import "ComponentLayout.h"
#import "Element.h"
#import <UIKit/UIKit.h>

@implementation ViewModel

- (Service *)service {
    
    if (!_service) {
        _service = [Service new];
    }
    return _service;
}

- (void)reloadData:(LayoutCompeltionBlock)completion error:(void(^)(void))errorCompletion {
    
    [self.service fetchMockDataWithParam:nil completion:^(NSArray<ComponentModel *> *models, NSError *error) {
        if (models.count > 0 && error == nil) {
            dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL);
            dispatch_async(queue, ^{
                NSMutableArray * array = [NSMutableArray new];
                for (ComponentModel * model in models) {
                    ComponentLayout * layout = [self preLayoutFrom:model];
                    [array addObject:layout];
                }
                dispatch_async(dispatch_get_main_queue(), ^{
                    if (completion) {
                        completion(array);
                    }
                });
            });
        } else {
            errorCompletion();
        }
    }];
}

- (void)loadMoreData:(LayoutCompeltionBlock)completion {
    [self reloadData:completion error:nil];
}

- (ComponentLayout *)preLayoutFrom:(ComponentModel *)model {
    
    ComponentLayout * layout = [ComponentLayout new];
    layout.cellWidth = [UIScreen mainScreen].bounds.size.width;
    
    CGFloat cursor = 0;
    
    CGFloat x = 5.;
    CGFloat y = 0.;
    CGFloat height = 0.;
    
    NSMutableArray * textElements = [NSMutableArray array];
    NSArray * texts = model[@"texts"];
    for (NSUInteger i = 0; i < texts.count; i++) {
        NSString * text = texts[i];
        CGSize size = [text boundingRectWithSize:CGSizeMake(MAXFLOAT, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading  attributes:@{NSFontAttributeName : [UIFont systemFontOfSize:7]} context:nil].size;
        if ((x + size.width) > layout.cellWidth) {
            x = 5;
            y += (size.height + 10);
            height = y + size.height + 5;
        }
        CGRect frame = CGRectMake(x, y, size.width + 5, size.height + 5);
        x += (size.width + 10);

        Element * element = [Element new];
        element.value = text;
        element.frame = frame;
        [textElements addObject:element];
    }
    cursor += height + 5;
    
    x = 5.; y = cursor; height = 0.;
    
    NSMutableArray * imageElements = [NSMutableArray array];
    NSArray * images = model[@"images"];
    for (NSUInteger i = 0; i < images.count; i++) {
        NSString * url = images[i];
        CGSize size = CGSizeMake(40, 40);
        if ((x + size.width) > layout.cellWidth) {
            x = 5;
            y += (size.height + 5);
            height = (y - cursor) + size.height + 5;
        }
        CGRect frame = CGRectMake(x, y, size.width, size.height);
        x += (size.width + 5);
        
        Element * element = [Element new];
        element.value = url;
        element.frame = frame;
        [imageElements addObject:element];
    }
    cursor += height + 5;
    
    layout.cellHeight = cursor;
    layout.textElements = textElements;
    layout.imageElements = imageElements;
    return layout;
}

@end
複製代碼
- (void)setupData:(ComponentLayout *)layout asynchronously:(BOOL)asynchronously {
    _layout = layout; _asynchronously = asynchronously;
    
    [self displayImageView];
    if (!asynchronously) {
        for (Element * element in layout.textElements) {
            UILabel * label = (UILabel *)[_labelReusePool dequeueReusableObject];
            if (!label) {
                label = [UILabel new];
                [_labelReusePool addUsingObject:label];
            }
            label.text = element.value;
            label.frame = element.frame;
            label.font = [UIFont systemFontOfSize:7];
            [self.contentView addSubview:label];
        }
        [_labelReusePool reset];
    }
}
複製代碼

代碼也是很是的簡單, 便是根據獲取的的數據進行排版而已, 而後cell直接獲取排版後的frame進行一步賦值操做而已. 有興趣也能夠看看sunnyxxFDTemplateLayoutCell.

重用池

眼尖的同窗確定就看到上面代碼由相似於cell的重用機制, 這個其實就是下面要講到的重用池的概念, 原理也是很是簡單的, 就是維護了兩個隊列, 一個是當前隊列, 一個是可重用隊列.

#import "ReusePool.h"

@interface ReusePool ()
@property (nonatomic, strong) NSMutableSet * waitUsedQueue;
@property (nonatomic, strong) NSMutableSet * usingQueue;
@property (nonatomic, strong) NSLock * lock;
@end

@implementation ReusePool

- (instancetype)init
{
    self = [super init];
    if (self) {
        _waitUsedQueue = [NSMutableSet set];
        _usingQueue = [NSMutableSet set];
        _lock = [NSLock new];
    }
    return self;
}

- (NSObject *)dequeueReusableObject {
    
    NSObject * object = [_waitUsedQueue anyObject];
    if (object == nil) {
        return nil;
    } else {
        [_lock lock];
        [_waitUsedQueue removeObject:object];
        [_usingQueue addObject:object];
        [_lock unlock];
        return object;
    }
}

- (void)addUsingObject:(UIView *)object {
    if (object == nil) {
        return;
    }
    [_usingQueue addObject:object];
}

- (void)reset {
    NSObject * object = nil;
    while ((object = [_usingQueue anyObject])) {
        [_lock lock];
        [_usingQueue removeObject:object];
        [_waitUsedQueue addObject:object];
        [_lock unlock];
    }
}

@end
複製代碼

這裏因爲我這個重用隊列, 並不只僅針對於UI, 因此在多線程訪問的狀況下, 是須要加鎖處理的, 這裏就用最通用的pthread-mutex進行加鎖.

預解碼

這裏作的就是經過url進行的異步解碼的處理, 相關原理的文章其實不少, 自行查閱.

@implementation NSString (Extension)

- (void)preDecodeThroughQueue:(dispatch_queue_t)queue completion:(void(^)(UIImage *))completion {
    
    dispatch_async(queue, ^{
        CGImageRef cgImage = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:self]]].CGImage;
        CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(cgImage) & kCGBitmapAlphaInfoMask;
        
        BOOL hasAlpha = NO;
        if (alphaInfo == kCGImageAlphaPremultipliedLast ||
            alphaInfo == kCGImageAlphaPremultipliedFirst ||
            alphaInfo == kCGImageAlphaLast ||
            alphaInfo == kCGImageAlphaFirst) {
            hasAlpha = YES;
        }
        
        CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
        bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
        
        size_t width = CGImageGetWidth(cgImage);
        size_t height = CGImageGetHeight(cgImage);
        
        CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, CGColorSpaceCreateDeviceRGB(), bitmapInfo);
        CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage);
        cgImage = CGBitmapContextCreateImage(context);
        
        UIImage * image = [[UIImage imageWithCGImage:cgImage] cornerRadius:width * 0.5];
        CGContextRelease(context);
        CGImageRelease(cgImage);
        completion(image);
    });
}

@end
複製代碼

預渲染

好比imageView.layer.cornerRadiusimageView.layer.masksToBounds = YES;這類操做會致使離屏渲染, GPU會致使新開緩衝區形成消耗. 爲了不離屏渲染,你應當儘可能避免使用 layerbordercornershadowmask 等技術,而儘可能在後臺線程預先繪製好對應內容。這種SDWebImage的緩存策略是有很大的參考意義的.

@implementation UIImage (Extension)

- (UIImage *)cornerRadius:(CGFloat)cornerRadius {
    
    CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height);
    UIBezierPath * bezierPath = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:cornerRadius];
    UIGraphicsBeginImageContextWithOptions(self.size, false, [UIScreen mainScreen].scale);
    CGContextAddPath(UIGraphicsGetCurrentContext(), bezierPath.CGPath);
    CGContextClip(UIGraphicsGetCurrentContext());
    
    [self drawInRect:rect];
    
    CGContextDrawPath(UIGraphicsGetCurrentContext(), kCGPathFillStroke);
    UIImage * image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return image;
}

@end
複製代碼
- (void)displayImageView {
    for (Element * element in _layout.imageElements) {
        UIImageView * imageView = (UIImageView *)[_imageReusePool dequeueReusableObject];
        if (!imageView) {
            imageView = [UIImageView new];
            [_imageReusePool addUsingObject:imageView];
        }
        UIImage * image = (UIImage *)[ComponentCell.asyncReusePool dequeueReusableObject];
        if (!image) {
            NSString * url = element.value;
            [url preDecodeThroughQueue:concurrentQueue completion:^(UIImage * image) {
                [ComponentCell.asyncReusePool addUsingObject:image];
                dispatch_async(dispatch_get_main_queue(), ^{
                    imageView.image = image;
                });
            }];
        } else {
            imageView.image = image;
        }
        imageView.frame = element.frame;
        [self.contentView addSubview:imageView];
    }
    [ComponentCell.asyncReusePool reset];
    [_imageReusePool reset];
}
複製代碼

這樣經過和重用池的結合就能夠達到很好的效果, 固然由於我這裏是一張相同的圖片, 正常狀況下須要設計一整套的緩存策略, hash一下url什麼的.

經過上述的方案結合, 咱們已經可以比較流暢的顯示那麼多數據了 並且幀數仍是不錯的, 但還有優化的空間.

異步繪製

進過了上面的優化, 咱們其實已經作的不錯了, 可是看下界面的層次, 是否是有點得了密集恐懼症... 接下來要講的異步繪製就可以很好的解決這個問題. 並更加的優化流暢度. 異步繪製的原理麼, 就是異步的drawInLayer, 異步的畫在上下文上並統一繪製.

- (void)displayLayer:(CALayer *)layer {
    
    if (!layer) return;
    if (layer != self.layer) return;
    if (![layer isKindOfClass:[AsyncDrawLayer class]]) return;
    if (!self.isAsynchronously) return;
    
    AsyncDrawLayer *tempLayer = (AsyncDrawLayer *)layer;
    [tempLayer increaseCount];
    
    NSUInteger oldCount = tempLayer.drawsCount;
    CGRect bounds = self.bounds;
    UIColor * backgroundColor = self.backgroundColor;
    
    layer.contents = nil;
    dispatch_async(serialQueue, ^{
        void (^failedBlock)(void) = ^{
            NSLog(@"displayLayer failed");
        };
        if (tempLayer.drawsCount != oldCount) {
            failedBlock();
            return;
        }
        
        CGSize contextSize = layer.bounds.size;
        BOOL contextSizeValid = contextSize.width >= 1 && contextSize.height >= 1;
        CGContextRef context = NULL;
        
        if (contextSizeValid) {
            UIGraphicsBeginImageContextWithOptions(contextSize, layer.isOpaque, layer.contentsScale);
            context = UIGraphicsGetCurrentContext();
            CGContextSaveGState(context);
            if (bounds.origin.x || bounds.origin.y) {
                CGContextTranslateCTM(context, bounds.origin.x, -bounds.origin.y);
            }
            if (backgroundColor && backgroundColor != [UIColor clearColor]) {
                CGContextSetFillColorWithColor(context, backgroundColor.CGColor);
                CGContextFillRect(context, bounds);
            }
            [self asyncDraw:YES context:context completion:^(BOOL drawingFinished) {
                if (drawingFinished && oldCount == tempLayer.drawsCount) {
                    CGImageRef CGImage = context ? CGBitmapContextCreateImage(context) : NULL;
                    {
                        UIImage * image = CGImage ? [UIImage imageWithCGImage:CGImage] : nil;
                        dispatch_async(dispatch_get_main_queue(), ^{
                            if (oldCount != tempLayer.drawsCount) {
                                failedBlock();
                                return;
                            }
                            layer.contents = (id)image.CGImage;
                            layer.opacity = 0.0;
                            [UIView animateWithDuration:0.25 delay:0.0 options:UIViewAnimationOptionAllowUserInteraction animations:^{
                                layer.opacity = 1.0;
                            } completion:NULL];
                        });
                    }
                    if (CGImage) {
                        CGImageRelease(CGImage);
                    }
                } else {
                    failedBlock();
                }
            }];
            CGContextRestoreGState(context);
        }
        UIGraphicsEndImageContext();
    });
}
複製代碼

代碼其實也沒什麼好講的只要注意下多線程重繪時的問題也就能夠了.

- (void)asyncDraw:(BOOL)asynchronously context:(CGContextRef)context completion:(void(^)(BOOL))completion {
    
    for (Element * element in _layout.textElements) {
        NSMutableParagraphStyle * paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
        paragraphStyle.lineBreakMode = NSLineBreakByCharWrapping;
        paragraphStyle.alignment = NSTextAlignmentCenter;
        [element.value drawInRect:element.frame withAttributes:@{NSFontAttributeName:[UIFont systemFontOfSize:7],
                                                                 NSForegroundColorAttributeName:[UIColor blackColor],
                                                                 NSParagraphStyleAttributeName:paragraphStyle}];
    }
    completion(YES);
}
複製代碼

當在當前線程所有繪製完上下文後, 會統一的渲染到 layer上, 很好理解的其實.

這樣咱們的圖層就會被合成, 幀率也一直保持在60幀的樣子.

語法技巧

OC如今也支持類屬性了, 具體的使用能夠看我swiftrenderTree.

@property (class, nonatomic,strong) ReusePool * asyncReusePool;

static ReusePool * _asyncReusePool = nil;

+ (ReusePool *)asyncReusePool {
    if (_asyncReusePool == nil) {
        _asyncReusePool = [[ReusePool alloc] init];
    }
    return _asyncReusePool;
}

+ (void)setAsyncReusePool:(ReusePool *)asyncReusePool {
    if (_asyncReusePool != asyncReusePool) {
        _asyncReusePool = asyncReusePool;
    }
}
複製代碼

碰到的坑

其實理想的效果是以下圖這樣的. 全部的圖片都被異步繪製到layer上.

- (void)asyncDraw:(BOOL)asynchronously context:(CGContextRef)context completion:(void(^)(BOOL))completion {
    
    for (Element * element in _layout.textElements) {
        NSMutableParagraphStyle * paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
        paragraphStyle.lineBreakMode = NSLineBreakByCharWrapping;
        paragraphStyle.alignment = NSTextAlignmentCenter;
        [element.value drawInRect:element.frame withAttributes:@{NSFontAttributeName:[UIFont systemFontOfSize:7],
                                                                 NSForegroundColorAttributeName:[UIColor blackColor],
                                                                 NSParagraphStyleAttributeName:paragraphStyle}];
    }
    completion(YES);
    //TODO:
    // for (Element * element in _layout.imageElements) {
    // UIImage * image = (UIImage *)[ComponentCell.asyncReusePool dequeueReusableObject];
    // if (!image) {
    // NSString * url = element.value;
    // [url preDecodeThroughQueue:concurrentQueue completion:^(UIImage * image) {
    // [ComponentCell.asyncReusePool addUsingObject:image];
    // [image drawInRect:element.frame]; //這裏異步回調上下文獲取不到報錯
    // completion(YES);
    // }];
    // } else {
    // [image drawInRect:element.frame];
    // completion(YES);
    // }
    // }
    // [_asyncReusePool reset];
}
複製代碼

但這裏碰到一個問題, CGContextRef, 也就是UIGraphicsGetCurrentContext()拿到的是當前線程的上下文, 而線程和Runloop是一一對應的, 只有當前線程的上下文才能進行繪製, 而網絡圖片又須要進行異步下載解碼, 勢必會進行併發下載, 從而上下文獲取不到, 繪製不能.報錯以下:

我嘗試了一下將繪圖上下文傳入其餘線程並渲染:

for (Element * element in _layout.imageElements) {
        UIImage * image = (UIImage *)[ComponentCell.asyncReusePool dequeueReusableObject];
        if (!image) {
            NSString * url = element.value;
            [url preDecodeThroughQueue:concurrentQueue completion:^(UIImage * image) {
                [ComponentCell.asyncReusePool addUsingObject:image];
                CGContextDrawImage(context, element.frame, image.CGImage);
                completion(YES);
            }];
        } else {
            [image drawInRect:element.frame];
            completion(YES);
        }
    }
    [_asyncReusePool reset];
複製代碼

惋惜的是, 使用CGContextDrawImage傳入當前上下文也不能解決這個問題, 緣由是上一個線程的上下文已通過期了...

常駐線程

碰到這個問題, 首先想到的就是多是在上一條線程掛了以後, context失效致使的, 那咱們可使用runloop開啓一條常駐線程來保住這個上下文.

#import "PermenantThread.h"

@interface Thread : NSThread
@end

@implementation Thread
- (void)dealloc {
    NSLog(@"%s", __func__);
}
@end

@interface PermenantThread()
@property (strong, nonatomic) Thread *innerThread;
@end

@implementation PermenantThread

- (instancetype)init
{
    if (self = [super init]) {
        self.innerThread = [[Thread alloc] initWithBlock:^{
            CFRunLoopSourceContext context = {0};
            CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
            CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
            CFRelease(source);
            CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);
        }];
        [self.innerThread start];
    }
    return self;
}


- (void)executeTask:(PermenantThreadTask)task {
    if (!self.innerThread || !task) return;
    [self performSelector:@selector(__executeTask:) onThread:self.innerThread withObject:task waitUntilDone:NO];
}

- (void)stop {
    if (!self.innerThread) return;
    [self performSelector:@selector(__stop) onThread:self.innerThread withObject:nil waitUntilDone:YES];
}

- (void)dealloc {
    NSLog(@"%s", __func__);
    [self stop];
}

- (void)__stop {
    CFRunLoopStop(CFRunLoopGetCurrent());
    self.innerThread = nil;
}

- (void)__executeTask:(PermenantThreadTask)task {
    task();
}

@end

複製代碼

然而在原覺得是由於子線程銷燬致使的上下文過時, 在我用Runloop作了一個常駐線程後, 同樣沒有效果... 真是醉了... 誰知道上下文過時會是什麼形成的麼?

解決方法

查了一些資料和問了一些大佬, 獲得各類反饋的解決方案, dispath_async_f這個函數是沒有用的, 傳遞的上下文和繪圖上下文並無關係, 因此這個是不對的, 最後嘗試使用自建上下文解決了這個問題.

CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef context = CGBitmapContextCreate(NULL, contextSize.width, contextSize.height, 8, contextSize.width * 4, colorSpace, kCGImageAlphaPremultipliedFirst | kCGImageByteOrderDefault);
CGColorSpaceRelease(colorSpace);
CGAffineTransform normalState = CGContextGetCTM(context);
CGContextTranslateCTM(context, 0, bounds.size.height);
CGContextScaleCTM(context, 1, -1);
CGContextConcatCTM(context, normalState);
if (backgroundColor && backgroundColor != [UIColor clearColor]) {
    CGContextSetFillColorWithColor(context, backgroundColor.CGColor);
    CGContextFillRect(context, bounds);
}
UIGraphicsPushContext(context);
複製代碼

繪圖的時候不使用drawInRect, 而使用CGContextDrawImage的API

dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrent", DISPATCH_QUEUE_CONCURRENT);
    for (Element * element in _layout.imageElements) {
        UIImage * image = (UIImage *)[ComponentCell.asyncReusePool dequeueReusableObject];
        if (!image) {
            dispatch_group_enter(group);
            dispatch_group_async(group, concurrentQueue, ^{
                NSString * url = element.value;
                [url preDecodeWithCGCoordinateSystem:YES completion:^(UIImage * image) {
                    [ComponentCell.asyncReusePool addUsingObject:image];
                    CGContextDrawImage(context, element.frame, image.CGImage);
// completion(YES);
                }];
                dispatch_group_leave(group);
            });
        } else {
            CGContextDrawImage(context, element.frame, image.CGImage);
            completion(YES);
        }
    }
    dispatch_group_notify(group, concurrentQueue, ^{
        completion(YES);
    });
    [_asyncReusePool reset];
複製代碼

因爲是異步繪製, 每下載一張圖片就進行繪製, 會形成一直刷幀, 因此使用線程組來進行統一繪製.

這裏還有一個坑就是 drawInRectCGContextDrawImage的繪製座標系不同, 因此須要加一下判斷便可.

最後 本文中全部的源碼均可以在github上找到 -> SQPerformance

GitHub Repo:coderZsq.project.ios
Follow: coderZsq · GitHub
Resume: coderzsq.github.io/coderZsq.pr…

相關文章
相關標籤/搜索