一張高解析度照片在 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。三種機型支援按日、週、月或季租用;節點供應狀態以控制台即時顯示為準。