概览:
优化前客户端包体大概271M优化后包体大小大概是166M(我本地广告插件Applovin打包会有问题,这个去掉了不算在内),大概可以在之前的基础上优化保底100M左右
其中ab包优化了82M(203->121)
其他感悟:
笔者之前在广州,也是从事中东游戏开发,游戏偏休闲。当时所有的资源包括ugc几乎都是从远端下载的(其实配置文件也是走的远端,也有checksum,我们现在配置是走的https,本地缓存一个版本,传给远端如果返回的字节数不够就是版本一致直接用缓存的否则服务器会回传配置二进制文件),他们给每一个文件都搞了一个checksum文件,每次请求并不是直接请求资源的url而是请求对应的checksum文件获取md5,版本信息进行比对,确实需要下载再由客户端去请求真实的资源url地址。现在这家公司并没有类似的做法,于是我在2025年上半年利用平常周末时间开发了一个
方便管理资源的工具,可以将资源移除,然后需要的时候移回来。很多资源并不是一直需要待在包体里面的,可能一个活动一年才出一次,那么活动结束后下个版本完全可以移除它
1.纹理压缩(重点)
1.1普通ui的image压缩方案(ASTC8X8)

cs
using UnityEditor;
using UnityEngine;
using System.Collections.Generic;
public class TextureASTCViewer : EditorWindow
{
enum Tab
{
ASTC8x8,
NonASTC8x8
}
Tab currentTab;
Vector2 scroll;
List<string> astcTextures = new();
List<string> nonAstcTextures = new();
// 可配置过滤
List<string> ignoreKeywords = new()
{
"Editor/",
"3rdParty/",
"Resources/",
"RTLTMPro/",
"Runtime/",
"Reporter/",
"spine/", // spine不去处理 避免扯上技术债务
"icon.png" // 游戏图标 忽略
};
string newFilter = "";
[MenuItem("Tools/sprite处理/ASTC Viewer")]
static void Open()
{
GetWindow<TextureASTCViewer>("ASTC Viewer");
}
void OnEnable()
{
ScanTextures();
}
void OnGUI()
{
DrawToolbar();
DrawFilterSetting();
DrawList();
GUILayout.Space(10);
if (GUILayout.Button("重新扫描"))
{
ScanTextures();
}
if (GUILayout.Button("批量设置纹理压缩格式"))
{
AssetDatabase.StartAssetEditing();
try
{
foreach (var item in nonAstcTextures)
{
EasyUseEditorTool.SetTextureFormat(item);
}
}
finally
{
AssetDatabase.StopAssetEditing();
AssetDatabase.Refresh();
EditorUtility.ClearProgressBar();
}
}
}
void DrawToolbar()
{
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
if (GUILayout.Toggle(currentTab == Tab.ASTC8x8, "ASTC 8x8", EditorStyles.toolbarButton))
{
currentTab = Tab.ASTC8x8;
}
if (GUILayout.Toggle(currentTab == Tab.NonASTC8x8, "非 ASTC 8x8", EditorStyles.toolbarButton))
{
currentTab = Tab.NonASTC8x8;
}
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
}
void DrawFilterSetting()
{
EditorGUILayout.LabelField("路径过滤", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
newFilter = EditorGUILayout.TextField("新增过滤关键字", newFilter);
if (GUILayout.Button("添加", GUILayout.Width(60)))
{
if (!string.IsNullOrEmpty(newFilter) && !ignoreKeywords.Contains(newFilter))
{
ignoreKeywords.Add(newFilter);
newFilter = "";
}
}
EditorGUILayout.EndHorizontal();
for (int i = 0; i < ignoreKeywords.Count; i++)
{
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField(ignoreKeywords[i]);
if (GUILayout.Button("删除", GUILayout.Width(60)))
{
ignoreKeywords.RemoveAt(i);
break;
}
EditorGUILayout.EndHorizontal();
}
GUILayout.Space(10);
}
void DrawList()
{
scroll = EditorGUILayout.BeginScrollView(scroll);
var list = currentTab == Tab.ASTC8x8 ? astcTextures : nonAstcTextures;
foreach (var path in list)
{
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField(path);
if (GUILayout.Button("Ping", GUILayout.Width(60)))
{
var obj = AssetDatabase.LoadAssetAtPath<Object>(path);
EditorGUIUtility.PingObject(obj);
Selection.activeObject = obj;
}
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndScrollView();
}
void ScanTextures()
{
astcTextures.Clear();
nonAstcTextures.Clear();
string[] guids = AssetDatabase.FindAssets("t:Texture",new string[] { "Assets/Art"});
int count = guids.Length;
for (int i = 0; i < count; i++)
{
string path = AssetDatabase.GUIDToAssetPath(guids[i]);
EditorUtility.DisplayProgressBar("扫描Texture", path, (float)i / count);
if (!IsPathValid(path))
continue;
var importer = AssetImporter.GetAtPath(path) as TextureImporter;
if (importer == null)
continue;
var setting = importer.GetPlatformTextureSettings("Android");
if (setting.format == TextureImporterFormat.ASTC_8x8)
{
astcTextures.Add(path);
}
else
{
nonAstcTextures.Add(path);
}
}
EditorUtility.ClearProgressBar();
Repaint();
}
bool IsPathValid(string path)
{
// 非Assets目录过滤
if (!path.StartsWith("Assets"))
return false;
foreach (var keyword in ignoreKeywords)
{
if (path.Contains(keyword) || path.Contains(keyword.ToLower()))
return false;
}
return true;
}
}
工具函数
2.
cs
//注意ios和android都要override并且设置对应的压缩格式 ,不override是不会生效的
public static class EasyUseEditorTool
{
public static bool SetTextureFormat(string assetPath,TextureImporterFormat defaultFormat= TextureImporterFormat.ASTC_8x8)
{
assetPath = assetPath.ToUnityPath();
AssetImporter assetImporter = AssetImporter.GetAtPath(assetPath);
if (Application.isPlaying) return false;
if (!assetPath.EndsWith(".png") && !assetPath.EndsWith(".jpg") && !assetPath.EndsWith(".tga"))
{
return false;
}
if (!assetPath.StartsWith("Assets/Art"))
{
return false;
}
bool needImport = false;
TextureImporter textureImporter = (TextureImporter)assetImporter;
//有一个问题就是textureImporter的纹理类型 可能做ui的话多数会是gui 但是不能排除其他的 这里不方便直接指定
if (textureImporter.isReadable)
{
Debug.LogError("警告!isReadable == true,已自动修正" + assetPath);
textureImporter.isReadable = false;
needImport = true;
}
var androidSetting = textureImporter.GetPlatformTextureSettings("android");
if (!androidSetting.overridden)
{
androidSetting.overridden = true;
needImport = true;
}
if (androidSetting.format != defaultFormat)
{
androidSetting.format = defaultFormat;
androidSetting.overridden = true;
needImport = true;
}
var iosSetting = textureImporter.GetPlatformTextureSettings("ios");
if (!iosSetting.overridden)
{
iosSetting.overridden = true;
needImport = true;
}
if (iosSetting.format != defaultFormat)
{
iosSetting.format = defaultFormat;
iosSetting.overridden = true;
needImport = true;
}
if (needImport)
{
textureImporter.SetPlatformTextureSettings(androidSetting);
if(iosSetting != null)
{
textureImporter.SetPlatformTextureSettings(iosSetting);
}
textureImporter.SaveAndReimport();
return true;
}
return false;
}
}
1.2.spine纹理压缩格式6X6
因为spine美术特别敏感所以我和普通ui区分开了做了单独的工具单独处理spine纹理压缩,对于之前已经设置了压缩格式的我们不做处理,避免影响美术效果,这里采用ASTC6x6,这个档位的压缩格式不会导致图片压缩变形,质量有保证,压缩效率也非常高
要注意spine多数情况情况下不要8x8,这会导致变糊,效果还是很明显的,记得使用6x6

cs
using UnityEditor;
using UnityEngine;
using System.Collections.Generic;
using System.Linq;
using System.IO;
public class SpineTextureCompress : EditorWindow
{
enum Tab
{
ASTC6x6,
NonASTC6x6
}
Tab currentTab;
Vector2 scroll;
List<string> astcTextures = new();
List<string> nonAstcTextures = new();
// 可配置过滤
List<string> ignoreKeywords = new()
{
"Editor/",
"3rdParty/",
"Resources/",
"RTLTMPro/",
"Runtime/",
"Reporter/",
"icon.png" // 游戏图标 忽略
};
string newFilter = "";
[MenuItem("Tools/spine纹理压缩处理")]
static void Open()
{
GetWindow<SpineTextureCompress>("Spine Texture Viewer");
}
void OnEnable()
{
ScanTextures();
}
void OnGUI()
{
DrawToolbar();
DrawFilterSetting();
DrawList();
GUILayout.Space(10);
if (GUILayout.Button("重新扫描"))
{
ScanTextures();
}
if (GUILayout.Button("批量设置纹理压缩格式"))
{
AssetDatabase.StartAssetEditing();
try
{
foreach (var item in nonAstcTextures)
{
EasyUseEditorTool.SetTextureFormat(item,TextureImporterFormat.ASTC_6x6);
}
}
finally
{
AssetDatabase.StopAssetEditing();
AssetDatabase.Refresh();
EditorUtility.ClearProgressBar();
}
}
}
void DrawToolbar()
{
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
if (GUILayout.Toggle(currentTab == Tab.ASTC6x6, "ASTC 6x6", EditorStyles.toolbarButton))
{
currentTab = Tab.ASTC6x6;
}
if (GUILayout.Toggle(currentTab == Tab.NonASTC6x6, "非 ASTC 6x6", EditorStyles.toolbarButton))
{
currentTab = Tab.NonASTC6x6;
}
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
}
void DrawFilterSetting()
{
EditorGUILayout.LabelField("路径过滤", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
newFilter = EditorGUILayout.TextField("新增过滤关键字", newFilter);
if (GUILayout.Button("添加", GUILayout.Width(60)))
{
if (!string.IsNullOrEmpty(newFilter) && !ignoreKeywords.Contains(newFilter))
{
ignoreKeywords.Add(newFilter);
newFilter = "";
}
}
EditorGUILayout.EndHorizontal();
for (int i = 0; i < ignoreKeywords.Count; i++)
{
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField(ignoreKeywords[i]);
if (GUILayout.Button("删除", GUILayout.Width(60)))
{
ignoreKeywords.RemoveAt(i);
break;
}
EditorGUILayout.EndHorizontal();
}
GUILayout.Space(10);
}
void DrawList()
{
scroll = EditorGUILayout.BeginScrollView(scroll);
var list = currentTab == Tab.ASTC6x6 ? astcTextures : nonAstcTextures;
foreach (var path in list)
{
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField(path);
if (GUILayout.Button("Ping", GUILayout.Width(60)))
{
var obj = AssetDatabase.LoadAssetAtPath<Object>(path);
EditorGUIUtility.PingObject(obj);
Selection.activeObject = obj;
}
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndScrollView();
}
void ScanTextures()
{
astcTextures.Clear();
nonAstcTextures.Clear();
var allSpines = AssetDatabase.FindAssets("t:SkeletonDataAsset", new[] { "Assets/Art" });
var texturePaths = allSpines
.Select(AssetDatabase.GUIDToAssetPath)
.Select(skeletonPath =>
{
var basePath = skeletonPath.Replace("_SkeletonData.asset", "");
return new[] { ".png", ".jpg", ".tga" }
.Select(ext => basePath + ext)
.FirstOrDefault(File.Exists);
})
.Where(path => !string.IsNullOrEmpty(path))
.ToList();
int count = texturePaths.Count;
for (int i = 0; i < count; i++)
{
var path = texturePaths[i];
EditorUtility.DisplayProgressBar("扫描Texture", path, (float)i / count);
if (!IsPathValid(path))
continue;
var importer = AssetImporter.GetAtPath(path) as TextureImporter;
if (importer == null)
continue;
var setting = importer.GetPlatformTextureSettings("Android");
if ( setting.overridden)
{
astcTextures.Add(path);
}
else
{
nonAstcTextures.Add(path);
}
}
EditorUtility.ClearProgressBar();
Repaint();
}
bool IsPathValid(string path)
{
// 非Assets目录过滤
if (!path.StartsWith("Assets"))
return false;
foreach (var keyword in ignoreKeywords)
{
if (path.Contains(keyword) || path.Contains(keyword.ToLower()))
return false;
}
return true;
}
}
3.优化il2cpp.so大小
besthttp库太大了(我们项目的特例每个项目可能不太一样,一些无效代码可以去掉,或者你只是用到了一个库很小的功能,那完全可以自己写),导致lib2cpp.so有点大20.5M了,优化后可以到17.5M。注意这里的优化是双份收益,因为你上google play你还是尽量要兼容32位,所以总的包体比较大,尽管google play的分发机制会让你只下载64位(我们的手机多64位)的,并且它还会对包体进行进一步压缩大概30%,我认为这个优化还是有必要的。
这里就是去掉besthttp库改用c#原生网络接口去实现,这主要影响聊天语音,玩家更换头像,facebook反馈等涉及到http的上传下载等操作
某些活动1年才会有一次,但是包确实实实在在躺在app中,需要剔除工具剔除,并且可以用git管理,然后可以一键导回原来的游戏。这种比较适用于随着开发进行,资源越来越爆炸,必须进行裁剪的情况
点击资源安全回收以及清理无引用资源后相关文件会被转移到工程外
并且这些文件保留了完整的相对树目录可以轻易完成资源回滚到工程

这个的实现比较复杂,不是一句话两句话能搞定的,暂时不贴代码,贴不下。不过这个我有放到github,改天把链接贴过来。
为什么有这个工具还是有强迫症,随着项目的进行工程越来越大,各种image各种spine资源,各种重复的资源,因此需要一个剔除重复资源的工具,我们只保留一个,并且保留的那一个会放到common文件夹中,common的话直接标记abname,如果你是yooAssets项目直接标记这个文件夹是依赖打包即可,注意不要搞静态,线上翻过车。静态除非你百分百笃定比如就是字体,然后我们一个版本周期内不改(1-2周?)执行顺序是先清理无任何引用资源,然后是清理重复资源,其中这一步就会做资源的重定向,它会replace资源meta中的guid 替换guid之后 引用关系也就对上了,
这个是这个工具的精髓,尤其对于我们自己写ab包打包,不使用任何第三方的工具,纯手鲁的话这个工具就相当有效了。简而言之我们就是为了避免资源冗余打包。当然你使用yooAssets它确实非常不错,但是我们可以控制的也就少了,ok我去看yooAssets的代码了。
4.关闭ab包的类型树(推荐)
这里以yooAssets项目为例,自己的项目也可以关闭,这个东西一点用没有 ,当然你要分析ab包的问题出在哪里那还是不关闭。关闭这个带来的收益蛮大的100多M的游戏能省6-8.2M的空间
