热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

转载Gradle基础

原文地址作为Android开发你必须明白的Gradle基础作为一个Android开发程序员,如果你的build.gradle都只能靠IDE生成或者从别的项目中复制粘贴来完成,那么你

原文地址

作为Android开发你必须明白的Gradle基础

作为一个Android开发程序员,如果你的build.gradle都只能靠IDE生成或者从别的项目中复制粘贴来完成,那么你该好好的看完这篇文章,掌握一下你不知道的Gradle基础。

文中的图片均来自于网络,侵删

Gradle是一个基于JVM的构建工具,目前Android Studio中建立的工程都是基于gradle进行构建的。Gradle的与其他构建工具(ant、maven)的特性主要包括:

  • 强大的DSL和丰富的gradle的API
  • gradle就是groovy
  • 强大的依赖管理
  • 可拓展性
  • 与其他构建工具的集成

三种构建脚本

Gradle的脚本都是配置型脚本。每一种脚本类型实际上都是某个具体的gradle的API中的类对象的委托,脚本执行对应的其实是其委托的对象的配置。在一个完整的gradle的构建体系中,总共有三种类型的构建脚本,同时也分别对应着三种委托对象

脚本类型委托对象
Init scriptGradle
Settings scriptSettings
Build scriptProject

init.gradle

对应的就是上面的Init script,实际上就是Gradle对象的委托,所以在这个init 脚本中调用的任何属性引用以及方法,都会委托给这个 Gradle 实例。

Init script的执行发生在构建开始之前,也是整个构建最早的一步。

配置Init scrip的依赖

每个脚本的执行都可以配置当前脚本本身执行所需要的依赖项。Init scrip的配置如下:

// initscript配置块包含的内容就是指当前脚本本身的执行所需要的配置
// 我们可以在其中配置比如依赖路径等等
initscript {
repositories {
mavenCentral()
}
dependencies {
classpath group: 'org.apache.commons', name: 'commons-math', version: '2.0'
}
}

使用Init scrip

要使用一个定义好的Init scrip,主要有以下几个方式

  • 在执行gradle命令的时候,通过-I--init-script命令选项指定脚本的路径

    这种方式可以针对具体的一次构建。

  • 把一个init.gradle文件放到 *USER_HOME*/.gradle/ 目录

  • 把一个文件名以.gradle结尾的文件放到Gradle 分发包*GRADLE_HOME*/init.d/ 目录内

    以上的两种方式是全局的,对机器内的构建都会起作用

settings.gradle

对应的是Settings script脚本类型,是Settings对象的委托。在 脚本中调用的任何属性引用以及方法,都会委托给这个 Settings 实例。

Settings script的执行发生在gradle的构建生命周期中的初始化阶段。Settings脚本文件中声明了构建所需要的配置,并用以实例化项目的层次结构。在执行settings脚本并初始化Settings对象实例的时候,会自动的构建一个根项目对象rootProject并参与到整个构建当中。(rootProject默认的名称就是其文件夹的名称,其路径就是包含setting脚本文件的路径)。

下面是一张关于Settings对象的类图:

《转载 Gradle基础》 image.png

每一个通过include方法被添加进构建过程的project对象,都会在settings脚本中创造一个ProjectDescriptor的对象实例。

因此,在settings的脚本文件中,我们可以访问使用的对象包括:

  • Settings对象
  • Gradle对象
  • ProjectDescriptor对象

获取settings文件

在gradle中,只要根项目/任何子项目的目录中包含有构件文件,那么就可以在相应的位置运行构建。而判断一个构建是否是多项目的构建,则是通过寻找settings脚本文件,因为它指示了子项目是否包含在多项目的构建中。

查找settings文件的步骤如下:

  1. 在与当前目录同层次的master目录中搜索setting文件
  2. 如果在1中没有找到settings文件,则从当前目录开始在父目录中查找settings文件。

当找到settings文件并且文件定义中包含了当前目录,则当前目录就会被认为是多项目的构建中的一部分。

build.gradle

对应的就是前面提到的Build script脚本类型,是gradle中Project对象的委托。在脚本中调用的任何属性引用以及方法,都会委托给这个 Project 实例。

配置脚本依赖

在build.gradle文件中有一个配置块buildScipt{}是用于配置当前脚本执行所需的路径配置等的(与initScript形似)。

buildscript {
// 这里的repositories配置块要与Project实例当中的repositories区分开来
// 这里的repositories配置是指脚本本身依赖的仓库源,其委托的对象实际上是ScriptHandler
repositories {
mavenLocal()
google()
jcenter()
}
// 与前面的repositories配置块相同,也要与Project当中的dependencies配置块区分开来
dependencies {
classpath 'com.android.tools.build:gradle:3.1.2'
}
}

这里补充关键的一点,在build.gradle文件中,不管buildScript{}配置块被放在哪个位置,它总是整个脚本文件中最先被执行的

三个构建块

每个gradle构建都包含三个基本的构件块:

  • project
  • task
  • property

每个构建包含至少一个project,进而又包含一个或者多个task。project和task暴露的属性(property)可以用来控制构建。

Project

我们对project的理解更多来源于项目目录中的build.gradle文件(因为它其实就是project对象的委托)。Project对象的类图如下所示:

《转载 Gradle基础》 image.png

项目配置

在build.gradle脚本文件中,我们不仅可以对单独project进行配置,也可以定义project块的共有逻辑等,参考下面的定义。

《转载 Gradle基础》 image.png

常见的例子比如:

// 为所有项目添加仓库源配置
allprojects {
repositories {
jcenter()
google()
}
}
// 为所有子项目添加mavenPublish的配置块
subprojects {
mavenPublish {
groupId = maven.config.groupId
releaseRepo = maven.config.releaseRepo
snapshotRepo = maven.config.snapshotRepo
}
}

Task

任务是gradle构建的基础配置块之一,gradle的构建的执行就是task的执行。下面是task的类图。

《转载 Gradle基础》 image.png

task的配置和动作

当我们定一个一个task的时候,会包含配置和动作两部分的内容。比如下面的代码示例:

task test{
println("这是配置")
doFirst{
// do something here
}
doLast(){
// do something here
}
}

目前task的动作(action)声明主要包含两个方法:

  • doFirst
  • doLast

这些动作是在gradle的构建生命周期中的执行阶段被调用。值得注意的是,一个task可以声明多个doFirstdoLast动作。也可以为一些已有的插件中定义的task添加动作。比如:

// 为test任务添加一个doLast的动作
test.doLast{
// do something here
}

在task的定义之中,除了动作块以外的是配置块,我们可以声明变量、访问属性、调用方法等等。这些配置块的内容发生在gradle的构建生命周期中的配置阶段。因此task中的配置每次都会被执行。(动作块只有在实际发生task的调用的时候才会执行)。

task的依赖

gradle中任务的执行顺序是不确定的。通过task之间的依赖关系,gradle能够确保所依赖的task会被当前的task先执行。使用task的dependsOn()方法,允许我们为task声明一个或者多个task依赖。

task first{
doLast{
println("first")
}
}
task second{
doLast{
println("second")
}
}
task third{
doLast{
println("third")
}
}
task test(dependsOn:[second,first]){
doLast{
println("first")
}
}
third.dependsOn(test)

task的类型

默认情况下,我们常见的task都是org.gradle.api.DefaultTask类型。但是在gradle当中有相当丰富的task类型我们可以直接使用。要更改task的类型,我们可以参考下面的示例

task createDistribution(type:Zip){
}

更多关于task的类型,可以参考gradle的官方文档

Property

属性是贯穿在gradle构建始终的,用于帮助控制构建逻辑的存在。gradle中声明属性主要有以下两种方式:

  • 使用ext命名空间定义拓展属性
  • 使用gradle属性文件gradle.properties定义属性

ext命名空间

Gradle中很多模型类都提供了特别的属性支持,比如Project.在gradle内部,这些属性会以键值对的形式存储。使用ext命名空间,我们可以方便的添加属性。下面的方式都是支持的:

//在project中添加一个名为groupId的属性
project.ext.groupId="tech.easily"
// 使用ext块添加属性
ext{
artifactId='EasyDependency'
cOnfig=[
key:'value'
]
}

值得注意的是,只有在声明属性的时候我们需要使用ext命名空间,在使用属性的时候,ext命名空间是可以省略的。

属性文件

正如我们经常在Android项目中看到的,我们可以在项目的根目录下新建一个gradle.properties文件,并在文件中定义简单的键值对形式的属性。这些属性能够被项目中的gradle脚本所访问。如下所示:

# gradle.properties
# 注意文件的注释是以#开头的
groupId=tech.easily
artifactId=EasyDependency

有的时候,我们可能需要在代码中动态的创建属性文件并读取文件中的属性(比如自定义插件的时候),我们可以使用java.util.Properties类。比如:

void createPropertyFile() {
def localPropFile = new File(it.projectDir.absolutePath + "/local.properties")
def defaultProps = new Properties()
if (!localPropFile.exists()) {
localPropFile.createNewFile()
defaultProps.setProperty("debuggable", 'true')
defaultProps.setProperty("groupId", GROUP)
defaultProps.setProperty("artifactId", project.name)
defaultProps.setProperty("versionName", VERSION_NAME)
defaultProps.store(new FileWriter(localPropFile), "properties auto generated for resolve dependencies")
} else {
localPropFile.withInputStream { stream ->
defaultProps.load(stream)
}
}
}

关于属性很重要的一点是属性是可以继承的。在一个项目中定义的属性会自动的被其子项目继承,不管我们是用以上哪种方式添加属性都是适用的。

构建生命周期

前面提及到了gradle中多种脚本类型,并且他们都在不同的生命周期中被执行。

三个阶段

在gradle构建中,构建的生命周期主要包括以下三个阶段:

  • 初始化(Initialization)

    如前文所述,在这个阶段,settings脚本会被执行,从而Gradle会确认哪些项目会参与构建。然后为每一个项目创建 Project 对象。

  • 配置(Configuration)

    配置 Initialization 阶段创建的Project 对象,所有的配置脚本都会被执行。(包括Project中定义的task的配置块也都会被执行)

  • 执行(Configuration)

    这个阶段Gradle会确认哪些在 Configuration 阶段创建和配置的 Task 会被执行,哪些 Task会被执行取决于gradle命令的参数以及当前的目录,确认之后便会执行

监听生命周期

在gradle的构建过程中,gradle为我们提供了非常丰富的钩子,帮助我们针对项目的需求定制构建的逻辑,如下图所示:

《转载 Gradle基础》 image.png

要监听这些生命周期,主要有两种方式:

  • 添加监听器
  • 使用钩子的配置块

关于可用的钩子可以参考GradleProject中的定义,常用的钩子包括:

Gradle

  • beforeProject()/afterProject()

    等同于Project中的beforeEvaluateafterEvaluate

  • settingsEvaluated()

    settings脚本被执行完毕,Settings对象配置完毕

  • projectsLoaded()

    所有参与构建的项目都从settings中创建完毕

  • projectsEvaluated()

    所有参与构建的项目都已经被评估完

TaskExecutionGraph

  • whenReady()

    task图生成。所有需要被执行的task已经task之间的依赖关系都已经确立

Project

  • beforeEvaluate()
  • afterEvaluate()

依赖管理

在前面提及的Gradle的主要特性之中,其中的一点就是强大的依赖管理。Gradle中具备丰富的依赖类型,兼容多种依赖仓库。同时Gradle中的每一项依赖都是基于特定的范围(scope)进行分组管理的。

在gradle中添加为项目添加依赖的方式如下所示:

// build.gradle
// 添加依赖仓库源
repositories {
google()
mavenCentral()
}
// 添加依赖
// 依赖类型包括:文件依赖、项目依赖、模块依赖
dependencies {
// local dependencies.
implementation fileTree(dir: 'libs', include: ['*.jar'])
...
}

四种依赖类型

Gradle中的依赖类型有四类:

  • 模块依赖

    这是gradle中比较常见的依赖类型, 它通常指向仓库中的一个构件,如下所示:

    dependencies {
    runtime group: 'org.springframework', name: 'spring-core', version: '2.5'
    runtime 'org.springframework:spring-core:2.5',
    'org.springframework:spring-aop:2.5'
    runtime(
    [group: 'org.springframework', name: 'spring-core', version: '2.5'],
    [group: 'org.springframework', name: 'spring-aop', version: '2.5']
    )
    runtime('org.hibernate:hibernate:3.0.5') {
    transitive = true
    }
    runtime group: 'org.hibernate', name: 'hibernate', version: '3.0.5', transitive: true
    runtime(group: 'org.hibernate', name: 'hibernate', version: '3.0.5') {
    transitive = true
    }
    }

    模块依赖对应于gradle的API中的 ExternalModuleDependency对象

  • 文件依赖

    dependencies {
    runtime files('libs/a.jar', 'libs/b.jar')
    runtime fileTree(dir: 'libs', include: '*.jar')
    }

  • 项目依赖

    dependencies {
    compile project(':shared')
    }

    项目依赖对应于gradle的API中的 ProjectDependency对象

  • 特定的Gradle发行版依赖

    dependencies {
    compile gradleApi()
    testCompile gradleTestKit()
    compile localGroovy()
    }

管理依赖配置

gradle中项目的每一项依赖都是应用于一个特定的范围的,在gradle中用 Configuration对象表示。每一个Configuration对象都会有一个唯一的名称。Gradle的依赖配置管理如下所示:

《转载 Gradle基础》 image.png

自定义Configuration

在gradle中,自定义Configuration对象是非常简单的,同时定义自己的Configuration对象的时候,也可以继承于已有的Configuration对象,如下所示:

configurations {
jasper
// 定义继承关系
smokeTest.extendsFrom testImplementation
}
repositories {
mavenCentral()
}
dependencies {
jasper 'org.apache.tomcat.embed:tomcat-embed-jasper:9.0.2'
}

管理传递性依赖

在实际的项目依赖管理中存在这样的一种依赖关系:

  • 模块b依赖于模块c
  • 模块a依赖于模块b
  • 模块c成为了模块a的传递依赖

在处理上面这种传递性依赖的时候,gradle提供了强大的管理功能

使用依赖约束

依赖约束可以帮助我们控制传递性依赖以及自身的依赖的版本号(版本范围),比如:

dependencies {
implementation 'org.apache.httpcomponents:httpclient'
constraints {
// 这里httpclient是项目本身的依赖
// 这个约束表示,不管是项目本身的依赖是还是传递依赖都强制使用这个指定的版本号
implementation('org.apache.httpcomponents:httpclient:4.5.3') {
because 'previous versions have a bug impacting this application'
}
// commons-codec并没有被声明为项目本身的依赖
// 所以仅当commons-codec是传递性依赖的时候这段逻辑才会被触发
implementation('commons-codec:commons-codec:1.11') {
because 'version 1.9 pulled from httpclient has bugs affecting this application'
}
}
}

排除特定的传递性依赖

有的时候,我们所依赖的项目/模块会引入多个传递性依赖。而其中部分的传递性依赖我们是不需要的,这时候可以使用exclude排除部分的传递性依赖,如下所示:

dependencies {
implementation('log4j:log4j:1.2.15') {
exclude group: 'javax.jms', module: 'jms'
exclude group: 'com.sun.jdmk', module: 'jmxtools'
exclude group: 'com.sun.jmx', module: 'jmxri'
}
}

强制使用指定的依赖版本

Gradle通过选择依赖关系图中找到的最新版本来解决任何依赖版本冲突。 可是有的时候,某些项目会需要使用一个较老的版本号作为依赖。这时候我们可以强制指定某一个版本。例如:

dependencies {
implementation 'org.apache.httpcomponents:httpclient:4.5.4'
// 假设commons-codec的最新版本是1.10
implementation('commons-codec:commons-codec:1.9') {
force = true
}
}

要注意的是,如果依赖项目中使用了新版本才有的api,而我们强制使用了旧版本的传递依赖之后,会引起运行时的错误

禁止传递性依赖

dependencies {
implementation('com.google.guava:guava:23.0') {
transitive = false
}
}

在android plugin升级到3.0以后,提供了一种新的依赖配置项implement,它的作用就是解决了依赖传递性的问题。模块本身的依赖并不会暴露给被引用的项目

依赖关系解析

使用依赖关系解析规则

依赖关系解析规则提供了一种非常强大的方法来控制依赖关系解析过程,并可用于实现依赖管理中的各种高级模式。比如:

  • 统一构件组的版本

    很多时候我们依赖一个公司的库会包含多个module,这些module一般都是统一构建、打包和发布的,具备相同的版本号。这个时候我们可以通过控制依赖关系的解析过程做到版本号统一。

    configurations.all {
    resolutionStrategy.eachDependency { DependencyResolveDetails details ->
    if (details.requested.group == 'org.gradle') {
    details.useVersion '1.4'
    details.because 'API breakage in higher versions'
    }
    }
    }

  • 处理自定义的版本scheme

    configurations.all {
    resolutionStrategy.eachDependency { DependencyResolveDetails details ->
    if (details.requested.version == 'default') {
    def version = findDefaultVersionInCatalog(details.requested.group, details.requested.name)
    details.useVersion version.version
    details.because version.because
    }
    }
    }
    def findDefaultVersionInCatalog(String group, String name) {
    //some custom logic that resolves the default version into a specific version
    [version: "1.0", because: 'tested by QA']
    }

关于更多依赖关系解析规则的使用实例可以参考gradle的API中的 ResolutionStrategy

使用依赖关系的替代规则

依赖关系的替换规则和上面的依赖关系解析规则有点相似。实际上,依赖关系解析规则的许多功能可以通过依赖关系替换规则来实现。依赖关系的替换规则允许项目依赖(Project Dependency)和模块依赖(Module Dependency)被指定的替换规则透明地替换。

// 使用项目依赖替换模块依赖
configurations.all {
resolutionStrategy.dependencySubstitution {
substitute module("org.utils:api") with project(":api") because "we work with the unreleased development version"
substitute module("org.utils:util:2.5") with project(":util")
}
}
// 使用模块依赖替换项目依赖
configurations.all {
resolutionStrategy.dependencySubstitution {
substitute project(":api") with module("org.utils:api:1.3") because "we use a stable version of utils"
}
}

除了上面两种之外,还有其他三种的依赖关系规则处理。因为没有实际使用过,这里不过多阐述,想了解更多可以查看官方的文档Customizing Dependency Resolution Behavior

  • 使用组件元数据(meta-data)规则
  • 使用组件选择规则
  • 使用模块更换规则

插件开发

插件开发是gradle灵活的构建体系中的一个强大工具。通过gradle中的PluginAPI,我们可以自定义插件,把一些通用的构建逻辑插件化并广泛的运用。比如Android项目中都会使用的:com.android.application,kotlin-android,java等等。

网上关于插件开发的文章已经很多,这里不再赘述。这里推荐我写的一个Gradle插件,也是在我完全看了gradle的官方文档之后,结合前面提及到的依赖管理的知识写的:

  • EasyDependency

    一个帮助提高组件化开发效率的gradle插件,提供的功能包括:

    1. 发布模块的构件都远程maven仓库
    2. 动态更换依赖配置:对模块使用源码依赖或者maven仓库的构件(aar/jar)依赖

写在最后

全文基本是在看了gradle的官方文档及相关资料之后,按照自己的思路的整理和总结。关于gradle的使用和问题欢迎一起讨论。


推荐阅读
  • 生成式对抗网络模型综述摘要生成式对抗网络模型(GAN)是基于深度学习的一种强大的生成模型,可以应用于计算机视觉、自然语言处理、半监督学习等重要领域。生成式对抗网络 ... [详细]
  • 云原生边缘计算之KubeEdge简介及功能特点
    本文介绍了云原生边缘计算中的KubeEdge系统,该系统是一个开源系统,用于将容器化应用程序编排功能扩展到Edge的主机。它基于Kubernetes构建,并为网络应用程序提供基础架构支持。同时,KubeEdge具有离线模式、基于Kubernetes的节点、群集、应用程序和设备管理、资源优化等特点。此外,KubeEdge还支持跨平台工作,在私有、公共和混合云中都可以运行。同时,KubeEdge还提供数据管理和数据分析管道引擎的支持。最后,本文还介绍了KubeEdge系统生成证书的方法。 ... [详细]
  • CSS3选择器的使用方法详解,提高Web开发效率和精准度
    本文详细介绍了CSS3新增的选择器方法,包括属性选择器的使用。通过CSS3选择器,可以提高Web开发的效率和精准度,使得查找元素更加方便和快捷。同时,本文还对属性选择器的各种用法进行了详细解释,并给出了相应的代码示例。通过学习本文,读者可以更好地掌握CSS3选择器的使用方法,提升自己的Web开发能力。 ... [详细]
  • 解决Cydia数据库错误:could not open file /var/lib/dpkg/status 的方法
    本文介绍了解决iOS系统中Cydia数据库错误的方法。通过使用苹果电脑上的Impactor工具和NewTerm软件,以及ifunbox工具和终端命令,可以解决该问题。具体步骤包括下载所需工具、连接手机到电脑、安装NewTerm、下载ifunbox并注册Dropbox账号、下载并解压lib.zip文件、将lib文件夹拖入Books文件夹中,并将lib文件夹拷贝到/var/目录下。以上方法适用于已经越狱且出现Cydia数据库错误的iPhone手机。 ... [详细]
  • sklearn数据集库中的常用数据集类型介绍
    本文介绍了sklearn数据集库中常用的数据集类型,包括玩具数据集和样本生成器。其中详细介绍了波士顿房价数据集,包含了波士顿506处房屋的13种不同特征以及房屋价格,适用于回归任务。 ... [详细]
  • 拥抱Android Design Support Library新变化(导航视图、悬浮ActionBar)
    转载请注明明桑AndroidAndroid5.0Loollipop作为Android最重要的版本之一,为我们带来了全新的界面风格和设计语言。看起来很受欢迎࿰ ... [详细]
  • CF:3D City Model(小思维)问题解析和代码实现
    本文通过解析CF:3D City Model问题,介绍了问题的背景和要求,并给出了相应的代码实现。该问题涉及到在一个矩形的网格上建造城市的情景,每个网格单元可以作为建筑的基础,建筑由多个立方体叠加而成。文章详细讲解了问题的解决思路,并给出了相应的代码实现供读者参考。 ... [详细]
  • 如何搭建Java开发环境并开发WinCE项目
    本文介绍了如何搭建Java开发环境并开发WinCE项目,包括搭建开发环境的步骤和获取SDK的几种方式。同时还解答了一些关于WinCE开发的常见问题。通过阅读本文,您将了解如何使用Java进行嵌入式开发,并能够顺利开发WinCE应用程序。 ... [详细]
  • 本文介绍了在CentOS上安装Python2.7.2的详细步骤,包括下载、解压、编译和安装等操作。同时提供了一些注意事项,以及测试安装是否成功的方法。 ... [详细]
  • 树莓派语音控制的配置方法和步骤
    本文介绍了在树莓派上实现语音控制的配置方法和步骤。首先感谢博主Eoman的帮助,文章参考了他的内容。树莓派的配置需要通过sudo raspi-config进行,然后使用Eoman的控制方法,即安装wiringPi库并编写控制引脚的脚本。具体的安装步骤和脚本编写方法在文章中详细介绍。 ... [详细]
  • 本文介绍了如何使用PHP向系统日历中添加事件的方法,通过使用PHP技术可以实现自动添加事件的功能,从而实现全局通知系统和迅速记录工具的自动化。同时还提到了系统exchange自带的日历具有同步感的特点,以及使用web技术实现自动添加事件的优势。 ... [详细]
  • VScode格式化文档换行或不换行的设置方法
    本文介绍了在VScode中设置格式化文档换行或不换行的方法,包括使用插件和修改settings.json文件的内容。详细步骤为:找到settings.json文件,将其中的代码替换为指定的代码。 ... [详细]
  • 本文讨论了在Windows 8上安装gvim中插件时出现的错误加载问题。作者将EasyMotion插件放在了正确的位置,但加载时却出现了错误。作者提供了下载链接和之前放置插件的位置,并列出了出现的错误信息。 ... [详细]
  • 本文介绍了在多平台下进行条件编译的必要性,以及具体的实现方法。通过示例代码展示了如何使用条件编译来实现不同平台的功能。最后总结了只要接口相同,不同平台下的编译运行结果也会相同。 ... [详细]
  • Imtryingtofigureoutawaytogeneratetorrentfilesfromabucket,usingtheAWSSDKforGo.我正 ... [详细]
author-avatar
施工的公司_534
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有