進階篇
最近遇到一個需求,對tableView有中級優化需求
要求 tableView 滾動的時候,滾動到哪行,哪行的圖檔才加載并顯示,滾動過程中圖檔不加載顯示;
頁面跳轉的時候,取消目前頁面的圖檔加載請求;
本人5年iOS開發經驗,曾就職于阿裡巴巴。 善于把艱澀的iOS知識轉化為通俗易懂的白話文字,同時也歡迎大家加入小編的iOS交流群 642363427,群裡會提供相關面試資料,書籍歡迎大家入駐!
以最常見的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沒進入到界面中(還不可見),不會調用- (UITableViewCell )tableView:(UITableView )tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath去渲染cell,在cell中如果設定loadImage,不會調用;
而當cell進去界面中的時候,再進行cell渲染(無論是init還是從複用池中取)
解釋下YYWebImage機制:
内部的YYCache會對圖檔進行資料緩存,以key:value的形式,這裡的key = imageUrl,value = 下載下傳的image圖檔
讀取的時候判斷YYCache中是否有該url,有的話,直接讀取緩存圖檔資料,沒有的話,走圖檔下載下傳邏輯,并緩存圖檔
問題所在:
如上設定,如果我們cell一行有20行,頁面啟動的時候,直接滑動到最底部,20個cell都進入過了界面,- (UITableViewCell )tableView:(UITableView )tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 被調用了20次,不符合 需求1的要求
解決辦法:
cell每次被渲染時,判斷目前tableView是否處于滾動狀态,是的話,不加載圖檔;
cell 滾動結束的時候,擷取目前界面内可見的所有cell
在2的基礎之上,讓所有的cell請求圖檔資料,并顯示出來
步驟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非滾動狀态下,才進行圖檔下載下傳并渲染
if (!tableView.dragging && !tableView.decelerating) {
//下載下傳圖檔資料 - 并緩存
[ImageDownload loadImageWithModel:model success:^{
//主線程重新整理UI
dispatch_async(dispatch_get_main_queue(), ^{
cell.imageView.image = model.iconImage;
});
}];
}
}
步驟2:
- (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;
});
}];
}
}
步驟3:
//手一直在拖拽控件
- (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
可以了解為使用者手已放開,試圖是否還在滾動(是否慣性效果)
ScrollView一次拖拽的代理方法執行流程:
目前代碼生效的效果如下:
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下的任務會暫停
停止滾動的時候 - 進入`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
預設情況 - defaultRunLoopMode
滾動時候 - trackingMode
滾動的時候,進入trackingMode,導緻defaultMode下的任務會被暫停,停止滾動的時候 ==> 進入defaultMode - 繼續執行defaultMode下的任務 - 例如這裡的defaultMode
大tips:這裡,如果使用RunLoop,滾動的時候雖然不執行defaultMode,但是滾動一結束,之前cell中的p_loadImgeWithIndexPath就會全部再被調用,導緻類似YYWebImage的效果,其實也是不滿足需求,
提示會被調用的代碼如下:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
//p_loadImgeWithIndexPath一進入`NSDefaultRunLoopMode`就會執行
[self performSelector:@selector(p_loadImgeWithIndexPath:)
withObject:indexPath
afterDelay:0.0
inModes:@[NSDefaultRunLoopMode]];
}
效果如上
滾動的時候不加載圖檔,滾動結束加載圖檔-滿足
滾動結束,之前滾動過程中的cell會加載圖檔 => 不滿足需求
版本復原到Runloop之前 - 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小白們可以嘗試思考下這個問題
問:UITableView的圓角性能優化如何實作
答:
讓伺服器直接傳圓角圖檔;
貝塞爾切割控件layer;
YYWebImage為例,可以先下載下傳圖檔,再對圖檔進行圓角處理,再設定到cell上顯示
問:YYWebImage 如何設定圓角? 在下載下傳完成的回調中?如果你在下載下傳完成的時候再切割,此時 YYWebImage 緩存中的圖檔是初始圖檔,還是圓角圖檔?(終于等到3了!!)
答: 如果是下載下傳完,在回調中進行切割圓角的處理,其實緩存的圖檔是原圖,等于每次取的時候,緩存中取出來的都是矩形圖檔,每次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小夥伴門能熬過這個冬天?