核心基础知识1

图片相关

安卓选择ETC2 8bit

苹果选择ETC PVRTC 4 bit,

RGBA32,32代表RGBA4个通道总共32位,每一个通道是8位。

通常图片的格式有jpg和png,jpg代表的是有损压缩无透明;png无损压缩有透明。显示同一张图的png要比jpg大;jpg和png是两种压缩方式。论你导入什么格式的图片,unity都会转化成纹理格式的图片;

为什么要转化成纹理呢,因为png和jpg,无法直接被GPU解压的,需要先转为为纹理格式,纹理格式是针对GPU的压缩格式,GPU是能够直接读取的;

一个字节是8位,一位是一个bit,一个字节8bit;

图片的格式实际代表的是一个像素占了多大内存或者说是占了几个字节

1024*1024的图作为参考,来看一下图片内存的计算,选择的格式位RGBA32,表示每一个像素占了32位,每个像素4字节;

1024*1024个像素*4个字节=1k*1024*4=1M*4=4M;贴图占用的内存;

RGBA Compressed ETC2 8 bits:使用ETC2来压缩RGBA,每个像素占8位(1个字节),压缩后1M

RGB Compressed ETC2 4 bits:使用ETC2来压缩RGB,每个像素占4位(0.5个字节),压缩后0.5M。

ASTC是像素块,一个Block是16个字节4*4 Block是宽4*高4的像素块

每个块有16个像素点,一个像素点占一个字节;1024*1024个像素就是一兆;

技能部分

三个模块:技能模块,buff模块,projectile(子弹)模块。

主动技能,被动技能,开关类技能,激活类技能

被动技能在技能初始化的时候生效

技能触发接口:AI或者UI操作触发

技能选目标,AI或者UI触发技能:不需要目标,指向性目标,指定地点AOE技能。

普通施法技能OnInitSkill()

技能流程:

1、前摇过程,有前摇时长,StartSkill()前摇阶段不允许其它技能释放,除非技能可强制立即释放bImmediately=true,前摇阶段配置时长,startSkillTime = 0.3s。

2、技能结算或者施法产生具体的技能效果(创建子弹,触发buff,创建法术场)。

3、后摇过程,有时间点,结束。后摇一般不能被打断,除非是连招或者强制释放类技能。

持续施法类技能:持续施法时长ContinuousTime;

1、技能前摇阶段StartSkill(),有前摇时长

2、引导开始ContinuousStart,引导持续阶段IntervalTime调用一次PlayerContinueousSKill()

3、达到ContinuousTime后调用ContinuousEnd,进入后摇阶段。

技能表现:动作,特效。

Buff部分

BaseBuff基类,ModifyAttriBuff继承自BaseBuff(修改角色的动态属性或者状态),MotionModifyBuff继承自ModifyAttriBuff。

BaseBuff包含:Caster(Buff施加者),Parent(Buff当前挂载的目标), skill(Buff由哪个技能创建),BuffNum(层数), BuffLevel(等级)BuffDuration(时长),BuffTag(基于标记位,标注这个Buff属于那些种类以及免疫哪些种类)BuffImmuneTag(免疫BuffTag)以及Context(Buff创建时的一些相关上下文数据)等等。

Buff执行流程:

1、Buff是否可创建,目标身上是否免疫此buff。

2、BuffAwake()事件,不加入buff容器中,比如收到负面效果,驱散所有负面效果,并加护盾。还未生效就销毁。

3、Buff加入到容器,OnBuffStart()

4、添加相同类型,Caster相等时Buff刷新OnBuffRefresh()

5、Buff销毁前,还没从Buff容器中移除,OnBuffRemove(),从Buff容器中移除OnBuffDestroy()

buff修改状态ModifyState:

1、眩晕Stun,2、定身Root  3、沉默Silence  4、无敌Invincible  5、隐身Invisible

Buff修改属性:一般将玩家的属性划分为两层,第一层时Core(核心层),第二层是External(外部层)。Core层是玩家各个其他模块的属性总和,而External层则是Buff修改属性的总和。两者相加既为玩家的实时属性。

buff修改运动:击飞时不能被击退,击飞过程又能被冰冻效果定住,然后又有破冰技能击退冰冻物体并解除冰冻效果。

运动都是通过MovementComponent来管理,MotionModifyBuff来与MovementComponent交互,MotionModifyBuff中有俩接口ApplyMotion(motionTypeId,priority, forceInterrupt)来向运动组件请求运动效果

参数含义:motionTypeId:运动类型id,配置项。包含运动位移参数及相关数据。

                   priority:运动优先级,每个运动都有优先级,低优先级不能打断高优先级。

                   forceInterrrupt:是否忽略优先级,强制打断当前的Motion。

击退的运动优先级是100,击飞的运动优先级是200。那么在击飞过程中,施加击退Buff调用ApplyMotion的时候会返回false,这时可以销毁掉这个击退Buff,即击飞时无法击退。如果击飞时被冰冻,且冻在半空中停止不动,那么我们就需要设计一个静止Buff:运动优先级是300,作用效果是速度设置为0,不受重力影响,同时修改Stun状态并挂载冰冻特效当破冰技消除冰冻效果时,则设置破冰Buff的位移效果为击退,设置运动优先级为100,forceInterrupt为true。此时ApplyMotion强制打断运动,冰冻Buff会触发

OnMotionInterrupt回调,在此接口中冰冻Buff自我销毁即可。

Buff监听事件

1、OnSkillExecuted(),监听某个主动技能执行成功

2、OnGiveDamageBefore(),OnGeiveDamageAfter(),监听我方对目标造成伤害。

3、OnTakeDamageBefore(),OnTakeDamageAfter(),监听我方收到伤害触发。

4、OnDeadBefore(),OnDeadAfter()监听我方死亡时触发

5、OnKill(),监听我方击杀目标触发

数据结构

栈和堆的应用举例,回文ABCDCBA

    class Program {
        static void Main(string[] args)
        {
            string str = Console.ReadLine();
            Stack<char> stack = new Stack<char>();
            Queue<char> queue = new Queue<char>();
            for (int i = 0; i < str.Length; i++)
            {
                stack.Push(str[i]);
                queue.Enqueue(str[i]);
            }
            bool isHui = true;
            while (stack.Count>0)
            {
                if (stack.Pop() != queue.Dequeue())
                {
                    isHui = false;
                    break;
                }
            }
            Console.WriteLine("是否是回文字符串:"+isHui);
            Console.ReadKey();
        }
    }

算法

Lua相关的

Lua 面试题_lua面试题_北海6516的博客-CSDN博客

一、Lua怎么热更的?

Lua代码可以直接在Lua虚拟机里运行

动态装载:app + 内置脚本解释器,由这个脚本解释器动态的执行脚本代码

二、点和冒号的区别

1、没用到self的时候,调用方法一样

2、用到self的方法,通过点调用第一个参数为当前的table
     冒号是语法糖,通过冒号调用系统会自动传递当前的table给self

三、pairs和ipairs区别

pairs遍历table里的所有元素,ipairs只遍历前面连续的元素pairs和ipairs的区别?

四、如果获取table长度?tabl内容是连续的可以用#,存在key-value形式的就需要遍历计数

五、table的排序

按照value值来排序,使用 table.sort(needSortTable, func) 即可,可以根据自己的需要重写func

local test_table = {2,1,3,"SORT","sort"}  
table.sort(test_table , function(a , b)
        return tostring(a) > tostring(b)
    end) 

 六、Lua和C#底层如何交互

C#与Lua进行交互主要通过虚拟栈实现,栈的索引分为正数与负数,若果索引为正数,则1表示栈底,若果索引为负数,则-1表示栈顶。

C# Call Lua:C#把请求或数据放在栈顶,然后lua从栈顶取出该数据,在lua中做出相应处理(查询,改变),然后把处理结果放回栈顶,最后C#再从栈顶取出lua处理完的数据,完成交互。

Lua Call C#:(1)Wrap方式:首先生成C#源文件所对应的Wrap文件,由Lua文件调用Wrap文件,再由Wrap文件调用C#文件。
(2)反射方式:当索引系统API、dll库或者第三方库时,如果无法将代码的具体实现进行代码生成,可采用此方式实现交互。缺点:执行效率低。

七、Lua如何实现面向对象的?

Lua中的类通过table和function模拟出来,继承通过 __index和setmetatable模拟出来,多态通过重写父类的方法模拟。

一、线程,协程

一个应用程序一般对应一个进程,一个进程一般有一个主线程,还有若干个辅助线程,线程之间是平行运行的,在线程里面可以开启协程,让程序在特定的时间内运行。

多线程程同时运行多个线程 ,而在任一指定时刻只有一个协程在运行,并且这个正在运行的协同程序只在必要时才被挂起。只有主线程可以访问Unity3D的对象、组件、方法。

Unity3d没有多线程的概念,不过unity也给我们提供了StartCoroutine(协同程序)和LoadLevelAsync(异步加载关卡)后台加载场景的方法。 StartCoroutine为什么叫协同程序呢,所谓协同,就是当你在StartCoroutine的函数体里处理一段代码时,利用yield语句等待执行结果,这期间不影响主程序的继续执行,可以协同工作。而LoadLevelAsync则允许你在后台加载新资源和场景,所以再利用协同,你就可以前台用loading条或动画提示玩家游戏未卡死,同时后台协同处理加载的事宜同步。

协程避免了无意义的调度,由此可以提高性能

yield return 语句只表示“暂时地”退出方法——事实上,你可把它看成暂停。 

能从主线程中访问Unity3D的组件,对象和Unity3D系统调用,C#中有lock这个关键字,以确保只有一个线程可以在特定时间内访问特定的对象,所有只能从主线程中访问。

如果同时你要处理很多事情或者与Unity的对象互动小可以用thread,否则使用coroutine。

 

二、值类型,引用类型

值类型:结构体,枚举,数值类型

引用类型:object,string,数组,自定义类,接口,委托

1.   值类型的数据存储在内存的栈中;引用类型的数据存储在内存的中,而内存单元中只存放堆中对象的地址。

2.    值类型存取速度快,引用类型存取速度慢。

3.    值类型表示实际数据,引用类型表示指向存储在内存堆中的数据的指针或引用

4.    值类型继承自System.ValueType,引用类型继承自System.Object

5.    栈的内存分配自动释放;而堆在.NET中会有GC(垃圾回收)来释放
 

三、堆栈

1.heap是堆,程序运行期间动态分配的内存空间,手动申请和释放的(GC)

stack是栈,编译期间就分配好的内存空间,系统自动分配和释放

   heap常用new关键字来分配。

3.stack空间有限,heap的空间是很大的自由区。

栈通常保存着我们代码执行的步骤,堆则不然,像是一个仓库,储存着我们使用的各种对象等信息

四、类和结构体区别

Class表现为行为,Struct长用于存储,传递参数时,Class按地址传递,Struct按值方式传递

1,Class引用类型,可以继承类接口被继承

      Struct值类型,只能继承接口,不能被继承

2,Class 可以new初始化,结构不能

3,Class有默认无参数的构造函数,Struct只能声明有参数的构造函数

3,Class可以用Abstrack和sealed,有protected修饰符,Struct没有

5,Class实例由垃圾回收机制保证内存的回收处理,Struct使用完后立即解除内存分配

结构体是一种值类型,而类是引用类型。(值类型、引用类型是根据数据存储的角度来分的)就是值类型用于存储数据的值,引用类型用于存储对实际数据的引用。那么结构体就是当成值来使用的,类则通过引用来对实际数据操作

五、抽象方法,虚方法,接口

抽象类

1.抽象类中的实体方法在子类中不可以重写,只可以被引用

2.抽象类中的抽象方法不可以有方法体,抽象类中的抽象方法在子类中必须重写

namespace Game.View
{
    //抽象类中的抽象方法不能用方法体,在子类中必须重写
    //抽象类中的非抽象方法,子类中只能引用,不能重写
    public abstract class BaseWindow
    {
        protected Transform mRoot;

        protected EScenesType mScenesType; //场景类型
        protected string mResName;         //资源路径名
        protected bool mResident;          //是否常驻 
        protected bool mVisible = false;   //是否可见
      

        //类对象初始化
        public abstract void Init();

        //类对象释放
        public abstract void Realse();

        //窗口控制初始化
        protected abstract void InitWidget();

        //窗口控件释放
        protected abstract void RealseWidget();

        //游戏事件注册
        protected abstract void OnAddListener();

        //游戏事件注消
        protected abstract void OnRemoveListener();

        //显示初始化
        public abstract void OnEnable();

        //隐藏处理
        public abstract void OnDisable();

        //每帧更新
        public virtual void Update(float deltaTime) { }

        //取得所以场景类型
        public EScenesType GetScenseType()
        {
            return mScenesType;
        }

        //是否已打开
        public bool IsVisible() { return mVisible;  }

        //是否常驻
        public bool IsResident() { return mResident; }

        //显示
        public void Show()
        {
            if (mRoot == null)
            {
                if (Create())
                {
                    InitWidget();
                }
            }

            if (mRoot && mRoot.gameObject.activeSelf == false)
            {
                mRoot.gameObject.SetActive(true);

                mVisible = true;

                OnEnable();

                OnAddListener();
            }
        }

        //隐藏
        public void Hide()
        {
            if (mRoot && mRoot.gameObject.activeSelf == true)
            {
                OnRemoveListener();
                OnDisable();

                //常驻内存
                if (mResident)
                {
                    mRoot.gameObject.SetActive(false);
                }
                else
                {
                    RealseWidget();
                    Destroy();
                }
            }

            mVisible = false;
        }

        //预加载的时候创建窗体
        public void PreLoad()
        {
            if (mRoot == null)
            {
                if (Create())
                {
                    InitWidget();//面板中的button响应
                }
            }
        }

        //延时删除
        public void DelayDestory()
        {
            if (mRoot)
            {
                RealseWidget();
                Destroy();
            }
        }

        //创建窗体
        private bool Create()
        {
            if (mRoot)
            {
                Debug.LogError("Window Create Error Exist!");
                return false;
            }

            if (mResName == null || mResName == "")
            {
                Debug.LogError("Window Create Error ResName is empty!");
                return false;
            }

            if (GameMethod.GetUiCamera.transform== null)
            {
                Debug.LogError("Window Create Error GetUiCamera is empty! WindowName = " + mResName);
                return false;
            }

            GameObject obj = LoadUiResource.LoadRes(GameMethod.GetUiCamera.transform, mResName);//UI存放的父物体类

            if (obj == null)
            {
                Debug.LogError("Window Create Error LoadRes WindowName = " + mResName);
                return false;
            }

            mRoot = obj.transform;

            mRoot.gameObject.SetActive(false);

            return true;
        }

        //销毁窗体
        protected void Destroy()
        {
            if (mRoot)
            {
                LoadUiResource.DestroyLoad(mRoot.gameObject);
                mRoot = null;
            }
        }

        //取得根节点
        public Transform GetRoot()
        {
            return mRoot;
        }

    }
}

虚方法

1.必须有方法体,可以在子类选择性的重写;

2.不重写也可被子类调用;

接口

1.不提供方法体

2.不能用关键字修饰

3.不能有接口和变量

4.在子类中必须全部实现

5.多继承

namespace Game.GameState
{
    public interface IGameState
    {
        GameStateType GetStateType();
        void SetStateTo(GameStateType gsType);
        void Enter();
        GameStateType Update(float fDeltaTime);
        void FixedUpdate(float fixedDeltaTime);
        void Exit();
    }
}

抽象类与接口

相同点:

  • 1、被继承
  • 2、不能实例化
  • 3、没有方法体
  • 4、派生类必须实现未实现的方法

区别:

  • 1、抽象基类可以定义字段。接口只能定义不能包含字段
  • 2、抽象类是一个不完整的类,需要进一步细化,而接口是一个行为规范。微软的自定义接口总是后带able字段,证明其是表述一类“我能做。。。”
  • 3、接口可以被多重实现,抽象类只能被单一继承
  • 4、抽象类更多的是定义在一系列紧密相关的类间,而接口大多数是关系疏松但都实现某一功能的类中
  • 5、抽象类是从一系列相关对象中抽象出来的概念事物的内部共性接口是为了满足外部调用而定义的一个功能约定, 因此反映的是事物的外部特性
  • 6、接口基本上不具备继承的任何具体特点,它仅仅承诺了能够调用的方法
  • 7、接口可以用于支持回调,而继承并不具备这个特点
  • 8、抽象类实现的具体方法默认为虚的,但实现接口的类中的接口方法却默认为非虚的,当然您也可以声明为虚的
  • 9、如果抽象类实现接口,则可以把接口中方法映射到抽象类中作为抽象方法而不必实现,而在抽象类的子类中实现接口中方法

抽象方法与虚方法的异同

1.抽象方法不可以有方法体,虚方法必须有方法体

2.抽象方法所在的类必须是抽象类,虚方法可以在任何类里;

2.抽象方法必须被重写,虚方法的重写有选择性

3.抽象方法不可以被子类调用,虚方法可以被子类调用;

六、常用的设计模式

1、工厂模式:所有的对象,都用一个对象去创造

一个Operation类俩doule字段,一个GetResult虚方法

public class Operation
{
    private double number1 = 0;
    private double number2 = 0;
    public double Number1
    {
        get { return number1; }
        set { number1 = value; }
    }
    public double Number2
    {
        get { return number2; }
        set { number2 = value; }
    }
    public virtual double GetResult()
    {
        double result = 0;
        return result;
    }
}

细分类加减乘除继承自Operation,重写虚方法实现两个数加减乘除

class OpertionAdd : Operation
{
    public override double GetResult()
    {
        double result = 0;
        return result=Number1 + Number2;
    }
}

调用工厂类OperationFactory的CreateOperation(string "+-*/")中new 细分类

public class OperationFactory
{
    public static Operation CreateOperation(string operate)
    {
        Operation oper = null;
        switch (operate)
        {
            case "+":
                oper = new OpertionAdd();
                break;
            case "-":
                oper = new OperationSub();
                break;
            case "*":
                oper = new OperationMul();
                break;
            case "/":
                oper = new OperationDiv();
                break;
        }
        return oper; 
    }
}

 运行获取最终结果

    Operation oper;
    oper=OperationFactory.CreateOperation("+");
    oper.Number1=1;
    oper.Number2=2;
    double result = oper.GetResult();

2、策略模式:传入参数====》策略模式(黑箱)====》得到不同答案

定义一个抽象类Srategy作为基类

    public abstract class Strategy
    {
        //算法方法
        public abstract void AlgorithmInterface(); 
    }

不同策略继承自抽象基类实现具体策略类

public class ConcreteStrategyA : Strategy
{
    public override void AlgorithmInterface()
    {
        Console.WriteLine("算法A实现");
    }
}

 策略模式(黑箱)

class Context
{
    Strategy strategy;
    //初始化时传入具体的策略对象
    public Context(Strategy strategy)
    {
        this.strategy = strategy;
    }
    //根据具体的策略对象,调用其算法的方法
    public void ContextInterface()
    {
        strategy.AlgorithmInterface();
    }
}

具体实现,都有一个new的过程

static void Main(string[] args)
    {
        //由于实例化不同的策略,所以最终在调用context.ContextInterface();时,所获得的结果就不尽相同
        Context context;
        context = new Context(new ConcreteStrategyA());
        context.ContextInterface();
 
        context = new Context(new ConcreteStrategyB());
        context.ContextInterface();
 
        context = new Context(new ConcreteStrategyC());
        context.ContextInterface();
    }

3、观察者模式:

通知接口:包含通知方法和状态

//通知者接口
interface Subject
{
    void Notify();
    string SubjectState
    {
        get;
        set;
    }
}

 通知类老板,继承并实现通知接口

delegate void EventHandler();
//老板通知类
public class Boss:Subject
{
    public event EventHandler Update;
    private string action;
    public string SubjectState
    {
        get{return action;}
        set{action=value ;}
    }
    public void Notify()
    {
        Update();//通知时调用更新
    }
 
}

观察者看股票或者新闻的同事:

//看股票的同事
class StockObserver
{
    private string name;
    private Subject sub;
    public StockObserver(string name,Subject sub)
    {
        this.name=name;
        this.sub=sub;
    }
    //关闭股票航行
    public void CloseStockMarket()
    {
         Console.WriteLine("{0}{1}关闭股票行情,继续工作",sub.SubjectState,name);
    }
}

具体实现:把同事a关股票的注册入boss的通知事件中,boss状态改变之后,发个通知。

    static void Main()
    {
        Boss huhansan=new Boss(); 
        StockObserver a=new StockObserver("a",huhansan);
        huhansan.Update+=new EventHandler(a.CloseStockMarket);
        huhansan.SubjectState="我回来了";
        huhansan.Notify();
    }

4、代理模式:

送礼物接口

interface IGiveGift
{
    void GiveDolles();
    void GiveFlowes();
    void GeveChocolate();
}

学校女生

//代理接口
 class SchoolGirl
 {
     private string name;
     public string Name
     {
         get { return name; }
         set { name = value; }
     }
 }

追求者,继承自送礼物接口

//追求者类
 class Pursuit : IGiveGift
 {
     SchoolGirl mm;
     public Pursuit(SchoolGirl mm)
     {
         this.mm = mm;
     }
     public void GiveDolls()
     {
         Console.WriteLine(mm.Name + "送你洋娃娃");
     }
     public void GiveFlowes()
     {
         Console.WriteLine(mm.Name + "送你鲜花");
     }
     public void GiveChocolate() 
     {
         Console.WriteLine(mm.Name + "送你巧克力");
     }
 }

代理类:

class Proxy : IGiveGift
 {
     Pursuit gg;
     public Proxy(SchoolGirl mm)
     {
         gg = new Pursuit(mm);
     }
     public void GiveDolls()
     {
         gg.GiveDolls();
     }
     public void GiveFlowes()
     {
         gg.GiveFlowes();
     }
     public void GiveChocolate() 
     {
         gg.GiveChocolate();
     }
 }

具体实现

        SchoolGirl jiaojiao = new SchoolGirl();
        jiaojiao.Name="jiaojiao";
 
        Proxy daili=new Proxy(jiaojiao);
        daili.GiveDolls();
        daili.GiveFlowes();
        daili.GiveChocolate();

5、建造者模式:

产品类:由多个部件组成 。

class Product
    {
        List<string> Parts = new List<string>();
        public void Add(string part)
        {
            Parts.Add(part);
        }
        public void Show()
        {
            Console.WriteLine("\n产品 创建-----");
            foreach (string part in Parts)
            {
                Console.WriteLine(part);
            }
        }
    }

抽象建造者:产品有两部分组成

    abstract class Builder
    {
        public abstract void BuilderPartA();
        public abstract void BuilderPartB();
        //声明一个得到产品建造后结果的方法
        public abstract Product GetResult();
    }

具体建造者

    class ConcreteBuilder1 : Builder
    {
        Product product = new Product();
        public override void BuilderPartA()
        {
            product.Add("部件A");
        }
        public override void BuilderPartB()
        {
            product.Add("部件B");
        }
        public override Product GetResult()
        {
            return product;
        }
    }

指挥者类

    class Director
    {
        public void Construct(Builder builder)
        {
            builder.BuilderPartA();
            builder.BuilderPartB();
        }
    }

具体实现

public void ShowKeHuDuan()
        {
            Director director = new Director();
            Builder b1 = new ConcreteBuilder1();
 
            director.Construct(b1);
            Product p1 = b1.GetResult();
            p1.Show();
 
            Console.Read();
        }

6、中介者模式:想定制一个分布在多个类中的行为,而又不想生成太多的子类的场合

模拟俩同事之间的对话,通过中介渠道对话。

抽象一个同事类,抽象一个中介类

//抽象同事类
abstract class Colleague
{
    protected Mediator mediator;
    //构造方法得到中介对象
    public Colleague(Mediator mediator)
    {
        this.mediator = mediator;
    }
}

//抽象中介类  
public class Mediator
{
    //定义一个抽象的发送消息方法,得到同事对象和发送消息
    public abstract void Send(string message, Colleague colleague);
}

具体俩同事类

class ConcreteColleague1 : Colleague
{
    public ConcreteColleague1(Mediator mediator)
        : base(mediator)
    {
 
    }
    public void Send(string message)
    {
        mediator.Send(message, this);
    }
    public void Notify(string message)
    {
        Debug.Log("同事1得到消息:" + message);
    }
}
class ConcreteColleague2 : Colleague
{
    public ConcreteColleague2(Mediator mediator)
        : base(mediator)
    {
 
    }
    public void Send(string message)
    {
        mediator.Send(message, this);
    }
    public void Notify(string message)
    {
        Debug.Log("同事1得到消息:" + message);
    }
}

具体中介类

class ConcreteMediator : Mediator
{
    private ConcreteColleague1 colleague1;
    private ConcreteColleague2 colleague2;
 
    public ConcreteColleague1 Colleague1
    {
        set { colleague1 = value; }
    }
    public ConcreteColleague2 Colleague2
    {
        set { colleague2 = value; }
    }
    public override void Send(string message, Colleague colleague)
    {
        if (colleague == colleague1)
        {
            colleague2.Notify(message);
        }
        else
        {
            colleague1.Notify(message);
        }
    }
}

具体实现

        ConcreteMediator m = new ConcreteMediator();
        ConcreteColleague1 c1 = new ConcreteColleague1(m); //让两个具体同事类认识中介者对象
        ConcreteColleague2 c2 = new ConcreteColleague2(m);
        m.Colleague1=c1;//中介者认识各个具体同事类对象
        m.Colleague2=c2;
        c1.Send("吃过饭了吗?");//具体同事类对象发送信息都是通过中介者转发的
        c2.Send("没有呢,你打算请客吗?");

2、单例模式:

七、MVC思想

MVC的核心思想是将一个应用分成三个基本部分:Model(模型)、View(视图)和Controller(控制器),这三个部分以最少的耦合协同工作,从而提高应用的可扩展性及可维护性。  业务逻辑、数据、界面显示分离的方法组织代码,用于传统的输入、处理和输出功能。

Model、Controller、View三部分,他们各自的职责如下。

Model是对应用状态和业务功能的封装,我们可以将它理解为同时包含数据和行为的领域模型(Domain Model),Model接收Controller的请求并完成相应的业务处理,在应用状态改变的时候可以向View发出相应的通知

View实现可视化界面的呈现并最终捕获用户的交互操作(如鼠标、键盘操作);

View捕获到用户的交互操作后直接发给Controller,Controller完成相应的UI逻辑。如果需要涉及业务功能的调用,Controller会直接调用Model.在完成UI处理之后,Controller会更新需要控制原View或者创建新的View对用户交互操作予以响应

mvc模式的核心思想是分离责任,使得数据、视图和逻辑部分分开,模型层关心的只是应用的状态以及业务逻辑而不用考虑数据如何展现给用户;视图层关心的是的只是如何根据模型的变化为用户提供用户界面;控制层则负责接收用户的输入然后将其交给对应的模型,它并不关心用户如何输入以及这些输入数据是如何作用于模型的。

八、UI框架

1、有个链表LinkedList<UIWindowBase> m_OpenWindowList;存放已经打开的UI。

2、打开UI:OpenUIWindow(int uiFormId, object userData, BaseAction<UIFormBase> onOpen),先判断UI是否已经打开IsExists(),是则Return。

3、根据Id先从对象池中获取:GameEntry.UI.Dequeue(uiFormId),获取到之后SetActive(true),调用

uiWindow.Open(userData),再加入到m_OpenWindowList列表。

4、对象池中没有,LoadUIAsset加载资源并实例化出uiObj,设置UI层级,uiObj加入实例资源池GameEntry.Pool.RegisterInstanceResource(uiObj.GetInstanceID(), resourcesEntity)。

5、uiWindow.Init(uiFormId, .UIGroupId, DisableUILayer ==1, IsLock == 1, userData)

执行回调函数onOpen,添加到m_OpenWindowList列表

6、CloseUIForm(int uiFormId),CloseUIForm(UIFormBase formBase),先从m_OpenWindowList移除,层级管理减少层级GameEntry.UI.SetSortingOrder(uibase);调用OnClose();最后回池GameEntry.UI.EnQueue(),closeTime时间之后从对象池中销毁。

九、AssetBundle

1、什么是AssetBundle和Resource有什么区别

Unity中ab包压缩方案 LZMA 和LZ4_lzma和lz4_画个小圆儿的博客-CSDN博客

AssetBundle支持动态下载替换,选择AssetBundle的原因就是,要做热更新,动态更新游戏资源,在需要的时候能够从远端的服务器上按需请求特定的资源,并加载到游戏中。

AssetBundles 可以包括的资源文件有模型文件(models)、材质(materials)、纹理(textures)和场景(scenes)。AssetBundles 不能包含脚本文件。

AssetBundle的.mainfest自身保存着互相的依赖关系

Resources系统非常适于快速开发原型和实验,游戏本身资源不是很多,也不考虑会更新资源,就把资源都放到resource下面。

Resources下可以放一些无需更新且启动时就需要的资源,譬如登陆场景的loading图等,游戏启动的逻辑简洁不易出错。

2、LZMA和LZ4压缩方式对比

LZMA流式压缩,压缩率比LZ4高,包体更小,只支持顺序读取,所以加载AB包时,要整个包解压,会造成卡顿和额外内存占用,LZMA加载AB包,要对所有资源缓存,短时间利用率不高的时候,造成很高的内存浪费

LZ4采用块压缩方式,数据被分为大小相同的块,被分别压缩,压缩率不如LZMA,但是读取效率高,使用LoadFromFile()或LoadFromStream()只会加载AB包的Header,相比于直接加载解压整块AB包

BuildAssetBundleOptions.None:使用LZMA算法压缩,压缩的包更小,但是加载时间更长。使用之前需要整体解压。一旦被解压,这个包会使用LZ4重新压缩。使用资源的时候不需要整体解压。在下载的时候可以使用LZMA算法,一旦它被下载了之后,它会使用LZ4算法保存到本地上。 
BuildAssetBundleOptions.UncompressedAssetBundle:不压缩,包大,加载快 
BuildAssetBundleOptions.ChunkBasedCompression:使用LZ4压缩,压缩率没有LZMA高,但是我们可以加载指定资源而不用解压全部

3、打包资源分配策略

1、UI:字体,预设,UI资源(图集公用,功能划分,Icon),大的背景图

2、场景

3、角色

4、特效,角色,战斗,UI

5、音效:战斗,UI,新手引导的对话

6、数据表,关卡,地图数据,机器人

7、Lua代码,热更代码

我们调用BuildPipeline.BuildAssetBundle来进行打包:

4、打包流程

Object obj = AssetDatabase.LoadMainAssetAtPath("Assets/Test.png");
BuildPipeline.BuildAssetBundle(obj, null,
                                  Application.streamingAssetsPath + "/Test.assetbundle",
                                 BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.CompleteAssets
                                 | BuildAssetBundleOptions.DeterministicAssetBundle, BuildTarget.StandaloneWindows);

    BuildPipeline.BuildAssetBundle有5个参数,第一个是主资源,第二个是资源数组,这两个参数必须有一个不为null,如果主资源存在于资源数组中,是没有任何关系的,如果设置了主资源,可以通过Bundle.mainAsset来直接使用它

    第三个参数是路径,一般我们设置为  Application.streamingAssetsPath + Bundle的目标路径和Bundle名称

    第四个参数有四个选项,BuildAssetBundleOptions.CollectDependencies会去查找依赖,BuildAssetBundleOptions.CompleteAssets会强制包含整个资源,BuildAssetBundleOptions.DeterministicAssetBundle会确保生成唯一ID,在打包依赖时会有用到,其他选项没什么意义

    第五个参数是平台,在安卓,IOS,PC下,我们需要传入不同的平台标识,以打出不同平台适用的包,注意,Windows平台下打出来的包,不能用于IOS

3、加载以及卸载

AssetBundle的如何加载_qq_35647121的博客-CSDN博客

1、WWW的assetBundle就是内部数据读取完后自动创建了一个assetBundle而已

Create完以后,等于把硬盘或者网络的一个文件读到内存一个区域,这时候只是个AssetBundle内存镜像数据块,还没有Assets的概念。

2、用AssetBundle.Load(同Resources.Load) 这才会从AssetBundle的内存镜像里读取并创建一个Asset对象,创建Asset对象同时也会分配相应内存用于存放(反序列化),异步读取用AssetBundle.LoadAsync

4、AssetBundle.Unload(flase)是释放AssetBundle文件的内存镜像,不包含Load创建的Asset内存对象。

AssetBundle.Unload(true)是释放那个AssetBundle文件内存镜像和并销毁所有用Load创建的Asset内存对象。

你 Instaniate一个Prefab,是一个对Assets进行Clone(复制)+引用结合的过程,GameObject transform 是Clone是新生成的。其他mesh / texture / material / shader 等,这其中些是纯引用的关系的,包括:Texture和TerrainData,还有引用和复制同时存在的,包括:Mesh/material /PhysicMaterial。引用的Asset对象不会被复制,只是一个简单的指针指向已经Load的Asset对象。

为什么第一次Instaniate 一个Prefab的时候都会卡一下?

因为在你第一次Instaniate之前,相应的Asset对象还没有被创建,要加载系统内置的 AssetBundle并创建Assets,第一次以后你虽然Destroy了,但Prefab的Assets对象都还在内存里,所以就很快了。

AssetBundle.Load(name): 从AssetBundle读取一个指定名称的Asset并生成Asset内存对象,如果多次Load同名对象,除第一次外都只会返回已经生成的Asset对象,也就是说多次Load一个Asset并不会生成多个副本(singleton)。

AssetBundle.Unload(false):释放AssetBundle文件内存镜像

AssetBundle.Unload(true):释放AssetBundle文件内存镜像同时销毁所有已经Load的Assets内存对象

Reources.UnloadAsset(Object):显式的释放已加载的Asset对象,只能卸载磁盘文件加载的Asset对象

Resources.UnloadUnusedAssets:用于释放所有没有引用的Asset对象

九、资源加载

Unity中的内存种类

实际上Unity游戏使用的内存一共有三种:程序代码、托管堆(Managed Heap)以及本机堆(Native Heap)

程序代码包括了所有的Unity引擎,使用的库,以及你所写的所有的游戏代码。在编译后,得到的运行文件将会被加载到设备中执行,并占用一定内存。

托管堆是被Mono使用的一部分内存。Mono项目一个开源的.net框架的一种实现,对于Unity开发,其实充当了基本类库的角色。托管堆用来存放类的实例(比如用new生成的列表,实例中的各种声明的变量等)。“托管”的意思是Mono“应该”自动地改变堆的大小来适应你所需要的内存,并且定时地使用垃圾回收(Garbage Collect)来释放已经不需要的内存。关键在于,有时候你会忘记清除对已经不需要再使用的内存的引用。

本机堆是Unity引擎进行申请和操作的地方,比如贴图,音效,关卡数据等。Unity使用了自己的一套内存管理机制来使这块内存具有和托管堆类似的功能

优化程序代码的内存占用:

默认的Mono包含库可以说大部分用不上,在Player Setting(Edit->Project Setting->Player或者Shift+Ctrl(Command)+B里的Player Setting按钮)

面板里,将最下方的Optimization栏目中“Api Compatibility Level”选为.NET 2.0 Subset,表示你只会使用到部分的.NET 2.0 Subset,不需要Unity将全部.NET的Api包含进去。

托管堆优化:

托管堆中存储的是你在你的代码中申请的内存(不论是用js,C#还是Boo写的)。

一般来说,无非是new或者Instantiate两种生成object的方法(事实上Instantiate中也是调用了new)。用对象池创建对象。

本机堆的优化

在 Resource.UnloadAsset()和Resources.UnloadUnusedAssets()时,只有那些真正没有任何引用指向的资源 会被回收,因此请确保在资源不再使用时,将所有对该资源的引用设置为null或者Destroy。这两个Unload方法仅仅对Resource.Load拿到的资源有效,而不能回收任何场景开始时自动加载的资源。

Unity会在Asset文件夹相同位置生成资源的.meta文件,用以记录资源设置参数与GUID 。

Unity 2020之后的版本,会在Library下生成ArtifactDB和SourceAssetDB文件。

SourceAssetDB包含.meta相关数据(上次修改日期、文件内容哈希、GUID和其他元数据信息),由此判断是否需要重新导入资源。

ArtifactDB包含每个源资源的导入结果的信息。每个Artifact都包含导入依赖项信息、Artifact元数据信息和 Artifact文件列表。

项目中经常遇到资源引用丢失:(1)美术资源与meta没有一起上传,导致prefab无法通过guid找到资源。(2)美术替换资源时,先删除原有资源,再导入创建新资源,导致原有的prefab引用的guid失效。

1.CPU访问内存是一个慢速过程,因此会使用cache来加速访问,PU如果在Cache中没有找到数据,称为一次Cache Missing,如果内存数据 指令是不连续的,会导致大量的Cache Missing。

Unity的ECS和DOTS的目的之一就是提高内存的连续性,减少Cache Missing。

Native内存最佳实践

  1. scene中GameObject的数量是否过多,数量过多会导致native内存显著增涨,在创建一个GameObject的时候,Unity会在C++中构建一个或者多个的Object来保存相关信息,因此,当发现Native内存过大时,优先检查Scene中的GameObject数量 。
  2. Audio Android设备上常常出现声音延迟过大,优先看这选项DSP buffer。左右声道完全一致 的开启Force to mono,
  3. code size,模板泛型的滥用,编译C++时,会把所有的泛型展开为静态类,如果一个类使用了四个泛型,编译出来的cpp文件可能高达25M,这对il2cpp的编译速度造成很大影响,因为一个单一的cpp文件,是无法并行编译的
  4. AssetBundle

(1)TypeTree

用于不同版本构建的AssetBundle可以在不同版本的Unity上保持兼容,防止序列化出错,如果Build AssetBundle的Unity版本和运行时的版本一致,可以关闭这个功能,关闭之后有三个好处

a.减少内存占用

b.减小包体大小

c.build和运行时会变快,因为当需要序列化有TypeTree的AssetBundle时,会序列化两次,先序列化TypeTree信息,再序列化数据,反序列化也需要两次

(2)LZ4&Lzma

LZ4是一种trunk-base的压缩技术,速度几乎是Lzma的10倍,但是压缩的体积会高出30%,trunk-base的压缩方式,在解压时可以减少内存占用,因为不需要解压整个文件,解压每个trunk的时候,可以复用buffer(在中国增强版中会推出一个基于LZ4的AssetBundle加密功能)

(3)Size&Count

就是AssetBundle的颗粒度控制,尽量减少AssetBundle的数量,可以减少AssetBundle头文件的内存和包体大小占用,有的资源的头文件甚至比数据还大,官方建议一个AssetBundle的大小在1-2M之间,不过这个建议是考虑网络带宽的影响,实际使用可以根据自身的环境设置。

  1. Texture (1)upload buffer,和DSP buffer类似,就是填满多少Texture数据时,向GPU push一次。

(2)没必要不开启read/write,Texture正常的加载流程为加载进内存 -> 解析 -> upload buffer -> 从内存中delete,开启选项后,不会从内存delete,导致内存和显存中都存在一份。

(3)不开启mip maps

  1. mesh

(1),read/write,非必要不开

    (2),compression,有些版本中开了不如不开。

Managed内存最佳实践

Don’t Null it,but Destroy it
不要置空就完事了,记得显式调用Destroy

Class VS Struct
可以关注Unity的DOTS和ECS

Closures and anonymous methods(闭包和匿名函数)

Coroutines(协程)

协程可以看作闭包和匿名函数的特例,在il2cpp中,每一个闭包和匿名函数,都会new一个对象出来,只是无法访问,里面的数据,即使是你认为用完就丢的局部变量,在你用完了之后,也不会立即释放,而是等到这个对象释放才释放,有的项目在游戏一开始就开启一个协程,一直到游戏结束,这样使用是错误的,会导致闭包中的数据一直占用内存,正确的做法是用到的时候生成一个协程,不用的时候就扔掉,协程的设计不是当作线程使用的。

ingleton(单例)

一定要慎用,在C++的年代,这就是万恶之源,不要什么都往这里面扔,会导致内存无法释放,注意单例的引用关系,当引用关系变得复杂时,很难确定哪些东西没有及时释放

十、热更流程

一、加载资源包信息

1、有三个资源包信息,CDN资源包信息,只读区资源包信息,可写区资源包信息。

2、可写区存在,加载可写区,检查更新。

3、只读取在,可写区不在,只读取写到可写区,检查更新。

4、只读区,可写区都不存在,则进入检查更新。

二、检查版本文件

1、版本文件不存在,进入到资源预加载流程。

2、版本文件存在对比版本是否一致,一致进入加载,不一致开始检查更新。

3、进入检查更新,找到要删除的文件删除,找到可写区和CDN资源MD5不一致的文件,判断找到的文件在只读取的MD5和CDN是否一致,一致从只读取更新到可写区,不一致,加入到下载列表,下载并更新可写区版本文件,进入到资源预加载流程。

三、预加载流程

1、判断资源包在可写区是否存在,存在从可写区加载,然后更新可写区版本

2、不存在,检查存在于只读取,存在从只读取加载。不存在,下载到可写区,更新可写区版本文件,从可写区加载。

六、渲染流程

CPU阶段:1、准备场景数据;2、不可见剔除;3、设置渲染状态(场景中的网格是怎么渲染的,比如使用哪个顶点着色器,片原着色器,光源属性,材质等);4、调用Drall。CPU阶段输出渲染的图元列表。

GPU阶段:分为几何阶段和光栅化阶段两部分。

顶点着色器:处理顶点,输入进来的每个顶点都会调用一次顶点着色器。主要进行坐标变换及逐顶点光照。

曲面细分着色器:用于细分图原。

几何着色器:逐图原着色操作。

裁剪:有的物体一部分在视野内,一部分不在,在视野中的需要裁剪,继续向下一流水线传递。

屏幕映射:裁剪后的齐次坐标转换到屏幕坐标,实际上是根据分辨率缩放的过程。

三角形设置:根据三个点设置三角形,计算边界上的像素的坐标信息。

三角形遍历:计算被三角形覆盖的像素,覆盖的每个像素都会形成一个片元,然后根据顶点信息对这些片元的像素进行插值运算。

片元着色器:对插值的结果进行着色,输出一个或多个颜色值。

逐片元操作:片元的可见性进行模板测试和深度测试,通过测试的片元的颜色值和已经储存在颜色缓冲区的色彩进行合并。

忽视下面的*****************************************************************************************************

CPU 应用阶段

 硬盘====》内存====》显存====》设置渲染状态(用那个顶点着色器,片元着色器,光源属性,材质等)====》调用DrawCall

GPU几何阶段和光栅化阶段

几何阶段

              

模型空间到齐次空间          细分图元          图元着色     不在屏幕内的    齐次坐标到屏幕坐标

计算顶点颜色                                                                                           屏幕自适应

光栅化阶段

  三个点形           遍历三角形         差值运算        透明度测试

  成三角形            内的像素            纹理采样        模板测试

                             深度测试                                  合并混合

alpha blend 工作原理:实际显示颜色 = 前景颜色*Alpha/255 + 背景颜色*(255-Alpha)/255

实际光照强度 I= 环境光(Iambient) + 漫反射光(Idiffuse) + 镜面高光(Ispecular);

环境光:Iambient= Aintensity* Acolor; (Aintensity表示环境光强度,Acolor表示环境光颜色)

漫反射光:Idiffuse = Dintensity*Dcolor*N.L;

(Dintensity表示漫反射强度,Dcolor表示漫反射光颜色,N为该点的法向量L为光源向量)

镜面反射光:Ispecular = Sintensity*Scolor*(R.V)^n;

(Sintensity表示镜面光照强度,Scolor表示镜面光颜色,R为光的反射向量V为观察者向量,n称为镜面光指数)

********************************************************************************************************************

八、安卓与Unity交互

Class.jar,导入安卓工程

修改aar

Unity调用Android

AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
AndroidJavaObject jo = jc.GetStatic<AndroidJavaObject>("currentActivity");

安卓调用Unity

MainActivity  extends UnityPlayerActivity;    会自动导入  com.unity3d.player.unityPlayerActivity

UnityPlayer.UnitySendMessage("游戏挂载体","方法名","");

最后把aar导到进untiy工程的下面目录

九、优化

2.对象池

3.释放AssetBundle占用的资源

4.降低模型的片面数,降低模型的骨骼数量,降低贴图的大小

5.使用光照贴图,使用多层次细节(LOD),使用着色器(Shader),使用预设(Prefab)。

UGUI优化

1,合理的分配图集,合理的分配图集可以降低drawcall和资源加载速度

    共用的图片放到一个或几共享的图集中

     同一个UI界面的图片尽可能放到一个图集中

     不同格式的图片分别放到不同的图集中,例如透明(带Alpha)和不透明(不带Alpha)的图片

原理:材质ID一样,或者贴图一样的可以合批。

2,适当的降低图片的尺寸

3,etc1格式的图片,图片2N次方,用shader通道分离

4,如果观察到Canvas.SendWillRenderCanvases耗时较高,可以检查下ScrollRect所在的Canvas是否开启了Pixel Perfect的选项,谨慎使用Canvas的Pixel Perfect选项,该选项会使得ui元素在发生位置变化时,造成layout Rebuild

5,使用缓存池来保存ScrollView中的Item,对于移出或移进View外的的元素,不要调用disable或enable,而是把它们放到缓存池里或从缓存池中取出复用

6,Canvas划分是个很大的话题。简单来说,因为一个Canvas下的所有UI元素都是合在一个Mesh中的,过大的Mesh在更新时开销很大,所以一般建议每个较复杂的UI界面,都自成一个Canvas(可以是子Canvas),在UI界面很复杂时,甚至要划分更多的子Canvas。同时还要注意动态元素和静态元素的分离,因为动态元素会导致Canvas的mesh的更新。最后,Canvas又不能细分的太多,因为会导致Draw Call的上升。我们后续将对UI模块做具体的讲解,尽请期待。

7,尽可能把频繁变化(位置,颜色,长宽等)的UI元素从复杂的Canvas中分离出来从而避免复杂的Canvas频繁重建

在UGUI中,网格的更新或重建(为了尽可能合并UI部分的DrawCall)是以Canvas为单位的,且只在其中的UI元素发生变动(位置、颜色等)时才会进行。因此,将动态UI元素与静态UI元素分离后,可以将动态UI元素的变化所引起的网格更新或重建所涉及到的范围变小,从而降低一定的开销。而静态UI元素所在的Canvas则不会出现网格更新和重建的开销。

8,尽可能避免使用UI/Effect,特别是Outline,会使得文本的Mesh增加4倍。

9,物体的显隐如果通过Activity来操作,会早上较大内存损耗,在enable函数中,状态从false设为true过程中,会执行enable函数,会把所有的状态设置脏标记,所有的物体都会进行重建,比如layout,material,顶点信息。尤其是文本信息中包含较多文字时候,

控制单个UI可以使用单个CanvasRender来控制显隐。GetComponent<MeshRenderer>().enabled = true;

控制一组UI的显示隐藏可以使用CanvasGroup来控制,使用CanvasGroup的alpha值来控制,不造成额外的性能损耗。

11,将屏幕中所有的头顶文字进行分组,放在不同的Canvas下

12,可以尝试通过添加一个 Layer 如 OutUI, 且在 Camera 的 Culling Mask 中将其取消勾选(即不渲染该 Layer)。从而在 UI 界面切换时,直接通过修改 Canvas 的 Layer 来实现“隐藏”。但需要注意事件的屏蔽。

13,Text.OnEnable 是在实例化一个 UI 界面时,UI 中的文本(即 Text 组件)进行了 OnEnable 操作,其中主要是初始化文本网格的信息(每个文字所在的网格顶点,UV,顶点色等等属性),而这些信息都是储存在数组中(即堆内存中),所以文本越多,堆内存开销越大。但这是不可避免的,只能尽量减少出现次数。
因此,我们不建议通过 Instantiate/Destroy 来处理切换频繁的 UI 界面而是通过 SetActive(true/false),甚至是直接移动 UI 的方式,以避免反复地造成堆内存开销。

14,Font.CacheFontForText主要是指生成动态字体Font Texture的开销, 一次性打开UI界面中的文字越多,其开销越大。如果该项占用时间超过2s,那么确实是挺大的,这个消耗也与已经生成的Font Texture有关系。简单来说,它主要是看目前Font Texture中是否有地方可以容下接下来的文字,如果容不下才会进行一步扩大Font Texture,从而造成了性能开销

15,在内存允许的情况下,对于UI界面进行缓存,尽可能减少UI界面相关资源的重复加载以及相关类的重复初始化;

场景优化

1,除了玩家控制的赛车以外都是static物体,都会进入static batching方式处理;frame debug

      static batching处理时,Unity会把具有相同材质的静态物体给放在一个drawcall里

2,cast shadows 设为off,取消勾选receive shadows。同时我也并不需要让它影响lightmap,所以也取消勾选lightmap static好了

5000多个drawcall变成2000;

3,删掉场景中不需要的16个点光源

   drawCall从2000变成600;

4,遮挡剔除,摄像机够选Occlusion,Window -> Occlusion Culling,调整下参数,bake一

被遮挡的游戏对象在Inspect属性面板上设置为静态 

空间换时间,接下来打开Wndow->Occlusion Culling,最后bake一下

产生2MB大小的occlusion data,最终drawcall变为300 drawcall

arraylist和linkedlist的区别

数据结构不同,效率不同,自由性不同,主要控件开销不同。

1、ArrayList是Array动态数组的数据结构,LinketList是Link(链表)双向链表的数据结构

2、随机访问List(get和set),ArrayList比LinkedList效率更高,ArrayList是利用数组的下标进行元素的访问,LinkedList是线性的数据存储方式,所以需要移动指针从前往后依次查找。

对数据进行Add和删除Remove的操作,LinkedList的速度很快。

3、ArrayList需要指定初始容量,LinkedList比ArrayList灵活,不需要指定初始容量。

4、ArrayList开销在Ilist列表预保留一定空间,LinkList开销在需要存储节点信息和节点指针,Arraylist插入新元素的时候,要判断是否扩容,扩容步长是原来的0.5倍,扩容是利用数组的复制,有一定的开销,ArrayList进行元插入的时候,需要移动插入之后的所有元素。

大神的面试题:

Unity面试题总结_北海6516的博客-CSDN博客

List底层是如何实现的?

 List内部是用数组实现的,如果不指定长度,则使用默认的空数组。

Add接口:添加元素前会先检查容量,容量为0则会增加到4,之后每次扩充都是增加到当前容量的2倍。改变容量会new一个新的数组,通过Array.Copy方法将原数组的元素复制过去。

Remove接口:通过IndexOf方法查找元素位置(线性O(n)),然后在RemoveAt中使用Array.Copy对数组进行覆盖。
Insert接口:与Add接口一样,先检查容量是否足够,不足则扩容一倍。同样使用的是数组覆盖的形式,将数组里指定元素后面的所有元素向后移动一个位置。
Clear接口:调用Array.Clear方法,在调用时并不会删除数组,而只是将数组中的元素设置为0或NULL,并设置_size为0而已。
Contains接口:执行线性O(n)搜索。是否相等是通过调用Equals()来确定的。
ToArray接口:它重新创建了一个指定大小的数组,将本身数组上的内容复制到新数组上再返回
Find接口:同样线性O(n)。
Enumerator接口:每次获取迭代器时,Enumerator都会被创建出来,如果大量使用迭代器,比如foreach,就会产生大量的垃圾对象,这也是为什么我们常常告诫程序员尽量不要使用foreach,因为List的foreach会增加新的Enumerator实例,最后由GC单元将垃圾回收掉。虽然.NET在4.0后已经修复了此问题,但仍然不建议大量使用foreach。
Sort接口:它使用了Array.Sort接口进行排序,而Array.Sort使用快速排序实现,故效率为O(nlgn)。
总结:List的效率并不高,大部分算法使用的是线性复杂度的算法,我们可以在创建List实例时指定容量,这样List就不会因为空间不够而抛弃原有的数组去重新申请数组了。另外也可以从源码中看出,代码是线程不安全的,它并没有对多线程做任何加锁或其他同步操作。由于并发情况下无法判断_size++的执行顺序,因此当我们在多线程间使用List时应加上安全机制。

Stack底层如何实现的?

Stack内部也是数组实现的,与List一样,也是按照2倍的容量去扩容,只是默认容量不一样,Stack默认构建一个容量为10的数组。

Dictionary底层是如何实现的?是如何解决冲突的?

 Dictionary底层仍然是用数组来实现的,Entry是个结构体,记录了数据的键值以及下一个元素的位置,entries数组记录每个元素,buckets数组主要用于碰撞检测,计算hash后,buckets数组对应位置记录entries数组元素的下标,所以buckets数组是存放索引的。

Dictionary使用的解决冲突方法是拉链法,又称链地址法。

总结:Dictionary是由数组实现的,其中buckets主要用来进行Hash碰撞,entries用来存储字典的内容,并且标识下一个元素的位置,拉链法来解决冲突的。从效率上看,同List一样,最好在新建时,确定大致数量,这样会使得内存分配次数减少,另外,使用数值作为键值比使用类实例的方式更高效,因为类对象实例的Hash值通常都由内存地址再计算得到。从内存操作上看,其大小以3→7→17→37→…的速度(每次增加2倍多)增长,删除时,并不缩减内存。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值