iOS 多圖列表三級(jí)緩存優(yōu)化處理

??????在平時(shí)的開(kāi)發(fā)中,經(jīng)常遇到會(huì)UITableViewUICollectionView用來(lái)展示列表圖片肤舞,為了提升APP的流暢度紫新,提高用戶(hù)體驗(yàn),需要開(kāi)發(fā)者在不影響圖片質(zhì)量的條件下作出最優(yōu)的優(yōu)化處理李剖。

1芒率、為什么要做優(yōu)化

一般地,如果不用第三方也不做優(yōu)化處理篙顺,加載一張網(wǎng)絡(luò)圖片的代碼如下:

NSURL *imgURL = [NSURL URLWithString:[NSString stringWithFormat:@"%@",Model.imageURL]];
NSData *imgData = [NSData dataWithContentsOfURL:imgURL];
cell.imageView.image = [UIImage imageWithData:imgData];

運(yùn)行代碼偶芍,上下滾動(dòng)幾下充择,在手機(jī)和模擬器上都運(yùn)行對(duì)比一下,在網(wǎng)絡(luò)良好的情況下匪蟀,發(fā)現(xiàn)不論上拉還是下拉都很卡椎麦,如下圖是我在iPhone 7上做的測(cè)試;

demo1

分析:上下滑動(dòng)時(shí)造成卡頓的原因

??????1材彪、根據(jù)UITableViewCellUICollectionViewCell的復(fù)用機(jī)制观挎,當(dāng)上拉時(shí)應(yīng)該不會(huì)像下拉時(shí)一樣卡頓,這是為什么呢段化?由于沒(méi)有對(duì)已經(jīng)下載的圖片做緩存處理嘁捷,所以在下拉滑動(dòng)時(shí)和上拉滑動(dòng)是一樣的,把圖片又重新下載了一次显熏。也就是說(shuō)我滑了幾次雄嚣,這張圖片就下載了幾次!
??????如果服務(wù)器存儲(chǔ)的圖片非常大喘蟆,一張圖片可能有幾百M(fèi)缓升,那么下載一張圖片就消耗的時(shí)間很長(zhǎng),即使在網(wǎng)絡(luò)很好的情況下履肃。另外由于沒(méi)有做緩存處理仔沿,每次上下滑動(dòng)坐桩,圖片都會(huì)重新下載尺棋,這樣就會(huì)浪費(fèi)很多流量。
??????2绵跷、由于下載圖片是耗時(shí)操作膘螟,此處是直接放到主線(xiàn)程中操作的,所以下拉時(shí)非衬刖郑卡荆残;
??????這樣帶給用戶(hù)最直接的體驗(yàn)就是APP的流暢度很差、體驗(yàn)很Low净当。解決這些問(wèn)題内斯,優(yōu)化APP體驗(yàn)是非常必要的,本文的主要目的就是介紹如何從上面這兩個(gè)角度來(lái)大量圖片的列表做優(yōu)化處理。當(dāng)然對(duì)多圖列表的優(yōu)化要從各個(gè)細(xì)節(jié)入手像啼,本文所用到的是能夠帶來(lái)明顯效果的優(yōu)化處理方式俘闯。對(duì)于更多的深層次的優(yōu)化處理也有很多優(yōu)秀的博客和開(kāi)源框架可以參考。


2忽冻、圖片緩存簡(jiǎn)單處理

(1)真朗、使用NSMutableDictionary緩存

??????針對(duì)圖片重復(fù)下載的問(wèn)題,先從簡(jiǎn)單的開(kāi)始優(yōu)化僧诚,把已經(jīng)下載的圖片做本地緩存處理遮婶。一般情況下蝗碎,可以用數(shù)組或者字典把已經(jīng)下載的圖片存儲(chǔ)到內(nèi)存中,考慮到每張圖片都是不同的旗扑,這里可以選擇使用字典來(lái)存儲(chǔ)已經(jīng)下載的圖片,由于每行Cell都是唯一的(當(dāng)然使用圖片的URL來(lái)標(biāo)記也可以)蹦骑,這里可以使用indexPath.row來(lái)標(biāo)記圖片,修改代碼如下:

@property (strong, nonatomic) NSMutableDictionary *imgCacheHashMap;

- (NSMutableDictionary *)imgCacheHashMap {
   if (_imgCacheHashMap == nil) {
       _imgCacheHashMap = [NSMutableDictionary dictionary];
   }
   return _imgCacheHashMap;
}

UIImage *cacheImg = [self. imgCacheHashMap objectForKey:[NSString stringWithFormat:@"%ld",indexPath.row]];
   if (cacheImg) {
       cell.bookImg.image = cacheImg;
       NSLog(@"======================\n");
   } else {
   NSURL *imgURL = [NSURL URLWithString:[NSString stringWithFormat:@"%@",Model.images]];
   NSData *imgData = [NSData dataWithContentsOfURL:imgURL];
   cell.imageView.image = [UIImage imageWithData:imgData];
   
   [self.imgCacheHashMap setObject:[UIImage imageWithData:imgData] forKey:[NSString stringWithFormat:@"%ld",indexPath.row]];
   }

運(yùn)行代碼,如下圖所示臀防,這是在iPhone 8P上運(yùn)行的效果脊串,可以發(fā)現(xiàn)上拉時(shí)還是和之前一樣非常卡,但是在上拉返回的時(shí)候比之前流暢一些來(lái)清钥。

demo2

為了驗(yàn)證數(shù)據(jù)是否成功緩存到了內(nèi)存中琼锋,我把網(wǎng)絡(luò)關(guān)了,然后在重復(fù)上下滑動(dòng)操作祟昭,此時(shí)發(fā)現(xiàn)圖片依然可以加載出來(lái),說(shuō)明緩存是成功的缕坎。

(2)、使用NSCache緩存

??????通過(guò)上面采用字典緩存的方法篡悟,雖然優(yōu)化了上拉的流暢度谜叹,避免了重復(fù)下載的圖片問(wèn)題,但是這只是對(duì)圖片優(yōu)化處理的第一步搬葬,還需要更多的細(xì)節(jié)需要處理荷腊,iOS系統(tǒng)為開(kāi)發(fā)者提供了一個(gè)專(zhuān)門(mén)用于緩存的類(lèi)NSCache類(lèi),NSCache相對(duì)于NSDictionary具有更多優(yōu)勢(shì)急凰。
??????《Effective Objective-C 2.0》一書(shū)中第50條也推薦在緩存處理時(shí)采用NSCache女仰。
??????NSCache比較重要的兩個(gè)屬性countLimittotalCostLimit,分別用來(lái)設(shè)置緩存數(shù)據(jù)數(shù)量和緩存數(shù)據(jù)占據(jù)內(nèi)存大小,修改代碼如下:

///
@property (strong, nonatomic) NSCache *imgCacheData;

- (NSCache *)imgCacheData {
    if (_imgCacheData == nil) {
        _imgCacheData = [[NSCache alloc]init];
        _imgCacheData.delegate = self;
        //設(shè)置緩存數(shù)據(jù)數(shù)量
        _imgCacheData.countLimit = LINK_MAX;
        //設(shè)置緩存數(shù)據(jù)占據(jù)內(nèi)存大小
        _imgCacheData.totalCostLimit = 180 * MAX_CANON * MAX_CANON;
    }
    return _imgCacheData;
}
- (void)cache:(NSCache *)cache willEvictObject:(id)obj {
    printf("\n================== remove old data \n");
}

Apple為開(kāi)發(fā)者的提供NSCache的使用方法和NSDictionary是一樣的,在Xcode中摁住command鍵點(diǎn)擊NSCache即可看到系統(tǒng)提供的方法抡锈。
那么NSCache相對(duì)與NSDictionary有那些優(yōu)勢(shì)呢疾忍?

  • 1、NSCache能在內(nèi)存將要被耗盡時(shí)自動(dòng)清理緩存床三,而不需要開(kāi)發(fā)者在收到內(nèi)存警告時(shí)手動(dòng)清理內(nèi)存一罩;

  • 2、NSCache是線(xiàn)程安全的撇簿,也就是說(shuō)可以在不同的線(xiàn)程中訪(fǎng)問(wèn)數(shù)據(jù)聂渊;

  • 3、當(dāng)緩存的數(shù)據(jù)超過(guò)設(shè)置的數(shù)量時(shí)四瘫,NSCache默認(rèn)優(yōu)先移除最先添加的數(shù)據(jù)汉嗽,遵循先進(jìn)先出規(guī)則
    為了驗(yàn)證當(dāng)緩存數(shù)據(jù)超過(guò)設(shè)置NSCache的最大緩存數(shù)量時(shí),系統(tǒng)是如何做內(nèi)存處理的莲组,我做了一個(gè)測(cè)試:

 NSCache *testCache = [[NSCache alloc]init];
 //設(shè)置緩存數(shù)據(jù)數(shù)量
 testCache.countLimit = 5;
 for (int i = 0;i< 10;i++) {
   [testCache setObject:[NSNumber numberWithInt:i] forKey:[NSString stringWithFormat:@"%d",i]];
 }
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
     for (int x = 0;x<10;x++) {
         NSNumber *number = [testCache objectForKey:[NSString stringWithFormat:@"%d",x]];
         NSLog(@"cache data ==============%@",number);
        }
    });

demo 3

通過(guò)上面的Log可以看出诊胞,設(shè)置testCache最大緩存數(shù)量為5,但是我添加了10條數(shù)據(jù),此時(shí)可以確定testCache中只存了5條數(shù)據(jù)撵孤。打印testCache中的數(shù)據(jù)迈着,可以看出之前緩存的5條數(shù)據(jù)已經(jīng)被清理了。

那么問(wèn)題來(lái)了邪码,當(dāng)用戶(hù)干掉進(jìn)程之后再重新打開(kāi)APP時(shí)裕菠,之前NSCache緩存的數(shù)據(jù)還在內(nèi)存中嗎,此處來(lái)做個(gè)測(cè)試闭专,用Log來(lái)展示奴潘,核心代碼如下:

    UIImage *cacheImg = [self.imgCacheData objectForKey:[NSString stringWithFormat:@"%ld",(long)indexPath.row]];
    //檢查內(nèi)存緩存
    if (cacheImg) {
        cell.bookImg.image = cacheImg;
        NSLog(@"======================cache\n");
} else {
       NSLog(@"=====================download\n");
}
demo4

cache表示從緩存中獲取的圖片,download表示重新下載的圖片影钉,查看打印的日志當(dāng)我殺掉進(jìn)程重新打開(kāi)APP時(shí)圖片又重新下載來(lái)一次,根據(jù)打印的日志可以得出結(jié)論:
4画髓、NSCache存儲(chǔ)的數(shù)據(jù),在APP被殺掉或者重啟后緩存的數(shù)據(jù)就沒(méi)有了;
那么平委,有沒(méi)有一種方法奈虾,當(dāng)我下載一次圖片之后就緩存起來(lái)以后再也不用浪費(fèi)流量下載了呢?當(dāng)然可以廉赔,作為開(kāi)發(fā)者肉微,當(dāng)下載完成之后可以把圖片存入硬盤(pán),這樣就不用擔(dān)心APP被進(jìn)程被干掉后蜡塌,下次打開(kāi)還要繼續(xù)下載圖片的問(wèn)題了碉纳。


3、二級(jí)緩存--圖片硬盤(pán)緩存處理

既然使用NSCache緩存的圖片在APP被干掉后再重新打開(kāi)數(shù)據(jù)就沒(méi)有了馏艾,可以把圖片存儲(chǔ)到手機(jī)磁盤(pán)中劳曹,這樣就一勞永逸了,只要不清理攒至,以后都不用再次下載了厚者。那么該如何處理呢躁劣,此處結(jié)合前面的內(nèi)存緩存邏輯迫吐,我整理了一下現(xiàn)在的業(yè)務(wù)邏輯:

業(yè)務(wù)流程圖 1

  • 1、先判斷內(nèi)存中是否有圖片緩存账忘,如果有直接使用志膀;
  • 2、判斷手機(jī)硬盤(pán)中是否有圖片緩存鳖擒,如果有直接使用溉浙,并加入到磁盤(pán)緩存中,此處是考慮到從內(nèi)存中取圖片會(huì)比硬盤(pán)中取圖片性能高很多蒋荚;
  • 3戳稽、如果手機(jī)硬盤(pán)中沒(méi)有圖片緩存,開(kāi)啟線(xiàn)程去下載,下載完成后刷新列表惊奇,然后在分別加入到內(nèi)存和硬盤(pán)中互躬;
// 獲取沙盒路徑
- (NSString *)getComponentFile:(NSString *)fileName {
    //獲取沙盒路徑
    NSString *ComponentFileName = [fileName lastPathComponent];
    //獲取Cache路徑
    NSString *cachePahtStr = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
    //獲取完整路徑
    NSString *fullPathStr = [cachePahtStr stringByAppendingPathComponent:ComponentFileName];
    
    return fullPathStr;
}   

cellForItemAtIndexPathcellForRowAtIndexPath代理中實(shí)現(xiàn)上面的邏輯代碼為:

 // 先判斷內(nèi)存中是否有緩存數(shù)據(jù)
 UIImage *cacheImg = [weakSelf.imgCacheData objectForKey:[NSString stringWithFormat:@"%ld",(long)indexPath.row]];
   // 檢查內(nèi)存緩存
        if (cacheImg) {
            cell.imageView.image = cacheImg;
            NSLog(@"======================cache\n");
        } else {
            NSString *fullPathStr = [weakSelf getComponentFile:[NSString stringWithFormat:@"%d",(int)indexPath.row]];
            // 是否有硬盤(pán)緩存
            NSData *imgData = [NSData dataWithContentsOfFile:fullPathStr];
            if (imgData) {
                NSLog(@"=====================hard disk\n");
                // 賦值操作
                cell.imageView.image = [UIImage imageWithData:imgData];
                // 加入到內(nèi)存中
                [weakSelf.imgCacheData setObject:[UIImage imageWithData:imgData] forKey:[NSString stringWithFormat:@"%ld",(long)indexPath.row]];
            } else {
                NSURL *imgURL = [NSURL URLWithString:[NSString stringWithFormat:@"%@", Model.images]];
                NSData *imgData = [NSData dataWithContentsOfURL:imgURL];
                NSLog(@"======================download\n");
                //加入內(nèi)存緩存中
                [weakSelf.imgCacheData setObject:[UIImage imageWithData:imgData] forKey:[NSString stringWithFormat:@"%ld",(long)indexPath.row]];
                //同時(shí)緩存到硬盤(pán)中
                [imgData writeToFile:fullPathStr atomically:YES];
                //返回主線(xiàn)程
                dispatch_async(dispatch_get_main_queue(), ^{
                //下載完成后 刷新cell
                    [collectionView reloadItemsAtIndexPaths:@[indexPath]];
                 });
           }
    }

4、圖片異步下載

通過(guò)上面的二級(jí)緩存方式颂郎,已經(jīng)解決了圖片重復(fù)下載的問(wèn)題吼渡,后面就主要解決圖片在第一次下載時(shí)在主線(xiàn)程下載造成的卡頓問(wèn)題,由于下載圖片成功的時(shí)間不確定乓序,跟網(wǎng)絡(luò)寺酪、設(shè)備等一些條件有關(guān),可以把圖片下載操作異步執(zhí)行替劈。
主要思路為:下載圖片時(shí)開(kāi)啟一個(gè)隊(duì)列去下載寄雀,下載完成之后再返回主線(xiàn)程刷新UI,使用GCDNSOperationQueue都可以到達(dá)需求:

///
@property (retain, nonatomic) dispatch_queue_t GCDQueue;

- (dispatch_queue_t)GCDQueue {
    if (_GCDQueue == nil) {
        _GCDQueue = dispatch_queue_create(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    }
    return _GCDQueue;
}
///
@property (strong, nonatomic) NSOperationQueue *queue;

- (NSOperationQueue *)queue {
    if (_queue == nil) {
        _queue = [[NSOperationQueue alloc]init];
    }
    return _queue;
}

cellForItemAtIndexPathcellForRowAtIndexPath代理方法中實(shí)現(xiàn):

// 強(qiáng)引用 ---> 弱引用
 __weak typeof (self) weakSelf = self;
 //開(kāi)啟下載隊(duì)列
 dispatch_async(weakSelf.GCDQueue, ^{
       //圖片下載代碼 
       //下載完成后加入到內(nèi)存緩存和硬盤(pán)緩存中
       //返回主線(xiàn)程刷新列表
       dispatch_async(dispatch_get_main_queue(), ^{
          [collectionView reloadItemsAtIndexPaths:@[indexPath]];
        });
});
// 強(qiáng)引用 ---> 弱引用
__weak typeof (self) weakSelf = self;
//圖片下載代碼 
//下載完成后加入到內(nèi)存緩存和硬盤(pán)緩存中
NSBlockOperation *downloadBlock = [NSBlockOperation  blockOperationWithBlock:^{
   // 返回主線(xiàn)程
        [[NSOperationQueue mainQueue]addOperationWithBlock:^{
            //此處 數(shù)據(jù)源已經(jīng)變了 直接刷新Cell會(huì)再次調(diào)用cellForItemAtIndexPath代理,
            [collectionView reloadItemsAtIndexPaths:@[indexPath]];
        }];
 }];
//加入到隊(duì)列中
[self.queue addOperation:downloadBlock];

5陨献、當(dāng)列表滑動(dòng)時(shí)暫停下載圖片 停止后恢復(fù)下載

??????我們知道咙俩,iOS系統(tǒng)的RunLoop可以用來(lái)處理APP中的各種事件,所以可以用RunLoop來(lái)監(jiān)聽(tīng)列表的滑動(dòng)操作湿故。
這里也可以使用UIScrollViewDelegate代理來(lái)監(jiān)聽(tīng)列表的滑動(dòng)操作阿趁,Apple官方提供的LazyTablbeImages Demo就是用的這個(gè)思路。

方案1:監(jiān)聽(tīng)UIScrollView 的 delegate 回調(diào)

那么現(xiàn)在業(yè)務(wù)邏輯是:

  • (1)坛猪、先設(shè)置一張默認(rèn)占位圖片脖阵;
  • (2)、在開(kāi)始下載圖片之前先判斷列表是否在滑動(dòng)中墅茉,如果列表正在上下滑動(dòng)命黔,先暫停下載;
  • (3)就斤、等到列表停止滑動(dòng)后再開(kāi)始下載悍募。此處結(jié)合前面的處理邏輯,我畫(huà)了一張流程圖:


    業(yè)務(wù)流程圖 2

此處主要在scrollViewDidEndDragging(用戶(hù)停止拖拽時(shí))和scrollViewDidEndDecelerating(列表完全停止滑動(dòng)時(shí))這兩個(gè)代理中來(lái)處理洋机,核心代碼為:

// MARK: - download Image
- (void)loadImageForOnscreenRows {
    if (self.bookList.count > 0) {
        NSArray *visiblePaths = [self.listView indexPathsForVisibleItems];
        for (NSIndexPath *indexPath in visiblePaths) {
            //============================= get Model data
           Model *model = self.dataList[indexPath.row];
            [self startImageDownload:model forIndexPath:indexPath];
        }
    }else{
        return;
    }
}

//download
- (void)startImageDownload:(Model*)imgModel forIndexPath:(NSIndexPath *)indexPath {
    
    // 強(qiáng)引用 ---> 弱引用
    __weak typeof (self) weakSelf = self;
    //開(kāi)啟下載隊(duì)列
//    dispatch_async(weakSelf.GCDQueue, ^{
//
//        //返回主線(xiàn)程
//        dispatch_async(dispatch_get_main_queue(), ^{
//
//            MainCollectionCell *cell = (MainCollectionCell *)[self.listView cellForItemAtIndexPath:indexPath];
//            cell.bookImg.image = [UIImage imageWithData:imgData];
//        });
//    });
    
    NSBlockOperation *downloadBlock = [NSBlockOperation  blockOperationWithBlock:^{
        //獲取圖片URL
        NSURL *imgURL = [NSURL URLWithString:[NSString stringWithFormat:@"%@",Model.imageURL]];
        NSData *imgData = [NSData dataWithContentsOfURL:imgURL];
        NSLog(@"======================download\n");
        NSData *finallyImgData = UIImageJPEGRepresentation([UIImage imageWithData:imgData], 0.8);
        //
        //加入內(nèi)存緩存中
        [self.imgCacheData setObject:[UIImage imageWithData:finallyImgData] forKey:[NSString stringWithFormat:@"%ld",(long)indexPath.row]];
        //獲取沙盒路徑
        NSString *fullPathStr = [weakSelf getComponentFile:[NSString stringWithFormat:@"%ld",(long)indexPath.row]];
        //同時(shí)緩存到硬盤(pán)中
        [finallyImgData writeToFile:fullPathStr atomically:YES];
        // 返回主線(xiàn)程
        [[NSOperationQueue mainQueue]addOperationWithBlock:^{
            //此處 數(shù)據(jù)源已經(jīng)變了 直接刷新Cell會(huì)再次調(diào)用cellForItemAtIndexPath代理坠宴,
            [weakSelf.listView reloadItemsAtIndexPaths:@[indexPath]];
        }];
    }];
    //加入到隊(duì)列中
    [self.queue addOperation:downloadBlock];
}


// MARK: - UIScrollViewDelegate
//用戶(hù)停止拖拽時(shí)
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
    if(decelerate) {
        [self loadImageForOnscreenRows];
    }
}
// 完全停止?jié)L動(dòng)時(shí)
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    
    [self loadImageForOnscreenRows];
}
finally demo.gif
方案2:利用RunLoop優(yōu)化處理

系統(tǒng)為開(kāi)發(fā)者提供的RunLoop有五種運(yùn)行模式:

運(yùn)行模式 備注
kCFRunLoopDefaultMode App的默認(rèn) Mode,通常主線(xiàn)程是在這個(gè) Mode 下運(yùn)行的
UITrackingRunLoopMode 界面跟蹤 Mode绷旗,用于 ScrollView 追蹤觸摸滑動(dòng)喜鼓,保證界面滑動(dòng)時(shí)不受其他 Mode 影響
kCFRunLoopCommonModes 占位Mode,沒(méi)有實(shí)際作用
UIInitializationRunLoopMode APP啟動(dòng)時(shí)進(jìn)入的第一個(gè)模式衔肢,啟動(dòng)完成后就不再使用
GSEventReceiveRunLoopMode 接受系統(tǒng)內(nèi)部事件庄岖,通常用不到

當(dāng)用戶(hù)滑動(dòng)時(shí),切換到UITrackingRunLoopMode模式下角骤,此時(shí)NSDefaultRunLoopMode下的任務(wù)就暫停隅忿,直到再次切換到NSDefaultRunLoopMode下的時(shí)候,再繼續(xù)之前的下載任務(wù)。這和官方提供的思路雖不同但是也可以到達(dá)優(yōu)化的效果背桐,核心代碼為:

//runLoop tasks blocks
typedef void(^runloopBlock)(void);
// tasks Array
@property (nonatomic, strong) NSMutableArray *tasksList;
// Max tasks
@property (nonatomic, assign) NSUInteger maxTaskCount;

- (void)loadView {
    [super loadView];
   // Set the maximum number of tasks
    self.maxTaskCount = 6;
    // create Timer
    CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(runLoopStayActive)];
    // join to RunLoop
    [displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
    // add runloop observer
    [self addRunLoopObserver];
}

// MARK: - ================================= About RunLoop
/// RunLoop Stay active
- (void)runLoopStayActive {
    //do-nothing
}
/// RunLoop Observer
- (void)addRunLoopObserver {
    // get current now RunLoop
    CFRunLoopRef nowRunloop = CFRunLoopGetCurrent();
    // create tasks
    CFRunLoopObserverContext context = {
        0,
        (__bridge void *)(self),
        &CFRetain,
        &CFRelease,
        NULL
    };
    //
    static CFRunLoopObserverRef runLoopDefaultModeObserver;
    runLoopDefaultModeObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                                  kCFRunLoopBeforeWaiting,
                                                  YES,
                                                  0,
                                                  &callBack,
                                                  &context);
    // add Observer for RunLoop
    CFRunLoopAddObserver(nowRunloop, runLoopDefaultModeObserver, kCFRunLoopCommonModes);
    // memory release
    CFRelease(runLoopDefaultModeObserver);
}
///
static void callBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    MainViewController *mainView = (__bridge MainViewController *)info;
    //
    if (mainView.tasksList.count == 0) return;
    // Take the task from the array
    runloopBlock block = [mainView.tasksList firstObject];
    // Performing tasks
    if (block) {
        block();
    }
    // Remove the task after performing the task
    [mainView.tasksList removeObjectAtIndex:0];
}
/// add tasks
- (void)addTasks:(runloopBlock)blocks {
    // sava new tasks
    [self.tasksList addObject:blocks];
    // if the maximum number of tasks is exceeded, remove the previous task
    if (self.tasksList.count > self.maxTaskCount) {
        [self.tasksList removeObjectAtIndex:0];
    }
}

cellForRowAtIndexPathcellForItemAtIndexPath代理中添加代碼:

 [self addTasks:^{
    // do samething
 }

以上的思路逐步完成了對(duì)一個(gè)列表的優(yōu)化處理刘陶,但是可做優(yōu)化的地方還有很多,各位小伙伴可以閱讀這篇博客iOS 保持界面流暢的技巧牢撼,根據(jù)該文中的所說(shuō)的角度來(lái)優(yōu)化自己的APP.

因?yàn)楹?jiǎn)書(shū)上傳的圖片不能超過(guò)10M匙隔,所以在視頻轉(zhuǎn)gif的時(shí)候掉幀嚴(yán)重,建議各位小伙伴運(yùn)行一下本文demo,這樣能很明顯看到優(yōu)化前和優(yōu)化后的效果熏版。


本文是我在列表圖片加載上的一些優(yōu)化處理的總結(jié)纷责,如果有不對(duì)或者有歧義的地方歡迎指出交流。

demo下載撼短,請(qǐng)戳這里再膳。

本文參考了SDWebImage的設(shè)計(jì)思路以及下面的分享和demo
Apple Developer LazyTablbeImages Demo
《Effective Objective-C 2.0》第50條:構(gòu)建緩存時(shí)選用NSCache而非NSDictionary
iOS 保持界面流暢的技巧
深入理解RunLoop
iOS線(xiàn)下分享《RunLoop》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末曲横,一起剝皮案震驚了整個(gè)濱河市喂柒,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌禾嫉,老刑警劉巖灾杰,帶你破解...
    沈念sama閱讀 221,198評(píng)論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異熙参,居然都是意外死亡艳吠,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門(mén)孽椰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)昭娩,“玉大人,你說(shuō)我怎么就攤上這事黍匾±该欤” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 167,643評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵锐涯,是天一觀(guān)的道長(zhǎng)磕诊。 經(jīng)常有香客問(wèn)我,道長(zhǎng)全庸,這世上最難降的妖魔是什么秀仲? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,495評(píng)論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮壶笼,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘雁刷。我一直安慰自己覆劈,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,502評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著责语,像睡著了一般炮障。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上坤候,一...
    開(kāi)封第一講書(shū)人閱讀 52,156評(píng)論 1 308
  • 那天胁赢,我揣著相機(jī)與錄音,去河邊找鬼白筹。 笑死智末,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的徒河。 我是一名探鬼主播系馆,決...
    沈念sama閱讀 40,743評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼顽照!你這毒婦竟也來(lái)了由蘑?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,659評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤代兵,失蹤者是張志新(化名)和其女友劉穎尼酿,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體植影,經(jīng)...
    沈念sama閱讀 46,200評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡谓媒,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,282評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了何乎。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片句惯。...
    茶點(diǎn)故事閱讀 40,424評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖支救,靈堂內(nèi)的尸體忽然破棺而出抢野,到底是詐尸還是另有隱情,我是刑警寧澤各墨,帶...
    沈念sama閱讀 36,107評(píng)論 5 349
  • 正文 年R本政府宣布指孤,位于F島的核電站,受9級(jí)特大地震影響贬堵,放射性物質(zhì)發(fā)生泄漏恃轩。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,789評(píng)論 3 333
  • 文/蒙蒙 一黎做、第九天 我趴在偏房一處隱蔽的房頂上張望叉跛。 院中可真熱鬧,春花似錦蒸殿、人聲如沸筷厘。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,264評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)酥艳。三九已至摊溶,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間充石,已是汗流浹背莫换。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,390評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留骤铃,地道東北人拉岁。 一個(gè)月前我還...
    沈念sama閱讀 48,798評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像劲厌,于是被迫代替她去往敵國(guó)和親膛薛。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,435評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容

  • Swift1> Swift和OC的區(qū)別1.1> Swift沒(méi)有地址/指針的概念1.2> 泛型1.3> 類(lèi)型嚴(yán)謹(jǐn) 對(duì)...
    cosWriter閱讀 11,109評(píng)論 1 32
  • 面向?qū)ο蟮娜筇匦裕悍庋b补鼻、繼承哄啄、多態(tài) OC內(nèi)存管理 _strong 引用計(jì)數(shù)器來(lái)控制對(duì)象的生命周期。 _weak...
    運(yùn)氣不夠技術(shù)湊閱讀 1,106評(píng)論 0 10
  • iOS面試題目100道 1.線(xiàn)程和進(jìn)程的區(qū)別风范。 進(jìn)程是系統(tǒng)進(jìn)行資源分配和調(diào)度的一個(gè)獨(dú)立單位咨跌,線(xiàn)程是進(jìn)程的一個(gè)實(shí)體,...
    有度YouDo閱讀 29,926評(píng)論 8 137
  • ## iOS常用問(wèn)題總結(jié)#### iOS基礎(chǔ)知識(shí)回顧##### 1硼婿、為什么說(shuō)Objective-C是一門(mén)動(dòng)態(tài)的語(yǔ)言...
    蟬始鳴閱讀 473評(píng)論 0 3
  • 項(xiàng)目中一直都有使用SDWebImage锌半,對(duì)這個(gè)框架有一定的了解,但是體系卻未能貫通寇漫,因此特地整理下,主要參考: i...
    林大鵬閱讀 1,471評(píng)論 2 13