文章目录
1.1. 介绍:
值类型:int,bool,float,double,char,enum,struct,short,long…
引用类型:class,object,interface,string,array,delegate+
1.2. 区别:
① 存储方式:值类型—>栈,引用类型—>堆,引用类型会在栈中存储指向堆数据的内存地址。
② 存储速度:值类型快,引用类型慢。
③ 存储内容:值类型存储实际数据,引用类型存储指向 内存堆中的指针与引用。
④ 释放方式: 值类型可以在栈中自动释放,引用类型需要在堆中GC释放。
⑤ 继承类:值类型 :System.ValueType : System.Object,引用类型:System.Object。
1.3. 底层
① 引用类型实例化时,先在栈中开辟空间,用于存储堆中对象地址,然后在堆内开辟空间,存储引用对象。
② 值类型直接在栈中开辟空间,值类型的引用地址也在栈空间中。
③ 在参数对象进入方法体时,实则是在栈中开辟了新的临时空间(对应参数的副本),当栈内值类型修改时,由于栈中内存地址不同,无法影响到本体。而引用类型的存储数据是一个堆内的地址,所以对于引用类型的修改是直接修改堆内对象。
④ 值类型变量中引用类型在堆中(struct中的string属性),引用类型中值类型数据也在堆中(类中的int属性)。
⑤ 内存分为一下三个模块:
2.1. 介绍:
string的修改,实则都是new 了一个新的string,在堆内开辟新的空间,此时栈内的副本也会指向新的对象。
string s = "富强民主";//当前堆内存:“富强民主”
s = "文明和谐";//当前堆内存:“富强民主”,“文明和谐”
s = "自由平等";//当前堆内存:“富强民主”,“我们和谐”,“自由平等”
2.2. StringBuilder和StringBuffer:
为了解决字符串修改重复创建的问题,当频繁的对一个字符串进行修改时,使用StringBuilder代替string。 StringBuilder非线程安全,性能略好,用于单线程`,StringBuffer线程安全,用于多线程。
2.3. StringBuilder一定比string性能好吗?
不一定。极少拼接的情况下 性能:string > StringBuilder,由于初始内存消耗:string < StringBuilder,大量拼接的情况下 性能:string < StringBuilder。
2.4. 字符串池:
字符串池 是CLR(公共语言运行库)一种针对反复修改字符串对象的优化措施,能减少一定性能消耗。原理是 内部开辟容器通过 键值对 的形式注册字符串对象,键 存储字符串对象的内容,值 存储字符串在堆上的引用。这样当新建字符串时,回去检查,如果不存在就在这个容器中开辟新的空间存放字符串。(废弃资源的回收利用)
3.1. 介绍:
3.1.1 Unity内部存在两个内存管理池 ↓
栈内存 主要用来存储较小的和短暂的数据(值类型变量,临时变量…)
堆内存 主要用来存储较大的和存储时间长的数据(引用类型变量,对象实例…)
unity中的变量只会在堆内存或栈内存上进行内存分配。非堆即栈。
3.1.2 变量的激活与否 ↓
当变量处于激活状态时,则其占用内存被标记为使用状态,该部分内存被标记为分配状态。
当变量未激活时,其占用内存不再需要,该部分内存可以被会受到内存池中再次使用。该操作即为GC。栈内存回收极其迅速,堆内存并不是及时回收的,此时其对应内存仍为使用状态,不再使用的内存只会在GC的时候被回收。
3.1.3 GC的定义
GC(垃圾回收)主要指堆上的内存分配与回收,unity会定时对堆内存进行GC操作。
3.2. GC算法:
C#:C#使用分代算法来进行GC操作。有着 内存整理,避免碎片化,压缩等特点。
分为以下三代:
简易流程:
详细流程:
3.3. GC存在的问题
3.4. GC的触发时机
3.5. 如何避免GC
减小临时变量的使用,多使用公共对象,多利用缓存机制。(将容器定义到函数外)。
减少new对象的次数。
string和StringBuilder的转换使用(详情见2)
定义容器时,尽量将空间大小设置为足够,减少容器的扩容。
帧更新函数进行计时器控制,防止内存爆炸。此外,可以选择在加载进度条中手动GC。
缓存对象池:常用于经常摧毁创建的对象,比如子弹,特效等…。将Destroy换成SetActive(false)。将Instantiate换位SetActive(true)。
减少拆 / 装箱:
装箱:value —> object 拆箱:object —> value。
产生GC的原因:装箱时,对于值类型会在堆内存上分配一个Object类型的引用来封装该值类型变量,其对应缓存就会产生垃圾。
多用泛型!处理多个代码对不同数据类型执行相同指令操作时!
**在使用协程时,yield return 0 —> yield return null ** 能有效避免装箱。
项目打包之前,删掉所有的Log。
4.1. 封装
将数据和行为相结合,通过行为约束代码修改数据的程度,增强数据安全性。(属性是C#封装特效的最好实例)。
4.2. 继承
提高代码复用度,增强软件可维护的重要手段,符合开闭原则(见设计模式)。继承就是把子类的共性聚集起来提取出一个父类,C#的继承是单继承,但具有传承性(这一点上与Java一致)。
4.3. 多态
同名的方法在不同环境下,会自适应转换为不同的逻辑表现。(例子:猫在走猫步,鸟儿在飞翔…)
① 当该关键字修饰类时,该类无法被继承。
② 当该关键字修饰方法时,该方法无法被重写。常常配合override关键字一起使用。
总结:sealed关键字的存在限制了方法或类的向下拓展。可以理解为绝育。即规定最终版,无法再去重写方法。
public class B : A{
protected override void M (){}
protected sealed override void N (){}
}
public sealed class C : B {
protected override void M (){}
//无法重写N方法了,因为在父类中已经被密封
}
//无法再继承C类了
7.1. 对比:
① 数据类型:结构体是值类型,类是引用类型。结构体存在栈中,类存在堆中。
② 参数传递:
结构体参数传递时是值类型的传递,函数中改变参数的值,结构体对象值是不变的。类参数传递时是引用的传递。
C#中结构体定义时,无法给参数赋值。而类可以使用构造函数给成员变量初始化。
③ 构造函数:
结构体无法声明无参构造,且有参构造不会顶替掉无参构造。
类可以声明无参构造,但如果写了有参构造,其会顶替调无参构造。
结构体需要在构造函数中初始化所有成员变量,而类随意。
④ 修饰与特性
结构体无法被 static 修饰,而类可以。
结构体无法被继承,而类可以。
结构体中无法声明析构函数,而类可以。
7.2. 使用环境:
结构体:
① 结构体是值类型在栈中,存取数据比堆快,容量小。适合去轻量级对象,比如:点,矩形,颜色…
② 如果对象是数据集合,优先考虑结构体,比如集合,坐标。
③ 变量传值时,希望传递对象是拷贝,而不是对象的引用地址,可用结构体。
类:
①类是引用对象,存在堆中,容量大,适合重量级对象。
② 如果对象需要使用继承多态等面向对象特征,使用类,比如玩家 ,怪物…
8.1. 对比:
① 接口不是类,无法被实例化(无构造函数和析构函数),抽象类只能间接实例化(实例化其子类时自动实例化父抽象类)抽象类有构造函数。
② 接口只能做方法声明,抽象类既可以做方法声明也可以做方法实现。
③ 接口只能包含抽象成员,完全抽象。而抽象类可以有实现成员,属于部分抽象。
④ 接口需要被子类实现,而抽象类需要被子类继承。
⑤ 接口中成员只能是public因此可以省略。抽象类随意。
⑥ 接口可以实现多继承,而抽象类只能实现单继承。
⑦ 抽象方法(抽象类和接口都有)需要被实现,故不能是静态也不能是私有。
8.2. 使用环境:
抽象类适合做某个领域的固有属性(把对象的共同点抽象提取出来)。而接口是用来定义某个领域的扩展功能。
抽象类:
接口:
当注重代码的扩展性和可维护性时,优先使用接口。
9.1. 样例
//静态构造函数
class Student{
public static string type;
static Student(){
//常常用于静态变量的赋值
type = "学生";
}
public void Study(){}
}
//调用时机
Student.type;
new Student().Study();
9.2. 特点
① 静态构造函数既没有访问修饰符,也没有参数。
② 在创建第一个实例或任何静态成员被引用时,.NET将自动调用静态构造函数来初始化类。
③ 一个类只能有一个静态构造函数并且最多仅运行一次。
④ 静态构造函数无法被继承。且如果类中没有该模块,但又在程序运行生存期内需要初始化静态成员,编译器会自动生成该语句块。
⑤ 如果该模块语句发生一场,运行时不会调用该构造函数,并且在程序运行盛器内,该类型保持未初始化。
每个虚函数都会有一个 虚函数表。该虚函数表实质是一个 指针数组。存放每一个对象虚函数入口地址。对于一个派生类来说,他会继承基类的虚函数同时增加自己的虚函数入口地址,如果派生类重写了虚函数,则继承过来的虚函数入口地址将被派生类的重写虚函数入口地址代替。程序会发生动态绑定,将父类指针绑定到实例化的对象上从而实现多态。
① 为空:引用无法为空,指针可以指向空对象。
② 初始化相关:引用必须初始化且初始化后无法改变,指定对那个对象的引用。而指针无需且可以改变指向。
③ 访问相关:引用可以直接访问对象,指针只能间接访问对象。
④ 数据大小:引用的大小是指所引用对象的大小。而指针大小是本身的大小,通常为4字节。
⑤ 其他特性:
12.1. 作用:
解决值类型和引用类型在函数内部改值或重新声明能够影响外部传入的变量也被修改。
12.2. 区别:
ref:ref传入的变量必须初始化但是在内部可改可不改。
out:out传入的变量可以不用初始化,但在内部必须修改其值。
案例:
RaycastHit hitInfo;
Physics.Raycast(ray,out hitInfo);
13.1 委托的介绍:
委托是约束集合中的一个类,相当于一组方法列表的引用,可以便捷地使用委托对方法集合进行操作。委托是对函数指针的封装。
13.2 委托和事件的区别:
关系:
事件可以看作是委托的一个变量。事件是基于委托存在的,事件是委托的安全包裹,让委托的使用更具有安全性。
区别:
① 声明 委托可以用 “ = ” 来赋值,事件不可以。
② 调用范围 委托可以在声明他的类外部调用,而事件只能在其内部调用。
③ 委托是一个类型,而事件只是一个对象。
④ **事件的本质就是 一个private 的委托与Add,Remove两个方法。**只能对自己进行 += , -= 操作。
14.1 布尔类型
bool,true / false,占一个字节。(bool -> System.Boolean),
14.2 有符号整数类型
sbyte , 占一个字节。(sbyte -> System.Byte),-128 - 127
short, ,占2个字节。(short -> System.Int16),-32768 - 32767
int,,占4个字节。(int -> System.Int32), -2147483648 - 2147483647
long,,占8个字节。(long -> System.Int64),大约1E+20。
14.3 无符号整数类型
byte , 占1个字节。(byte -> System.Byte),0 - 255
ushort,占2个字节。(ushort -> System.UInt16),0 - 65535
uint,占4个字节。(uint -> System.UInt32),0 - 4294967295
ulong,占8个字节。(ulong -> System.UInt64),大约1E+20。
14.4 浮点型
float,占4个字节。(float -> System.Single)
double,占8个字节。(double -> System.Double)
14.5 字符型
char ,占2个字节。(char -> System.Char)
案例:
Student s = new Student();
协变(out):
和谐,自然的变化。
里氏替换原则中,父类型容器可以装在子类型对象,子类可以转换为父类。感受是和谐的。
逆变(in):
逆常规,不正常的变化。
里氏替换原则中,子类对象不能装载父类对象。所以父类转换为子类,感受是不和谐的。
协变和逆变是用来修饰泛型的。只有泛型接口与委托能用。
案例:
//out修饰的泛型,只能作为返回值
delegate T Test<out T>();
//in修饰的泛型,只能作为参数
delegate T Test<in T>(T t);
作用:
在程序加载运行时,动态获取程序集,并且可以获取到程序集的信息反射在运行时动态获取类,对象,方法,对象数组数据的一种重要手段。
优点:允许在运行时发现并使用编译时仍解决不了的类型和成员。
缺点:
1.根据目标类型字符串搜索扫描程序集的元数据时间较长。
2.反射调用方法或者属性比较耗时。
通过反射获取实例:
反射可以直接访问类的构造,直接通过 getConstructor,访问这个构造函数,然后通过不同的参数列表,就可以具体定位到哪一个重载,通过这个方法,去得到类的实例,把对象就拿到了。
在遍历删除List中的元素时,会导致List.Count发生变化,从而使for循环错误,数组下标越界或删除错误值。
解决方案:
可以从后往前删除元素。
定义:一种 键值对 形式存放数据。key的类型无限制。
实现原理核心:Hash算法和 解决Hash冲突的算法。
Hash算法:
对实例对象和字符串来说,它们没有直接的数字作为Hash标准,因此它们需要通过内存地址计算一个Hash值,计算这个值的算法就是哈希函数。
除留余数法:取key被某个不大于散列表表长的数p除后余数作为散列地址。
Hash冲突:不同的key进行Hash计算,得到的结果一定是同一Hash地址。常用的解决办法是拉链法(链地址法)。
拉链法:将产生冲突的元素建立一个单链表,并将头指针地址存储至Hash表对应位置,这样定位到Hash位置后可通过遍历单链表来查找元素。
20.1 并发和并行
并发:在操作系统中,同一时间段,几个程序在同一个CPU上运行,但在任意时间点,只有一个程序在CPU上运行。
并行:当操作系统有多个CPU时,一个处理A线程,一个处理B线程,线程之间互不干扰,可以同时进行。
区别:并发在宏观表现为有多个程序同时运行,微观上这些程序是分时交替执行的。
并行在多CPU系统中,将并发执行的程序分配到不同CPU上处理,每个CPU用来处理一个程序,这样的程序可以实现同时执行。
**20.2 **进程
概念:一个进程好比一个程序,它是 操作系统 资源分配的最小单位。同一时间执行的进程数不会超过核心数。但单核CPU也可以运行多进程。只是会极快地在进程间来回切换实现的多进程。进程是CPU资源分配的基本单位。
20.3 线程
概念:线程相比进程即为应用程序的不同任务。线程依赖于进程。它是 程序执行过程中的最小单元,不过线程的堆共享堆不共享栈,这也会导致锁的问题。线程是独立运行和调度的基本单位。
20.4 线程与进程的区别
20.5 阻塞和非阻塞
阻塞指调用线程或进程被操作系统挂起。
非阻塞指调用线程或进程不会被操作系统挂起。
20.6 同步和异步
同步:阻塞模式。指一个进程在执行某个请求时,如果该请求需要一段时间才能返回信息,那么这个进程将会一直等待下去,直到收到返回信息才继续执行。
异步:非阻塞模式。无需一直等待下去,而是继续执行接下来的操作,不管其他进程的状态。当有消息返回时系统会通知进程进行处理。
20.7 协程
概念:协程即为 一种用户态的轻量级线程。
特点:协程有自己的寄存器上下文和栈。协程可以保留上一次调用时的状态,每次重入时,相当于进入上一次调用的状态,即进入上一次离开时所处的逻辑流位置。
优点:高并发 + 高扩展 + 低成本。
缺点:
题目说明:
输入某二叉树的前序遍历和中序遍历的结果,请构建该二叉树并返回其根节点。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。
输入输出样例:
Input: preorder = [3,9,20,15,7],inorder=[9,3,15,20,7] Output: [3,9,20,null,null,15,7]
代码:
class Solution {
//构建哈希表方便后续快速根据val得到在中序遍历的位置
private Map<Integer,Integer> map = new HashMap<>();
public TreeNode buildTree(int[] preorder,int[] inorder){
//判空
if (preorder.length == 0) return null;
int n = preorder.length;
//初始化哈希表
for (int i = 0;i < n;++i)
map.put(inorder[i],i);
//直接调用核心逻辑
return CoreBuildTree(preorder,inorder,0,n-1,0,n-1);
}
//核心算法逻辑
/*
* Param1:前序遍历数组
* Param2:中序遍历数组
* Param3:前序遍历左边界值
* Param4:前序遍历右边界值
* Param5:中序遍历左边界值
* Param1:中序遍历右边界值
* Return:返回当前递归的树根节点
*/
private TreeNode CoreBuildTree(int[] preorder,int[] inorder,int preorder_left,int preorder_right,int inorder_left,int inorder_right){
//递归函数优先考虑返回条件
if (preorder_left > preorder_right) return null;
//通过前序遍历拿到当前根节点值
int rootVal = preorder[preorder_left];
//根据当前根节点值找到其在中序遍历表中的下标
int rootIndex_inorder = map.get(rootVal);
//创建出当前根节点
TreeNode root = new TreeNode(rootVal);
//通过中序遍历表查出当前根节点有多少左子节点
int size_left_subTree = rootIndex_inorder - inorder_left;
//递归部分(左右两边)
root.left = CoreBuildTree(preorder,inorder,preorder_left + 1,preorder_left + size_left_subTree,inorder_left,rootIndex_inorder - 1);
root.right = CoreBuildTree(preorder,inorder,preorder_left + size_left_subTree+1,preorder_right,rootIndex_inorder + 1,inorder_right);
return root;
}
}
题目描述:
给定一个
m x n二维字符网格board和一个字符串单词word。如果word存在于网格中,返回true;否则,返回false。
案例:
输入:board = [["A","B","C","E"], ["S","F","C","S"], ["A","D","E","E"]], word = "ABCCED" 输出:true
代码:
class Solution {
public boolean exist(char[][] board, String word) {
//数组的长度
int m = board.length,n = board[0].length;
//存储某位置是否被访问过
boolean[][] visited = new boolean[m][n];
//遍历二维数组
for (int i = 0;i < m;++i){
for (int j = 0;j < n;++j){
boolean ans = aroundCheck(board,visited,i,j,word,0);
if (ans) return ans;
}
}
return false;
}
//核心递归算法
/*
Param1:二维字符网格
Param2:标记数组
Param3,4:当前所在的位置
Param5:当前比对的字符串从k开始的子串
Param6:字符串中的第几位
*/
private boolean aroundCheck(char[][] board,boolean[][] visited,int i,int j,String target,int k){
//作为递归函数优先考虑返回条件
if (board[i][j] != target.charAt(k)) return false;
if (k == target.length - 1) return true;
//将该位置设为已访问
visited[i][j] = true;
//准备向四个方向遍历
int[][] directions = {{0,1},{0,-1},{1,0},{-1,0}};
boolean res = false;
//分别查找各个方向是否符合条件
for (int[] dir : directions){
int curi = i + dir[0],curj = j + dir[1];
if (curi >= 0 && curi < board.length && curj >= 0 && curj < board[0].length)
boolean flag = aroundCheck(board,visited,curi,curj,target,k+1)
if (flag){
//找完了
res = true;
break;
}
}
//剪枝回溯
visited[i][j] = false;
return res;
}
}
1.1. A*寻路的基本原理
① 寻路消耗公式:f = g + h
f:寻路消耗
g:距离起点的距离,详细解释看以下具体寻路步骤
h:距离终点的曼哈顿距离,即当前点到终点的x差值 + y差值。之所以采用 曼哈顿距离,是因为该距离相比直接计算直线距离少了开方步骤,性能更高。
② 开启列表:详情如下。
③ 关闭列表:详情如下。
④ 格子对象的父对象。
1.2. A* 寻路具体步骤
1.3. A*的算法实现
1.3.1 寻路结点类
//格子类型枚举
public enum E_Node_Type
{
Walkable,Stoppable
}
//A*寻路结点类
public class AStarNode
{
//当前格子坐标
public int x;
public int y;
//寻路消耗f
public float f;
//距离起点的常规距离
public float g;
//距离终点的曼哈顿距离
public float h;
//父节点
public AStarNode father;
//格子类型
public E_Node_Type type;
//构造函数
public AStarNode( int x,int y ,E_Node_Type type)
{
this.x = x;
this.y = y;
this.type = type;
}
}
1.3.2 寻路管理器类(单例)
public class AStarManager
{
#region 单例模式
private static AStarManager instance;
public static AStarManager Instance
{
get
{
if (instance == null)
{
instance = new AStarManager();
}
return instance;
}
}
#endregion
//存储所有的格子
private AStarNode[,] nodes;
//开启列表
private List<AStarNode> openList;
//关闭列表
private List<AStarNode> closeList;
//地图尺寸
private int mapW;
private int mapH;
//初始化地图格子
public void InitMap(int mapW, int mapH)
{
//根据传入的地图尺寸,初始化格子列表
this.mapW = mapW;
this.mapH = mapH;
nodes = new AStarNode[mapW, mapH];
for (int i = 0; i < nodes.Length; i++)
{
for (int j = 0; j < nodes.Length; j++)
{
//TODO:这里随机数设置,方便测试
nodes[i, j] = new AStarNode(i, j, Random.Range(0, 100) < 20 ? E_Node_Type.Stoppable : E_Node_Type.Walkable);
}
}
}
//寻路核心算法(传入起点和终点)
public List<AStarNode> FindPath(Vector2 startPos, Vector2 endPos)
{
//判断传入的起点终点是否合法:①是否超出地图边界,②是否为不可行走
if (startPos.x < 0 || startPos.x >= mapW ||
startPos.y < 0 || startPos.y >= mapH ||
endPos.x < 0 || endPos.x >= mapW ||
endPos.y < 0 || endPos.y >= mapH)
{
return null;
}
//根据坐标先拿到起点终点对应的格子
AStarNode start = nodes[(int)startPos.x, (int)startPos.y];
AStarNode end = nodes[(int)endPos.x, (int)endPos.y];
if (start.type == E_Node_Type.Stoppable || end.type == E_Node_Type.Stoppable)
return null;
//先将起点格子加入开启列表中
start.father = null;
start.f = 0;
start.g = 0;
start.h = 0;
//清空之前列表信息
closeList.Clear();
openList.Clear();
closeList.Add(start);
while (true)
{
//分别遍历当前格子周围的点,如果合法并且没有在开启或关闭列表中,将其加入开启列表中
for (int i = -1; i <= 1; i++)
{
for (int j = -1; j <= 1; j++)
{
if (i == 0 && j == 0)
continue;
if (i == 0 || j == 0)
FindNearlyNodeToOpenList(start.x + i, start.y + j, 1, start, end);
else
FindNearlyNodeToOpenList(start.x + i, start.y + j, 1.4f, start, end);
}
}
//死路判断
if (openList.Count == 0)
return null;
//从开启列表中查找 寻路消耗最小的结点,放入关闭列表中。
openList.Sort(SortOpenList);
//放入关闭列表,删除开启列表的数据
closeList.Add(openList[0]);
start = openList[0];
openList.RemoveAt(0);
//如果查找到终点,直接返回
if (start == end)
{
List<AStarNode> path = new List<AStarNode>();
path.Add(end);
while (end.father != null)
{
path.Add(end.father);
end = end.father;
}
path.Reverse();
return path;
}
}
}
//自定义排序规则
private int SortOpenList(AStarNode a, AStarNode b)
{
if (a.f > b.f)
return 1;
else if (a.f < b.f)
return -1;
else
return 1;
}
//将临近点放入开启列表
private void FindNearlyNodeToOpenList(int x, int y, float g, AStarNode father,AStarNode end)
{
//边界测试
if (x < 0 || x >= mapW || y < 0 || y >= mapH)
return;
AStarNode node = nodes[x, y];
//是否被阻挡或者已经遍历过
if (node == null ||
node.type == E_Node_Type.Stoppable ||
closeList.Contains(node) ||
openList.Contains(node))
return;
//计算寻路消耗
node.father = father;
node.g = father.g + g;
node.h = Mathf.Abs(end.x - x) + Mathf.Abs(end.y - y);
node.f = node.g + node.h;
//存入到开启列表中
openList.Add(node);
}
}
1.4. A*优缺点:
1.5. 优化
1.6. 拉绳算法优化
定义:从起点开始,与路径网格的临边点进行相连,然后以此移动左右边界,判断移动后夹角变换,如果夹角变小了,说明距离终点更近了;反之,说明距离更远了,则选择另一个边界进行移动;如果同样变大,则需要进行路径合并,选择当前边界的点作为新的起点,并与旧起点连起来得到一条路径。
public int[] BubbleSort(int[] arr){
//如果数组个数在两个以下,无需排序
if (arr.Length < 2) return arr;
//当前 n-1 个元素排好序后,最后一个自动有序
for (int i = 0;i < arr.Length - 1;++i){
//单轮次是否存在元素互换
bool swapped = false;
for (int j = 1;j < arr.Length - i;++j){
//如果左 > 右,交换两元素,并标记布尔变量
if (arr[j - 1] > arr[j]){
int temp = arr[j];
arr[j] = arr[j - 1];
arr[j - 1] = temp;
swapped = true;
}
}
if (!swapped) break;
}
return arr;
}
递归模板:
明确目的:从给定的一个有序数组nums中找到目标值为target的所在下标。
public int BinarySearch(int[] nums,int l,int r,int target){
if (l > r) return -1;
int mid = (l + r) >> 1;
if (nums[mid] > target) return BinarySearch(nums,l,mid - 1,target);
else if (nums[mid] < target) return BinarySearch(nums,mid + 1,r,target);
else return mid;
}
迭代模板:
public int BinarySearch(int[] nums,int target){
int l = 0;
int r = nums.length;
int mid;
while (l <= r){
mid = (l + r) >> 1;
if (nums[mid] > target)
r = mid - 1;
else if (nums[mid] < target)
l = mid + 1;
else
return mid;
}
return -1;
}
//注意:牢记以下对应关系
// mid + 1时用i-1和i,mid时用j和j+1
public void QuickSort(int[] nums,int l,int r){
if (l >= r) return;
int i = l - 1,j = r + 1,mid = (l + r) >> 1;
while (i < j){
do ++i;while(nums[i] < mid);
do --j;while(nums[j] > mid);
if (i < j){
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
QuickSort(nums,l,j);
QuickSort(nums,j+1,r);
}
//主要是分治的思想
int[] nums = new int[N];
int[] tmp = new int[nums.length];
public void MergeSort(int[] nums,int l,int r){
if (l >= r) return;
int mid = (l + r) >> 1;
MergeSort(nums,l,mid);
MergeSort(nums,mid + 1,r);
int k = 0,i = l,j = mid + 1;
while (i <= mid && j <= r){
if (nums[i] <= nums[j]) tmp[k++] = nums[i++];
else tmp[k++] = nums[j++];
}
while (i <= mid) tmp[k++] = nums[i++];
while (j <= r) tmp[k++] = nums[j++];
for (i = l,j = 0;i <= r;i++,j++) nums[i] = tmp[j];
}
思路解析:
先拿1w和数据建立堆模型(最小堆),然后依次添加剩余元素,如果大于堆顶的元素(当前堆中最小元素),将其替换掉,并使用上浮或者下沉算法使其仍为一个最小堆,这样遍历完成后,堆中的1w个数即为要找的数据。建立堆的时间复杂度:O(mlogm) ,m = 1w,算法的时间复杂度为:O(nmlogm),n = 100。
优化思路:如果对10亿个数据找出1w个最大数,算法该如何优化?
分治。可以把所有的10亿个数据分组存放。比如放在1000个文件中。这样处理就可以在每个文件的10^6个数据中找到最大的10000个数,合并到一起再找出1000 * 10000中的最终结果即可。
定义:深搜是一种用于遍历或搜索树或图的算法。沿着树的深度遍历到树的节点,尽可能深的搜索树的分支,尽可能深的搜索树的分支。当结点v的所在边都已经被探寻过,搜索将回溯到发现结点v的那条边的起始节点。这一过程一直进行到已发现从源节点可达的所有结点为止。
算法模板:二叉树的中序遍历(递归)(Java版):
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> ans = new LinkedList<Integer>();
MidTraversal(root,ans);
return ans;
}
//递归函数
public void MidTraversal(TreeNode root,List<Integer> ans){
if (root == null) return ;
MidTraversal(root.left,ans);
ans.add(root.val);
MidTraversal(root.right);
}
定义:BFS是一种盲目搜索,目的是系统展开并检查图中所有结点,以寻找结果。换句话说,它不考虑结果的可能位置,彻底搜索整张图,直到找到结果为止。
**算法模板:**二叉树的层序遍历(Java版):
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> ans = new LinkedList<>();
if (root == null) return ans;
//队列生成
Deque<TreeNode> queue = new LinkedList<>();
queue.offer(root); //根节点进入队列
while (!queue.isEmpty()){
List<Integer> level = new LinkedList<>();
for (int i = 1;i <= queue.size();i++){
TreeNode curr = queue.poll();
level.add(curr.val);
if (curr.left != null)
queue.offer(curr.left);
if (curr.right != null)
queue.offer(curr.right);
}
ans.add(level);
}
return ans;
}
题目:
在一个 n * m 的二维数组中,每一行都按照从左到右 非递减 的顺序排序,每一列都按照从上到下 非递减 的顺序排序。请完成一个高效的函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
案例:
[ [1, 4, 7, 11, 15], [2, 5, 8, 12, 19], [3, 6, 9, 16, 22], [10, 13, 14, 17, 24], [18, 21, 23, 26, 30] ]
代码样例:(二维数组也可单次遍历出结果!类二分查找)
class Solution {
public boolean findNumberIn2DArray(int[][] matrix, int target) {
if (matrix.length == 0) return false;
int m = matrix.length,n = matrix[0].length;
int i = m-1,j = 0; //从左下角开始
while (i >= 0 && j < n){
int cur = matrix[i][j];
if (cur > target) --i;
else if (cur < target) ++j;
else return true;
}
return false;
}
}
1.1. 单一职责原则:
一个类最好只用来做一件事,只有一个引起其变化。(比如,人物的操作模块分为 攻击控制,移动控制,交互控制等)
1.2. 开闭原则:
开放扩展,封闭更改。**抽象化设计 **是实现开闭原则的的关键。(接口抽象类,方法重写…)
1.3. 里氏替换原则:
子类能够替换程序中父类对象出现的任何地方,并且保证其原来程序的逻辑行为不变且正确性不改变。在运用时,父类通常设计为抽象类或接口,让子类继承父类并实现在父类中声明的方法。运行时,子类实例替换父类,能有效地拓展程序的功能,从而不修改原有子类代码,增加原有功能可以通过一个新的子类来实现。
1.4. 依赖倒置原则:
抽象不应该依赖于细节,正如要针对接口编程,而不是针对实现编程。
1.5. 接口隔离原则:
使用多个的专门,而不是使用单一的总接口。
1.6. 合成复用原则:
尽量使用对象组合,而不是单一的靠继承达到复用目的。
1.7. 迪米特法则:
一个软件实体应尽可能少的与其他实体发生相互作用。
概念:属于 创建型模式。也叫 静态工厂模式。主要是一个工厂类通过传入的参数,动态决定创建哪一个产品类(继承同一个父类或接口)的实例。
特点:产品都继承于同一个父类或接口。工厂类中的工厂方法一般都是 静态方法。需要创建什么实例,就传入一个正确的参数即可,就可以获取到所需要的实例。只有一个工厂类创建所有产品。
优点:
缺点:
2.1 定义
通过工厂父类定义负责创建产品的公共接口,子类复杂生产具体对象。
2.2. 使用场合
角色技能是一系列类。游戏场景转换:角色AI状态管理等。
相比简单工厂模式,工厂方法模式增添了抽象的工厂父类。将不同产品的创建依托给不同的工厂类。
2.3. 应用实例:Java工具库中的Collection和迭代器的关系便是工厂模式的典型案例。
优点:
缺点:
2.4. 代码案例:
//产品接口
interface Phone {}
//苹果手机类
class IPhone : Phone{
public void print(){
Debug.Log("IPhone已生产");
}
}
//华为手机类
class HuaweiPhone : Phone{
public void print(){
Debug.Log("Huawei已生产");
}
}
//工厂接口
interface Factory{
//创建手机的函数
Phone CreatePhone();
}
//苹果工厂
class IPhoneFactory : Factory {
public Phone CreatePhone(){
return new IPhone();
}
}
//华为工厂
class HuaweiFactory : Factory {
public Phone CreatePhone(){
return new HuaweiPhone();
}
}
public class Main(){
Factory factory = new IPhoneFactory();
Phone phone = factory.CreatePhone();
phone.print();//IPhone已生产
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vth0qNYD-1675819698781)(D:\简历\工厂模式类图.png)]
定义:
向客户端提供一个接口,使客户端在不同指定产品的具体情况下,创建多个产品族(具有相同属性的同类型产品)的产品对象。
优点:
缺点:产品族扩展十分困难,要增加某一系列的产品,既要在抽象工厂和抽象产品里加代码,又要在具体实现里加代码。
定义:对象之间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。
案例:
//借方接口
interface Debit{
//借钱方法
void Borrow(Credit credit);
//通知借钱者
void NotifyCredits();
}
//借钱者
class Zhangsan : Debit
{
· //借钱人名单
private List<Credit> credits = new List<Credit>();
private bool isRich = false;
public void Borrow(Credit credit){
//借钱逻辑
credits.Add(credit);
}
public void NotifyCredits(){
//通知借钱者逻辑
foreach(Credit c in credits){
c.takeMoney();
}
}
}
//被借钱者
interface Credit{
void TakeMoney();
}
class Lisi : Credit{
public void TakeMoney(){
//要钱逻辑
}
}
该模式下大多数是异步的,且订阅者在订阅事件时,只关注事件本身,而不关注发布事件的人。发布者在发布的时候,也只会关注事件本身,而不关注谁订阅了该事件。该模式相比 观察者模式 完全隔离了订阅者和发布者的依赖关系。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4hSve9kv-1675819698782)(D:\简历\发布订阅者模式.png)]
5.1 饿汉模式
public class Singleton {
//在类加载的时候实例化,类加载只有一次
private static Singleton instance = new Singleton();
public static Singleton Instance
{
get
{
return instance;
}
}
}
缺点:
1.执行慢。2.脚本执行顺序无法保证,调用语句在类加载之后会空指针。
5.2 懒汉模式
public class Singleton{
//在调用指定实例方法时再实例化,效率略高于饿汉模式
private static Singleton instance;
public sttaic Singleton Instance
{
get
{
if (instance == null)
instance = new Singleton();
return instance;
}
}
}
缺点:
1.多线程,同步调用单例,判断不存在就会同时生产,生成多个实例,但可以用双向加锁解决。
5.3 饱汉模式
//直接new出来,不管你需不需要。
5.4 单例模式的优缺点
特点:
① 私有的构造方法(防止外部初始化)
② 私有的静态的实例化变量
③ 公有的静态的获取类实例对象属性
缺点:
①不适用于变化的对象,如果同一类型对象总是在不同用例场景发生变化,就会引起数据错误。
② 单例模式没有抽象层,所以扩展困难,一定程度上违背了 单一原则。
优点:
① 某些类创建比较复杂,对一些大型对象,这是一笔很大的系统开销。省去了new,降低系统内存使用频率,减轻GC压力。
使用场景:
声音播放,UI显示,资源加载,计时器等等…
定义 : 目标对象 和 代理对象 实现同一个接口,通过访问代理对象 来访问真正需要的目标对象。
实例代码:假设一辆汽车具有车载日志功能。我们用代理模式模拟其过程。
//汽车以及车载工具都具有移动功能
interface I_Moveable {
void Move();
}
//汽车类
class Car : I_Moveable {
public void Move(){/*汽车行驶逻辑*/ }
}
//日志代理类
class LogProxy : I_Moveable {
private I_Moveable car;
public LogProxy (I_Moveable car){
this.car = car;
}
public void Move(){
Log();
car.Move();
}
//记录日志代理方法
private void Log(){
Debug.Log("日志开始记录");
}
//装饰器模式:在原有功能上进行功能拓展,而不改变原有结构
public void Speak(){
//扩展逻辑
car.Move();
}
}
定义:命令模式(Command Pattern)是一种数据驱动的设计模式,它属于行为型模式。 请求以命令的形式包裹在对象中,并传给调用对象。 调用对象寻找可以处理该命令的合适的对象,并把该命令传给相应的对象,该对象执行命令。
案例说明:使用 远程遥控器 发出 命令 给 命令的执行者 执行对应逻辑,从而实现发出命令的人和命令执行者之间的 解耦合。
//命令相关
interface Command{
void execute();
}
//远程遥控器
class RemoteControl {
public Command onCommands[];
public Command offCommands[];
public RemoteControl(){
onCommands = new Command[10]();
offCommands = new Command[10]();
}
//设置对应命令
public void SetCommand(int num,Command on,Command off){
onCommands[num] = on;
offCommands[num] = off;
}
//打开被操作物
public void on(int num){
onCommands[num].execute();
}
//关闭被操作物
public void off(int num){
offCommands[num].execute();
}
}
//被操作物体
class Light{
//开灯逻辑
public void on(){}
//关灯逻辑
public void off(){}
}
//开灯的命令
class LightOnCommand : Command{
private Light light;
public LightOnCommand(Light light){
this.light = light;
}
public void execute(){
light.on();
}
}
//关灯的命令
class LightOffCommand : Command{
private Light light;
public LightOffCommand(Light light){
this.light = light;
}
public void execute(){
light.off();
}
}
//执行者
class Main{
static void Main(string[] args){
Light light = new Light();
RemoteControl control = new RemoteControl();
Command onLight = new LightOnCommand(light);
Command offLight = new LightOffCommand(light);
control.SetCommand(1,onLight,offLight);
control.on(1);
control.off(1);
}
}
定义:
实例:
手机导航算法中,导航会根据用户选择的模式不同而更改不同的算法(骑行,驾车,步行等),对于某些经常变动的算法,我们可以将其抽象为接口,让不同类型的情况继承接口,在子类中书写具体算法。并让具体采用该策略的类去继承该接口,并选择对应需求的子类去调用对应的算法。
代码样例:
//导航策略
interface NavigationStrategy{
void FindWay(Point a,Point b);
}
//公路导航
class NavRoad : NavigationStrategy{
public void FindWay(Point a,Point b){
//公路导航逻辑算法实现
}
}
//骑行导航
class NavRide : NavigationStrategy{
public void FindWay(Point a,Point b){
//骑行导航逻辑算法实现
}
}
//步行导航
class NavWalk : NavigationStrategy{
public void FindWay(Point a,Point b){
//步行导航逻辑算法实现
}
}
//手机类(包含导航功能)
class Telephone{
public NavigationStrategy nav ;
public Telephone(NavigationStrategy nav){
this.nav = nav;
}
//使用手机导航
public void FindWayWithPhone(Point a,Point b){
nav.FindWay(a,b);
}
}
定义:
允许一个对象在其内部状态改变时改变其行为,对象看起来似乎修改了它的类。,状态模式是一种对象行为型模式。
对游戏开发者来说,轻车熟路了。怪物 有限状态机 使用的逻辑即为状态模式。
有限状态机案例:通过FSM来控制怪物或者NPC的AI行径。
//对一个怪物来说,存在静止,追逐,攻击,死亡等状态,在一定条件下切换为对应的状态并做出反应即为AI的基础功能。
//状态的接口(包含进入时,进行时,结束时)
interface IState{
void EnterDo();
void UpdateDo();
void ExitDo();
}
//静止状态
class State_Idle : IState{
//状态机参数
public FSM fsm;
public State_Idle(FSM fsm){
this.fsm = fsm;
}
public void EnterDo(){
//进入静止状态时执行的逻辑
}
public void UpdateDo(){
//静止状态下帧更新逻辑
}
public void ExitDo(){
//退出该状态时执行的逻辑
}
}
//攻击状态
class State_Attack : IState{
public void EnterDo(){
//进入攻击状态时执行的逻辑
}
public void UpdateDo(){
//攻击状态下帧更新逻辑
}
public void ExitDo(){
//退出该状态时执行的逻辑
}
}
//怪物状态枚举
enum E_EnemyState{
Idle,Chase,Attack,Dead
}
//在怪物的脚本类里面去引用状态机
class FSM : MonoBehaviour{
public IState currState;
//存储状态
public Dictionary<E_EnemyState,IState> states = new Dictionary<E_EnemyState,IState>();
void Start(){
states.Add(E_EnemyState.Idle,new State_Idle(this));
states.Add(E_EnemyState.Attack,new State_Attack(this));
}
//对外提供转换状态的逻辑
public void TransitionState(E_EnemyState state){
if (currState != null)
currState.ExitDo();
currState = states[state];
currState.EnterDo();
}
}
定义:部分整体模式,用于把一组相似对象当作单一的对象。依据树形模式结构来组合对象,用来表示部分以及整体层次。多用于:你想表达对象的部分-整层次结构。你希望用户忽略组合对象与单个对象的不同,用户统一使用组合结构中的所有对象。
例如:不同界面的逻辑和UI通过同一个管理器统一控制。
典例:人口普查,县级汇总上报省级,省级汇总上报国家。从而形成一种树形模式。
**定义:**全名 Model View Controller,一种软件设计规范。
主要是用一种业务逻辑,数据,界面显示分离的方法组织代码,将业务逻辑聚集到一个不见里面,再改进和个性化定制界面及用户交互同时,不需要重新编写业务逻辑。
在同一个模块内:
MVC一般流程:
关于MVC各层之间对应的设计模式:
优点:
缺点:
定义:全称 Entity-Component-System(实体-组件-系统),是基于组合由于继承的一种模式,将不变的模块使用继承方便复用,将多变的部分用组合来方便拓展。
游戏中每个单元都是一个(怪物,相机等)实体,每个实体又是由一个或多个组件构成的。每个组件仅包含该组件需要关注的数据(例如技能组件保存技能伤害,范围等),而系统用来处理这些实体的集合,其只存逻辑,不存状态,类似于一个工具,例如技能系统会根据遍历到的每一个拥有技能组件的实体,根据状态执行技能。
优点:
缺点:
1.1. Awake:
概述:Awake用于游戏开始之前初始化变量或者游戏状态,在脚本周期中仅被调用一次(当脚本被禁用时同样会被调用)。
使用说明:Awake会在所有对象被初始化后调用,所以可以安全地与其他对象交互。一般情况下,在Awake中设置脚本间的引用,并用Start函数来传递信息。Awake在Start之前进行,并且无法执行协同程序。程序内的所有Awake函数会以随机顺序被调用。
1.2. OnEnable:
当对象变为可用或激活状态时被调用事件监听。
1.3. Start:
概述:同Awake在生命周期中仅被调用一次。但仅仅在脚本时里启用状态下才会调用。常常与Awake协同实现初始化顺序的控制。
1.4. Update:
帧更新函数,游戏运行每帧被调用一次。
1.5. FixedUpdate:
固定帧更新函数。每隔固定的时间间隔被调用一次,FU是逻辑帧,而Update是渲染帧。如果帧率很低,Update调用次数减少,会导致FU被调用多次。相反的,帧率过高,可能在Update更新频率中不会调用FixedUpdate。另外,U和FU独立计时器。
1.6. LateUpdate:
LU生命周期中晚于Update执行。常常用于摄像机相关的操作。避免出现摄像机先于玩家移动从而出现空帧的情况。
1.7. OnGUI:
1.8. OnDisable:
作用与OnEnable函数作用相反。不过多阐述。
1.9. OnDestroy:
当脚本所挂载对象被销毁时调用。
脚本生命周期执行次序:
Awake --> OnEnable --> Start --> FixedUpdate --> Update --> LateUpdate --> OnGUI --> OnDisable --> OnDestroy
必要条件:两个碰撞物体必须存在格子的碰撞器Collider,并且至少一个对象要有Rigidbody刚体组件。
3.1. 进程:
保存在硬盘上的程序运行后,会在内存中形成一个独立的内存体,这个内存体有自己的堆,不同进程间可以相互通信,上级单位是操作系统。一个应用程序相当于一个进程,操作系统以进程为单位,分配系统资源(CPU时间片,内存),进程是资源分配的最小单位。
3.2. 线程:
线程属于进程,是程序的实际执行者。**线程是操作系统能够进行运算调度的最小单位。**一个进程可以并发多个线程,每条线程执行不同的任务。线程有自己独立的栈和共享的堆,不共享栈。
3.3. 协程:
3.3.1:概述
协程是伴随主线程一起运行的一段程序。协程之间是并行执行的,协程与主线程也是并行执行,同一时间只能执行一个协程。提起协程,一定要想到线程,因为协程伴随主线程执行。一个协程可以拥有多个协程,协程不受操作系统控制,完全由程序控制。协程与线程一样,共享堆不共享栈。
3.3.2:与线程的对比
相同点:独立栈不独立堆。有自己的局部变量,有自己的指令指针。
不同点:
协程:协程与其他协程程序共享全局变量等信息。同一时间只能执行一段协程,开辟多个协程开销不大,协程常用来 对某任务进行分时处理。
线程:同一时间可以执行多个线程,开辟多条线程开销很大。线程适合多任务同时处理。
3.3.3 协程的作用:
在Unity中只有主线程才能访问U3D对象,方法,组件。当主线程资源消耗过大时,游戏出现卡顿,帧率下降等。为了解决这个麻烦,可以使用协程来处理资源的加载等繁琐工作。常常用于异步加载资源或场景。
3.3.4. 协程的应用:
//定义
IEnumerator Test(string str){
yield return null;
}
//启动协程
StartCoroutine(string methodName);
StartCoroutine(IEnumerator routine);
//停止协程
StopCoroutine(IEnumerator routine);
StopAllCoroutine();
3.3.5 协程的原理
协程的本质是 迭代器。
协程的特征:
① 方法的返回值是IEnumerator。②方法中有yield关键字。方法内的内容被 MoveNext()方法分为两部分;yield之前的代码会在第一次MoveNext()时执行,yield之后的代码会在第二次MoveNext()时执行。协程以帧为单位执行,也就是说每一帧都会判断当前帧是否满足协程所定义的条件。
4.1. Transform相关
① Transform.position:
//最基础的移动方式,当前位置每帧 += 计算好的新位置
public float speed = 3f;
void Update(){
transform.position += transform.forward * Time.deltaTime * speed;
}
② Transform.Translate:
//直接调用API,原理与第一种类似
public float speed = 3f;
void Update(){
transform.Translate(Vector3.forward * Time.deltaTime * speed);
}
4.2. Vector3相关
移动本质:仍然是改变物体的position。
③ Vector3.Lerp:
//两个向量之间的线性插值,适用于某点移动得到某点,缓动但是非匀速
public Transform target;
public float speed = 3f;
void Update(){
transform.position = Vector3.Lerp(transform.position,target.position,Time.deltaTime * speed);
}
④ Vector3.Slerp:
//两个向量之间的弧形插值,缓动,当前位置与目标位置越远,效果越明显,非匀速
public Transform target;
public float speed = 3f;
void Update(){
transform.position = Vector3.Slerp(transform.position,target.position,Time.deltaTime * speed);
}
⑤ Vector3.MoveTowards:
//与Lerp基本相同,但此函数多了个速度限制,并且是匀速到达目标,而Lerp和Slerp是到达时会放缓。
public Transform target;
public float speed = 3f;
void Update(){
transform.position = Vector3.MoveTowards(transform.position,target.position,Time.deltaTime * speed);
}
//如果实现移动可以采用以下算法
transform.position = Vector3.MoveTowards(transform.position,transform.position + transform.forward * Time.deltaTime * speed,Time.deltaTime * speed);
⑥ Vector3.SmoothDamp:
//平滑阻尼,无比丝滑的从A到B,速度可控,适用于摄像机跟随。
public Transform target;
public Vector3 currVelocity = Vector3.zero; //当前速度
public float smoothTime = 0.3f;//所需时间
void Update(){
transform.position = Vector3.SmoothDamp(transform.position,target.position,ref currVelocity,smoothTime);
}
4.3. Rigidbody相关:
移动原理:通过物理模拟来控制物体的位置,使用该组件相关移动时,在FixedUpdate方法中更新数据。
⑦ AddForce:
public Vector3 force;
public Rigidbody rb;
void FixedUpdate(){
rb.AddForce(force,ForceMode.Force);
}
⑧ MovePosition:
//移动刚体到一个新位置,会受物理影响
public Vector3 speed ;
public Rigidbody rb;
void FixedUpdate(){
rb.MovePosition(transform.position + speed * Time.deltaTime);
}
⑨ Velocity:
//瞬间给一个物体恒定的速度,将该物体提升到这个速度,保持。但相比跳跃功能,使用AddForce效果更好,因为AddForce是恒定高度。
5.1. 用Find查找游戏对象:
GameObject go = GameObject.Find("对象名");
5.2. 用标签查找:
GameObject go = GameObject.FindGameObjectWithTag("对象Tag");
5.3. 用类型查找:
T t = GameObject.FindObjectOfType<T>();
6.1. Invoke:
Invoke函数是U3D的一种委托机制。
使用Invoke需要注意的:
6.2. InvokeRepeating:
该方法被激活时设置了,但此时将对象设置为false,还会被执行!
6.3. Invoke和协程的区别:
Invoke:执行并没有被挂起,相当于设置完被调用函数的执行时间后即时向下执行。应用到每隔一段时间执行某个函数很方便。
Coroutine:新开一条执行序列并挂起,等待中断指令结束。开销不大。当需要挂起当前执行时使用,效率比Invoke高。
6.4. 隐藏物体与禁止脚本对于协程和Invoke的影响:
① 禁止脚本:
当协程被开启 或 Invoke执行后,关闭脚本开关,此时二者都会正常运行。
② 隐藏物体:
Invoke会正常运行,但Coroutine是伴随物体对象而存在的,协程不会正常运行!
7.1. 使用步骤:
计算机网络体系大致分为以下三种:OSI,TCP/IP,五层模型。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-evm0xsxU-1675819698785)(D:\简历\计算机网络层级.png)]
各层级的描述与解释:
应用层:为应用程序提供交互服务。应用层协议有很多,比如:域名系统DNS,支持万维网应用的HTTP协议,支持电子邮件的SMTP协议。
表示层:主要负责数据格式的转换,如 加密解密,转换翻译,压缩解压等。
会话层:负责在网络中两节点之间建立,维持和中止通信,如服务器验证用户登录。
运输层:向主机进程提供通用的数据传输服务。
– TCP:提供面向连接的,可靠的数据传输服务。
– UDP:提供无链接的,尽最大努力的数据传输服务,但无法保证传输可靠性。
网络层:选择合适的路由和交换节点,确保数据及时传送,主要包括IP协议。
数据链路层:简称链路层。将网络层传下来的IP数据包组装成帧,并在相邻链路上传送帧。
物理层:实现相邻节点间比特流的透明传输,尽可能屏蔽传输介质和通信手段的差异。
| UDP | TCP | |
|---|---|---|
| 是否连接 | 否 | 是 |
| 是否可靠 | 否 | 是,使用流量控制和拥塞控制 |
| 是否有序 | 无序 | 有序,有可能乱但会重排 |
| 传输速度 | 快 | 慢 |
| 连接对象个数 | 一对一,一对多,多对一,多对多 | 只能一对一 |
| 传输方式 | 面向报文 | 面向字节流 |
| 首部开销 | 小,仅8字节 | 最小20,最大60字节 |
| 使用场景 | 实时应用(IP电话,视频会议) | 可靠传输的环境 文件传输) |
总结:
TCP和UDP各有其好处与缺点,需要按照需求选择使用。
TCP和UDP各自应用场景:
TCP:
UDP :
一些常见的应用层协议:
| 应用层协议 | 应用 | 传输类型 |
|---|---|---|
| SMTP | 电子邮件 | TCP |
| TELNET | 远程终端接入 | TCP |
| HTTP | 万维网 | TCP |
| FTP | 文件传输 | TCP |
| DNS | 域名转换 | UDP |
| TFTP | 文件传输 | UDP |
| SNMP | 网络管理 | UDP |
| NFS | 远程文件服务器 | UDP |
3.1. 概述:
Three-way Handshake,是指建立一个TCP连接时,需要客户端和服务器总共发送3个包。三次握手的目的是链接服务器指定端口,建立TCP链接,并同步连接双方的顺序号和确认号并交换TCP信息。
3.2. 图解TCP三次握手:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K03J2WBC-1675819698785)(D:\简历\TCP三握详解.png)]
3.3. 机制详解:
理想状态下,TCP连接一旦建立,通信双方任何一方主动关闭连接之前,TCP连接都将被一直保持。
3.4. 为什么不是两次或者四次握手?
三个原因 ↓
1.访止已过期的连接请求报文突然又传送到服务器,因而产生错误和资源浪费。
解释:在双方两次握手即可建立连接的情况下,假设客户端发送A报文段请求建立连接,由于网络原因造成A暂时无法到达服务器,服务器接收不到请求报文段就不会返回确认报文段。
客户端在长时间得不到应答的情况下重新发送请求报文段B,这次B顺利到达服务器,服务器随即返回确认报文并进入ESTABLISHED状态,客户端在收到确认报文后也进入ESTABLISHED状态,双方建立连接并传输数据,之后正常断开连接。
这时姗姗来迟的A报文段才到达服务器,服务器随即返回确认报文并进入ESTABLISHED状态,但是已经进入CLOSED状态的客户端无法再接受确认报文段,更无法进入ESTABLISHED状态,这将导致服务器长时间单方面等待,造成资源浪费。
2.三次握手才能让双方均确认自己和对方的发送和接收能力都正常。
解释:① 客户端只是发送处请求报文段,什么都无法确认,而服务器可以确认自己的接收能力和对方的发送能力正常;
② 客户端可以确认自己发送能力和接收能力正常,对方发送能力和接收能力正常;
③ 服务器可以确认自己发送能力和接收能力正常,对方发送能力和接收能力正常;
可见三次握手才能让双方都确认自己和对方的发送和接收能力全部正常,这样就可以愉快地进行通信了。
3.告知对方自己的初始序号值,并确认收到对方的初始序号值。
解释:TCP实现了可靠的数据传输,原因之一就是TCP报文段中维护了序号字段和确认序号字段,通过这两个字段双方都可以知道在自己发出的数据中,哪些是已经被对方确认接收的。这两个字段的值会在初始序号值得基础递增,如果是两次握手,只有发起方的初始序号可以得到确认,而另一方的初始序号则得不到确认。
3.5 谈谈SYN洪泛攻击:
概述:
SYN洪泛攻击属于DOS攻击的一种,利用TCP协议缺陷,发送大量的半连接请求,耗费CPU和内存资源。
原理:
检测方法:当服务器上看到大量半连接状态时,特别是源IP地址随机,则基本可以断定是一次SYN攻击。
防范:
3.6 三握阶段若最后一次ACK包丢失,会发生什么?
服务端:
客户端:
4.1 图解:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8r4KJvsu-1675819698785)(D:\求职资料\四次挥手.png)]
4.2 过程详述:
TIME-WAIT 状态,此时客户端到TCP连接还没有释放,必须经过2*MSL(最长报文段寿命)时间后,才进入CLOSED状态。而服务端只要收到客户端确认,就立即进入CLOSED状态,可以看到,服务端结束TCP的时间比客户端早一点点。总结:“我先撤了”,“收到”,“我也撤了”,“好嘞”。
服务器收到客户端的FIN报文后,可能还有一些数据要传输,所以不能马上关闭连接,但是会做出响应,返回ACK报文段。接下来会继续发送数据,数据发送完后,服务器向客户端发送FIN报文,表示数据已经发送完成,请求关闭连接,服务端的ACK和FIN一般会分开发送,从而导致多了一次,因此需要四次挥手。
9.1 为什么客户端的TIME-WAIT必须等待?
1.确保ACK报文能够到达服务端,从而使服务器正常关闭连接。
第四次挥手时,客户端第四次挥手的ACK报文不一定会到达服务端。服务端会超时重传FIN / ACK报文,此时如果客户端已经断开了连接,那么就无法相应服务端的二次请求,这样服务器迟迟收不到FIN/ACK的确认报文,就无法正常断开连接。MSL是报文段在网络上存活最长时间。客户端等待2MSL后,即【客户端ACK报文1MSL超时 + 服务端FIN报文1MSL传输】,就能收到服务器重传的FIN/ACK报文,然后客户端重传一次ACK报文,并重新启动2MSL计时器。如此保证服务器正常关闭。如果服务端重发的FIN没有成功在2MSL时间里传给客户端,服务端则会超时直到断开连接。
2.防止已失效的连接请求报文段出现在之后的连接中。
TCP要求在2MSL内不使用相同的序列号,客户端在发送完最后一个ACK报文端后,再经过时间2MSL,就可以保证本连接的持续时间内产生所有的报文段都从网络中消失。这样就可以使下一个连接中不会出现旧的连接报文段。或者即使收到这些过时的报文,也可以不处理它。
9.2 如果已经建立连接,但客户端出故障了怎么办?
分析题目换个问法,如果三握四挥的包丢失了怎么办?如“客户端重发FIN丢失”的问题。简而言之:通过定时器 + 超时重试机制 ,尝试获取确认,直到最后会自动断开连接。具体来说:TCP设有一个保活计时器,服务器每收到一次客户端的数据,就会重新复位计时器,时间通常设置为两个小时,若两个小时还没收到客户端的任何数据,服务器就开始重试,每隔75分钟发送一个检测报文段,若一连发送10个报文后客户端依旧没有回应,那么服务器就认为连接已经断开。
9.3 TIME-WAIT状态过多会产生什么后果?
服务端:
短时间内关闭大量Client连接,就会造成服务器上出现大量TIME-WAIT连接,严重消耗服务器资源,此时部分客户端就会显示连接不上。
客户端:
导致端口资源被占用,因为端口就65535个,被占满就会导致无法创建新的连接。
解决办法:
9.4 TIME_WAIT是服务端状态还是客户端?
TIME_WAIT是主动断开连接的一方进入的状态,一般情况下,都是客户端所处的状态,服务器端一般设置不主动关闭连接。
TIME_WAIT需要等待2MSL,大量短连接的情况下,TIME_WAIT会太多,这也会消耗很多系统资源。对服务器来说,再HTTP协议里指定KeepAlive(浏览器会用一个TCP处理多个HTTP请求),由浏览器主动断开连接,可以一定程度减少服务器这个问题。
TCP主要提供了检验和,序列号/确认应答,超时重传,滑动窗口,拥塞控制和流量控制等方法实现了可靠性传输。
定义:
进行数据传输时,如果传输的数据比较大,就需要拆分多个数据包进行发送。TCP协议需要对数据进行确认后,才可发送下一个数据包。这样一来,就会再等待确认应答包环节浪费时间。
解决方案:
为了避免在该情况,TCP引入了窗口概念。窗口大小指不需要等待确认应答包而可以继续发送数据包的最大值。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Srue6Ofe-1675819698786)(D:\求职资料\滑动窗口.png)]
左边是已经发送并且被确认的分组,滑动窗口右边是还没轮到的分组。
随着信息传输的过程,窗口部分不断向右移动,让还没有轮到的分组进入窗口内部。可以看到滑动窗口起到了一个限流的作用,也就是说当前滑动窗口的大小决定了当前TCP发送包的速率,而滑动窗口的大小却决于拥塞控制窗口和流量控制窗口两者间的最小值。
TCP一共使用了四种算法来实现拥塞控制:
发送方维持一个叫做拥塞窗口cwnd(congestion window)的状态变量。当cwndssthresh时,改用拥塞避免算法。
慢开始:
不要一开始就发送大量数据,由小到大逐渐增加拥塞窗口的大小。
拥塞避免:
拥塞避免算法让拥塞窗口缓慢增长,即经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1而不是加倍。这样拥塞窗口按线性规律缓慢增长。
快重传:
剔除一些不必要的拥塞报文,提高网络吞吐量。比如接收方再收到一个失序的报文段后立即出发重复确认,而不要等到自己发送数据时捎带确认。规定:发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段。而不需要继续等待重传计时器时间到期。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IJslSPgd-1675819698786)(D:\求职资料\快重传机制.png)]
快恢复:
主要配合快重传。当发送方连续收到三个重复确认时,就执行“乘法减小算法”,把ssthresh门限减半(为了预防网络发生拥塞),但接下来并不执行慢开始算法,如果网络出现拥塞的话就不会收到好几个重复确认,收到三个重复就确认网络状态正常。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Kj166Tva-1675819698786)(D:\求职资料\快恢复算法.png)]
总结:
301和302的区别:
相同点:
301和302都表示重定向,浏览器拿到服务器返回的状态码后会自动跳转到一个新的URL地址,这个地址可以从Location首部获取(用户看到自己输入的地址A变到了另一个地址B)。
不同点:
301表示旧地址A资源以及被永久移除了,搜索引擎在抓取新的内容同时也将旧的网址A跳转到B。302表示A资源还在(仍旧可以访问),这个重定向临时从A转到了B,搜索引擎会抓取新的内容而保存旧的地址。
发生情况:
GET和POST的区别:
幂等性:一次或多次去请求某一个资源应具有同样的副作用。意味着对统一URL的多个请求应该返回同样的结果。
HTTP/1.0中,默认使用短连接。也就是说,浏览器和服务器每次进行一次HTTP操作,就建立一次连接,但任务结束就中断连接。如果客户端浏览器访问的某个HTML或其他类型的Web页中包含其他Web资源,如JS,图像,CSS等,当浏览器遇到这样一个Web资源,就会建立一个HTTP对话。
HTTP/1.1起,默认使用长连接,用以保持连接特性。使用长连接的HTTP协议,会在响应头中加入 Connection:keep-alive。
在使用长连接的情况下,当一个网页打开完成后,客户端与服务端间用于传输HTTP数据的TCP连接不会关闭,如果客户端再次访问这个服务器上的网页,会继续使用这一条已经建立的连接。Keep-Alive不会永久保持连接,他有一个保持时间。实现长连接要服务端和客户端都支持长连接。
HTTP协议长短连接本质是 TCP协议的长短连接。
请求报文格式:
GET/samp1e.jspHTTP/1.1 请求行
Accept:image/,gif.image/jpeg, 请求头部
Accept-Language:zh-cn
Connection:Keep-Alive
Host:localhost
User-Agent:Mozila/4.0(compatible;MSIE5.01;window NT5.0)
Accept-Encoding:gzip,deflate
username=shawn&password=6666 请求主体
响应报文格式:
HTTP/1.1 200 OK
Server:Apache Tomcat/5.0.12
Date Mon,60ct2003 13:23:42 GMT
Content-Length:112
<htm1>
<head>
<tit1e>HTTP响应示例<tit1e>
</head>
<body>
Hel1o HTTP!
</body>
</htm1>
HTTP1.0 & HTTP 1.1对比:
HTTP1.1 & HTTP2.0对比:
| \ | HTTP | HTTPS |
|---|---|---|
| 协议 | 运行在TCP | 运行在SSL,SSL又在TCP之上 |
| 端口 | 80 | 443 |
| 安全性 | 无加密,安全性差 | 有加密机制,安全性高 |
| 资源消耗 | 较少 | 更多 |
| 是否需要证书 | 不需要 | 需要 |
HTTPS优缺点:
优点:
安全性:
使用HTTPS协议可认证用户和服务器,确保数据发送到正确的客户机和服务器;
HTTPS协议是由SSL + HTTP协议构建的可进行加密传输,身份认证的网络协议,比HTTP安全,防止数据在传输过程中不被窃取,改变,保证数据的完整性。
HTTPS是现行框架下最安全的解决方案,虽然不是绝对安全,但大幅增加了中间人攻击的成本。
SEO:
采用HTTPS加密的网站在搜索结果中排名会更高。
缺点:
HTTPS加密原理:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ilbMK6Gy-1675819698786)(D:\Downloads\HTTPS原理.png)]
总结:服务器使用密钥(随机码KE)对数据进行对称加密并发送给客户端,客户端使用相同的密钥(随机码KEY)解密数据。
1.域名解析:域名 —> ip地址
浏览器搜索自己的DNS缓存(一张域名与IP的的对应表);如果没有,则搜索操作系统的DNS缓存;如果没有,搜索操作系统的hosts文件。如果都没呀,则找tcp/ip参数中设置的首选DNS服务器,即本地DNS服务器,如果没有,将本地DNS服务器将IP返回给操作系统,缓存IP。
2.发起TCP的三握,建立连接。浏览器会以一个随机端口向服务器的web程序80端口发起TCP连接。
3.建立TCP连接后发起HTTP请求。
4.服务器响应HTTP请求后,客户端得到HTML代码。服务器web应用程序收到HTTP请求后,就开始处理请求,之后返回给浏览器HTML文件。
5.浏览器解析HTML代码,并请求HTML中的资源。
6.浏览器对页面渲染,并呈现给用户。
Cookie:
HTTP Cookie是服务器发送到用户浏览器并保存在本地的一小块数据,他会在浏览器下次向同一服务器发起请求时携带并发送到服务器上。通常,其用于告知服务端两个请求是否来自同一浏览器,如保持用户的登陆状态。主要用于以下三个方面:
Session:
Session代表当前服务器和客户端一次会话的过程。Session对象存储特定用户会话所需的属性及其配置信息。这样,当用户在应用程序的Web之间跳转时,存储在Session对象中的变量不会丢失,而是整个用户会话中一直存在下去。当客户端关闭会话,或者Session超时失效时会话结束。
Cookie和Session如何配合?
用户的第一次请求服务器时,服务器根据用户提交的相关信息,创建的对应的Session,请求返回时将此Session的唯一标识信息SessionID返回给浏览器,浏览器接收到ID后,会将此信息存入Cookie,同时Cookie记录该Session属于哪个域名。
当用户第二次访问服务器时,请求会自动判断该域名下是否存在Cookie信息,如果存在将Cookie信息也发送给服务端,服务端会从Cookie中获取SessionID,再根据SessionID查找对应的Session信息,如果没有找到,说明用户没有登录或失效,如果找到,则登录成功。
总结:Cookie —存储—> SessionID —查找—> Session
Cookie和Session的区别
如何防止分布式Session的问题:
在互联网公司为了可以支撑更大的流量,后端往往需要多台服务器共同来支撑前端用户请求,那如果用户在A服务器登录了,第二次请求跑到服务B就会出现登录失效问题。
分布式Session一般会有以下几种解决方案:
DDOS:
DDos全称Distributed Denial of Service,分布式拒绝服务攻击。最基本的DOS攻击过程如下:
DDoS则是采用分布式的方法,通过在网络上占领多台“肉鸡”,用多台计算机发起攻击。
DOS攻击现在基本没啥作用了,因为服务器的性能都很好,而且是多台服务器共同作用,V1的模式黑客无法占上风。对于DDOS攻击,预防方法有:
XXS:
XSS也称cross-site scripting,跨站脚本。这种攻击是由于服务器将攻击者存储的数据原原本本地显
示给其他用户所致的。比如一个存在XSS漏洞的论坛,用户发帖时就可以引入带有
SQL注入:
SQL注入就是在用户输入的字符串中加入SQL语句,如果在设计不良的程序中忽略了检查,那么这些注入进去的SQL语句就会被数据库服务器误认为是正常的SQL语句而运行,攻击者就可以执行计划外的命令或访问未被授权的数据。
SQL注入的原理主要有以下4点:
避免SQL注入的一些方法:
多台服务器以对称的方式组成一个服务器集合,每台服务器有等价的地位,能互相分担负载。
进程定义:进程好比一个程序,它是操作系统分配资源的最小单位,他的同一时刻执行的进程数不会超过CPU核心数。
线程定义:程序内任务与任务之间的关系,线程依赖于进程,是程序执行过程中的最小单元。独立调度的基本单位。
并发:一段时间内,多个任务都会被处理;但在某一时刻,只有一个任务在执行。单核处理器可以做到并发。比如:两个进程A,B,A运行在一个时间片之后,切换到B,B运行一个时间片后又切换到A。切换速度足够快,所以宏观表现为一段时间能同时运行多个程序。
并行:同一时刻,多个任务在执行。需要多核处理器才能完成,在微观上能同时执行多条指令,不同程序被放到不同处理器上运行,这个是物理上多个进程同时进行。
进程:
线程:
对linux来说,进程和线程最大区别就是 地址空间,对于线程切换,上述第一步不需要做,第二部是进程线程切换都需要做的。
对比:
由于每个进程有自己的虚拟地址空间,线程是共享所在进程的虚拟地址空间,在同一个进程中的线程进行线程切换时不涉及虚拟地址空间的转换。
为什么虚拟地址空间切换费时:
进程都有自己的虚拟地址空间,把虚拟地址转换为物理地址需要查找页表,页表查找是一个很慢的过程,因此通常使用Cache来缓存常用的地址映射,这样可以加速页表查找,这个Cache就是TLB(translation Lookaside Buffer,TLB本质上就是一个Cache,是用来加速页表查找的)。
由于每个进程都有自己的虚拟地址空间,那么显然每个进程都有自己的页表,那么当进程切换后页表也要进行切换,页表切换后TLB就失效了,Cache失效导致命中率降低,那么虚拟地址转换为物理地址就会变慢,表现出来的就是程序运行会变慢,而线程切换则不会导致TLB失效,因为线程无需切换地址空间,因此我们通常说线程切换要比较进程切换块,原因就在这里。
5.1. 管道
概述:这种方式分为两类。匿名管道和命名管道。匿名管道是单向的,只能在有亲缘关系的进程间通信;命名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。
优缺点:速度慢,容量有限。
5.2. 信号量
概述:一个计数器,可以用来控制多个进程对共享资源的访问。常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
优缺点:不能传递复杂消息,只能用来同步。
5.3 消息队列
概述:消息的链接表,有足够权限的进程可以向队列中添加消息,被赋予读权限的进程可以读队列中的消息,消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。
优缺点:容量收到系统限制,注意第一次读的时候,要考虑上一次有没有读完数据的问题。
5.4 共享内存
概述:映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的IPC方式,他是针对其他进程间通信方式运行效率低专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步与通信。
优缺点:很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通信,不过没这个必要。
5.5. Socket
概述:可用于不同机器间的进程通信。
优缺点:任何进程间都可以通信,但速度慢。
6.1 临界区:通过多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。
优点:保证某一时刻只有一个线程能访问数据。
缺点:虽然临界区同步速度快,但却只能用来同步本进程内的线程,不可用来同步多个进程的线程。
6.2 互斥量:为协调共同对一个共享资源的单独访问而设计。互斥量跟临界区相似,比临界区复杂,互斥对象只有一个,只有拥有互斥对象的线程才具有访问资源的权限。
优点:使用互斥不仅仅能在统一应用程序不同线程中实现资源的安全共享,而且可以在不同应用程序的线程之间实现对资源的安全共享。
缺点:
6.3 信号量:为控制一个具有有限数量用户资源而设计。它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。互斥量是信号量的一种特殊情况,当信号量的最大资源数=1就是互斥量了。
优点:适用于Socket(套接字)程序中线程的同步。
缺点:
6.4 事件:用来通知线程有一些事件已发生,从而启动后继任务的开始。
优点:事件对象通过通知操作的方式来保持线程的同步,并且可以实现不同进程中的线程同步操作。
7.1 临界区:当多个线程访问一个独占性共享资源时,可以使用临界区对象。拥有临界区的线程可以访问被保护起来的资源或代码段,其他线程若想访问,则被挂起,直到拥有临界区的线程放弃临界区为止,以此达到用原子方式操作共享资源的目的。
7.2 事件:事件机制,则允许一个线程在处理完一个任务后,主动唤醒另外一个线程执行任务。
7.3 互斥量:互斥对象和临界区对象非常相似,只是其允许在进程间使用,而临界区只限制与同一进程的各个线程之间使用,但是更节省资源,更有效率。
7.4 信号量:当需要一个计数器来限制可以使用某共享资源的线程数目时,可以使用”信号量"对象。
区别:
从线程的运行空间来说,分为用户级线程(user-level thread,ULT)和内核级线程(kernel-level,KLT)
内核级线程:这类线程依赖于内核,又称为内核支持的线程或轻量级进程。无论是在用户程序中的线程还是系统进程中的线程,它们的创建、撤销和切换都由内核实现。比如英特尔5-8250U是4核8线程,这里的线程就是内核级线程
用户级线程:它仅存在于用户级中,这种线程是不依赖于操作系统核心的。应用进程利用线程库来完成其创建和管理,速度比较快,操作系统内核无法感知用户级线程的存在。
临界区:每个进程中访问临界资源的那段程序称为临界区,一次仅允许一个进程使用的资源称为临界资源。
解决冲突的办法:
概念:在两个或者多个并发进程中,如果每个进程持有某种资源而又等待其它进程释放它或它们现在保持着的资源,在未改变这种状态之前都不能向前推进,称这一组进程产生了死锁。通俗的讲就是两个或多个进程无限期的阻塞、相互等待的一种状态。
产生条件:
如何处理:常用的处理死锁的方法有:死锁预防、死锁避免、死锁检测、死锁解除、鸵鸟策略。
进程一共有5种状态,分别是创建、就绪、运行(执行)、终止、阻塞。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U80pGpiM-1675819698787)(D:\简历\进程状态图.png)]
运行态→阻塞态:往往是由于等待外设,等待主存等资源分配或等待人工干预而引起的。
阻塞态→就绪态:则是等待的条件已满足,只需分配到处理器后就能运行。
运行态→就绪态:不是由于自身原因,而是由外界原因使运行状态的进程让出处理器,这时候就变成就绪态。例如时间片用完,或有更高优先级的进程来抢占处理器等。
就绪态→运行态:系统按某种策略选中就绪队列中的一个进程占用处理器,此时就变成了运行态。
分页:
把内存空间划分为大小相等且固定的块,作为主存的基本单位。因为程序数据存储在不同的页面中,而页面又离散的分布在内存中,因此需要一个页表来记录映射关系,以实现从页号到物理块号的映射。
访问分页系统中内存数据需要两次的内存访问(一次是从内存中访问页表,从中找到指定的物理块号,加上页内偏移得到实际物理地址;第二次就是根据第一次得到的物理地址访问内存取出数据)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vlMGIS4o-1675819698787)(D:\求职资料\分页.png)]
分段:
分页是为了提高内存利用率,而分段是为了满足程序员在编写代码的时候的一些逻辑需求(比如数据共享,数据保护,动态链接等)。
分段内存管理当中,地址是二维的,一维是段号,二维是段内地址;其中每个段的长度是不一样的,而且每个段内部都是从0开始编址的。由于分段管理中,每个段内部是连续内存分配,但是段和段之间是离散分配的,因此也存在一个逻辑地址到物理地址的映射关系,相应的就是段表机制。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MzjMP4n9-1675819698787)(D:\求职资料\分段.png)]
分段和分页的对比:
交换空间:
操作系统把物理内存(physical RAM)分成一块一块的小内存,每一块内存被称为页(page)。当内存资源不足时,Linux把某些页的内容转移至硬盘上的一块空间上,以释放内存空间。硬盘上的那块空间叫做交换空间(swap space),而这一过程被称为交换(swapping)。物理内存和交换空间的总容量就是虚拟内存的可用容量。
用途:
13.1 物理地址:
物理地址就是内存中真正的地址,它就相当于是你家的门牌号,你家就肯定有这个门牌号,具有唯一性。不管哪种地址,最终都会映射为物理地址。
13.2 线性地址 & 虚拟地址:
在实模式下,段基址+段内偏移经过地址加法器的处理,经过地址总线传输,最终也会转换为 物理地址。
在保护模式下,段基址+段内偏移被称为 线性地址,不过此时的段基址不能称为真正的地址,而是会被称作为一个选择子的东西,选择子就是个索引,相当于数组的下标,通过这个索引能够在GDT中找到相应的段描述符,段描述符记录了段的起始、段的大小等信息,这样便得到了基地址。如果此时没有开启内存分页功能,那么这个线性地址可以直接当做物理地址来使用,直接访问内存。如果开启了分页功能,那么这个线性地址又多了一个名字,这个名字就是虚拟地址。
13.3 有效地址 & 逻辑地址
不论在实模式还是保护模式下,段内偏移地址都叫做有效地址。有效地址也是逻辑地址。
线性地址可以看作是虚拟地址,虚拟地址不是真正的物理地址,但是虚拟地址会最终被映射为物理地址。下面是虚拟地址->物理地址的映射。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9RL6qfo2-1675819698787)(D:\Downloads\虚拟地址与物理地址的映射.png)]
在程序运行过程中,如果要访问的页面不在内存中,就发生缺页中断从而将该页调入内存中。此时如果内存己无空闲空间,系统必须从内存中调出一个页面到磁盘对换区中来腾出空间。
最后两种算法都使用了工作集算法。工作集算法提供了合理的性能开销,但是它的实现比较复杂。WSc1ock是另外一种变体,它不仅能够提供良好的性能,而且可以高效地实现。
总结:最好的算法是老化算法和WSClock算法。他们分别是基于LRU和工作集算法。他们都具有良好的性能并且能够被有效的实现。还存在其他一些好的算法,但实际上这两个可能是最重要的。
缓冲区溢出是指当计算机向缓冲区填充数据时超出了缓冲区本身的容量,溢出的数据覆盖在合法数据上。
危害有以下两点:
造成缓冲区流出的主要原因是程序中没有仔细检查用户输入。
虚拟内存是什么:
虚拟内存就是说,让物理内存扩充成更大的逻辑内存,从而让程序获得更多的可用内存。虚拟内存使用部分加载的技术,让一个进程或者资源的某些页面加载进内存,从而能够加载更多的进程,甚至能加载比内存大的进程,这样看起来好像内存变大了,这部分内存其实包含了磁盘或者硬盘,并且就叫做虚拟内存。
如何实现虚拟内存:
虚拟内存中,允许将一个作业分多次调入内存。采用连续分配方式时,会使相当一部分内存空间都处于暂时或永久的空闲状态,造成内存资源的严重浪费,而且也无法从逻辑上扩大内存容量。因此,虚拟内存的实需要建立在离散分配的内存管理方式的基础上。虚拟内存的实现有以下三种方式:
IO多路复用是指内核一旦发现进程指定的一个或者多个O条件准备读取,它就通知该进程。O多路复用适用如下场合:
硬链接:
在目录下创建一个条目,记录着文件名与Index编号,这个index就是源文件的index,删除任意一个条目,文件依然存在,只要引用数量不为0,但是硬链接有限制,无法跨越文件系统,也不能对目录进行链接。
软链接:
符号链接文件保存着源文件所在的绝对路径,在读取时会定位到源文件上,可以理解为windows的快捷方式。当源文件被删除了,链接文件就打不开了。因为记录的是路径,所以可以为目录建立符号链接。
如何处理中断:
中断和轮询的区别:
轮询:CPU对特定设备轮流询问,效率低等待时间长,CPU利用率不高。
中断:通过特定事件提醒CPU。容易遗漏问题,CPU利用率不高。
所有的用户进程都是运行在用户态的,但是我们上面也说了,用户程序的访问能力有限,一些比较重要的比如从硬盘读取数据,从键盘获取数据的操作则是内核态才能做的事情,而这些数据却又对用户程序来说非常重要。所以就涉及到两种模式下的转换,即**用户态->内核态->用户态,而唯一能够做这些操作的只有系统调用**,而能够执行系统调用的就只有操作系统。
一般用户态->内核态的转换我们都称之为trap进内核,也被称之为陷阱指令(trap instruction)。
他们的工作流程如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2EPZSmaS-1675819698787)(D:\求职资料\用户态与内核态.png)]
对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶段:
正式因为这两个阶段,linux系统产生了下面五种网络模式的方案:
其中,IO多路复用模型指的是:使用单个进程同时处理多个网络连接IO,他的原理就是select、poll、epoll不断轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。该模型的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。
select,poll,epoll的区别:
(1) select:时间复杂度O(n)
select仅仅知道有I/O事件发生,但并不知道是哪几个流,所以只能无差别轮询所有流,找出能读出数据或者写入数据的流,并对其进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。
(2) poll:时间复杂度O(n)
poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,但是它没有最大连接数的限制,原因是它是基于链表来存储的。
(3)epoll:时间复杂度O(1)
epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以说epoll实际上是事件驱动(每个事件关联上的。select,pol,epoll都是IO多路复用的机制。I/O多路复用就是通过一种机制监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),就通知程序进行相应的读写操作。
但select,poll,epoll本质上都是同步/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步/O则无需自己负责进行读写,异步/O的实现会负责把数据从内核拷贝到用户空间。
--函数闭包案例
function f1(x)
--改变了变量的生命周期,x本应被销毁回收
return function(y)
return x + y;
end
end
f1(10)(5) --15
--变长参数列表
function f2( ... )
args = { ... }
for i = 1,#args do
print(args[i])
end
end
f2(666,"hhh",false,nil)
总结一句话:闭包原理会延长Lua函数方法变量的生命周期。
--ipairs:通过索引遍历,,无法找到 <= 0的自定义索引,并且一旦下标断序,后面的内容都无法找到。
a = {[0] = 0,[-1] = -1,2,3,4}
for i,j ipairs(a) do
print(i .. j)
end
--输出内容:1_2,2_3,3_4
--Table = 哈希表 + 数组
local t = {[1]=1,2,[3]=3,4,[5]=5,[6]=6}
--拆分如下:
--哈希表:{[1] = 1,[3] = 3,[5] = 5,[6] = 6}
--数组:{2,4} ,这数组中元素放入哈希表重新匹配得遍历结果为
-- {[1] = 2,[2] = 4,[3] = 3,[5] = 5,[6] = 6}
print('ipairs')
for index, value in ipairs(t) do
print(value) --2,4,3
end
--pairs:遍历到所有内容,但自定义索引会放在最后
for i,j pairs(a) do
print(1 .. j)
end
--输出内容:1_2,2_3,3_4,0_0,-1_-1
如果我既需要解决顺序遍历的问题,也需要遇到nil遍历完全,使用什么方法遍历
for index = 1,maxSize do
if table[index] ~= nil then
--对应处理--
end
end
Student = {
age = 18,
sex = true,
Learn = function(t)
print("好好学习" .. t.age)
end
}
Student.name = "shawn"
--调用函数时,使用:相当于默认给形参添加了一个自己类型的值,以下两句代码意义相同。
Student.Learn(Student) -- 好好学习18
Student:Learn() -- 好好学习18
--声明函数时,也可以使用以上规则
Student.Speak = function()
print("说话1")
end
--self关键字代表使用:时默认传入的第一个参数
function Student:Speak2()
print("说话2".. self.name)
end
Student.Speak() -- 说话1
Student:Speak2() --说话2shawn
总结:在表内部访问自己的变量时,必须指定是哪个对象的相关属性,否则会找不到,尽管同样在类的内部。**: **在使用时,会把自己当作第一个参数传入函数。
--封装
--一个学生类(表)(三种属性定义方法)
Student = {id = 1}
Student.name = "Shawn"
Student["sex"] = true
--给学生类添加一个new对象的方法
function Student:new()
local stu = {} --本质是定义个空表
self.__index = self;
setmetatable(stu,self)--把对象设为类的子表
return stu
end
--测试模块
local myStu = Student:new()
print(myStu["name"]) --Shawn
--修改对象的属性
myStu.id = 999 --修改子表(对象)的属性
print(myStu["id"]) --999
--继承
--C#继承语法:class 类名 : 父类
--写一个用于继承的函数
function Student:subClass( className )
_G[className] = {} --在大G表中设置一个空表(新的子类)
local stu = _G[className]
self.__index = self
setmetatable(stu,self)
end
--测试模块
Student:subClass("pupil") --定义一个小学生子类
print(pupil.name) --Shawn
--修改子类的值,其值便不顺着其元表修改而修改
pupil["name"] = "小学生"
print(pupil.name) --小学生
--多态
--相同行为,多种表现
Student:subClass("Player")
Player.x = 0
Player.y = 0
function Player:Move( value )
self.x = self.x + value
self.y = self.y + value
print(self.x)
print(self.y)
end
--高级玩家是玩家的子类,其移动方法相比不同玩家强
--方法重写
Player:subClass("SuperPlayer")
function SuperPlayer:Move()
self.base.Move(self , 2)
end
local sp1 = SuperPlayer:new()
sp1:Move()--2,2
sp1:Move()--4,4
local sp2 = SuperPlayer:new()
sp2:Move()--2,2
6.1 如何实现浅拷贝
6.2 如何实现深拷贝
核心思想逻辑:使用递归遍历表中所有元素。
function copy_Table(obj)
function copy(obj)
if type(obj) ~= "table" then
return obj;
end
local newTable = {};
for k,v in pairs(obj) do
newTable[copy(k)] = copy(v);
end
return setmetatable(newTable,getmetatable(obj));
end
return copy(obj)
end nm
2.1. 为什么使用Lua作为热更新语言,不用C#实现?
1.语言性质问题:
热更新本身对于资源热更是很容易的,Unity自带的AB包就可以实现,麻烦的是代码热更新。由于C#是编译型语言,Unity在打包后,会将C# 编译成一种中间代码,再由Mono虚拟机编译成汇编代码供给各个平台执行,打包后就变成二进制了,跟着程序同时启动,无法进行更改。
Lua是解释型语言 ,并不需要事先编译成块,而是运行时动态解释执行的,这样Lua就和游戏资源没有区别,因此可以在运行时直接从Web服务器上下载到持久化目录呗其他Lua文件调用。
2.平台限制原因:
C#只能在安卓上实现热更,苹果不能。在安卓上可以通过反射机制实现动态代码加载从而实现热更。实现原理是将频繁更改的逻辑部分独立出来做成DLL,在主模块调用这些DLL,只有作为业务模块的DLL需要修改。游戏运行时通过这些反射机制加载就实现了热更。
苹果平台对于反射机制有限制,无法实现这样的热更。之所以限制反射,是因为 反射功能太过强大,会给系统带来很大的安全隐患。
ILRuntime(C#热更):
该项目基于C#平台提供了一个纯以实现快速,方便的IL运行时,使得其在不支持JIT的硬件环境(IOS等)能够实现代码的热更。
编译原理:将代码分为两个dll文件,启动时仅启动一个,另一个dll通过反射启动,在修改后使用第二个dll将第一个dll替换掉,达到一个热更效果。
2.1. 导出热更的流程
① 打包热更资源对应的md5信息。
② 上传热更AB包到热更服务器。
③ 上传版本信息到版本服务器。
2.2. 游戏热更流程
① 启动游戏
② 根据当前版本号,和平台号去版本服务器上检查是否有热更。
③ 从热更服务器上下载MD5文件,比对需要热更的具体文件列表。
④ 从热更服务器上下载需要热更的资源,解压到热更新源目录。
⑤ 游戏运行加载资源,优先到热更目录中加载,再到母包资源加载。
无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD
本教程将在Unity3D中混合Optitrack与数据手套的数据流,在人体运动的基础上,添加双手手指部分的运动。双手手背的角度仍由Optitrack提供,数据手套提供双手手指的角度。 01 客户端软件分别安装MotiveBody与MotionVenus并校准人体与数据手套。MotiveBodyMotionVenus数据手套使用、校准流程参照:https://gitee.com/foheart_1/foheart-h1-data-summary.git02 数据转发打开MotiveBody软件的Streaming,开始向Unity3D广播数据;MotionVenus中设置->选项选择Unit
HashMap中为什么引入红黑树,而不是AVL树呢1.概述开始学习这个知识点之前我们需要知道,在JDK1.8以及之前,针对HashMap有什么不同。JDK1.7的时候,HashMap的底层实现是数组+链表JDK1.8的时候,HashMap的底层实现是数组+链表+红黑树我们要思考一个问题,为什么要从链表转为红黑树呢。首先先让我们了解下链表有什么不好???2.链表上述的截图其实就是链表的结构,我们来看下链表的增删改查的时间复杂度增:因为链表不是线性结构,所以每次添加的时候,只需要移动一个节点,所以可以理解为复杂度是N(1)删:算法时间复杂度跟增保持一致查:既然是非线性结构,所以查询某一个节点的时候
Unity自动旋转动画1.开门需要门把手先动,门再动2.关门需要门先动,门把手再动3.中途播放过程中不可以再次进行操作觉得太复杂?查看我的文章开关门简易进阶版效果:如果这个门可以直接打开的话,就不需要放置"门把手"如果门把手还有钥匙需要旋转,那就可以把钥匙放在门把手的"门把手",理论上是可以无限套娃的可调整参数有:角度,反向,轴向,速度运行时点击Test进行测试自己写的代码比较垃圾,命名与结构比较拉,高手轻点喷,新手有类似的需求可以拿去做参考上代码usingSystem.Collections;usingSystem.Collections.Generic;usingUnityEngine;u
之前说过10之后的版本没有3dScan了,所以还是9.8的版本或者之前更早的版本。 3d物体扫描需要先下载扫描的APK进行扫面。首先要在手机上装一个扫描程序,扫描现实中的三维物体,然后上传高通官网,在下载成UnityPackage类型让Unity能够使用这个扫描程序可以从高通官网上进行下载,是一个安卓程序。点到Tools往下滑,找到VuforiaObjectScanner下载后解压数据线连接手机,将apk文件拷入手机安装然后刚才解压文件中的Media文件夹打开,两个PDF图打印第一张A4-ObjectScanningTarget.pdf,主要是用来辅助扫描的。好了,接下来就是扫描三维物体。将瓶
关闭。这个问题不符合StackOverflowguidelines.它目前不接受答案。要求我们推荐或查找工具、库或最喜欢的场外资源的问题对于StackOverflow来说是偏离主题的,因为它们往往会吸引自以为是的答案和垃圾邮件。相反,describetheproblem以及迄今为止为解决该问题所做的工作。关闭9年前。Improvethisquestion是否有适用于这些的3d游戏引擎?
西安华为OD面试体验开始投简历技术面试进展工作进展开始投简历去年一整年一直在考研和工作之间纠结,感觉自己的状态好像当时的疫情一样差劲。之前刚毕业的时候投了个大厂的简历,结果一面写算法的时候太拉跨了,虽然知道时dfs但是代码熟练度不够,放在平时给足时间自己可以调试通过,但是熟练度不够那面试当时就写不出来被刷了。说真的算法学到后期我感觉最重要的是熟练度和背板子(对于我这种普通玩家来说),面试题如果一上来短时间内想不出思路就完蛋了。然后由于当时找的工作不是很理想就又想考研了。但是考研是有风险的,我自我感觉自己可能冲不上那个学校,而找工作一个没成可以继续找嘛。本着抱着试试看的态度在boss上投了简历,
文章目录1.自动驾驶实战:基于Paddle3D的点云障碍物检测1.1环境信息1.2准备点云数据1.3安装Paddle3D1.4模型训练1.5模型评估1.6模型导出1.7模型部署效果附录show_lidar_pred_on_image.py1.自动驾驶实战:基于Paddle3D的点云障碍物检测项目地址——自动驾驶实战:基于Paddle3D的点云障碍物检测课程地址——自动驾驶感知系统揭秘1.1环境信息硬件信息CPU:2核AI加速卡:v100总显存:16GB总内存:16GB总硬盘:100GB环境配置Python:3.7.4框架信息框架版本:PaddlePaddle2.4.0(项目默认框架版本为2.3
安全产品安全网关类防火墙Firewall防火墙防火墙主要用于边界安全防护的权限控制和安全域的划分。防火墙•信息安全的防护系统,依照特定的规则,允许或是限制传输的数据通过。防火墙是一个由软件和硬件设备组合而成,在内外网之间、专网与公网之间的界面上构成的保护屏障。下一代防火墙•下一代防火墙,NextGenerationFirewall,简称NGFirewall,是一款可以全面应对应用层威胁的高性能防火墙,提供网络层应用层一体化安全防护。生产厂家•联想网御、CheckPoint、深信服、网康、天融信、华为、H3C等防火墙部署部署于内、外网编辑额,用于权限访问控制和安全域划分。UTM统一威胁管理(Un
2022年10月21日星期五【数据指标】加密货币总市值:$0.95万亿BTC市值占比:38.51%恐慌贪婪指数:23极度恐慌 【今日快讯】1、【政讯】1.1.1、美联储布拉德:市场预期美联储11月会加息75个基点1.1.2、美联储哈克:将维持加息一段时间1.2、美国10年期国债收益率触及4.197%,为2008年6月以来最高1.3、法国数字转型部长:政府将专注于DeFi和Web31.4、巴西ATM机将于11月3日起支持USDT1.5、美众议院副议长将于11月初加入a16zCrypto担任政府事务主管1.6、香港数字资产托管机构FirstDigitalTrust首席执行官:香港仍是安全