老實說咱揍,UITableView性能優(yōu)化 這個話題喉刘,最經(jīng)常遇到的還是在面試中,常見的回答例如:
Cell復用機制
Cell高度預先計算
緩存Cell高度
圓角切割
等等. . .
進階篇
最近遇到一個需求,對tableView有中級優(yōu)化需求
要求
tableView
滾動的時候,滾動到哪行,哪行的圖片才加載并顯示,滾動過程中圖片不加載顯示;頁面跳轉(zhuǎn)的時候渣慕,取消當前頁面的圖片加載請求;
以最常見的cell加載webImage為例:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"]; if (!cell) { cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"]; } DemoModel *model = self.datas[indexPath.row]; cell.textLabel.text = model.text; [cell.imageView setYy_imageURL:[NSURL URLWithString:model.user.avatar_large]]; return cell;}
解釋下cell的復用機制:
如果cell沒進入到界面中(還不可見)抱慌,不會調(diào)用- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath去渲染cell,在cell中如果設置loadImage,不會調(diào)用;
而當cell進去界面中的時候眨猎,再進行cell渲染(無論是init還是從復用池中取)
解釋下YYWebImage機制:
內(nèi)部的YYCache會對圖片進行數(shù)據(jù)緩存抑进,以
key
:value
的形式,這里的key = imageUrl
睡陪,value = 下載的image圖片
讀取的時候判斷YYCache中是否有該url寺渗,有的話,直接讀取緩存圖片數(shù)據(jù)兰迫,沒有的話信殊,走圖片下載邏輯,并緩存圖片
問題所在:
如上設置汁果,如果我們cell一行有20行涡拘,頁面啟動的時候,直接滑動到最底部据德,20個cell都進入過了界面鳄乏,- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 被調(diào)用了20次跷车,不符合 需求1的要求
解決辦法:
cell每次被渲染時,判斷當前tableView是否處于滾動狀態(tài)橱野,是的話朽缴,不加載圖片;
cell 滾動結束的時候水援,獲取當前界面內(nèi)可見的所有cell
在2的基礎之上密强,讓所有的cell請求圖片數(shù)據(jù),并顯示出來
- 步驟1:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"]; if (!cell) { cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"]; } DemoModel *model = self.datas[indexPath.row]; cell.textLabel.text = model.text; //不在直接讓cell.imageView loadYYWebImage if (model.iconImage) { cell.imageView.image = model.iconImage; }else{ cell.imageView.image = [UIImage imageNamed:@"placeholder"]; //核心判斷:tableView非滾動狀態(tài)下蜗元,才進行圖片下載并渲染 if (!tableView.dragging && !tableView.decelerating) { //下載圖片數(shù)據(jù) - 并緩存 [ImageDownload loadImageWithModel:model success:^{ //主線程刷新UI dispatch_async(dispatch_get_main_queue(), ^{ cell.imageView.image = model.iconImage; }); }]; }}
- 步驟2:
- (void)p_loadImage{ //拿到界面內(nèi)-所有的cell的indexpath NSArray *visableCellIndexPaths = self.tableView.indexPathsForVisibleRows; for (NSIndexPath *indexPath in visableCellIndexPaths) { DemoModel *model = self.datas[indexPath.row]; if (model.iconImage) { continue; } UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath]; [ImageDownload loadImageWithModel:model success:^{ //主線程刷新UI dispatch_async(dispatch_get_main_queue(), ^{ cell.imageView.image = model.iconImage; }); }]; }}
- 步驟3:
//手一直在拖拽控件- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{ [self p_loadImage];}//手放開了-使用慣性-產(chǎn)生的動畫效果- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{ if(!decelerate){ //直接停止-無動畫 [self p_loadImage]; }else{ //有慣性的-會走`scrollViewDidEndDecelerating`方法或渤,這里不用設置 }}
dragging
:returns YES if user has started scrolling. this may require some time and or distance to move to initiate dragging
可以理解為,用戶在拖拽當前視圖滾動(手一直拉著)
deceleratingreturns
:returns YES if user isn't dragging (touch up) but scroll view is still moving
可以理解為用戶手已放開许帐,試圖是否還在滾動(是否慣性效果)
ScrollView一次拖拽的代理方法執(zhí)行流程:
當前代碼生效的效果如下:
RunLoop小操作
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"]; if (!cell) { cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"]; } DemoModel *model = self.datas[indexPath.row]; cell.textLabel.text = model.text; if (model.iconImage) { cell.imageView.image = model.iconImage; }else{ cell.imageView.image = [UIImage imageNamed:@"placeholder"]; /** runloop - 滾動時候 - trackingMode劳坑, - 默認情況 - defaultRunLoopMode ==> 滾動的時候,進入`trackingMode`成畦,defaultMode下的任務會暫停 停止?jié)L動的時候 - 進入`defaultMode` - 繼續(xù)執(zhí)行`trackingMode`下的任務 - 例如這里的loadImage */ [self performSelector:@selector(p_loadImgeWithIndexPath:) withObject:indexPath afterDelay:0.0 inModes:@[NSDefaultRunLoopMode]];}//下載圖片距芬,并渲染到cell上顯示- (void)p_loadImgeWithIndexPath:(NSIndexPath *)indexPath{ DemoModel *model = self.datas[indexPath.row]; UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath]; [ImageDownload loadImageWithModel:model success:^{ //主線程刷新UI dispatch_async(dispatch_get_main_queue(), ^{ cell.imageView.image = model.iconImage; }); }];}
效果與demo.gif的效果一致
runloop - 兩種常用模式介紹: trackingMode && defaultRunLoopMode
- 默認情況 - defaultRunLoopMode
- 滾動時候 - trackingMode
- 滾動的時候,進入trackingMode,導致defaultMode下的任務會被暫停,停止?jié)L動的時候 ==> 進入defaultMode - 繼續(xù)執(zhí)行defaultMode下的任務 - 例如這里的defaultMode
大tips:這里循帐,如果使用RunLoop框仔,滾動的時候雖然不執(zhí)行defaultMode,但是滾動一結束拄养,之前cell中的p_loadImgeWithIndexPath就會全部再被調(diào)用离斩,導致類似YYWebImage的效果,其實也是不滿足需求,
- 提示會被調(diào)用的代碼如下:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ //p_loadImgeWithIndexPath一進入`NSDefaultRunLoopMode`就會執(zhí)行 [self performSelector:@selector(p_loadImgeWithIndexPath:) withObject:indexPath afterDelay:0.0 inModes:@[NSDefaultRunLoopMode]];}
效果如上
- 滾動的時候不加載圖片,滾動結束加載圖片-滿足
- 滾動結束瘪匿,之前滾動過程中的cell會加載圖片 => 不滿足需求
版本回滾到Runloop之前 - git reset --hard runloop之前
解決: 需求2. 頁面跳轉(zhuǎn)的時候跛梗,取消當前頁面的圖片加載請求;
- (void)p_loadImgeWithIndexPath:(NSIndexPath *)indexPath{ DemoModel *model = self.datas[indexPath.row]; //保存當前正在下載的操作 ImageDownload *manager = self.imageLoadDic[indexPath]; if (!manager) { manager = [ImageDownload new]; //開始加載-保存到當前下載操作字典中 [self.imageLoadDic setObject:manager forKey:indexPath]; } [manager loadImageWithModel:model success:^{ //主線程刷新UI dispatch_async(dispatch_get_main_queue(), ^{ UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath]; cell.imageView.image = model.iconImage; }); //加載成功-從保存的當前下載操作字典中移除 [self.imageLoadDic removeObjectForKey:indexPath]; }];}- (void)viewWillDisappear:(BOOL)animated{ [super viewWillDisappear:animated]; NSArray *loadImageManagers = [self.imageLoadDic allValues]; //當前圖片下載操作全部取消 [loadImageManagers makeObjectsPerformSelector:@selector(cancelLoadImage)];}@implementation ImageDownload- (void)cancelLoadImage{ [_task cancel];}@end
思路:
創(chuàng)建一個可變字典棋弥,以indexPath:manager的格式核偿,將當前的圖片下載操作存起來
每次下載之前,將當前下載線程存入顽染,下載成功后漾岳,將該線程移除
在viewWillDisappear的時候,取出當前線程字典中的所有線程對象,遍歷進行cancel操作粉寞,完成需求
話外篇:面試題贈送
最近網(wǎng)上各種互聯(lián)網(wǎng)公司裁員信息鋪天蓋地尼荆,甚至包括各種一線公司 ( X東 X乎 都扛不住了嗎-。-)iOS本來就是提前進入寒冬唧垦,iOS小白們可以嘗試思考下這個問題
問:UITableView的圓角性能優(yōu)化如何實現(xiàn)
答:
讓服務器直接傳圓角圖片捅儒;
貝塞爾切割控件layer;
YYWebImage為例,可以先下載圖片野芒,再對圖片進行圓角處理蓄愁,再設置到cell上顯示
問:YYWebImage 如何設置圓角? 在下載完成的回調(diào)中?如果你在下載完成的時候再切割,此時 YYWebImage 緩存中的圖片是初始圖片狞悲,還是圓角圖片?(終于等到3了4樽ァ!)
答: 如果是下載完摇锋,在回調(diào)中進行切割圓角的處理丹拯,其實緩存的圖片是原圖,等于每次取的時候荸恕,緩存中取出來的都是矩形圖片乖酬,每次set都得做切割操作;
問: 那是否有解決辦法?
答:其實是有的融求,簡單來說YYWebImage 可以拆分成兩部分咬像,默認情況下,我們拿到的回調(diào)生宛,是走了 download && cache的流程了县昂,這里我們多做一步,取出cache中該url路徑對應的圖片陷舅,進行圓角切割倒彰,再存儲到 cacha中,就能保證以后每次拿到的就都是cacha中已經(jīng)裁切好的圓角圖片
詳情可見:
NSString *path = [[UIApplication sharedApplication].cachesPath stringByAppendingPathComponent:@"weibo.avatar"];YYImageCache *cache = [[YYImageCache alloc] initWithPath:path];manager = [[YYWebImageManager alloc] initWithCache:cache queue:[YYWebImageManager sharedManager].queue];manager.sharedTransformBlock = ^(UIImage *image, NSURL *url) { if (!image) return image; return [image imageByRoundCornerRadius:100]; // a large value};
SDWebImage
同理莱睁,它有暴露了一個方法出來待讳,可以直接設置保存圖片到磁盤中,無需修改源碼
“winner is coming”,如果面試正好遇到以上問題的仰剿,請叫我雷鋒~
衷心希望各位iOS小伙伴門能熬過這個冬天创淡?