2.1 什么是 PixelMap
在 HarmonyOS NEXT 中,PixelMap 是所有图片处理的核心对象。
很多开发者第一次接触图片开发时,认为:
图片 = jpg/png 文件
实际上并不是。
JPG、PNG、WebP 都只是磁盘上的文件格式。
例如:
photo.jpg它在磁盘里面保存的是:
压缩数据而不是:
RGB RGB RGB RGB...图片真正能够被屏幕显示之前,需要经过一次完整的解码流程。
整个流程如下:
photo.jpg │ ▼ ImageSource │ ▼ JPEG 解码 │ ▼ RGBA 像素数据 │ ▼ PixelMap │ ▼ ArkUI Image │ ▼ GPU │ ▼ 屏幕显示因此:
PixelMap 就是一张图片解码后的像素数据。
所有图片编辑都是围绕 PixelMap 完成。
例如:
微信修改头像:
选择图片 ↓ PixelMap ↓ 裁剪 ↓ 旋转 ↓ 压缩 ↓ 上传小红书:
相册 ↓ PixelMap ↓ 滤镜 ↓ 贴纸 ↓ 文字 ↓ 保存美图秀秀:
图片 ↓ PixelMap ↓ 美颜 ↓ AI ↓ 导出所以可以说:
没有 PixelMap,就没有图片编辑。
2.2 PixelMap 为什么存在?
有人会问:
既然已经有 jpg 文件了。
为什么还需要 PixelMap?
原因很简单。
例如:
我们想修改图片左上角一个像素。
JPG:
FFFFFFFFFFABCD239A... ......看到什么了吗?
什么也看不到。
因为 JPG 是经过:
- DCT 离散余弦变换
- 量化
- 霍夫曼编码
以后得到的一串压缩数据。
CPU 根本不知道:
第100万个字节 是不是 第100万个像素。所以必须:
JPG ↓ 解码 ↓ PixelMap ↓ 修改 ↓ 重新编码 ↓ JPG整个世界所有图片编辑器都是这样工作的。
包括:
- Photoshop
- Lightroom
- 美图秀秀
- 醒图
- Snapseed
都是先解码,再编辑。
2.3 PixelMap 内存结构
PixelMap 本质就是:
一块连续申请出来的内存。
例如:
创建:
const pixelMap = await imageSource.createPixelMap()实际上系统做了下面几件事情:
读取图片 ↓ 申请内存 ↓ JPEG 解码 ↓ 写入内存 ↓ PixelMap 返回假设:
图片:
1000 × 1000系统会申请:
1000000 个 Pixel每一个 Pixel 都紧挨着。
例如:
Pixel0 Pixel1 Pixel2 Pixel3 Pixel4 ...... Pixel999999连续存放。
这就是为什么:
PixelMap 可以高速访问。
因为:
CPU 最喜欢:
连续内存而不是:
链表 树 Map图片处理之所以快,就是因为:
PixelMap 的布局非常适合 CPU Cache。
2.4 PixelMap 的内存布局
PixelMap 默认使用:
RGBA8888什么意思?
一个 Pixel:
包含:
Red Green Blue Alpha每一个:
8 bit所以:
8 + 8 + 8 + 8 = 32bit即:
4 Byte一个 Pixel:
内存里面就是:
R G B A例如:
红色:
255 0 0 255绿色:
0 255 0 255蓝色:
0 0 255 255透明:
255 255 255 0因此:
PixelMap 真正的数据就是:
255 0 0 255 255 0 0 255 255 0 0 255 0 255 0 255 ...... ......连续排列。
2.5 一个 Pixel 占多少内存?
RGBA8888:
R 8bitG 8bitB 8bitA 8bit因此:
4 Byte假设:
图片:
1920 ×1080像素:
1920 × 1080 = 2073600内存:
2073600 × 4 = 8294400 Byte约等于:
7.91MB很多人觉得:
图片只有:
2MB为什么内存:
8MB原因就在这里。
图片:
2MB是:
压缩以后。
PixelMap:
8MB是:
解码以后。
完全不是一个概念。
2.6 为什么一张照片会占几十 MB?
现在手机:
1200 万像素例如:
4032 ×3024像素:
12192768内存:
12192768 × 4 = 48771072约:
46MB如果:
5000 万像素:
8192 ×6144内存:
8192 × 6144 × 4 = 201MB所以:
为什么:
很多 App:
打开照片:
瞬间:
内存:
上涨:
200MB。
就是因为:
PixelMap 已经解码了。
2.7 PixelMap 生命周期
PixelMap 生命周期可以分成六个阶段。
第一阶段 创建
例如:
const imageSource = image.createImageSource(path)然后:
const pixelMap = await imageSource.createPixelMap()系统:
申请内存 ↓ JPEG 解码 ↓ PixelMap第二阶段 编辑
例如:
旋转 裁剪 缩放 滤镜 文字 水印全部发生在:
PixelMap上。
第三阶段 显示
例如:
Image(pixelMap)ArkUI:
直接:
GPU:
渲染。
第四阶段 保存
例如:
PixelMap ↓ ImagePacker ↓ JPEG ↓ File保存的时候:
重新编码。
第五阶段 上传
企业项目:
一般:
保存 ↓ OSS ↓ CDN上传的不再是:
PixelMap。
而是:
JPEG。
第六阶段 释放
例如:
this.pixelMap = undefined没有引用以后。
GC:
回收。
内存:
释放。
2.8 PixelMap 创建过程源码解析
很多人觉得:
createPixelMap()只是:
new 一个对象。
实际上:
里面流程远比想象复杂。
createPixelMap() │ ▼ 读取文件 │ ▼ 判断图片格式 │ ▼ JPEG Decoder │ ▼ 申请 RGBA 内存 │ ▼ 颜色空间转换 │ ▼ Alpha 通道处理 │ ▼ 生成 PixelMap真正耗时的是:
JPEG 解码而不是:
new PixelMap因此:
第一次打开:
一般:
几十毫秒。
大图:
几百毫秒。
都是正常现象。
2.9 PixelMap 创建与复制
很多新人:
喜欢:
let newMap = oldMap以为:
复制了一份。
其实:
只是:
引用。
真正复制:
需要:
重新创建。
例如:
原图 48MB复制:
48MB + 48MB = 96MB再复制:
144MB再复制:
192MB所以:
企业项目:
几乎不会:
无限复制:
PixelMap。
一般:
只有:
原图 ↓ 结果图两份。
2.10 为什么图片编辑都围绕 PixelMap?
因为:
PixelMap:
拥有:
- 原始 RGBA 数据
- Alpha 通道
- 宽高
- 色彩空间
- 可直接绘制
- 可重新编码
所有图片编辑 API:
最终操作的都是:
PixelMap例如:
旋转:
PixelMap ↓ Canvas.rotate() ↓ 新的 PixelMap裁剪:
PixelMap ↓ Canvas.drawImageRect() ↓ 新的 PixelMap水印:
PixelMap ↓ Canvas.drawText() ↓ 新的 PixelMap压缩:
PixelMap ↓ ImagePacker ↓ JPEG所以:
整个图片编辑流程实际上就是:
ImageSource ↓ PixelMap ↓ Canvas ↓ PixelMap ↓ ImagePacker ↓ JPEG