Unity笔记

一、随意记

Image和RawImage的区别

  • Image比rawImage更消耗性能
  • Image只能使用Sprite属性的图片,但是RawImage是各种图片设置都可以使用
  • Image适合放一些有操作的图片,裁剪平铺旋转什么的,针对Image Type属性
  • RawImage就放单独展示的图片就可以了,性能就会比Image好很多

Resources和AssetBundle使用区别

Resources

  • Resources属于内部加载,打包出来后的文件是不存在的,不利于更新
  • Resources会使打包出来程序变大
  • 一般内存占用不大,全局使用且变化不大的物体可以使用Resources加载

AssetBundle

  • AssetBundle属于外部加载,方便热更新,打包出来程序会小很多

线程、进程、协程(包括lua协程的实现方式和unity协程的实现方式)

对三者的介绍

  • 前情提要:计算机的核心是CPU,它承担了所有的计算任务,操作系统是计算机的管理者,它负责任务的调度,资源的分配和管理,统领整个计算机硬件;应用程序是具有某种功能的程序,程序是运行于操作系统之上的。
  • 进程:是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。每个进程都有自己的独立内存空间,不同进程通过进程间通信来同行。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。
  • 线程:线程是进程的一个实体,是cpu调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(例如程序计数器,一组寄存器和栈),但是它可以与同属一个进程的其他线程共享进程中所拥有的全部资源。线程间通信主要通过共享内存,上下文切换很快,资源开销较少。相比进程不够稳定容易丢失数据。
  • 协程:协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,回复到先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。

对三者的比较

进程和线程的比较

线程是指进程内的一个执行单元,也是进程内可调度的实体。区别如下:

  • 地址空间:线程是进程内的一个执行单元,进程内至少有一个线程,他们共享进程的地址空间,而进程有自己独立的地址空间。
  • 资源拥有:进程是资源分配和拥有的单位,同一个进程内的线程共享进程的资源
  • 线程是处理器调度的基本单位,进程不是
  • 二者都可以并发执行
  • 每个独立的线程有一个程序运行的入口、顺序执行的序列和程序的出口,但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
线程和协程的比较
  • 一个线程可以有多个协程,一个进程也可以有多个协程。
  • 协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态。

协程的实现方式

Unity协程的实现方式

Unity在每一帧(Frame)都会去处理对象上的协程,主要是在LateUpdate后去处理协程(检验协程的条件是否满足)

Lua协程的实现方式

协程是对lua线程的封装,一个协程有自己的栈和调用链,所有协程之间的协作要考虑栈之间的数据交换,发生错误之后的栈恢复等等。

lua协程的流程:
  • 创建协程:
    coroutine.create函数,调用的api是luaB_cocreate(另外一种是coroutine.wrap,对应的api是luaB_cowrap,是对luaB_cocreate的一层封装,区别 在于协程出错返回,该函数会继续传播这个错误,如果成功则原样返回协程结果,不会在最前面加一个bool值表示成功失败。)
    代码:
static int luaB_cocreate(lua_State *L){
	lua_State *NL;         
	lua_checktype(L,1,LUA_TFUNCTION);    //第一个参数一定是一个函数对象
	NL = lua_newthread(L);  //新建线程
	lua_pushvalue(L,1);  //将函数移到顶部
	lua_xmove(L,NL,1);  将函数对象转移到NL
	return 1;
	///解释:
	///L为原来的线程,NL是新建的线程,把L栈顶的函数转移给NL,然后把NL压入L的栈顶返回。现在Nl栈顶是一个函数对象,此即为协程的主函数。创建完毕则等着后面的启动(resume) 
}
  • 协程启动:
    创建完协程,通过coroutine.resume(co,···)启动协程,c层调用的事luaB_coresume:
  • 协程的让出:
    调用coroutine.yield(···)让出当前的协程的执
  • 协程的恢复
    调用resume函数,但是执行的代码是另外一部分

动画

1、动画状态机

重点注意:

  • Animator的每个状态都可以挂载脚本,创建脚本,继承于StateMachineBehaviour类,用于检测状态机中动画切片(Anamation)的运行状态

2、行为树

参考网址

定义

行为树是一个包含逻辑节点和行为节点的树结构,每次需要找出一个行为的时候,会从树的根节点出发,遍历各个节点,找出第一个和当前数据相符合的行为。

工作流

行为树由多种不同类型的节点构成,它们都拥有一个共同的核心功能,即它们会返回三种状态中的一个作为结果。这三种状态分别是:

  • 成功-Success;
  • 失败-Failure;
  • 运行中-Running;
    running这个状态非常重要,它可以让一个节点持续运行一段时间来维持某些行为。
比如一个“walk(行走)”的节点会在计算寻路和让角色保持行走的过程中持续返回“Running”来让角色保持这一状态。
如果寻路因为某些原因失败,或是除了某些状况让行走的行为不得不中止,那么这个节点会返回“Failure”来告诉它的父节点;
如果这个角色走到了指定的目的地,那么节点返回“Success”来表示这个行走的指令已经成功完成。

一共有三种节点类型,它们分别是:

  • 组合节点-Composite:组合节点通常可以拥有一个或更多的子节点。这些子节点会按照一定的次序或是随机地执行,并会根据执行的结果向父节点返回“Success”、“Failure”,或是在未执行完毕时“Running”这样的结果值。
    最常用的组合节点是 Sequence(次序节点),它很简单地按照固定的次序运行子节点,任何一个子节点返回 Failure,则这个组合节点向它的父节点返回 Failure;当所有子节点都返回 Success 时,这个组合节点返回 Success。
  • 修饰节点-Decorator:修饰节点也可以拥有子节点,但是不同于组合节点,它只能拥有一个子节点。取决于修饰节点的类型,它的功能要么是修改子节点返回的结果、终止子节点,或是重复执行子节点等等。
    一个比较常见的修饰节点的例子是 Inverter(逆变节点),它可以将子节点的结果倒转,比如子节点返回了 Failure,则这个修饰节点会向上返回 Success,以此类推。
  • 叶节点-Leaf:叶节点是最低层的节点,它们不会拥有子节点。叶节点是最强大的节点类型,它们是真正让你的树做具体事情的基础元素。通过与组合节点和修饰节点的配合,再加上你自己对叶节点功能的定义,你可以实现非常复杂的、智能的行为逻辑。
改进方法

分等级的行为树。有一个做决策的行为树A,和一个按照命令执行的行为树B。A根据游戏世界的情况做出决策,然后将命令放到Database里,然后B根据命令做出动作。由于两个行为树都放在一个Game Object里,所以Database是A、B共享的。通常,决策者A并不会每一帧都做出决策,而是设定一个冷却时间。

好处
  • 让决策逻辑和执行逻辑分离。面对同样的决策,不同Game Object可能有不同的执行方法。
  • 玩家控制的角色和AI控制的角色可以分享同一个执行逻辑——只需要负责玩家控制的代码将命令存放到Database里面供执行逻辑使用就可以了。

四元数和万向锁

参考文章

渲染

剧情编辑器、技能编辑器

flux插件

内存对齐

参考文章

定义:编译器为程序中的每个“数据单元”安排在适当的位置上
内存对齐规则:

1.对于结构的各个成员,第一个成员位于偏移为0的位置,以后的每个数据成员的偏移量必须是 min(#pragma pack()指定的数,这个数据成员的自身长度)的倍数

2.在所有的数据成员完成各自对齐之后,结构或联合体本身也要进行对齐,对齐将按照 #pragram pack 指定的数值和结构或者联合体最大数据成员长度中比较小的那个 也就是 min(#pragram pack() , 长度最长的数据成员);

#pragram pack(n) 表示的是设置n字节对齐,vc6默认的是8

内存对齐作用

1.平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常

2.硬件原因:经过内存对齐之后,CPU的内存访问速度大大提升。

UGUI源码解析

参考地址

  • 只要添加Canvas将会打断和之前元素DrawCall的合并,每个Canvas都会开始一个全新的DrawCall。当Canvas需要重绘的时候会调用SendWillRenderCanvases()方法
  • Rebuild()方法就是 UpdateGeometry(更新几何网格)和 UpdateMaterial (更新材质)
  • 顶点数据准备完毕后会调用canvasRenderer.SetMesh()方法来提交。SetMesh()方法最终在C++中实现,毕竟由于UI的元素很多,同时参与合并顶点的信息也会很多,在C++中实现效率会更好
  • Profiler中看到Canvas.SendWillRenderCanvases()效率过低就是因为参数Rebuild()的元素过多
  • 如果某个UI需要重建,首先需要将它加入“待重建队列”,等到下一次Unity系统回调Canvas.SendWillRenderCanvases()方法时一起Rebuild()
  • 由于元素的改变可分为布局变化、顶点变化、材质变化,所以分别提供了三个方法SetLayoutDirty();SetVerticesDirty();SetMaterialDirty();供选择。为什么UI发生变化一定要加入待重建队列中呢?其实这个不难想象,一个UI界面同一帧可能有N个对象发生变化,任意一个发生变化都需要重建UI那么肯定会卡死。所以我们先把需要重建的UI加入队列,等待一个统一的时机来合并。
  • UI的网格我们都已经合并到了相同Mesh中,还需要保证贴图、材质、Shader相同才能真正合并成一个DrawCall。UGUI开发时使用的是Sprite对象,其实Sprite对象只是在Texture上又封装的一层数据结构,它记录的是Sprite大小以及九宫格的区域,还有Sprite保存在哪个Atals中。如果很多界面Prefab引用了这个Sprite,它只是通过GUID进行了关联,它并不会影响到已经在Prefab中保存的Sprite
  • 相同图集中的Sprite是如何合并DrawCall的,从原理上来讲,每个Mesh都需要给顶点设置UV信息,也就是说我们只需要将图集上的某个区域一一抠出来贴到Mesh正确的区域即可。如下代码所示,只要观察GenerateSimpleSprite()方法,UGUI通过Sprites.DataUtility.GetOuterUV(activeSprite)方法将当前待显示的Sprite的UV信息取出来,通过vh.AddVert()和vh.AddTriangle()来填充Mesh信息。
  • UGUI的Text就是位图字体,先通过TTF字体将字体形状生成在位图中,接着就是将正确的UV设置给字体的Mesh,这和前面介绍的Image组件几乎一样了。如下代码所示,首先需要根据文本的区域、字体、填充文字调用GetGenerationSettings()创建文本生成器,顶点、uv信息都会被填充好,由于每个文本都是一个Quad,所以还需要设置它们的位置
  • 前面我们介绍的Image和Text 合并网格都用到了VertexHelper类,它只是个普通的类对象,里面只保存了生成Mesh的基本信息并非Mesh对象,最后通过这些基本信息就可以生成Mesh网格了
  • UGUI的layout布局功能效率不高,因为它得计算出每个子对象的区域,在GetChildSizes()方法中拿到每个元素的区域。而最核心的计算在LayoutUtility. GetLayoutProperty()方法中,把每个实现ILayoutElement接口的对象的信息取出来。由于Image和Text都实现了ILayoutElement接口,所以LayoutGroup下的Image和Text元素会自动布局。Layout还有Min Wdith和Flexible Width可设置最小宽高和弹性宽高,这都需要进行额外的计算产生额外的开销。
  • Content Size Fitter、VerticalLayoutGroup、HorizontalLayoutGroup、 AspectRatioFitter、GridLayoutGroup组件效率是很低的,它们势必会导致所有元素的Rebuild()执行两次。
    1、界面第一次打开需要进行第一次Rebuild()
    2、Layout组件要算位置或者大小会强制再执行一次Rebuild()
  • Mask(遮罩组件):由于裁切需要同时裁切图片和文本,所以Image和Text都会派生自MaskableGraphic。如果要让Mask节点下的元素裁切,那么它需要占一个DrawCall,因为这些元素需要一个新的Shader参数来渲染。Image对象在进行Rebuild()时,UpdateMaterial()方法中会获取需要渲染的材质,并且判断当前对象的组件是否有继承IMaterialModifier接口,如果有那么它就是绑定了Mask脚本,接着调用GetModifiedMaterial方法修改材质上Shader的参数
  • Mask的原理就是利用了StencilBuffer(模板缓冲),它里面记录了一个ID,被裁切元素也有StencilBuffer(模板缓冲)的ID,并且和Mask里的比较,相同才会被渲染。因为模板缓冲可以提供模板的区域,也就是前面设置的圆形图片,所以最终会将元素裁切到这个圆心图片中。 在Mask外面放一个普通的图片,默认情况下Stencil Ref的值是0,所以它不会被裁切,永远会显示出来。
  • RectMask2D会将RectTransform的区域作为_ClipRect传入Shader中,并且激活UNITY_UI_CLIP_RECT的Keywords。Stencil Ref 的值是0 表示它并没有使用模板缓冲比较,如果只是矩形裁切,RectMask2D并且它不需要一个无效的渲染用于模板比较,所以RectMask2D的效率会比Mask要高。在Shader的Frag处理像素中,被裁切掉的区域是通过UnityGet2DClipping()将Color.a变成了透明。
  • 点击事件: UGUI的事件本质上就是发送射线,由于UI的操作有一些复杂的手势,所以UGUI帮我们又封装了一层。创建任意UI时都会自动创建EventSystem对象,并且绑定EventSystem.cs和StandaloneInputModule.cs。EventSystem会将该对象绑定的所有InputModule脚本收集起来保存在SystemInputModules对象中。
  • UI是如何确定出点击到那个元素上的:在EventSystem中遍历所有module.Raycast()方法
  • 如何判断点击的事件:首先遍历Canvas下每一个参与渲染的Graphic对象,如果勾选了raycastTarget并且点击射线与它们相交,此时先存起来。由于多个UI有相交的情况,但由于Mesh都合批了,第一个与射线相交的对象是没有意义的,但是我们只需要响应在最上面的UI元素,这里只能根据depth来做个排序了,找到最上面的UI元素,最后再抛出正确的点击事件。所以,GraphicRaycaster组件越多越卡,raycastTarget勾选的越多越卡

二、Lua相关

lua String的组成

参考文章

typedef union TString{
	L_Umaxalign dummy;   //保证内存对齐提高cpu提取速度
	struct{
		CommonHeader;  // 用于GC类型
		lu_byte reserved;  //是否是保留字
		unsigned int hash; //该字符串的哈希值
		size_t len; //字符串长度
	} tsv;
} TString;

Lua table的组成,如何解决hash冲突

typedef union TKey{
	struct{
		TValuefields;
		int next;  //用于标记链表中下一个节点
	}nk;
	TValue tck;
}Tkey;

typedef struct Node{
	TValue i_val;
	TKey i_key;
}Node;

typedef struct Table{
	CommonHeader;
	lu_byte flags;  //元方法的标记,用于查询table是否包含某个类别的元方法
	lu_byte lsizenode; //表示table的hash部分大小
	unsigned int sizearray; //table的数组部分大小
	TValue *array;   //table的array数组首节点
	Node *node; //table的hash表首节点
	Node *lastfree; //表示table的hash表空闲节点的游标
	struct Table *metatable; //元表
	GCObject *gclist;  //table gc相关参数
} Table;
为了提高table的插入查找效率,在table的设计上,采用了array数组和hashtable(哈希表)两种数据的结合
所以table会将部分整形key作为下标放在数组中,其余的整形key和其他类型的key都放在hash表中。
解决hash冲突的方式
  • 链地址法处理冲突:所有关键字为同义词的节点链接在同一个单链表中。若选定的散列表长度为m,则可将散列表定义为一个由m个头指针数组T[0…m-1].凡是散列地址为i的节点,均插入到以T[i]为头指针的单链表中。T中各分量的初值均应为空指针。在链地址法中,装填因子可以大于1,但一半均取值<=1.
  • 特性:1、查找和插入等同于链地址法复杂度(低)
    2、内存开销近似等同于开放地址法(低)。
  • table采用链地址法的形式处理冲突,但是链表中的额外节点是hash表中的节点,并不需要额外开辟链表节点。

Lua gc相关的api,gc策略,三色标记

参考网址

gc算法分析

定义:采用标记-清除算法,即一次gc分两步:
  • 从根节点看来是遍历gc对象,如果可达,则标记
  • 遍历所有gc对象,清除没有被标记的对象
二色标记法:lua5.1之前采用的算法

最简单的标记-清除算法,缺点是gc的时候不能被打断,所以会严重卡住主流程

三色标记法:lua5.1开始采用的算法
定义:
  • 白色:在gc开始阶段,所有对象颜色都为白色,如果遍历一遍之后,对象还是白色的将会被清除
  • 灰色:灰色用在分步遍历阶段,如果由对象为灰色,那么遍历将不会停止
  • 黑色:确实被引用的对象,将不会被清除,gc完成之后会重置为白色
实现方法:

luajit使用状态机来执行gc算法,共有6种状态:

  • GCSpause:gc开始阶段,初始化一些属性,将一些跟节点(主线程对象,主线程环境对象,全局对象等)push到灰色链表中
  • GCSpropagate:分步进行扫描,每次从灰色链表中pop一个对象,遍历该对象的子对象,例如如果该对象为table,并且value没有设置为week,则会遍历table所有table可达的value,如果value为gc对象且为白色,则会被push到灰色链表中,这一步一直持续到灰色链表为空。
  • GCSatomic:原子操作,因为GCSpropagate是分步的,所以分步过程中可能会有新的对象创建,这时候将再进行一次补充遍历,这遍历时不能被打断的,但因为绝大部分工作被GCSpropagate做了,所以过程会很快。新创建的没有被引用的userdata,如果该userdata自定义了gc元方法,则会加入到全局的userdata链表中,该链表会再最后一步GCSfinalize。
  • GCSsweep:遍历所有全局GC对象,每次遍历40个,如果gc对象为白色,将被释放。
  • GCSfinalize:遍历GSSatomic生产的userdata链表,如果该userdata还存在gc元方法,调用该元方法,每次处理一个。
触发gc的时刻:

1、luajit中有两个判断是否需要gc的宏:

  • gc.togal:代表当前已经申请的内存
  • gc.threshold:代表当前设置gc的阈值
    2、在每个申请内存的地方调用这两个宏,如果gc.togal >= gc.threshold 则申请的所有对象都有gc消耗
lua gc api:
lua可以通过collectgarbage([opt,[,arg]])来进行gc操作,
  • “collect”:执行一个完整得垃圾回收周期,这是一个默认得选项
  • “stop”:停止垃圾收集器(前提是还在运行),实现方式是将gc.threshold设置为与i个巨大的值,不再触发gc step操作
  • “restart”:将重启垃圾收集器(前提是已经停止,如果没有停止,那就继续运行)
  • “count”:返回当前使用的程序内存量(单位是kbytes),返回gc->total/1024
  • “step”:执行垃圾回收的步骤,这个步骤的大小由参数arg(较大的数值意味着较多的步骤),如果这一步完成了一个回收周期则函数返回true。
  • “setpause”:设置回收器的暂停参数,并返回原来的暂停数值。该值是一个百分比,影响gc.threshold的大小,即影响触发下一次gc的时间。
  • “setstepmul”:设置回收器的步进乘数,并返回原值。该值代表每次自动step的步长倍率,影响每次gc step的速率。
gc优化

1、内存分配算法优化
2、减少gc遍历对象,即减少那些明确常驻内存的gc对象遍历

tolua的原理运行机制

c#调用lua

c#调用lua的原理是lua的虚拟机

lua调用c# :基于去反射
去反射定义:

把所有的c#类的public成员变量、成员函数,导出到一个相对应的Wrap类中,而这些成员函数通过特殊的标记,映射到lua的虚拟机中,当在lua中调用相对应的函数时,直接调用映射进去的c# Wrap函数。

wrap文件 的原理

参考网址

  • wrap文件是对c#文件的封装,lua对wrap类的函数调用,是间接的对c#实例进行操作

lua怎么调用c#

wrap文件内容解析

注册部分
BeginClass:负责类在lua中的初始化部分
  • 用于创建类、类的元表、类的元表的元表(类的元表是承载每个类方法和属性的实体,类的元表的元表指的是类的父类)
  • 将类添加到loaded表中
  • 设置每个类的元表的通用的元方法和属性,_gc,name,ref,_cal,_index_newindex
RegFunction

每一个refFunction就是将每个函数转为一个指针,然后添加到类的元表中,与将一个c函数注册到lua中是一样的。

RegVar

每一个变量或属性石被包装成get_xxx,set_xxx函数注册添加到类的元表的gettag,settag表中去,用于调用和获取。

EndClass部分
  • 设置类的元表
  • 把类加到所在模块代表的表中
每个函数的实体部分
[MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
static int GetComponent(IntPtr L)
{
    try
    {
        //获取栈中参数的个数
        int count = LuaDLL.lua_gettop(L);
        //根据栈中元素的个数和元素的类型判断该使用那一个重载
        if (count == 2 && TypeChecker.CheckTypes<string>(L, 2))
        {
            //将栈底的元素取出来,这个obj在栈中是一个fulluserdata,需要先将这个fulluserdata转化成对应的c#实例,也就是调用这个GetComponent函数的GameObject实例
            UnityEngine.GameObject obj = (UnityEngine.GameObject)ToLua.CheckObject(L, 1, typeof(UnityEngine.GameObject));
            //将栈底的上一个元素取出来,也就是GetComponent(string type)的参数
            string arg0 = ToLua.ToString(L, 2);
            //通过obj,arg0直接第调用GetCompent(string type)函数
            UnityEngine.Component o = obj.GetComponent(arg0);
            //将调用结果压栈
            ToLua.Push(L, o);
            //返回参数的个数
            return 1;
        }
        //另一个GetComponent的重载,跟上一个差不多,就不详细说明了
        else if (count == 2 && TypeChecker.CheckTypes<System.Type>(L, 2))
        {
            UnityEngine.GameObject obj = (UnityEngine.GameObject)ToLua.CheckObject(L, 1, typeof(UnityEngine.GameObject));
            System.Type arg0 = (System.Type)ToLua.ToObject(L, 2);
            UnityEngine.Component o = obj.GetComponent(arg0);
            ToLua.Push(L, o);
            return 1;
        }
        //参数数量或类型不对,没有找到对应的重载,抛出错误
        else
        {
            return LuaDLL.luaL_throw(L, "invalid arguments to method: UnityEngine.GameObject.GetComponent");
        }
    }
    catch (Exception e)
    {
        return LuaDLL.toluaL_exception(L, e);
    }
}

pairs和ipairs的区别

遍历范围
  • ipairs只能用于遍历key为整数,且key从1开始递增的key-value
  • pairs可以遍历所有key-value
有序性
  • ipairs保证遍历过程以key的顺序进行;
  • pairs遍历是以table中的存储数据的顺序进行的。
注意事项

以table存取数据,应尽量将有序的数据与无序的数据分开存储在不同的table中。如果因为项目需求不得不维护一个有序和无序混合的table,而遍历处理时又要求按照顺序进行处理,那么直接通过ipairs和pairs没办法满足需求,这时候需要引入一个table来保存遍历顺序。

require和dofile的区别

前提

lua文件是以chunk(代码块)的方式存在的,其本质是一个函数
而加载代码文件,通常会见到require、dofile、loadfile等函数

require
  • 定义:在加载一个.lua文件时,require会先在package.loaded中查找此模块是否存在,如果存在,直接返回模块,如果不存在,这加载模块文件。
  • 特点:只加载一次,并且对于模块会按照特定的搜索规则去查找文件并加载。
dofile和loadfile
  • dofile:读入代码文件并编译执行,每调用dofile一次,都会重新编译执行一次。
  • loadfile:编译代码,将整个模块文件当成一个函数返回,但是不执行代码。
  • dofile是对loadfile的一次封装。

local变量的作用域

在被声明的就那个代码块内有效

如何copy一个table

浅拷贝

拷贝的数据是和其他数据共享一个table对象的,只有最顶层的变量数据是在新的table中。

function table.shallowCopy(tb)
	local copy = {}
	for k,v in pairs(tb) do
		copy[k] = v
	end
	return copy	
end
深拷贝

递归深拷贝,包含了元表的深拷贝

function table.deepCopy(tb)
	if tb == nil then
		reutrn nil     --在这里,如果是return而不是return nil,那么在setmetatable函数中会报错
						    --第二个参数错误;因为在lua中nil也是一种变量类型,no return value和return nil是有区别的;
	end
	local copy = {}
	for k,v in pairs(tb) do
		if type(v) == 'table' then
			copy[k] = table.deepCopy(v)
		else
			copy[k] = v
		end
	end
	setmetatable(copy,table.deepCopy(getmetatable(tb)))
	return copy
end

table内部相互引用如何解决

  • 做法:用弱引用table()

三、c#

源码查询地址

字典的内部实现,优缺点,字典如何实现有序

Dictionray 的内部实现

参考文章

1、内部的函数和变量

entry结构体

private struct Entry{
	public int bashCode;  //哈希值,如果没有被使用,则为-1
	public int next;  //下一个元素的下标索引,如果没有下一个则为-1
	public TKey key; //存放元素的键
	public TValue value;  //存放元素的值
}

关键的私有变量

private int[] buckets;      //hash桶
private Entru[] entries;   //Entry 数组,存放元素
private int count ;  //当前entries的index位置
private int vaersion; //当前版本,防止迭代过程中集合被更改
private int freeList; //被删除Entry在entries中的下标index,这个位置是空闲的
private int freeCount;  //有多少个被删除的Entry,有多少个空闲的位置
private IEqualityComparer<TKey> comparer; //比较器
private KeyCollection keys;  //存放key的集合
private ValueCollection values;  //存放value的集合 
2、 初始化

初始化函数:

private void Initialize(int capacity){
            int prime = HashHelpers.GetPrime(capacity); 
            this.buckets = new int[prime];
            for (int i = 0; i < this.buckets.Length; i++)
            {
                this.buckets[i] = -1;
            }
            this.entries = new Entry<TKey, TValue>[prime];
            this.freeList = -1;
}

参考网址:https://blog.csdn.net/zhaoguanghui2012/article/details/88105715

优缺点
  • 优点:查找快速
  • 缺点:性能消耗高(用了hash,解决hash冲突时就会有这些消耗)
如何实现有序
  • 用linq查询实现
  • 用SortDictionary类进行排序(检索运算复杂度为logN的二叉排序树)

List 泛型

内部实现:
  • 构造了一个空的数组,默认容量是4
  • 当Add一个元素的时候,判断如果当前数组大小和元素的个数相等时,这时候要扩容,按照2倍的规则扩容的

值类型和引用类型

值类型:
  • 值类型直接存储其值。
  • 均隐式派生自System.ValueType
  • byte,short,int,long,float,double,decimal,char,bool 和 struct 统称为值类型
  • 值类型变量声明后,不管是否已经赋值,编译器为其分配内存
  • 值类型的实例通常是在线程栈上分配的(静态分配),但是在某些情形下可以存储在堆中。
  • 值类型在栈内分配空间大小因变量类型而异
  • 值类型不受GG控制,作用域结束时候,会被操作系统释放
引用类型:
  • 引用类型存储对其值的引用。部署:托管堆上部署了所有引用类型。
  • 基类为Objcet
  • 引用类型当声明一个类时,只在栈中分配一小片内存用于容纳一个地址,而此时并没有为其分配堆上的内存空间。当使用 new 创建一个类的实例时,分配堆上的空间,并把堆上空间的地址保存到栈上分配的小片空间中
  • 引用类型的对象总是在进程堆中分配(动态分配)
  • 引用类型在栈内的空间大小相同
  • 引用类型的内存管理由GC完成

装箱和拆箱

参考文章

定义
  • 装箱:隐式的将一个值型转换为引用型对象
  • 拆箱:将一个引用型对象转换成任意值型
理由

通过装箱和拆箱操作,能够在值类型和引用类型中架起一做桥梁.换言之,可以轻松的实现值类型与引用类型的互相转换,装箱和拆箱能够统一考察系统,任何类型的值最终都可以按照对象进行处理

打包总结和资源优化

参考文章

C#和android、os的原生代码交互

c++一个空类占几个字节

在C++中空类会占一个字节,这是为了让对象的实例能够相互区别。具体来说,空类同样可以被实例化,并且每个实例在内存中都有独一无二的地址,因此,编译器会给空类隐含加上一个字节,这样空类实例化之后就会拥有独一无二的内存地址。如果没有这一个字节的占位,那么空类就无所谓实例化了,因为实例化的过程就是在内存中分配一块地址。

静态方法和非静态方法能相互调用吗

定义

静态方法是在类中使用staitc修饰的方法,在类定义的时候已经被装载和分配。而非静态方法是不加static关键字的方法,在类定义时没有占用内存,只有在类被实例化成对象时,对象调用该方法才被分配内存。

非静态方法可以调用静态,静态不能调用非静态

静态方法中只能调用静态成员或者方法,不能调用非静态方法或者非静态成员,而非静态方法既可以调用静态成员或者方法又可以调用其他的非静态成员或者方法。

静态属性的加载和释放

一、静态变量在类被加载的时候分配内存。

类在什么时候被加载?
当我们启动一个app的时候,系统会创建一个进程,此进程会加载一个Dalvik VM的实例,然后代码就运行在DVM之上,类的加载和卸载,垃圾回收等事情都由DVM负责。也就是说在进程启动的时候,类被加载,静态变量被分配内存。

二、静态变量在类被卸载的时候销毁。
  • 类在什么时候被卸载?
    在进程结束的时候。
    说明:一般情况下,所有的类都是默认的ClassLoader加载的,只要ClassLoader存在,类就不会被卸载,而默认的ClassLoader生命周期是与进程一致的,本文讨论一般情况。

四、游戏网络交互的机制

短连接和长连接,网络包abc如何顺序到达

短连接

连接->传输数据->关闭连接
HTTP是无状态的,浏览器和服务器每进行一次HTTP操作,就建立一次连接,但任务结束就中断连接。
也可以这样说:短连接是指SOCKET连接后发送后接收完数据后马上断开连接

长连接

连接->传输数据->保持连接 -> 传输数据-> 。。。 ->关闭连接。
长连接指建立SOCKET连接后不管是否使用都保持连接,但安全性较差

保证包的顺序传输

具体步骤如下:

  • (1)为了保证数据包的可靠传递,发送方必须把已发送的数据包保留在缓冲区;

  • (2)并为每个已发送的数据包启动一个超时定时器;

  • (3)如在定时器超时之前收到了对方发来的应答信息(可能是对本包的应答,也可以是对本包后续包的应答),则释放该数据包占用的缓冲区;

  • (4)否则,重传该数据包,直到收到应答或重传次数超过规定的最大次数为止。

  • (5)接收方收到数据包后,根据数据包的序号排序,先进行CRC校验,如果正确则把数据交给上层协议,然后给发送方发送一个累计应答包,表明该数据已收到,如果接收方正好也有数据要发给发送方,应答包也可方在数据包中捎带过去。

socket原理讲解

参考文章

Protobuf 的解析原理

参考文章

1、定义

Protobuf是一个网络通信协议,提供了高效率的序列化和反序列化机制,序列化就是把对象转换成二进制数据发送给服务端,反序列化就是将收到的二进制数据转换成对应的对象。

2、tag的解释
//rule   type  name tag 
optional  int32 a = 1;   //这个定义不是赋值,它只是定义了a字段的tag,tag包含了数据类型(int32)和字段序号(1)

tag有三个作用,一个保证字段不重复,二是保证它是数据流中的位置,三是标记了数据类型。所以,tag是由fieldNumber和wireType组成,fieldNumber保证了字段不重复和它是数据流中的位置,wireType标记了数据类型

3、优点
体积小、效率高
  • 采用了Varint编码:
    原理:值越小的数字,使用越少的字节数表示
    作用:通过减少表示数字的字节数从而进行数据压缩
    Varint编码高位有特殊含义:如果是1,表示后续的字节也是该数字的一部分,如果是0,表示这是最后一个字节,且剩余7位都用来表示数字。
使用简单、兼容性好、维护简单
  • 使用Json的时候,如果某个字段值为null或某个key为null,那么android或ios相应的json解析就可能报错,因为json序列化对数据是顺序写入,然后再顺序读取,这么一来,如果某些字段没有赋值,那么整个解析包偏移量都会出错,所以没有赋值的字段必须传入一个默认值。
    而protobuf则不会有这个问题,因为有tag。每一个字段都tag,如果field本身没有赋值,那么编码时tag不会被写入流中。相对应的,解析的时候没有field的tag,那么该字段也会被无视掉。
  • 另外一种情况是,服务器在协议中增加了一个新字段,客户端没有增加,那么客户端在解析协议时,不会报错,而是直接跳过这个新增加的字段
  • 总结:采用T - L - V 的数据存储方式
加密性好
跨平台

五、排序算法

类别排序方法时间复杂度最好情况时间复杂度最坏情况时间复杂度平均情况空间复杂度-辅助空间稳定性
插入排序直接插入排序O(n)O(n2)O(n2)O(1)稳定
插入排序希尔排序O(n)O(n2)~O(n1.3)O(1)不稳定
交换排序冒泡排序O(n)O(n2)~O(n2)O(1)稳定
交换排序快速排序O(nlogn)O(n2)O(nlogn)O(nlogn)不稳定
选择排序直接选择排序O(n2)O(n2)O(n2)O(1)不稳定
选择排序堆排序O(nlogn)O(nlogn)O(nlogn)O(n2)不稳定
归并排序O(nlogn)O(nlogn)O(nlogn)O(n)稳定
基数排序O(n+m)O(k*(n+m))O(k*(n+m))O(n+m)稳定
  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值