如何基于颜色映射HashMap快速重绘Bitmap?逐像素方法性能不佳
高效实现Bitmap颜色映射替换的方案
兄弟,我太懂这种逐像素遍历卡成狗的痛苦了!你现在用的getPixel()和setPixel()方法效率极低——它们每次调用都要在Java层和Native层之间来回切换,对于稍微大一点的Bitmap来说,这种频繁的JNI调用就是性能杀手,真机上慢到离谱完全正常。
下面给你几种高效稳定的替代方案,按实现复杂度和性能排序:
方案1:批量像素数组处理(最快实现,性能提升显著)
这个方法通过一次性读取所有像素到Java数组,在内存中完成替换后再批量写回Bitmap,把JNI调用从width*height次降到2次,性能提升非常明显。
public void recolorFast(Bitmap origBitmap, Bitmap newBitmap, Map<Integer, Integer> colorReplace) { int width = origBitmap.getWidth(); int height = origBitmap.getHeight(); int[] pixels = new int[width * height]; // 一次性读取所有像素到数组(仅1次JNI调用) origBitmap.getPixels(pixels, 0, width, 0, 0, width, height); // 遍历数组完成颜色替换(纯Java内存操作,无JNI开销) for (int i = 0; i < pixels.length; i++) { Integer targetColor = colorReplace.get(pixels[i]); if (targetColor != null) { pixels[i] = targetColor; } // 无匹配颜色则保留原像素 } // 一次性写入处理后的像素(仅1次JNI调用) newBitmap.setPixels(pixels, 0, width, 0, 0, width, height); }
额外优化点:
- 把
HashMap<Integer, Integer>换成SparseIntArray,它是Android专门为int键值对设计的容器,内存占用更小,查找速度比HashMap更快:// 替换示例:把HashMap转成SparseIntArray SparseIntArray colorReplace = new SparseIntArray(); colorReplace.put(0xFF0000FF, 0xFFFF0000); // 蓝色转红色
方案2:RenderScript并行处理(超大Bitmap最优解)
如果你的Bitmap尺寸特别大(比如4K级别的图片),RenderScript是更好的选择——它会自动利用CPU多核心甚至GPU进行并行计算,性能比Java数组方法还要强,而且内存压力更小。
步骤1:编写RenderScript内核脚本
在src/main/rs/目录下创建script_color_replace.rs文件:
#pragma version(1) #pragma rs java_package_name(com.your.package.name) // 替换成你的包名 rs_allocation colorMapKeys; rs_allocation colorMapValues; int colorMapSize; // 每个像素的处理逻辑 uchar4 __attribute__((kernel)) replaceColor(uchar4 in, uint32_t x, uint32_t y) { // 把输入像素转换为ARGB int格式 int origColor = (in.a << 24) | (in.r << 16) | (in.g << 8) | in.b; // 查找颜色映射表 for (int i = 0; i < colorMapSize; i++) { int key = rsGetElementAt_int(colorMapKeys, i); if (key == origColor) { int targetColor = rsGetElementAt_int(colorMapValues, i); // 把目标颜色转回uchar4格式 uchar4 out; out.a = (targetColor >> 24) & 0xFF; out.r = (targetColor >> 16) & 0xFF; out.g = (targetColor >> 8) & 0xFF; out.b = targetColor & 0xFF; return out; } } // 无匹配则返回原像素 return in; }
步骤2:Java层调用RenderScript
public void recolorWithRenderScript(Bitmap origBitmap, Bitmap newBitmap, Map<Integer, Integer> colorReplace) { RenderScript rs = RenderScript.create(origBitmap.getContext()); // 把颜色映射表转为数组 int[] keys = new int[colorReplace.size()]; int[] values = new int[colorReplace.size()]; int index = 0; for (Map.Entry<Integer, Integer> entry : colorReplace.entrySet()) { keys[index] = entry.getKey(); values[index] = entry.getValue(); index++; } // 创建RenderScript分配对象 Allocation inputAlloc = Allocation.createFromBitmap(rs, origBitmap); Allocation outputAlloc = Allocation.createFromBitmap(rs, newBitmap); Allocation keysAlloc = Allocation.createSized(rs, Element.I32(rs), keys.length); Allocation valuesAlloc = Allocation.createSized(rs, Element.I32(rs), values.length); // 复制数据到分配对象 keysAlloc.copyFrom(keys); valuesAlloc.copyFrom(values); // 加载自定义脚本并设置参数 ScriptC_color_replace script = new ScriptC_color_replace(rs); script.set_colorMapKeys(keysAlloc); script.set_colorMapValues(valuesAlloc); script.set_colorMapSize(keys.length); // 执行并行处理 script.forEach_replaceColor(inputAlloc, outputAlloc); // 把处理结果写回Bitmap outputAlloc.copyTo(newBitmap); // 释放资源(避免内存泄漏) inputAlloc.destroy(); outputAlloc.destroy(); keysAlloc.destroy(); valuesAlloc.destroy(); script.destroy(); rs.destroy(); }
关于ColorFilter的说明
你提到的PorterDuffColorFilter或ColorMatrixColorFilter更适合批量颜色变换(比如调整色调、亮度、透明度,或者把所有颜色按公式混合),但没办法直接实现你需要的“精确键值对颜色替换”——因为它们是基于连续的数学变换逻辑,而非离散的映射表。所以如果你的需求是替换特定的几种颜色,上面两种方案更合适。
最后补充的优化建议
- 确保
origBitmap和newBitmap的颜色格式一致(推荐用ARGB_8888),避免格式转换带来的额外开销。 - 如果不需要保留原Bitmap,可以直接在原Bitmap上修改(前提是
origBitmap.isMutable()返回true),节省内存。 - 处理大Bitmap时,尽量避免在主线程执行,放到子线程中防止UI卡顿。
内容的提问来源于stack exchange,提问作者Bogdan Samchuk
相关产品推荐
相关产品推荐

