ImageKnifePro 源码解读(七):降采样——大图解码时省内存的关键一步
一张 4000x3000 的照片,RGBA 四通道,每像素 4 字节,解码后占 4000 3000 4 = 48 MB。列表里塞 20 张这样的图片,光 PixelMap 就接近 1 GB,在大部分鸿蒙设备上会直接触发 OOM。但实际显示区域可能只有 360x270,真正需要的内存不到 400 KB。降采样的作用就是在解码阶段把图片缩小到接近显示尺寸,让系统分配的内存和实际需要的像素数量匹配。 ImageKnifePro 的降采样入口是 8 个枚举值,7 个具体策略对象(NONE 对应 nullptr)。 PNG 用 floor、WEBP 用 round、其余直接整除。PNG 下取整是因为它的行过滤器在非整像素宽度时可能产生解码错误,取小值更安全。WEBP 有损编码对像素数不那么敏感,四舍五入能保留更多像素。JPEG 等格式的解码器本身就按整数倍缩放,直接除法截断即可。 所有策略都继承 循环 5 次,依次右移 1、2、4、8、16 位并或上原值,覆盖 32 位整数的全部情况。输入 6 得到 4,输入 13 得到 8,输入 1 得到 1。这个操作的目的是把缩放因子对齐到 2 的幂次——JPEG 解码器的 这个策略的设计意图是做兜底保护。在 NONE 返回空串(不参与 key 计算),DEFAULT 只写策略名不写尺寸(因为它不依赖请求尺寸),其余策略把枚举值和请求宽高都编入字符串。 在 降采样依赖组件的显示尺寸,这个尺寸从哪来?ImageKnifePro 用了两种方式获取。 对于 尺寸变化后,把 vp 转成 px( 对于 两种方式都有同一个设计考量:如果组件还没完成首次测算( 降采样算出目标尺寸后,最终传递给系统解码器的接口是 系统解码器收到 DesiredSize 后,JPEG 会利用 DCT 系数做 1/2、1/4、1/8 倍降采样——这是硬件级别的加速,不需要先解码再缩放。PNG 和 WEBP 的解码器实现各有不同,但都能利用这个参数减少内存分配。 值得注意的是 MEMORY 和 QUALITY 后缀的差异归结为一个操作: DEFAULT 策略看似保守,但它的存在解决了一个实际问题:C 侧 ARKUI_NODE_IMAGE 组件处理超大分辨率图片时会爆栈。源码注释明确写到这一点。把 NONE 强制提升为 DEFAULT,是用少量计算开销换取稳定性的务实选择。 以上就是本篇内容的所有了~有什么问题欢迎在评论区提出 项目地址:ImageKnifePro一、DownSampler——策略路由的入口
DownSampler 类,定义在 downsampling/down_sampler.h 中。它用一个静态 unordered_map 把枚举值映射到具体的策略对象,整个生命周期内只创建一次。static DownSamplingBase *GetDownSampling(DownSamplingStrategy strategy)
{
static std::unordered_map<DownSamplingStrategy, DownSamplingBase *> downSamplerMap = {
{DownSamplingStrategy::NONE, nullptr},
{DownSamplingStrategy::DEFAULT, new DownSamplingDefault()},
{DownSamplingStrategy::FIT_CENTER_MEMORY, new DownSamplingFitCenterMemory()},
{DownSamplingStrategy::FIT_CENTER_QUALITY, new DownSamplingFitCenterQuality()},
{DownSamplingStrategy::CENTER_INSIDE_MEMORY, new DownSamplingCenterInside(true)},
{DownSamplingStrategy::CENTER_INSIDE_QUALITY, new DownSamplingCenterInside(false)},
{DownSamplingStrategy::AT_LEAST, new DownSamplingAtLeast()},
{DownSamplingStrategy::AT_MOST, new DownSamplingAtMost()}
};
return downSamplerMap[strategy];
}CenterInside 用同一个类的两个实例覆盖了 MEMORY 和 QUALITY 两种模式——通过构造参数 memory_ 布尔值来区分行为。这种设计比给每种变体都写一个子类要简洁。CalculateSize 是外部调用的统一入口。它先做三个前置判断:原图宽高为零直接返回、SVG 格式跳过降采样、策略为 NONE 返回空尺寸。通过这些检查后,调用对应策略的 GetScaleFactor 拿到缩放因子,再根据图片格式做不同的取整操作。if (format == ImageFormat::PNG) {
imageSize.width = std::floor(sourceSize.width / scaleFactor);
imageSize.height = std::floor(sourceSize.height / scaleFactor);
} else if (format == ImageFormat::WEBP) {
imageSize.width = std::round(sourceSize.width / scaleFactor);
imageSize.height = std::round(sourceSize.height / scaleFactor);
} else {
imageSize.width = sourceSize.width / scaleFactor;
imageSize.height = sourceSize.height / scaleFactor;
}二、DownSamplingBase——HighestOneBit 与 2 的幂对齐
DownSamplingBase,这个基类提供了两个关键工具方法。HighestOneBit 用来找出一个整数中最高位的 1 所代表的值。实现方式是经典的位运算展开:先把最高位以下的所有位都填为 1,然后用 num - (num >> 1) 取出最高位。uint32_t HighestOneBit(uint32_t num)
{
int pos = 1;
const int count = 5;
for (int i = 0; i < count; i++) {
num |= (num >> pos);
pos += pos;
}
return num - (num >> 1);
}OH_DecodingOptions_SetDesiredSize 在 2 的幂次缩放时效率最高。GetFactorAsBit 是对 HighestOneBit 的一层封装,确保返回值最小为 1(不放大)。GetFactor 则计算原图与请求尺寸的最大整数比值,再通过 HighestOneBit 对齐后取倒数,返回的是一个小于等于 1 的浮点比例。三、七种具体策略的计算逻辑
DEFAULT——超大图保护
DownSamplingDefault 是最简单的策略。它不依赖请求宽高,只看原图像素总量是否超过 7680 * 4320(约 3317 万像素)。低于这个阈值返回 1(不缩放),超过则返回 resolutionSource / resolutionMax8k,按面积比例线性缩小。const uint32_t resolutionMax8k = 7680 * 4320;
uint32_t resolutionSource = sourceSize.width * sourceSize.height;
if (resolutionSource <= resolutionMax8k) {
return 1;
} else {
return resolutionSource / resolutionMax8k;
}ImageKnifeNodeImage 的构造函数中,NONE 策略也会被强制改为 DEFAULT——因为 C 侧 ARKUI_NODE_IMAGE 接收超大像素图片时可能爆栈崩溃。FIT_CENTER_MEMORY——内存优先的等比缩放
DownSamplingFitCenterMemory 先算出请求宽高与原图宽高的最小比例 percentageFactor(保证图片完整放入目标区域),再对齐到 2 的幂。关键在于最后一步:如果对齐后的缩放因子比实际需要的比例小(图片仍然偏大),就左移一位(乘以 2),宁可图片模糊一些也要省内存。uint32_t scaleFactor = GetFactorAsBit(std::min(...));
if (percentageFactor != 0 && scaleFactor < (1 / percentageFactor)) {
scaleFactor <<= 1;
}FIT_CENTER_QUALITY——质量优先的等比缩放
DownSamplingFitCenterQuality 用的是基类的 GetFactor,取的是最大整数比值对齐后的结果。这里用 std::max 而不是 std::min,意味着缩放因子偏小,保留更多像素,图片质量更高,但内存占用也更大。CENTER_INSIDE——同一类两种模式
DownSamplingCenterInside 通过构造参数 memory_ 区分行为。当 factor == 1(原图已经小于请求尺寸)时,取 max 保持原样;否则取 min 确保图片完全放入。在 MEMORY 模式下,如果对齐后的因子仍然不够大,再左移一位多缩一倍。AT_LEAST——取最大比例
DownSamplingAtLeast 取请求与原图的最大比例做初始缩放(std::max),round 后得到目标宽高,再通过 GetFactorAsBit 对齐。这种策略保证目标图片至少和请求尺寸一样大——适合裁剪场景,先缩到刚好够大再裁切。AT_MOST——取最小比例
DownSamplingAtMost 走的逻辑更复杂。先取 ceil 向上取整的最大比例,然后做两次 2 的幂对齐,确保缩放后的图片不超过请求尺寸。最后还有一个兜底检查:如果对齐后的值小于 greaterOrEqualSampleSize,再左移一次。四、降采样信息如何参与 CacheKey
DownSampler::GetDownSamplingInfo 生成的字符串会被拼入内存缓存的 key 中。同一张图片在不同尺寸的组件中显示时,降采样参数不同,key 也就不同,各自缓存各自的 PixelMap——这是预期行为。static std::string GetDownSamplingInfo(Image_Size requestSize, DownSamplingStrategy strategy)
{
if (strategy == DownSamplingStrategy::NONE) {
return std::string();
}
if (strategy == DownSamplingStrategy::DEFAULT) {
return "DownSampling:Default";
}
return "DownSampling:" + std::to_string(static_cast<int>(strategy)) + "," +
std::to_string(requestSize.width) + "," + std::to_string(requestSize.height);
}ImageKnifeNodeInternal::IsUpdateNecessary 方法中,生成 memoryKey 时会调用 DownSampler::GetDownSamplingInfo(GetComponentSize(), option->downSampling)。如果组件大小没变、图片源没变、策略没变,就复用已有的 key 跳过重新加载。五、组件尺寸获取——onSizeChange 与 OnMeasure
ImageKnifeNodeImage(标准 Image 节点),通过注册 NODE_ON_SIZE_CHANGE 或 NODE_EVENT_ON_AREA_CHANGE 事件获取。API 21 以上用 NODE_ON_SIZE_CHANGE(开销更低),以下用 NODE_EVENT_ON_AREA_CHANGE 兼容。void ImageKnifeNodeInternal::OnComponentSizeChange(ArkUI_NodeEvent *event)
{
static std::pair<int, int> index = GetSizeEventIndex();
auto componentEvent = OH_ArkUI_NodeEvent_GetNodeComponentEvent(event);
width_ = componentEvent->data[index.first].f32;
height_ = componentEvent->data[index.second].f32;
if ((width_ != componentEvent->data[0].f32) ||
(height_ != componentEvent->data[1].f32)) {
imageKnifeRequest_->SetComponentSize(GetComponentSize());
OnMeasureComplete();
ImageKnifeDispatcher::GetInstance().EnqueueMainSrc(imageKnifeRequest_);
}
}GetComponentSize 内部乘以 GetDisplayDensity()),设置到 request 上,然后重新入队主图加载——因为组件变大可能需要更高分辨率的图片。ImageKnifeNodeCustom(自绘制节点),通过 ARKUI_NODE_CUSTOM_EVENT_ON_MEASURE 事件获取布局约束中的 maxWidth 和 maxHeight,单位直接就是 px,不需要再做密度换算。measureComplete_ == false),SVG 图片的解码会被延迟到测算完成后再发起。因为 SVG 的解码宽高取决于组件尺寸,提前解码只会浪费资源。六、与解码器的配合——OH_DecodingOptions_SetDesiredSize
OH_DecodingOptions_SetDesiredSize。在 DecodeInterceptorDefault::ConfigDecodeOption 中:Image_Size imageSize = task->GetDesiredImageSize();
if (imageSize.width != 0 && imageSize.height != 0) {
OH_DecodingOptions_SetDesiredSize(args.decodeOption, &imageSize);
}GetDesiredImageSize 返回的就是 DownSampler::CalculateSize 的计算结果。如果宽高都为零(策略为 NONE 或测算未完成),就不设置 DesiredSize,解码器按原图尺寸解码。ImageKnifeNodeImage 构造函数中的一段逻辑:当 objectFit 设为 ARKUI_OBJECT_FIT_AUTO(自适应宽高)时,策略被强制改为 DEFAULT。因为自适应模式下组件不设置百分比宽高,不会触发 onSizeChange,也就拿不到组件尺寸,除了 DEFAULT 之外的策略都无法计算。七、策略选择的权衡
scaleFactor <<= 1。左移一位意味着缩放因子翻倍,图片缩小一倍,内存减半,但像素信息也丢失一半。在列表快速滚动的场景下,MEMORY 策略可以显著降低内存峰值;在详情页、大图预览场景下,QUALITY 策略保留更多细节。