unity-AssetBundle资源冗余检测
- 最近搞了下 unity 的 AssetBundle(以下简称 AB) 资源冗余检测,并导出一个md文件列出冗余的资源及其被打入进哪几个AB中,方便排除冗余
思路
- 科普下AB资源冗余,当然这里并不会详细说明,只会放个链接 点我 (需要科学上网)
- 可以搜到大部分的文章都是在说一个 侑虎科技 的第三方检测平台,我也去上面检测了下正确检测出来,据说是免费几次后就需要付费。但是资源不加密就上传上去貌似不是很靠谱,资源大了上传也麻烦。所以就自己实现了一个检测。
- 检测的思路也很简单
- 先用主AB生产一个所有 资源及所在AB 的一个 映射表
- 递归遍历打出来的包下的所有的AB,通过
AssetDatabase.GetDependencies
获取到 AB中资源名字在 AssetDatabase 中所有的依赖。(此时要求工程下Asset下有正常的资源) - 遍历所有依赖,是否在 映射表 中,如果 不存在 且 超过两次,相同 资源A 被打进了两个AB中,而不是 资源A 打成一个AB,被其他AB依赖进去。
- 最后会收集到这些 不存在 且 超过两次 的资源名及被打进去的AB文件名,导出到一个md文件中,使用md编辑器查看(这里推荐个md客户端叫 Haroopad,平常都用这个写md)
- 检测的思路也很简单
源码 及 使用
鉴于源码就一个cs文件,就不上传到git,直接这里贴了,同时也会上传几个测试的 源资源(test_res.rar)、打包出的 有冗余(ABoutput.rar)、无冗余(ABoutput_red.rar) 的资源
使用:
分别解压出来,test_res目录放到工程Asset目录下(AssetDatabase才能找到资源,获取依赖),打包出的资源随意放(最好英文路径)
选择打包出的资源的主AB
开始检测,有冗余会导出md文件
资源传送门:unity3d冗余测试资源.rar(csdn传了一天还在审查,放在百度盘,失效说声)
源码:
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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357using System;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using UnityEditor;
using System.IO;
using System.Linq;
class ABRedundancyChecker : EditorWindow
{
class CRedAsset
{
public CRedAsset()
{ }
public string mName = "";
public string mType = "";
public List<string> mUsers = new List<string>();
}
List<Type> mAssetTypeList = new List<Type> {
typeof(Material), typeof(Texture2D), typeof(AnimationClip), typeof(AudioClip), typeof(Sprite), typeof(Shader), typeof(Font), typeof(Mesh)
};
const string kABRedundencyDir = "/a_ABRedundency"; //输出文件的目录
const string kSearchPattern = "*.assetbundle";
string kResultPath = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory) + kABRedundencyDir;
//const string kABPath = "Assets/test_res/ABoutput_red";
//const string kManiFest = "ABoutput";
bool mIsForQ6 = false;
string mABPath = "D:\\svn_q6_app\\win64\\q6-v0.0.63.0\\q6_Data\\StreamingAssets\\resource";
string mMainAb = "resource";
List<string> mAllABFiles = null;
Dictionary<string, string> mAssetGenMap = null;
Dictionary<string, CRedAsset> mRedAssetMap = null;
float mCheckTime = 0f;
static ABRedundancyChecker mInstance = null;
public static ABRedundancyChecker Ins
{
get
{
if (mInstance == null)
mInstance = new ABRedundancyChecker();
return mInstance;
}
}
//[MenuItem("AB冗余检测/AB检测")]
////[MenuItem("Q5/Bundle相关/Bundle冗余检测")]
//public static void Launch()
//{
// ABRedundancyChecker.Ins.StartCheck();
//}
// 提供给其他脚本调用的接口
public void StartCheck(string path, string abName)
{
mABPath = path;
mMainAb = abName;
mIsForQ6 = true;
StartCheck();
}
void StartCheck()
{
EditorUtility.DisplayCancelableProgressBar("AB资源冗余检测中", "资源读取中......", 0f);
mCheckTime = UnityEngine.Time.realtimeSinceStartup;
if (mAllABFiles == null)
mAllABFiles = new List<string>();
if (mAssetGenMap == null)
mAssetGenMap = new Dictionary<string, string>();
if (mRedAssetMap == null)
mRedAssetMap = new Dictionary<string, CRedAsset>();
if (!GenAssetMap(mABPath, mMainAb))
{
EditorUtility.ClearProgressBar();
EditorUtility.DisplayDialog("错误", "请检查是否选择正确的AB资源", "Ok");
return;
}
GetAllFiles(mAllABFiles, mABPath, kSearchPattern);
int startIndex = 0;
EditorApplication.CallbackFunction myUpdate = null;
myUpdate = () =>
{
string file = mAllABFiles[startIndex];
AssetBundle ab = null;
try
{
ab = CreateABAdapter(file);
string[] arr = file.Split('/');
CheckABInfo(ab, arr[arr.Length - 1]);
}
catch (Exception e)
{
Debug.LogError("MyError:" + e.StackTrace);
}
finally
{
if (ab != null)
ab.Unload(true);
}
bool isCancel = EditorUtility.DisplayCancelableProgressBar("AB资源冗余检测中", file, (float)startIndex / (float)mAllABFiles.Count);
startIndex++;
if (isCancel || startIndex >= mAllABFiles.Count)
{
EditorUtility.ClearProgressBar();
if (!isCancel)
{
CullNotRed();
mCheckTime = UnityEngine.Time.realtimeSinceStartup - mCheckTime;
EditorUtility.DisplayDialog("AssetBundle资源冗余检测结果", Export(), "Ok");
}
mAllABFiles.Clear();
mAllABFiles = null;
mAssetGenMap.Clear();
mAssetGenMap = null;
mRedAssetMap.Clear();
mRedAssetMap = null;
Resources.UnloadUnusedAssets();
EditorUtility.UnloadUnusedAssetsImmediate();
GC.Collect();
EditorApplication.update -= myUpdate;
startIndex = 0;
}
};
EditorApplication.update += myUpdate;
}
//适配项目打包(有加密) 或 原生打包
AssetBundle CreateABAdapter(string path)
{
//if (mIsForQ6)
// return UtilCommon.CreateBundleFromFile(path);
//else
return AssetBundle.LoadFromFile(path);
}
bool GenAssetMap(string path, string maniFest)
{
path = path.Replace("\\", "/");
AssetBundle maniFestAb = CreateABAdapter(System.IO.Path.Combine(path, maniFest));
if (maniFestAb == null)
return false;
AssetBundleManifest manifest = maniFestAb.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
if (manifest == null)
return false;
string[] allBundles = manifest.GetAllAssetBundles();
maniFestAb.Unload(true);
foreach (string abName in allBundles)
{
string filePath = System.IO.Path.Combine(path, abName);
AssetBundle ab = CreateABAdapter(filePath);
foreach (string asset in ab.GetAllAssetNames())
{
mAssetGenMap.Add(asset.ToLower(), abName);
}
foreach (string asset in ab.GetAllScenePaths())
{
mAssetGenMap.Add(asset.ToLower(), abName);
}
ab.Unload(true);
}
if (mAssetGenMap.Count == 0)
return false;
return true;
}
void CheckABInfo(AssetBundle ab, string abName)
{
EditorSettings.serializationMode = SerializationMode.ForceText;
string[] names = ab.GetAllAssetNames();
string[] dependencies = AssetDatabase.GetDependencies(names);
string[] allDepen = dependencies.Length > 0 ? dependencies : names;
string currDep = "";
for (int i = 0; i < allDepen.Length; i++)
{
currDep = allDepen[i].ToLower();
CalcuDenpend(currDep, abName);
//UnityEngine.Object obj = ab.LoadAsset(currDep, typeof(UnityEngine.Object));
//if (obj != null)
//{
// Debugger.Log("--- obj type:{0}", GetObjectType(obj));
//}
}
}
//todo: 待加入 类型
void CalcuDenpend(string depName, string abName)
{
if (depName.EndsWith(".cs"))
return;
if (!mAssetGenMap.ContainsKey(depName)) //不存在这个ab,记录一下
{
if (!mRedAssetMap.ContainsKey(depName))
{
CRedAsset ra = new CRedAsset();
ra.mName = depName;
ra.mType = "我了个去";
mRedAssetMap.Add(depName, ra);
ra.mUsers.Add(abName);
}
else
{
CRedAsset ra = mRedAssetMap[depName];
ra.mUsers.Add(abName);
}
}
}
// mRedAssetMap 中 CRedAsset 的 mUsers 只有一个的,视为不冗余的资源,直接打到了该 ab 中
void CullNotRed()
{
List<string> keys = new List<string>();
foreach (var item in mRedAssetMap)
{
if (item.Value.mUsers.Count == 1)
keys.Add(item.Key);
}
foreach (var value in keys)
mRedAssetMap.Remove(value);
}
List<string> GetAllFiles(List<string> files, string folder, string pattern)
{
folder = folder.Replace("\\", "/");
System.IO.DirectoryInfo dir = new System.IO.DirectoryInfo(folder);
foreach (var file in dir.GetFiles(pattern))
{
files.Add((System.IO.Path.Combine(folder, file.Name).Replace("\\", "/")).ToLower());
}
foreach (var sub in dir.GetDirectories())
{
files = GetAllFiles(files, System.IO.Path.Combine(folder, sub.Name), pattern);
}
return files;
}
string GetObjectType(UnityEngine.Object obj)
{
string longType = obj.GetType().ToString();
string[] longTypeArr = longType.Split('.');
return longTypeArr[longTypeArr.Length - 1];
}
private string AppendSlash(string path)
{
if (path == null || path == "")
return "";
int idx = path.LastIndexOf('/');
if (idx == -1)
return path + "/";
if (idx == path.Length - 1)
return path;
return path + "/";
}
string Export()
{
if (mRedAssetMap.Count == 0)
return "未检查到有资源冗余";
List<CRedAsset> raList = mRedAssetMap.Values.ToList<CRedAsset>();
string currTime = System.DateTime.Now.ToString("yyyyMMdd_HHmmss");
string path = string.Format("{0}/{1}_{2}.md", kResultPath, "ABRedundency", currTime);
if (!System.IO.Directory.Exists(kResultPath))
System.IO.Directory.CreateDirectory(kResultPath);
using (FileStream fs = File.Create(path))
{
StringBuilder sb = new StringBuilder();
sb.Append(string.Format("## 资源总量:{0},冗余总量:{1},检测时间:{2},耗时:{3:F2}s\r\n---\r\n", mAllABFiles.Count, raList.Count, currTime, mCheckTime));
sb.Append("| 排序 | 资源名称 | 资源类型 | AB文件数量 | AB文件名 |\r\n");
sb.Append("|---|---|:---:|:---:|---|\r\n");
CRedAsset ra = null;
StringBuilder abNames = new StringBuilder();
raList.Sort((CRedAsset ra1, CRedAsset ra2) =>
{//排序优先级: ab文件个数 -> 名字
int ret = ra2.mUsers.Count.CompareTo(ra1.mUsers.Count);
if (ret == 0)
ret = ra1.mName.CompareTo(ra2.mName);
return ret;
});
for (int i = 0; i < raList.Count; i++)
{
ra = raList[i];
foreach (var abName in ra.mUsers)
abNames.Append(string.Format("**{0}**, ", abName));
//abNames.Append(string.Format("{0}<br>", abName)); //另一种使用换行
sb.Append(string.Format("| {0} | **{1}** | {2} | {3} | {4} |\r\n"
, i + 1, ra.mName, ra.mType, ra.mUsers.Count, abNames.ToString()));
abNames.Length = 0;
}
byte[] info = new UTF8Encoding(true).GetBytes(sb.ToString());
fs.Write(info, 0, info.Length);
}
return "有冗余,导出结果:" + path.Replace("\\", "/");
}
//---------------- gui begin ------------
[ ]
static void Init()
{
EditorWindow.GetWindow(typeof(ABRedundancyChecker), false, "AB资源冗余检测");
}
void Awake()
{
mInstance = this;
}
string mSelPath = "";
public void OnGUI()
{
EditorGUILayout.Space();
EditorGUILayout.LabelField("路径:", EditorStyles.boldLabel);
EditorGUILayout.Space();
GUILayout.Label(mSelPath);
EditorGUILayout.Space();
if (GUILayout.Button("选择主AB文件"))
mSelPath = EditorUtility.OpenFilePanelWithFilters("选择主AB文件", mSelPath, null);
EditorGUILayout.Space();
//mIsForQ6 = EditorGUILayout.Toggle("是否Q6(Q6有解密机制)", mIsForQ6);
EditorGUILayout.Space();
if (GUILayout.Button("开始检测"))
{
if (mSelPath == "")
EditorUtility.DisplayDialog("错误", "请先 选择主AB文件", "Ok");
else
{
mSelPath = mSelPath.Replace("\\", "/");
string[] arr = mSelPath.Split('/');
mMainAb = arr[arr.Length - 1];
mABPath = mSelPath.Substring(0, mSelPath.LastIndexOf('/'));
StartCheck();
}
}
}
}