[编程基础]异步,并发,协程原理

linux操作系统在设计上将虚拟空间划分为用户空间和内核空间,两者做了隔离是相互独立的,用户空间给应用程序使用,内核空间给内核使用。

1。异步

应用程序和内核

内核具有最高权限,可以访问受保护的内存空间,可以访问底层的硬件设备。而这些是应用程序所不具备的,但应用程序可以通过调用内核提供的接口来间接访问或操作。所谓的常见的IO模型就是基于应用程序和内核之间的交互所提出来的。以一次网络IO请求过程中的read操作为例,请求数据会先拷贝到系统内核的缓冲区(内核空间),再从操作系统的内核缓冲区拷贝到应用程序的的地址空间(用户空间)。而从内核空间将数据拷贝到用户空间的过程中,会经历两个阶段:
- 等待数据准备
- 拷贝数据
也正是因为有了这两个阶段,才提出来各种网络I/O模型。

同步和异步

同步:在发出一个同步调用时,在没有得到结果之前,该调用就不返回
异步:在发出一个异步调用后,调用者不会立刻得到结果,该调用就返回了

同步和异步的概念描述的是应用程序与内核的交互方式,同步是指应用程序发起I/O请求后需要等待或者轮询内核I/O操作完成后才能继续执行;而异步是指应用程序发起I/O请求后仍继续执行,当内核I/O操作完成后会通知应用程序,或者调用应用程序注册的回调函数。就是函数执行来同步操作,那后面的程序就在这个同步操作没有结束,是不会再执行的。执行来异步操作,就是不管你操作有没有结束,函数后续的都会继续执行。

阻塞和非阻塞

阻塞调用:是指调用结果返回之前,调用者会进入阻塞状态等待。只有在得到结果之后才会返回
非阻塞调用:是指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。

阻塞和非阻塞的概念描述的是应用程序调用内核IO操作的方式,阻塞是指I/O操作需要彻底完成后才返回用户空间,而非阻塞是指I/O操作被调用后立刻返回用户一个状态值,无需等到I/O操作完成。而这里主要将的是结果的概念。如果阻塞操作没有完成,就任何返回值都没有。非阻塞的话,不管有没有完成,都会给一个返回值的。
常见的网络I/O模型大概四种
1. 同步阻塞IO(Blocking IO)
2. 同步非阻塞IO(Non-blocking IO)
3. IO多路复用(IO Multiplexing)
4. 异步IO(Asynchronous IO)

IO多路复用

多路I/O复用模型是利用select,poll,epll可以同时监察多个流的I/O事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll是指轮询那些真正发出流事件的流),并且只依次顺序的处理就绪的流,这种做法就避免流大量的无用操作。这里“多路”指的是多个网络连接,“复用”指的是复用同一线程。采用多路I/O复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗)。IO多路复用用的是异步阻塞。
#二,并发
并发,在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。
并发和并行但区别:
- 并发(concurrency):逻辑上具备同时处理多个任务但能力。
- 并行(parallesim):物理上在同一时刻执行多个并发任务,依赖多核处理器等物理设备。
多线程或多进程是并行的基本条件但是单线程也可用到协程做到并发。通常情况下,用多进程来实现分布式和负载平衡,减轻单进程垃圾回收压力;用多线程抢夺更多的处理器资源;用协程来提高处理器时间片利用率。现代系统中,多核CPU可以同时运行多个不同的进程或者线程。所以并发程序可以是并行的,也可以不是。

三,协程

1,线程模型

在现代计算机结构中,先后提出过两种线程模型:用户级线程(user-level threads)和内核级线程(kernel-level threads)。所谓用户级线程是指:应用程序在操作系统提供的单个控制流的基础上,通过在某些控制点(比如系统调用)上分离出一些虚拟的控制流,从而模拟多个控制流的行为。由于应用程序对指令流的控制能力相对较弱,所以用户级线程之间的切换往往受线程本身行为以及线程控制点选择的影响。线程是否能公平的获得处理器时间取决于这些线程的代码特征。而且,支持用户级线程的应用程序代码很难做到跨平台移植,以及对于多线程模型的透明。用户级线程模型的优势是线程切换效率高,因为它不涉及系统内核模式和用户模式之间的切换;另一个好处是应用程序可以采用适合自己特点的线程选择算法,可以根据应用程序的逻辑来定义线程的优先级,当线程数量很大时,这一优势尤为明显。但是这同样会增加应用程序代码的复杂性。有一些软件包可以减轻程序员的负担。
内核级线程往往指操作系统提供的线程语义,由于操作系统对指令流有完全的操控能力,甚至可以通过硬件中断来强迫一个进程或线程暂停执行,以便把处理器时间移交给其他进程或线程,所以,内核级线程有可能应用各种算法来分配处理器时间。线程可以有优先级,高优先级的线程被优先执行,它们可以抢占正在执行的低优先级线程。在支持线程语义的操作系统中,处理器的时间通常通常是按线程而非进程来分配,因此,系统有必要维护一个全局的线程标,在线程表中记录每个线程的寄存器,状态以及其他一些信息。然后系统在适当的时候挂起一个正在执行的线程,选择一个新的线程在当前处理器上继续执行。由于时间点的执行代码可能分布在操作系统的不同位置,所以在现代操作系统中,线程调度往往比较复杂,其代码通常分布在内核模块的各处
内核级线程的好处是:应用程序无须考虑是否要在适当的时候把控制权交给其他线程,不必担心自己霸占处理器而导致其他线程得不到处理器时间。应用程序只要按照正常的指令流来实现自己的逻辑就好了,内核会妥善地处理好线程之间共享处理器的资源分配问题。然而,这种对应用程序的便利也是有代价的,即,所有的线程切换都是在内核模式下完成的,因此,对于在用户模式下运行的线程来说,一个线程被切换出去,以及下次轮到它的时候再被切换进来,要设计两种模式切换:从用户模式切换到内核模式,在从内核模式切换回用户模式
除了线程切换的开销是一个考虑因素以外,线程的创建和删除也是一个重要的考虑指标。当线程的数量较多时,这部分的开销是相当可观的,虽然线程的创建和删除比起进程要轻量得多,但是一个进程内建立起一个线程的执行环境,数据结构的初始化工作,以及完成与系统环境相关的一些初始化工作,这些负担是不可避免的。另外线程数量较多时,伴随而来的线程切换开销也必然随之增加。所以,当应用程序或者系统进程需要的线程数量可能比较多的时候,通常可采用线程池为一种优化措施。
在支持内核级线程的系统环境中,进程可以容纳多个线程,这导致了多线程程序设计模型,由于多个线程在同一个进程环境中,它们共享了几乎所有的资源,所以线程之间的通信要方便和高效得多,这往往是进程间通信所无法比拟的,但是,这种便利性也很容易使线程之间因同步不正确而导致数据破坏,而且这种错误存在不确定性,因而相对来说难以发现和调试。

2,什么是协同式和抢占式

许多协同式多任务操作系统,也可以看成协程运行系统。说到协同式多任务系统,一个常见的误区是认为协同式调度比抢占式调度“低级”,因为我们所熟悉的操作系统都是从协同式调度过度到抢占式多任务系统的。实际上,调度方式并无高下,完全取决于应用场景。抢占式系统允许操作系统剥夺进程执行权限,抢占控制流,因而天然适合服务器和图像操作系统,因为调度器可以优先保证对用户交互和网络事件的快速相应。协同式调度则等到进程时间片用完或系统调用时转移执行权限,因此适合实时或分时等等对运行时间有保障的系统。
另外,抢占式系统依赖于CPU的硬件支持。因为调度器需要“剥夺”进程的控制权,就意味着调度器需要运行在比普通进程高的权限上,否则任何任何“流氓”进程都可以去剥夺其他进程了。只有CPU支持了执行权限后,抢占式才成为可能。而协同式多任务适用于那些没有处理器权限支持的场景,这些场景包含资源受到限制的嵌入式系统和实时系统。在这些系统中,程序均以协程的方式运行。调度器负责控制流的让出和恢复。通过协程的模型,无需硬件支持,我们就可以在一个“简陋”的处理器上实现一个多任务的系统。

协程的基本概念

“协程”可以理解为纯用户态的线程,其通过协作而不是抢占来进行切换。相对于进程或者线程,协程所有的操作都可以在用户态完成,创建和切换的消耗更低。总的来说,协程为协同任务提供来一个运行时抽象,这种抽象非常适合于协同多任务调度和数据流处理。在现代操作系统和编程语言中,因为用户态线程切换比内核态线程小,协程成为来一种轻量级的多任务模型。
从编程角度上看,协程的思想本质上就是控制流的主动让出和恢复机制,迭代器常被用来实现协程,所以大部分的语言实现的协程都有yield关键字。

协程,进程,线程的特点以及区别

进程

  • 进程是资源分配的最小单位
  • 进程间不共享内存,每个进程拥有自己独立的内存
  • 进程间可以通过信号,信号量,共享内存,通道,队列等来通信
  • 新开进程开销大,并且CPU切换进程成本也大
  • 进程由操作系统调度
  • 多进程方式比多线程更加稳定

线程

  • 线程是进程执行流的最小单位
  • 线程来自于进程,一个进程下可以开多个线程
  • 每个线程都有自己一个栈,不共享栈,但多个线程能共享一个属于进程但堆
  • 线程因为是在一个进程内,所以内存可以共享
  • 线程也是由操作系统调度,线程是CPU调度的最小单位
  • 新开线程开销成本比进程小,CPU切换线程成本也比进程小
  • 某个线程发生致命错误的时候,会导致整个进程崩溃
  • 进程间读写变量存在锁的问题处理起来相对麻烦

协程

  • 对于操作系统来说只有,进程,线程,协程是有应用程序显式调度,非抢占式
  • 协程的执行最终还是要靠线程,应用程序来调度协程选择合适的线程来获取执行权
  • 切换非常快,开销成本低。一般占用栈大小原小于线程,所以可以开更多的协程
  • 协程比线程更轻量级

[编程基础]GC高级算法

在讨论完三大基础算法以后,我们现在来看看这基础算法的结合以后做成的高级算法。其中最具有代表性的就是:分代回收,增量回收,和并行回收,这三种。

1:分代回收

首先,我们来聊聊高级GC算法中最重要也是最常用的一种,即分代回收。由于GC和程序处理的本质是无关的,因此它所消耗的时间越短越好。分代回收的目的,正是为了在程序运行期间,将GC所消耗的时间尽量缩短。分代回收的基本思路,是利用来一般程序所具备的性质,即大部分对象都会在短时间内成为垃圾,而进过一定时间依然存活的对象往往拥有较长的寿命。如果寿命长的对象更容易存活下来,寿命短的对象则会被很快遗弃,那么如果对分配不久,诞生时间较短的“年轻”对象进行重点扫描,应该可以更有效的回收大部分垃圾。

在分代回收中,对象按照生成时间进行分代,刚诞生不久的年轻对象划分为新生代,而存活来较长时间的对象划分为老生代。根据具体实现方式的不同,可能还会划分更多的代。如果上述关于对象寿命的假说成立的话,那么只要对新生代对象进行扫描,就可以回收遗弃对象中很大一部分。像这种只扫描新生代对象的回收操作,被成为小回收。小回收中在扫描的时候如果遇到属于老生代的对象时,就不会对这个对象进行递归扫描了。这样一来,需要扫描的对象数量也会减少。然后把回收一次以后残留的对象划分到老生代中。

分代回收

上图中,任何地方都没有再被引用的对象F将会通过大回收进行回收。

上图(1)中的D被老生代E所引用,如果根据我们上面所有,如果在回收标记的时候,不对老生代中的对象进行递归引用标记操作的话,那D将不被任何对象引用,那将在一次GC中被回收掉。那这样的话,就就出现我们不期望的结果。所以我们需要做一次操作来,实现老生代对新生代的引用,这个操作一个记录,就是上图中的记录集,这个记录集就是对老生代对新生代引用变更的一个记录。在做小回收的时候,这个引用集将作为一个根来对待。

要让分代回收正确的工作,必须使记录集的内容保持更新。因此,在老生代到新生代的引用产生的瞬间,就必须对该引用进行记录,而负责执行这个操作的子程序,需要被嵌入到所有涉及对象更新操作的地方。这种检查程序需要对所有涉及修改对象内容的地方进行保护,因此被成为写屏障。

随着程序的不断运行,老生代区域中的“死亡”对象也在不断增加。为了避免这些“死亡”对象对内存的白白占用,所以需要有时候对所有的区域都进行一次扫描回收,这个操作叫做大回收或者完全回收。分代回收通过减少GC中扫描对象数量,达到减少GC带来平均中断时间的效果。不过由于还是要经历大回收的,所有最大中断时间并没有得到什么改善。

2:增量回收

在对实时性要求很高的程序中,比起缩短GC的平均中断时间,往往更重视缩短GC的最大中断时间。在这些实时性要求很高的程序中,必须能够对GC所产生的中断时间做出预测。在一般的GC算法中,做出这样的保证是不可能的,因为GC产生的中断时间与对象的数量和状态有关。因此,为了维持程序的实时性,不等到GC全部完成,而是将GC划分多个部分逐一执行。这种方式就叫做增量回收。在增量回收中,由于GC的过程是渐进的,在回收过程中程序本身也会继续运行,对象之间的引用关系也可能会发生变化。如果已经完成扫描和标记的对象被修改,对新的对象产生引用,这个新对象就不会被标记,这样明明还被引用的却会被回收。所以在增量回收中也采用了写屏障。当已经被标记的对象的引用发生变化时,通过写屏障会将新被引用的对象作为扫描的起点记录下来。由于增量回收的过程是分布渐进式的,可以将中断时间控制在一定长度之内。另一方面,由于中断操作需要消耗一定的时间,GC消耗的总时间会相应的增加。

3:并行回收

在现在的计算机中,一块芯片搭载多个cpu核心的多核处理器已经是一个司空见惯的东西了。那么在这种环境下,就需要利用多线程来充分发挥多CPU的性能,并行回收正是通过最大限度利用多CPU的处理能力来进行GC操作的一种方式。并行回收的基本原理是:是在原有的程序运行的同时进行GC操作,这一点和增量回收是相似的。因此也会遇到增量回收相同的问题。为了解决这个问题也需要写屏障来对当前的状态信息保持更新。

[编程基础]GC基础算法

基本术语

1.垃圾(Garbage)

就是需要被回收的对象

如果程序可能会直接或者间接地引用一个对象,那么这个对象就被视为“存活”,与之相反的,没有被引用到的就被视为“死亡”。将这些“死亡”对象找出来,然后作为垃圾进行回收,这就是GC的本质。

2.根(root)

就是判断对象是否可被引用的起始点。

至于哪里才是根,不同语言,不同编辑器都有不同的规定,但基本上是将变量和运行栈作为根。

三大基础GC算法

1.标记清除法/标记压缩法

标记清除是最早开发出的GC算法。原理非常简单,首先从根开始将可能被引用的对象用递归的方式进行标记,然后将没有标记到的对象作为垃圾进行回收。

标记清除算法

图中显示了标记清除算法的大致原理,1阶段显示了随着程序运行而分配出一些对象的状态,一个对象对其他的对象引用。2阶段开始执行GC操作,从根开始对可能被引用的对象打上“标记”,大多数情况下,这种标记通过对象内部的标志(flag)来实现。3阶段,被标记的对象所引用的对象也打上标记,一直重复这样的操作,就可以把从根开始可能被间接引用到的对象都打上标记。此阶段为标记阶段。

标记阶段完成时,被标记的对象就被视为“存活”对象,4阶段把所有对象都扫描一遍,将没有被标记的对象进行回收,这一阶段为清除阶段。

在扫描的同时,还需要将存活的标记状态清理掉,以便下一次GC操作做好准备,标记清除的消耗时间和存活对象数与对象总数的总和相关。

标记压缩算法,是标记清除算法的变形,他不是将没被标记的对象清除,而是不断压缩。

2:复制收集算法

标记清除算法有一个很大的问题,在分配大量对象的时候,当存活很少一部分的时候,还是需要对全部对象进行一次扫描,就算是不能在用的也要进行扫描,所需要用到的时间大大超过必要值。复制收集算法则试图克服这一问题,在复制收集算法里,会从根结点开始把被引用和间接引用的对象复制到另外一个空间,然后把原空间给回收。

复制收集

(1)为开始GC时候的内存状态,(2)在原对象空间以外再开辟了一个空间,然后把根引用到的对象(A)给复制过去,(3)把A所引用的对象也给复制过去,递归向下,把所有被根间接引用的对象都复制到新的空间内,而“死亡”对象就还留在旧空间内。(4)回收旧的对象空间,使用新的对象空间。这样就把所有的死亡对象都给释放掉了,留下的还存活的对象。

复制收集对比标记清除中,只有一开始标记的时候是需要递归遍历的,后面标记清除中的扫描所有的对象进行清除这步是不需要的。所有减少了不必要的扫描开销。

但是复制对象的内存开销会比较大,如果存活的对象比较多的情况下,这种算法就十分消耗内存。

还有一个好处就是,关系比较近的对象,可能会放在距离较近的内存空间,可以提高程序运行性能。

3:引用计数法

引用计数方式是GC算法中最简单,也是最容易实现的一种。它的原理:在每个对象中保存该对象的引用计数,当引用发生增减的时候对引用计数进行更新。当引用计数为0的时候,该对象将不被引用,因此可以释放相应的内存空间。

引用计数

图中(1)部分中,记录着所有对象都保存着自己被多少个其他对象进行引用的数量。(2)中对象引用发生变化时,引用计数也跟着变化。当引用b到d的引用失效,d的引用就为0,将被回收,所以d所引用对象的引用计数也将相应的减少。(3)中对所有引用计数为0的对象进行释放。在这个GC过程中,不需要对所有的对象进行扫描。

在这个GC算法中当对象不被引用的瞬间就会被释放。在其他GC算法中,要预测对象是否能被释放是一个很困难的事,但是在引用计数中,当为0就能被释放了。而且这个释放操作是针对每个对象个别执行的,因此对比其他GC算法,中断时间也比较短。

引用计数最大的缺点就是,不能释放被循化引用的对象

如上图所示,三个对象没有被其他对象所引用,但是相互之间循环引用了。因此它们永远都不会被释放,

还有一个问题就是,必须在引用计数发生变化的时候做出正确的增减,如果出现问题,就会出现内存错误,少加了就会释放过早,多加了就会出现内存泄漏。

最后一个缺点,引用计数管理不适合并行处理,如果多个线程同时对引用计数进行更新,引用计数可能会产生不一致的问题。为了避免这种情况的发生,对引用计数的操作必须采用独占的方式来进行。如果引用操作频繁发生,每次都要使用加锁等并发控制机制的话,其开销也是不可小觑的

[unity3D]assetbundle卸载方案

前言

关于ab包的卸载,最重要的一部分就是实例化以后的gameobject还依赖这个ab包的资源导致ab包卸载以后gameobject会出现资源丢失,但是如果不被卸载,内存就会持续增加。现阶段有两个方案1:引用计数,被引用了几次;2:弱引用,销毁gameobject内存回收,引用自动消除。

assetbundle卸载原理

当场景中的object被移除后,unity不会自动卸载相关的asset,所以asset的卸载需要我们自己来调用。Asset卸载清理可以是特定时刻卸载,也能手动强制卸载。

调用方法为Assetbundle.Unload(bool)和Resource.UnLoadUnusedAssets这两个方法。

AssetBundle.Unload(true)

卸载实例化的Asset,如果其中的某些asset还在被object所引用,就会出现丢失情况

通过这种方法卸载,资源会被卸载的很彻底,完全从内存移除即使还在代码中引用,也会被移除。

AssetBundle.UnLoad(false)

切断ab与其asset之间的关联,不会卸载asset,所以引用不会丢失,asset还在内存中,但是切断例联系,所以再次从ab包中实例化asset的时候不会返回已经初始化过的asset,而是重新实例化,就会出现asset的冗余。

Resource.UnloadUnusedAssets

卸载不被引用的asset,如果asset被gameobject引用,不会卸载。

被场景引用,不会被卸载

被代码引用,不会被卸载。

调用的消耗很大,该函数的主要作用是查找并卸载不再使用的资源。

Asset的引用计数卸载方案

主要是当加载asset的资源的时候,加载一次计数+1,卸载一次计数-1,当减到不用当时候就可以unloadAsset了。

在做UI当资源管理当时候,因为当前所用的资源都是这个window窗口的。所以可以用一个资源栈来做。当打开一个窗口当时候,做一个标志位,在这个标志位以上的资源都是这个窗口所使用加载的。当关闭窗口的时候,把这个阶段的资源全部卸载掉。使用栈就需要窗口的打开和关闭是一一对应的。

Asset的弱引用卸载方案

关键词WeakReference。这个类的作用就是可以在GC的时候释放对象,回收内存。当我们把Asset做弱引用,那当产生一次GC的时候,我们就可以调用Resource.UnloadUnusedAssets来回收Asset。这个方法的危险点就是在于调用Resource.UnloadUnusedAssets的时候消耗很大,所以调用的时机得把握好。AssetBundle.UnLoad(false)也可以,但是释放对象得走一次GC,c#的GC自动调用的时间不知道,手动调用的话会使内存一下增加很多。

[周总结]第二次总结

前言

这周过的十分堕落,不知何故,看了一周电影。可能学习的累是一种需要只发去适应的吧。

工作

在这周工作里,写了一个最小堆的实践,定时器。出了一个加密包,和几个渠道包,把bugly接入到了新项目里面,正式的把XUPorterk框架放入项目了,暂时还没有去查看它这个框架是怎么把sdk的包给拷贝到xcode工程里。

学习

本来是需要看3D编程大师的,但是在学到现在的时候,感觉有点厌倦了,可能是光看却不知道怎么应用,导致这样到情况,现在看下来是用c++实现的,但是据说是可以用c#去写。这块需要去看看。但是又想先看shader入门经典的,自己真的是学东西没有目标性的,任何东西都想知道,但是不去专注的话,应该是学一般,然后不用,接着就忘记了。

终结

这周是一个放纵的一周,我认了,周末也是因为来台风,导致看了两天的电影。电影真的是消磨时间的好帮手,过去的时间既往不咎了,希望接下来的一周能好好的去学东西,有点收获了。不要忘记自己想要的东西。

[周总结]第一次周总结

万事开头难,坚持更难。

前言

最近一直在写博客,我戏称为论文。其实很早以前就知道,学到的知识需要用自己的语言说出来,那才是正的学到了。但是因为我比较懒,所以很会找借口,不知道写在哪里啊,没有电脑什么的。自从前段时间,以前用的便宜的vps被封了,才狠下心来,买个稍微好点的wps,和一个域名,来好好做自己的知识积累(此处吐槽公司大佬,让我入了域名这个坑)。

正文

加密

如果是为了不能让玩家读到我们文件的信息的话,其实只需要文件的前面随机加几个一堆字符,然后在读取的时候就舍去这部分的内容,这种简单是很容易破解,但是也是一种防止资源被盗取的最简单的使用。但是为了过包,在公司大佬的提示下,如果做切片验证的话,我就算前面加了一堆一样的但是在切片时候,可读文件还是一样的,照样能找到相识度;所以考虑用aes加密。aes加密是相当于把每个字符修改了。值得注意的是aes的标准是16个字节为一个单位的加密(也可以使用别的标准)。做10轮加密,密钥最小需要的16字节的密钥;还能设置初始化向量为16字节。在加密的时候专业人士提供了一个顾虑,如果我们把密钥明文写在代码里,被别人获取到了,那不就很容易被破解;在考虑到这个顾虑以后,我们想到用伪随机来生成密钥,具体原理没有去详细了解,现在用下来,使用的种子如果是一样的化,那生成出来的随机数也是一样的。所以打算用伪随机生成密钥来加密文件。

渲染

经过一段时间的思考,觉得对于一个3D游戏前端来说,如果不懂渲染那核心知识基本上就很少了,对于逻辑代码来说,如果不涉及很高深的游戏,很难有大的突破。在以前的时候也看过一段时间的渲染基础,向量计算呀,矩阵转换啊,渲染流程啊,这些以前都会,现在都忘记了,再学的时候还是要去查看下书上概念。所以在学东西的时候没有用到,很容易忘记,学以致用,古人诚不欺我。这段时间从头看了坐标转换的知识点。

unity

这个是我现阶段吃饭的工具,但是当我从新去研究的时候,发现自己还是很多东西不懂,很多东西还只是在使用阶段,没有进阶原理。比如前几天研究的实体池,在研究的时候还是有个东西没有明白,在ui不显示时候,设置layer层overdraw还是不能降低。对于这个引擎还是有很多知识点要去了解的。

编程基础

对于GC的了解深入了一点,第一次接触到最小堆的概念。感觉自己对于数据结构的东西还是只在知识层面,还没有到应用层面。链表的翻转也从头写了一下,感觉也不是那么的难。自己的基础补充道路还是很长的。感觉看不到头了。

结语

能开始写总结就是一个好的开始,希望自己能继续坚持下去。路漫漫,总有一天能让我走完的。

[编程基础]最小/大堆的实现

定义:1:一个完全二叉树,可以是空

2:子结点不能比父结点大/小

3:每一个结点的子树也是一个堆

知识点

1:二进制左移,右移。

2:完全二叉树,因为对应的树结点都是有两个叶结点。1:3,4;2:5,6;3:7,8;4:9,10,所以子结点的父结点都是除2得。对应的数字为当前数字-1右移一位。

(i - 1) >> 1;//找到i的跟结点
tmp << 1, (tmp << 1) + 1;//为tmp的左右子树

3:比较二叉树的大小。先从右子树对比树结点,如果大或者小,就和调换值;再用左子树和树结点比较。因为我们存值的时候是用从左到右这样存的。

4:添加值。添加到堆的最后,然后开始向上比较。

5:获取值。最大值or最小值,已经在root结点上,所以取走最上面的值就可以来;再把最后到值放到最顶部开始比较。

[unity3D]关于设置物体父节点和设置物体显示

最近在做系统的gameobject池管理,所以需要研究一下物体不可见,和设置parent的最优搭配

物体不可见方案

1:可移动物体位置到摄像机可照射范围之外;2:设置物体大小到0;3:禁用component组件;4:禁用gameobject;5:设置layer层级为摄像机不可见层级

测试数据
2D界面设置大小
2D界面设置layer层级

测试下来发现禁用layer为最优化的方案。

此测试为3D摄像机的测试数据,只测试了cpu计算耗时,此耗时为unity内部实现方法的耗时,即函数耗时。

缩小scale在3D摄像机里是不会减少DC的但是在2D下,是可以减少DC的。猜测的区别为:在2D摄像机下,会重组mesh,当scale为0的时候会被剔除出mesh渲染中,这个为2D的实现方法,后续整理这块数据。在3D摄像机下的时候,不管scale是否为0,这个物体的矩阵还是存在的,所以还是要进行矩阵转换和渲染。

在2D下移走坐标是不会减少DC的,但是加一个2Dmask就可以。猜测为:UI和2Dmask的矩阵求交集,超出就不渲染。

后续补充:

在设置layer的时候,你如果设置物体父节点的layer但是子节点是不管用的,所以你只能循环子节点来设置,在这个时候产生了循环遍历,消耗就变成指数级别的上升。最优选择为设置大小。还产生了GC。

设置parent方案

设置parent为null,和设置parent为具体的gameobject;Setparent第二个参数worldPositionStays为true和false,当这个值设置成false的时候是不会修改localposition的坐标,这个就相当于,直接把物体的世界坐标给修改了。

测试结果为设置parent不能为null,为null是最耗时的, worldPositionStays可设置为false,这样最优的,设置成true的时候设置parent的耗时和设置parent的层级有关系,如果层级最深的话耗时最长

方案选择

根据以上的测试结果,方案为设置物体的parent为最浅根节点worldPositionStays设置为false,以设置layer的显示层级来决定是否渲染。

拓展发散

当需要隐藏一个物体不被摄像机渲染的时候,我们可以选择设置layer。还有一个测试数据为当获取物体的localposition的时候比获取物体的position要快将近一倍。所以unity保存物体的坐标系是否为localpostion和一个世界坐标的转换矩阵,然后position为使用的时候才去计算。还有设置parent为null的时候慢了超过1/9,设置为null的时候是否重新生成了一份场景物体树的根节点。这些都需要以后好好研究一下。最后一句,不考虑数量级的优化都是假的。