[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来保存。

不安全代码

暂无

[工具]配表工具

前言

配表在一个游戏里是一个很重要的部分,它能让策划通过数值来控制游戏很大一部分体验。所以配表在一个游戏里是必不可少的一个环节。最近我就在做一个配表工具,现在就写了一篇文章来介绍一下我这个打表工具的思路

正文

配表的关注点

  1. 方便
  2. 内存
  3. 健壮

初级版本

现在在我们客户端开发游戏,一般都需要在两个地方读取配置,unity中的c#使用,热更新lua里使用的。所以一个简单的配表工具,就是直接生成一份lua的table配表

table = {
    key = {
        value =
    }
}
return table

用来在lua中读取。还有一份资源专门用来给c#读取的配置,这个格式很多比如json,xml,或者unity中的asset的资源。这样的可视化的好处是方便策划用来检查配置是否正确,也方便程序直接修改配表来更改配置的可以不用打表就可以测试功能,使用也比较方便,都有第三方的插件用来生成对应结构供运行时使用。但是这种的坏处就是内存占用比较高,一套配置在运行中中需要分别生成c#和lua两套结构,也容易被破解然后直接被获取应用配置。

升级版本

碰到问题,那我们就去解决这个问题,首先我们先解决这种可视化,能被用户破解修改的这个问题。我决定选择二进制文本来解决这个问题,因为二进制文件必须知道我们序列化结构,才能把二进制文本解析成我们代码所需要的结构。所以我们直接把配表生成二进制文件,然后在运行时,在反序列化成对应的类型(int,string,bool等),这样之后,我们的内存压力也变小了,因为二进制文件比json这种可视结构要小很多。而且我们还能按需序列化了,能占用更少的运行时内存。但是使用二进制去解析有个问题,就是如果生成二进制所使用的结构和解析二进制使用的结构不匹配,会出现解析失败。

三代版本

现在先解决上一个版本新出现的一个优化点,按需加载配置。先说一下这个概念吧,就是按游戏的进度,每次所需要使用的配置内容不会特别多,很多配置很有可能后面都不需要使用了,所以我们可以每次使用的时候就加载那一行的数据,这样我们就可以少序列化很多数据,减少运行时内存压力。在配表中,我们会有一个唯一key,用来读取某一行数据,那根据这个功能,我就生产了一张key对应当前行的二进制索引位置,从这个位置开始序列化这一行的数据。

最后解决序列化的问题

在解决序列化问题的时候,我决定使用protobuf的一部分特性,然后把那些需要定制化的结构,都在运行时候自己再解析,只支持一些基础类型,而且还能结构公用

message ConfigValue{
    int32 int_value = 1;
    string string_val = 2;
    float float_val = 3;
    bool bool_val = 4;
}

message ConfigRow{
     repeated ConfigValue config_val = 1;           //行数据
}

message ConfigIndex{
    ConfigValue key = 1;
    int32 star_index = 2;
    int32 end_index = 3;
}

message ConfigTable{
    repeated string config_title = 1;   //表头
    repeated string config_type = 2;    //字段类型
    string key = 3;                     //key的字段名字
    repeated ConfigIndex config_index = 4;  //key对应数据中的位置

}
message ConfigData{
    repeated ConfigRow config_row = 1;  //数据内容
}

结构如上,定义了一个configValue这个结构,它是用来保存基础类型,在我的规划里,我只需要知道int,string,float,bool这四种基础类型,List这样的数组通过定义一个特殊的切割符号,通过string导出使用。configRow这个结构就是用来导出一行的数据;configData就是结构就是这张配置的数据内容。
Configindex是代表每一行的唯一key和当前行在configData二进制中的位置。
Config_title代表的是表头,就是这个字段列表
Config_type代表的就是这个表头所代表的类型

示例

{ "configRow": [ { "configVal": [ { "intValue": 1 }, { "stringVal": "测试普通+伴随" }, { "intValue": 2001 }, { "stringVal": "111,0,47.5" }, { "floatVal": 10 }, { "stringVal": "1|2|3" } ] }, { "configVal": [ { "intValue": 2 }, { "stringVal": "测试普通+嘿嘿" }, { "intValue": 2004 }, { "stringVal": "888,255,47" }, { "floatVal": 33 }, { "stringVal": "4|5|6" } ] } ] }
{ "configTitle": [ "id", "des", "key", "coordinate", "testFloat", "TestListInt" ], "configType": [ "int", "string", "int", "vector3", "float", "listInt" ], "configIndex": [ { "key": { "intValue": 2001 }, "starIndex": 2, "endIndex": 64 }, { "key": { "intValue": 2004 }, "starIndex": 66, "endIndex": 128 } ] }

打出的内容如上所示
我们就可以通过获再configTitle中获取key知道key是第三个元素,并且它的类型是int,所以我们就能从configIndex里的intvalue取值,2001这个key的元素的在二进制里的索引和大小,然后序列化configRow得到相对应的值。因为我们的字段和类型都是一一对应的,所以当我们删除了字段coordinate的时候

{ "configRow": [ { "configVal": [ { "intValue": 1 }, { "stringVal": "测试普通+伴随" }, { "intValue": 2001 }, { "floatVal": 10 }, { "stringVal": "1|2|3" } ] }, { "configVal": [ { "intValue": 2 }, { "stringVal": "测试普通+嘿嘿" }, { "intValue": 2004 }, { "floatVal": 33 }, { "stringVal": "4|5|6" } ] } ] }
{ "configTitle": [ "id", "des", "key", "testFloat", "TestListInt" ], "configType": [ "int", "string", "int", "float", "listInt" ], "configIndex": [ { "key": { "intValue": 2001 }, "starIndex": 2, "endIndex": 50 }, { "key": { "intValue": 2004 }, "starIndex": 52, "endIndex": 100 } ] }

对应的索引下标就会删除,我们的二进制位置也就做了一次偏移。这样我们只需要在处理配表的时候做一次容错,找不到configtitle的时候,返回一个空的对象,那么在没有使用这个字段的时候不会因为字段的删除导致解析出错,影响到程序运行。
这样策划在打表的时候就不需要关心结构是否匹配,只需要无脑的把表打出来,然后程序在用的时候再对字段做出处理。

最后再说点

每家公司,每个策划组都有自己配表的方式,有的人因为一张表过大,喜欢分多个excel然后打表的时候整合再一起;有的人想要简化运行时做结构生成,想要在打表的时候就把结构也生成出来;所以需求千变万化,都是使用者的习惯来做定制化的,我只能表达我遇到的问题,所解决的问题。

[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传递的,这里还产生了一个装箱操作,所以消耗会更大。

[资源管理器]之前资源加载的纠错

前言

这篇文章是为了之前在做资源依赖的时候关于Asset->Assetbundle依赖加载之中错误的一个纠正

正文

在之前的文章我说了一种依赖加载的方案,就是记录用Asset资源对于它所依赖的assetbundle的依赖关系,然后通过这种的依赖关系来进行资源加载,这种方案的好处就是,当我Asset不在依赖Assetbundle的时候就可以卸载这个Assetbundle的,而不需要因为我Asset所在的assetbundle没有卸载就不去卸载依赖的Assetbundle。但是今天在和大佬在聊天的时候,他和我说了一种情况,我们选确定一个依赖关系AB1依赖AB2,然后我们先加载AB2在加载AB1然后卸载AB2再加载AB2然后去实例化AB1中的资源。是不是看起来很绕,其实很简单,就是去确定一点,在Unity中的AB包之间的依赖关系,会不会因为多次加载他们的唯一标识会修改,导致AB1中所记录到的AB2的标识没有更新,导致找不到AB2。通过测试,我发现如果第一次走这样的情况,其实AB1的资源是不会丢失的。loadAB1->instanceAB1(丢失资源)->loadAB2(资源被自动赋值上去,正常现实)。但是第二次加载了就会出现问题loadAB1->instanceAB1(丢失资源)->loadAB2(自动赋值)->DestroyAB1->unloadAB2->loadAB2->instaceAB1(丢失资源)。在这种情况下资源就不会赋值上去了。

结束

因为我在之前的测试不研究,也因为对AB的加载过程没有那么熟悉,导致得到了一个错误的结论。现在特出一篇文章作为纠错。之后我会很严谨的测试不同的情况。

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

前言

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

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

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

引用类型中包含的值类型

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

正文

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

一位仙气少年

在编程世界里,我们程序启动有一个虚拟机在跑。当我们把世界转向一个玄幻的世界里,那这个虚拟机我们让他叫做小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类型的子弹使用的时候(拆箱),它会把它放置在空间槽内的东西都打包拿出来,变成一个真正的子弹,那部分被它用来放物体的空间槽就被遗弃了。等待回收。

后记

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

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

前言

在之前的那片文章上写了我们可以根据Asset的依赖,来做ab包的加载。这样可以在Asset不在用的时候及时释放内存中的资源,减少内存的峰值。也可以减少一次性回收资源太多,导致cpu峰值。

正文

设计思路

准备阶段

首先我们需要在打资源的时候做一次资源相互依赖的配置。然后把这份配置放置在StreamingAssets下,然后可以动态热更下来。在使用的时候,我们先根据ab包里总的Minifest来把每个ab包中有那些资源给读取出来,然后再根据资源依赖关系配置,我们就可以把这两个文件关联起来,asset->depedencyAssets->assetbundle。

加载阶段

根据这样的关系我们在加载的时候,当需要加载一个asset的时候,我们可以去读取它所关联的depedencyAssets资源,然后判断这个资源有在被使用,如果被使用了,我们就给他的引用计数+1;如果没有我们就根据这个asset去找它在哪个assetbundle中,然后把这个assetbundle给加载出来,再看下它所引用的asset有没有加载出来,加载出来就引用计数加1。在这里我们可以看到,只在第一次加载的时候会去给它所依赖的Asset做一次引用计数+1,因为依赖的Asset只需要去关心它是不是被依赖了,而不需要关心依赖它的那个资源被加载几次;因为只要它还在被引用,那就自然而然依赖的资源就不能被回收释放的。

卸载阶段

当我们需要卸载一块资源的时候,我们首先要做的就是,把这个资源的引用计数给-1,然后当他已经完全没有被引用了,那我们就去寻找它所依赖的资源,然后也都减1;然后我们查找这个资源所在的assetbundle包,去遍历它里面的Asset是不是都不被引用了,如果都不被引用了,我们就给这个资源做个标记要被回收的;不立刻被回收而是去做标记的好处就是,当我们的下一次还要用ab包里的资源的时候,我们可以不用再去加载ab包了,直接使用从这个ab包里拿资源,然后把这个ab包在状态给修改回去。我们将找一个空余的时间去把所有不用的ab包给回收掉。

关注点


这张图我们看到没有我们要加载的资源

这张图是我们第一次加载资源的时候它所被引用到的对象
这个时候当我们调用Resources.UnloadAsset卸载资源,它就会变成第一张图,这个资源就从内存里消失了,然后我们不去卸载ab包,第二次我们直接调用资源加载,我们看到的情况和之前是一样的。
但是,如果我们这个时候把ab包给卸载了,然后重新加载ab再加载asset之后,我们将看到这样的资源依赖图

这个时候我们再去调用Resources.UnloadAsset卸载资源你会看到资源没有被依赖,但是没有从内存中消失。

这个时候我们要卸载这个图片必须得调用assetbundle.Unload(true)来卸载,或者调用Resources.UnloadUnusedAssets()这来卸载,后一种卸载方式是会循环遍历所有的资源然后去卸载,所有会有很大的消耗;前一种的卸载方式是把资源依赖给强行解放了,所以当你实际上资源还在使用的话,就会变紫色,丢资源。
在这种Asset没有被回收的情况下,我们assetbundle没有回收,然后我们再从assetbundle中把资源加载出来,实际上使用的资源还是这个,它会被重新被引用上。但是如果我们调用了assetbundle.Unload(false)来把ab给回收了,但是没有去调用Resources.UnloadUnusedAssets()来回收asset,那你会发现这个丢失引用的资源就成了冗余资源,我再次从新加载ab,加载asset的时候,就是重新生成一份资源了

就成这样的情况了。

总结

源码地址
我们在做asset的资源管理的时候,一般来说直接调用true来做一次强行的资源回收,但是如果需要很在意表现,不想表现穿帮,对于一些资源就算冗余也无所谓的情况下,那最后调用一次Resources.UnloadUnusedAssets()来做一次资源的回收兜底行为。下一次我是做场景加载呢,还是做一个资源打包编辑器呢?看时间吧!鸽的前兆。

【系统】unity3D中的assetbundle资源管理器(一)

导言

assetbundle 的解释

说起 U3D 的 assetbundle 大家应该都不会很陌生吧,我的理解是 —— assetbundle 属于一种 unity 自己可认的压缩文件格式,把你想放在一个包里的资源给打包到一起,这样就形成了一个 assetbundle。

但是使用这个压缩格式还有一个注意点,就是可能你要打包的资源,它引用到了另外一处资源,即它对一个外部资源有依赖。所以你在打 assetbundle 的时候,unity 会把这个依赖关系记载在 manifest 的 Dependecies 上,虽然是对 asset 的依赖,但是 unity 帮你管理的是大范围的依赖关系,所以记录下来的是 assetbundle 对 assetbundle 的依赖。

为什么要做 assetbundle 的资源管理器

当我们在使用 assetbundle 做资源热更新的时候,我们需要了解,在使用asset资源的时候,assetbundle是怎么被使用的。比如我们需要加载一个prefab的时候,我们需要先解包这个prefab所在的assetbundle,然后我们还需要解包这个prefab所依赖的asset所在的assetbundle。这个asset到asset的依赖是unity帮我们管理的,会记录在asset中,我们在用文本打开资源的文件的时候,是可以看到在unity中这个asset的所有信息,包括它依赖的asset,(这部分就不详细展开了)。但我们把assetbundle在内存中解压之后,内存就会存放这个assetbundle的镜像,内存就会变大,如果我们只解压不去卸载这部分内存的话,那游戏内存就会持续升高,超出系统所给的内存大小,系统就会把引用杀死,造成闪退。所以我们需要在加载的时候解压assetbundle,在不用的时候把这个assetbundle给回收了。

实现方案

准备阶段

我们将在这个阶段先解压assetbundle的整个manifest,然后给所有的assetbundle给记一个int的唯一标识,这步是为了在后面使用assetbundle的时候可以使用int来做标识,减少string的使用,节省gc。再把所有assetbundle所包含的asset的资源和这个assetbundle做好一个依赖关系。

使用阶段

当我们要加载某一个asset的时候,我们先判断这个asset有被加载,加载过的,我们就asset做一个引用计数+1的操作,如果没有的话,我们就去加载这个asset所在的assetbundle,然后我们在加载assetbundle的时候,看这个assetbundle还使用了哪些assetbundle,我们都做一次加载,做一次引用计数加1的操作,然后我们在实例化出来这个asset。

使用完成阶段

但我们卸载asset的时候,会对这个asset来做一个引用计数-1的操作,如果引用计数小于等于0了,我们就可以卸载这个asset,进而对这个assetbundle来做一次-1的操作,但assetbundle的计数也小于等于0的时候,我们就可以卸载这个assetbundle了,对它所依赖的assetbundle的引用计数-1。这一整个assetbundle的使用周期就结束了

注意点

在做assetbundle的引用计数的时候,我遇到了一个问题,我是使用GetDirectDependencies来获取这个assetbundle所依赖的单一次的assetbundle,所以如果所依赖的assetbundle再依赖我这个assetbundle的时候,我这时候就会出现循环依赖(a->b->a这种情况),然后卸载就会出现问题,因为我在引用计数的时候对a其实做了两次引用计数了,卸载我其实就引用计数-1,然后因为还有一次引用计数所以b不会被卸载,所以对a的引用也不会减少,这样实际上我们其实不需要引用了,但是事实上循环引用导致我们卸载不掉。
在这里我提供两个解决方案:

  1. 我们做一次调用栈的计数,如果这次调用循环中,如果这次调用的资源是记录在栈里的,那我们就不做引用计数+1的操作了。
  2. 我们换一个接口,GetAllDependencies 这个接口返回的就是这个assetbundle所依赖的所有资源,包括它依赖的依赖。这样我们就可以一次性对所有的资源做引用计数加1了。

优化点

  1. 上面所说的assetbundle的循环依赖,其实我们可以做关于asset的依赖管理,asset的依赖是不太会出现asset之间的循环依赖。这种优化还能解决我们关于assetbundle的提早释放。比如assetbundle(a)中两个资源(a1,a2)对两个assetbundle(b,c)里的资源(b1,c1)做依赖。(a1->b1,a2->c1)这种情况的依赖在我现在的方案中,因为是记录的assetbundle的依赖关系,所以就算a1被卸载了,不使用了,但是a还有一个引用,所以a不会被卸载,b和c也就不会被卸载。其实b不再被使用了,但是还是没有办法被卸载了。如果我们做的是asset之间的依赖引用,那么我们a1被卸载了,b就不再被引用了,就可以被卸载出来

  2. 因为我们使用的assetbundle做的资源依赖管理,所以我们调用的卸载接口是直接assetbundle.Unload(true)。这个接口的问题就是,如果我们的资源还在使用,却调用到了这个接口,那么资源就会丢失,就会出现紫色的。所以最后还是要回到使用asset的依赖管理上。

demo

第一版本的demo

[周总结]第二次总结

前言

这周过的十分堕落,不知何故,看了一周电影。可能学习的累是一种需要只发去适应的吧。

工作

在这周工作里,写了一个最小堆的实践,定时器。出了一个加密包,和几个渠道包,把bugly接入到了新项目里面,正式的把XUPorterk框架放入项目了,暂时还没有去查看它这个框架是怎么把sdk的包给拷贝到xcode工程里。

学习

本来是需要看3D编程大师的,但是在学到现在的时候,感觉有点厌倦了,可能是光看却不知道怎么应用,导致这样到情况,现在看下来是用c++实现的,但是据说是可以用c#去写。这块需要去看看。但是又想先看shader入门经典的,自己真的是学东西没有目标性的,任何东西都想知道,但是不去专注的话,应该是学一般,然后不用,接着就忘记了。

终结

这周是一个放纵的一周,我认了,周末也是因为来台风,导致看了两天的电影。电影真的是消磨时间的好帮手,过去的时间既往不咎了,希望接下来的一周能好好的去学东西,有点收获了。不要忘记自己想要的东西。