C#Assembly详解

2025-12-13 0 741

Assembly(翻译成中文是集会), 这里把它翻译为配件程序集, 以示和组件(Component)加以区别。一个配件有时候是指一个EXE或者DLL文件, 实际上是一个应用程序(就是指带有主程序入口点的模块)或者一个库文件。但是配件实际上可以是由一个或者多个文件组成(dlls, exes, html等等), 代表一组资源, 以及类型的定义和实现的集合.。一个配件也可以包含对其它配件的引用。 所有这些资源、类型和引用都在一个列表(manifest)中描述manifest也是配件的一部分,所以配件是一个自我描述的,不需要其它附加的部件。

对其描述配件的另一个重要特性是,它是.Net环境下类型标识的一部分,也可以说是基本单位。因为,区分一个类型的标识就是包含这个类型的配件名字加上类型名本身。

举个例子,配件A定义了类型T, 配件B也定义了同名类型T,但是.Net把这两个类型认为是不同的类型。 注意,不要把配件(assembly)和命名空间(namespace)混淆起来。其实命名空间仅仅是用来把类型名用树的形式组织起来的手段。对于运行是环境来讲,类型名就是类型名,和名字空间一点关系都没有。 总之,记住配件名加上类型名唯一标识一个运行时类型。 另外,配件也是.Net框架用于安全策略的基本单位,许多安全策略都是基于配件的。

怎样生成一个配件呢?

生成一个配件的最简单办法就是用.Net编译器。例如:下面是一个C#程序ctest.cs

public class CTest
{
public CTest()
{
System.Console.WriteLine( "Hello from CTest" );
}
}

命令行这样写:

csc /t:library ctest.cs(csc是c sharp compile的意思)

然后,你可以用ILDM查看一下这个配件中究竟定义了什么。

产生配件的另外一种办法是,把多个模块(modules, 它也是由编译器产生的,对于C#,就是用/target:module选项,用配件连接器(al.exe)(assembly link)装配成一个配件。

私有配件和共享配件之间有什么区别?

私有配件通常只被一个应用程序使用,一般它被保存在应用程序目录或者其子目录下面.。共享配件通常保存在全局的配件catch缓冲区中, 它是一个由.Net运行时环境维护的配件仓库

共享配件通常是许多程序都要使用的代码库,比如.Net框架的类库就是如此。

事实上, 我们应该如下区分三种配件:

* 私有(private):

只对一个应用程序可见; 这是缺省配置, 其它的应用程序不能对其引用,这个配件必须在应用程序目录或者其子目录下面有个拷贝.

* 公有(public):

对其它的应用程序可见, 不管它在什么目录下面(可以是URL),其它的应用程序都可以对其直接引用.

* 公有共享(public shared):

共享的带有版本控制的配件的当前实现, 应该使用这种类型. 这种类型特别适合于第三方控件.

Net环境怎样查找配件?

当然是按照路径查找, 规则如下:

* 在应用程序所在目录及其子目录下面私有配件

* 对于共享组件, 除了上面的规则, 再加上.Net提供的共享配件缓冲区路径.

配件怎样版本化?

我们已经知道所有的类型对象是使用全局的ID标识的, 那么配件是怎样版本化呢?

配件通过版本号控制所谓的版本兼容性,引用配件的时候,就需要给出配件名字和版本号.

版本号分为4个部分(举例, 5.5.2.33). 分类如下:

不兼容: 前两个不同

可能兼容: 前两个相同, 第3个不同

兼容: 前三个相同, 第4个不同

注意:版本控制只适用于共享配件!

介绍

在传统的Windows应用程序开发中,动态连接库(DLL)为软件提供了一种重要的可重用机制。同样组件对象模型(COM)也通过DLLs和EXEs的形式提供了组件重用机制。在.NET的世界里, 则由assembly(译者注:可以翻译为程序集,不过感觉不十分贴切,因此以下均保留了英文原文)提供了类似的可重用的代码绑定机制Assembly中包含了可以在CLR(Common Language Runtime)中执行的代码。所有的.NET应用程序都是由一个或多个assembly组成的,不论你在创建一个Console, WinForms,WebForms应用程序或者一个类库时,实际上你都是在创建assembly。甚至.NET本身也是通过assembly来实现其功能。

一个assembly可以由一个或者多个文件组成,简单来说,你可以把assembly理解成一个逻辑上的DLL。每个assembly必须有一个单独的执行入口如DllMain, WinMain, Main等。Assembly也有一套配置(Deploying)和版本控制(Versioning)的机制。和传统的DLL等COM组件相比,.NET有着明显的优点(我们将在后面看到),另外它还可以避免一些诸如DLL兼容性等问题的困扰(地狱般的困扰,译者深有体会),并可以大大简化配置上存在的问题。

静态和动态的Assembly

通常我们可以用Visual Studio.NET或命令行编译器(.NET SDK中带的)来生成assembly。

如果你正确的编译你的代码,assembly就会以DLL或者EXE的形式保存在磁盘上。像这样保存在物理磁盘上的assembly被称为静态assembly。

.NET也允许你通过Reflection APIs来动态生成assembly。(Reflection指获得assembly信息以及assembly类型信息的功能,类型信息指assembly中的class, interface, member, method等内容。Reflection APIs在System.Reflection名称空间内)。像这样的驻留在内存里的assembly被称作动态assembly。如果需要,动态assembly也可以保存在磁盘中。

系统需求

下面我们将主要讨论静态assembly,本文所带例程需要运行在装有.NET SDK的机器上(Beta1或者Beta2)。你可以使用Visual Studio.NET来创建单文件的assembly。

私有的和共享的Assembly

当启动一个.NET应用程序的时候,程序首先要检查自己的安装目录中是否有需要的assembly,如果几个程序运行,那么每个都要在自己的安装目录中查找自己需要的assembly。也就是说每个程序都使用自己的assembly备份,这样的assembly称为私有assembly。它们只在应用程序的安装目录范围内有效。

一些情况下,你可以发现多个应用程序需要使用共享的assembly而不只是使用他们自己的,对这种情况,你可以在全局assembly缓存(译者:Global Assembly Cache,这个翻译有点不伦不类,大家明白就好)中管理该assembly(后面会提到)。这样的assembly在全局过程中有效,可以被机器内的所有程序共享,被称为共享Assembly。如果应用程序不能在自己的安装目录中得到需要的assembly,它将在全局assembly缓存中查找。如果愿意你可以考虑把你的assembly成为共享assembly。

Assembly的优点

在深入assembly细节之前,我们先来大概了解一下和传统的COM组件相比,assembly有那些优点:

Assembly可以把你从DLL地狱中解救出来。

DLL地狱很折磨人,典型的COM组件应用通常是把一个单独版本的组件放在指定的机器上,这样带来的问题就是开发人员在更新或者维护组件时常常会因为组件版本的向后兼容性的限制而碰钉子。而.NET中解决这个问题的方式很简单:建一个私有的assembly好了,它将有能力管理同一组件的不同版本,assembly保留其不同版本的copy,如果不同的应用程序需要使用同一组件的不同版本,那么通过调用组件不同的copy就可以。这样就可以避免组件兼容性常常出现的问题。.NET也允许我们跨机器来共享assembly,当然这种共享要受到严格的限制。

Assembly支持并行(side-by-side execution)执行

这么说有点不好理解,不过很简单,也就是说同一assembly的不同版本可以在同一个机器上同时执行。不同的应用程序

可以同时使用同一assembly的不同版本。共享式assembly支持这种并行执行。

Assembly是自描述的

COM组件需要把一些细节信息保存在系统注册表或类型库里。当使用COM组件的程序运行时,它首先要去注册表里收集组件的细节信息然后才能调用。不象COM组件,.NET Assembly是自描述的,它们不需要把任何信息保存在注册表里,所有的信息都存在于assembly自己的元数据(Metadata)里了(后面会讲到Metadata)。

配置简化

assembly是自描述的,它不依赖于注册表保存信息,因此完全可以使用XCOPY之类的方法来配置它。

卸载容易

不需要注册表,当然简单的删掉就算是卸载了。

Assembly的结构

载创建一个assembly之前,我们先来了解一下assembly的组成结构。Assembly由以下几部分组成:

Assembly Manifest(译者:Assembly清单?不贴切,其实类似于一个目录或者入口)

包含assembly的数据结构的细节。

类型元数据(Type Metadata)

包含assembly中允许的类型数据。(前面提到过,class, interface,member, property等)

Microsoft Intermediate Language code (MSIL)

单文件和多文件Assembly

上面提到的assembly结构中包含的东西可以被绑定到一个单独的文件里。这样的assembly叫单文件assembly。另外,所有的MSIL代码和相关的元数据也可以被分到多个文件中,这些文件中每一个单独的文件称为一个.NET Module(模块),.NET module中也可以包括其他一些文件如图像文件或资源文件。

下面我们了解一下assembly manifest的更详细的信息。Assembly manifest保存了assembly细节的数据结构。对多文件assembly来说,assembly manifest好像一个“绑定器”把多个文件绑定到一个assembly中。请注意Manifest和Metadata并不相同,Metadata保存的是在assembly和module里用到的数据类型(如class, interface, method等)的相应信息,而Manifest是用来描述assembly本身结构的细节信息的。

对单文件Assembly来说,Manifest嵌在DLL或EXE文件内,对多文件assembly, Manifest可以内嵌在每个文件中也可以存在于一个委托(constituent)文件里。后面将会有详细说明。

下面列出了Manifest中的主要信息:

  • *Assembly名字
  • 版本号
  • Assembly运行的机器的操作系统和处理器
  • Assembly中包含的文件列表
  • 所有assembly依赖的信息
  • Strong Name信息

Metadata

Metadata数据是对assembly中数据的定义。每个EXE或DLL包含自己的详细的类型信息,这种数据叫Metadata。主要包括以下信息:

  • Assembly的名字和版本
  • Assembly暴露出的类型信息
  • 基本的类和接口信息细节
  • 安全访问细节
  • 属性细节(complier and custom)

Modules

前面提过Assembly可以有一个或多个Modules组成。Module可以看作是一系列可管理的功能模块。它们转化为MSIL,一旦代码在runtime中运行,它们就可以被加入assembly。请注意module本身并不能执行,要利用它们首先要把它们加到assembly里。当然一个module可以被加到多个assembly中;配置一个assembly的同时也必须配置所用的modules。

创建单文件Assemblies

现在我们了解了.NET Assembly的一些基本知识,下面我们可以用C#创建一个简单的assembly。你可以用VS.NET或者命令行编译器,下面这个例子可以使用命令行编译:

using System;

public class Employee
{

string m_name;

public string Name
{
get
{
return m_name;
}

set
{
m_name=value;
}
}

public int GetSalary()
{
//put your logic instead of hard coded value
return 10000;
}
}

上面的代码说明创建了一个叫Employee的类,该类包含了一个的方法和一个属性,你可以在文本编辑器中输入以上代码并保存为employee.cs。用下面的形式做命令行编译:

csc /t:library employee.cs (csc是C Sharp Compiler)

执行过上面的命令,你将得到一个叫Employee.dll的文件,这就是一个单文件的assembly。

创建多文件的Assembly

这里我们将建立一个叫CompanyStaff的assembly,包括两个类Clerk和Manager。下面我们看看创建多文件assembly的两种办法:

第一种方法是分别编译Clerk和Manager两个类到不同的modules中,然后把两个modules加到CompanyStaff DLL中去得到最终的assembly。这时CompanyStaff DLL将管理assembly manifest。这种方法可以用正常的命令行编译实现。(这里是CSC)

第二种方法是分别编译Clerk和Manager两个类到不同的modules中,然后生成一个单独的包含有assembly manifest的文件,并用这个文件来表示最终的assembly。这种方法将使用一个叫做AL.EXE的工具来创建assembly。

使用命令行编译器创建多文件assembly

我们将进行以下步骤:

  • 创建一个叫Clerk的类到一个module中,包含一个方法叫GetClerkName,返回一个数组包含公司内职员的名字。
  • 创建一个叫Manager的类到一个module中,包含一个方法叫GetManagerName,返回一个数组包含公司内经理的名字。
  • 创建CompanyStaff类,包含一个叫做DisplayStaff的方法来实例化Clerk和Manager两个类并把其中职员及经理的名字简单的打印出来。把这个类编译到一个assembly(DLL)中,这时也就将Clerk和Manager的module信息编译到最终的DLL中去了。
  • 创建一个客户端程序来使用该assembly。

Step1: 创建Clerk Module

把下面的代码输入到Clerk.cs中

using System;

public class Clerk
{
public string[] GetClerkNames()
{
string[] names={\”Clerk1\”,\”Clerk2\”,\”Clerk3\”};
return names;
}
}

用命令行编译这个类:

csc /t:module clerk.cs

这里/t:module开关告诉编译器把代码编译成一个module。

需要说明的是,在beta1中编译时,如果使用C# compiler,将得到扩展名为.dll的module,如果用VB.NET的complier,得到的扩展名为.MCM。而在beta2种得到的都是扩展名为.NETMODULE的module.

Step2: 输入下面代码到Manager.cs文件

using System;

public class Manager
{
public string[] GetManagerNames()
{
string[] names={\”Manager1\”,\”Manager2\”,\”Manager3\”};
return names;
}
}

用下面的命令行形式编译:

csc /t:module manager.cs

Step3: 创建CompanyStaff assembly

在companystaff.cs文件中输入以下代码:

using System;

public class CompanyStaff
{
public void DisplayStaff()
{
Clerk c=new Clerk();
Manager m=new Manager();
string[] ClerkNames;
string[] ManagerNames;
ClerkNames=c.GetClerkNames();
ManagerNames=m.GetManagerNames();
Console.WriteLine(\”Clerks :\”);
Console.WriteLine(\”=======\”);
for(int i=0;i<ClerkNames.Length;i++)
{
Console.WriteLine(ClerkNames);
}
Console.WriteLine();
Console.WriteLine(\”Managers\”);
Console.WriteLine(\”=======\”);
for(int i=0;i<ManagerNames.Length;i++)
{
Console.WriteLine(ManagerNames);
}
}
}

用下面的命令行形式编译:

csc /t:library /addmodule:clerk.dll /addmodule:manager.dll companystaff.cs

这里/addmodule开关用来把前面建好的两个module加到CompanyStaff.dll中,也就是一个多文件assembly中。

Step4: 创建一个客户程序来使用assembly

在SimpleClient.cs文件中输入以下代码。

using System;

public class SimpleClient
{
public static void Main()
{
CompanyStaff cs =new CompanyStaff();
cs.DisplayStaff();
Console.Write(\”Press Enter To Exit…\”);
Console.ReadLine();
}
}

用下面的命令行形式编译:

csc simpleclient.cs /r:companystaff.dll

这样就准备好了,你可以运行simpleclient.exe,将会列出clerk和manager的名字。

用AL工具创建一个多文件assembly

现在我们可以使用AL来创建CompanyStaff assembly了。AL是一个用来整合一个或多个MSIL代码文件或者资源文件并生成一个带有管理manifest assembly的工具。和前面例子中的Step1与Step2一样生成modules。因为我们要在一个独立的文件中建立assembly manifest,所以我们不必再亲自创建CompanyStaff.dll文件,我们要用AL来生成它。

输入下面的命令行:

al clerk.dll manager.dll /out:CompanyStaffAL.dll /t:library

AL命令需要接受MSIL文件或资源,以空格分开,另外我们还要指定输出文件名(这里是CompanyStaffAL.dll,是为了与前面已经生成的文件名区分开)

现在你的assembly准备好了,我们可以创建一个客户程序来使用它。在前面的例子里,我们把DisplayStaff方法写在了CompanyStaff类内,现在,我们可以通过AL得到CompanyStaff.dll,所以我们可以在客户程序中写一个同样的代码来实现同样的功能了。

在SimplaClientAL.cs文件中输入下面代码:

using System;

public class SimpleClientAL
{
public void DisplayStaff()
{
Clerk c=new Clerk();
Manager m=new Manager();
string[] ClerkNames;
string[] ManagerNames;
ClerkNames=c.GetClerkNames();
ManagerNames=m.GetManagerNames();
Console.WriteLine(\”Clerks :\”);
Console.WriteLine(\”=======\”);
for(int i=0;i<ClerkNames.Length;i++)
{
Console.WriteLine(ClerkNames);
}
Console.WriteLine();
Console.WriteLine(\”Managers :\”);
Console.WriteLine(\”=======\”);
for(int i=0;i<ManagerNames.Length;i++)
{
Console.WriteLine(ManagerNames);
}
}

public static void Main()
{
SimpleClientAL cs =new SimpleClientAL();
cs.DisplayStaff();
Console.Write(\”Press Enter To Exit…\”);
Console.ReadLine();
}
}

编译上面的代码并运行,你可以得到和前面的例子一样的结果。

共享式assembly和全局assembly缓存

到目前为止,我们看到的都是私有式的assembly。当然在.NET应用中,我们多数都在单独的使用一些私有式的assembly,然而有时候你可能会需要在很多个应用程序中共享一个单独的assembly的备份。我们前面提到过,共享assembly需要把assembly放到全局assembly缓存中去(Global Assembly Cache)。全局assembly缓存是磁盘上一个特殊的目录,一般它位于<driver>\\WINNT\\ASSEMBLY目录下。注意当安装过.NET后,这个目录在explorer下显示和其他目录有点不同,如果想看一下它的实际内容,你可以用命令行的形式来查看。

注意:不能简单的把你的assembly copy到这个目录下。首先你需要给你的assembly一个strong name,然后可以用AL把这个assembly安装到全局assembly缓存中去。

Strong Name

如果想把assembly设为共享,为了和其他共享的assembly区分开来,每一个assembly都需要一个唯一标志,这个标志指的就是Strong Name。 Strong Name是通过公钥加密技术生成的。一个有私钥的assembly可以生成和令一个带有不同私钥的assembly完全不同的strong name。.NET SDK使用一个叫SN.EXE(Shared Name)的工具来产生这样的公钥/私钥对。

Versioning

向前面看到的那样,多数时候,.NET assembly被用作私有模式。对这样的assembly,因为它们都位于应用程序自己的目录下,所以versioning看起来并不是十分重要。然而对共享式assembly来说,versioning是很重要的。共享式assembly可以以并行的形式使用(前面提到过并行使用的概念),因此完全有可能在同一台机器上存在同一个assembly的不同版本。当应用程序要使用一个assembly时候,它就应该提供最近的或以前的版本的信息。如果开发者需要使用不同版本,就需要在代码中明确的设置版本号,其格式如下:

<major version>.<minor version>.<build number>.<revision>

Runtime将通过前两个参数来决定当前使用的版本是否和以前的版本兼容(major version和minor version)。如果这两个参数有变化,那么assembly将被认为是不兼容的,全局assembly缓存会为该assembly生成一个单独的入口,如果在代码中指定了版本号的话,major version就是可选的了。

下面显示了.NET如何将同一个assembly(EmployeeShared)的不同版本视为不同的assembly的例子。

创建一个共享式的assembly

现在你应该已经知道什么是共享式assembly了,下面我们将创建一个叫EmployeeShared的assembly。创建一个共享式assembly包括以下几个步骤:

  • 创建assembly代码并在代码中指定其版本号。
  • 用SN工具创建一个公钥/私钥对。
  • 编译assembly并用上一步中创建的公钥/私钥对签名。
  • 在全局assembly缓存中安装该assembly。

Step1: 创建assembly代码并在代码中指定其版本号

在EmploeeShared.cs文件中输入以下代码:

using System;
using System.Runtime.CompilerServices;
[assembly:AssemblyVersionAttribute(\”1.1\”)]
public class EmployeeShared
{
string m_name;

public string Name
{
get
{
return m_name;
}

set
{
m_name=value;
}
}

public int GetSalary()
{
//put your logic instead of hard coded value
return 10000;
}
}

我们创建了这个类,包含一个属性Name和一个方法GetSalary。注意AssemblyVersionAttribute的用法,它为该assembly设置了版本信息。

Step2: 用SN工具创建一个公钥/私钥对

为了给你的assembly赋一个Strong Name,你需要一个公钥/私钥对。可以使用.NET SDK提供的工具SN (Shared Name),输入以下命令行:

Sn -k employshared.snk

该命令在指定文件里创建了一个钥匙对,参数-k表示我们要把钥匙对写到输出文件里。

扩展名.SNK只是个习惯,你可以让它叫任何名字。

Step 3: 编译assembly并用上一步中创建的公钥/私钥对签名

现在我们可以用钥匙对为我们的assembly签名了,这可以通过在编译时加上/a.keyfile开关来实现:

csc /t:library employeeshared.cs /a.keyfile:employeeshared.snk

注意如果你在使用VS.NET,你可以更简单的在AssemblyInfo文件中指明key文件。如下所示:

[assembly:AssemblyKeyFile(\”employeeshared.snk\”)]

你也可以在这个文件里加上我们前面提过的般本号属性而不用在源代码里指定。

Step4: 在全局assembly缓存中安装该assembly

我们的assembly已经用过私钥签名了,下面可以把它放在全局assembly缓存中了。像前面一样使用AL,命令行如下:

al /I:employeeshared.dll

开关/I表示我们要将assembly安装到全局assembly缓存中。

好了,现在assembly被安装在全局assembly缓存并且可以使用了。想验证一下的话到explorer中看一下Assembly目录。

注意:在Beta2中,安装assembly到全局assembly缓存中可以使用一个叫GACUTIL的工具。可以使用ILDASM.exe查看assembly信息。

有时候你可能需要分析assembly,尤其是别人开发的assembly。这种情况下你可以使用一个叫ILDASM (Intermediate Language Disassembler)的工具。这个工具就象我们从前用过的OLE View或者VB的object viewer一样,你可以把你的assembly或者module导入这个工具来查看assembly各方面的特性,比如它包含的member, method和manifest。看起来就像下面这样。

你可以通过双击树结构中的节点得到更多的信息。

总结

assembly是.NET的组成模块。.NET应用程序由一个或者多个assembly组成。一个.NET assembly由一个或多个文件组成并且在其自身的manifest中保存自己的注册信息。通常一个assembly只为一个应用程序服务,这样的assembly叫做私有式assembly。你也可以通过配置让一个assembly的copy为多个应用程序服务。这样的assembly叫做共享式assembly。共享式assembly在全局assembly缓存中被管理。共享式assembly必须有一个在整个机器范围内唯一标识的strong name。

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

申明:本文由第三方发布,内容仅代表作者观点,与本网站无关。对本文以及其中全部或者部分内容的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。本网发布或转载文章出于传递更多信息之目的,并不意味着赞同其观点或证实其描述,也不代表本网对其真实性负责。

左子网 编程相关 C#Assembly详解 https://www.zuozi.net/36527.html

常见问题
  • 1、自动:拍下后,点击(下载)链接即可下载;2、手动:拍下后,联系卖家发放即可或者联系官方找开发者发货。
查看详情
  • 1、源码默认交易周期:手动发货商品为1-3天,并且用户付款金额将会进入平台担保直到交易完成或者3-7天即可发放,如遇纠纷无限期延长收款金额直至纠纷解决或者退款!;
查看详情
  • 1、描述:源码描述(含标题)与实际源码不一致的(例:货不对板); 2、演示:有演示站时,与实际源码小于95%一致的(但描述中有”不保证完全一样、有变化的可能性”类似显著声明的除外); 3、发货:不发货可无理由退款; 4、安装:免费提供安装服务的源码但卖家不履行的; 5、收费:价格虚标,额外收取其他费用的(但描述中有显著声明或双方交易前有商定的除外); 6、其他:如质量方面的硬性常规问题BUG等。 注:经核实符合上述任一,均支持退款,但卖家予以积极解决问题则除外。
查看详情
  • 1、左子会对双方交易的过程及交易商品的快照进行永久存档,以确保交易的真实、有效、安全! 2、左子无法对如“永久包更新”、“永久技术支持”等类似交易之后的商家承诺做担保,请买家自行鉴别; 3、在源码同时有网站演示与图片演示,且站演与图演不一致时,默认按图演作为纠纷评判依据(特别声明或有商定除外); 4、在没有”无任何正当退款依据”的前提下,商品写有”一旦售出,概不支持退款”等类似的声明,视为无效声明; 5、在未拍下前,双方在QQ上所商定的交易内容,亦可成为纠纷评判依据(商定与描述冲突时,商定为准); 6、因聊天记录可作为纠纷评判依据,故双方联系时,只与对方在左子上所留的QQ、手机号沟通,以防对方不承认自我承诺。 7、虽然交易产生纠纷的几率很小,但一定要保留如聊天记录、手机短信等这样的重要信息,以防产生纠纷时便于左子介入快速处理。
查看详情

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务