Cocos Creator远程资源加载踩坑记录

Cocos Creator远程资源加载踩坑记录

记录Cocos Creator项目开发中遇到的资源加载相关问题。

资源加载机制

1.1 资源加载机制

Cocos Creator的资源系统采用延迟加载策略,主要特点包括:

特性 说明
延迟加载 资源按需加载,减少初始内存占用
缓存机制 加载的资源自动缓存,避免重复加载
引用计数 自动管理资源生命周期
异步加载 不阻塞主线程,保证游戏流畅度

1.2 资源类型分类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
资源类型
├── 图片资源
│ ├── Sprite (精灵)
│ ├── Texture (纹理)
│ └── Atlas (图集)
├── 音频资源
│ ├── Music (背景音乐)
│ └── Effect (音效)
├── 数据资源
│ ├── JSON
│ ├── XML
│ └── Plist
└── 场景/预制体
├── Scene
└── Prefab

二、远程资源加载技术

2.1 HTTP请求工具封装

在发布Native版本时,需要自定义HTTP请求工具来获取远程资源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
/**
* HTTP请求管理器
* 支持GET/POST请求,支持文本和二进制数据
*/
const HttpRequest = {
// 当前请求地址集合,用于防重复请求
_urls: {},

// HTTP事件定义
Events: {
NO_NETWORK: "http_request_no_network",
UNKNOWN_ERROR: "http_request_unknown_error"
},

/**
* GET请求
* @param {string} url - 请求地址
* @param {function} completeCallback - 成功回调
* @param {function} errorCallback - 失败回调
*/
get(url, completeCallback, errorCallback) {
this._sendRequest(url, null, false, completeCallback, errorCallback);
},

/**
* 获取二进制数据(用于图片等资源)
* @param {string} url - 请求地址
* @param {function} completeCallback - 成功回调
* @param {function} errorCallback - 失败回调
*/
getByArraybuffer(url, completeCallback, errorCallback) {
this._sendRequest(url, null, false, completeCallback, errorCallback, 'arraybuffer');
},

/**
* GET请求(带参数)
* @param {string} url - 请求地址
* @param {object} params - 查询参数
* @param {function} completeCallback - 成功回调
* @param {function} errorCallback - 失败回调
*/
getWithParams(url, params, completeCallback, errorCallback) {
this._sendRequest(url, params, false, completeCallback, errorCallback);
},

/**
* POST请求
* @param {string} url - 请求地址
* @param {object} params - 请求参数
* @param {function} completeCallback - 成功回调
* @param {function} errorCallback - 失败回调
*/
post(url, params, completeCallback, errorCallback) {
this._sendRequest(url, params, true, completeCallback, errorCallback);
},

/**
* 构建参数字符串
* @param {object} params - 参数对象
* @returns {string} URL参数字符串
*/
_getParamString(params) {
let result = "";
for (let name in params) {
result += name + "=" + encodeURIComponent(params[name]) + "&";
}
return result.substr(0, result.length - 1);
},

/**
* 发送HTTP请求
*/
_sendRequest(url, params, isPost, completeCallback, errorCallback, responseType) {
if (!url) return;

// 构建完整URL
let newUrl = params ? url + "?" + this._getParamString(params) : url;

// 防重复请求检查
if (this._urls[newUrl]) {
cc.warn("地址不能重复请求: " + url);
return;
}
this._urls[newUrl] = true;

// 创建XMLHttpRequest
let xhr = cc.loader.getXMLHttpRequest();

if (isPost) {
xhr.open("POST", url);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.setRequestHeader("Accept", "application/json");
} else {
xhr.open("GET", newUrl, true);
}

// 设置响应类型
if (responseType === 'arraybuffer') {
xhr.responseType = responseType;
}

// 错误处理
xhr.onerror = () => {
delete this._urls[newUrl];
if (!errorCallback) return;
if (xhr.readyState === 1 && xhr.status === 0) {
errorCallback(this.Events.NO_NETWORK);
} else {
errorCallback(this.Events.UNKNOWN_ERROR);
}
};

// 响应处理
xhr.onreadystatechange = () => {
if (xhr.readyState !== 4) return;
delete this._urls[newUrl];

if (xhr.status === 200) {
if (completeCallback) {
const response = responseType === 'arraybuffer'
? xhr.response
: xhr.responseText;
completeCallback(response);
}
} else {
if (errorCallback) errorCallback(xhr.status);
}
};

// 发送请求
xhr.send(isPost ? JSON.stringify(params) : null);
}
};

// 挂载到全局
global.game = global.game || {};
game.http = HttpRequest;

2.2 远程JSON加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* 远程JSON加载器
*/
const RemoteJsonLoader = {
/**
* 加载远程JSON文件
* @param {string} url - JSON文件地址
* @param {function} callback - 回调函数 (error, data)
*/
load(url, callback) {
// 发布Native版本使用HTTP获取
game.http.get(url, (data) => {
try {
const json = JSON.parse(data);
callback(null, json);
} catch (e) {
callback(e, null);
}
}, (status) => {
callback(new Error('HTTP Error: ' + status), null);
});

/*
// 发布Web版本可直接使用cc.loader
cc.loader.load({ url: url }, (err, json) => {
callback(err, json);
});
*/
}
};

使用示例:

1
2
3
4
5
6
7
8
9
10
// 加载配置文件
RemoteJsonLoader.load('https://example.com/config/game.json', (err, config) => {
if (err) {
cc.error('加载配置失败:', err);
return;
}
// 使用配置数据
game.config = config;
cc.log('配置加载成功:', config);
});

2.3 远程Plist图集加载

Plist是游戏开发中常用的图集格式,包含多张小图的坐标信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
/**
* Plist解析工具
*/
const PlistParser = {
// 大括号正则
BRACE_REGEX: /[\{\}]/g,

/**
* 解析尺寸字符串 {width,height}
*/
parseSize(sizeStr) {
sizeStr = sizeStr.slice(1, -1);
const arr = sizeStr.split(',');
return new cc.Size(
parseFloat(arr[0]),
parseFloat(arr[1])
);
},

/**
* 解析向量字符串 {x,y}
*/
parseVec2(vec2Str) {
vec2Str = vec2Str.slice(1, -1);
const arr = vec2Str.split(',');
return new cc.Vec2(
parseFloat(arr[0]),
parseFloat(arr[1])
);
},

/**
* 解析三角形索引
*/
parseTriangles(trianglesStr) {
return trianglesStr.split(' ').map(parseFloat);
},

/**
* 解析顶点数据
*/
parseVertices(verticesStr) {
return verticesStr.split(' ').map(parseFloat);
},

/**
* 解析矩形 {{x,y},{w,h}}
*/
parseRect(rectStr) {
rectStr = rectStr.replace(this.BRACE_REGEX, '');
const arr = rectStr.split(',');
return new cc.Rect(
parseFloat(arr[0] || 0),
parseFloat(arr[1] || 0),
parseFloat(arr[2] || 0),
parseFloat(arr[3] || 0)
);
},

/**
* 解析Plist数据并创建图集
*/
parsePlist(plist, texture) {
const info = plist.metadata;
const frames = plist.frames;

// 创建图集
const atlas = new cc.SpriteAtlas();
const spriteFrames = atlas._spriteFrames;

for (let key in frames) {
const frame = frames[key];
let rotated = false;
let sourceSize, offsetStr, textureRect;

// 根据format版本解析数据
if (info.format === 0) {
rotated = false;
sourceSize = `{${frame.originalWidth},${frame.originalHeight}}`;
offsetStr = `{${frame.offsetX},${frame.offsetY}}`;
textureRect = `{{${frame.x},${frame.y}},{${frame.width},${frame.height}}}`;
}
else if (info.format === 1 || info.format === 2) {
rotated = frame.rotated;
sourceSize = frame.sourceSize;
offsetStr = frame.offset;
textureRect = frame.frame;
}
else if (info.format === 3) {
rotated = frame.textureRotated;
sourceSize = frame.spriteSourceSize;
offsetStr = frame.spriteOffset;
textureRect = frame.textureRect;
}

// 创建精灵帧
const spriteFrame = new cc.SpriteFrame();
spriteFrame.setTexture(
texture,
this.parseRect(textureRect),
!!rotated,
this.parseVec2(offsetStr),
this.parseSize(sourceSize)
);

// 处理网格数据(如果存在)
if (frame.triangles) {
const vertices = this.parseVertices(frame.vertices);
const verticesUV = this.parseVertices(frame.verticesUV);

spriteFrame.vertices = {
triangles: this.parseTriangles(frame.triangles),
x: [],
y: [],
u: [],
v: []
};

for (let i = 0; i < vertices.length; i += 2) {
spriteFrame.vertices.x.push(vertices[i]);
spriteFrame.vertices.y.push(vertices[i + 1]);
}
for (let i = 0; i < verticesUV.length; i += 2) {
spriteFrame.vertices.u.push(verticesUV[i]);
spriteFrame.vertices.v.push(verticesUV[i + 1]);
}
}

// 使用文件名作为key
const name = cc.path.mainFileName(key);
spriteFrames[name] = spriteFrame;
}

return atlas;
}
};

远程Plist加载器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* 远程Plist图集加载器
*/
function loadRemotePlist(url, callback) {
// 发布Native版本:使用HTTP获取plist文本
game.http.get(url, (data) => {
// 解析plist XML
const plist = cc.plistParser.parse(data);

// 获取纹理文件名
const textureFileName = plist.metadata.realTextureFileName
|| plist.metadata.textureFileName;
const textureUrl = cc.path.join(cc.path.dirname(url), textureFileName);

// 加载纹理
cc.loader.load(textureUrl, (err, tex) => {
if (err) {
return callback(err);
}
// 解析并创建图集
const atlas = PlistParser.parsePlist(plist, tex);
callback(null, atlas);
});
}, (status) => {
callback(new Error('Failed to load plist: ' + status));
});

/*
// 发布Web版本可直接使用cc.loader
cc.loader.load(url, (err, plist) => {
if (err) {
cc.log("plist err:" + JSON.stringify(err));
return callback(err);
}
const textureFileName = plist.metadata.realTextureFileName
|| plist.metadata.textureFileName;
const textureUrl = cc.path.join(cc.path.dirname(url), textureFileName);
cc.loader.load(textureUrl, (err, tex) => {
if (err) return callback(err);
const atlas = PlistParser.parsePlist(plist, tex);
callback(null, atlas);
});
});
*/
}

三、图片黑边/灰边问题处理

3.1 问题现象

在Cocos Creator中使用透明图片时,可能会出现以下问题:

  • 图片边缘出现黑色或灰色边框
  • 半透明区域颜色异常
  • 旋转后边缘出现杂色

3.2 问题原因

主要原因是**Alpha预乘(Premultiply Alpha)**处理不一致:

1
2
3
4
5
6
7
正常处理流程:
图片资源 -> TexturePacker打包 -> Cocos Creator渲染

问题场景:
1. 打包时未开启预乘,渲染时开启了预乘
2. 打包时开启了预乘,渲染时未开启预乘
3. 混合模式(Blend Mode)设置不正确

3.3 解决方案

方案一:TexturePacker预乘设置

在TexturePacker中打包图集时,勾选”Premultiply Alpha”选项。

方案二:Cocos Creator编辑器设置(推荐)

  1. 图片资源设置

    • 选择图片或合图资源
    • 在属性检查器中勾选 Premultiply Alpha
  2. Sprite组件设置

    • 设置 Src Blend FactorONE
1
2
3
// 代码中设置
const sprite = node.getComponent(cc.Sprite);
sprite.srcBlendFactor = cc.macro.BlendFactor.ONE;

方案三:运行时动态设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 修复图片黑边问题
* @param {cc.Node} node - 包含Sprite的节点
*/
function fixBlackEdge(node) {
const sprite = node.getComponent(cc.Sprite);
if (sprite) {
// 设置混合模式
sprite.srcBlendFactor = cc.macro.BlendFactor.ONE;
sprite.dstBlendFactor = cc.macro.BlendFactor.ONE_MINUS_SRC_ALPHA;
}
}

// 批量修复
function fixAllSprites(rootNode) {
const sprites = rootNode.getComponentsInChildren(cc.Sprite);
sprites.forEach(sprite => {
sprite.srcBlendFactor = cc.macro.BlendFactor.ONE;
});
}

3.4 混合模式对照表

Src Blend Dst Blend 效果
SRC_ALPHA ONE_MINUS_SRC_ALPHA 标准Alpha混合
ONE ONE_MINUS_SRC_ALPHA 预乘Alpha混合(推荐)
ONE ONE 叠加模式
SRC_ALPHA ONE 发光效果

四、Widget组件自适应布局

4.1 Widget组件简介

Widget组件用于自动调整节点的坐标和宽高,适应不同分辨率的屏幕。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 获取Widget组件
const widget = node.getComponent(cc.Widget);

// 开启顶部对齐
widget.isAlignTop = true;
widget.top = 10;

// 开启底部对齐
widget.isAlignBottom = true;
widget.bottom = 10;

// 立即更新布局
widget.updateAlignment();

4.2 立即获取对齐结果

Widget组件默认在下一帧才更新布局,如需立即获取更新后的值,需手动调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
const node = cc.find('Canvas/TopBar');
const widget = node.getComponent(cc.Widget);

// 手动更新对齐
widget.updateAlignment();

// 现在可以获取正确的位置和尺寸
const worldPos = node.convertToWorldSpaceAR(cc.Vec2.ZERO);
const width = node.width;
const height = node.height;

cc.log('更新后的位置:', worldPos);
cc.log('更新后的尺寸:', width, height);

4.3 常用布局模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* 顶部固定导航栏
*/
function setupTopBar(node) {
const widget = node.addComponent(cc.Widget);
widget.isAlignTop = true;
widget.isAlignLeft = true;
widget.isAlignRight = true;
widget.top = 0;
widget.left = 0;
widget.right = 0;
widget.updateAlignment();
}

/**
* 底部固定工具栏
*/
function setupBottomBar(node) {
const widget = node.addComponent(cc.Widget);
widget.isAlignBottom = true;
widget.isAlignLeft = true;
widget.isAlignRight = true;
widget.bottom = 0;
widget.left = 0;
widget.right = 0;
widget.updateAlignment();
}

/**
* 全屏背景
*/
function setupFullscreenBg(node) {
const widget = node.addComponent(cc.Widget);
widget.isAlignTop = true;
widget.isAlignBottom = true;
widget.isAlignLeft = true;
widget.isAlignRight = true;
widget.top = 0;
widget.bottom = 0;
widget.left = 0;
widget.right = 0;
widget.updateAlignment();
}

五、Unity AssetBundle版本管理

5.1 版本统一的重要性

在使用Unity进行游戏开发时,AssetBundle的版本统一至关重要:

1
2
3
4
问题场景:
- 团队成员使用不同版本的Unity(如4.1和4.2)
- 4.1版本打出的AssetBundle在4.2版本中可能无法读取
- 导致热更新资源加载失败、游戏崩溃等问题

5.2 版本管理规范

1. Unity版本锁定

1
2
3
4
项目根目录/ProjectSettings/ProjectVersion.txt
---
m_EditorVersion: 2019.4.10f1
m_EditorVersionWithRevision: 2019.4.10f1 (5319b55fb807)

2. 版本检查脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using UnityEditor;
using UnityEngine;

public class VersionChecker
{
[InitializeOnLoadMethod]
static void CheckVersion()
{
const string RequiredVersion = "2019.4.10f1";
string currentVersion = Application.unityVersion;

if (currentVersion != RequiredVersion)
{
Debug.LogError($"Unity版本不匹配!当前版本: {currentVersion}, 要求版本: {RequiredVersion}");
EditorUtility.DisplayDialog(
"版本警告",
$"当前Unity版本({currentVersion})与项目要求版本({RequiredVersion})不一致,\n" +
"请使用正确的Unity版本打开项目,以避免AssetBundle兼容性问题。",
"确定"
);
}
}
}

3. 资源打包脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using UnityEditor;
using System.IO;

public class AssetBundleBuilder
{
[MenuItem("Tools/Build AssetBundles")]
static void BuildAllAssetBundles()
{
string outputPath = "Assets/StreamingAssets/AssetBundles";

if (!Directory.Exists(outputPath))
{
Directory.CreateDirectory(outputPath);
}

// 使用当前平台的目标构建设置
BuildPipeline.BuildAssetBundles(
outputPath,
BuildAssetBundleOptions.ChunkBasedCompression,
EditorUserBuildSettings.activeBuildTarget
);

Debug.Log("AssetBundle打包完成!");
}
}

5.3 热更新版本验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
using System;
using UnityEngine;

public class HotUpdateManager : MonoBehaviour
{
[Serializable]
public class AssetBundleManifest
{
public string version;
public string unityVersion;
public BundleInfo[] bundles;
}

[Serializable]
public class BundleInfo
{
public string name;
public string hash;
public long size;
}

/// <summary>
/// 检查资源版本兼容性
/// </summary>
public bool CheckVersionCompatibility(AssetBundleManifest manifest)
{
// 检查Unity版本
if (manifest.unityVersion != Application.unityVersion)
{
Debug.LogError($"Unity版本不兼容!本地: {Application.unityVersion}, 资源: {manifest.unityVersion}");
return false;
}

// 检查资源版本
string localVersion = PlayerPrefs.GetString("AssetVersion", "0");
if (string.Compare(manifest.version, localVersion) < 0)
{
Debug.LogWarning("远程资源版本低于本地版本");
return false;
}

return true;
}
}

六、开发环境常见问题

6.1 PowerShell执行策略

在Windows PowerShell中运行Node.js或npm命令时,可能遇到以下错误:

1
无法加载文件 xxx.ps1,因为在此系统上禁止运行脚本

解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
# 以管理员身份运行PowerShell
# 查看当前执行策略
Get-ExecutionPolicy
# 显示: Restricted

# 修改执行策略
Set-ExecutionPolicy RemoteSigned
# 输入 Y 确认

# 验证修改
Get-ExecutionPolicy
# 显示: RemoteSigned

6.2 方法存在性检查

在JavaScript中安全调用可能不存在的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 安全调用方法
* @param {*} func - 待调用方法
* @param {*} context - 执行上下文
* @param {...*} args - 参数
*/
function safeCall(func, context, ...args) {
try {
if (func && typeof func === 'function') {
return func.apply(context, args);
}
} catch (e) {
console.error('方法调用失败:', e);
}
}

// 使用示例
window.onload = function() {
// 安全调用可能不存在的方法
safeCall(test, window, 'param1', 'param2');
};

function test(param1, param2) {
console.log('方法执行:', param1, param2);
}

图片黑边问题

问题现象

使用透明图片时可能出现:

  • 图片边缘出现黑色或灰色边框
  • 半透明区域颜色异常
  • 旋转后边缘出现杂色

问题原因

主要原因是**Alpha预乘(Premultiply Alpha)**处理不一致:

1
2
3
4
5
6
7
正常处理流程:
图片资源 -> TexturePacker打包 -> Cocos Creator渲染

问题场景:
1. 打包时未开启预乘,渲染时开启了预乘
2. 打包时开启了预乘,渲染时未开启预乘
3. 混合模式(Blend Mode)设置不正确

解决方案

方案一:TexturePacker预乘设置

在TexturePacker中打包图集时,勾选”Premultiply Alpha”选项。

方案二:Cocos Creator编辑器设置(推荐)

  1. 选择图片或合图资源
  2. 在属性检查器中勾选 Premultiply Alpha
  3. Sprite组件设置 Src Blend FactorONE
1
2
const sprite = node.getComponent(cc.Sprite);
sprite.srcBlendFactor = cc.macro.BlendFactor.ONE;

混合模式对照表

Src Blend Dst Blend 效果
SRC_ALPHA ONE_MINUS_SRC_ALPHA 标准Alpha混合
ONE ONE_MINUS_SRC_ALPHA 预乘Alpha混合(推荐)
ONE ONE 叠加模式
SRC_ALPHA ONE 发光效果

Widget组件自适应

立即获取对齐结果

Widget组件默认在下一帧才更新布局,如需立即获取更新后的值:

1
2
3
4
5
6
7
8
9
10
const node = cc.find('Canvas/TopBar');
const widget = node.getComponent(cc.Widget);

// 手动更新对齐
widget.updateAlignment();

// 现在可以获取正确的位置和尺寸
const worldPos = node.convertToWorldSpaceAR(cc.Vec2.ZERO);
const width = node.width;
const height = node.height;

常用布局模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 顶部固定导航栏
function setupTopBar(node) {
const widget = node.addComponent(cc.Widget);
widget.isAlignTop = true;
widget.isAlignLeft = true;
widget.isAlignRight = true;
widget.top = 0;
widget.left = 0;
widget.right = 0;
widget.updateAlignment();
}

// 底部固定工具栏
function setupBottomBar(node) {
const widget = node.addComponent(cc.Widget);
widget.isAlignBottom = true;
widget.isAlignLeft = true;
widget.isAlignRight = true;
widget.bottom = 0;
widget.left = 0;
widget.right = 0;
widget.updateAlignment();
}

// 全屏背景
function setupFullscreenBg(node) {
const widget = node.addComponent(cc.Widget);
widget.isAlignTop = true;
widget.isAlignBottom = true;
widget.isAlignLeft = true;
widget.isAlignRight = true;
widget.top = 0;
widget.bottom = 0;
widget.left = 0;
widget.right = 0;
widget.updateAlignment();
}

PowerShell执行策略

Windows PowerShell运行Node.js或npm命令时可能报错:

1
无法加载文件 xxx.ps1,因为在此系统上禁止运行脚本

解决方案:

1
2
3
4
# 以管理员身份运行PowerShell
Get-ExecutionPolicy
Set-ExecutionPolicy RemoteSigned
Get-ExecutionPolicy

小结

这篇文章记录了Cocos Creator项目开发中的踩坑经验,包括资源加载、图集解析、黑边问题、UI布局等。希望对你有帮助。


有问题欢迎交流。