云端 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