老實說,UITableView性能優化 這個話題,最常常遇到的仍是在面試中,常見的回答例如:ios
最近遇到一個需求,對tableView
有中級優化需求git
tableView
滾動的時候,滾動到哪行,哪行的圖片才加載並顯示,滾動過程當中圖片不加載顯示;以最多見的cell加載webImage爲例:github
- (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
沒進入到界面中(還不可見),不會調用- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
去渲染cell,在cell中若是設置loadImage
,不會調用;cell
進去界面中的時候,再進行cell渲染(不管是init仍是從複用池中取)YYCache
會對圖片進行數據緩存,以key
:value
的形式,這裏的key = imageUrl
,value = 下載的image圖片
YYCache
中是否有該url,有的話,直接讀取緩存圖片數據,沒有的話,走圖片下載邏輯,並緩存圖片如上設置,若是咱們cell一行有20行,頁面啓動的時候,直接滑動到最底部,20個cell都進入過了界面,- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
被調用了20次,不符合 需求1
的要求web
解決辦法:面試
cell
每次被渲染時,判斷當前tableView
是否處於滾動狀態,是的話,不加載圖片;cell
滾動結束的時候,獲取當前界面內可見的全部cell
2
的基礎之上,讓全部的cell
請求圖片數據,並顯示出來- (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非滾動狀態下,才進行圖片下載並渲染 if (!tableView.dragging && !tableView.decelerating) { //下載圖片數據 - 並緩存 [ImageDownload loadImageWithModel:model success:^{ //主線程刷新UI dispatch_async(dispatch_get_main_queue(), ^{ cell.imageView.image = model.iconImage; }); }]; } }
- (void)p_loadImage{ //拿到界面內-全部的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; }); }]; } }
//手一直在拖拽控件 - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{ [self p_loadImage]; } //手放開了-使用慣性-產生的動畫效果 - (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
能夠理解爲用戶手已放開,試圖是否還在滾動(是否慣性效果)緩存
當前代碼生效的效果以下:
性能優化
- (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下的任務會暫停 中止滾動的時候 - 進入`defaultMode` - 繼續執行`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
app
- 默認狀況 - defaultRunLoopMode
- 滾動時候 - trackingMode
- 滾動的時候,進入
trackingMode
,致使defaultMode
下的任務會被暫停,中止滾動的時候 ==> 進入defaultMode
- 繼續執行defaultMode
下的任務 - 例如這裏的defaultMode
大tips:這裏,若是使用RunLoop,滾動的時候雖然不執行
defaultMode
,可是滾動一結束,以前cell中的p_loadImgeWithIndexPath
就會所有再被調用,致使相似YYWebImage
的效果,其實也是不知足需求,async
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ //p_loadImgeWithIndexPath一進入`NSDefaultRunLoopMode`就會執行 [self performSelector:@selector(p_loadImgeWithIndexPath:) withObject:indexPath afterDelay:0.0 inModes:@[NSDefaultRunLoopMode]]; }
效果如上
- 滾動的時候不加載圖片,滾動結束加載圖片-知足
- 滾動結束,以前滾動過程當中的
cell
會加載圖片 => 不知足需求
git reset --hard runloop以前
解決: 需求2. 頁面跳轉的時候,取消當前頁面的圖片加載請求;
- (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
思路:
indexPath
:manager
的格式,將當前的圖片下載操做存起來viewWillDisappear
的時候,取出當前線程字典中的全部線程對象,遍歷進行cancel
操做,完成需求最近網上各類互聯網公司裁人信息鋪天蓋地,甚至包括各類一線公司 ( X東 X乎 都扛不住了嗎-。-)iOS原本就是提早進入寒冬,iOS小白們能夠嘗試思考下這個問題
答:
YYWebImage
爲例,能夠先下載圖片,再對圖片進行圓角處理,再設置到cell
上顯示答: 若是是下載完,在回調中進行切割圓角的處理,其實緩存的圖片是原圖,等於每次取的時候,緩存中取出來的都是矩形圖片,每次set
都得作切割操做;
答:實際上是有的,簡單來講YYWebImage
能夠拆分紅兩部分,默認狀況下,咱們拿到的回調,是走了 download
&& cache
的流程了,這裏咱們多作一步,取出cache
中該url
路徑對應的圖片,進行圓角切割,再存儲到 cache中,就能保證之後每次拿到的就都是cacha
中已經裁切好的圓角圖片
詳情可見:
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小夥伴門能熬過這個冬天?
參考資料