Android—Gradle教程(二)

前言

在上一篇文章中,對(duì)Gradle基礎(chǔ)以及構(gòu)建機(jī)制進(jìn)行了詳細(xì)的講解,在這一篇中將會(huì)對(duì)Gradle核心模型以及Gradle插件進(jìn)行講解。

1.Gradle核心模型

1.1 Gradle鉤子函數(shù)

講鉤子函數(shù),還是得拿出Gradle執(zhí)行流程圖

如圖所示

  • gradle在生命周期三個(gè)階段都設(shè)置了相應(yīng)的鉤子函數(shù)調(diào)用。
  • 使用鉤子函數(shù),處理自定義的構(gòu)建:
    • 初始化階段:gradle.settingsEvaluated和gradle.projectsLoaded。(在settings.gradle中生效)
    • 配置階段:project.beforeEvaluate和project.afterEvaluate;gradle.beforeProject、gradle.afterProject及gradle.taskGraph.taskGraph.whenReady。
    • 執(zhí)行階段:gradle.taskGraph.beforeTask和gradle.taskGraph.afterTask。

而Gradle也可以監(jiān)聽各個(gè)階段的回調(diào)處理:

  • gradle.addProjectEvaluationListener
  • gradle.addBuildListener
  • gradle.addListener:TaskExecutionGraphListener (任務(wù)執(zhí)行圖監(jiān)聽),TaskExecutionListener(任務(wù)執(zhí)行監(jiān)聽),TaskExecutionListener、TaskActionListener、StandardOutputListener ...

概念又說了一大堆,擼碼驗(yàn)證一下!

  1. 打開AS,創(chuàng)建一個(gè)普通工程項(xiàng)目。
  2. 進(jìn)入項(xiàng)目build.gradle(外層)文件
  3. 在末尾添加如下代碼:
// =======================================
// Gradle提供的鉤子函數(shù)
// 配置階段:
gradle.beforeProject {
    println "gradle.beforeProject"
}
gradle.afterProject {
    println "gradle.afterProject"
}
gradle.taskGraph.whenReady {
    println "gradle.taskGraph.whenReady"
}
beforeEvaluate {

    println "beforeEvaluate"
}
afterEvaluate {
    println "afterEvaluate"
}

//==================
// 為gradle設(shè)置監(jiān)聽
gradle.addProjectEvaluationListener(new ProjectEvaluationListener() {
    @Override
    void beforeEvaluate(Project project) {
        println "Configure listener beforeEvaluate"
    }

    @Override
    void afterEvaluate(Project project, ProjectState state) {
        println "Configure listener afterEvaluate"
    }
})


gradle.addBuildListener(new BuildListener() {
    @Override
    void buildStarted(Gradle gradle) {
        println "Build listener buildStarted"
    }

    @Override
    void settingsEvaluated(Settings settings) {
        println "Build listener settingsEvaluated"
    }

    @Override
    void projectsLoaded(Gradle gradle) {
        println "Build listener projectsLoaded"
    }

    @Override
    void projectsEvaluated(Gradle gradle) {
        println "Build listener projectsEvaluated"
    }

    @Override
    void buildFinished(BuildResult result) {
        println "Build listener buildFinished"
    }
})

task runGradle{
    println "configure runGradle AAAAAA"
    doFirst {
        println "doFirst runGradle AAAAAA"
    }
}

代碼解析

最上面那段代碼就是上一篇文章也寫過相同的,隨后為Gradle設(shè)置了配置監(jiān)聽以及運(yùn)行監(jiān)聽。然后我們運(yùn)行一下這個(gè)runGradle任務(wù)看下效果:

Starting Gradle Daemon...
Connected to the target VM, address: '127.0.0.1:65159', transport: 'socket'
Gradle Daemon started in 2 s 697 ms

> Configure project :
configure runGradle AAAAAA
Configure listener afterEvaluate
gradle.afterProject
afterEvaluate

> Configure project :app
Configure listener beforeEvaluate
gradle.beforeProject
Configure listener afterEvaluate
gradle.afterProject
Build listener projectsEvaluated
gradle.taskGraph.whenReady

> Task :runGradle
doFirst runGradle AAAAAA
Build listener buildFinished

BUILD SUCCESSFUL in 8s
1 actionable task: 1 executed
14:46:28: Task execution finished 'runGradle'.
Disconnected from the target VM, address: '127.0.0.1:65159', transport: 'socket'

從這個(gè)運(yùn)行效果可以看出,配置階段它暫時(shí)分為了兩個(gè)(因?yàn)楝F(xiàn)在只有Project以及app的Gradle),在配置project.gradle的時(shí)候,并沒有執(zhí)行beforeEvaluatebeforeProject這兩個(gè)方法;而這兩個(gè)方法卻在配置app.gradle的時(shí)候執(zhí)行了。

所以上一篇留下的小瑕疵在這里得到了最終解釋(為什么配置階段沒運(yùn)行那兩方法),因?yàn)樵谂渲?code>project.gradle的時(shí)候,是不會(huì)運(yùn)行那兩方法的。

現(xiàn)在繼續(xù)回到運(yùn)行效果這里,這次重點(diǎn)放在前三句以及末尾幾句。

我們?cè)谑褂肁ndroidStudio編譯項(xiàng)目的時(shí)候,往往都是第一次編譯的很慢,但只要編譯好了,當(dāng)天再次編譯的時(shí)候就非常快;而編譯好的項(xiàng)目長時(shí)間不編譯也會(huì)出現(xiàn)編譯很慢的情況,這是什么原因呢?

答案就在于:Starting Gradle Daemon... 這段代碼。

1.2 Gradle守護(hù)進(jìn)程(Daemon)

項(xiàng)目啟動(dòng)時(shí),會(huì)開啟一個(gè)client,然后啟動(dòng)一個(gè)Daemon,通過client向daemon收發(fā)請(qǐng)求,項(xiàng)目關(guān)閉,client關(guān)閉,Daemon保持啟動(dòng),有類似項(xiàng)目再次部署時(shí),會(huì)直接通過新的client訪問已經(jīng)啟動(dòng)的Daemon,所以速度很快,默認(rèn)daemon不使用3小時(shí)后關(guān)閉;不同項(xiàng)目兼容性考慮,也可使用--no-daemon 啟動(dòng)項(xiàng)目,就沒有速度優(yōu)勢(shì)了。

所以在這個(gè)運(yùn)行效果里面能看到: Connected to the target VM, address 運(yùn)行開始,連接Daemon Disconnected from the target VM, address 運(yùn)行結(jié)束,關(guān)閉連接Daemon

在我們使用Gradle的時(shí)候,當(dāng)有多個(gè)library工程項(xiàng)目時(shí),往往會(huì)對(duì)版本進(jìn)行統(tǒng)一化,因此這就需要使用Gradle屬性的擴(kuò)展功能。

1.3 Gradle屬性擴(kuò)展

  • 使用ext對(duì)任意對(duì)象屬性進(jìn)行擴(kuò)展:

    • 對(duì)project進(jìn)行使用ext進(jìn)行屬性擴(kuò)展,對(duì)所有子project可見。
    • 一般在root project中進(jìn)行ext屬性擴(kuò)展,為子工程提供復(fù)用屬性,通過rootProject直接訪問
    • 任意對(duì)象都可以使用ext來添加屬性:使用閉包,在閉包中定義擴(kuò)展屬性。直接使用=賦值,添加擴(kuò)展屬性。
    • 由誰進(jìn)行ext調(diào)用,就屬于誰的擴(kuò)展屬性。
    • 在build.gradle中,默認(rèn)是當(dāng)前工程的project對(duì)象,所以在build.gradle直接使用"ext="或者"ext{}"其實(shí)就是給project定義擴(kuò)展屬性
  • 使用gradle.properties以鍵值對(duì)形式定義屬性,所有project可直接使用

1.3.1 使用ext對(duì)任意對(duì)象屬性進(jìn)行擴(kuò)展

在project.gradle里添加如下代碼

ext {// project 屬性擴(kuò)展,能在別的工程可見
    prop1 = "prop1"
    prop3 = "prop3"
}

ext.prop2 = "prop2"

println prop1
println prop2

task runProExtPro{
    println "runProExtPro\t"+project.ext.prop3
    println "runProExtPro\t"+project.prop2
}

運(yùn)行任務(wù)runProExtPro后的效果

...略
prop1
prop2
runProExtPro    prop3
runProExtPro    prop2
...略

從這個(gè)運(yùn)行效果可知通過ext這個(gè)屬性會(huì)開啟一個(gè)閉包,在閉包內(nèi)可以進(jìn)行多屬性擴(kuò)展,擴(kuò)展后,也可在外部進(jìn)行單屬性擴(kuò)展。因?yàn)檫@里訪問是在當(dāng)前project.gradle環(huán)境下運(yùn)行的,現(xiàn)在在app.gradle里面訪問試試。

task runAppExtPro{
    println "runAppExtPro\t"+project.prop3
    println "runAppExtPro\t"+project.prop2
}

注意看,這里已經(jīng)把ext給去掉了,因?yàn)樵谶@加上會(huì)提示對(duì)應(yīng)屬性不存在,所以在訪問ext擴(kuò)展屬性時(shí),推薦直接通過project.xx的方式直接訪問。現(xiàn)在來看看運(yùn)行runAppExtPro效果:

...略
runAppExtPro    prop3
runAppExtPro    prop2
...略

從這里可以看出:對(duì)project進(jìn)行使用ext進(jìn)行屬性擴(kuò)展,對(duì)所有子project可見。

當(dāng)我們配置版本信息的時(shí)候,不想吧擴(kuò)展屬性,配置在根project.gradle里面的時(shí)該怎么辦呢?此時(shí)就有了另一種擴(kuò)展方式。

1.3.2 使用gradle.properties定義屬性

打開gradle.properties,在里面添加如下屬性:

MIN_SDK_VERSION=21
TARGET_SDK_VERSION=30
COMPILE_SDK_VERSION=30
BUILD_TOOL_VERSION=30.0.3

打開對(duì)應(yīng)子project.gradle或者我們依賴的library庫,就可以使用我們剛剛擴(kuò)展的屬性。

android {
    compileSdkVersion Integer.parseInt(COMPILE_SDK_VERSION)
    buildToolsVersion BUILD_TOOL_VERSION

    defaultConfig {
        applicationId "com.hqk.gradledemo01"
        minSdkVersion Integer.parseInt(MIN_SDK_VERSION)
        targetSdkVersion Integer.parseInt(TARGET_SDK_VERSION)
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

   ...略
...略

現(xiàn)在依然能夠編譯成,并且所有子project都可以統(tǒng)一使用 gradle.properties擴(kuò)展的屬性版本號(hào)。當(dāng)然我們也可以專門寫一個(gè)task來驗(yàn)證一下: 在app.gradle里,添加如下代碼

task checkVersion{
    println "runAppGradle"
    println "MIN_SDK_VERSION:"+MIN_SDK_VERSION
    println "TARGET_SDK_VERSION:"+TARGET_SDK_VERSION
    println "COMPILE_SDK_VERSION:"+COMPILE_SDK_VERSION
    println "BUILD_TOOL_VERSION:"+BUILD_TOOL_VERSION
}

運(yùn)行效果

...略
runAppGradle
MIN_SDK_VERSION:21
TARGET_SDK_VERSION:30
COMPILE_SDK_VERSION:30
BUILD_TOOL_VERSION:30.0.3
...略

完美運(yùn)行,也打出來想要的效果。不過到這為止,寫的task幾乎都是打印輸出,都還沒寫過復(fù)雜邏輯。那么如果想要實(shí)現(xiàn)復(fù)雜邏輯,要怎樣定義task呢?

1.4 Gradle自定義任務(wù)

在build.gradle中自定義任務(wù):

  • task定義的任務(wù)其實(shí)就是DefaultTask的一種具體實(shí)現(xiàn)類的對(duì)象
  • 可以使用自定義類繼承DeaflutTask:
    • 在方法上使用@TaskAction注解,表示任務(wù)運(yùn)行時(shí)調(diào)用的方法。
    • 使用@Input表示對(duì)任務(wù)的輸入?yún)?shù)。
    • 使用@OutputFile表示任務(wù)輸出文件。
    • 使用inputs,outputs直接設(shè)置任務(wù)輸入/輸出項(xiàng)。
    • 一個(gè)任務(wù)的輸出項(xiàng)可以作為另一個(gè)任務(wù)的輸入項(xiàng) (隱式依賴關(guān)系)。

1.4.1 文件數(shù)據(jù)寫入Demo

class WriteTask extends DefaultTask {
    @Input
//    @Optional
    // 表示可選
    String from
    
    @OutputFile
//    @Optional
    // 表示可選
    File out
    
    WriteTask() {

    }
    @TaskAction
    void fun() {
        println " @TaskAction fun()"
        println from
        println out.toString()
        out.createNewFile()
        out.text=from
    }
}

task myTask(type: WriteTask) {
    from = "a/b/c" // 輸入
    out = file("test.txt") // 輸出
}

從這段代碼可知,定義了一個(gè)WriteTask自定義任務(wù),里面兩個(gè)屬性,分別用對(duì)應(yīng)注解表示輸入輸出對(duì)象,隨后定義了myTask 任務(wù),將字符串寫入file文件里,運(yùn)行來看看效果。

如圖所示

當(dāng)Gradle運(yùn)行成功時(shí),同級(jí)目錄下新增了txt文件,里面的內(nèi)容就是我們剛剛寫入字符串?,F(xiàn)在這個(gè)demo來升級(jí)一下,目前是一個(gè)字符串寫入文件,那么能不能將一個(gè)文件的內(nèi)容寫入在另一個(gè)文件里呢?現(xiàn)在來試試:

class WriteTask extends DefaultTask {
////    @Input
////    @Optional
//    // 表示可選
//    String from
////    @OutputFile
////    @Optional
//    // 表示可選
//    File out
    WriteTask() {

    }
    @TaskAction
    void fun() {
        println " @TaskAction fun()"
//        println from
//        println out.toString()
//        out.createNewFile()
//        out.text=from
        println inputs.files.singleFile
        def inFile = inputs.files.singleFile

        def file = outputs.files.singleFile
        file.createNewFile()
        file.text = inFile.text

    }
}

task myTask(type: WriteTask) {
//    from = "a/b/c" // 輸入
//    out = file("test.txt") // 輸出
    inputs.file file('build.gradle')
    outputs.file file('test.txt')
}

現(xiàn)在將輸入輸出的方式改了,通過inputs.outputs.的方式進(jìn)行輸入輸出。里面邏輯是將build.gradle里面的內(nèi)容寫入test.txt里面,運(yùn)行看看效果:

從這里看出,已經(jīng)成功將build.gradle里面的內(nèi)容寫入test.txt里面了。

到這里,數(shù)據(jù)寫入demo 已經(jīng)寫完了?,F(xiàn)在開始新的demo:文件壓縮

1.4.2 文件壓縮Demo

app.gradle里面添加如下代碼:

task zip(type: Zip) {
    archiveName "outputs.zip"http:// 輸出的文件名字
    destinationDir file("${buildDir}/custom")// 輸出的文件存放的文件夾
    from "${buildDir}/outputs"http:// 輸入的文件
}

通過這段代碼可知,將會(huì)吧同級(jí)目錄下的${buildDir}運(yùn)行成功的build目錄下的outputs文件里面的內(nèi)容進(jìn)行壓縮處理。

注意:這里之所以會(huì)壓縮,注意型參,類型為Zip,表示啟用的是Zip壓縮任務(wù)。就和我們剛剛自定義的文件寫入形參類型為type: WriteTask

現(xiàn)在運(yùn)行task zip看看效果:

從這個(gè)效果圖可知:這個(gè)壓縮已經(jīng)成功壓縮了。但問題來了,因?yàn)閱为?dú)執(zhí)行task zip任務(wù)是不會(huì)啟用APK編譯的,因?yàn)閮烧卟]有任何關(guān)聯(lián)(上一篇講解過),那么如果壓縮的目標(biāo)不存在(apk并沒有編譯生成對(duì)應(yīng)的build文件夾)會(huì)怎樣?吧目標(biāo)文件夾刪除試一下:

運(yùn)行效果

...略

> Task :app:zip NO-SOURCE
Build listener buildFinished

注意看,這里提示 NO-SOURCE,并沒有任何資源,也就是壓縮失敗了。那么能不能等壓縮目標(biāo)創(chuàng)建 好了再來壓縮呢?或者說,執(zhí)行壓縮任務(wù)的時(shí)候,就算目標(biāo)任務(wù)不存在也要提前編譯好后再來壓縮。

現(xiàn)在繼續(xù)改造代碼:

//task zip(type: Zip) {
//    archiveName "outputs.zip"http:// 輸出的文件名字
//    destinationDir file("${buildDir}/custom")// 輸出的文件存放的文件夾
//    from "${buildDir}/outputs"http:// 輸入的文件
//}

afterEvaluate {
    println tasks.getByName("packageDebug")
    task zip(type: Zip) {
        archiveName "outputs2.zip"http:// 輸出的文件名字
        destinationDir file("${buildDir}/custom")// 輸出的文件存放的文件夾
        from tasks.getByName("packageDebug").outputs.files// 輸入的文件
        tasks.getByName("packageDebug").outputs.files.each {
            println it
        }
    }
}

在這里我將壓縮任務(wù)轉(zhuǎn)移到了app.gradleafterEvaluate 閉包里面,也就是說,apk在編譯配置即將結(jié)束的時(shí)候,會(huì)將task zip任務(wù),注入在Gradle執(zhí)行流程里,當(dāng)單獨(dú)運(yùn)行task zip任務(wù)的時(shí)候,因?yàn)樗赼pk編譯執(zhí)行流程里面,所以它就會(huì)啟動(dòng)apk的編譯,隨后執(zhí)行task zip任務(wù)就能達(dá)到想要的效果了。現(xiàn)在繼續(xù)單獨(dú)運(yùn)行task zip任務(wù)試試:

注意:形參type: Zip的任務(wù)只能存在一個(gè),所以要把外面的注釋掉

代碼這沒有運(yùn)行按鈕了,那么就用右邊工具來輔助運(yùn)行,注意左邊并沒有編譯好的文件夾,點(diǎn)擊右邊運(yùn)行:

運(yùn)行結(jié)束后,左邊如愿以償多了對(duì)應(yīng)的build文件夾,里面也有對(duì)應(yīng)的壓縮包,而且名字也能對(duì)上。

到這里文件壓縮demo已經(jīng)完美的實(shí)現(xiàn)了,但是這個(gè)功能只能給你自己這一個(gè)項(xiàng)目使用,那萬一想給他人使用或者說給其他項(xiàng)目使用怎么辦呢?那這個(gè)就遇到用到插件了。

2.Gradle插件

2.1 什么是Gradle插件

  • Gradle插件是提供給gradle構(gòu)建工具,在編譯時(shí)使用的依賴項(xiàng)。插件的本質(zhì)就是對(duì)公用的構(gòu)建業(yè)務(wù)進(jìn)行打包,以提供復(fù)用
  • Gradle插件分為:腳本插件和二進(jìn)制插件 (實(shí)現(xiàn)Plugin的類)
  • Gradle插件通過apply方法引入到工程

這里說到Gradle插件分為:腳本插件和二進(jìn)制插件,那么對(duì)應(yīng)有何區(qū)別?

  • 腳本插件實(shí)現(xiàn)了一些列的任務(wù),并且進(jìn)行了組裝,按照提供的API就可以直接使用
  • Gradle腳本插件,是提供實(shí)現(xiàn)的任務(wù)封裝,需要自行組裝?;蛘呤怯玫降囊恍┚唧w業(yè)務(wù)的封裝。

2.2 Gradle 腳本插件

既然是腳本,那么就創(chuàng)建對(duì)應(yīng)的腳本:在項(xiàng)目根目錄創(chuàng)建腳本文件script.gradle,里面寫入代碼:

afterEvaluate {
    println tasks.getByName("packageDebug")
    task zip(type: Zip) {
        archiveName "outputs3.zip"http:// 輸出的文件名字
        destinationDir file("${buildDir}/custom")// 輸出的文件存放的文件夾
        from tasks.getByName("packageDebug").outputs.files// 輸入的文件
        tasks.getByName("packageDebug").outputs.files.each {
            println it
        }
    }
}

仔細(xì) 看這個(gè)腳本,可以發(fā)現(xiàn):腳本插件里面的內(nèi)容和剛剛我們?cè)?code>app.gradle里面寫入的內(nèi)容一模一樣,接下來按照apply方法引入到工程試試:

進(jìn)入app.gradle里面


apply from: '../script.gradle'

android {
    compileSdkVersion Integer.parseInt(COMPILE_SDK_VERSION)
    buildToolsVersion BUILD_TOOL_VERSION
    
...略
}

//task zip(type: Zip) {
//    archiveName "outputs.zip"http:// 輸出的文件名字
//    destinationDir file("${buildDir}/custom")// 輸出的文件存放的文件夾
//    from "${buildDir}/outputs"http:// 輸入的文件
//}

//afterEvaluate {
//    println tasks.getByName("packageDebug")
//    task zip(type: Zip) {
//        archiveName "outputs2.zip"http:// 輸出的文件名字
//        destinationDir file("${buildDir}/custom")// 輸出的文件存放的文件夾
//        from tasks.getByName("packageDebug").outputs.files// 輸入的文件
//        tasks.getByName("packageDebug").outputs.files.each {
//            println it
//        }
//    }
//}

記得這里要把剛剛的壓縮注釋掉?,F(xiàn)在繼續(xù)點(diǎn)擊右邊的運(yùn)行看看效果:

從這個(gè)效果上看,已經(jīng)完美運(yùn)行成功!腳本插件就這么簡單!那么二進(jìn)制插件又該是怎樣的?

2.3 Gradle 二進(jìn)制插件

//apply from: '../script.gradle'
apply plugin: MyPlugin

android {
    compileSdkVersion Integer.parseInt(COMPILE_SDK_VERSION)
    buildToolsVersion BUILD_TOOL_VERSION
...略
}

//task zip(type: Zip) {
//    archiveName "outputs.zip"http:// 輸出的文件名字
//    destinationDir file("${buildDir}/custom")// 輸出的文件存放的文件夾
//    from "${buildDir}/outputs"http:// 輸入的文件
//}
//afterEvaluate {
//    println tasks.getByName("packageDebug")
//    task zip(type: Zip) {
//        archiveName "outputs2.zip"http:// 輸出的文件名字
//        destinationDir file("${buildDir}/custom")// 輸出的文件存放的文件夾
//        from tasks.getByName("packageDebug").outputs.files// 輸入的文件
//        tasks.getByName("packageDebug").outputs.files.each {
//            println it
//        }
//    }
//}

//=============================================
// 插件:1. 腳本插件
// 2. 二進(jìn)制插件

class MyPlugin implements Plugin<Project> {

    @Override
    void apply(Project target) {
        println "MyPlugin apply"

        target.afterEvaluate {
            println "MyPlugin afterEvaluate "+target.tasks.getByName("packageDebug")
            target.task(type: Zip, "zip") {//第二個(gè)參數(shù)要指定是哪個(gè)方法
                archiveName "outputs4.zip"http:// 輸出的文件名字
                destinationDir target.file("${target.buildDir}/custom")// 輸出的文件存放的文件夾
                from target.tasks.getByName("packageDebug").outputs.files// 輸入的文件
                target.tasks.getByName("packageDebug").outputs.files.each {
                    println it
                }
            }
        }
    }


這里看到,定義了MyPlugin 類實(shí)現(xiàn)了對(duì)應(yīng)的Plugin<Project> 接口,在對(duì)應(yīng)的target.afterEvaluate里面定義了任務(wù)target.task(type: Zip, "zip"),第一個(gè)參數(shù)明確什么類型,第二個(gè)參數(shù)表示當(dāng)前任務(wù)名為zip壓縮。

現(xiàn)在刪除之前運(yùn)行的結(jié)果,繼續(xù)運(yùn)行右邊的任務(wù),看看效果:

哈哈哈,這個(gè)插件也如期的運(yùn)行成功了。到這里這篇教程差不多就結(jié)束了。

3. 結(jié)束語

相信看到這里的小伙伴,對(duì)Gradle的核心模型以及Gradle插件有了一個(gè)全新的認(rèn)知。在下一篇里,將會(huì)繼續(xù)深入Gradle講解。

原創(chuàng)不易,如果本篇文章對(duì)小伙伴們有用,希望小伙伴們多多點(diǎn)贊支持一下。筆者也好更快更好的更新教程。

本文轉(zhuǎn)自 https://juejin.cn/post/7024069982325571592,如有侵權(quán),請(qǐng)聯(lián)系刪除。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容