Android官方技术文档翻译——清单合并 【翻译】

翻译工作耗时费神,如果你觉得本文翻译得还OK,请点击文末的“顶”;如有错讹,敬请指正。谢谢。

这个新的合并工具是gradle android 插件的 0.10 版中引入的。截至 0.11 版本,该 gradle 插件默认情况下都是使用此合并工具。

如果想恢复使用旧的清单合并工具,可以在你的 build.gradle 中添加以下配置:

android {
    useOldManifestMerger true
}

Manifest 文件排序

一般情况下,有三种类型的清单文件需要合并成一个最终的应用程序清单,这里按照优先级顺序列出:

Product flavors 和构建类型所指定的清单文件。
应用程序的主清单文件。
类库的清单文件。

第一种类型的清单文件通常会重写清单的内容,因为它专门提供用于特定的交付的应用程序。然后,第三种类型的清单文件通常会被合并入上一步产生的主清单中。合并的规则取决于每个节点类型,可以使用“tools:”命名空间属性来更改。

由于多个product flavors和构建类型,这些清单文件合并的结果的组合可能会是一个矩阵。然而,对于每一个组装的步骤,每一个flavor group和构建类型的值都只能选择一个,导致出现了潜伏的覆盖主清单文件的清单文件的排序列表。

例如,下面的 FlavorGroups: abi,density,API,Prod/Internal 导致形成了以下可能的flavors的矩阵:

ABIDensityAPIProd/Internal
x86mdpi9prod
armhigh14internal
mipsxhigh15
xxhigh

这样就形成了3x4x3x2 种可能的组合。然而,对于每一次执行组装,只能是一个group里的falvor,定义在原始的 build.gradle 的flavor group 的属性形成了一个可能要合并的清单 文件的列表,并且这个列表由高优先级到低优先级排序。

例如,构建 x86-high-15-prod 的 variant ,会查找以下的清单文件以进行合并

x86/AndroidManifest.xml
high/AndroidManifest.xml
15/AndroidManifest.xml
internal/AndroidManifest.xml

在这个有序列表中,每个文件根据其在列表中的顺序都有一个优先级,合并工具将使用这个优先级来决定哪个XML元素或属性将覆盖一个较低优先级的设置。

因此,合并工具的输入将如下:

  • 由flavor或编译类型的清单文件按优先级组成的有序列表,这些会被经常引用作为 flavors的清单。
  • 主清单文件
  • 由一些类库声明(或依赖传递而有的)的清单文件组成的有序列表。
  • 用于占位符和XML产生的注入值

Android 清单文件合并
在一个清单文件中的每个元素都可以根据它的元素类型(比如activity,intent-filter)和一个可选的键值(key value)来识别。一些元素,如“activity”,必须有一个key,因为在一个AndroidManifest.xml中可能存在多个这样的元素。其他的元素,像“application”,就不要求一定要有一个key,因为只能有一个这样的元素。

元素的类型和键值对代表了一个清单元素的身份。

合并的activity的始终是两个相同类型的元素之间,一个来自更高优先级的清单文件,一个来自较低优先级的清单文件。每一个合并的activity都有一个默认的行为,这一点将在随后进行描述。此外,在节点或一个指定的属性上的每个默认合并的activity都可能会被including工具的指定标记所覆盖。

合并过程中还会把对每一个节点的合并结果记录下来,这将在“日志”章节描述。

元素和属性的合并过程


隐式声明
一些属性都会有默认值(默认定义在在线文档中)。当一个较高优先级的元素没有定义默认值为X的属性时,如果一个较低优先级的元素也恰好定义了一个值同样是X的属性,那么这个属性仍然会被添加到合并的元素中 (当然它的值是X), 因为它表示了类库对这个属性的值的一个明确的选择,从而不去考虑把默认值作为它的值,而是为某特性设置一个正确的值(以防默认值发生改变)。

大多数有默认值的属性,如果在低优先级的属性中已经定义了值,那么默认值将被manifest合并工具忽略;定义的值会被合并,因为在默认值 和一个设置的值之间,并没有冲突。在生成的合并的元素中,这个属性会被设置为设定的值。

然而,下面列出的几种情况例外:

<uses-feature android:required>默认值为 true。在与其他属性合并时,将使用“或”的合并策略。这时因为如果任何一个库需要该特性,那么生成的应用程序也将需要此特性。
<uses-library android:required>同 uses-feature:required。
<uses-sdk android:minSdkVersion>

默认值为 1。

将使用更高优先级文件的版本,但导入一个较新版本的库时将会产生错误。 

<uses-sdk android:maxSdkVersion>同 uses-sdk:minSdkVersion
<uses-sdk android:targetSdkVersion>同 uses-sdk:minSdkVersion

自动升级
当导入一个target SDK 版本比项目低的库时,它可能需要显式声明地授予权限 (可能还需要进行其他更改),以使得类库在以后运行时能正常运行。这将由清单合并工具自动进行。

占位符支持
当属性值包含一个占位符 (见下面的格式)时,合并工具将把此占位符的值换成一个注入的值。注入的值是在build.gradle里面定义的。

占位符值的语法是 ${name},因为@符号已经预留给了链接。在最后的文件合并发生之后,并且生成合并后的 android 的清单文件输出之前,带有占位符的所有值将都会被替换为注入的值。如果变量名是未知的,将导致构建失败。

占位符字符串可以有一个前缀或后缀,以实现只替换部分的内容。

示例:

android:authority="${applicationId}.foo"

android:authority=”com.acme.${localApplicationId}”

android:authority=”com.acme.${localApplicationId}.foo”

隐式占位符 ${applicationId} 的值将由现有的build.gradle的 applicationId值自动提供。

示例:

<activity

    android:name=".Main">

    <intent-filter>
        <action android:name="${applicationId}.foo">
        </action>
    </intent-filter>

</activity>

通过以下的gradle的声明:

android {

   compileSdkVersion 19
   buildToolsVersion "19.0.2"

   productFlavors {
       flavor1 {
           applicationId = "com.android.tests.flavorlib.app.flavor1"
       }
   }

    ...
}

一旦合并,<action android:name> 将会是

<action android:name=“com.android.tests.flavorlib.app.flavor1.foo”>

对于自定义的占位符替换,可以使用以下的 DSL 来配置占位符的值:

android { 
    defaultConfig { 
        manifestPlaceholders = [ activityLabel:"defaultName"] 
} 
productFlavors { 
    free { } 
    pro { 
        manifestPlaceholders = [ activityLabel:"proName" ] 
    } 
}

它将替换下面声明中的占位符:

<activity android:name=".MainActivity"android:label="${activityLabel}" >

合并策略的常见描述
XML的合并可能是在节点级别上的合并,也可能是在属性级别上的合并。

在节点级别上,默认的合并策略是,只要没有冲突就合并属性和子元素。当两个相同标识的元素具有相同的属性,并且属性的值不同时,就会出现冲突。

举个例子:

<activity

 android:name="com.foo.bar.ActivityOne"

android:theme=”@theme1”/>

与下面的声明进行合并时不会产生冲突:

<activity

 android:name="com.foo.bar.ActivityOne"

android:screenOrientation=”landscape/>

同样

<activity

 android:name="com.foo.bar.ActivityOne"

android:theme=”@theme1”/>

与下面的声明合并时也不会产生冲突,因为在这两个元素中都定义的"theme"属性具有相同的值。

<activity

 android:name="com.foo.bar.ActivityOne"

android:theme="@theme1"

android:screenOrientation=”landscape/>

但是,

<activity

 android:name="com.foo.bar.ActivityOne"

android:theme=”@theme1”/>

与下面的声明合并时就会产生冲突,因为在这两个元素中定义的"theme"属性的值并不相同。

<activity

 android:name="com.foo.bar.ActivityOne"

android:theme=”@theme2”

android:screenOrientation=”landscape/>

现在,每个元素都可以有子元素和规则,用于匹配那些将遵循同样的基本原则的属性,它们具有相同标识的子元素将匹配在一起。如果一个子元素仅存在于其中一个父元素,并不会冲突。

标记
标记是在工具(tools)命名空间中的一个特别的属性,用来描述对如何解决冲突所采取的决定。

所有标记都属于 Android 工具命名空间,因此您必须包含至少一个标记的任何 AndroidManifest.xml 中声明该命名空间:

xmlns:tools="​ ​http://schemas.android.com/tools​​"

示例:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"

   package="com.android.tests.flavorlib.app"

   xmlns:tools="http://schemas.android.com/tools">

   <application

       android:icon="@drawable/icon"

       android:label="@string/app_name"

       tools:replace=”icon, label”/>

</manifest>

当要合并的元素之间存在冲突时,必须显式添加一些标记来指导清单合并工具(Manifest Marger)。在节点级别,应使用 tools:node 属性,在属性级别,应使用 tools:attr 属性。

tools:node 标记
当一个节点存在冲突并且需要解决时,就应该要有一个 tools:node="maker_value" 的属性存在。

<tools:node> 属性值清单合并工具的行为
<tools:node="merge">这是节点合并的隐式的默认模式,节点只要不冲突就会被合并。
<tools:node="replace">用注解的那一个替换低优先级的声明。
<tools:node="strict">当另一个具有相同标识的节点存在并且不是严格相等时,将导致构建失败。
<tools:node="merge-only-attributes">只合并较低优先级的声明中的属性。
<tools:node="remove">从生成的 XML 中删除所注解的元素。不论是否可能冲突,低优先级的声明都不会被合并进去。
<tools:node="removeAll">移除所有相同节点类型(不是关键必须的)的元素 。

tools:attr 标记
在任何特别的元素中,可能会有许多与标记相关的属性,用于解决所有属性的冲突问题。

<tools:strict=”x, y, z”>属性默认的隐式模式,会在当尝试合并有不同值的低优先级属性声明时产生错误。
<tools:remove=”x, y, z”>当合并时,从任何较低优先级的声明中删除 x、 y、 z 属性。
<tools:replace=”x, y, z”>把任何低优先级声明的x,y,z属性的值替换为所提供的值(必须是在同一节点上)。

选择器
每一个 tools:node 或 tools:attr 声明都可以通过一个 tools:selector 的属性进行扩展,这个属性是合并策略是否应该被应用到当前的低优先级的 XML 的描述的上下文信息。例如,当仅在一个特定的库,而不是任何的库,才需要删除一个权限时,它会非常有用:   

<permission
         android:name="permissionOne"
         tools:node="remove"
         tools:selector="com.example.lib1">

tools:overrideLibrary 标记
这是一个特殊的标记,仅与use-sdk的声明一起使用,用于在导入的库的最小SDK版本比应用程序的最小SDK版本还要新时,对这个库是否导入进行重写。

如果没有这样的标志,清单合并就会失败。这个标记将允许用户忽略最低的SDK版本而选择哪些库可以被导入。 

例如,在main android 清单中: 

<uses-sdk android:targetSdkVersion="14" android:minSdkVersion="2"

tools:overrideLibrary="com.example.lib1, com.example.lib2"/>

将允许具有以下清单的库被导入而不出错: 

<manifest 
    xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.lib1">        
    <uses-sdk android:minSdkVersion="4" />    
</manifest>

日志
在清单合并期间的每个操作或决策都需要是

  • 被记录的
  • 枨式化以让计算机能够解析的
  • 按节点排序的 (因为一个节点的多个属性可能会产生好几种合并决定)

Blame
通过元素或属性所在的一些指示,可以获取一个“blame”类型的输出,以限制合并的XML中产生的每一个元素和属性。

合并策略
每个元素类型都有一特定的默认合并策略附属于它。例如,大部分的元素类型,像activity或application都有一个默认合并策略,这个策略是所有属性和子元素都会被合并(假设没有冲突)到所生产的元素当中。不过,其他元素,如顶级的manifest,默认合并策略是只合并子元素。这意味着较低优先级的 AndroidManifest.xml 的manifest 元素的属性都没有资格能够合并进去。

每个元素也可以拥有或不拥有一个与它关联的键。例如,application没有键,在每一个 AndroidManifest.xml 中只能有一个 <application>元素。 大部分带键的元素都使用“ android:name 属性”来表示它们的键值,或其他内容等

合并
没有冲突的属性会被合并,子元素也会依据它们各自的合并策略进行合并。

只合并子元素
属性不会被合并,只有子元素会根据它们各自的合并政策进行合并。

总是合并
始终保持元素的“原样”,并添加到生成的合并文件里的共同的父元素中。

元素合并策略和键的列表

节点类型合并策略
action合并android:name 属性
activity合并android:name 属性
application合并没有键
category合并android:name 属性
data合并没有键
grant-uri-permission合并没有键
instrumentation合并android:name 属性
intent-filter总是合并子元素的action和categories android: name attribute。允许相同的键有多个声明。
manifest只合并子元素没有键
meta-data合并android:name 属性
path-permission合并没有键
permission-group合并android:name 属性
permission合并android:name 属性
permission-tree合并android:name 属性
provider合并android:name 属性
receiver合并android:name 属性
screen合并属性 screenSize
service合并android:name 属性
supports-gl-texture合并android:name 属性
supports-screen合并没有键
uses-configuration合并没有键
uses-feature合并首先是属性名称,如果不存在,则接着是 glEsVersion 属性
uses-library合并android:name 属性
uses-permission合并android:name 属性
uses-sdk合并没有键
自定义元素合并没有键

包名称智能替换
有些属性是包依赖属性,意思就是说这些属性支持通过由清单节点中的package属性提供的包设置,对部分完全限定的类名称的智能替换。

下面描述的每个属性都可以有一个局部的类名称,这个类名称是以一个点或不包含任何点开头的。

示例:

<manifest

   xmlns:android="http://schemas.android.com/apk/res/android"

   package="com.example.app1">

   <application>

       <activity android:name=".Main" />

   </application>

</manifest>

将被扩充成:   

<application>

    <activity android:name="com.example.app1.Main" />

</application>

这是独立于build.gradle里的任何包设置的(译者注,关于package和applicationId可以看一下我的系列博客中的另一篇文章的介绍)。例如,你build.gradle 包含以下内容:

android {

   compileSdkVersion 19

   buildToolsVersion "19.0.2"

   productFlavors {

       flavor1 {

           applicationId = "com.android.tests.flavorlib.app.flavor1"

       }

}

扩充的结果仍然是

<activity android:name=”com.example.app1.Main”>

如果你需要让注入的值作为扩充的属性值,可以使用 ${applicationId} 占位符,例如:

<manifest

   xmlns:android="http://schemas.android.com/apk/res/android"

   package="com.example.app1">

   <application>

       <activity android:name="${applicationId}.Main" />

   </application>

</manifest>

下面是可以使用这种智能替换的能力包独立属性的列表:

节点类型属性的本地名称
activityname, parentActivityName
activity-aliasname, targetActivity
applicationname, backupAgent
instrumentationname
providername
receivername
servicename

属性标记示例


重写来自库的属性
使用 tools:replace="x, y, z" 将会重写从外部库的activity 的XML声明中导入的 x,y,z 属性。

更高级别的声明

<activity

 android:name="com.foo.bar.ActivityOne"

android:screenOrientation="portrait"

android:theme="@theme1"

tools:replace=”theme”/>

和一个较低优先级的声明:

<activity

 android:name="com.foo.bar.ActivityOne"

android:theme="@olddogtheme"

android:windowSoftInputMode="stateUnchanged"

android:exported="true" >

将产生:

<activity

 android:name="com.foo.bar.ActivityOne"

android:screenOrientation="portrait"

android:theme="@theme1"

android:windowSoftInputMode="stateUnchanged"

android:exported="true"/>

删除来自库的属性。
使用 tools:remove="x, y, z" 将会在产生的XML中删除 x,y,z 属性的声明。

更高优先级的声明

<activity

 android:name="com.foo.bar.ActivityOne"

android:hardwareAccelerated="true"

tools:remove="android:theme,android:screenOrientation" />

和一个较低优先级的声明:

<activity

 android:name="com.foo.bar.ActivityOne"

android:screenOrientation="landscape"

android:theme="@olddogtheme"

android:windowSoftInputMode="stateUnchanged"

android:exported="true"/>

将产生:

<activity

 android:name="com.foo.bar.ActivityOne"

android:hardwareAccelerated="true"

android:windowSoftInputMode="stateUnchanged"

android:exported="true"/>

强制更新属性值
毫无疑问,所有声明属性几乎都带有“strict”的合并策略,所以如果两个要合并的元素都有一个同样名称的属性但值却不同,就是一个需要明确解决的冲突。

所以,一个较高优先级的声明

<activity

 android:name="com.foo.bar.ActivityOne"

android:theme=”@newdogtheme”/>

和一个较低优先级的声明:

<activity

 android:name="com.foo.bar.ActivityOne"

android:theme=”@olddogtheme”/>

与一个较高优先级的声明

<activity
    android:name="com.foo.bar.ActivityOne"
    android:theme=”@newdogtheme”
    tools:strict=”theme”/>

和一个较低优先级的声明:

<activity

    android:name="com.foo.bar.ActivityOne"

    android:theme=”@olddogtheme”/>

是完全等价的,并且都将不能正确地合并,除非添加一个 tools:replace="theme" 的属性。

混合操作
如果用户想要删除某些属性且重写其他属性同时保存另一组的原始属性,只需依次添加所有标记。

例如:

<activity

 android:name="com.foo.bar.ActivityOne"

 android:windowSoftInputMode="stateUnchanged"

 android:theme="@theme1"

 tools:remove="android:exported, android:screenOrientation"

 tools:replace="android:theme"/>

和一个较低优先级的声明:

<activity

 android:name="com.foo.bar.ActivityOne"

 android:screenOrientation="landscape"

 android:theme="@olddogtheme"

 android:exported="true"/>

将产生:

<activity

 android:name="com.foo.bar.ActivityOne"

 android:theme="@theme1"

 android:windowSoftInputMode="stateUnchanged" />

需要注意的是,如果低优先级的声明中包含 android:windowSoftInputMode或者未明确标记为删除或替换的任何属性,将生成一个构建错误。

元素标记示例


移除元素
如果要删除任何一个库的某个元素,需要在更高优先级的文件中声明

<activity-alias

android:name="foo.bar.alias">

<meta-data

android:name="zoo"

tools:node="remove"/>

</activity-alias>

与下面进行合并

<activity-alias

android:name="foo.bar.alias">

<meta-data

android:name="zoo"

android:value="@string/bear"/>

</activity-alias>

将产生:

<activity-alias

android:name="foo.bar.alias">

</activity-alias>

移除所有元素
如果要作任何一个库的一个特定类型的所有元素,需要在更高优先级的文件中声明

<activity-alias

android:name="foo.bar.alias">

<meta-data  

tools:node="removeAll" />

</activity-alias>

与下面进行合并

<activity-alias

android:name="foo.bar.alias">

<meta-data

android:name="zoo"

android:value="@string/bear"/>

<meta-data

android:name="cage"

android:value="@string/iron"/>

</activity-alias>

将产生:

<activity-alias

android:name="foo.bar.alias"

</activity-alias>

元素替换

<activity-alias

android:name="foo.bar.alias"

tools:node="replace">

<meta-data

android:name="zoo"/>

</activity-alias>

与下面进行合并

<activity-alias

android:name="foo.bar.alias">

<meta-data

android:name="cage"

android:value="@string/iron"/>

</activity-alias>

将产生:

<activity-alias

android:name="foo.bar.alias">

<meta-data

android:name="zoo"

tools:node="remove"/>

</activity-alias>

选择器示例
使用包名称来选择库这里,我们有三个库要合并进一个主清单文件中。主清单

<manifest

   xmlns:android="http://schemas.android.com/apk/res/android"

   xmlns:tools="http://schemas.android.com/tools"

   package="com.example.main">

   <permission

         android:name="permissionOne"

         tools:node="remove"

         tools:selector="com.example.lib1">

   </permission>

   <permission

         tools:node="removeAll"

         tools:selector="com.example.lib3">

   </permission>

   <permission

            android:name="permissionThree"

            android:protectionLevel="signature"

            tools:node="replace">

   </permission>

</manifest>

与库1 

<manifest

   xmlns:android="http://schemas.android.com/apk/res/android"

   package="com.example.lib1">

   <permission android:name="permissionOne"

            android:protectionLevel="signature">

   </permission>

   <permission android:name="permissionTwo"

            android:protectionLevel="signature">

   </permission>

</manifest>

和库2

<manifest

   xmlns:android="http://schemas.android.com/apk/res/android"

   package="com.example.lib2">

   <permission android:name="permissionThree"

            android:protectionLevel="normal">

   </permission>

   <permission android:name="permissionFour"

            android:protectionLevel="normal">

   </permission>

</manifest>

及库3 

<manifest

   xmlns:android="http://schemas.android.com/apk/res/android"

   package="com.example.lib2">

   <permission android:name="permissionFive"

            android:protectionLevel="normal">

   </permission>

</manifest>

将产生:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"

   package="com.example.main" >

   <permission

       android:name="permissionThree"

       android:protectionLevel="signature" >

   </permission>

   <permission

       android:name="permissionTwo"

       android:protectionLevel="signature" >

   </permission>

   <permission

       android:name="permissionFour"

       android:protectionLevel="normal" >

   </permission>

</manifest>

日志记录不会被组织为最终产生输出文件的事件和决策的线性集合。相反,为了方便开发人员,日志文件将按输入文件中发生冲突的顶级XML节点来进行组织 (无论当我们想想文件的节点删除的时候,它是否存在 于输出文件中)。

日志记录:
一个日志记录既是一个节点记录 (描述在该特定节点上采取的所有操作),也是一个包含错误消息和警告的消息记录。

日志文件 = 日志记录*

日志记录 = 节点记录 | 消息

消息

消息=文件:行号:列号 严重性:\n描述

描述=(\t内容\n)*

名称
file生成日志条目的输入文件
line-number生成日志条目的输入的文件行号
column-number生成日志条目的输入文件列号
严重性Error, Warning, Info
description日志条目有效载荷(译者注:有效载荷即记载着信息的那部分数据)

示例

/Users/jedo/src/app/src/main/AndroidManifest.xml:3:9 Error:

Attribute activity@screenOrientation value=(portrait) from AndroidManifest.xml:3:9

is also present at flavorlib:lib1:unspecified:3:18 value=(landscape)

Suggestion: add 'tools:replace="icon"' to <activity> element at AndroidManifest.xml:1:5 to override

节点记录

名称
node-type    
XML 节点类型
node-key节点的键的属性值
record-type[Action |Log ] *

node-type#node-key\n

\t(node_action:Action)*

\t\t(attribute_action:Action)*

操作格式

名称
action-typeadded | rejected | implied
targetnode | attribute
target-name节点的键名称或属性名称
origin原始值的位置

示例:

application

ADDED from AndroidManifest.xml:10:5

MERGED from flavorlib:lib2:unspecified:3:5

android:label

ADDED from AndroidManifest.xml:12:9

REJECTED from flavorlib:lib2:unspecified:3:55

android:icon

ADDED from AndroidManifest.xml:11:9

REJECTED from flavorlib:lib2:unspecified:3:18


receiver#com.example.WidgetReceiver

ADDED from ManifestMerger2Test0_main.xml:60:9

android:labelADDED from ManifestMerger2Test0_main.xml:61:13

android:iconADDED from ManifestMerger2Test0_main.xml:62:13

android:nameADDED from ManifestMerger2Test0_main.xml:63:13

构建错误
当发生构建错误时,则应显示特定节点失败的日志,并在后面有一段向用户描述的错误消息。举个例子:

更高优先级的声明

<activity

 android:name="com.foo.bar.ActivityOne"

 android:screenOrientation="portrait"

 android:theme=”@theme1”/>

和一个较低优先级的声明:

<activity

 android:name="com.foo.bar.ActivityOne"

 android:screenOrientation=”landscape/>

会同时产生日志文件和输出结果(能让人类,计算机以及IDE都能识别的确切格式还未确定)。

/Users/jedo/src/app/src/main/AndroidManifest.xml:3:9 Error:

Attribute activity@screenOrientation value=(portrait) from AndroidManifest.xml:3:9

is also present at flavorlib:lib1:unspecified:3:18 value=(landscape)

Suggestion: add 'tools:replace="icon"' to <activity> element at AndroidManifest.xml:1:5 to override

参考资料:

Android官方技术文档翻译——清单合并

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值