本文作者:Gisercyw
背景
应用程序蕴含两个局部,代码和资源,资源通常包含配置文件、图标、图片、字体等,他们都间接影响到应用程序的包大小并且肯定水平会影响应用程序的运行速度。在社交直播业务开发中不难发现,以下的两类场景对资源管理的诉求会绝对强烈:
- 在游戏的开发过程中,个别须要应用到大量的图片、音频等资源来丰盛整个游戏内容,而大量的资源就会带来治理上的艰难, 一个好的资源管理也会为当前性能优化提供很大的帮忙。
- 经营流动须要应用资源管理形式并配合浏览器缓存来实现流动资源的预加载 / 预申请, 以晋升页面性能与用户体验。
基于下面两个场景的目标,咱们须要一个通用的资源管理计划,让咱们在游戏或者流动开发中,无需关怀资源加载的细节,只须要指定加载的资源,并且在对应的逻辑地位中增加相应的执行加载代码即可实现对我的项目资源的治理。
调研
游戏框架通常具备较齐备的资源管理计划,而这些资源管理计划具备上面共性和性能:
- 欠缺的资源加载根本机制,比方加载资源、查找资源、销毁资源、缓存资源
- 多资源配置文件治理与分组
- 反对资源过程状态监督
- 资源模块化
- 反对预加载 / 预申请
- 反对自定义资源解决,这样可能让加载更具备灵活性
这里我分为两类管理器,一类是资源预加载库,一类是游戏的资源管理库,并依据资源管理具备的性能点比照下目前已有的资源管理计划的特点。
在调研完这些计划后,我认为上述计划并不齐全适宜咱们,咱们须要的是可能笼罩咱们游戏与流动业务开发更为通用的资源管理计划,不会与任何游戏引擎绑定,这是与其余计划最实质的不同。其次在设计上须要思考更多的是性能问题,采纳插拔式的代码组织形式,在保障主包体积稳固的根底上,通过插件扩大特定场景的性能需要,例如将预申请性能能够作为外围性能,而面向各个场景的资源转换、缓存性能能够作为独立的插件。具体的不同如下:
- 绝对于游戏资源管理,大部分的游戏资源管理更多的目标是为其游戏引擎提供开箱即用的资源管理工作,跟游戏引擎耦合较深;另外尽管大型游戏引擎具备齐备的资源管理体系,然而在预申请等场景下并不反对;
- 绝对于这些预加载库,其作用次要是资源加载,而资源管理器的定位不只是资源的加载器,还包含资源管理,缓存,解析,转换等性能,并且在资源优先级等方面都做了残缺的定义。另外在通过团队的测试后,发现 resource-loader 与 PxLoader 这类预加载库,在音视频资源的解决与加载上会存在一些兼容性问题,不反对 SSR/SSG 等服务端渲染等。
整体设计
针对业务开发中的外围场景,在放弃资源管理外围模块根底上,通过插件化架构,设计出资源管理器的整个体系,如下图所示。
依赖能力
资源管理器次要依赖 Extension 模块的注册能力和 WebWorker 的多线程能力。
资源解析与转换是一项耗时的操作,特地是在须要大量资源的游戏场景中,如果只是并发的加载、解析大量的资源,因为 Javascript 单线程的起因,会容易产生卡顿景象,导致页面无奈及时响应,而 Web Worker 使得网页中进行多线程编程成为可能。当主线程在解决界面事件时,Worker 能够在后盾运行,帮你解决大量的资源加载、解析、转换、缓存工作,当实现这些操作后,将加载后果或者缓存数据返回给主线程,由主线程更新 UI。资源管理器内置了 WebWorker 来解析、加载资源,每种类型资源的解决都能够通过开启 Worker 通道来实现,默认是开启的,同时也提供了开启参数可能笼罩默认配置,须要留神的是并非所有的环境都反对 Workers,在一些场景下设置不开启可能更适合。如下所示,以解决 Image 资源转换 Buffer 为例,通过指定资源转换脚本的 URI 来执行 Worker 线程。
import WorkController from 'music/WorkController';
const MAX_WORKER_NUM = navigator.hardwareConcurrency || 6;
const loadBufferImageCode = `
async function loadBufferImage(url) {const result = await fetch(url);
if (!result.ok)
{throw new Error('failed to load');
}
const imageBuffer = await result.arrayBuffer();
return imageBuffer;
}
onmessage = async (e) =>
{
const {
data: {
uuid,
id,
}
} = e
try
{const bufferImage = await loadBufferImage(e.data.data[0]);
postMessage({
data: bufferImage,
uuid,
id,
}, [bufferImage]);
}
catch(error)
{
postMessage({
error,
uuid,
id,
});
}
};
`;
let worker = WorkController.workerPool.pop();
if (!worker && WorkController.WorkersNumber < MAX_WORKER_NUM) {
const workerURL = URL.createObjectURL(new Blob([loadBufferImageCode], {type: 'application/javascript'})
);
WorkController.WorkersNumber++;
worker = new Worker(workerURL);
worker.addEventListener('message', (event: MessageEvent) => {WorkController.complete(event.data);
WorkController.next();});
}
插件注册能力是资源管理器的一个根本能力,形式是主性能通过主包引入,其余性能通过插件的模式按需引入,既可能保障主包的稳固,又可能减小整个包体积。资源管理器的外围性能是资源预加载,而针对特定类型资源的解析、缓存、转换则是通过对应插件来实现,插件模块的次要办法类型定义如下所示,提供了插件解决的基本功能。
declare const ExtensionModule: {
/**
* 移除插件
*/
remove(...extensions: Array<ExtensionOptionType>): any;
/**
* 注册插件
*/
add(...extensions: Array<ExtensionOptionType>): any;
/**
* 增加 / 删除扩大时的解决性能
*/
registerHandler(type: ExtensionType, onAdd: ExtensionHandler, onRemove: ExtensionHandler): any;
/**
* 解决插件列表
*/
handleExtensions(type: ExtensionType, list: any[]): any;
};
外围模块
对于特定类型的资源,在资源管理器底层会通过资源检测、资源映射、加载解析、资源缓存的流程,每个环节都是独立的,其中局部环节并不是必须的,因而不是每个资源都会齐全走完这几步,例如如果是预申请资源,则不须要缓存,因为预申请利用的是浏览器缓存,对于须要应用的性能,能够通过插件或者参数设置开启。
内部接口
内部接口次要提供了两类接口,一类独自的资源接口(Resource),一类是缓存接口(Cache)。而资源为了满足模块化的场景,咱们又将其分为 Resource 与 Bundle,Resource 提供全局资源的操作,Bundle 提供模块化资源的操作。在这些简洁易用的接口根底上,咱们能够轻松实现资源的预申请、资源预加载、手动加载与主动加载,资源缓存解决等操作。
性能应用
上面选取几种业务中常见的的场景来介绍资源管理器的理论应用形式,能够满足小游戏或者流动开发中资源加载与转换的需要。
预加载
预加载是一种浏览器机制,应用浏览器闲暇工夫来事后下载 / 加载用户接下来很可能会浏览的页面 / 资源,当用户拜访某个预加载的链接时,如果从缓存命中, 页面就得以疾速出现。预加载个别会配合 loading 或者加载页来出现,正当的无效加载交互设计能够缩小用户焦虑,加重用户期待的压力,而每个阶段预加载资源的调配可能无效升高页面访问速度,缩小页面切换时的闪动问题,进而达到晋升用户体验的目标。
资源管理器的预加载性能能够通过简洁的 api 来实现,如下所示:
const loadAssets = [
{
src: 'https://someurl.png',
type: 'IMAGE', // 图片
},
{
src: 'https://someurl.mp3',
type: 'AUDIO', // 音频资源
},
{
src: 'http://someurl.mp4',
type: 'VIDEO', // 视频资源
},
{
src: 'https://someurl.ttf',
type: 'FONT', // 字体资源
subType: 'ttf',
},
{
src: 'https://someurl.json',
type: 'JSON', // JSON 资源
}
];
const LoadingPage = () => {const [progress, setProgress] = React.useState(0);
React.useEffect(() => {const load = async () => {const res = await Resource.loadResource(loadAssets, (progress) => {setProgress(Number(progress.toString().match(/^\d+(?:\.\d{0,2})?/)) * 100);
});
};
load();}, []);
return (<div> 资源加载进度:{progress}%</div>
);
};
资源模块化
在游戏开发中,咱们会须要将资源依照不同的性能和场景划分与应用,如下图所示,资源管理器中能够将图片,脚本,多媒体等资源指定为多个 Bundle,其中每种类型资源还能够依据页面划分成多个 Bundle,比方图片能够依据首屏图片、弹窗与浮层图片、非首屏图片分成多个 Bundle,而后在游戏运行过程中,依照需要去加载不同的 Bundle,以缩小启动时须要加载的资源数量,从而缩小首次下载和加载游戏时所需的工夫。
// 增加
Resource.addBundle('first-scene', {
mainBg: 'backgroundA.png',
avatar: 'avatarA.png',
font: 'fontA.ttf',
});
// 增加
Resource.addBundle('next-scene', {
mainBg: 'backgroundB.png',
avatar: 'avatarB.png',
font: 'fontB.ttf',
});
// 加载
const firstSceneResource = await Resource.loadBundle('first-scene');
// 加载
const nextSceneResource = await Resource.loadBundle('next-screen');
资源转换
以图片类型资源转换为例,首先要启用图片转换插件,次要通过以下形式注册插件
import ResourceImagePlugin from 'resource-image-plugin';
Resource.addPlugin(ResourceImagePlugin)
而后通过 formatType 参数指定转换类型,resource-image-plugin 能够反对以下类型转换:Buffer、Blob、BitMap、PixiTexture
png 转 Bitmap
const res = await Resource.loadResource(
{
src: 'https://p5.music.126.net/obj/wo3DlcOGw6DClTvDisK1/24086412116/de58/ecc0/3ef8/d0ce5485ed549eeb0e77b8a2e54bb4c4.png',
formatType: 'Bitmap',
}
);
png 转 Pixi Texture
const res = await Resource.loadResource(
{
src: 'https://p5.music.126.net/obj/wo3DlcOGw6DClTvDisK1/24086412116/de58/ecc0/3ef8/d0ce5485ed549eeb0e77b8a2e54bb4c4.png',
formatType: 'Texture',
}
);
总结
目前资源管理器曾经社交直播多个业务中落地,其不仅为 Alice.js 底层提供开箱即用的资源管理能力,同时为社交直播经营流动提供了预加载的伎俩,将来还会针对外部其余场景适配与反对,例如反对 3D 资源 / 模型、智能化加载等。
本文次要剖析了资源管理的现状与存在问题,在业务游戏化背景下,摸索了合乎社交直播业务倒退的资源管理解决方案,并介绍了不同场景下的应用形式,如果您对此内容感兴趣,能够评论交换。
参考资料
- Cocos:https://github.com/cocos/cocos-engine
- Pixi.js:https://github.com/pixijs/pixijs
本文公布自网易云音乐技术团队,文章未经受权禁止任何模式的转载。咱们长年招收各类技术岗位,如果你筹备换工作,又恰好喜爱云音乐,那就退出咱们 grp.music-fe(at)corp.netease.com!