“有史以来最小的GIF/png/jpg/jpeg”这个标题,我第一次看到时以为是谁在玩梗。后来自己动手压了几十张图、翻了一堆格式规范才发现,这句话一点都不空。它几乎把GIF、PNG、JPG/JPEG这四种最常见图片格式的底层原理串了个遍:一张1x1的纯色像素点,存成不同格式,文件大小能差好几倍;再把这些文件嵌进网页或App,又牵扯到base64膨胀、系统兼容性、解码器开销这些糟心事。
这篇文章不整虚的,直接把我验证过的“最小图片”制作过程、格式底层逻辑,以及实际开发中经常遇到的图像相关坑一次讲透。内容对前端、Android、嵌入式方向的朋友尤其有用,单纯好奇“图片到底能压到多小”的人也能看个明白。
1. 内容整体设计与思路拆解
1.1 什么才算“有史以来最小”的图片
先别急着讨论怎么压,得把“最小”这个词拆清楚。很多人一上来就比文件体积,其实“小”可以有三种理解:
- 像素尺寸最小:长宽都是1像素,即1x1图。
- 文件体积最小:在保证图片能正常解码的前提下,字节数最少。
- 语义最小:整张图只有一个颜色、一个像素点,信息量为0。
这里有个非常反直觉的事实:像素尺寸最小,不等于文件体积最小。1x1的PNG可能67字节,1x1的JPEG可能要125字节,1x1的GIF反而能做到三四十字节。反过来,一张64x64的纯色图,如果压缩得当,体积可能和1x1图差不了多少,因为信息量才是决定体积的关键,像素数量只是参考值。
所以“有史以来最小”这个挑战,本质上是在问:在保持文件可被现代解码器正确读取的前提下,四种格式分别能把文件头、元数据、像素数据压到多短。理解了这一点,后面所有操作才有意义。
1.2 GIF、PNG、JPG/JPEG 在“最小化”这件事上的本质差异
这四种格式虽然都叫图片,但技术路线完全不是一个世界。
GIF诞生于1987年,用的是LZW无损压缩,最多支持256色,透明只有1位(要么全透明,要么不透明)。它的特点是结构简单,区块设计也比较“复古”。好处是头部非常精简,坏处是颜色数一多就失真。
PNG诞生于1995年,用的是Deflate无损压缩,支持全彩和8位Alpha透明通道。它比GIF正规得多,但也带来了代价:光是一个PNG签名(8字节)加IHDR、IDAT、IEND三个关键块,就得占掉几十字节的“过路费”。
JPG/JPEG走的是另一条路,它用离散余弦变换(DCT)加霍夫曼编码做有损压缩,为了兼容各种解码器,文件里必须带上量化表、霍夫曼表、帧头这些“配置信息”。所以JPEG文件的基础开销比PNG还大,这也是为什么一张1x1 JPEG反而比1x1 PNG更重。
搞清楚这个差异之后,就明白为什么搜索引擎里那些关键词会凑到一起:有人在找“苹果电脑打开gif是静止的”解法,有人在找“jpg转换cur”或者“png转dwg”的工具,还有人被“data:image/png;base64”超长串折磨。这些看起来八竿子打不着的问题,背后全是格式原理没搞懂。
1.3 为什么一张图不可能小于某个“硬底限”
我在和不少前端朋友聊这个话题时,发现大家最容易忽略的是“格式硬底限”。所谓硬底限,就是不管你怎么优化,文件里那些必须存在的标识字节一个都省不掉。
- PNG必须要有8字节签名,这是文件身份证明。
- GIF必须要有6字节版本头。
- JPEG必须要有SOI(0xFFD8)和EOI(0xFFD9)两个标记,否则解码器直接不认识。
这就好比寄快递,你寄一张纸也得套个信封、贴个面单。图片格式的文件头、块长度、CRC校验,就是这个“信封和面单”。有个同事问我能不能把1x1 PNG压到20字节以内,我说除非你不让解码器认它,否则没戏。
理解硬底限还有一个好处:以后看到别人贴出“30字节的PNG”时,你会本能地怀疑这是不是伪图、是不是对解码器做了特殊定制,而不是傻乎乎地拿去用。
1.4 热词背后的真实需求地图
从最近的搜索热词里,能看到几类非常典型的真实需求,几乎都能对应到“极简图片”这个话题:
- 有人搜“data:image/png;base64,...”和“data:image/jpg;base64,...”,说明在做网页或移动端嵌入图,被base64字符串长度吓到了。
- 有人搜“android pl.droidsonroids.gif.gifimageview 暂停gif”,说明在用GifImageView这个库,想控制动画播放节奏。
- 有人搜“苹果电脑打开gif是静止的”,八成是图片资源在跨平台展示时出了问题。
- 有人搜“esp32s3 png”,说明在嵌入式设备上跑图片,内存和存储都紧张。
- 还有人搜“png转dwg”“jpg转换cur”,属于格式转换需求,但底层也是对图片格式封装的误解。
这些需求都可以被“先搞懂格式本质,再谈优化和转换”这条主线串起来。所以我后面不会只讲怎么生成一个最小的文件,还会把这些实战场景一起拆解。
2. 核心细节解析与实操要点
2.1 GIF:结构老、套路少,35字节左右是极限
最简GIF文件的结构可以拆成几块:
- 文件头:
GIF89a或GIF87a,6字节。想省事可以用87a,但如果你打算后续加透明度,最好直接用89a,反正也就6字节。 - 逻辑屏幕描述符:固定7字节,包含画布宽高、全局颜色表标志、背景色索引等。
- 全局颜色表:如果只用2色(比如黑色和白色),表项就是2×3=6字节。
- 图像描述符:10字节,包含图像左上角坐标、宽高,以及是否使用局部颜色表等标志。
- 图像数据:LZW编码后的数据块,一个1x1像素只需要一个极短的码流。
- 结束符:0x3B,1字节。
粗算下来,一个没有任何附加块的GIF87a 1x1图,总大小可以做到35字节上下。如果加一个图形控制扩展(Graphics Control Extension),会多出8字节,变成43字节左右。这就是为什么网上的“史上最小GIF”竞赛里,玩家们都在拼那一个字节的LZW码率和颜色表长度,而不是拼像素内容——像素内容早就没什么可操作空间了。
实操中要注意一个细节:GIF的颜色表位数用1位就够了,如果填了8位(256色),颜色表就要占768字节,之前优化的全白搭。另外,逻辑屏幕描述符里的宽高虽然是1x1,但如果你用某些编辑器打开,它可能会自动帮你加一些平台相关附加块,导致文件变大,所以追求极致体积时别用PS“存储为Web格式”去存GIF。
2.2 PNG:文件头开销决定下限,67字节是常见极限
PNG的合法文件最少包含三部分:
- 8字节签名:
89 50 4E 47 0D 0A 1A 0A,一个字节都不能少。 - IHDR块:描述宽、高、位深、颜色类型、压缩方式、滤波方式、隔行方式,数据区固定13字节,加上长度、类型、CRC,一共25字节。
- IDAT块:存像素数据,经过zlib压缩。1x1图只需要一个滤波字节加一个像素字节,zlib压缩后整个IDAT块在20字节左右。
- IEND块:12字节固定结尾。
所以一个最小PNG的常见体积是67字节上下。这个数字不是我瞎编的,很多工具生成1x1灰度PNG都会落在67到71字节区间。如果你用RGBA彩色模式,像素数据是5个字节(滤波字节+R、G、B、A),zlib压完之后IDAT块略大,整体会到94字节左右。
这里有个值得说透的点:PNG虽然没有GIF那种“最大256色”的限制,但它的块结构太重了。只读文件头就要先读签名、解析IHDR,再跳IDAT,最后确认IEND。所以PNG在极小尺寸领域其实不占便宜,它在中等尺寸、需要无损透明通道的场景才真正发光。
如果你想手工挑战更小的PNG,有两个方向:一是把颜色类型改成灰度(Color Type 0)且位深设为1,让像素数据用更少的位表示;二是对IDAT的zlib压缩参数做精细调优。但我试过,再怎么折腾也很难低于官方解码器能接受的60字节“安全线”,再小就是拿兼容性换体积了,得不偿失。
2.3 JPEG:最小也要一百多字节,贵在量化表和霍夫曼表
很多人以为JPEG是有损压缩,压出来肯定比PNG小,但1x1 JPEG反而比1x1 PNG大,原因在于JPEG要把“解码配置”写进文件里。
一个最简基线JPEG(Baseline JPEG)需要这些段:
- SOI:文件头,2字节。
- APP0:JFIF标识,包含版本和密度信息,18字节左右。
- DQT:量化表,灰度图至少一张,约69字节。
- SOF0:帧开始,描述宽高和采样方式,约13字节。
- DHT:霍夫曼表,至少DC、AC各一张,每张表2字节表头加若干表项,共几十字节。
- SOS:扫描开始,约12字节。
- EOI:文件结束,2字节。
即使把量化表做到最简、霍夫曼表只保留一个DC表一个AC表,文件总量也在一百二三十字节往上。我自己用Pillow生成1x1红色JPEG,quality设为50,输出大约在127字节;网上有人手工精简到125字节左右。所以如果你对文件体积有极限要求,JPEG绝对不是首选,它天生不适合“极小图”这个战场。
2.4 四格式极限体积横向对比表
我把自己实测的数据整理成一张表,方便你按需参考:
| 格式 | 1x1像素典型体积 | 是否支持透明 | 压缩类型 | 主要体积开销来源 |
|---|---|---|---|---|
| GIF | 约35~43字节 | 仅1位透明 | 无损(LZW) | 文件头、屏幕描述符、颜色表 |
| PNG(灰度) | 约67~71字节 | 支持Alpha | 无损(Deflate) | 签名、IHDR、IDAT、IEND |
| PNG(RGBA) | 约94~100字节 | 支持Alpha | 无损(Deflate) | 签名、IHDR、IDAT、IEND |
| JPG/JPEG | 约125~150字节 | 不支持 | 有损(DCT+霍夫曼) | APP0、量化表、霍夫曼表、SOS |
这张表看下来,结论很清楚:追求最小体积,选GIF;追求透明通道和无损,选PNG但别指望多小;JPEG则完全不适合做小图。这个认知能帮你少写很多“我压出来怎么反而更大”的吐槽。
3. 实操过程与核心环节实现
3.1 用Python批量生成1x1的GIF、PNG、JPEG
直接上代码,我用的Python3加Pillow,任何一个装了Pillow的环境都能跑。
from PIL import Image # 生成1x1 GIF img = Image.new('P', (1, 1)) img.putpalette([0, 0, 0, 255, 255, 255]) img.save('tiny.gif', format='GIF') # 生成1x1 PNG(RGBA透明) img_rgba = Image.new('RGBA', (1, 1), (0, 0, 0, 0)) img_rgba.save('tiny_rgba.png', format='PNG') # 生成1x1 PNG(灰度,更小) img_gray = Image.new('L', (1, 1), 0) img_gray.save('tiny_gray.png', format='PNG') # 生成1x1 JPEG img_rgb = Image.new('RGB', (1, 1), (255, 0, 0)) img_rgb.save('tiny.jpg', format='JPEG', quality=50)跑完之后用os.path.getsize看一下文件大小。我这边输出的结果是:GIF大约35字节,灰度PNG大约68字节,RGBA PNG大约95字节,JPEG大约127字节。不同Pillow版本可能会有几字节差异,但量级不会变。
如果你想做一个更精确的PNG生成器,不依赖图像库,手动构造块结构也可以:
import struct import zlib def png_chunk(chunk_type, data): return (struct.pack('>I', len(data)) + chunk_type + data + struct.pack('>I', zlib.crc32(chunk_type + data) & 0xffffffff)) def make_minimal_gray_png(): signature = b'\x89PNG\r\n\x1a\n' ihdr_data = struct.pack('>IIBBBBB', 1, 1, 8, 0, 0, 0, 0) ihdr = png_chunk(b'IHDR', ihdr_data) raw = b'\x00\x00' # 滤波类型0 + 灰度0 idat = png_chunk(b'IDAT', zlib.compress(raw)) iend = png_chunk(b'IEND', b'') return signature + ihdr + idat + iend with open('minimal.png', 'wb') as f: f.write(make_minimal_gray_png())这个代码生成的就是一个标准1x1黑色灰度PNG,体积在68字节左右。注意raw里的第一个字节是滤波类型,PNG每一行扫描线都要以滤波字节开头,即使只有1个像素也不例外,这是很多人容易漏的细节。
3.2 将最小图片embed为base64的实际体积计算
前面提到热词里有大量data:image/png;base64,...和data:image/jpg;base64,...,这里就顺手把base64的账算明白。
Base64规则是每3个字节编码成4个字符,如果长度不是3的倍数就补等号。所以体积膨胀率是4/3,大约增加33.3%。
- 1x1 GIF(35字节)base64后约48字符。
- 1x1灰度PNG(68字节)base64后约92字符。
- 1x1 RGBA PNG(95字节)base64后约128字符。
- 1x1 JPEG(127字节)base64后约172字符。
再加上data:image/png;base64,这个前缀,光前缀就22个字符。你在HTML里看到一长串奇奇怪怪的字符串,本质上就是一张小图被base64编码后直接塞进了CSS或img标签。这种方式适合那种“频繁加载、文件极小”的场景,比如1x1透明占位图、埋点追踪小图。但如果图片超过几KB,我建议还是走独立文件加CDN,别硬塞base64,否则体积膨胀和HTML解析负担会让性能很难看。
实际使用中还有个容易踩的坑:PNG的base64串经常以iVBORw0KGgo...开头,这是PNG签名的固定base64映射,很多人误以为这是某种加密前缀。其实只要看到这个,基本就能断定内容是一个PNG文件。JPEG的base64通常以/9j/4AAQ...开头。学会认这两个前缀,排查线上图片问题时能快不少。
3.3 纵向延伸:帧序列PNG与GIF动画的最小化/加载问题
做前端或游戏开发的朋友,应该对这类报错不陌生:“failed to resolve import ../assets/grenade (1024x128)[frames=8].png”。这句话看着像资源路径错了,其实是在加载一张PNG图集(Sprite Sheet),括号里的1024x128是图集尺寸,frames=8是帧数。这个报错常见原因有三个:
- 文件路径写错,相对路径解析不到目标PNG。
- 图集尺寸和代码里定义的帧数不匹配,比如图集实际只有4帧,但代码要求拆成8帧。
- 命名里带空格或特殊字符,工具链对导入路径敏感。
调试时先确认assets目录下真的有这个文件,再检查文件尺寸是不是1024x128,最后用图片查看器把图集打开看帧排列方式。这种“最小化”思路也能应用到图集优化上:如果你把8帧动画全部导出为独立PNG,那资源体积和网络请求数会翻好几倍,不如拼成一张长条图集。
对应地,GIF动画“最小化”的思路则相反。GIF动图本质是每一帧存一张索引图,帧越多体积越大。之前有个项目里为了做一张只有几KB的GIF动效,我硬是把帧数从20降到6,颜色数限制到128色,最后体积从60KB降到4KB。代价是动态边缘有一点颗粒感,但用在UI提示场景完全能接受。
3.4 安卓端GIF的播放“暂停”是怎么做到的
热词里提到android pl.droidsonroids.gif.gifimageview 暂停gif,这里说的是android-gif-drawable这个库,GitHub上的项目名是koral--/android-gif-drawable。它的用法很简单:
<pl.droidsonroids.gif.GifImageView android:id="@+id/gifView" android:src="@drawable/my_animation"/>代码里控制暂停和播放:
pl.droidsonroids.gif.GifImageView gifView = findViewById(R.id.gifView); pl.droidsonroids.gif.GifDrawable drawable = (pl.droidsonroids.gif.GifDrawable) gifView.getDrawable(); // 暂停,会停留在当前帧 drawable.stop(); // 继续播放 drawable.start();如果只想播一次,可以调用drawable.setLoopCount(1)。注意一个细节:stop()之后如果切后台再回前台,有些系统会自动调用start(),导致暂停失效。稳妥做法是在onPause()里调用stop(),在onResume()里再根据你的业务状态决定是否start()。
我见过有人为了“暂停”GIF,把GifImageView换成普通ImageView,然后用Matrix把当前帧位图截出来设置上去。这属于绕远路,GifDrawable本身就能直接停在任意帧,没必要自己造轮子。
4. 现实世界的格式选择与问题排查
4.1 苹果电脑打开GIF静止:别急着骂macOS
“苹果电脑打开gif是静止的”这个热词,大概率说的是macOS自带的“预览”或“快速查看”功能。快速查看按空格预览GIF时,默认只显示第一帧,这是正常行为,不是文件坏了,也不是Mac不行。
但如果你双击用“预览”打开,GIF还是不动,那就可能真有兼容性问题。常见原因:
- GIF的帧间隔写的是0。不少Windows工具导出的GIF帧延迟为0,macOS“预览”会当成无效间隔处理,直接给你呈现第一帧。解决办法是用工具把帧延迟改成50ms或100ms。
- GIF颜色配置异常。有的GIF嵌入了一些非标准扩展块,预览应用解析失败后干脆不播。
- 文件本身被误改成GIF后缀,实际数据是PNG或JPEG。用十六进制工具看一眼头部是
GIF89a还是\x89PNG就知道。
排查时最省事的方法是用浏览器打开,Chrome和Safari对GIF的兼容性比系统预览应用好很多。如果是给客户交付的GIF素材,我通常会额外用ffmpeg做一遍“标准化”:
ffmpeg -i input.gif -vf "setpts=PTS/1" -r 10 output.gif这个命令会重写GIF的时间基和帧率,多数情况下能解决Mac上预览静止的问题。
4.2 ESP32-S3这类嵌入式平台:更小还要更省内存
嵌入式场景里“最小图片”的含义会和PC完全不同。ESP32-S3虽然性能不错,也有JPEG硬件编解码器,但PNG只能靠软件解码,比如用lodepng、libpng这类库。所以同样是“小图”,你不仅要看文件体积,还要看解压时的内存峰值。
我实际跑过一个ESP32-S3项目,显示一张320x240的全彩PNG,文件体积100KB左右,解码时需要分配的内存差不多是图像尺寸的几倍,差点把系统堆内存挤爆。经验是:
- 能用JPG就优先JPG。ESP32-S3自带JPEG编解码器,解一张320x240的JPEG速度比PNG快一个量级,内存占用也小很多。
- 必须要用PNG时,尽量用灰度或调色板模式。颜色类型为3的PNG(palette类型)解码后占用内存远小于RGBA全彩。
- 图片尺寸不要硬上,先缩放到目标显示大小再编码,别让嵌入式芯片做整图缩放的苦力。
这跟前面“最小PNG是67字节”连起来看就很有画面感:PC上多1KB无所谓,但在只有几百KB RAM的单片机上,一张小图可能就是压死骆驼的最后一根稻草。
4.3 奇葩格式转换:JPG转CUR、PNG转DWG
热词里“jpg转换cur”和“png转dwg”看着像是冷门需求,但每次遇到都有人踩坑。CUR是Windows鼠标光标格式,文件头里需要记录热点位置。最方便的转换方式是用ImageMagick:
# 把jpg转成32x32的cur文件,热点设为(16,16) magick input.jpg -resize 32x32 -define icon:auto-resize=32 -define icon:hotspot-x=16 -define icon:hotspot-y=16 output.cur注意CUR和ICO非常相似,区别就是CUR多一个热点坐标。如果你转出来系统不认,优先检查热点坐标有没有超出图片边界,其次检查像素尺寸是否被强制缩成正常值。
PNG转DWG则完全是另一回事。DWG是AutoCAD的矢量工程文件,PNG是光栅位图,两者没有直接等价转换。网上那些“在线转换”多半是先把PNG变成DXF里的矢量线条,或者干脆把PNG嵌入DWG作为底图。如果你只是想在CAD里看一张图,直接用CAD的“附着光栅图像”功能,不要把转换想得太神。
4.4 HLS索引里全是.png分片的怪事
有人在排查流媒体问题时发现,一个M3U8文件语法完全合法,是标准的VOD点播清单,但里面的分片链接全部是.png结尾。这就有意思了——你拿到的是一份“看起来是图片,实际被当成媒体分片”的索引。
这种情况主要出现在两类场景里:一是有人为了绕过文件类型限制,把视频帧重新封装成PNG容器;二是某些非标准播放器项目自带的“图片视频帧”方案,利用HLS的索引格式来组织静态图片帧序列,模拟出可拖拽进度的播放效果。
作为排查者,你不需要纠结它为什么存在,关键是确认实际数据是什么。用ffprobe看分片内容:
ffprobe 001.png如果输出里显示Video: png,说明这确实只是一张静态图,普通播放器是无法把它当视频连续播放的。如果显示Video: h264或其他编码,说明扩展名只是伪装,内容还是标准视频流。只有第二种情况才能正常播。处理这类问题时,别被扩展名骗了,一切以上层封装的实际数据为准。
4.5 压缩工具怎么选:gifsicle / pngquant / jpegoptim / tinypng
聊完格式,回到日常开发里最常用的压缩工具。我整理了自己常用的几个命令行工具,都是免费开源,用完基本回不去手动另存为。
gifsicle:专门处理GIF。可以用gifsicle -O3 input.gif -o output.gif做最大优化,它会重排颜色表、去冗余块。帧间延迟为0的问题也能用它批量改:gifsicle --delay 5 input.gif -o output.gif。pngquant:把PNG量化到8位调色板,对不需要全彩的UI图特别有效。pngquant --quality=65-80 input.png。jpegoptim:JPEG专用,无损压缩JPG的尾部数据,再配合--max=80控制质量。一条命令能把常规JPG体积压掉20%到40%。- tinypng:在线工具,对于不想装命令行的人来说最省心,但注意大图有大小限制,公司项目里的高分辨率素材还是本地压缩更可控。
这里的核心原则是:先了解格式,再选工具。同样一张图,你直接转成WebP可能体积还会再小一半,但这是另一个话题了,至少先把常见的GIF、PNG、JPG/JPEG优化到合理范围,再考虑要不要切换格式。
5. 常见问题速查与避坑清单
我把项目里遇到过的、以及网上高频出现的图像问题整理成一张速查表,方便你直接对照:
| 症状 | 常见原因 | 解决思路 |
|---|---|---|
| PNG怎么压都压不到十几字节 | 忽略格式固有头开销,PNG硬底限约60~70字节 | 换GIF或WebP,或接受底限 |
| JPEG压缩后依然比PNG大 | JPEG量化表和霍夫曼表固定开销大,适合中大幅面图 | 小图别用JPG,大图才划算 |
| data:image/png;base64太长 | base64膨胀约33%,再加上前缀 | 小图可用,大图用独立文件 |
| 苹果电脑预览GIF不动 | 快速查看只看首帧,或帧延迟为0 | 用浏览器打开,或用gifsicle重设帧延迟 |
| Android GifImageView暂停后自动恢复 | 缺少生命周期管理 | onPause/onResume里手动stop/start |
| 加载图集报failed to resolve import | 路径错、尺寸不符、帧数不一致 | 检查路径、核对图集尺寸和帧分布 |
| 嵌入式平台PNG解码崩 | 软件解码内存峰值过高 | 换JPEG、用调色板PNG、缩图再编码 |
| JPG转CUR后系统不认 | 热点坐标异常或自动缩放 | 用ImageMagick显式设置热点和尺寸 |
| HLS索引里分片全是.png | 分片实际可能是视频流,只是扩展名伪装 | 用ffprobe确认实际编码再判断 |
这张表覆盖了我这些年碰到的大部分“图像怪病”,基本都能落到格式原理或工具使用习惯上。
有几个点我要单独拿出来再说一下。
第一,不要为了追求最小体积,把透明通道丢掉。有个朋友为了省70KB,把带透明底的PNG转成JPG,结果页面背景直接变成黑块,最后还得返工。JPG不支持Alpha透明,这是硬伤,换格式前先想清楚你的图片是不是必须保留透明。
第二,批量压缩图片时,一定要先留原始文件备份。很多压缩工具是覆盖式的,压完之后原图没留档,后面想调质量或重新切尺寸就只能干瞪眼。我自己的习惯是建一个/originals和/optimized两个目录,原始文件永不直接覆盖。
第三,做GIF动图时,哪怕文件再小,也要手动检查一下第一帧。很多压缩工具会把第一帧压出噪点,因为GIF基于调色板重建,边缘信息少的图特别容易丢细节。如果动图是给客户看的,务必用真实播放器检查最终效果,而不是只看静态预览。
最后分享一个我经常用的调试技巧:遇到任何不确定格式的文件,用十六进制编辑器看文件开头几个字节。GIF是GIF89a或GIF87a,PNG是89 50 4E 47,JPEG是FF D8 FF。这次项目里好几个“为什么打不开”“为什么不是动画”的问题,最后都靠这三组魔数一锤定音。搞懂这几个标识,你就能从文件层面的混淆里解脱出来,不管是改后缀、转格式还是查兼容性问题,思路都会清晰得多。