[编程基础]c#GC简介

GC的前世今生

GC的概念并非才诞生不久,早在1958年,由大名鼎鼎的图灵奖得主john mccarthy所实现的lisp语言就已经提供了GC的功能,这是GC的第一次出现。lisp的程序员认为内存管理太重要的,所以不能由程序员自己来管理。
但是后来的日子里lisp却没有成气候,采用内存手动管理的语言占据了上风,以c为代表。出于同样的理由,不同的人却又不同的看法,c程序员也认为内存管理太重要的,所以不能由系统来管理,并且讥笑lisp程序慢如乌龟的运行速度。的确,在那个对每一个byte都要精心计算的年代GC的速度和对系统资源的大量占用使很多人都无法接受。而后,1984年由dave ungar开发的smalltalk语言第一次采用了Generational garbage collection的技术,但是amalltalk也没有得到十分广泛的应用。
直到20世纪90年代中期GC才以主角的身份登上历史的舞台,这不得不归功于Java的进步,今日的GC已非吴下阿蒙。Java采用VM(virtual machine)机制,由VM来管理程序的运行当然也包括对GC管理。90年代末期.net出现了,.net采用了和Java类似由CLR(Common Language Runtime)来管理。这两大阵营的出现将人们引入了以虚拟平台为基础的开发时代,GC也在这个时候越来越得到大众的关注。
为什么要使用GC呢?也可以说为什么要使用内存自动管理?有以下一个原因:
1. 提高了软件开发的抽象度;
2. 程序员可以将精力集中在实际的问题上而不用分心来管理内存的问题;
3. 可以使用模块的接口更加的清晰,减少模块间的耦合;
4. 大大减少了内存人为管理不当所带来的bug;
5. 使内存管理更加高效。
总的来说就是GC可以使程序员可以从复杂的内存问题中摆脱出来,从而提高了软件开发的速度,质量和安全性。

什么是GC

GC就是垃圾收集,这里仅就内存而言。Garbage Collector以应用程序的root为基础,遍历应用程序在Heap上动态分配的所有对象,通过识别它们是否被引用来确定哪些对象是已经死亡的,哪些仍然需要被使用。已经不再被引用程序root引用或者别的对象所引用的对象就是已经死亡的对象,即所谓的垃圾,需要被回收。

1,Mark-Compact 标记压缩算法

简单的吧.NET的GC算法看作Mark-Compact算法。阶段1:Mark—Sweep 标记清除阶段,先假设heap中所有对象都可以回收,然后找出不能回收的对象,给这些对象打上标记,最后heap中没有打标记的对象都是可以被回收的;阶段2:Compact 压缩阶段,对象回收之后heap内存空间变得不连续,在heap中移动这些对象,使它们重新从heap基地址开始连续排列,提高局部性。
<a href="https://i.loli.net/2019/10/08/TLm9Uy5Kaq2frvF.png"><img src="https://i.loli.net/2019/10/08/TLm9Uy5Kaq2frvF.png" alt="" /></a>
Heap内存经过回收,压缩之后,可以继续采用前面的heap内存分配方法,即仅用一个指针记录heap分配的起始地址就可以。主要处理步骤:将线程挂起->确定roots->创建reachable objects graph->对象回收->heap压缩->指针修复。可以这样理解roots:heap中对象的引用关系错综复杂(交叉引用,循环引用),形成复杂的graph,roots是CLR在heap之外可以找到的各种入口。
GC搜索roots的地方包括全局对象,静态变量,局部对象,函数调用参数,当前CPU寄存器中的对象指针等。主要可以归为两类:已经初始化了的静态变量,线程仍在使用的对象。Reachable objects:指根据对象引用关系,从roots出发可以到达的对象。从roots出发可以创建reachable objects graph,剩余对象即为unreachable,可以被回收。
发现无法访问的对象时,它就使用内存复制功能来压缩内存中可以访问的对象,释放分配给不可访问对象的地址空间块。
指针修复是因为compact过程中移动了heap对象,对象地址发生变化,需要修复所有引用指针,包括stack,cpu register中的指针以及heap中其他对象的引用指针。传给COM+的托管对象也会成为root,并且具有一个引用计数以兼容COM+的内存管理机制,引用计数为0时,这些对象才可能成为被回收对象。Pinned objects指分配之后不能移动的对象,GC在指针修复时无法修改非托管代码中的引用指针。pinned objects会导致heap出现碎片。

2,托管堆

在垃圾回收器由CLR初始化以后,它会分配一段内存用于存储和管理对象。此内存为托管堆。
每个托管进程都有一个托管堆,进程中的所有线程都在同一个托管堆上为对象分配内存。
堆上分配的对象越小,垃圾回收器必须执行的工作就越少。分配对象时,请勿使用超出你需求的舍入值。
当触发垃圾回收时,垃圾回收器将回收由死对象占用的内存。回收进程会对活动对象进行压缩,以便将它们一起移动,并移除死空间,从而使堆更小一些。这将确保一起分配的对象都位于托管堆上,从而保留它们的局部性。
GC分配两个托管堆:一个用于小型对象(小型对象堆或SOH),一个用于大型对象(大型对象堆或LOH)
对象小于85000字节分配到小型SOH上,大于则分配到LOH上。
触发垃圾回收后,GC将寻找存在的对象并将它们压缩。但是由于压缩成本很高,GC会扫过LOH,列出没有被清除的对象列表以供以后重新使用,从而满足大型对象的分配请求。相邻的被清除对象将组成一个自由对象。

3,代数

虽然对象的生命周期因应用而异,但对于大多数应用来说,大部分的对象在创建不久即变成垃圾。因此针对不同age的对象也就不难理解了。
在.NET垃圾回收器中分三代,0,1,2。
小对象始终在第0代中进行分配,或者根据它们的生存期,可能会提升为第一代或者第二代。
大型对象始终在第二代中分配。
回收一代时,同时也会回收它前面所有代。
用户代码分配只能在SOH中的第0代或LOH中分配。只有GC可以在第一代和第二代中分配对象。
在mono中为写屏障来记录老代对新代的引用。

4,Finalization Queue和Freachable Queue

这两个队列和.NET对象所提供的Finalize方法有关。这两个队列并不用于存储真正的对象。而是存储一组指向对象的指针。当程序中使用new操作符在Managed Heap上分配空间时,GC会对其进行分析,如果该对象含有Finalize方法则在Finalization Queue中添加一个指向该对象的指针。
在GC被启动以后,经过Mark阶段分辨出哪些是垃圾。再在垃圾中搜索,如果发现垃圾中有被Finalization Queue中的指针所指向的对象。则把这个对象从垃圾中分离出来,并将指向它的指针移动到Freachable Queue中。这个过程被称为对象的复生(Resurrecction)。那为什么要救活它呢?因为这个对象的Finalize方法还没有被执行,所以还不能死去。当Freachable Queue中的指针指向的对象执行完Finalize方法以后,这个指针就会从队列中剔除,这个对象就能在下一次GC中被回收了。
.NET Framework的System.GC类提供了控制Finalize的两个方法,ReRegisterForFinalize和SuppressFinalize。前者是请求系统完成对象的Finalize方法,后者是请求系统不要完成对象的Finalize方法。ReRegisterForFinalize方法其实就是将指向对抗的指针重新添加到Finalization Queue中。如果在对象的Finalize方法中调用ReRegisterForFinzlize方法,这样就会形成一个在堆上永远不会死去的对象。

5,弱引用

如果应用程序代码访问一个正由该程序使用的对象时,垃圾回收器就不能回收该对象,那么就认为这个应用程序对该对象有强引用。
弱引用允许应用程序访问对象,同时也允许垃圾回收器收集相应的对象。如果不存在强引用,则弱引用的作用时间只限于收集对象前的一个不确定时间段。使用弱引用时,应用程序仍可以对该对象进行强引用,这样做可防止该对象被收集。但始终存在这样的风险:垃圾回收器在重新建立强引用之前先处理改对象。
若要对某对象建立弱引用,请使用要跟踪的对象实例创建WeakRefernce。然后将Target属性设置为该对象,将该对象的原始引用设置为null。

 aa a = new aa();
            a.a = 2;
            WeakReference c = new WeakReference(a);
            a = null;
            GC.Collect();
            Console.WriteLine(c.Target != null);
            Console.WriteLine(c.IsAlive);
            Console.WriteLine(c.Target);
/*
延时执行if判断的时候,c.Target是为null的
 var t1 = Task.Run(async delegate { await Task.Delay(5000); if (c.IsAlive)
                {
                }
            });
*/
/*
这段代码如果执行的话,在上一个GC期间c.Target是不为Null的,但是如果不执行的话,c.Target是null
  if (c.IsAlive)
                {
            }
*/
            GC.Collect();
/*
在GC调用完以后c.target为null。
*/
          Console.WriteLine(c.IsAlive);
            Console.ReadKey();

WeakRefernce也是一个引用对象,当这个对象也在被引用的时候,期内部的target是不会被回收的。

短弱引用和长弱引用

  • 短弱引用:
    垃圾回收功能回收对象后,短弱引用的目标将会变成null。弱引用本身是托管对象,与其他任何托管对象一样需要经过垃圾回收。
  • 长弱引用:
    在对象的Finalize方法已调用后,长弱引用获得保留。这样,便可以重新创建该对象,但该对象仍保持不可预知的状态,仍然可能在下一次的GC中被回收掉。若要使用长弱引用,请在WeakReference构造函数中指定true。
    如果对象类型不包含Finalize方法,或者Finalize方法没有被执行(调用了GC.SuppressFinalize();),那么将被应用为短弱引用。弱引用只在目标被收集前有效,运行终结器后可以随时收集目标。
    如果要建立强引用并重新使用对象,请将WeakReference的Target属性强行转换为对象类型。使用前请先判断Target是否为null。

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

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切换线程成本也比进程小
  • 某个线程发生致命错误的时候,会导致整个进程崩溃
  • 进程间读写变量存在锁的问题处理起来相对麻烦

协程

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

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

定义: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结点上,所以取走最上面的值就可以来;再把最后到值放到最顶部开始比较。

工作问题

涉及的专业知识:1:编程语言;2:unity基础框架;3:渲染知识。

编程语言:

问题1:

class entrusttest
    {
        delegate void hello();
        static List<hello> a = new List<hello>();
        public entrusttest()
        {
            for (int i = 0; i < 10; i++)
            {
                a.Add(delegate
                {
                    Console.WriteLine(i);
                });
            }
            for(int i = 0; i < a.Count; i++)
            {
                a[i]();
            }
        }
    }

问题为打印出来的值为什么。答案为:全部都是10;
根据大佬的说法:这个为闭包概念,i会变成全局的唯一变量。
学习重点:lamda表达式
在c#基础上看到过这个概念,但是实际应用就没用过。

问题2

class entrusttest1
    {
        delegate void hello();
        public entrusttest1()
        {
            abc();
        }
        public void abc()
        {
            hello a;
            a = b;
            a += c;
            a += d;
            a();
        }
        public void b()
        {
            Console.WriteLine("a");
        }
        public void c()
        {
            Console.WriteLine("c");
        }
        public static void d()
        {
            Console.WriteLine("dddd");
        }
    }

这里委托+=是否可以为静态函数
答案是:可以
拓展:在构造函数里不能直接执行委托属性。必须包一层方法。构造函数和普通函数有什么区别呢?如果都是线性执行的话那有啥区别。
学习重点:函数。

问题3

class box
    {
        long a = 1000000000L;
        public box()
        {
            object c = a;
            int d = (int)c;
        }
    }

这里是否会抛出异常。
答案是:会,装箱的类型和拆箱的类型不匹配
学习重点:拆装箱,值类型和引用类型
拓展:值类型在装箱的时候怎么把栈上的内存放到堆上。内存变化。

问题4

soket的实现
答案:
学习重点:
拓展:

问题5

hashtable是怎么存内容的,又能存对象,也能存基础类型
答案:存对象的时候是保存对象的指针,keys是把指针再存到一个数组里(未验证)
学习重点:数据结构
拓展:

问题6

UI系统框架改怎么实现
答案:根据大佬理解,应该是做一个UI栈,先进后出。
学习重点:数据结构知识,设计模式
拓展:

问题7

找到字符串中第一个出现两次的字符
答案:

string str = "dafdfureqwnfahfgi";
        Hashtable c;
        public stringtext()
        {
            c = new Hashtable();
            char[] b = str.ToCharArray();
            for(int i = 0; i < b.Length; i++)
            {
                if (c.Contains(Convert.ToInt32( b[i])))
                {
                    Console.WriteLine(b[i].ToString());
                    return;
                }
                else
                {
                    c.Add(Convert.ToInt32(b[i]), Convert.ToInt32(b[i]));
                }
            }

        }

大佬给了一个新的解答方案,用二进制做匹配,保存进制位,如果位一样,就相当于前面有相等的值:

 class stringtext1
    {
        int n = 0;
        string str = "dfdafaffasrwer";
        public stringtext1()
        {
            char[] b = str.ToCharArray();
            for (int i = 0; i < b.Length; i++)
            {
                int offset = Convert.ToInt32(b[i])-96;
                int c = 1 << offset;
                n = c^n;
                int r = n & c;
                if (r == 0)
                {
                    Console.WriteLine(b[i].ToString());
                    return;
                }
            }
        }

    }

学习重点:字符比较转换
拓展:是否还有效率更高的解答

问题8

c#的GC是怎么实现的

答案:

学习重点

拓展:引用类型的回收,值类型的回收

问题9

lua的GC实现原理

答案:

学习重点:

拓展:

问题10

lua元表的概念

答案:

学习重点:

拓展:

问题11

事件,委托有什么不同

答案:

学习重点:

拓展

unity框架知识

问题1

unity的渲染顺序
答案:先camera depth的值,越小越优先;sorting layer层,越小越优先;sorting order 越小越优先;renderqueue越小越优先;z轴大小;创建顺序。
学习重点:渲染顺序。
拓展:renderqueue的值2500为分界线,renderqueue的值相等,大2500则远到近,小于则近到远。个属性分别实在哪些组件上。

问题2

unity的遮挡剔除
答案:
学习重点:遮挡剔除
拓展:剔除算法放在哪一步做。

问题3

unity怎么在label上加描边
答案:
学习重点:渲染label的方法
拓展:在哪步做渲染加描边最优

问题4

unity的协程是怎么实现暂停后继续执行当前行
答案:协程实现了一个迭代器
学习重点:协程实现
拓展:C#的迭代器

问题5

一个物体被渲染到看到,经历里几次坐标转换
答案:物体坐标到世界坐标,世界坐标到摄像机坐标,摄像机坐标到裁剪空间,裁剪坐标到屏幕空间,屏幕空间到视口空间。
学习重点:坐标转换
拓展:坐标转换计算

问题6

unity系统方法调用顺序
答案:Awake>OnEnable>Start>FixedUpdate>Update>LateUpdate>OnGUI>OnDestroy
学习重点:各种函数的调用时机
拓展:系统全部函数调用顺序和时机

问题7

recttransform的介绍

答案:

学习重点

拓展

问题8

scrollview加3D特效

答案:

学习重点

拓展

问题9

聊天的图文混搭

答案:

学习重点

拓展

渲染

问题1

光栅化做了什么
答案:
学习重点:
拓展:

问题2