1 背景
转转游戏账号业务中,以王者荣耀为例,用户关注的核心指标是:英雄总数、皮肤总数、特色皮肤等重点信息。但是在旧版商品列表展示的图片,是用户上传的图片,以及一些简单业务规则生成的图片,没有突出账号相关重点特色。
虽然在商详(商品详情页)内部已经呈现出了完整的验号报告,但是用户需要从商列(商品列表页)点击进入到商详,存在较长的转换路径,降低了关键信息的触达效率。
于是我们将游戏账号相关重点信息生成大图,展示在商列及商详。
商列(商品列表页)大图:
商详(商品详情页)大图:
2 从前端生成大图,到后端生成大图
2.1 前端生成大图的考量与实现方案
需求初期,大图由前端生成,主要出于以下考虑:
- 前端可利用浏览器原生渲染能力处理复杂布局和样式,开发验证速度较快。
- 前端能更灵活地支持图像处理,开发成本较低。
前后端进行交互,由后端提供生成图片所需的全量物料图片信息。前端拿到物料之后,通过页面api,与Puppeteer截图相结合的方式生成大图。步骤如下:
- 前端创建一个页面,通过访问后端接口拿到对应的物料数据(小图url、文字等)。对这些图片资源url数据进行实时访问,在页面上进行图片与文字的绘制,进而生成大图的页面。
- 将此页面的地址,传到另一个Puppeteer服务。Puppeteer服务会启动一个浏览器实例,再次访问传入的页面,然后进行截图。
2.2 前端生成大图遇到的问题及原因分析
在上线之后,发现前端生成大图会有一定概率的超时异常导致生成图片失败,平均2~3s,超时情况大于5s。在生成只有12个拼接图片的情况下尚且超时,随着我们商详大图需求的引入,拼接图片数量有时会超过600。此时超时情况会更加严重。
通过对整体链路的分析,发现Puppeteer服务截图是一个耗时较多的操作,其大概步骤如下:
- 申请服务内存、磁盘等资源;
- 创建浏览器进程并启动;
- 通过网络访问传入页面URL;
- 渲染页面并且进行截图。
2.3 改进方案-后端生成大图
为了解决前端方案的性能瓶颈,我们评估了迁移到后端生成图片的可行性。对于上面遇到的问题,后端可以有针对性地进行解决:使用Java中awt包下的画图api拼接生成大图,避免了Puppeteer启动浏览器、渲染页面带来的开销。
能否迁移到后端,有两个衡量标准,第一个是性能,即耗时。第二个是后端生成的图片UI样式,即能否达到UI验收标准。
于是我们先在本地测试,发现相同图片耗时仅仅需要20ms左右(相较前端平均2~3秒的时间有较大提升)。即使涉及500多张图片的拼接,平均耗时也只在2s左右。另外在生成的图片样式效果上也达到了UI验收的要求。
前端生成大图:
后端生成大图:
3. 后端生成大图的具体实现
以上文提到的、规则相对复杂的商品详情页大图为例进行说明。通过分析UI原型,我们发现其结构具有清晰的模块化特征:从上往下看依次为基础信息模块、分类皮肤信息模块。其中皮肤信息模块又可分为分类标题模块、单个皮肤单元模块。

这里需要用到的操作,包括绘制图片、文字、伸缩图片、平移图片等。由于后端使用的编程语言为Java,所以先简要介绍一下Java图片处理相关的api。
3.1 Java图像处理api
在Java图像处理中,java.awt.image.BufferedImage.BufferedImage与java.awt.Graphics2D是两个核心类,它们密切协作以实现图像的创建、编辑和渲染。
- BufferedImage是图像数据的画布容器,负责存储像素信息。主要用于读写图像文件。
- Graphics2D是操作图像的画笔,负责绘制和修改图像内容。在BufferedImage上绘制内容。支持绘制文字、图像平移缩放等操作。
3.1.1 创建BufferedImage
ini
/**
* 读取本地文件
*/
BufferedImage imageFromFile = ImageIO.read(new File("本地图片路径"));
/**
* 从网络中读取图片
*/
BufferedImage imageFromUrl= ImageIO.read(new URL("网络图片路径"));
/**
* 通过构造方法创建。构造参数指定宽和高。
*/
BufferedImage combinedImage = new BufferedImage(100, 200, BufferedImage.TYPE_INT_RGB);
3.1.2 绘制图片与文字
java
public static void main(String[] args) throws IOException {
BufferedImage backGroundImage = ImageIO.read(new File("输入路径"));
BufferedImage combinedImage = new BufferedImage(backGroundImage.getWidth(), backGroundImage.getHeight(), BufferedImage.TYPE_INT_RGB);
Graphics2D graphics = combinedImage.createGraphics();
try {
// 画图
graphics.drawImage(backGroundImage, 0, 0, null);
// 写文字
graphics.setFont(new Font("微软雅黑", Font.BOLD, 20));
graphics.setColor(Color.WHITE);
graphics.drawString("文字内容", 20, 20);
} finally {
// 释放资源
graphics.dispose();
}
// 保存结果
File output = new File("输出路径");
ImageIO.write(combinedImage, "jpg", output);
}
3.1.3 图片伸缩
java
public static void main(String[] args) throws IOException {
// 伸缩前的画面
BufferedImage originImage = ImageIO.read(new File("文件路径"));
// 伸缩后的画面
BufferedImage scaleImage = getScaleImage(originImage, 0.7, 0.7);
File output = new File("输出路径");
ImageIO.write(scaleImage, "jpg", output);
}
/**
* 对原始图片按比例进行伸缩
*/
public static BufferedImage getScaleImage(BufferedImage originImage, double scaleFactorWidth, double scaleFactorHeight) {
if (Objects.isNull(originImage) || scaleFactorWidth <= 0 || scaleFactorHeight <= 0) {
return originImage;
}
// 等比例压缩比例
int scaledWidth = (int) (originImage.getWidth() * scaleFactorWidth);
int scaledHeight = (int) (originImage.getHeight() * scaleFactorHeight);
// 创建新的 BufferedImage
BufferedImage scaledImage = new BufferedImage(scaledWidth, scaledHeight, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = scaledImage.createGraphics();
try {
// 绘制缩放后的图像
g2d.drawImage(originImage, 0, 0, scaledWidth, scaledHeight, null);
} finally {
g2d.dispose();
}
return scaledImage;
3.2 基础信息部分
原始UI图如下,我们需要将具体文字信息写入到对应位置内。
实现思路:
- 首先将原始UI图绘制到画布;
- 设置文字字体、大小,以及横纵坐标参数,绘制文字。
效果图如下:
3.3 模块化拼接皮肤分类部分
皮肤分类模块分开来看,可以按照皮肤类型分成若干个大类,如史诗皮肤、限定皮肤等。同时在每个皮肤分类模块前有对应标题。
3.3.1 绘制标题
通过api绘制标题图片到指定位置即可,这样可省去字体设置与字体居中的步骤。
3.3.2 生成皮肤图片单元
每一个皮肤分类中,各个皮肤单元都由四部分元素组成,分别是角标图片、底图图片、文字、浮层图片。

我们拿到这四部分基础原始数据之后,按如下步骤进行绘制:
- 绘制皮肤底图到画布;
- 绘制浮层覆盖到皮肤底图之上;
- 绘制角标图片到指定位置;
- 绘制文字到指定位置。
由于各类小图片信息是短时间内不会变更的,所以这里会对各类小图片进行本地缓存,避免频繁网络请求导致的资源浪费。
3.3.3 生成皮肤分类模块
将所有生成的皮肤图片单元按指定横纵坐标绘制。

3.3.4 拉伸背景与边框
原始UI切图如下:
由于每个账号对应的皮肤数量不同,需要让背景图与边框适配对应数量的皮肤图片总高度。

3.3.5 拼接生成各分类组合大图
通过对每个皮肤分类,重复以上步骤,即可生成各分类组合大图。
4 总结
4.1 性能提升
生成耗时从平均2-3秒(前端+Puppeteer)降至毫秒级(简单图片)至秒级(超复杂图片如500+皮肤),解决了超时问题。
4.2 用户体验
确保了用户在浏览商品时,能快速地获取到游戏账号的核心价值信息。
4.3 扩展性与维护性
通过模块化思想,将图片拼接的核心逻辑(图片加载、绘制、文字渲染、布局、背景处理)抽象为可复用的基础服务模块。再结合动态配置来定义不同游戏的大图布局、元素样式、数据映射规则等,实现了业务逻辑与渲染逻辑的解耦。使新游戏品类的接入效率大幅提升。
现已应用在王者荣耀、原神、火影忍者、枪战王者等多款游戏。
原神: 火影忍者:
枪战王者:
关于作者
张廉洁 转转Java开发工程师
> 转转研发中心及业界小伙伴们的技术学习交流平台,定期分享一线的实战经验及业界前沿的技术话题。
> 关注公众号「转转技术」(综合性)、「大转转FE」(专注于FE)、「转转QA」(专注于QA),更多干货实践,欢迎交流分享~