0904学习 agile Posted on Sep 4 2023 面试 unity基础 ##装箱拆箱 --- - 装箱是将`值类型转换为引用类型`,详细点就是`值类型到Object类型或值类型所实现的任何接口类型的隐式转换` - 将引用类型转换为值类型,详细点就是`从object类型到值类型`或`从接口类型到实现该接口的值类型`的显示转换 --- ###装箱步骤 - 首先从托管堆中为新生成的引用对象`分配内存` - 然后将值类型的数据`拷贝`到刚刚分配的内存中 - `返回`托管堆中新分配对象的`地址`。这个地址就是一个指向对象的引用了 --- ###拆箱 - 首先获取托管堆中属于值类型那部分字段的地址 - 将引用对象中的值拷贝到位于线程堆栈上的值类型实例中 --- ###为什么要进行装箱和拆箱 - 方便参数的存储和传递,保证代码通用性。(调用一个含类型为 Object 的参数的方法,该 Object 可支持任意为型,可以传递值类型进来),(一个非泛型的容器,将元素类型定义为 Object,也可以存储值类型) - Struct 通过重载函数来避免拆箱、装箱 - 通过泛型来避免拆箱、装箱 - 通过继承统一的接口提前拆箱、装箱,避免多次重复拆箱、装箱 ###装箱时,变为引用对象,会多出一个方法表指针,这会有何用处呢? ```C# public interface IBox { void Change(int i); } public struct BoxA : IBox { public int BoxValue; public void Change(int i) { BoxValue = i; } } var boxStruct = new BoxA(); boxStruct.BoxValue = 10; boxStruct.Change(20); //20 boxStruct.ToString(); //valueType中实现了ToString方法,所以不需要装箱 boxStruct.GetType(); //装箱,GetType方法在Object类中 IBox ibox = boxStruct; //因为接口为引用类型,所以转成接口也会装箱 ibox.Change(39); //20,没有变成39 boxStruct.ToString(); //valueType中实现了ToString方法,所以不需要装箱 object o = ibox; // 在将o转型为IBox时,这里不会进行装箱,当然更不会拆箱, // 因为o已经是引用类型,再因为它是IBox类型,所以可以直接调用Change, // 于是,更改的也就是已装箱对象中的字段了,达到期望的效果。 ((IBox)o).Change(40); //40 ibox.ToString(); ((BoxA)o).Change(41); //还是40 // 没改掉的原因是o在拆箱时,生成的是临时的栈实例BoxA, // 所以,改动是基于临时BoxA的,并未改到装箱对象 ibox.ToString(); ``` --- ##委托和事件 --- ```C# public delegate string TestDelegate(int i); class TestDelegateA { public TestDelegate testDelegate; public event TestDelegate delegateEvent; public TestDelegateA() { testDelegate += TestFun; //在定义的类中可以使用= delegateEvent = i => $"====>"; } public string TestFun(int i) { Debug.Log(">>>>:{i}"); return $"==={i}"; } public void DoDelegate() { testDelegate?.Invoke(10); delegateEvent?.Invoke(20); } } var testDelegateA = new TestDelegateA(); //外部类中委托可以直接调用Invoke方法 testDelegateA.testDelegate.Invoke(1); // delegateEvent在外部类不能直接执行方法 // testDelegateA.delegateEvent.Invoke(1) //委托在外部类中可以使用=,+= -=三种运算符 testDelegateA.testDelegate = i => ""; testDelegateA.testDelegate += i => ""; //事件可以使用+=,-=运算符,不支持=运算符 testDelegateA.delegateEvent += i => ""; testDelegateA.DoDelegate(); //事件也不能赋值给另外一个值,他只能+=,-= // var t = testDelegateA.delegateEvent; //type:TargetSourceProjection+TestDelegate Debug.Log($"type:{testDelegateA.testDelegate.GetType()}"); Delegate a = testDelegateA.testDelegate; foreach (var @delegate in a.GetInvocationList()) { Debug.Log($"{@delegate.GetMethodInfo()}"); @delegate.DynamicInvoke(2); } ``` --- - 多播委托(`MulticastDelegate`)继承自 `Delegate` - delegate和event本质都是多播委托(MultipleDelegate),它用数组的形式包装了多个Delegate - Delegate类和C中函数指针有点像,同时类型安全 - 多播委托,即是函数指针链 - 单个委托实际上就是调用函数指针,而多个委托,则是通过多播委托组合单个委托形成一个新的托管函数,在这个托管函数里面进行单个函数一一调用 - delegate(委托)在定义时,会自动创建一个继承于MultipleDelegate的子类 - 委托在调用()时,编译器会翻译为.Invoke() - 事件(event)则是对私有的委托进行了包装,实现对委托方法的增加或移除 - 只能在声明类的`内部invoke`,也只能在`声明类`使用`=`操作符 - 事件的属性器不是get和set而是`add和remove`。如果用了属性器就不能用`=`操作符 - 委托用delegate声明,事件用event声明 - `Func` 可以接受 0 个至 16 个传入参数,`必须具有返回值` - `Action` 可以接受 0 个至 16 个传入参数,`无返回值` --- ##GC --- - 垃圾回收(GC)的意思是 “内存自动回收”。运行时会自动`跟踪`在 堆内存上的所有`内存引用`,并且会不时地`遍历这些引用`,`判断`这些`内存是不是`还会被程序所`使用`。所有不再被使用的内存就是 垃圾,它们可以被用于新的内存申请。 --- ###垃圾回收的几种算法 - `引用计数`:在一个对象被引用的情况下,将其引用计数加1,反之则减1,如果计数值为0,则在GC的时候回收,这个算法有个问题就是循环引用 - `根搜索标记清除算法(Mark-Sweep)`关键点是,清除后,并不会执行内存的压缩 - `根搜索标记整理算法(Mark-Compact)`关键点,清除后,会执行内存压缩,不会有内存碎片 - `分代收集算法(Generational Collection)`对内存对象进行分代标记,避免全量垃圾回收带来的性能消耗 --- ###垃圾回收的流程 - `GC准备阶段` 暂停进程中的所有线程,避免线程在 GC检测根期间访问堆内存 - `GC 的标记阶段` 首先,会`默认`托管堆上`所有的对象`都是`垃圾` (可回收对象),然后开始遍历`根对象`并构建一个`由所有和根对象之间有引用关系的对象构成的对象图`,然后 GC 会`挨个遍历根对象和其引用对象`,如果根对象没有任何引用对象 (null)GC 会忽略该根对象。对于`含有引用对象的根对象以及其引用对象`,GC 将其`纳入对象图`中,如果发现已经处于对象图中,则换一个路径遍历,避免无限循环.(`所有的全局和静态对象指针是应用程序的根对象。`) - `垃圾回收阶段` 完成遍历操作后,对于没有被纳入对象图中的对象,执行清理操作 - `碎片整理阶段` 如果`垃圾回收算法包含这个阶段`,则会对剩下的保留的对象进行一次内存整理,重新归类到堆内存中,`相应的引用地址也会对应的整理,避免内存碎片的产生`(unity没有这个) ###分代垃圾回收 - 0 代: 从未被标记为回收的新分配对象 - 1 代: 上一次垃圾回收中没有被回收的对象 - 2 代: 在一次以上的垃圾回收后仍然未被回收的对象 - CLR 初始化时,为每代分配了一定容量。如果分配的新对象超过预算,会触发 GC。通常会优先检查第 0 代的内存,并将处理后的内存归并到第 1 代。如果第一代过大,会同时检查第 0、1 代处理 ###非托管对象的回收 - 非托管资源:文件句柄,Socket,数据库连接,非托管内存或资源 - 非托管对象的管理方式: `Finalize`(析构方法) 和 `Dispose` ###Finally与IDispose --- - 都是为了确保非托管资源得到释放 - `System.Object` 定义了 `Finalize()` 虚方法,不能用 `override` 重写 - `Finalize`中`只能`释放非托管资源而`不能`对任何托管的对象/资源进行操作,因为你无法预测析构函数的运行时机,所以,当析构函数被执行的时候,也许你进行操作的托管资源已经被释放了。这样将导致严重的后果 - `finalize`虽然无需担心因为没有调用finalize而使非托管资源得不到释放,但因为由垃圾回收器管理,`不能保证立即释放非托管资源`;而`dispose一调用便释放非托管资源` - 只有`类类型`才能重写`finalize`,而结构不能;类和结构都能实现`IDispose`,在`结构`上`重写Finalize`是`不合法`的,因为结构是`值类型`,不在堆上,`Finalize`是垃圾回收器调用来清理托管堆的,而结构不在堆上 - `using`关键字,实际内部也是实现`IDisposable`方法 - `GC.SuppressFinalize`:请求公共语言运行时(CLR)不要调用该对象上的析构函数,`如果已经手动释放了资源可以使用该语句来请求gc不用再回收该资源了` --- ### 请简述非 sealed 类的 IDisposable 实现方法 ```C# class BaseClass : IDisposable { private bool disposed = false; ~BaseClass() { Dispose(disposing: false); } protected virtual void Dispose(bool disposing) { if (disposed) return; if (disposing) { // free managed resources... } // free unmanaged resources... disposed = true; } public void Dispose() { Dispose(disposing: true); GC.SuppressFinalize(this); } } ``` - 引入disposed变量用于判断是否已经回收过,如果回收过则不再回收; - 使用protected virtual来确保子类的正确回收,注意不是在Dispose方法上加 - 用disposing来判断是.NET的终结器回收还是手动调用Dispose回收,终结器回收不再需要关心释放托管内存 - 使用GC.SuppressFinalize(this)来避免调用析构方法 ####子类继承于这类、且有更多不同的资源需要管理时 ```C# class DerivedClass : BaseClass { private bool disposed = false; protected override void Dispose(bool disposing) { if (disposed) return; if (disposing) { // free managed resources... } // free unmanaged resources... base.Dispose(disposing); } } ``` - 继承类也需要定义一个新的、不同的disposed值,不能和老的disposed共用 - 其它判断、释放顺序和基类完全一样 - 在继承类释放完后,调用base.Dispose(disposing)来释放父类 --- ###Boehm GC Boehm GC用的是`Mark-Sweep算法(标记清除)`,是`非分代`(non-generational)和`非压缩`(non-compacting)的。由于它是`非分代`的,`必须遍历整个内存`,并且随着内存的增长,它的性能就会降低。非压缩`会有内存碎片化`的问题,可能会导致之后`生成的对象都没法放进这些间隙中`,又去申请内存,导致内存不断上升 --- ###Incremental GC - Unity 不会在每次运行时进行完整的垃圾收集,而是将垃圾收集工作负载拆分到多个帧中。 - 这意味着,不必单次长时间中断程序的执行来让垃圾收集器完成工作,Unity 会进行多次短时间的中断。 - 虽然这不能整体上加快垃圾收集速度,但将工作负载分布到多个帧可以极大减少垃圾收集“尖峰”破坏应用程序流畅性的问题 - 增量GC可以解决主线程卡顿问题。由于进行一次GC主线程会被迫停止,遍历所有节点,决定哪些可以被GC掉,这些操作会有个明显的峰值产生,卡顿非常明显 --- ###何时会触发垃圾回收 --- - 在堆内存上进行内存分配操作而内存不够的时候都会触发垃圾回收来利用闲置的内存 - GC会自动的触发,不同平台运行频率不一样 - GC可以被强制执行 --- ###GC操作带来的问题 - GC回收垃圾时会暂停所有线程,垃圾回收后再开启所有线程,如果垃圾回收的时间长,那么程序会出现明显的卡顿。这会导致游戏帧率下降,游戏在运行时可能卡卡顿顿,帧率下降 - 堆内存的碎片化,堆内存碎片会造成两个结果,一个是游戏占用的内存会越来越大,一个是GC会更加频繁地被触发 --- ###降低GC的影响的方法 - 减少GC的运行次数; - 减少单次GC的运行时间; - 将GC的运行时间延迟,避免在关键时候触发,比如可以在场景加载的时候调用GC --- ###具体实现 --- ####策略性的 - 对于子弹等不断重复出现消失的物体使用对象池Object Pooling管理 - 游戏载入新场景时,显示调用GC.Collection回收垃圾(因为载入场景本身就要等待,GC运行造成卡顿也没事) - 启用增量垃圾收集 - 不要在需要频繁调用的函数中写有会产生内存分配操作的代码,尤其是在Update中 - 尽量为需要长时间存活的资源创建大对象 - 减少装箱拆箱代码 - 字符串内存优化 - 使用SriptableObject(脚本化对象),其相当于资源文件,在`实例化的时只是复制了引用`(这一点其实说的就是MonoBehaviour痛点,每次实例化对象,都是完全复制,而非引用,对内存的消耗极大.而如果使用ScriptableObject实例化一次,他就会以资源的形式存储在asset文件中,其他地方如果使用的话直接引用就可以了.可以像Prefabs一样直接拖拽进去就可以了) - `分散对象创建或删除的时间`(集中在短时间内大量创建新对象,特别是大对象,会导致突然需要大量内存,只能进行主 GC,以回收内存或整合内存碎片,从而增加主 GC 的频率,集中删除对象,道理也是一样的。 它使得突然出现了大量的垃圾对象,空闲空间必然减少,从而大大增加了下一次创建新对象时强制主 GC 的机会) --- ####代码相关的 - 如果要销毁一个物体,不要置为Null,而是要Destroy,然后置为Null。(因为置为Null,在C++层未销毁)(设置为Null,有利于 GC 收集器判定垃圾,从而提高了 GC 的效率) - 如果某个方法只需要在某段时间内或某个条件下重复调用,而不是一直被重复调用,使用Invoke Repeating或者Coroutine来替代Update方法。(Update会被每帧调用,即使里面只有一行代码,也会耗时的,用协程虽然也会产生内存,但好处在于之后就不会再被调用。) - Debug.log()会消耗很多性能,游戏完成后尽量去掉无用的Debug - 减少Find,Component的使用,可以在Init中直接一次获取,而不是用的时候再获取 - 慎用单例并管理好单例,因为静态中的引用不会被释放,如果其引用的某个东西引用了一个资源,那么资源不会被释放,内存一直占着 - 尽量避免使用静态字段(静态字段所引用的对象不会被GC回收,如果该对象较大或该对象又引用了很多其他对象,那么这一系列的对象都一直存活。如果在接下来的程序中,我们不需要这些对象了,那么一直存活的这些对象会占用内存。) - 减少匿名函数和闭包的使用(所有的匿名函数和闭包在c#编IL代码时都会被new成一个Class(匿名class),所以在里面所有函数,变量以及new的东西,都是要占内存的 - 合理使用数据结构与算法,例如了解常用的`List<T>,Array,Stack<T>,Queue<T>,Dictionary<TKey,TValue>`的源码以便更合理地使用 - 从脚本中获取材质时使用Renderer.sharedMaterial而不是Renderer.Material,后者会产生一份新的copy - struct中最好不要有引用类型的变量,这会使得GC会检测整个struct - 如果这个类被高频应用,且类中都是对字段的操作,没有引用类型,改用struct - 将暂时不用的以后还需要使用的物体隐藏起来而不是直接销毁 - 大量字符串拼接的操作用StringBuilder - 避免频繁Instantiate来实例化GameObject,这会导致很多内存分配 - 释放AssetBundle占用的资源 - 利用缓存,将需要重复调用的方法中的临时变量改为类的成员变量,例如Update要调用很多次,则需要产生很多临时变量的内存垃圾 - 链表重用与清除,同变量缓存类似,对链表、字典等需要反复用到的在用完时清除 - 对`GameObject.tag`的获取会分配内存在存储返回的字符串,如果需要比较判断时,用GameObject.CompareTag()来替代,`Input.GetTouch()`和`Input.touchCount()`来代替`Input.touches`,或者用`Physics.SphereCastNonAlloc()`来代替`Physics.SphereCastAll()` - for循环嵌套时把循环次数多的放在里面 - 注意协程造成的GC,协程中的yield语句本身不需要进行堆内存分配, 但由`yield return`带来的返回值可能需要分配堆内存,调用`StartCoroutine()`方法会产生少量的垃圾, 因为Unity需要创建一些类的实例用于管理协程。注意,不要让某一个协程长时间存在,因为协程只要没被释放,里面的所有变量,即使是局部变量(包括值类型),也都会在内存里 ```C# //yield return 0; //由于需要返回0,引发了装箱操作,所以会产生内存垃圾。这种情况下,为了避免内存垃圾,我们可以这样返回: yield return null; 另外一种对协程的错误使用是每次返回的时候都new同一个变量 //while(!isComplete) //{ // yield return new WaitForSeconds(1f); //} 我们可以采用缓存来避免这样的内存垃圾产生 WaitForSeconds delay = new WaiForSeconds(1f); while(!isComplete) { yield return delay; } ``` - 对具有返回值的Unity函数和第三方插件函数,返回结果时需要分配内存,同样采用变量缓存的方式定义一个变量来接收结果 ```C# void ExampleFunction() { for(int i=0; i < myMesh.normals.Length;i++) { Vector3 normal = myMesh.normals[i]; } } //改进 void ExampleFunction() { Vector3[] meshNormals = myMesh.normals; for(int i=0; i < meshNormals.Length;i++) { Vector3 normal = meshNormals[i]; } } ``` - 对于需要在Update中但不需要每帧都运行的函数,设置定时器来保证每隔一段时间触发该函数 ```C# private float timeSinceLastCalled; private float delay = 1f; void Update() { timSinceLastCalled += Time.deltaTime; if(timeSinceLastCalled > delay) { ExampleGarbageGenerationFunction(); timeSinceLastCalled = 0f; } } ``` - 不要在Update中频繁更新Text的内容,因为每次调用Update时都会分配新的字符串,源源不断产生垃圾,应该先检测Text的内容是否会发生变化 --- ###Lua GC机制 --- lua采用了`标记清除式(Mark and Sweep)GC算法`:遍历所有对象,将还在使用的对象打上``标记`,遍历完成后,没有标记的对象就是垃圾对象,要`清理`掉 --- ####双色标记清除法 - Lua5.0及其更早的版本: - Lua的GC是一次性不可被打断的过程 - Mark算法是双色标记清除算法 - 系统中对象的非黑即白,要么被引用,要么不被引用 **问题**:在GC的过程中如果新加入对象,这时候新加入的对象无论怎么设置都会带来问题 - 如果设置为`白色`,则如果处于`回收阶段`,则该对象会在`没有遍历其关联对象的情况下被回收 ` - 如果标记为`黑色`,那么`没有被扫描就被标记为不可回收`,是不正确的 --- ####三色标记清除法:gc可以中断,与主程序交替执行 - `white`:`可回收状态`。如果该对象未被GC标记过则此时白色代表`当前对象为待访问状态`。举例:新创建的`对象的初始状态`就应该被设定为`白色`,因为该对象`还没有被GC标记`到,所以保持初始状态颜色不变,仍然为白色。如果`该对象在GC标记阶段结束`后,仍然为`白色`则此时`白色代表当前对象为可回收状态`。但其实本质上白色的设定就是为了标识可回收 - `gray`:`中间状态`,当前对象为`待标记状态`。当前对象已经被GC访问过,但是该对象引用的其他对象还没有被标记。 - `black`:`不可回收状态`,当前对象为`已标记状态`。当前对象已经被GC访问过,并且对象引用的其他对象也被标记了 - 白色分为`白1`和`白2` - 在GC标记阶段结束而清除阶段尚未开始时,如果新建一个对象,由于其未被发现引用关系,原则上应该被标记为白色,于是之后的清除阶段就会按照白色被清除的规则将新建的对象清除 - lua依然会将新建对象标识为白色,不过是`当前白`(比如白1)。而lua在清扫阶段只会清扫“旧白”(比如白2)。 --- #####步骤 - 初始时,所有对象都标记`white` - 将`root GC`遍历对象,标记`gray`,并加入`gray链表` - 从`gray链表`中取出对象 - 将该对象引用到的其他对象标记 `gray`,并加入`gray链表` - 将该对象标记 `black` - 重复步骤3,直至`gray链表为空`时结束 - 结束后,`white`的即为`不可达对象`,进行回收 --- #### lua 需要gc的是哪些数据 - 所有对象都要接受gc管理回收 --- #### lua gc是怎么触发的 - 自动触发:当lua使用的内存达到阀值,便会触发GC - 手动触发: - `collectgarbage("step")`: 单步运行垃圾收集器。 - `collectgarbage("collect")`: 做一次完整的垃圾收集循环 --- ####lua gc什么情况下会卡顿 - 持续生成大量大table,并且引用新数据 - 大量使用弱引用table --- ####Lua GC垃圾回收优化 - 减少对象生成,少写这种动态生成`Closure` 就可以减少对象的生成 ```lua --执行完这个函数以后,因为没有对象指向 Closure 用完再不久的将来又会被回收。 function test() local fn = function() print("test") end fn() fn() end ``` - [垃圾回收提速](https://github.com/Yu2erer/LuaJIT-5.3.6/tree/Lua5.3.6-NOGC)。让垃圾回收所要遍历的对象大幅减少,就可以为垃圾回收提速了。所有配置都存在于 Lua 的 table中,而这一部分肯定是不需要被回收的,但是每次垃圾回收的时候,又会不停的扫描递归遍历,不合理。同时代码中的很多全局函数,也是根本不需要被回收的,也会被扫描到,于是就想到一个想法,给这些对象打上标记,让他们不被遍历不被清理,就可以大幅度的提速了 --- ####中途创建的对象的颜色处理 - 前向操作:新创建对象为`白色`,被一个黑色对象引用,则将当前新创建对象标记为`灰色` - 后退操作:新创建对象为白色,被黑色对象引用,该``黑色对象`退回到`灰色`,塞入到灰色链表中,后续一次性扫描处理 - 对大部分数据,都是`前向操作`,对于table类型数据,是`后退操作` - table属于频繁操作的对象,如果反复将table中新创建的对象都设置成灰色,则`灰色链表`会容易变得很大,所以为了提高性能,就将table塞入到灰色链表中,后续一次性处理即可 --- ##Unity协程 --- ###协程、线程、进程 - 协程: - 协程是伴随着主线程一起运行的一段程序 - 协程与协程之间是并行执行,与主线程也是并行执行,同一时间只能执行一个协程 - 一个线程可以拥有多个协程,协程不是被操作系统内核所管理,而完全是由程序所控制 - 协程和线程一样共享堆,不共享栈,协程由程序员在协程的代码里显示调度 - 线程: - 线程从属于进程,是程序的实际执行者。线程是操作系统能够进行`运算调度`的最小单位 - 线程拥有自己`独立的栈`和`共享的堆`,共享堆,不共享栈,线程亦由操作系统调度 - 进程: - 一个应用程序相当于一个进程,操作系统会以进程为单位,分配系统资源(CPU 时间片、内存等资源),进程是`资源分配`的最小单位 --- ###StartCoroutine ```C# public Coroutine StartCoroutine(IEnumerator routine) public Coroutine StartCoroutine(string methodName) public void StopCoroutine(IEnumerator routine) public void StopCoroutine(Coroutine routine) public extern void StopCoroutine(string methodName); ``` --- - 协程中要以IEnumerator为返回值,通过yield来暂停协程的执行 - 开启协程传入参数可以`string`或者`IEnumerator` - 关闭协程传入参数可以`string`或者`IEnumerator`,以及`Coroutine` - 注意不管是开启协程还是关闭协程都不能用`IEnumerable` - `yield return`除了用于IEnumerator<T>以外,还可以用于IEnumerator ,以及`Coroutine` 、`YieldInstruction`子类,`CustomYieldInstruction`子类 - `yield`能在一个函数中支持多次(`不是多个`)返回,其本质和async/await一样,也是状态机 - 协程其本质其实是通过IEnumerator迭代器实现的一种状态机,故其本质还是单线程的,一旦协程卡住整个线程也会卡住 - `IEnumerable、IEnumerable<T>`:遍历集合的枚举器,可以用`foreach`遍历集合中的各个元素 --- ```C# public IEnumerator TestEnumerator() { Debug.Log($"TestEnumerator:111:{Time.frameCount}"); yield return new WaitForSeconds(1); Debug.Log($"TestEnumerator:222:{Time.frameCount}"); yield return StartCoroutine(TestStartStartCoroutine()); Debug.Log($"TestEnumerator:333:{Time.frameCount}"); yield return 0; Debug.Log($"TestEnumerator:444:{Time.frameCount}"); yield return TestEnumerable(); Debug.Log($"TestEnumerator:555:{Time.frameCount}"); yield return null; Debug.Log($"TestEnumerator:666:{Time.frameCount}"); yield return new MyEnumerator(KeyCode.A); //发觉不能迭代这样 // yield return new MyEnumerable(KeyCode.A); Debug.Log($"TestEnumerator:777:{Time.frameCount}"); yield break; } public IEnumerator<int> TestStartStartCoroutine() { Debug.Log($"TestStartStartCoroutine:111:{Time.frameCount}"); yield return 4; Debug.Log($"TestStartStartCoroutine:222:{Time.frameCount}"); } public IEnumerable<float> TestEnumerable() { Debug.Log($"TestEnumerable:111:{Time.frameCount}"); yield return 3.0f; Debug.Log($"TestEnumerable:222:{Time.frameCount}"); } public class MyEnumerable : IEnumerable<string> { private Enum __enum; public MyEnumerable(Enum @enum) { __enum = @enum; } public IEnumerator<string> GetEnumerator() { return new MyEnumerator(__enum); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } } public class MyEnumerator : IEnumerator<string> { private string[] __values; private int __index = -1; private int __count = 0; private bool __dispose = false; public MyEnumerator(Enum array) { __values = Enum.GetNames(array.GetType()); __index = -1; __count = __values.Length; } public bool MoveNext() { if (++__index < __count) { Debug.Log($"enum:{__values[__index]},{Time.frameCount}"); return true; } return false; } public void Reset() { __index = -1; } public string Current => __values[__index]; object IEnumerator.Current => Current; protected virtual void _Dispose(bool dispose) { if (__dispose) { return; } if (dispose) { __values = null; } __dispose = true; } ~MyEnumerator() { _Dispose(false); } public void Dispose() { _Dispose(true); GC.SuppressFinalize(this); } } Debug.Log($"startCoroutine======>before:{Time.frameCount}====="); StartCoroutine(TestEnumerator()); // StopCoroutine(TestEnumerator()); Debug.Log($"startCoroutine======>after:{Time.frameCount}====="); foreach (var s in new MyEnumerable(HttpRequestHeader.Accept)) { } ``` --- ###IEnumerable以及IEnumerator --- ```C# public interface IEnumerator { object Current { get; } bool MoveNext(); void Reset(); } public interface IEnumerator<out T> : IEnumerator, IDisposable { T Current { get; } } public interface IEnumerable { IEnumerator GetEnumerator(); } public interface IEnumerable<out T> : IEnumerable { IEnumerator<T> GetEnumerator(); } ``` --- ###自己实现一个协程 ```C# public class MyCoroutineManager { public List<IEnumerator> Enumerators = new List<IEnumerator>(); public void StartCoroutine(IEnumerator e) { Enumerators.Add(e); } } var myCortManager = new MyCoroutineManager(); myCortManager.StartCoroutine(TestEnumerator()); var delete = new List<IEnumerator>(); while (true) { if (myCortManager.Enumerators.Count == 0) { break; } delete.Clear(); foreach (var item in myCortManager.Enumerators) { if (!item.MoveNext()) { delete.Add(item); } } foreach (var item in delete) { myCortManager.Enumerators.Remove(item); } } ``` - unity为了实现协程在干的事情其实就是支持了一堆yield return 的返回值, 再写一个调度器,根据返回值来按时候唤醒(调用.MoveNext()) --- ###协程、Invoke、InvokeRepeating的执行区别 - 禁用对应脚本 - Coroutine:执行 - Invoke:执行 - InvokeRepeating:执行 - 禁用对应的gameobject - Coroutine:不执行 - Invoke:执行 - InvokeRepeating:执行 - 销毁对应的脚本或者销毁对应的gameObject - Coroutine:不执行 - Invoke:不执行 - InvokeRepeating:不执行 --- - Invoke函数必须输入方法名,对于上下文变量、属性的引用就会尤为不便,因此不能传有参方法进Invoke - Invoke执行没有被挂起,相当于设置完被调用函数的执行时间后即时向下执行 - 协程相比Invoke拥有更多参数选择,比如等待一帧后执行,等待某个函数执行完毕后执行等 --- ###yield return支持的协程响应类有以下这些 --- ```C# //下一帧执行后续代码 yield return null; //下一帧执行后续代码 yield return 数字; //结束迭代器 yield break; //Unity用于进行基于协程的异步操作的基类 yield return new AsyncOperation(); //协程的嵌套 yield return IEnumerator; //用于协程的嵌套和监听Coroutine类是StartCoroutine返回的协程句柄 yield return new StartCoroutine(""); //WWW继承CustomYieldInstruction,CustomYieldInstruction可以用于实现自定义协程响应类,CustomYieldInstruction继承自IEnumerator yield return new WWW(); //在这帧结束,在 Unity 渲染每一个摄像机和 GUI 之后,在屏幕上显示下一帧之前执行后续代码。 //WaitForEndOfFrame继承自YieldInstruction yield return new WaitForEndOfFrame(); //下一帧FixedUpdate开始时执行后续代码 //WaitForFixedUpdate继承自YieldInstruction yield return new WaitForFixedUpdate(); //延时设置的等待时间之后一帧的Update执行完成后运行,受到Time.timeScale影响 //WaitForSeconds继承自YieldInstruction yield return new WaitForSeconds(等待时间/秒); //延时设置的等待时间之后一帧的Update执行完成后运行 //WaitForSecondsRealtime继承自CustomYieldInstruction yield return new WaitForSecondsRealtime(等待时间/秒); //WaitUntil继承自CustomYieldInstruction yield return WaitUntil(Func<bool> predicate) //WaitWhile继承自CustomYieldInstruction yield return WaitWhile(Func<bool> predicate) ``` --- ##异步 --- ###同步方法和异步方法区别 - 同步方法调用在程序继续执行之前,需要等待同步方法执行完毕返回结果 - 异步方法则在被调用之后,立即返回以便程序在被调用方法完成其任务的同时执行其它操作 --- ###BeginInvoke异步 --- ```C# public void AsyncButtonClick1() { Func<string, int> click = s => { Debug.Log($"传参:『{s}』开始点击了:{Thread.CurrentThread.ManagedThreadId}"); Thread.Sleep(1000); Debug.Log($"点击结束了:{Thread.CurrentThread.ManagedThreadId}"); return 1; }; Debug.Log($"click=======>>11111>>{Thread.CurrentThread.ManagedThreadId}>>>"); var asyncResult = click.BeginInvoke("点击1", delegate(IAsyncResult ar) { Debug.Log($"透传AsyncState:{ar.AsyncState},{Thread.CurrentThread.ManagedThreadId}"); var result = click.EndInvoke(ar); Debug.Log($"result:{result}"); }, "透传数据"); Debug.Log($"click=======>>222>>{Thread.CurrentThread.ManagedThreadId}>>>"); StartCoroutine("__Corotine"); Debug.Log($"click=======>>333>>{Thread.CurrentThread.ManagedThreadId}>>>"); } private IEnumerator __Corotine() { Debug.Log($"__Corotine11111:{Thread.CurrentThread.ManagedThreadId}"); yield return new WaitForEndOfFrame(); Debug.Log($"__Corotine222:{Thread.CurrentThread.ManagedThreadId}"); } public void AsyncButtonClick2() { Action<string> click = s => { Debug.Log("开始点击"); Thread.Sleep(1000); Debug.Log("点击结束"); }; var asyncResult = click.BeginInvoke("点击", delegate(IAsyncResult ar) { Debug.Log("回调了"); click.EndInvoke(ar); }, "透传"); // while (!asyncResult.IsCompleted) // { // Debug.Log($"正在点击加载中...{Time.frameCount}"); // } Debug.Log("开始等待"); //不填参数或者值为负数表示一直等到完成后,才会执行下一行代码 asyncResult.AsyncWaitHandle.WaitOne(); asyncResult.AsyncWaitHandle.WaitOne(-1); //表示不等待 asyncResult.AsyncWaitHandle.WaitOne(0); //表示等10毫秒 asyncResult.AsyncWaitHandle.WaitOne(10); Debug.Log("点击结束了!!!"); } public interface IAsyncResult { //透传参数 object AsyncState { get; } //可以获取AsyncWaitHandle.WaitOne WaitHandle AsyncWaitHandle { get; } bool CompletedSynchronously { get; } //判断是否完成 bool IsCompleted { get; } } ``` --- - 调用BeginInvoke可以执行任务,BeginInvoke对应参数是首先传递委托的参数,紧接着回调方法,以及透传字段 - 调用EndInvoke将一直阻塞到异步调用完成 - asyncResult.AsyncWaitHandle.WaitOne():不填参数或者值为负数表示一直等到完成后,才会执行下一行代码,等于0不需要等待。大于0,等待对应时间 --- ###Thread --- - Start():启动线程 - Sleep(int):静态方法,暂停当前线程指定的毫秒数 - Abort():通常使用该方法来终止一个线程; - Join():当前线程会立即被执行,其他所有的线程会被暂 - IsBackground=true,则该线程为后台线程。后台线程将会随着主线程的退出而退出,而IsBackground=FALSE的线程还会继续执行下去,直到线程执行结束 --- ```C# public void ThreadsOnClick() { var stopWatch = new Stopwatch(); stopWatch.Start(); ThreadStart threadStart = delegate { __DoLongTimeThing("ThreadsOnClick"); }; Thread thread = new Thread(threadStart); thread.Start(); // Thread属性 IsBackground=true,则该线程为后台线程。后台线程将会随着主线程的退出而退出, // 而IsBackground=FALSE的线程还会继续执行下去,直到线程执行结束。 thread.IsBackground = true; __Log("join 线程"); //等待执行完成 thread.Join(); stopWatch.Stop(); __Log($"耗时:{stopWatch.ElapsedMilliseconds}"); } public void ThreadsOnClick2() { var stop = new Stopwatch(); stop.Start(); var r = DoingThread("现成直播", delegate(string s) { Thread.Sleep(5000); return s.GetHashCode(); }); __Log("获取值"); var result = r.Invoke(); stop.Stop(); __Log($"result:{result},耗时:{stop.ElapsedMilliseconds}"); } private Func<T> DoingThread<T1, T>(T1 s, Func<T1, T> func) { var t = default(T); void ThreadStart() { t = func.Invoke(s); } var thread = new Thread((ThreadStart)ThreadStart); thread.Start(); return delegate { thread.Join(); return t; }; } ``` --- ###AutoResetEvent - `WaitOne` - 当线程调用`WaitOne`方法时,它会进入等待状态,直到接收到信号 - 如果`AutoResetEvent`处于无信号状态(`false`),`WaitOne`方法将阻塞线程的执行 - 如果`AutoResetEvent`处于有信号状态(`true`),`WaitOne`方法将消耗该信号,并使`AutoResetEvent`重新进入无信号状态(`false`) - `Set`: - 当线程调用`Set`方法时,它会将`AutoResetEvent`的状态设置为有信号(true) - 如果有线程正在等待信号,它将被唤醒并开始行 - 如果没有线程在等待信号,调用`Set`方法也会将`AutoResetEvent`的状态设置为有信号,但不会有任何其他影响 - 即使多次调用`Set`方法,`AutoResetEvent` 也只会保持有信号状态一次,直到被消耗 --- ```C# private AutoResetEvent __producerEvent = new AutoResetEvent(false); private AutoResetEvent __consumerEvent = new AutoResetEvent(true); private int __index = -1; public void AutoResetEvent() { var product = new Thread(__Product); product.Name = "生产者"; var consume = new Thread(__Consume); consume.Name = "顾客"; consume.Start(); Thread.Sleep(20); product.Start(); product.Join(); consume.Join(); __Log("执行完了????"); } private void __Product() { for (int i = 0; i < 5; i++) { __Log($"生产者准备生产:{i}"); __consumerEvent.WaitOne(); // 等待消费者消费物品 __index = i; __Log($"生产者生产了:{__index}"); __producerEvent.Set(); } } private void __Consume() { for (int i = 0; i < 5; i++) { __Log($"消费者第:{i}次进店"); __producerEvent.WaitOne(); __Log($"消费者消费了:{__index}"); __consumerEvent.Set(); } } ``` --- 根据实际需求来选择`初始的信号状态`。如果希望线程在开始时能够`继续执行而不被阻塞`,可以使用`有信号状态`(true);如果希望线程在(开始时等待信号才能执行),可以使用`无信号状态`(false)。 --- ###ManualResetEvent - 在调用 `Set`方法后,所有等待线程都会被唤醒并继续执行,直到显式调用 `Reset` 方法将`ManualResetEvent`设置回无信号状态为止。即每次调用`Set`方法会唤醒所有等待线程 - 需要显式调用 `Reset`方法将其重置为无信号状态。即`ManualResetEvent`会保持有信号状态,直到调用`Reset`方法将其置回无信号状态 - 在调用`WaitOne`方法时,如果`ManualResetEvent` 处于无信号状态,线程会被阻塞直到接收到信号。即使 `ManualResetEvent`处于有信号状态,线程会继续执行,而不会自动将其重置为无信号状态 - `ManualResetEvent`适用于广播场景,`AutoResetEvent`适用于排队任务的场景 --- ```C# ThreadPool.SetMaxThreads(10, 10); ThreadPool.SetMinThreads(5, 5); ThreadPool.GetMaxThreads(out var workerThreads, out var completionPortThreads); __Log($"先:workerThreads:{workerThreads},completionPortThreads:{completionPortThreads}"); ManualResetEvent manualResetEvent = new ManualResetEvent(false); ThreadPool.QueueUserWorkItem(delegate(object state) { manualResetEvent.WaitOne(); __DoLongTimeThing("做点大事情"); __Log("大事情干完了没???"); }); ThreadPool.QueueUserWorkItem(new WaitCallback(delegate(object state) { __DoLongTimeThing("做小事情"); Thread.Sleep(5000); manualResetEvent.Set(); })); ThreadPool.GetMaxThreads(out workerThreads, out completionPortThreads); __Log($"后:workerThreads:{workerThreads},completionPortThreads:{completionPortThreads}"); manualResetEvent.WaitOne(); __Log("我是不是被堵塞了???"); ``` --- - `QueueUserWorkItem()`方法用来启动一个多线程 - `ThreadPool`相对于`Thread`来说可以减少线程的创建,有效减小系统开销 - `ThreadPool`不能控制线程的执行顺序 - 不能获取`ThreadPool`内线程取消/异常/完成的通知 ###Task --- ```C# ////1.new方式实例化一个Task,需要通过Start方法启动 Task task = new Task(delegate { __DoLongTimeThing("new Task"); }); // task.Start(); //同步执行,task会阻塞主线程 task.RunSynchronously(); __Log("终于把task1干完了!!!"); ////2.Task.Factory.StartNew(Func func)创建和启动一个Task var task2 = Task.Factory.StartNew(delegate { __DoLongTimeThing("Task.Factory"); }); ////3.Task.Run(Func func)将任务放在线程池队列,返回并启动一个Task var task3 = Task.Run(delegate { __DoLongTimeThing("task.run"); }); Task.WaitAll(new Task[] { task2, task3 }); Task<string> task4 = new Task<string>(delegate { __DoLongTimeThing("new task with return"); return "task4"; }); task4.Start(); var task5 = Task<string>.Factory.ContinueWhenAll(new Task[] { task4 }, delegate(Task[] tasks) { __DoLongTimeThing("Task<string>.Factory.ContinueWhenAll"); return "task5"; }); // task.Resut获取结果时会阻塞线程,即如果task没有执行完成,会等待task执行完成获取到Result var task5Result = task5.Result; __Log($"result:{task5Result}"); var cancel = new CancellationTokenSource(); // source.Token.Register(Action action) 注册取消任务触发的回调函数 cancel.Token.Register(delegate { __Log("任务被取消!!!"); }); var index = 0; var task6 = Task.Factory.StartNew(delegate { while (!cancel.IsCancellationRequested) { Thread.Sleep(1000); __Log($"醒来的第{++index}次"); } }); // Thread.Sleep(5000); //source.Cancel()方法请求取消任务,IsCancellationRequested会变成true // cancel.Cancel(); //source.CancelAfter(5000) 实现5秒后自动取消任务 cancel.CancelAfter(5000); task6.Wait(); __Log("还在睡吗???task6"); ``` --- - `Task<T>`:如果调用方法想通过调用异步方法获取一个T类型的返回值,那么签名必须为`Task<TResult>`; - `Task`:如果调用方法不想通过异步方法获取一个值,仅仅想追踪异步方法的执行状态,那么我们可以设置异步方法签名的返回值为`Task`; - `async/await`是基于`Task`的,而`Task`是对`ThreadPool`的封装改进,主要是为了更有效的控制线程池中的线程;`ThreadPool`基于`Thread`的,主要目的是减少Thread创建数量和管理Thread的成本 --- ###async/await --- ```C# public async void NoReturn() { Debug.Log($"NoReturn Sleep before Task,线程ID:{Thread.CurrentThread.ManagedThreadId}"); await TestTask(); //主线程执行 Debug.Log($"NoReturn Sleep after Task,线程ID:{Thread.CurrentThread.ManagedThreadId}"); } public async void OnClick() { Debug.Log("Start Click"); // await NoReturn(); // await NoReturnTask(); // var s = await ReturnString(); // Debug.Log($"result:{s}"); Debug.Log("End Click"); } // 无返回值 async Task == async void public async Task NoReturnTask() { Debug.Log($"NoReturnTask Sleep before Task,线程ID:{Thread.CurrentThread.ManagedThreadId}"); await TestTask(); //主线程执行 Debug.Log($"NoReturnTask Sleep after Task,线程ID:{Thread.CurrentThread.ManagedThreadId}"); } public async Task<string> ReturnString() { Debug.Log($"ReturnString Sleep before Task,线程ID:{Thread.CurrentThread.ManagedThreadId}"); var task = Task<string>.Run(delegate { Debug.Log($"多线程 Sleep before,线程ID:{Thread.CurrentThread.ManagedThreadId}"); Thread.Sleep(1000); Debug.Log($"多线程 Sleep after,线程ID:{Thread.CurrentThread.ManagedThreadId}"); return "task string"; }); await task; Debug.Log($"ReturnString Sleep after Task,线程ID:{Thread.CurrentThread.ManagedThreadId}"); return task.Result; } private Task TestTask() { return Task.Factory.StartNew(delegate { Debug.Log($"多线程 Sleep before,线程ID:{Thread.CurrentThread.ManagedThreadId}"); Thread.Sleep(1000); Debug.Log($"多线程 Sleep after,线程ID:{Thread.CurrentThread.ManagedThreadId}"); }); } ``` --- - `async/await`本质上只是一个语法糖,它并不产生线程,只是在编译时把语句的执行逻辑改了,相当于过去我们用callback,这里编译器帮你做了 - `async/await`只是表示这个方法需要编译器进行特殊处理,并不代表它本身一定是异步的 - `await`:异步方法在碰到await表达式之前都是使用同步的方式执行 - Unity提供了一个名为 `UnitySynchronizationContext`的默认 `SynchronizationContext` 它会自动收集每个帧排队的任何异步代码,并在主要的Unity线程上继续运行它们。 - 线程的转换是通过`SynchronizationContext`来实现,如果做了`Task.ConfigureAwait(false)`操作,运行`MoveNext`时就只是在`线程池`中拿个空闲线程出来执行;如果`Task.ConfigureAwait(true)-(默认)`,则会在异步操作前`Capture当前线程的SynchronizationContext`,异步操作之后运行MoveNext时`通过SynchronizationContext转到目标之前的线程` - `SynchronizationContext`代表线程上下文,主要有两个方法: - `Send()`:是简单的在当前线程上去调用委托来实现(同步调用)。也就是在子线程上直接调用线程执行,等线程执行完成后子线程才继续执行 - `Post()`:在线程池上去调用委托来实现(异步调用)。这是子线程会从线程池中找一个线程去调线程,子线程不等待UI线程的完成而直接执行自己下面的代码 - 编译器要求`await后面的类型`必须要有`GetAwaiter方法`,同时`GetAwaiter返回类型`必须满足以下条件: - 必须继承`INotifyCompletion`接口,并实现其中的`OnCompleted(Action continuation)`方法 - 必须包含`IsCompleted`属性 - 必须包含`GetResult()`方法 - `Task`类中的`GetAwaiter`主要是给编译器用的。 ```C# public async Task MyAawiterTest() { Debug.Log($"MyAawiterTest start:{Thread.CurrentThread.ManagedThreadId}"); await new MyAawiter(); Debug.Log($"MyAawiterTest finish:{Thread.CurrentThread.ManagedThreadId}"); } public class MyAawiter : INotifyCompletion { public MyAawiter() { Debug.Log($"Create MyAawiter {Thread.CurrentThread.ManagedThreadId}"); } public void OnCompleted(Action continuation) { continuation?.Invoke(); } public bool IsCompleted { get; } public void GetResult() { } public MyAawiter GetAwaiter() { return new MyAawiter(); } } //看到这是完全同步进行的,只要满足条件就可以放在await后面 await MyAawiterTest(); ``` ```C# public struct LazyAwaiter<T> : INotifyCompletion { private readonly Lazy<T> _lazy; public LazyAwaiter(Lazy<T> lazy) => _lazy = lazy; public T GetResult() => _lazy.Value; public bool IsCompleted => true; public void OnCompleted(Action continuation) { } } public static class LazyAwaiterExtensions { public static LazyAwaiter<T> GetAwaiter<T>(this Lazy<T> lazy) { return new LazyAwaiter<T>(lazy); } } var lazy = new Lazy<int>(() => 42); var result = await lazy; ``` --- ###Task的副作用 --- - 死锁现象,通常的Task.Result、Task.Wait等都会阻塞当前线程,当配合上await后,则很容易造成context争用死锁 - 解决死锁的方法有三种 - 将代码同步化,顺序执行,不存在争抢线程资源 - 不使用await关键字 - 使用ConfigureAwait(false)(将上述代码的ConfigureAwait(true)的true部分改为false): ```C# private void OnEnable() { Debug.Log($"start:{Thread.CurrentThread.ManagedThreadId}"); var task = AsyncTest(); Debug.Log($"task===={Thread.CurrentThread.ManagedThreadId}==="); //task.Result这里会卡住,等结果 Debug.Log($"task is :{task.Result},{Thread.CurrentThread.ManagedThreadId}"); } async Task<int> AsyncTest() { Debug.Log($"async start :{Thread.CurrentThread.ManagedThreadId}"); //await 会卡主,等task结束的时候就会,切换到原来线程,但此时原来主线程也在卡着,就造成死锁 //ConfigureAwait(true);这个方法可以不加,表面默认就是切回原来现成, //如果主线程不卡主的情况下,async start的log和async finish 的log打印出来的线程id一样 //ConfigureAwait(false),表示task.run结束之后不切换线程,此时解决了死锁的问题,但引发另一个问题 //async finish的log打印出来的线程id和task start打印出的线程id一样,也就是,task.run之后没有切换线程 //换一句解释“剩下”的代码逻辑无法访问UnityEngine的大部分API await Task.Run(delegate { Debug.Log($"task start :{Thread.CurrentThread.ManagedThreadId}"); Thread.Sleep(1000); Debug.Log($"task end :{Thread.CurrentThread.ManagedThreadId}"); }).ConfigureAwait(true); Debug.Log($"async finish :{Thread.CurrentThread.ManagedThreadId}"); return 1; } ``` - `Task`和`struct`搭配由于`struct`的`值拷贝`会出问题 ```C# public struct TaskStruct { private int Ref; //值拷贝跟async有关,即使没有await,只留下async还是值拷贝 public async Task<int> Inc() { Ref++; await Task.Yield(); return Ref; } public void Print() { //ref:0 Debug.Log($"ref:{Ref}"); } } var taskStruct = new TaskStruct(); //copy了个新的struct对象实例来操作 var r=await taskStruct.Inc(); // result:1 Debug.Log($"result:{r}"); taskStruct.Print(); ``` - `Task`中如果没有await `async方法`执行的话,async方法抛出来的异常会被吞并 ```C# public async Task TestException() { Debug.Log("start testException"); await Task.Run(delegate { Debug.Log("task first"); Thread.Sleep(1000); Debug.Log("task finish"); }); Debug.Log("finish TestException"); throw new Exception("test exception!!!"); } //添加了await的话异常接收正常 await TestException(); //异常被吞了 TestException(); ``` --- ##StringBuilder --- - c# StringBuilder的本质是`单向链表` - StringBuilder本身包含了`m_ChunkPrevious`指向的是上一个扩容时保存的数据 - 扩容的本质就是给这个链表新增一个节点 - StringBuilder每次扩容的长度是`不固定的`,实际的扩容长度是`max(当前追加字符的剩余长度,min(当前StringBuilder长度,8000))`,每次扩容新增的节点存储块的`容量都会增加`。大部分使用时遇到的情况是首次为16、二次为16、三次为32、四次为64以此类推 - c# StringBuilder类的`ToString`本质就是`倒序遍历单向链表`,每一次遍历都获取当前StringBuilder的`m_ChunkPrevious`字符数组获取数据拼接完成之后,然后获取m_ChunkPrevious指向的上一个StringBuilder实例,最终把结果组装成一个字符串返回 - `m_ChunkLength`描述当前Chunk存储信息的长度。也就是存储了字符数据的长度,不一定等于字符数组的长度 - `m_ChunkOffset`描述当前Chunk在整体字符串中的起始位置,方便定位。 - `Length`属性描述的是内部保存整体字符串的长度,等于`m_ChunkLength+m_ChunkOffset`。 --- ```C# var sb = new StringBuilder(); sb.Append('1', 10); sb.Append('2', 6); sb.Append('3', 24); sb.Append('4', 15); sb.Append("hello world"); sb.Append("nice to meet you"); Debug.Log($"{sb}"); var p = sb; char[] data; var type = sb.GetType(); int count = 0; while (p != null) { count++; data = type.GetField("m_ChunkChars", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(p) as char[]; Debug.Log($"倒数第{count}个StringBuilder内容:{new string(data)}"); p = type.GetField("m_ChunkPrevious", BindingFlags.Instance | BindingFlags.NonPublic) .GetValue(p) as StringBuilder; } ``` --- ###为什么说采用单链表能避免复制操作 - 如果又有新的字符串需要拼接且其长度超过字符数组空闲的容量时,可以考虑新开辟一个新空间专门存储超额部分的数据。这样,先前部分的数据就不需要进行复制了 - 整个数据被存储在两个不相连的部分,怎么关联他们,采用链表的形式将其关联是一个可行的措施 --- ###为什么采用逆向链表,即每个节点保留指向前一个节点的引用 - 对`StringBuilder`使用最广泛的功能就是拼接字符串了,即向尾部添加新数据 - 在这个基础上,如果采用`正向链表`(每个节点保留下一个节点的引用),那么多次拼接字符串在数组容量不够的情况下,势必需要每次循环找到最后一个节点并添加新节点,时间复杂度为`O(n)` - 采用`逆向链表`,因为用户所持有的就是`最后一个节点`,只需要在当前节点上做些处理就可以添加新节点,时间复杂度为`O(1)` --- 0912学习 0911学习