iOS 介面效能優化淺析

Castie1發表於2019-03-04

GitHub Repo:coderZsq.project.ios

Follow: coderZsq · GitHub

Blog: Castie! Note

Resume: coderzsq.github.io/coderZsq.pr…

日常扯淡

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

當然現在對於iOS的要求是比以前有了很大的提高, 只會用OC寫個tableview現在是幾乎很難找到工作了, 然而我除了公司的專案其他時間基本不會碰OC的程式碼, 所以當我要寫這個OC版的效能優化的時候, 好多語法都不太記得都需要現查… 完全記不住API.

當然在做效能優化的時候, 你還是需要了解一些C系語言的底層知識, 這些雖然不一定能夠在程式碼上能夠體現, 但對於理解和設計是有非常大的幫助的, 而且現在網上的講底層原理的部落格實在是太多了, 都是你抄我我抄你的, 可能會有些許的水分, 建議還是從runtime-723, GNUStep, 這類原始碼來自己看會比較靠譜, 當然看這些程式碼的時候可能會看不太懂C++的語法, 可以去我的github-page上看看一些我整理的C++語法的總結, 應該會挺有幫助的.

準備工作

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

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

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環境的話, 建議你去自行安裝.

iOS 介面效能優化淺析

完成之後, 我們通過瀏覽器訪問就能夠得到測試資料, 測試資料分為兩塊, 一塊是文字, 一塊是圖片的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的需要強制越權.

#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什麼的.

iOS 介面效能優化淺析

通過上述的方案結合, 我們已經能夠比較流暢的顯示那麼多資料了 而且幀數還是不錯的, 但還有優化的空間.

非同步繪製

iOS 介面效能優化淺析

進過了上面的優化, 我們其實已經做的不錯了, 但是看下介面的層次, 是不是有點得了密集恐懼症… 接下來要講的非同步繪製就能夠很好的解決這個問題. 並更加的優化流暢度.
非同步繪製的原理麼, 就是非同步的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);
}
複製程式碼
iOS 介面效能優化淺析

當在當前執行緒全部繪製完上下文後, 會統一的渲染到layer上, 很好理解的其實.

iOS 介面效能優化淺析

這樣我們的圖層就會被合成, 幀率也一直保持在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上.

iOS 介面效能優化淺析
- (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是一一對應的, 只有當前執行緒的上下文才能進行繪製, 而網路圖片又需要進行非同步下載解碼, 勢必會進行併發下載, 從而上下文獲取不到, 繪製不能.報錯如下:

iOS 介面效能優化淺析

我嘗試了一下將繪圖上下文傳入其他執行緒並渲染:

    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傳入當前上下文也不能解決這個問題, 原因是上一個執行緒的上下文已經過期了…

iOS 介面效能優化淺析

常駐執行緒

碰到這個問題, 首先想到的就是可能是在上一條執行緒掛了之後, 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…

相關文章