雲端 Mac 驗證 iOS 影像縮圖降採樣與像素上限

雲端 Mac 驗證 iOS 影像縮圖降採樣與像素上限

一張高解析度照片在 iOS 列表中只佔一小格,快速捲動時卻可能反覆觸發解碼。如果程式先載入原圖,再把 UIImage 縮成顯示尺寸,畫面上雖然只看到縮圖,解碼時卻可能已經處理了整張影像。在雲端 Mac 執行迴歸測試時,先把「縮圖不得超過預定的像素邊長」訂成可驗證的契約,比只觀察列表捲動是否順暢更可靠。

先訂輸出像素上限,不靠檔案大小猜測

本文以最大邊長 320 像素作為測試範例,並非所有介面都適用的固定值。實際專案應依據儲存格的點數、目標螢幕的倍率與裁切方式,推算要請求的影像尺寸。例如,要顯示在高倍率螢幕上的圖片,輸出像素尺寸就不能只按儲存格的點數計算。確定上限後,讓圖片載入入口接收這個值;不要在各個列表控制器分別寫死常數。

將測試用的 JPEG 放進測試目標的資源,命名為 landscape.jpg。樣本原始的寬與高都必須大於測試上限,否則即使實作直接回傳原圖,測試也可能誤判通過。樣本檔應隨儲存庫一同提交,不要在測試時才從網路下載;另外保留長寬比不同的橫向與直向樣本,避免只測到一種版面。檢查測試目標的資源設定,確認執行時確實能從測試 bundle 取得檔案。

輸出尺寸的斷言回答的是「產生的影像有多大」,不是「尖峰記憶體用量有多高」。兩者必須分開驗收。

在解碼入口產生縮圖

使用 ImageIO 從檔案 URL 建立影像來源,並在產生縮圖時設定最大像素邊長。範例函式回傳 CGImage,方便測試直接讀取像素寬高;介面層需要 UIImage 時,再於呼叫處轉換。建立影像來源時關閉立即快取,避免先主動解碼原圖;產生縮圖時則要求立即完成縮圖解碼。

import Foundation
import ImageIO

func makeThumbnail(at url: URL, maxPixelSize: Int) -> CGImage? {
    guard maxPixelSize > 0 else { return nil }

    let sourceOptions: [CFString: Any] = [
        kCGImageSourceShouldCache: false
    ]
    guard let source = CGImageSourceCreateWithURL(
        url as CFURL, sourceOptions as CFDictionary
    ) else {
        return nil
    }

    let thumbnailOptions: [CFString: Any] = [
        kCGImageSourceCreateThumbnailFromImageAlways: true,
        kCGImageSourceCreateThumbnailWithTransform: true,
        kCGImageSourceThumbnailMaxPixelSize: maxPixelSize,
        kCGImageSourceShouldCacheImmediately: true
    ]
    return CGImageSourceCreateThumbnailAtIndex(
        source, 0, thumbnailOptions as CFDictionary
    )
}

這裡使用 CreateThumbnailFromImageAlways,讓測試明確涵蓋縮圖產生路徑;CreateThumbnailWithTransform 則讓輸出遵循影像的方向資訊。不同方向的樣本可能使寬高對調,因此斷言應檢查兩條邊的最大值,不要把「寬必須大於高」當成所有圖片通用的規則。輸入無效或無法解碼時,函式會回傳 nil;介面層仍須提供可見的失敗狀態,不能假設每個檔案都有效。

用固定樣本進行尺寸迴歸測試

將正式使用的函式放進應用程式目標,並讓測試目標可以存取。以下測試假設測試資源已加入 bundle;如果資源檔位於子目錄,應依專案實際路徑調整尋找方式。樣本遺失或解碼失敗時,XCTUnwrap 會直接指出失敗位置,而不會讓可選值被忽略。

import XCTest
import ImageIO
@testable import Sample

final class ThumbnailTests: XCTestCase {
    func testThumbnailRespectsPixelLimit() throws {
        let url = try XCTUnwrap(
            Bundle(for: Self.self).url(
                forResource: "landscape",
                withExtension: "jpg"
            )
        )
        let image = try XCTUnwrap(
            makeThumbnail(at: url, maxPixelSize: 320)
        )

        XCTAssertGreaterThan(image.width, 0)
        XCTAssertGreaterThan(image.height, 0)
        XCTAssertLessThanOrEqual(
            max(image.width, image.height), 320
        )
    }

    func testRejectsInvalidLimit() throws {
        let url = try XCTUnwrap(
            Bundle(for: Self.self).url(
                forResource: "landscape",
                withExtension: "jpg"
            )
        )
        XCTAssertNil(makeThumbnail(at: url, maxPixelSize: 0))
    }
}

把 Sample 換成實際的模組名稱。除了尺寸測試,再加入損毀檔案樣本,驗證呼叫端能處理 nil;也加入帶有方向資訊的樣本,確認顯示方向符合預期。測試圖片應有明確來源與使用權限,避免把個人相簿檔案提交到儲存庫。

在雲端 Mac 執行測試並排查誤報

先用 xcrun simctl list devices available 查看目前安裝的模擬器,再選擇實際存在的裝置作為 xcodebuild 的 destination。專案名稱、scheme 和測試目標也要換成儲存庫中的名稱;不要直接複製範例參數,便假設該裝置可用。

xcrun simctl list devices available
xcodebuild \
  -project Sample.xcodeproj \
  -scheme Sample \
  -destination 'platform=iOS Simulator,name=iPhone 16' \
  -only-testing:SampleTests/ThumbnailTests \
  test

若測試回報「找不到圖片」,先檢查檔案是否已加入測試目標的資源,而不是調高尺寸上限。若輸出超過上限,確認列表實際呼叫了 makeThumbnail,沒有繞回先建立原始 UIImage 的舊路徑。在 CI 中,也應保留失敗測試的命令輸出,並固定使用同一批樣本檔。換圖卻不檢查像素尺寸與方向,會讓前後兩次結果失去可比性。

尺寸驗收與記憶體觀察分開進行

尺寸測試適合每次提交都執行,但無法模擬真實列表同時持有多張圖片、圖片快取與快速捲動的情況。合併變更前,應在目標裝置上使用具代表性的圖片集合,沿實際列表操作路徑反覆捲動,觀察記憶體峰值及圖片釋放情況。記錄裝置、系統版本、圖片數量與操作步驟,再比較修改前後的結果;不要把某一次模擬器讀數當成所有裝置的結論。

最後檢查三件事:縮圖的請求尺寸是否根據顯示需求推算;所有圖片載入入口是否都經過同一個降採樣函式;無效圖片是否有可見的處理方式。如此一來,雲端 Mac 上的自動化測試負責守住像素契約,裝置上的量測則負責回答實際記憶體用量問題;兩種證據各有明確界線。

常見問題

載入 UIImage 後再縮小,等於降採樣嗎?

不等於。原圖可能已先以完整尺寸解碼;應在 ImageIO 建立縮圖時就指定最大像素邊長。

縮圖尺寸測試通過,能證明記憶體用量安全嗎?

不能。斷言只檢查輸出尺寸;仍須以代表性影像和實際捲動操作,在目標裝置上另外量測峰值。

在專屬實體節點上開始作業

為下一項任務設定雲端 Mac

依照模型所需記憶體或建置負載選擇 Mac mini。三種機型支援按日、週、月或季租用;節點供應狀態以控制台即時顯示為準。

立即租用雲端 Mac