IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Gradle学习笔记 -> 正文阅读

[移动开发]Gradle学习笔记

在这里插入图片描述


gradle同maven、ant一样,可以作为构建工具进行项目的构建,不同的是,gradle中的包可以被项目所引用(也就是说相比较于构建工具,gradle更像是一个框架)。

1、生命周期

gradle生命周期包括初始化阶段、配置阶段、执行阶段。

  1. 初始化阶段解析整个工程中所有的Project,构建所有的Project对应的project对象。
  2. 配置阶段解析所有project对象中的task,构建好所有task的拓扑图。
  3. 执行阶段执行具体的task及其依赖task。
Initialization初始化阶段
Configuration配置阶段
Execution执行阶段

build.gradle中有如下方法用于监听gardle的生命周期。
No方法名描述
1beforeEvaluate配置阶段开始前的监听回调
2afterEvaluate配置阶段完成之后的监听回调
3buildFinishedgradle生命结束之后的监听

示例:

在settings.gradle中输出(初始化阶段实际上就是执行了settings.gradle文件)。

println '初始化阶段开始执行...'

在build.gradle中添加如下方法

/**
 * 配置阶段开始前的监听回调
 */
this.beforeEvaluate {}

/**
 * 配置阶段完成之后的监听回调
 */
this.afterEvaluate {
    println '配置阶段执行完毕..'
}

/**
 * gradle生命结束之后的监听
 */
this.gradle.buildFinished {
    println '执行阶段执行完毕..'
}

在控制台输入:gradle clean后得到如下结果:

E:\idea-projects\hello-gradle>gradle clean
初始化阶段开始执行…

Configure project :
> 配置阶段执行完毕…
执行阶段执行完毕…

BUILD SUCCESSFUL in 612ms
1 actionable task: 1 up-to-date

gradle还有其他方法可以监听生命周期。

No方法描述
1this.gradle.beforeProject{}等同于beforeEvaluate
2this.gradle.afterProject{}等同于afterEvaluate
3this.gradle.addListener{}其他监听

执行gradle的build命令(build实际上也是一个task)会先将build所依赖的task先执行完,最后在执行build。

compileJava
processResources
classes
javaDoc
compileTestjava
processTestResources
jar
testClasses
uploadArchives
assemible
test
check
build

task的依赖实际上就是一个有向无环图,这种依赖关系在配置阶段完成之后生成。

2、Project

project包括一个根project和若干个子project(子project中同样可以包含project),project中一定会包含有一个build.gradle文件,输入gradlew project可以查看当前项目所有的project,project的api基本可以分为以下几种。

Project Api组成
gradle生命周期Api
Project相关Api
task相关Api
属性相关Api
file相关Api
其他Api

gradle生命周期Api就是以上一系列的gradle生命周期监听函数。

2.1、Project相关Api

Project相关Api与project的生命周期有关。

  1. getAllProjects()方法可以获取到项目中所有的project(类似于gradlew project的功能)。

     // build.gradle文件添加以下代码
     getProjects()
     
     def getProjects() {
         println '-------------------------------------------'
         this.getAllprojects().eachWithIndex {
             Project project, int index ->
                 println((index == 0 ? 'Root project : ' : '+--- project : ') + project.name)
         }
         println '-------------------------------------------'
     }
    

    执行任意的gradle命令都会执行build.gradle脚本,以clean命令为例
    输入gradlew build

    >Configure project :
    -------------------------------------------
    Root project : hello-gradle
    ±-- project : gradle-back
    ±-- project : gradle-web
    -------------------------------------------

  2. getSubProjects方法可以获取到项目中所有的子project。

    getProjects()
    
    def getProjects() {
        this.getSubprojects().eachWithIndex { Project project, int i ->
            println('Sub Project : ' + project.name)
        }
    }
    

    > Configure project :
    Sub Project : gradle-back
    Sub Project : gradle-web

  3. getParent方法可以获取到父project。

    在gradle-back模块的build.gradle文件输入如下代码:

    // 获取父project
    getParentProject()
    def getParentProject() {
        println('parent project name: ' + this.getParent().name)
    }
    

    > Configure project :gradle-back
    parent project name: gradle-test

  4. getRootProject方法可以获取到根project。

    // 获取根project
    println("root project : ${this.getRootProject().name}")
    

    > Configure project :
    root project : gradle-test

  5. project方法可以在父工程或根工程中完成对子工程的配置。
    由于project是树形结构的,那么可以通过父project获取到所有的子project,随后对子project进行修改操作。
    图片

    // 配置gradle-web模块
    project('gradle-web') { Project project ->
        group 'com.gradle'
        version '1.0.0-release'
        dependencies {
    
        }
    }
    

    一个gradle的project对应一个gradle.build文件,因此一般情况下不会在父工程中指定子工程的配置,子工程的配置信息写在其build.gradle 文件中,父工程配置整个项目公用的信息。

  6. allProjects方法配置当前工程和以当前工程为根的所有工程的信息。

    allprojects {
        group 'com.gradle'
        version '1.0.0-release'
        dependencies {
    
        }
    }
    // 查看gradle-web的group
    println(project('gradle-web').group)
    
  7. subProjects方法配置当前工程的子工程的信息。

    // 该配置只针对子工程,不包含当前工程
    subprojects { Project project ->
        apply from: '../publishMaven.gradle'
    }
    // 查看gradle-web的group
    println(project('gradle-web').group)
    

    apply from 用于导入其他的.gradle配置文件(gradle的配置文件可以写在多个文件中,使用的时候进行导入即可)。

2.2、属性相关Api

打开Project接口的源码发现其有如下默认属性:
Project接口
gradle的默认属性非常的少,但是可以对其进行扩展。

2.2.1、ext + 闭包可以定义扩展属性。

// 使用ext扩展块对project进行属性扩展
ext {
    defaultGroup = 'com.it.gradle'
    defaultVersion = '1.0.0-release'
}

subprojects {
    group this.defaultGroup
    version this.defaultVersion
    dependencies {

    }
}

使用扩展属性的好处是可以在其他的project中获取到本project定义的属性,也可以在其他project中指定此project的扩展属性(没有此作用,相当于直接定义变量)。

// 根project使用ext为每个子工程定义属性
subprojects {
    ext {
        defaultGroup = 'com.it.gradle'
        defaultVersion = '1.0.0-release'
    }
}

虽然以上形式属性只在根project中定义一次,但是经过编译后会在所有的子project中生成2个属性(从本质上来说还是相当于重复定义了多个变量)。可以将ext 扩展块直接定义在根工程中,如果子工程想要使用属性,则需先使用this.rootProject获取到根工程后再获取属性,这样属性只定义了一次。

根工程:

// 根project定义扩展属性
ext {
    defaultGroup = 'com.it.gradle'
    defaultVersion = '1.0.0-release'
}

子工程

group this.rootProject.defaultGroup
version this.rootProject.defaultVersion

groovy规定,父工程的所有属性可以被子工程所继承,所以子工程的代码可以简写为:

group this.defaultGroup
version this.defaultVersion

为了使项目更加模块化,可以将扩展属性定义在其他配置文件中,随后通过apply from引入到根工程中。

新建common.gradle文件,并编写ext扩展块:

ext {
    defaultGroup = 'com.it.gradle'
    defaultVersion = '1.0.0-release'
}

根project引入common.gradle文件:

// 引入扩展属性
apply from: this.file('common.gradle')

2.2.2、gradle.properties文件中定义扩展属性

通过上面的Project接口源码可以发现,project默认引入了gradle.properties文件,所以可以直接将扩展属性写在gradle.properties中,但是由于是.properties文件,所以属性只能定义key-value的格式(ext扩展块可以定义list、map…格式的属性)。

gradle.properties文件:

isLoadTest=true

settings.gradle文件:

// 判断是否引入test工程
if (hasProperty('isLoadTest') ? isLoadTest.toBoolean() : false) {
    include 'test'
}

2.3、文件相关Api

gradle对文件的操作主要有以下几种:

文件相关Api
路径相关Api
get...
文件操作相关Api
文件定位
文件拷贝
文件数遍历

2.3.1. 路径相关Api

路径相关Api有以下几种:

No.方法名描述
1getRootDir()获取根工程下文件所在路径
2getBuildDir()获取当前工程下build文件所在路径
3getProjectDir()获取当前工程下project文件所在路径
println "the root file path : ${getRootDir().absolutePath}"
println "the build file path : ${getBuildDir().absolutePath}"
println "the project file path : ${getProjectDir().absolutePath}"

2.3.2. 文件操作常用Api

file() 方法用于在project下寻找文件(相对路径)。

// 在project相对路径下寻找common.gradle文件
def extText = file('./common.gradle')
println extText.text

files() 方法用于在project下寻找多个文件(与file()方法相似)

// 在project相对路径下寻找common.gradle文件
def extFiles = files('./common.gradle')
extFiles.each {println it.text}

copy() 方法用于实现的文件拷贝。

// 将copy-test.txt拷贝到父项目build文件夹下
// 将copy-test.txt拷贝到父项目build文件夹下
copy {
    from file('copy-test.txt')
    into getRootProject().getBuildDir().absolutePath
    exclude {} // 排除不想拷贝的文件
    rename {}  // 重命名文件
}

fileTree() 方法用于文件树的遍历。

// 遍历build文件夹下所有文件
fileTree('build') { FileTree fileTree ->
    fileTree.visit { FileTreeElement element ->
        println "the file name is ${element.name}"
    }
}

2.4、其他类型Api

其他Api包含两部分:

其他Api
依赖相关Api
外部命令执行

2.4.1. 依赖相关Api

buildscript() 方法用于配置项目仓库地址和插件:

buildscript { ScriptHandler scriptHandler ->
    // 配置工程的仓库地址
    scriptHandler.repositories {
        jcenter() // 添加jcenter仓库
        mavenCentral() // 添加maven中央仓库
        mavenLocal()  // 添加maven本地仓库
        maven { // 添加个人maven仓库
            name = 'personal' // 仓库名字
            url = 'http://localhost:8081/nexus/repository' // 仓库地址
            credentials{// 仓库认证配置信息
                username = 'admin'
                password = '123456'
            }
        }
        // 其他仓库可以直接写在下面
        ivy {}  // 添加ivy仓库(Ant使用)
    }
    // 配置工程的"插件"依赖地址(此插件是gradle使用到的插件)
    scriptHandler.dependencies {
        classpath: 'com.android.tools.build:gradle:2.2.2'
        classpath: 'com.tencent.tinker:tinker-patch-gradle-plugin:1.7.7'
    }
}

repositories() 方法也可以配置仓库信息:

repositories {
    jcenter() // 添加jcenter仓库
    mavenCentral() // 添加maven中央仓库
    mavenLocal()  // 添加maven本地仓库
    maven { // 添加个人maven仓库
        name = 'personal' // 仓库名字
        url ='http://localhost:8081/nexus/repository' // 仓库地址
        credentials{// 仓库认证配置信息
            username = 'admin'
            password = '123456'
        }
    }
    // 其他仓库可以直接写在下面
    ivy {}  // 添加ivy仓库(Ant使用)
}

dependencies() 方法用于配置项目依赖:

dependencies {
    compile 'junit:junit:4.12'
    compile ('org.springframework.boot:spring-boot-starter-web:5.3.0') {
        exclude group: 'org.springframework' // 排除依赖
        exclude module: 'spring-boot-starter-web'
    }
}

2.4.2. 外部命令

exec() 方法用于执行外部命令。

// 将api拷贝到项目外
task(name: 'apkCopy') {
    doLast {
        def sourcePath = this.buildDir.path + '/outputs/apk'
        def desPath = '/usr/local/apk'
        def command = "mv -f ${sourcePath} ${desPath}"
        exec{
            try{
                executable 'bash'
                args '-c ', command
            }catch(GradleException e) {
                println 'copy fail : ' + e.message
            }
        }
    }
}

3、Task

3.1、Task的创建与配置

执行gradlew tasks可以查看工程中有多少个task。可以通过task()方法直接定义一个task。

task helloTask {
    println 'i am hello task'
}

通过gradlew helloTask执行task。

>Configure project :
i am hello task

task还可以通过TaskContainer的create()方法来创建。

this.tasks.create(name: 'helloTask') {
    println 'i am hello task'
}

以上两种方式创建的task并无任何区别,通过task()方法创建的task实际上也会被添加到project的taskContainer中,TaskContainer实际上是一个管理类,负责管理project中的Task,其有如下主要方法:

No.方法名描述
1findByPath通过路径查找task,找不到返回null
2getByPath通过路径查找task,找不到抛出异常
3create创建一个task

在task创建的时候可以直接指定一些配置。

// 指定task配置
task helloTask(group:'com.it.utool','description':'task study') { 
    println 'i am hello task'
}

也可以在闭包中调用task的setter方法进行配置。

this.tasks.create(name: 'helloTask') {
    setGroup('com.it.utool')  // 为task归类(相同group的task会被放在一起)
    setDescription('task study') // 为task添加描述
    println 'i am hello task'
}

task的所有配置可以通过查看Task源码来查找。
Task

3.2、Task执行详解

task闭包中的代码会在task的配置阶段执行,想要让代码在执行阶段执行,需要调用task的doFirst方法和doLast方法。

this.tasks.create(name: 'helloTask') {
    setGroup('com.it.utool')  // 为task归类(相同group的task会被放在一起)
    setDescription('task study') // 为task添加描述

    doFirst {
        // 执行阶段执行代码
    }

    doLast {
        
    }
}

也可以定义多个doFirstdoLastdoFirst方法会在已有task之前添加逻辑,doLast会在已有方法之后添加逻辑),执行的顺序就是代码中的顺序。doFirstdoLast也可以写在代码块外面。

this.tasks.create(name: 'helloTask') {
    setGroup('com.it.utool')  // 为task归类(相同group的task会被放在一起)
    setDescription('task study') // 为task添加描述
}
helloTask.doFirst {
    // 执行阶段执行代码
}

例:计算build时常:

// 计算build时常
def buildStartTime=0,buildEndTime=0

this.afterEvaluate { Project project ->
    // 保证要找的task寻找完毕
    def preBuildTask = project.tasks.getByName('preBuild')
    preBuildTask.doFirst {
        buildStartTime = System.currentTimeMillis()
        println 'the start time is: ' + buildStartTime
    }

    def buildTask = project.tasks.getByName('build')
    buildTask.doLast {
        buildEndTime = System.currentTimeMillis()
        println "the build time is : ${(buildEndTime - buildStartTime) / 1000}s"
    }
}

以上代码发现,通过doFirstdoLast的合理使用可以在已有Task的基础上添加自己的逻辑。

3.3、Task依赖和执行顺序

task有三种方式可以指定执行顺序:

Task执行顺序
dependsOn强依赖方式
通过Task输入输出指定
通过Api指定执行顺序

3.3.1. dependsOn强依赖方式

// 定义TaskX
task taskX {
    doLast {
        println 'taskX'
    }
}
// 定义TaskY
task taskY {
    doLast {
        println 'taskY'
    }
}

// 定义TaskZ并依赖于TaskX和TaskY
task taskZ(dependsOn: [taskX,taskY]) {
    doLast {
        println 'taskZ'
    }
}

输入:gradlew taskZ

>Task :taskX
taskX

>Task :taskY
taskY

>Task :taskZ
taskZ

通过以上taskZ的执行结果可以发现,taskZ依赖于taskX和taskY,并且在taskX和taskY都执行后在执行(taskX和taskY没有依赖关系,所以执行顺序是随机的)。

使用Task类的denpendsOn()方法也可以为task指定依赖。

taskZ.dependsOn(taskX,taskY)

task还可以在配置阶段动态指定依赖。

task lib1 {
    doLast {
        println 'lib1'
    }
}

task lib2 {
    doLast {
        println 'lib2'
    }
}

task onlib {
    doLast {
        println 'nolib'
    }
}

// 定义TaskZ并依赖于以lib开头的task
task taskZ {
    dependsOn this.tasks.findAll {task ->
         task.name.startsWith('lib')
    }
    doLast {
        println 'taskZ'
    }
}

输入:gradlew taskZ后观察输出结果发现,taskZ依赖于lib1和lib2。

3.3.2 通过Task输入输出指定执行顺序

例:文件的写入和读取

class VersionMessage {
    String versionName
    String versionCode
    String versionInfo
}

// 扩展属性
ext {
    versionName = '1.0.0'
    versionCode = '100'
    versionInfo = 'this is the first version, add some base info'
    destFile = file('release.xml')
}

定义一个task写入文件。

task writeTask {
    // 将project属性传递给task(为task指定输入)
    inputs.property('versionName', this.versionName) 
    inputs.property('versionCode', this.versionCode)
    inputs.property('versionInfo', this.versionInfo)
    if (this.destFile != null && !this.destFile.exists()) { // 文件不存在则创建文件
        this.destFile.createNewFile()
    }
    outputs.file this.destFile // 为task指定输出

    // 执行阶段
    doLast {
        def data = inputs.getProperties() // 取得输入数据
        File file = outputs.getFiles().getSingleFile() // 取得输出文件
        // 将Map转换为对象
        def message = new VersionMessage(data)
        // 写入xml
        def sw = new StringWriter()
        def xmlBuilder = new MarkupBuilder(sw)

        if (file.text == null || file.text.size() <= 0) { // file中没有内容
            xmlBuilder.releases {
                release {
                    versionCode(message.versionCode)
                    versionName(message.versionName)
                    versionInfo(message.versionInfo)
                }
            }
            // 将节点追加到文件中
            file.withWriter {writer -> writer.append(sw.toString())}
            println 123
        } else {
            // 将新生成的xml数据插入到根节点之前
            xmlBuilder.release {
                versionCode(message.versionCode)
                versionName(message.versionName)
                versionInfo(message.versionInfo)
            }
            def lines = file.readLines()
            def length = lines.size() - 1
            file.withWriter {writer ->
                lines.eachWithIndex{ String line, int index ->
                    if(index != length) {
                        writer.append(line + '\r\n') // 将原信息写入
                    }else if(index == length) { // 最后一行
                        // 写入新生成的信息
                        writer.append('\r\r\n' + sw.toString() + '\r\n')
                        writer.append(lines.get(length)) // 写入最后一行
                    }
                }
            }
        }
    }
}

定义一个task读取文件。

task readTask {
    inputs.file this.destFile
    doLast {
        def file = inputs.files.singleFile
        println file.text
    }
}

以上代码看来两个task没有任何的依赖关系,但是二者都引用了destFile这个属性,destFile对writeTask来说是输出文件,对readTask来说时输入文件,所以这两个task通过destFile这个属性关联了起来。gradle规定输出属性对应的task会先被执行,所以如果writeTask和readTask同时执行的话,writeTask一定会先执行。

新建一个testTask并依赖于writeTask和readTask。

task testTask {
    dependsOn writeTask,readTask
    doLast {
        println 'test task !!! '
    }
}

执行testTask发现writeTask会先执行。

3.3.3. 通过Api指定执行顺序

mustRunAfter可以使task在指定task执行后执行,mustRunAfter可以指定一个task,也可以指定一个task数组。

定义3个task: taskX、taskY、taskZ,并让按照taskX->taskY->taskZ的顺序执行。

task taskX {
    doLast {
        println 'taskX'
    }
}

task taskY {
    mustRunAfter taskX  //taskY在taskX执行后执行
    doLast {
        println 'taskY'
    }
}

task taskZ {
    mustRunAfter taskY //taskZ在taskY执行后执行
    doLast {
        println 'taskZ'
    }
}

打乱执行顺序,在控制台输入:gradlew taskY taskZ taskX

>Task :taskX
taskX

>Task :taskY
taskY

>Task :taskZ
taskZ

发现任务按照taskX->taskY->taskZ的顺序执行。

shouldRunAfter的用法与mustRunAfter完全相同,区别在于shouldRunAfter(仅仅是建议)不强制指定执行顺序。

3.4、Task挂接到生命周期

将自定义task挂接到gradle生命周期只需要在生命周期task执行前后执行自定义task即可。

可以让生命周期task依赖于自定义task,这样自定义task就可以在生命周期task之前执行。

// 自定义一个task
task selfTask {
    doLast {
        println 'self task...'
    }
}

this.project.afterEvaluate { project ->
    def buildTask = tasks.findByName('clean')
    if (buildTask == null) {
        throw new GradleException('build task not find...')
    }
    buildTask.dependsOn selfTask // clean之前执行selfTask
}

finizedBy可以指定当前task之后执行的task。

this.project.afterEvaluate { project ->
    def buildTask = tasks.findByName('clean')
    if (buildTask == null) {
        throw new GradleException('build task not find...')
    }
    buildTask.finalizedBy selfTask // clean之后执行selfTask
}

4、其他模块

4.1、settings.gradle文件

gradle项目中的settings.gradle文件实际上是通过配置代码来配置Setting类,而Settings类的作用则是在初始化时配置项目。
Settings
Settings类的主要方法为include,将我们新建的子工程引入进来,这样gradle才会把它当做一个工程去处理。

4.2、SourceSet类

SourceSet类负责管理项目中所有的代码、资源、第三方库存放的位置。gradle约定源码存放在java文件夹下,资源存放在resources文件夹下(gradle项目结构与maven相似,但是maven项目结构不可以修改,gradle提供了一系列的DSL,可以让你方便的定义或者修改配置)。

sourceSets {
    main {
        java {
            srcDir 'src/java' // 指定源码目录
        }
        resources {
            srcDir 'src/resources' //资源目录
        }
    }
}

4.3、自定义Plugin

将完成某一特定功能的task都封装在一起就形成了插件,这样项目只要引入了插件,就能调用特定的功能。

gradle插件工程和普通gradle工程目录结构区别不大,在main文件夹下有一个groovy文件夹用于存放groovy类,有一个resources文件夹用于存放配置文件。

Project

插件的build.gradle文件与普通项目不同。

apply plugin: 'groovy'

dependencies {
    implementation gradleApi() // gradle sdk
    implementation localGroovy() // groovy sdk
}

sourceSets {
    main {
        groovy {
            srcDir 'src/main/groovy'
        }
        resources {
            srcDir 'src/main/resources'
        }
    }
}

在groovy中创建一个com.it.study的包并里面创建一个名为GradleStudyPlugin的groovy类。

package com.it.study

import org.gradle.api.*

/**
 * 自定义插件
 */
class GradleStudyPlugin implements Plugin<Project> {

    /**
     * 插件被引入时要执行的方法
     * @param project 引入当前插件的Project
     */
    @Override
    void apply(Project project) {
        println 'Hello Plugin : ' + project.name
    }
}

这样就完成了一个简单的自定义插件,如果想要让其他项目或模块引入此插件,则需要声明引入方式。

resources文件夹下新建META-INF/gradle-plugins文件夹,并在里面新建com.it.study.properties文件,并输入:

implementation-class=com.it.study.GradleStudyPlugin

这样做就声明了自定义插件的插件名为com.it.study

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-10-21 12:18:02  更:2021-10-21 12:18:35 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 0:58:39-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码