@NewPan 貝聊科技 iOS 菜鳥工程師git
你們好,我是 NewPan,我以前寫過一篇 iOS一次立竿見影的啓動時間優化 - 簡書,從標題也能夠看得出來,那篇文章是關於啓動時間優化的,獲得了你們不錯的反響。此次咱們來說講如何優化首頁的渲染時間。github
上圖是貝聊家長版首頁的設計圖,從上圖能夠看出,這個首頁仍是很複雜的,郭耀源在他的 深刻理解RunLoop | Garan no dou 裏提到:objective-c
UI 線程中一旦出現繁重的任務就會致使界面卡頓,這類任務一般分爲3類:排版,繪製,UI 對象操做。數組
- 排版一般包括計算視圖大小、計算文本高度、從新計算子式圖的排版等操做。
- 繪製通常有文本繪製 (例如
CoreText
)、圖片繪製 (例如預先解壓)、元素繪製 (Quartz)等操做。 3.UI 對象操做一般包括UIView
/CALayer
等 UI 對象的建立、設置屬性和銷燬。
貝聊這個首頁已經把「排版,繪製,UI 對象操做」這三個方面耗時操做所有涵蓋了,若是直接基於 UIKit
那一套去寫的話,須要花不少時間去作性能調優。因此貝聊的首頁直接採用了 AsyncDisplayKit,雖然須要從新去學習 AsyncDisplayKit
那套 boxing 佈局規則,可是效果很明顯,咱們的列表在很老的 iPhone 5 上快速滾動都不會出現明顯卡頓。緩存
咱們先看一下優化前的首頁耗時,這個耗時是指從後臺數據加載到設備之後進行解析最後渲染成 UI 這個過程的耗時。測試設備爲我本身的 iPhone 6s Plus(國行 64GB),我總共測試了十組數據。安全
// 第 1 組.
2018-08-14 16:20:38.831014+0800 beiliao[2429:991848] 從數據加載完成到首頁開始渲染耗時: 1.172843
// 第 2 組.
2018-08-14 16:21:15.409550+0800 beiliao[2431:992484] 從數據加載完成到首頁開始渲染耗時: 1.199685
// 第 3 組.
2018-08-14 16:21:50.329775+0800 beiliao[2433:993092] 從數據加載完成到首頁開始渲染耗時: 1.203976
// 第 4 組.
2018-08-14 16:22:30.805793+0800 beiliao[2435:993740] 從數據加載完成到首頁開始渲染耗時: 1.022340
// 第 5 組.
2018-08-14 16:23:10.874299+0800 beiliao[2437:994402] 從數據加載完成到首頁開始渲染耗時: 1.127660
// 第 6 組.
2018-08-14 16:23:43.988901+0800 beiliao[2439:994997] 從數據加載完成到首頁開始渲染耗時: 0.991278
// 第 7 組.
2018-08-14 16:24:19.291121+0800 beiliao[2441:995581] 從數據加載完成到首頁開始渲染耗時: 0.970286
// 第 8 組.
2018-08-14 16:24:53.831283+0800 beiliao[2444:996330] 從數據加載完成到首頁開始渲染耗時: 0.550910
// 第 9 組.
2018-08-14 16:25:30.564408+0800 beiliao[2446:996948] 從數據加載完成到首頁開始渲染耗時: 1.339828
// 第 10 組.
2018-08-14 16:26:07.003846+0800 beiliao[2452:997656] 從數據加載完成到首頁開始渲染耗時: 0.978076
複製代碼
能夠看到,數據範圍從 0.550910 - 1.339828,平均值爲 1.05563。並且這裏有個特色就是,不論是 iPhone X 仍是更舊的設備,都同樣的耗時,由於這裏阻塞的是 UI 線程。併發
上一小節說貝聊的首頁採用的是 AsyncDisplayKit
,對於排版,繪製,UI 對象操做這三項,前兩項已經被 AsyncDisplayKit
扔到後臺線程,最後前兩項的結果會被同步到 UI 線程進行視圖渲染。因此影響貝聊首頁渲染的應該是「UI 對象操做」。app
接下來祭出」Time Profiler「,找到耗時的代碼,若是有使用 Time Profiler 的問題,請參考我以前寫的文章 iOS用 TimeProfiler 揪出那些耗時函數 - 簡書。async
上圖有一個 -fetchAnimationImages
方法,它的實現是下面這樣的。就是對一組序列幀進行加載,這個方法耗時 0.284 秒。函數
+ (NSArray<NSString *> *)fetchAnimationImageNames {
NSMutableArray<NSString *> *names = @[].mutableCopy;
for (int i = 2; i <= 23; i++) {
[names addObject:[NSString stringWithFormat:@"BLDKLoadMoreAnimation-000%02d", i]];
}
return names.copy;
}
+ (NSArray<UIImage *> *)fetchAnimationImages {
NSMutableArray<UIImage *> *images = @[].mutableCopy;
for (NSString *imageName in [self fetchAnimationImageNames]) {
[images addObject:[UIImage imageNamed:imageName]];
}
return images.copy;
}
複製代碼
一樣的,我又分析了其餘的方法,最後,這些方法都調用了一個系統的方法 -imageNamed:
。因而我把 -fetchAnimationImages
中調用-imageNamed:
的地方註釋掉。
+ (NSArray<UIImage *> *)fetchAnimationImages {
NSMutableArray<UIImage *> *images = @[].mutableCopy;
for (NSString *imageName in [self fetchAnimationImageNames]) {
// [images addObject:[UIImage imageNamed:imageName]];
}
return images.copy;
}
複製代碼
再次打開」Time Profiler「,看到耗時函數裏已經沒有 -fetchAnimationImages
這個方法了。
至此,咱們驗證了,影響首頁渲染耗時的最大凶手是 -imageNamed:
這個方法。
咱們每天都在用這個方法加載 UI 元素,可是歷來沒想過這個方法是壓死駱駝的最後一根稻草。
從系統文檔來看,這個方法會去 bundle
中加載圖片資源,解碼數據,最後根據用戶的設備分辨率的不一樣渲染到屏幕上。咱們知道這個過程可能會很耗時,尤爲當圖片文件很大的時候,因此 SDWebImage
、AFNetworking
、YYWebImage
把圖片解碼這樣的操做都放到了子線程。
文檔上面沒有寫 ,經評論裏朋友提醒,再仔細看了一下文檔,-imageNamed:
這個方法是不是線程安全的In iOS 9 and later, this method is thread safe.
,也就是說 iOS 9 之後這個方法是線程安全的。受這些第三方庫的啓發,我開始嘗試把 -imageNamed:
這個方法放到子線程運行,並在各個機型上測試,發現沒有出現問題。
咱們都知道, -imageNamed:
這個方法會有緩存,只要加載過一次,再次加載就會獲得緩存的優化。因而,我開始嘗試將本地資源圖片提早進行預加載。
那爲何這個預加載可行呢?由於這個時機很重要,從 -application:didFinishLaunchingWithOptions:
到首頁請求回來這個時間,恰好 CPU 和 IO 都是空閒的(或者你能夠經過其餘手段把這段時間的 CPU 和 IO 預留出來,具體請參考 iOS一次立竿見影的啓動時間優化 - 簡書),這段時間你就能夠把本地圖片資源都加載好,等請求回來的時候,首頁須要調用的 -imageNamed:
方法都已預加載過一遍,再次加載都會享受高速緩存的優化,這樣就能達到優化的效果。
實現思路大體以下:
-imageNamed:
方法到自定義的實現,在這個實現中把圖片名字緩存到本地。-application:didFinishLaunchingWithOptions:
方法中開始預加載上次 APP 啓動緩存好的圖片。具體應該使用 GCD 併發的在子線程中加載。具體實現代碼以下:
BLImagePreloadManager.h
文件以下:
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface BLImagePreloadManager : NSObject
/**
* 手動添加須要預加載的圖片名(圖片名數組).
*
* @warning 在 load 方法中添加才能執行到.
*/
+ (void)preloadImagesWithImageNames:(NSArray<NSString *> *)imageNames;
/**
* 手動添加須要預加載的圖片名.
*
* @warning 在 load 方法中添加才能執行到.
*/
+ (void)preloadImageWithImageName:(NSString *)imageName;
/**
* 嘗試預加載 `-imageName:` 的圖片(方法會自動切換到子線程).
*/
+ (void)preloadImagesIfNeed;
/**
* 存儲預加載的圖片名稱.
*/
+ (void)storeImageNameForPreload:(NSString *)imageName;
@end
NS_ASSUME_NONNULL_END
複製代碼
BLImagePreloadManager.m
文件以下:
#import "BLImagePreloadManager.h"
#import "UIImage+ImageDetect.h"
#import "BLGCDExtensions.h"
static NSString *const kBLImagePreloadManagerStoreKey = @"com.ibeiliao.preload.images.store.key.www";
static BOOL _isStoreTimeTick = NO;
static NSTimeInterval const kBLImagePreloadManagerStoreImageTimeInterval = 10;
static NSMutableSet<NSString *> *_kImageNameCollectSetM = nil;
static dispatch_queue_t _ioQueue;
static NSMutableArray<NSString *> *_manualPreloadImageNames = nil;
@implementation BLImagePreloadManager
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_ioQueue = dispatch_queue_create("com.ibeiliao.image.preload.queue", DISPATCH_QUEUE_SERIAL);
});
}
+ (void)preloadImagesWithImageNames:(NSArray<NSString *> *)imageNames {
NSParameterAssert(imageNames.count);
BLAssertMainThread;
if (!imageNames.count) {
return;
}
[self manualPreloadArrayInitIfNeed];
NSAssert(_manualPreloadImageNames, @"添加預加載行爲時機太晚, 預加載已經完成, 請在 load 方法中執行添加預加載圖片行爲");
if (!_manualPreloadImageNames) {
return;
}
[_manualPreloadImageNames addObjectsFromArray:imageNames];
}
+ (void)preloadImageWithImageName:(NSString *)imageName {
NSParameterAssert(imageName);
if (!imageName.length) {
return;
}
if (![imageName isKindOfClass:[NSString class]]) {
return;
}
[self preloadImagesWithImageNames:@[imageName]];
}
+ (void)preloadImagesIfNeed {
if (@available(iOS 9.0, *)) {
[self manualPreloadArrayInitIfNeed];
NSArray<NSString *> *imageNames = [[NSUserDefaults standardUserDefaults] valueForKey:kBLImagePreloadManagerStoreKey];
if (imageNames.count) {
[_manualPreloadImageNames addObjectsFromArray:imageNames];
}
if (!_manualPreloadImageNames || !_manualPreloadImageNames.count) {
return;
}
BOOL bl_imageWithNameEnable = [UIImage respondsToSelector:@selector(bl_imageNamed:)];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
for (NSString *imageName in _manualPreloadImageNames) {
if ([imageName isKindOfClass:[NSString class]]) {
bl_imageWithNameEnable ? [UIImage bl_imageNamed:imageName] : [UIImage imageNamed:imageName];
}
}
});
}
}
+ (void)storeImageNameForPreload:(NSString *)imageName {
NSParameterAssert(imageName);
if (![imageName isKindOfClass:[NSString class]]) {
return;
}
if (_isStoreTimeTick) {
return;
}
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_kImageNameCollectSetM = [NSMutableSet set];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kBLImagePreloadManagerStoreImageTimeInterval * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
_isStoreTimeTick = YES;
[self internalFinishCollectImageName];
});
});
dispatch_async(_ioQueue, ^{
if (_kImageNameCollectSetM && imageName.length) {
[_kImageNameCollectSetM addObject:imageName];
}
});
}
+ (void)internalFinishCollectImageName {
if (!_kImageNameCollectSetM || !_kImageNameCollectSetM.count) {
[self releaseResources];
return;
}
dispatch_async(_ioQueue, ^{
[[NSUserDefaults standardUserDefaults] setObject:[_kImageNameCollectSetM allObjects] forKey:kBLImagePreloadManagerStoreKey];
[self releaseResources];
});
}
+ (void)releaseResources {
_kImageNameCollectSetM = nil;
_ioQueue = nil;
_manualPreloadImageNames = nil;
}
+ (void)manualPreloadArrayInitIfNeed {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if(!_manualPreloadImageNames && !_isStoreTimeTick) {
_manualPreloadImageNames = @[].mutableCopy;
}
});
}
@end
複製代碼
有了這一層優化之後,仍然在個人 iPhone 6s Plus 上進行十組測試,咱們一塊兒來看下優化後的結果:
// 第 1 組.
2018-08-14 18:37:03.434442+0800 beiliao[2603:1056626] 從數據加載完成到首頁開始渲染耗時: 0.253540
// 第 2 組.
2018-08-14 18:38:11.953393+0800 beiliao[2608:1057951] 從數據加載完成到首頁開始渲染耗時: 0.265548
// 第 3 組.
2018-08-14 18:38:41.851729+0800 beiliao[2610:1058585] 從數據加載完成到首頁開始渲染耗時: 0.263075
// 第 4 組.
2018-08-14 18:39:13.515297+0800 beiliao[2612:1059171] 從數據加載完成到首頁開始渲染耗時: 0.293209
// 第 5 組.
2018-08-14 18:39:47.610475+0800 beiliao[2614:1059832] 從數據加載完成到首頁開始渲染耗時: 0.268341
// 第 6 組.
2018-08-14 18:40:55.798904+0800 beiliao[2618:1061142] 從數據加載完成到首頁開始渲染耗時: 0.263902
// 第 7 組.
2018-08-14 18:41:25.785528+0800 beiliao[2621:1061772] 從數據加載完成到首頁開始渲染耗時: 0.257506
// 第 8 組.
2018-08-14 18:41:56.550695+0800 beiliao[2623:1062409] 從數據加載完成到首頁開始渲染耗時: 0.291573
// 第 9 組.
2018-08-14 18:42:27.200791+0800 beiliao[2625:1063009] 從數據加載完成到首頁開始渲染耗時: 0.233717
// 第 10 組.
2018-08-14 18:42:58.853888+0800 beiliao[2627:1063666] 從數據加載完成到首頁開始渲染耗時: 0.299298
複製代碼
能夠看到,數據範圍從 0.253540 - 0.299298,平均值爲0.268981。比優化前的平均值1.05563,減小 75%,效果很是明顯。