[c#]值类型内存分布

前言

最近公司对于gc考虑的很严格,希望堆上内存只在系统初始化的时候才分配,以减少因为gc导致的cpu卡顿。在这个基础上仔细看了一下值类型的内存分布,了解一下值类型在内存上究竟怎么分配的。

值类型

在定义值类型的变量的时候会直接算好占用多少内存。

public struct DataUnion
{
    public long _i64;
    public ulong _u64;
    public double _f64;
}

在定义的时候就已经开辟了24字节的内存保存,就算不存值内存也已经开辟了。
但是他有一个好处就是不会增加gc计算。

public struct DataUnion{
    public data _data
}

在struct中保存的是引用类型对象的引用,是固定大小的,根据操作系统的位数,32位的占4字节,64位的占8字节。每次传递的时候值类型里的数据都会被复制一份,所以引用也会被拷贝一份。

自定义内存布局方式

通过使用[StructLayout(LayoutKind.Explicit)]和[FieldOffset]特性,这个结构体可以在相同的内存位置上保存多种不同类型的数据,通过这个特性,我们能使用同一个结构保存不同类型的值。

[StructLayout(LayoutKind.Explicit)]
public struct SlateDataBase
{
    [FieldOffset(0)]
    public long _i64;
    [FieldOffset(0)]
    public bool _bool;
    [FieldOffset(0)]
    public ulong _u64;
    [fieldOffset(8)]
    public string _str;
    [FieldOffset(0)]
    public IntPtr  _next;
}

SlateDataBase _dataBase = new SlateDataBase(){_bool = false};
private SlateDataBase _dataBase1 = new SlateDataBase();

void main(){
    IntPtr ptr1 = Marshal.AllocHGlobal(Marshal.SizeOf<SlateDataBase>());
    Marshal.StructureToPtr(_dataBase,ptr1,false);
    _dataBase1._next = ptr1;
    SlateDataBase data = Marshal.PtrToStructure<SlateDataBase>(_dataBase1._next);
    data._bool;
}

上段代码使用了IntPtr来保存对象指针,这样避免了结构布局中的循环依赖问题。同时通过了StructLayout来控制内存布局。但是我知道有一个问题:

  1. 如果我分配的内存不是从0开始的,那保存的值会有问题

注意点

  1. 在使用StructLayout做内存布局的时候是在创建的时候就分配内存了,值类型可相互覆盖,所以都可以从统一位置做偏移,但是引用类型保存的是指针大小,它的大小和对齐方式跟值类型不一样,所以它不能覆盖值类型的内存,得重新取一块内存保存。
  2. 需要自己手动做InPtr所占内存的回收,而且使用的是指针很容易出内存问题。
  3. 内存大小是通过最大偏移加类型所占内存。上面所占内存为8字节+string所占(8字节)=16字节

值类型分配地方

一般来说,值类型所分配的地方有两个地方,一个是堆上,一个是栈上。那什么时候分配在堆上,什么时候分配在栈上呢?


public struct SData{
    public int _int;
}

public class Data{
    public int _int;
    public ulong _ulong;
}

public class Data1{
    public Data _data;
    public SData _sdata;
}

public Data1 _data1 = new Data1();

public void Main(){
    SData sdata = new SData();
    sdata._int = 10;
    _data1._sdata = sdata;
    sdata._int = 20;
    Data data = new Data();
    data._int = 20;
    _data1._data = data;
}

上面的示例代码值类型的struct分别在堆和栈上都分配了。
栈上分配:
在函数内创建的sdata是分配在栈上,它的什么周期是跟着函数体的结束而一起结束,内存也就被回收掉了。
堆上分配:
当我实例化了_data1这个引用类型对象的时候,它就在堆上分配了内存,分别是_data的指针大小+_sdata这个struct所占用的内存大小。所以这个时候_sdata是分配在堆上,当我把函数内的
sdata赋值给_sdata的时候,是把sdata的内容给复制过去。所以当我赋值完了之后把sdata的int改成20,实际上_sdata中的_int还是10。

引用类型

在定义引用类型的时候在没有实例化引用类型对象的时候是不会申请内存的。

public class Data{
    public long _i64; 
}

void main(){
    public Data data;
}

在使用_i64的时候需要创建一个Data的引用类型对象,相当于对_i64做了一次装箱操作。
当内部保存的类型是引用类型的时候,因为不会出现拷贝引用类型数据,所以引用不会被拷贝。

实例化

当我实例化的时候,就会在堆上分配引用类型中值类型所占用的内存,在堆上分配一个8字节的空间,然后把指针地址返回给data。data就通过这个地址找到这片空间找到里面的_i64所写入的内容。

gc回收

之前说过,不讲了。

泛型类型

根据上面的特性,在构造值类型的泛型时,使用struct来保存

public struct ValueVariable<T> where T : struct{
    private T _value;
}

public ValueVAriable<int> _int_value = 10;
public ValueVAriable<bool> _bool_value = false;

这样我在保存一个值类型的时候也是通过值类型保存,所以不会产生gc问题。
引用类型就可以直接使用object来保存。

不安全代码

暂无

[XLua]lua和c#交互

前言

在移动游戏发展到今天,热更技术已经有很多种渠道了,一种是使用lua,一种是c#更新dll,还有最新出来的TypeScript的热更新。这次我就简单的聊些使用率最多的lua热更新中的交互,也希望能说出一些交互上可以优化的东西。

知识大纲

  1. 函数
  2. 值类型
  3. 引用类型
  4. struct类型
  5. string类型
  6. lua和c的交互调用栈
  7. IntPtr类型

初始化

获取全局表

在lua初始化的时候会初始化一个luaEnv对象,他会去c里拿到一个LUA_REGISTRYINDEX这是一个registry的表的索引,这个表是c可以自由使用,但是lua代码不能访问他。

注册lua状态机

使用luaL_newstate创建了一个新的lua状态机(可以理解为lua虚拟机),主要用来为每一个lua线程创建独立的函数栈和线程栈,以及lua所需要的内存管理,字符串管理,gc管理。创建两个主要的数据结构lua_state和global_State。

  • lua_State:主线程栈结构
  • global_State:全局状态机,维护全局字符串,内存管理函数,gc等信息

加载lua标准库

使用luaL_openlibs用来加载lua的标准库到指定的lua状态机。

注册一个用来lua和c#交互的对象

通过创建一个ObjectTranslator对象来做lua和c#的对象交互,传入一个luaEnv对象,和一个lua状态机的指针。创建了一个弱value的一个table放在lua中。注册lua对象回收中__gc的回调方法。使用lua_pushlightuserdata创建一个自己管理的xlua标志位。注册所有被此应用所用的所有程序集到assemblies中

注册xlua全局表

translator.OpenLib(rawL);
import_type这个方法为lua中获取全局c#方法的回调函数

注册一个lua异常捕获

LuaAPI.lua_atpanic(rawL, StaticLuaCallbacks.Panic);使用这个方法注册一个在c#中去处置lua程序崩溃的方法

注册一个lua的print方法

LuaAPI.lua_pushstdcallcfunction(rawL, StaticLuaCallbacks.Print);LuaAPI.xlua_setglobal(rawL, "print")
注册一个print的方法注册到global全局表中的print

初始化Init脚本

注册一个全局的CS表,设置CS的元方法。设置xlua的元方法。

获取_G全局表

LuaAPI.xlua_getglobal(rawL, "_G");translator.Get(rawL, -1, out _G);获取_G

注册类

通过Wrap中的__Register的方法用来注册当前这个类的实例化函数,静态函数,属性,静态属性,实例化类的方法

注册函数

通过BeginObjectRegister在栈顶注册一个class.FullName名字的metatable表。给他塞入一个xlua的标志,覆盖两个关于“gc”和“tostring”的元方法。创建三个table,用来存放类的函数,设置属性方法,获取属性方法。调用RegisterFunc把所需要的函数方法注册进去。最后在结束的时候替换掉“index”和“newIndex”这两个元方法

注册静态函数

通过BeginClassRegister中把当前类的静态函数注册到table中,然后通过SetCSTable把当前类名的table放在cstable下,然后替换元方法__call为c#的函数,最后创建两个table用来放置静态属性;还是通过RegisterFunc把静态属性的获取和设置添加进去。

加载lua脚本

DoString

通过xluaL_loadbuffer编译lua脚本,通过lua_setfenv设置当前lua编译脚本为传进来的table的upvalue值,然后通过Lua_pcall执行编译之后的脚本,放回栈顶的对象。

Lua和c#交互

c#调用lua方法

在DoString之后把DoString所执行的脚本设置到一个luatable对象里面,然后通过这个luatable对象去lua栈里拿到luatable所在的table。在tryGetGetFuncByType发现我们要转换的类型不是我们定义的那些基础类型,所以通过GetObject去获取到一个转换委托ObjectCast这个委托函数里,然后通过需要转换到类型调用不同的转换函数,函数就是通过调用CreateDelegateBridge来创建一个action委托的,委托里包含了lua方法的索引。当函数调用的时候就是调用委托里的PCall来调用到lua中的方法。因为在tryGetGetFuncByType没有注册,所以使用了反射去创建当前类型的对象。

c#获取lua值类型

也是在tryGetGetFuncByType里把int ,bool,float这的值类型通过从lua栈里push出number,boolean,integer的值通过类型转换变换出来。

c#获取lua的table类型

当把table定义成一个list对象的时候,它也和调用函数一样回去判断有没有在tryGetGetFuncByType里定义了这个list类型的ObjectCast,如果没有也是通过反射在genCaster里去创建这个对象,把lua栈内的值取出来。

c#获取lua的string

当需要在lua中使用到c#的string时,把string转成byte[]传到lua中。
当把lua中的string传到c#的时候,也是把string转成byte[]接收过来,转成string

lua调用函数

在init的时候生成的时候会设置import_type为CS的index方法中的查找到c#里的ImportType去根据className去translator.FindType找到当前这个className到类型,然后通过GetTypeId去找放回类型的唯一id,如果这个类型没有缓存,那就会去生成一个当前类型名字的metatable,然后通过DelayWrapLoader中注册的loader去加载生成的wrap类的Register生成的当前所需要的方法的metatable,给lua调用,然后把类型的id传到交互栈中。当调用这个方法的时候就可以通过在RegisterFunc的时候push进去的lua_pushstdcallcfunction来掉到这个函数了。

lua调用值类型

当lua需要设置值类型的时候,同样会定义一个属性赋值方法,通过函数调用的方法调用到这个方法,然后通过lua_tointeger去栈里拿到这个方法一起传过来的number类型的值,然后赋值过去。
当lua需要设置值的时候,还是定义一个属性获取的方法,通过函数调用先通过Get方法从userdata中获取到这个需要赋值的对象,然后通过UnPack来把这个需要赋值的属性取出来,然后修改掉对应的属性;最后通过push把属性塞到lua栈中

lua调用struct类型

当lua需要获取一个struct类型的值时,因为lua中没有struct这样的结构类型所以会把struct包成一个object传到lua栈中当userdata存下来。
当lua需要设置一个struct类型的值时,获取到栈内的userdata然后存成ObjectCast然后转成struct,取lua栈内第二个参数来设置struct的值

但是在xlua中vector和color这两种struct会做一次处理,会把里面的struct的结构内容给拆分出来,通过基础的值类型进行传递。

lua调用引用对象

通过addObject把创建出来的object存在objecttranslator中,返回一个存放的index,然后把这个index传到xlua_pushcsobj里去创建一个userdata并且把这个对象的方法,属性都传进去做metatable属性。然后就可以获取到这个userdata赋值给lua。

lua和c#相互引用对象的生命周期

通过上面知道在c#中需要被lua所调用到的引用类型对象都需要在objecttranslator中存起来,然后把所生成的index传到lua的userdata中去,然后给这个userdata设置了一个元表,修改了gc这个元方法,所以当lua的userdata不在被引用的时候就会触发lua的gc,然后调用到gc这个元方法,调用到ObjectTranslator中的collectObject然后保存列表中清理掉。
在c#获取到lua的对象需要创建一个object的对象,然后把lua中的reference通过luaL_ref获取到保存下来,然后当这个对象不在被引用的时候,调用到c#的dispose函数,然后在里面去LuaAPI.lua_unref释放掉这个lua对象的引用。

总结

通过上面的数据交互,我们可以知道,在lua和c#的交互,我们需要注意到引用类型的交互,因为他们会分别在lua端和c#端创建一个objcet和table(userdata)对象,而这两种对象都会产生gc压力,还有一个就是struct对象也是需要注意的,虽然在xlua中vextor会拆成基础值类型传递,但是大部分我们自定义的struct还是会被包装成一个object传递的,这里还产生了一个装箱操作,所以消耗会更大。

[lua]upvalue和闭包

概念

当一个函数内部A嵌套另一个函数B定义时,内部的函数体可以访问外部的函数的局部变量,这种特征我们称为词法定界,B中被A访问到的变量我们称为upvalue。而闭包是指一个函数加上它可以正确访问到的upvalue。技术上来讲,运行时,我们执行的是闭包,而不是函数,函数仅仅是闭包的一个原型声明;闭包在完全不同的上下文中也是很有用途的。因为函数被存储在普通的变量内我们可以很方便的重定义或预定义函数。通常当你需要原始函数有一个新的实现时可以重定义函数。upvalue你可以理解成是一块特殊的内存,独立管理内部数据的,所以可以在多个闭包之间提供一种数据共享的机制。

示例解释

upvalue

function f1(n)
    local function f2()
        print(n)
    end
    return f2
end
local g1 = f1(11)
g1()                    --打印11
local g2 = f2(22)
g2()                    --打印22
g1()                    --打印11

在上面的例子里,我们可以看出来当g1 = f1(11)这个被执行完的时候,局部变量n的生命周期应该是结束了的。但是因为它成为了f2函数的upvalue,所以它的生命周期跟着g1的生命周期一起了,被延长了。下面的g2是重新执行了一遍f2所以它重新包了一个n做upvalue,这个时候upvalue的数据没有共享。

闭包

function f1(n)
    local function f2()
        print(n)
    end
    n = n+10
    return f2
end
g1 = f1(10)
g1()                    --打印20

在上面的这个例子里,我们发现n在包在f2内部的时候,成为f2的upvalue,但f1还没执行完成n还是f1的局部变量,所以执行了n = n+10的这个的时候,n的变化也在f2执行的时候被表现出来的。

upvalue的数据共享

function f1(n)
    local function f2()
        print(n)
    end
    local function f3()
        n = n +10
    end
    n = n+1
    return f2,f3
end
local g2,g3 = f1(1)
g2()                        --打印2
g3()
g2()                        --打印12

从上面的例子我们可以发现,当f1执行结束之后,lua会把内部所引用的n做成一个upvalue来作为f2和f3的upvalue,所以当我们g3调用了n = n+10的时候,n的改变也就改了那共同upvalue内的n,所以可以被g2函数所打印出来变化。

function f1(n)
    local function f4()
        local function f2()
            print(n)
        end
        local function f3()
            n = n +10
        end
        return f2,f3
    end
    n = n+1
    return f4
end
local g1 = f1(1)
local g2,g3 = g1()
g2()                    --2
g3()
g2()                    --12

所以我们就很容易理解上面这个例子为啥还是一样的打印结果,因为upvalue在一个函数内就只会有一份相同的。

总结

闭包这个概念在很多语法中都有的,之前有一点点了解,最近在做一些lua的内存泄漏优化的时候,发现因为一些全局表的函数引用,导致了一些传入进去的值被引用到,没办法释放,引起了一些内存上的增长,所以我们在给函数传值的时候,需要注意到这个函数什么时候被释放的,是不是不会被释放掉,如果它在一个全局table中,那么它所引用到的值我们就需要手动释放。关于upvalue的lua中是怎么实现的,之后有机会讲解一下源码。

【基础】VO算法

前言

最近遇到了怪物在ai寻路的目标点为同一个的时候,会出现相互挤压的情况。所以开始研究群体避障算法。这片文章主要讲一下最基础的VO(Velocity Obstacle)算法。

正文

其实vo算法不是很难的,就是你得实时算它前面是不是会碰到物体,因为是简单的,所以单个物体只管自己的。我先晒一张经典的图

这张图在所以讲vo算法的文章里都会出现的,它直观的展示了vo是怎么算物品之间的碰撞。现在我就基于这个图来详细讲解下相关的知识点。

闵可夫斯基和

在这张图里我们会看到B里有一个实体圈,和一个虚线圈。然后你会发现生成的B⨁−A这个三角形实际上是从A的中心点PA为起点画的,所以可以理解那个虚拟的B圈实际上是把A圈给叠加上了。这里涉及到了一个集合知识点就是闵可夫斯基和(闵氏和)。
概念:两个图形A,B的闵氏和C={a+b|a∈A,b∈B}
其实就是从原点向着图形A内部的每一个点做向量,将图形B沿每个向量移动,最终位置的并集就是闵氏和。
我们来看一个图片吧

在这张图里a(A,B,C,D)b(A,C,E)。现在我们用它做一个闵氏和,先把b往DA向量方向移动,得到了一个(F,B,Q)这个图型,其他的向量也按这样的移动,我们最后就会得到一个并集成(F,G,I,M,L,J)这个图型。这个图形就是我们要算的a和b的闵氏和。
结合这个知识点,我们现在来看VO的那张图我们就很容易理解了,把B和A放在同一个圆心上,然后沿着圆的四周往外扩散A的半径向量。得到虚框B。

相对位移

当我们A沿着VA移动,B沿着VB移动的时候,我们要确定两个图形是否会碰撞到,因为我们是从a画的一个扇形,所以我们需要计算下A的相对于B的移动向量,这个就用向量相减就好了。算出一个VA-VB的向量,然后我们PA质点沿着VA-VB的方向画一个和B虚框相切的一个图形B⨁−A。因为实际计算不是求的相对速度,是VA的实际速度,所以我们需要加上VB。这样我们就得到了一个深色的三角形。

结论

当我们的速度不属于这个深色三角形内的时候,他们就不会发生碰撞。

补充

因为VO是把对象都当成一个独立的个体,所以它在计算的时候是实时计算的,当两个物体独立计算的时候,所以他们会单独计算自己的避障速度方向,当A,B沿着避障方向移动的时候,他们在下一次计算会发现自己不在对方的碰撞区域内,所以会修改方向沿着之前会碰撞的方向移动,再下一次计算的时候发现自己又在碰撞区域内了,又开始偏移移动,这样往复以后,就会出现抖动。后面我研究研究VO的优化算法,再讲解下怎么解决这个问题。

【unity】协程和迭代器

前言

在很早之前忘了是哪次面试还是什么时候,有人问我协程是什么,那时候我根本就没有这个概念,所以在网上找了一些资料,发现很多人都是讲协程和什么线程这种概念的区别,某一次我看到了某篇文章,上面详细讲解了一次协程的原理,和自己实现一个协程。那时候我也想自己去了解一下,但是因为各种各样的缘由,我在前段时间看书看到了迭代器这一章的时候,根据迭代器的定义,发现协程其实就是一个迭代器的一种实现。现在我就简单的基于协程的原理来通过迭代器实现下这个功能。

正文

迭代器

迭代器是一种设计模式,在所有讲设计模式的书上都有介绍的。它是对于遍历的一种封装,它依赖于一个叫迭代器的接口,我们通过实现这个迭代器的接口可以对各种集合的一个遍历,我们可以不用关系这个集合是数组,列表还是散列表的。

c#中的实现

  1. IEnumerable和IEnumerator
  2. IEnumerable<T>和IEnumerator<T>

上面的两个的区别是一个是基于泛型的接口还有一个是非泛型的接口

枚举源

借助c#语言的一个强大的功能,能够生成创建枚举源的方法。这些方法称为“迭代器方法”。迭代器方法用于定义请求如何在序列中生成对象。使用yield return 上下文关键字定义迭代器方法。可以在迭代器方法中使用多个离散yield return语句。迭代器有一个重要的限制:在同一个方法中不能同时使用return语句和yield return语句。

IEnumerable和IEnumerator

当我们自己实现一个迭代器的时候,我们需要创建两个类,分别继承IEnumerable和IEnumerator这两个接口,他们最重要的是IEnumerator,IEnumerable这个接口需要实现的方法是返回一个IEnumerator的对象,然后我们通过对IEnumerator进行遍历。在实现IEnumerator接口的时候我们需要实现一个是MoveNext这个方法,它是用来判断集合中是否还有下一个元素,Reset这个方法用来重置遍历,下一次从头遍历。Current这个方法是用来获取当前遍历的元素。Dispose这个方法是用来对迭代器进行回收。枚举器用来是读取集合中的数据,不能用来修改基础集合。在最开始的时候枚举数定位在集合中第一个元素的最前面,所以在reset的时候把这个枚举数定义为-1,当我们调用MoveNext的时候会判断当前的枚举数(++curindex)是否在集合的最后一位,如果已经在最后一位了,那就会返回一个false,然后current的值不变,如果不在最后一位,那就把current的值修改到下一个元素。因为是通过枚举数来确定是否在集合的最后一位,所以当我们对集合做出了修改,就会导致枚举器的行为是不确定的。在这里我发现了一个事情,就是默认的IEnumerator的current的返回值是一个object的类型,这个会不会就是人们常说的用foreach来遍历集合会有很高的消耗呢?毕竟如果是一个int这样的值类型会有一次拆装箱的操作。如果说被优化了,那优化点就是值类型的泛型他们单独实现。通过我查看了下他们的源码发现真的是这样的,他们对list的IEnumerator自己改成了一个Enumerator的一个结构(struct)内部实现的Current是一个T的泛型,所以这里就不会出现我看到的拆装箱的操作。

yield

  1. yield return <expression>
  2. yield break

使用yield return 语句可一次返回一个元素。
迭代器方法运行到yield return语句时,会返回一个expression,并保留当前在代码中的位置。下次调用迭代器函数的时候,将从该位置重新开始执行。
使用yield break来终止迭代器。
返回yield或IIEnumerable的迭代器的IEnumerator类型为object。所以当我们yield return返回一个int这样的值类型的时候会进行一次装箱操作。
yield break
是用来跳出迭代器的。

协程

    IEnumerator test2 = test();
                while (test2.MoveNext())
                {
                    Console.WriteLine(test2.Current);
                }
     IEnumerator test()
            {
                Console.WriteLine(DateTime.Now);
                yield return new test1();

                Console.WriteLine(DateTime.Now);
            }

            public class test1
            {
                public test1()
                {
                    System.Threading.Thread.Sleep(2000);
                }
            }

看到上面的代码有没一种很熟悉的感觉,这个是我实现的一个延时执行下一行代码的协程,在实现的时候,我一直在想,其实我们可以把expression当成是一个同步阻塞方法,当这个方法执行结束了之后,我们的迭代器再执行下面的方法。

结束语

万变不离其宗,掌握了原理其实很多东西都是很简单的。当前我就吃了研究不深的亏,很多东西都是用就完事了。现在回想起来,知识这个东西还是不能拖的,趁着热情还在,仔细研究透。

【性能】ugui对象不可视

前言

在很早之前我做过一次性能分析,分析物体在各种不可视方案下的性能开销但是这个是在对于Gameobject的分析,现在在做ugui的界面显隐的时候,又多出了好几种方案,现在来最这几种方案做一个分析。

正文

测试代码还是和以前一样,对物体进行10万次显隐操作。

隐藏控件

Canvas

在ugui中,界面的渲染都是基于canvas的,所以当我们隐藏掉canvas这个控件的时候,界面也将看不见,所以我们先来做一次canvas的控件显隐操作

因为我的子物体有6个image所以当canvas被隐藏的时候,它会去回调image的渲染回调,所以我们可以看到调用image.OnCanvasHierarchyChanged调用了120万次。回调中因为会用到一些对象,所以它的GC压力很大。

Image

对于单一的物体,我们以Image为例,它的渲染是基于它的Image这个控件,所以当我们把控件隐藏了,它将不再被渲染

这里我们看到它的主要开销是OnEnable和OnDisable这两个方法。

设置透明

CanvasGroup

CanvasGroup是用来控制子控件的alpha等一些界面属性的,所以当我们把它的alpha设置成0的时候,子节点的alpha的值也就变成0了。

看到ugui放出来的CanvasGroup的代码可以看到它有一部分的实现在c++里的,当CanvasGroup的alpha的值发生变化,应该是要发送消息告诉子节点的透明度的变化,所以这里的120万的调用我们猜测是因为子节点有六个,所以要抛消息到所有的子节点上。

image

当image的alpha的值被设置成0的时候,我们就看不到这个组件了

可以看到它就只需调用自身,没有其他的函数调用。

设置大小

这个就在之前的测试里做过的,因为大小为0的时候,它将不会被渲染出来,所以也就不会可视化出来。改变的是矩阵。

结论

在我看来我觉得还是设置大小是最优的显隐方案,用代码测试一次就好了。大家一起来看看还有什么情况是没有测试到的呢。

【编程基础】值类型和引用类型

前言

值类型和引用类型,对于使用了托管内存的编程语言来说,他们的最大差距就是在分配和回收上的差距。

  • 值类型
    它是在存储在栈上的,当你在创建它的时候,它被压进栈内,在它出栈前,它都将可用,把它传进别的函数内使用的时候,这个时候复制了一份它的内容,然后把这个新拷贝内容给压进栈内。所以我们在函数内改变的值,只是改变了这个新压入栈的值,当然了我们也可以把这个值的引用传入进来。这里说将的值类型其实都是在函数中的局部变量。

  • 引用类型
    它是存在托管堆上的,我在这里所说的类型区分都是基于托管内存的编程语言来说的。当我们创建一个对象的时候,先告诉托管堆给我们分配一块内存,然后它把这个内存的地址引用返回给我们,然后我们就在这块内存内做一些操作,存一些数据,一些别的托管内存的一些指针。

引用类型中包含的值类型

如果引用类型中有值类型的变量实例,那它会在分配给它的内存中开辟值类型所需要的内存大小,给值类型用。那这个时候只值类型就被放在托管堆上了。

正文

看完上面的一些东西,是不是感觉很迷茫呢?有些迷糊值类型和引用类型究竟有哪些区别,究竟是放在堆上还是在栈上,它们之间啥关系呢?下面我就来讲讲。

一位仙气少年

在编程世界里,我们程序启动有一个虚拟机在跑。当我们把世界转向一个玄幻的世界里,那这个虚拟机我们让他叫做小m,是一个刚刚从师门出发,将要去入世,去闯荡一番。那现在我们来给他一些入世需要的装备吧。

绝世武功?

在给小m发放装备的时候 ,我们先去了解小m有哪些功法,他可以克隆一切物体,还会一种世界上绝无仅有的功法,那就是空间术。当他在学习这门功法的时候,他也继承了前人开辟的一大块空间(这个就是托管堆的空间大小),他可以自己去这块大的空间里切割一部分归自己所用。在了解了这些之后,我们来给小m发放装备了,俗话说得好的好,功夫再好也怕手枪,所以我们先给他一把枪,这把枪有个特殊的属性,就是能发射任何东西,但是,大家也知道,弹夹有个特性,就是最后压进来的子弹是最先发射出去的(这就是栈)。其实有了这把枪我们就可以不需要再去给他发放什么装备了。

空间戒子

小m在收拾东西的时候,发现自己想带的东西太多了。这个时候他想起来了自己的空间术。他在仔细研究之后,发现在自己切割下来的空间内,能放和自己相关的任何东西,那他就开始创建了一个空间指环,但是有个不同的情况就是,空间术创建的时候,它的空间大小是不能更改的,当你创建的时候是多大,那就是多大,当你创建的空间指环不够你使用的时候,你就需要重新创建一个更大的空间,然后把原来空间的东西给搬到新空间里,在这个操作是,需要很耗费小m的功力,需要花费他很多时间。现在小m已经创建了一个空间戒子了,他现在把这个戒子带到身上去了(获取引用)。

Hello Word

现在小m带齐了自己的所有装备开始下山了,他下山后看着这个未知的世界,他不知道怎么的就像对着这个世界说上一句“Hello Word”。那现在它就开始使用它的枪打出这几个字了,它开始一个字符一个字符的压入枪内d,r,o,w o,l,l,e,h。然后一口气打出了九发,但是它觉得太不爽了,连续扣动扳机太累的。转念一想,自己有个空间术吗?这把枪不是可以射出任何东西,是不是可以把我这个空间术内的这一整块空间射出去呢?它就开始使用空间术来创建了一个特殊的空间,就是为了能把字符全部装进,这个空间他为了保险,还做了一个限制,就是我只能放一次字符的集合,后面再放进去,那就只能重新在创建一个新的空间,把之前的字符集合给拷贝进去,然后再添加。好了,现在我们开始把这个字符串空间打出去了。一枪出去,天空上炸出了一整句“Hello Word”。小m现在太兴奋了,感觉自己创造了功法的一种新的使用方法。然后疯狂的往天上打枪。

子弹不够了?

突然小m发现他打出来的子弹是空弹,啥都看到。这个时候他也发现了不正常的情况,就是他不能再使用空间术了,这个时候小m慌了,他觉得他最大的一个依仗没有了,他懊恼自己的浪费。这个时候,他疯狂的翻自己的秘籍,就像知道,怎么让自己重新获得空间术。他查了一遍又一遍,终于发现自己的功法不能用是因为他没办法自己开辟空间,只能用继承下来前人的那一整块空间,但是在刚刚那一波疯狂输出的时候,这块继承的空间以及被消耗殆尽了。然后他再一次翻阅前人所遗留下来的文档,发现了,这个空间术在开辟空间的时候,只是暂用了前人的空间,前人空间有个特殊的属性,就是,当发现你没有再使用的时候,它就会把这被你借走的空间给拿回来,把里面的东西清空了。但是这个属性跑起来的这段时间内,小m就没有办法使用空间术了。小m发现了这样的一个属性的时候很高兴呀,感觉自己将再一次能掌握到空间术了,他开始研究起来,究竟什么情况下会被认识这块空间不被自己使用了。原来是自己把空间术所返回过来引用给遗弃掉,就是当自己从枪里发射出去,其实就是把引用给抛走了。现在小m就开始等待空间恢复了。

这颗子弹真奇怪

小m在探险的时候,发现了一种特殊的材质(接口),我们称这个材质为I。这个材质自带一种空间属性,小m用这个材质做了一枚子弹(struct),当这枚子弹在使用的时候被当成I类型使用的时候(装箱),它就突然变成了一个引用类型,它会把它所有的东西都包装到空间槽内,然后把自己变成一个引用类型的子弹。当把它当成一个struct类型的子弹使用的时候(拆箱),它会把它放置在空间槽内的东西都打包拿出来,变成一个真正的子弹,那部分被它用来放物体的空间槽就被遗弃了。等待回收。

后记

这篇太难产了,想换个写法,感觉自己还是写不出来,这种的写法不太适合我

[设计模式]单例模式初探

前言

最近在审核自己以前的代码,看来下自己之前为了方便,很多小的功能都用单例模式来进行编写。因为已经距离现在有一定的时间了,有些东西没有考虑清楚,今天我就初级的讲一下单例的几个坑。

正文

我需要一个对象

当我们在使用一个引用对象的时候,我们需要把它实例化,然后才能使用,实例化的概念就是在内存上给你分配一块内存,然后根据对象的构造函数,往这块内存里写数据,最后把这块内存的引用返回给你。所以当我们实例化之后,我们就在内存上占用了一块内存。

我是个渣男

当我们这个对象在使用完了之后,我已经不再需要它了,那我就需要让它消失,把它所占用的内存给回归到内存池里去,可以让别的对象所用。但是我不想每次都要自己去管理让它被销毁。它能不能自觉点,我不需要的时候就去自我销毁,减少我的工作量呢?在现在很多高级语言里,他们的虚拟机都给我们集成了这项功能,它的名字就叫GC(垃圾回收)。当我需要一个对象的时候,我就向托管堆里去申请一块内存来使用,当我们不在使用的时候,我们把对这块内存的引用给释放了。然后通过root节点找到它引用对象的再引用,一直找到最后的引用节点(就是一层一层的递归寻找)把它们标记成被引用,然后当GC回收的时候,它们找到这块内存的标记是引用就会过滤掉,把那些不再被引用到的内存给回收掉了。正是因为在打标记的时候需要做递归寻找,这就是为啥会说GC有很大的消耗的原因,需要我们管理好引用对象的生成。

我觉得你很有用,你可以一直跟着我

当我们需要经常使用一个对象的时候,我不想经常性的实例化这个对象,我希望它能永远的存在。这个时候静态类就出现了,我们可以直接通过它这个类名去直接调用它里面的方法,它的所有方法和变量都应该做成静态的。当一个静态对象的出现,它就进化成root节点之一。将从它开始查找打标记。

你不要一开始就出现

当我们把一个类定义成静态类的时候,它里面的所有变量,属性,方法都将成为静态的,也就是说只要程序一启动,它就把它所需要的内存所占用了。有的时候,我们可能会在很晚的时候调用它,或者在这一次应用启动的时候都不会调用它里面的方法,那它在启动的时候就分配内存,就会造成内存泄漏了。这个时候我们就可以在在普通类里声明一个静态变量,它的类型就是这个类。给一个静态属性,当我们调用这个属性的时候才开始实例化这个变量,这个时候才开始分配内存,当它是一个静态属性的时候,它还是一个能永远存在内存中的root节点。

你是一个成熟的对象,用完就要放手

当我们生成了一个静态属性的时候,它是一个root节点,所以它所引用的引用也会一直在内存中,不会被释放。所以对象我们在使用完了之后要把它释放掉,我们就要定义好这种类里的方法都不需要缓存数据的。要不然内存爆炸的锅就要你来背一背了。

哦!同时出生了这么多个

当我们使用了多线程的时候,就会发生这样的事,这个对象被同时要求做好几件事,当第一次调用的时候,它就会同时生成好几份对象,因为在调用的时候它是没有实例化的,所以我们就会同时进行实例化,那它就同时会创建好几个对象。这个时候我们就要把这个方法设置为同步方法。让它在同一时间只能被一次调用。

你干活可真慢

当我们把这个获取对象的方法设置成同步方法的时候,它在同一时间只能被调用一次,当我们在多线程的时候,其实我们只是为了规避它被创建多次,这个时候,我们可以把它的同步方法放在第一次创建生成对象的时候,当对象已经存在了,那就算被同步使用,其实也不会对结果产生什么差异,毕竟我们在这里是尽量使用方法内部的变量 ,而不会去使用类内的局部变量。

总结

class Singlecase{
    private static Singlecase instance = null;
    private static object locker = null object();
    public static Singlecase Instance{
        get{
            if(instance == null){
                lock(locker){
                    if(instance == null){
                        instance = new Singlecase();
                    }
                }
                return instance;
            }
        }
    }
}

上面的实例代码就是单例模式,单例模式在我的理解力就是为了能做一个全局的工具类,为了做一些通用的简单计算,不涉及一些数据的保存,只是为了一些数据处理。当我们使用单例模式的时候,需要保证我们这个类对一个不需要的对象,一直保持着引用,而使它的内存没有被回收掉。

【系统】Unity3D中的AssetBundle资源管理器(终)

前言

水不动了,一篇小小的资源管理器已经水了好几篇了,太让人难堪了。但是也让我知道了,沉淀没有想象的那么容易,就在以为自己思考的很全面的时候,测试下来又会冒出很多问题。

正文

问题

这次遇到的问题是在以下情况下出现的,上次我们用的异步做资源加载,但有一种情况我没有考虑到(也是我实际使用时碰到的一种情况)我们需要给一个 sprite 赋值一张图片,如果是在异步的情况下,这个资源回调还没有加载回来时,又因为刷新问题需要给这个 sprite 重新赋值一张图片,如果这时旧的图片在新的图片前面加载出来,那么最终显示的图片就是我们不想要的旧图片了。这也是异步的加载的一个缺陷,我们不能保证加载回来的顺序。

解决办法

我会在加载的时候给加载的接口一个当前需要赋值的对象的索引 id —— 这个索引 id 是唯一的。将这个 id 传到资源管理器里做一次判断,如果这个唯一索引已经对应上了一个 assetname 的话,就把之前记录下来的 assetname 所对应的 asset 卸载,然后在资源管理器里和需要加载的 asset 做一次绑定。这样的话,就能保证所加载的资源不会因为异步加载的顺序所产生一些显示错误。

更新

说明

之前在讲异步加载的时候,有一个特性我遗漏了,就是异步相当于开了一个线程池来同时加载所有的东西,这样在设备高速发展的今天,我们能高效的运用多核技术,使我们的游戏更流畅的运行。

技术更新

unity 的异步接口有个很有用的功能,就是如果你直接去获取 assetbundleCreaterequest 中的 assetbundle 属性 或者 assetbundleRequest 中的 asset 属性时,它在获取这个资源的时候,属于阻塞获取,除非 assetbundle,asset 加载回来,否则进程会一直阻塞在这里。但是我们说了,异步加载其实是线程池在跑的,所以这个阻塞不会影响到其他资源已经创建的加载。我们可以根据这个特性,来做一个同步接口,这个接口的好处就是,我们可以避免在打开 ui 界面的时候 因为资源没有加载 而实例化界面对象所产生的显示问题。这是这个特性的一种好的使用方案。

还有一种使用方案是,我们可以在界面打开的时候预加载一组 AssetbundleRequest 的对象,它是异步方法,所以当我们在界面打开的时候,它可能就已经加载了一部分了。打开的时候,我们可以直接去获取它的 asset,如果没有加载成功,它就会一直阻塞在这里,直到加载成功返回,这样做的好处是能让我们感觉界面打开的更流畅。

后语

这个资源管理器本来是为了沉淀自己的技术而做的一个技术文档,却没想到最后成了每周完成一篇blog的任务了。但是在写这篇文章的时候,我也感觉到了自己对于技术的把握还是差了一点,有些东西还是要多看、多学,很多很有用的特性都可能是自己一下子想不出来的。但是后期的文章,我将改变下方案,我会把周更变成月更,这样给自己更多的时间去对于相关的技术做一个查缺补漏。

【系统】Unity3D中的AssetBundle资源管理器(三)

前言

上一次讲到了用asset做资源管理,但是在asset的管理的时候,我们是同步把所需要的asset全部都加载出来的,前段时间看了下别人的管理机制,发现可以用unity的资源加载依赖来做这件事,就是不用我们自己加载asset,而是改用记录asset的依赖就好了。

正文

基于unity自身的依赖加载

在以前我们了解过,unity在加载一个资源的时候,如果它所依赖的assetbundle已经被打开了,那当我们加载它的时候,unity会自动把它所依赖资源一起加载出来,让asset能正常使用。在这个情况下,那当我们在根据asset来记录依赖的时候,我们只需要把它依赖的asset所在的assetbundle给加载出来,然后去对asset做一个引用计数的记录,这样的话,我们就可以省下去做加载依赖的asset这步的操作。在上一次我们所测试下来的结果,让我在这个版本做的方案就是,对assetbundle的卸载使用true这个参数,让它完全卸载掉。所以asset对于我来说就是一个做依赖关系绑定的用途。

改用异步的方式来做资源加载

在体验游戏的时候,我们可能最讨厌的就是界面卡顿,当点一个按钮需要打开一个界面的时候,我们发现,界面需要很久才出现,在等待的时候,我们会产生点击失败的感觉,这样很影响用户体验了。这种卡顿在我之前的方案里,很大一部分是因为我使用的是同步加载接口,它会阻塞住线程,等待资源全部加载回来才结束。这样的结果不是我们想要的表现方式。所以我改成用异步的方案来加载资源。但是在加载的时候,因为是异步,所以也会出现加载资源回来的顺序不一定是按照我们加载之前的顺序回来的。而且要抵消全部加载回来才显示的问题,我们可以做一个UI表现的动效,来抹平加载时间内的UI没有资源展示。

加载方案

当我们加载一个UI界面的时候,我们可以用对这个UI界面所需要的资源进行一个大的分类,如果是那种滑动列表内的资源,我们可以做一个优先级比较低的加载顺序,然后滑动列表加载进来的时候,做一个进入动画。那界面上顺序比较高的是哪个呢?我认为应该是玩家第一眼就能见到的界面,当我们在加载回来之后就可以直接显示了,那些一开始让玩家见不到面的资源,就可以在播动效的时候,再加载回来,其实不会慢很多,但是我们这样做就可以及时响应玩家的操作。我们需要给资源做一个引用列表,哪几个需要显示的prefab在引用它,还要记录一个状态,如果在加载中,所有需要引用的资源直接添加到引用列表里,如果它已经加载成功了,那就直接返回回调。

骚操作(我一般叫奇巧淫技)

在unity自带的资源依赖管理里,我们在加载出来一个prefab的时候如果它的资源没有加载成功,但是我们现实出来,它是会变紫红色的,但是,如果我们在接下来的时间内能加载出它依赖的asset所在的assetbundle的话,unity就会自动把资源赋值上去,正常现实的。所以在加载如果玩家看不到的资源,我们就可以一开始不用理会,只要第一眼能看到的资源以及加载成功了,我们就可以直接显示prefab。这里我就不知道如果出现这样的情况在unity底层会不会有cpu的消耗,一直在找丢失的资源。

卸载方案

如果我们使用了异步来做资源管理的话,会出现一个问题,就是我们如果这个资源还在加载的时候,我们就已经要把它卸载了,那我们就要在资源加载结束的时候做一个判断,这个资源是不是还用。所以我们需要给这个资源的回调做一个管理,如果不需要的话,我们就把回调事件-1,当资源加载回来之后,发现自己没有任何一个资源需要引用了,那它就把自己的状态变成卸载状态,等待下一次的回收。

资源的引用计数的问题

在之前我们所做的所有方案都是在一个理想的状态下做的,如果我们生成的一个prefab的资源,它被clone了,直接走unity的实例化,然后再把它给赋值到一个其他的地方,这个时候我们是不知道它真实的生命周期,这样的情况下,我如果默认它的生命死亡时间是跟某一界面,或者某一模块相关的话,那我们卸载了资源,它在别的地方的实体就会出现紫红色,然后出现问题。所以这种情况下,我现在想到的办法就是框架限制,资源的生命周期就限制死了,不能其他的地方使用我这个里面的资源,你要用自己去加载去。这样的话就能暂时解决了。其实这种方案也是有问题的,靠自觉,不知道什么时候就会出现问题的。只能等下一次解决了。