2015-08-03 95 views
17

在我的iOS應用程序中,我有UICollectionView,它顯示大約1200個小(35x35點)圖像。這些圖像存儲在應用程序包中。如何提高包含大量小圖片的UCollectionView的性能?

我正確地重用UICollectionViewCell秒,但仍然有取決於我如何處理圖像加載性能問題:

  • 我的應用程序是應用程序擴展和那些具有有限內存(40 MB在這種情況下)。將所有1200張圖片放入資產目錄並使用UIImage(named: "imageName")加載它們導致內存崩潰 - 系統緩存的圖像填滿了內存。在某些時候,應用程序需要分配更大的內存部分,但由於緩存的圖像,這些部分不可用。操作系統不是觸發內存警告和清理緩存,而是殺死了應用程序。

  • 我改變了避免圖像緩存的方法。我把圖像放到我的項目中(不是作爲目錄),我現在用NSBundle.mainBundle().pathForResource("imageName", ofType: "png")加載它們。該應用程序不再因內存錯誤而崩潰,但單個圖像的加載需要更長的時間,即使在最新的iPhone上,快速滾動也是滯後的。

我在圖像上完全控制研究,並可以將它們例如以.JPEG或優化他們(我已經嘗試過ImageOptim並沒有成功一些其他的選擇)。

我該如何解決這兩個性能問題?


編輯1:

我也試過在後臺線程加載圖像。這是從我的UICollectionViewCell子類代碼:

private func loadImageNamed(name: String) { 
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), { [weak self]() in 
     let image = bundle.pathForResource(name, ofType: "png")?.CGImage 
     if name == self?.displayedImageName { 
      dispatch_async(dispatch_get_main_queue(), { 
       if name == self?.displayedImageName { 
        self?.contentView.layer.contents = image 
       } 
      }) 
     } 
    }) 
} 

滾動到某一位置以編程時,這使得滾動平滑而不消耗額外的存儲器,用於緩存(例如,當UICollectionView滾動到頂部)它會導致另一個問題 :在滾動動畫過程中,圖像不會更新(滾動速度太快無法加載),滾動完成後會顯示錯誤的圖像以顯示秒數 - 並且一個接一個地替換爲正確的圖像。這在視覺上非常令人不安。


編輯2:

我不能組小圖像成更大組成的圖像,並通過this answer的建議顯示的那些。

原因:

  • 考慮不同的屏幕大小和方向。必須爲它們中的每一個預先製作圖像,這將使應用下載巨大。
  • 小圖像可以按不同順序顯示,其中一些可能在某些情況下隱藏。我完全無法爲每種可能的組合和順序預先分解圖像。
+1

你需要某種形式的緩存可以從您的需求,標準的圖像填充,但隨後快速訪問一次的事情是建立和運行,從而滾動等是光滑的。以下鏈接似乎提供了這樣的機制。看起來它使用內存映射文件並避免了大部分開銷。也許值得一瞧? https://開頭github上。com/path/FastImageCache –

回答

4

將PNG更改爲JPEG將無助於保存內存,因爲從文件加載映像到內存時,它會從壓縮數據中提取到未壓縮的字節。

而對於性能問題,我建議您異步加載圖像並使用委託/塊更新視圖。並保留一些圖像內存(但不是所有的,比方說100)

希望這有助於!

+0

請參閱我的編輯。 – drasto

+0

@drasto爲你的「編輯1」問題應該是你沒有設置內容的圖像爲零之前分派一個新的任務獲取圖像。 – Kevin

2

檢查本教程:

http://www.raywenderlich.com/86365/asyncdisplaykit-tutorial-achieving-60-fps-scrolling

它使用AsyncDisplayKit到在後臺線程加載圖像。

+0

請參閱我的編輯。 – drasto

+0

爲什麼不試試這個教程?它完全解決了你的問題。 –

+0

我正在閱讀它。但它似乎並不是我所需要的:它使用AsyncDisplayKit在後臺加載圖像,並在加載時顯示一些佔位符。但佔位符在我的情況下看起來非常糟糕 - 想象一下,在收集視圖中有1200個35x35圖像,您在底部,按一個按鈕滾動到頂部。大約1100個佔位符將滾動。這不會讓你對滾動的內容有太多的感覺...... – drasto

5

我可以提出可能可以解決您的問題的替代方法:
考慮將圖像塊呈現爲單個合成圖像。這樣大的圖像應該覆蓋應用程序窗口的大小。對於用戶來說,它看起來像是收集小圖像,但從技術上講,它將是大圖像的表格。

您目前的佈局:

|  |  |  | 
| cell | cell | cell | -> cells outside of screen 
|  |  |  | 
************************ 
*|  |  |  |* 
*| cell | cell | cell |* -> cells displayed on screen 
*|  |  |  |* 
*----------------------* 
*|  |  |  |* 
*| cell | cell | cell |* -> cells displayed on screen 
*|  |  |  |* 
*----------------------* 
*|  |  |  |* 
*| cell | cell | cell |* -> cells displayed on screen 
*|  |  |  |* 
************************ 
|  |  |  | 
| cell | cell | cell | -> cells outside of screen 
|  |  |  | 

建議佈局:

|     | 
|  cell with  | 
| composed image | -> cell outside of screen 
|     | 
************************ 
*|     |* 
*|     |* 
*|     |* 
*|     |* 
*|  cell with  |* 
*| composed image |* -> cell displayed on screen 
*|     |* 
*|     |* 
*|     |* 
*|     |* 
*|     |* 
************************ 
|     | 
|  cell with  | 
| composed image | -> cell outside of screen 
|     | 

理想的情況下,如果你預先渲染這樣的合成圖像,並把它們在構建時的項目,但也可以使它們在運行。肯定第一個變體的工作速度要快得多。但是在任何情況下,單張大圖像的成本都較低,然後將圖像分開。

如果您有可能預渲染它們,然後使用JPEG格式。在這種情況下,可能您的第一個解決方案(在主線程上使用[UIImage imageNamed:]加載映像)可以很好地工作,因爲使用的內存少,佈局要簡單得多。

如果您必須在運行環境中渲染它們,那麼您將需要使用當前的解決方案(在後臺工作),並且在快速動畫發生時仍然會看到圖像錯位,但在這種情況下,它將是單錯位一個圖像覆蓋窗口框架),所以它應該看起來更好。

如果您需要知道用戶點擊了什麼圖片(原始小圖片35x35),您可以使用附加到單元格的UITapGestureRecognizer。當識別到手勢時,可以使用locationInView:方法計算小圖像的正確索引。

我不能說它100%可以解決你的問題,但它是有道理的嘗試。

+0

這是一個好主意,但我不幸的是不能這麼做 - 有很多原因讓我必須將它顯示爲許多小圖片。首先,圖像每次可能以不同的順序出現,有些可能會丟失等。其次,對於不同的屏幕尺寸,特定屏幕上會顯示不同的圖像。例如,在iPhone 6 Plus上,更多圖像適用於iPhone 5S屏幕。我將不得不爲每個屏幕大小和方向分別預先製作圖像,這將使應用程序的下載規模巨大。有更多的原因,我爲什麼不能做你的建議... – drasto

3

您應該創建一個隊列以異步加載圖像。最佳選擇是後進先出隊列。你可以看看這個LIFOOperationQueue。 一個重要的事情是防止顯示錯誤的圖像,需要分開處理。爲此,當您創建一個操作來加載圖像時,請將其當前的indexPath作爲標識符。然後在回調函數,檢查給定indexPath是可見的更新視圖

if (self.tableView.visibleIndexPath().containsObject(indexPath) { 
    cell.imageView.image = img; 
} 

你也應該需要定製的LIFOOperationQueue有隊列中的任務的最大數量,以便它可以消除不必要的任務。最好設置任務的最大數量爲1.5 * numberOfVisibleCell

最後一件事,你應該在willDisplayCell代替cellForRowAtIndexPath

+0

謝謝你有趣的答案。但是,我還有一些其他問題:通過編輯1中的代碼進行異步加載,LIFOOperationQueue是否有優勢?它將在編輯1中描述的編程式滾動時解決視覺問題嗎?爲什麼我應該在'willDisplayCell'而不是'cellForRowAtIndexPath'中加載圖像? – drasto

+1

如果你使用隊列,你可以取消不必要的任務,而你的答案不能這樣做。它仍然無法解決視覺問題,所以你必須自己動手,正如我在答案中所描述的那樣。當然,willDisplay總是在cellForRow之前調用,所以它會更快 – sahara108

2

要解決在EDIT 1描述問題創造負荷圖像操作,你應該重寫prepareForReuse方法在UICollectionViewCell子類,圖層的內容復位到零:

- (void) prepareForReuse 
{ 
    self.contentView.layer.contents = nil; 
} 

加載您的圖像中的背景,你可以使用下一個:

- (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath 
{ 
    NSString* imagePath = #image_path#; 
    weakself; 
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 
     //load image 
     UIImage *image = [UIImage imageWithContentsOfFile:imagePath]; 

     //redraw image using device context 
     UIGraphicsBeginImageContextWithOptions(image.size, YES, 0); 
     [image drawAtPoint:CGPointZero]; 
     image = UIGraphicsGetImageFromCurrentImageContext(); 
     UIGraphicsEndImageContext(); 

     dispatch_async(dispatch_get_main_queue(), ^{ 
      strongself; 
      if ([[self.collectionView indexPathsForVisibleItems] indexOfObject:indexPath] != NSNotFound) { 
       cell.contentView.layer.contents = (__bridge id)(image.CGImage); 
      } else { 
       // cache image via NSCache with countLimit or custom approach 
      } 
     }); 
    }); 
} 

如何改善:

  1. 使用NSCache或自定義緩存算法爲了不滾動時的所有時間加載 圖像。
  2. 使用正確的文件格式。 JPEG解壓縮對照片等圖像的處理速度更快。使用PNG圖像爲與 顏色的平坦區域,鋒利的線條,等
4
  1. 沒有必要從文件目錄中出現的每個單元時間內獲取圖像。
  2. 一旦你獲取圖像,你可以將它保存在NSCache中,下次你只需要從NSCache獲取圖像,而不是從文檔目錄再次獲取圖像。
  3. 爲NSCache objCache創建一個對象;
  4. 在你cellForItemAtIndexPath,只寫下來

    UIImage *cachedImage = [objCache objectForKey:arr_PathFromDocumentDirectory[indexPath.row]]; 
    
    if (cachedImage) { 
        imgView_Cell.image = cachedImage; 
    } else { 
        dispatch_queue_t q = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul); 
        dispatch_async(q, ^{ 
         /* Fetch the image from the Document directory... */ 
         [self downloadImageWithURL:arr_PathFromDocument[indexPath.row] completionBlock:^(BOOL succeeded, CGImageRef data, NSString* path) { 
          if (succeeded) { 
           dispatch_async(dispatch_get_main_queue(), ^{ 
            UIImage *img =[UIImage imageWithCGImage:data]; 
            imgView_Cell.image = img; 
            [objCache setObject:img forKey::arr_PathFromDocument[indexPath.row]]; 
           }); 
          } 
         }]; 
        }); 
    } 
    
  5. 一旦獲取圖像,與路徑設置在NSCache。下一次它將檢查是否已經下載,然後只從緩存設置。

如果您需要任何幫助,請告訴我。

謝謝!

+0

這是一個很好的答案,可以解決您的問題。另一個需要研究的東西是一個像https://github.com/path/FastImageCache這樣的開源項目,但它使用相同的原理。您仍然可能遇到的問題是擴展名爲40MB的上限。 – mclaughlinj

+0

@mclaughlinj我不認爲緩存是一個可行的選擇,因爲可用內存非常有限:40MB用於*所有*。這是針對所有視圖,動畫,數據結構。另外,鍵盤擴展存在非常奇怪的內存管理 - 在應用程序因內存限制死亡之前,您並不總是收到內存警告。我有*可能* 10MB保留在那裏,所以我可以使用5MB進行緩存,但這太少了,無法使用。 – drasto

3

夫婦的事情,你可以爲此做,

  1. 你應該只加載這些圖像在其中獲取的,並卸載它們時不重新獲得這會讓你的應用程序使用較少的內存 。
  2. 當你在後臺加載圖像時,在後臺線程中解碼它,然後將其設置爲圖像視圖...在默認情況下 圖像將在您設置時解碼並且通常會在主線程上發生 線程,這將使滾動平滑,您可以通過下面的代碼解碼圖像。

    static UIImage *decodedImageFromData:(NSData *data, BOOL isJPEG) 
    { 
        // Create dataProider object from provided data 
        CGDataProviderRef dataProvider = CGDataProviderCreateWithCFData((__bridge CFDataRef)data); 
        // Create CGImageRef from dataProviderObject 
        CGImageRef newImage = (isJPEG) ? CGImageCreateWithJPEGDataProvider(dataProvider, NULL, NO, kCGRenderingIntentDefault) : CGImageCreateWithPNGDataProvider(dataProvider, NULL, NO, kCGRenderingIntentDefault); 
    
    
    
    // Get width and height info from image 
    const size_t width = CGImageGetWidth(newImage); 
    const size_t height = CGImageGetHeight(newImage); 
    
    // Create colorspace 
    const CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB(); 
    // Create context object, provide data ref if wean to store other wise NULL 
    // Set width and height of image 
    // Number of bits per comoponent 
    // Number of bytes per row 
    // set color space 
    // And tell what CGBitMapInfo 
    const CGContextRef context = CGBitmapContextCreate(NULL,width, height,8, width * 4, colorspace, (CGBitmapInfo)kCGImageAlphaPremultipliedLast); 
    
    //Now we can draw image 
    CGContextDrawImage(context, CGRectMake(0, 0, width, height), newImage); 
    // Get CGImage from drwan context 
    CGImageRef drawnImage = CGBitmapContextCreateImage(context); 
    CGContextRelease(context); 
    CGColorSpaceRelease(colorspace); 
    
    // Create UIImage from CGImage 
    UIImage *image = [UIImage imageWithCGImage:drawnImage]; 
    
    CGDataProviderRelease(dataProvider); 
    CGImageRelease(newImage); 
    CGImageRelease(drawnImage); 
    return image; 
    

    }