Unity中ab包压缩方案 LZMA 和LZ4

LZMA

1.LZMA采用流压缩方式(stream-based),压缩率会比LZ4更高,体现在包体更小,但是问题也很严重。LZMA只支持顺序读取,所以加载AB包时,需要将整个包解压,会造成卡顿和额外内存占用。这也是为什么在有些复杂UI上首次打开会造成卡顿。

2.加载AB包后将所有资源进行了缓存,导致了如果AB包资源利用率在短时间利于率不高的时候,造成了很高的内存浪费。

3.一套引用计数规则非常复杂,当资源过多的时候建立引用关系都是很费时的,其中的常驻包的设置逻辑也是非常具有不确定性。

LZ4优化

1.LZ4采用块压缩方式(chunk-based),块压缩的数据被分为大小相同的块,被分别压缩,虽然压缩率不及LZMA,但是读取效率比LZMA高非常多

2.LZ4压缩的AB包,使用LoadFromFile()或LoadFromStream()只会加载AB包的Header,相比于直接加载解压整块AB包,效率更进一步提高。另外一个很重要的点,由于可以只加载Header,因此AB包可以做到一旦加载到内存后就再不卸载,此时只需要管理从AB包中读取出来的资源的生命周期。

3.对于之前使用引用计数的优化,由于Unity原本资源管理就是使用引用计数去维护,这里再建立一套内部的引用计数,不仅多余而且很浪费CPU资源,而且效果不一定很好。这个时候我们可以建立一套弱引用管理体系,通过弱引用去持有资源,在触发Resource.UnloadUnusedAssets()再去清除弱引用失效的对象。

将LZMA的压缩方式修改为LZ4

将打包参数添加BuildAssetBundleOptions.ChunkBasedCompression即可

BuildAssetBundleOptions buildOption = BuildAssetBundleOptions.IgnoreTypeTreeChanges | BuildAssetBundleOptions.DeterministicAssetBundle | BuildAssetBundleOptions.ChunkBasedCompression; LZ4读取AB包 //读取ab包 AssetBundle.LoadFromFileAsync(url) 这里需要注意一点的就是,LoadFromFileAsync与WWW或UnityWebRequest区别在于是在windows下不需要file://前缀


原文链接:https://blog.csdn.net/qq_38721111/article/details/129184791

谈一谈Unity 的 Scriptable Build Pipeline

Unity的Scriptable Build Pipeline (SBP) 提供了一种更灵活的构建方式,以package 的形式提供,将一些常用的构建类和接口从C++层移动到了C#层。,与传统的构建接口不同,SBP在构建时并不会生成manifest,而是会在library下生成缓存,实际首次构建时长会比buildpipeline慢一些,但如果再次调用构建接口,就会使用缓存来增量构建,减少构建时长。除此之外,SBP在构建完成后会在AB包目录输出构建步骤的耗时的详细信息和耗时日志,可以用Trace Event Profiling Tool打开, 方便profiler不同的构建步骤。Unity的Scriptable Build Pipeline (SBP) 提供了一种更灵活的构建方式,以package 的形式提供,将一些常用的构建类和接口从C++层移动到了C#层。,与传统的构建接口不同,SBP在构建时并不会生成manifest,而是会在library下生成缓存,实际首次构建时长会比buildpipeline慢一些,但如果再次调用构建接口,就会使用缓存来增量构建,减少构建时长。除此之外,SBP在构建完成后会在AB包目录输出构建步骤的耗时的详细信息和耗时日志,可以用Trace Event Profiling Tool打开, 方便profiler不同的构建步骤。


以下是个人总结的一些SBP的优点与缺点。

优点:

良好的设计接口,能更灵活的控制构建过程,例如深度定制AB包构建内容,指定单个Ab包压缩格式、依赖查找回调等。 将构建任务粒度拆分更细,例如压缩、打图集、编译脚本,文件系列化等。为构建并行化为了准备 使用BuildCache,加快构建速度,不同构建机之间可以共享同一份BuildCache 构建更细粒度的步骤profiler

缺点:

Task的设计看起来很美好,但大部份任务并不支持并行化,自定义task的可行性比较低 增量构建不完善,依然有bug,使用buildcache后bug会传播到其它构建线 过度的设计,看似提供了更灵活的接口,实则很难正确的实现这些接口,比如看似可以通过IBundleBuildContent的BundleLayout来控制每个ab包的内容,但由于unity的依赖关系很难从上层应用通过Unity提供的接口计算正确,所以从上层来控制每个ab包的内容基本上不可能 单个ab包的压缩格式通过回调来指定的做法比较不合理,这样做需要用字典来存储每个ab包压缩格式,增加了额外内存和构建时长,同时这样的代码写起来也比较麻烦。一种比较简单的做法是在AssetBundleBuild中增加一个压缩格式参数 Build Cache在Library下占用硬盘空间较大

参考链接

trace-event-profiling-tool

unity3d冷启动黑屏优化

问题分析

在 Unity 开屏的开发过程中,通常在 Unity 中编写是 Unity 的开屏页,在 Android 端运行时会额外添加unity_static_splash, 来保证Android 端展示时不是 黑屏,但是 unity_static_splash 只能使用 图片资源,如果涉及到需要根据屏幕尺寸进行动态适配的时候,一张图片就不能满足要求。 同时如果 Unity 开屏和unity_static_splash 展示相同内容的话,由于适配方式不一致,会出现界面抖动。

Static Splash Image 原理

在 Unity 中通常可以设置 Static Splash Image 来屏蔽黑屏,那么Static Splash Image 是如何实现的在 Android 端展示并且消失的? 将 Unity 项目导出 Android 项目之后,可以在drawable 文件夹中找到对应的图片

Android 端启动 Unity 的 Activity 是UnityPlayerActivity,分析源码过程

//UnityPlayerActivity  
@Override protected void onCreate(Bundle savedInstanceState)
    {
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        super.onCreate(savedInstanceState);

        String cmdLine = updateUnityCommandLineArguments(getIntent().getStringExtra("unity"));
        getIntent().putExtra("unity", cmdLine);

        mUnityPlayer = new UnityPlayer(this);
        setContentView(mUnityPlayer);
        mUnityPlayer.requestFocus();
    }

UnityPlayer 是一个 FrameLayout, 具体实现

// UnityPlayer
public UnityPlayer(Context var1) {
    super(var1);
    if (var1 instanceof Activity) {
      currentActivity = (Activity)var1;
      this.c = currentActivity.getRequestedOrientation();
    }

    a(currentActivity);
    this.q = var1;
    if (currentActivity != null && this.k()) {
      this.m = new l(this.q, com.unity3d.player.l.a.a()[this.getSplashMode()]);
      this.addView(this.m);
    }
    ....
    ...
}

分析代码,new l 添加到了布局里面,猜测应该是这个类

//l  
public l(Context var1, int var2) {
    super(var1);
    this.a = var2;
    this.b = this.getResources().getIdentifier("unity_static_splash", "drawable", this.getContext().getPackageName());
    if (this.b != 0) {
      this.forceLayout();
    }

  }

可以看到L 这个类也是个 View, 读取unity_static_splash,然后添加到父布局里面。 这样就了解了unity_static_splash 的原理 ​

黑屏优化

出现黑屏的前提是不能使用unity_static_splash,可能是因为

需要根据屏幕适配调整布局 播放视频或者其他资源

添加自定义布局展示开屏

针对以上的情况,我们可以手动编写布局,然后同样添加到UnityPlayerActivityd的布局中实现开屏的展示 java复制代码//MyUnityPlayerActivity
@Override protected void onCreate(Bundle bundle) { super.onCreate(bundle); ViewGroup layoutView = (FrameLayout) View.inflate(this, R.layout.activity_splash, null); addContentView(layoutView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); }

添加自定义布局展示任意内容。同时需要将 Unity中 Static Splash Image 设置为空

关于添加布局消失

在添加了自定义展示的开屏之后 ,需要处理的就是 什么时机让自定义布局消失。两种方案

利用 AndroidJavaObject, 在 Unity 中调用 Android 端的函数,实现关闭布局。 利用延时机制,设置 1s 或者 0.5 s 之后关闭

以上两种方案都会有一些问题

方案一导致 Unity 开屏内容会有一部分遮挡 Unity 开屏展示时机不确定,延时可能短或者长,短就会出现黑屏(因为没有unity_static_splash),长就会遮挡 Unity 开屏。

分析unity_static_splash 的消失时机,找到合适的关闭自定义布局的时机 java复制代码//MyUnityPlayerActivity
private void a() { this.a(new Runnable() { public final void run() { UnityPlayer.this.removeView(UnityPlayer.this.m); UnityPlayer.f(UnityPlayer.this); } }); }

UnityPlayer 系统通过移除布局来实现关闭unity_static_splash 界面,那么可以监听父布局的子View 移除,如果是 L ,那么就代表unity_static_splash 被关闭,同时关闭我们自己的自定义布局就可以了

关闭自定义布局

java复制代码//MyUnityPlayerActivity @Override protected void onCreate(Bundle bundle) { super.onCreate(bundle); ... ... mUnityPlayer.setOnHierarchyChangeListener(new ViewGroup.OnHierarchyChangeListener() { @Override public void onChildViewAdded(View parent, View child) {

  }

  @Override public void onChildViewRemoved(View parent, View child) {
      //判断是l,关闭自定义布局
    if (child instanceof com.unity3d.player.l) {
      removeSplashLayout();
    }
  }
});

添加布局监听,判断类型,关闭自定义布局

Unity项目优化之数据集合优化

通过详细的理解Array、List、和Dictionaries让你的游戏运行速度快十倍

这篇文章的主要目标是:过详细的理解Array、List、和Dictionaries,让你的游戏运行速度快十倍。

我应该使用Array吗?我应该使用List吗?噢 ,等等,或者使用Dictionaries? 为什么我的循环花了那么长时间呢? ? 在我代码中看不到任何的错误,那么为什么我的代码执行那么慢? 为什么查找我想要的对象话费那么长时间? 我看不到任何的GC(垃圾回收器)处理,然而我的游戏为什么那么的延迟?

“这些是我们在开发一个游戏的时候经常遇到的一些常见问题。” 但正是这些常见问题最让游戏开发者苦恼了!! 几毫秒的延迟就可以让游戏开发者失去理智!! 最终,我们总是推卸责任喊道:“这个是Unity引擎的错误,我的代码是完美的!!” 那好吧,这并非总是如此,只是有的时候我们不正确使用数据结构中的集合造成的,且心里咒骂着:“集合是如此的慢!” 在应用程序中,我们一般通过以下两种方式去管理相邻对象组: 1. 通过创建对象数组(Array) 2. 通过创建对象的集合 我们应该记得每一种集合的具体用法,以及它的优点和缺点,并且知道在什么情况下使用它是最佳的。

在文章中,我将列举在Unity中所有常用的数组,这样有利于你更好的理解集合。

什么是集合?

集合是特殊的类用于数据的存储和检索,集合类通常是用来为元素动态的分配内存,并且通过下标索引来访问列表里的每一个元素等等。 这些类创建Object类的对象的集合,在C#中所有数据类型的基类是Object类。集合可以根据应用程序的请求动态的扩展和缩减,这就是集合的主要优势。 集合使得内存管理和数据管理的过程变得相当简单。 那么,在Unity中常用的集合有那几个呢? 在Unity中Dictionary(字典) 和 List(列表)是最常用的集合,让我给初学者对于Dictionary(字典) 和 List(列表)的一些基本概念。如果知道的朋友可以跳过。

  1. List C#List< T >类代表一个强大的List类型(其实就是泛型List类)可以通过索引访问的列表对象,它可以存储没有指定类型的对象集合。 它和其他集合一样都有以下功能:添加(Add),插入(Insert),移除(Remove),查找(Search)等等。 List的索引表示方式和Array一样,然后它的主要优势是动态的指定容器的大小。 例如,我们可以这样定义一个Object的List<>: List<GameObject> myListOfGameObjects = new List<GameObject>();

  2. Dictionary Dictionary实际上使一个哈希表类型的替代品 Dictionary代表一个键值对 例如,如果5代表Red,10代表Green,我们便在Dictionary中通过5键(Key)找到Red这个值(Value)。 因此,我们如果想要找到Red这个值(Value),只要记住5这个键(Key)即可 那么,Dictionary是怎么查找数据的呢? 例如,我们可以这样定义一个Dictionary对象: //Dictionary<int, String>: //在这个例子中,“int”是键,“String”是值 Dictionary<int,String> myDictionary = new Dictionary<int, String>();

现在,这篇文章的主要目标是关于优化使用集合,而不是学习集合,因此我们将忽略集合的学习。 但是,如果你想要详细的学习它,你可以下面的链接,这将会帮你更详细的学习集合的概念:

https://link.juejin.cn/?target=https%3A%2F%2Fmsdn.microsoft.com%2Fen-us%2Flibrary%2Fybcx56wz.aspx https://www.dotnetperls.com/ https://link.juejin.cn/?target=http%3A%2F%2Fwww.tutorialspoint.com%2Fcsharp%2Fcsharp_collections.htm

集合是怎么影响游戏的呢?

让我们一起看一个例子,并且理解集合石怎么影响我们的游戏的。 1.在Unity中依照下面方式来设置场景 css复制代码a) 创建一个空的游戏物体(Empty Game),并且更改名字(你随意,在这里我命名为Test)

2.创建一个脚本,并且命名为你喜欢的名字 css复制代码a) 在这里我把命名为GenericCollectionsTest.cs

b) 我使用的是C#作为我的脚本语言,你也可以使用Javascript,如果你愿意的话。

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;

public class GenericCollectionsTest : MonoBehaviour
{
    #region PUBLIC_DECLARATIONS
    public int numberOfIterations = 10000000;
    #endregion
    #region PRIVATE_DECLARATIONS
    private Stopwatch stopWatch;
    private List<int> intList;
    private Dictionary<int, int> intDictionary;
    private int[] intArray;
    #endregion
    #region UNITY_CALLBACKS

    void Start()
    {
        stopWatch = new Stopwatch();
        intArray = new int[numberOfIterations];
        intList = new List<int>();
        intDictionary = new Dictionary<int, int>();
        AddFakeValuesInArray(numberOfIterations);
        AddFakeValuesInList(numberOfIterations);
        AddFakeValuesInDictionay(numberOfIterations);
    } 

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            PerformTest();
        }

        if (Input.GetKeyDown(KeyCode.S))
        {
            SearchInList(111);
            SearchInDictionary(numberOfIterations - 1);
            UnityEngine.Debug.Log("SearchComplete");
        }
    }
    #endregion 

    #region PRIVATE_METHODS 
    private void AddFakeValuesInArray(int iterations)
    {
        for (int i = 0; i < iterations; i++)
        {
            intArray[i] = Random.Range(0, 100);
        }
    } 

    private void AddFakeValuesInList(int iterations)
    {
        for (int i = 0; i < iterations; i++)
        {
            intList.Add(Random.Range(0, 100));
        }
        intList[iterations - 1] = 111;
    } 

    private void AddFakeValuesInDictionay(int iterations)
    {
        for (int i = 0; i < iterations; i++)
        {
            intDictionary.Add(i, Random.Range(0, 100));
        }
        intDictionary[iterations - 1] = 111;
    } 

    private void SearchInList(int value)
    {
        #region FIND_IN_LIST
        stopWatch.Start();
        int index = intList.FindIndex(item => item == value);
        stopWatch.Stop();
        UnityEngine.Debug.Log("Index " + index);
        UnityEngine.Debug.Log("Time Taken to Find in List  "+stopWatch.ElapsedMilliseconds +" ms");
        stopWatch.Reset();
        #endregion

        #region CHECK_IF_CONTAINS_VALUE_IN_LIST
        stopWatch.Start();
        bool containsValue = intList.Contains(value);
        stopWatch.Stop();
        UnityEngine.Debug.Log(containsValue);
        UnityEngine.Debug.Log("Time Taken To Check in List "+stopWatch.ElapsedMilliseconds +" ms");
        stopWatch.Reset();
        #endregion
    } 

    private void SearchInDictionary(int key)
    {
        #region FIND_IN_DICTIONARY_USING_REQUIRED_KEY
        stopWatch.Start();
        int value = intDictionary[key];
        stopWatch.Stop();
        UnityEngine.Debug.Log(("Time Taken to Find in Dictionary   " + stopWatch.ElapsedMilliseconds + " ms"));
        stopWatch.Reset();
        #endregion 

        #region CHECK_IF_DICTIONARY_CONTAINS_VALUE
        stopWatch.Start();
        bool containsKey = intDictionary.ContainsKey(key);
        stopWatch.Stop();
        UnityEngine.Debug.Log(containsKey);
        UnityEngine.Debug.Log("Time taken to check if it contains key in Dictionary" + stopWatch.ElapsedMilliseconds + " ms");
        stopWatch.Reset();
        #endregion
    } 

    private void PerformTest()
    {
        #region ARRAY_ITERATION
        stopWatch.Start();
        for (int i = 0; i < intArray.Length; i++)
        {

        }
        stopWatch.Stop();
        UnityEngine.Debug.Log("Time Taken By Array "+stopWatch.ElapsedMilliseconds + "ms");
        stopWatch.Reset();
        #endregion 

        #region LIST_ITERATION
        stopWatch.Start();
        for (int i = 0; i < intList.Count; i++)
        { 

        }
        stopWatch.Stop();
        UnityEngine.Debug.Log("Time Taken By List "+stopWatch.ElapsedMilliseconds + "ms");
        stopWatch.Reset();
        #endregion

        #region LIST_ITERATION_BY_FOREACH_LOOP
        stopWatch.Start();
        foreach (var item in intList)
        { 

        }
        stopWatch.Stop();
        UnityEngine.Debug.Log("Time Taken By List Using foreach  "+stopWatch.ElapsedMilliseconds + "ms");
        stopWatch.Reset();
        #endregion 

        #region DICTIONARY_ITERATIOn_LOOP
        stopWatch.Start();
        foreach (var key in intDictionary.Keys)
        {

        }
        stopWatch.Stop();
        UnityEngine.Debug.Log("Time Taken By Dictionary "+stopWatch.ElapsedMilliseconds + "ms");
        stopWatch.Reset();
        #endregion
    }
    #endregion
}

3.分解代码和理解代码 a) 让我们来分解这个代码,并且一步一步的理解代码 b) 在这里显示了一些数据,如下表所示:

intArray

    一个int类型的数组(Array)

intList

    一个int类型的列表(List)

intDictionary

    一个键值都是int类型的字典(Dictionary)

现在,让我们来看看Start()方法

void Start()

    {

        stopWatch = new Stopwatch();

        intArray = new int[numberOfIterations];

        intList = new List<int>();

        intDictionary = new Dictionary<int, int>();

        AddFakeValuesInArray(numberOfIterations);

        AddFakeValuesInList(numberOfIterations);

        AddFakeValuesInDictionay(numberOfIterations);

    }

在这里,我初始化了数组(Array),列表(List),字典(Dictionary),并且为他们添加了一些随机值。

e) 正如你在代码中看到的,我创建了一个Private(私有的)方法为那些集合添加一些随机数。

f) 在这里我也使用了Stopwatch对象用来时间和性能测试,并且也在Start()里进行了初始化

g) 如果你还不知道Stopwatch是怎么运作的,在往后学习之前,先去了解它,这样有利于你接下来的理解。

h) 请参考以下链接: https://www.dotnetperls.com/stopwatch 现在让我们看一看PerformTest()方法

j) 我把该方法分为了4块,这样更有利于理解:

ARRAY_ITERATION

    这里我们只遍历该数组

LIST_ITERATION

    这里我们只使用简单的for循环遍历列表

LIST_ITERATION_BY_FOREACH_LOOP

    这里我们使用foreach循环遍历列表

DICTIONARY_ITERATION_LOOP

    这里我们遍历字典

k) 正如你在Update()中看到,我们在按下“Space”键(空格键)时调用PerformTest()。

l) 现在,我们将执行项目去测试一番。

为了获得性能的确切数据,我们让每一个种类型的数据集合迭代10万次。 你的输出应该跟下面的图片一样:

那么,这就意味着Array(数组)是最好的吗?我们应该只使用Array(数组)就行了嘛?不,不是这样的。正如我们之前说的那样,我们要有计划的使用集合。

o) 只是我们要明确我们的需要,按自己的需求指定需用的集合类型。

P) 让我们一起去了解一些,我们应该在什么情况下使用什么样的集合类型。

情况1:在整个游戏中,对象的数量保持不变

· 在这种情况下使用List(列表)和Dictionary(字典)是不合适的,很显然对象的数量没有改变。然而使用一个集合为什么会给内存和CPU造成额外的消耗呢? ·在这里,Array(数组)的效率是List(列表)的两倍。

情况2:在游戏中对象的数量在不断的变化

·我们从上面中了解到Array(数组)不是动态分配的,显然,我在这种情况下应该使用List(列表)。因为对象在持续的改变,所以List(列表)比Dictionary要快很多。 ·List(列表)常用来管理对象池 ·List(列表)比Dictionary(字典)快将近8倍左右 ·使用foreach循环来遍历List比使用for循环多消耗将近 3倍的时间(这个在《关于Foreach你不知道的事儿》 中有详细说明)。 所以这意味着我们应该完全停止使用字典吗? 不是的,让我们通过下面的例子更好的理解它。在代码中有两个方法SearchInList() 和 SearchInDictionary()。 通过下面表格进行了解:

SearchInList()

    方法的第一部分是传递一个值给列表,然后在列表中去查找这个值,第二部分是判断这个列表是否存在该值,最后根据判断条件返回相应的布尔值

SeatchInDictionary()

    方法的第一部分是根据这个传入的键去找到这个键对应的值,第二部分通过使用ContainsKey()方法判断这个方法里是否有指定的键

让我们在一次运行项目进行测试,且在运行中按下“S”键后看输出日志的显示。 输出将是这样的:

结论

从上图中便能得知,使用Dictionary(字典)进行搜索几乎不消耗任何时间 因此,如果在整个游戏运行的时候需要不断的寻找一些对象时,明智的选择就是选择使用Dictionary(字典)。 接受它吧,你的游戏不能没有集合! 是的,这是正确的。我们只需要知道在什么情况下使用什么类型的集合。 结论很简单,有三个基本原则: 1.当一个对象的数量保持不变时和需要频繁的查找对象时不要使用List(列表)。 2.如果是动态的对象,且不需要频繁查找对象时,使用List(列表)是最佳的选择。 3.需要快速查找,并且对象的改变很小时,使用Dictionary(字典)是最佳的选择。 4.当一个对象的数量保持不变时,使用Array(数组)是最佳的选择(自己添加的)

在MacOS中的Shell脚本执行sed -i命令报错

MacOS sed -i命令报错

需要删除test.txt中的所有hello world字符串,命令如下, sed -i '/hello world/d' test.txt 但执行时会报错,报错信息如下: sed: 1: "test.txt": undefined label 'est.txt'


原因: Mac中用-i命令进行替换时,强制要求备份,-i要加一个备份文件路径的参数 sed -i 'copypath' '/hello world/d' test.txt 如果不想备份,路径填空就行 sed -i '' '/hello world/d' test.txt