SUMTEC -- There's a thing in my bloglet.

But it's not only one. It's many. It's the same as other things but it exactly likes nothing else...

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

答案是:System.Security.Cryptography (.NET FX 4.0,见下图)

 

这个Quiz看过的人不超过160个,说明大家越来越水了。打口水仗各个精神百倍,唾沫星子四处飞。真正来实际的,就全都瞎火了。还记得很久之前某人的一个说.NET咋咋不好的一个系列贴吗,其中有一集说Metadata很大又没有用应该去掉等。当时这个帖子多么火热啊!可惜是否真这样,谁又真的研究过呢?要挑事一定要拿出证据来,批驳别人也一样的。当时我对此兴趣缺缺,只想说这不是什么重要的问题,大家洗洗睡吧,所以也就没多深挖。后来因为有SL4的项目,需要对XAP包瘦身,于是就顺道研究了一下.NET的DLL空间大小都分布在什么样的地方。正因如此,就自己写了一个小工具,可以看到底是什么占空间。

 

比如拿mscorlib为例吧,整个文件中大约60%的空间是IL代码,剩余的40%是MetaData,这其中的60%(也就是整个文件的约24%)由TableHeap占用。而TableHeap当中,MethodTable占了36%(整个文件的8.64%),ParamTable占了21%(整个文件的5%),CustomAttributeTable占了12%(2.88%),FieldTable占10%(2.4%)……(见下图)

 

 

等会儿,什么是MetaData?那些个什么Heap又是什么,Table又是什么?图中那个.text什么的又是啥?好,我们从大往小讲,先说说.text。

 

Windows的可执行文件会根据所承载的内容放在不同的段里面,比如.text就是放代码的段,可读可执行不可写;其实一般的应用有可能还有另一个.data的数据段,不可执行可读写。对于.NET的可执行文件(包括dll)来说,只有.text段,所有你的代码资源什么的通通被打包到这个段里面。好,这部分就解释到此,更细节的就于此无关了。

 

接下来,我们说说堆。.NET文件的堆包括以下几个:

BlobHeap,用于保存各种大小类型不一的元数据,比如方法的签名信息等;

GuidHeap,用于保存各种程序级别的GUID,一般来讲你可以忽视它;

StringHeap,用于保存程序本身的字符串,例如命名空间名、函数名、类名等等,这个部分是UTF-8的C编码字符串,也就是以0结尾的;

TableHeap,用于保存各种数据长度固定的表,后面再详细解释;

UserStringHeap,用于保存程序中的各种字符串常量,比如:

string s = "你好"; // 这个“你好”就会被保存在这个堆当中。这个堆和StringHeap的区别是,他使用UTF-16来保存的,并且是.NET格式编码,即,前面一个7BitEncoded的表示长度的数值,后面跟着字符串。

 

现在,我们不清楚的就该剩下TableHeap了,这里我不做详细的解释了,只拿一个MethodTable来说明问题。MethodTable会记录该方法的名称指针(指向StringHeap),方法的签名指针(也就是这个方法有多少个参数,分别是什么类型等等,指向BlobHeap),方法参数的描述指针(比如每一个参数是否和COM的什么描述有关,或者参数名是什么等等,指向ParamTable),方法的其它各种常规属性比如调用制式,以及一个指向Body的指针等。当然了,我们可以想象,还会描述这里面有哪些类的TypeDefTable,这个类里面都有什么属性的PropertyTable,有哪些字段的FieldTable……

 

如果你在这个的基础之上再去看看IL的格式,就会发现IL里面对函数的调用等,都不是直接给出一个地址,而是一个MetaData中的记录编号(第几个方法)。即便我们不需要反射,我们也不可能完全去掉MetaData而只能简化一小部分。比如说Param这个表里面的内容就几乎不是必须的,比如说如果你不没有在某个参数上打标签(Attribute),也与COM无关,并且也不期望通过反射来进行参数的查找,那这个函数就可以不产生Param表的一项。至于说方法的签名,你可以说其实不是必须的,但是这个和托管的概念是很有关联的——缺乏这个信息,核心就不能校验你的调用是否给出了正确的参数。在DLL的小版本改进中,随时可能会出现参数多了少了的问题。而是用该Dll的代码在调用的时候,就可能会出现各种莫名其妙的错误,甚至是不容易察觉的错误。

 

这么仔细想一下,Meta想要瘦身也不是一个容易的事情——要么损失托管的好处,要么优化不了多少。你看看上面的数据,每一项占整个文件的大小都不大,除非你整个的去掉。

 

当然了,如果你希望能优化点是一点儿,那么我可以给你几个提示:

1、以下的优化你必须自己写工具来处理,你可以利用一个开源的Cecil库来自己裁剪。当然了,某些混淆器也提供这样的功能,但是效果如何没研究过,而且很多时候貌似是以输出一个更大的文件为结果的; 

2、目前的StringHeap输出其实很傻,Enabled属性的getter叫做get_Enabled,是会被输出成两个字符串。但实际上C编码字符串的好处是,你可以指向一个字符串的中间。也就是说,你可以指输出get_Enabled,然后Enabled指向get_之后一个字节;

3、其实一般情况下,私有成员是不希望被使用的,如果你认为无反射需要,其实可以将这部分的成员名称随机指向一个已存在的StringHeap字符串。当然,如果你认为某个类有需要被反射调用使用,那么你可以给该类打上一个特定的标签(Attribute),你的工具则识别并跳过它;

4、ParamTable中大多数项可以裁剪掉,那里面大多数就记一下这个函数中各个参数的名称叫什么,如果你不需要反射并查找参数名称来进行各种匹配,貌似是可以不输出的;

5、不要试图重复利用ParamTable中已存在的项,这样做是徒劳的,因为实际上核心是按照顺序读取的——它加载文件的时候,会有一个指针指向当前使用的Param项,然后根据MethodTable中记录的ParamTable项数来读取若干条记录。总之,别浪费时间尝试用2当中的思路;

6、一个文件中的类、方法、属性等,最好不要超过65535个,一旦超过了,对这些内容的记录就会从2个字节增长为4个字节;

7、减少程序中的字符串常量,比如说,你可以利用反射来读取类和方法的名称时,就不要写一个“Type”这样的字符串,又或者自己用合适的编码比如UTF-8来编码一个文件并进行加载等。

 

通过使用上述方法,你有可能可以节约大概10%-50%的空间,文件越大效果越不明显。因为当中会有大量的IL代码,这部分你是很难进行优化的,至少你需要改动代码甚至结构。

 

P.S.:

1、各种资料请自己动手搜索吧,比如搜搜CLI或者UserStringHeap之类的; 

2、那个查看大小的程序,实在对不起,基于某种原因我不可能给大家提供。但是其实你下载一个Cecil的代码,改吧改吧还是很容易实现的。

posted on 2011-08-11 16:11  Sumtec  阅读(1599)  评论(1编辑  收藏  举报