unity-AssetBundle资源冗余检测

  • 最近搞了下 unity 的 AssetBundle(以下简称 AB) 资源冗余检测,并导出一个md文件列出冗余的资源及其被打入进哪几个AB中,方便排除冗余

思路

  • 科普下AB资源冗余,当然这里并不会详细说明,只会放个链接 点我 (需要科学上网)
  • 可以搜到大部分的文章都是在说一个 侑虎科技 的第三方检测平台,我也去上面检测了下正确检测出来,据说是免费几次后就需要付费。但是资源不加密就上传上去貌似不是很靠谱,资源大了上传也麻烦。所以就自己实现了一个检测。
    • 检测的思路也很简单
      1. 先用主AB生产一个所有 资源及所在AB 的一个 映射表
      2. 递归遍历打出来的包下的所有的AB,通过 AssetDatabase.GetDependencies 获取到 AB中资源名字在 AssetDatabase 中所有的依赖。(此时要求工程下Asset下有正常的资源)
      3. 遍历所有依赖,是否在 映射表 中,如果 不存在超过两次,相同 资源A 被打进了两个AB中,而不是 资源A 打成一个AB,被其他AB依赖进去。
      4. 最后会收集到这些 不存在超过两次 的资源名及被打进去的AB文件名,导出到一个md文件中,使用md编辑器查看(这里推荐个md客户端叫 Haroopad,平常都用这个写md)

源码 及 使用

  • 鉴于源码就一个cs文件,就不上传到git,直接这里贴了,同时也会上传几个测试的 源资源(test_res.rar)、打包出的 有冗余(ABoutput.rar)、无冗余(ABoutput_red.rar) 的资源

  • 使用:

    1. 分别解压出来,test_res目录放到工程Asset目录下(AssetDatabase才能找到资源,获取依赖),打包出的资源随意放(最好英文路径)

    2. 选择打包出的资源的主AB
      这里写图片描述

      这里写图片描述

    3. 开始检测,有冗余会导出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
    357
    using 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 ------------

    [MenuItem("AB冗余检测/AB检测")]
    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();
    }
    }
    }
    }