Android開發(fā)之Gradle?進階Tasks深入了解
前言
Gradle自定義Task看起來非常簡單,通過tasks.register等API就可以輕松實現(xiàn)。但實際上為了寫出高效的,可緩存的,不拖慢編譯速度的task,還需要了解更多知識。
本文主要包括以下內(nèi)容:
- 定義Task
- 查找Task
- 配置Task
- 將參數(shù)傳遞給Task構造函數(shù)
- Task添加依賴
- Task排序
- Task添加說明
- 跳過Task
- Task支持增量編譯
- Finalizer Task
定義Task
如上所說,自定義Task一般可以通過register API實現(xiàn)
tasks.register("hello") { doLast { println("hello") } } tasks.register<Copy>("copy") { from(file("srcDir")) into(buildDir) }
如果是kotlin或者kts中,也可以通過代理來實現(xiàn)
val hello by tasks.registering { doLast { println("hello") } } val copy by tasks.registering(Copy::class) { from(file("srcDir")) into(buildDir) }
register與create的區(qū)別
除了上面介紹的register,其實create也可以用于創(chuàng)建Task,那么它們有什么區(qū)別呢?
- 通過register創(chuàng)建時,只有在這個task被需要時才會真正創(chuàng)建與配置該Task(被需要是指在本次構建中需要執(zhí)行該Task)
- 通過create創(chuàng)建時,則會立即創(chuàng)建與配置該Task
總得來說,通過register創(chuàng)建Task性能更好,更推薦使用
查找Task
我們有時需要查找Task,比如需要配置或者依賴某個Task,我們可以通過named方法來查找對應名字的task
tasks.register("hello") tasks.register<Copy>("copy") println(tasks.named("hello").get().name) // or just 'tasks.hello' if the task was added by a plugin println(tasks.named<Copy>("copy").get().destinationDir)
也可以使用tasks.withType()方法來查找特定類型的Task
tasks.withType<Tar>().configureEach { enabled = false } tasks.register("test") { dependsOn(tasks.withType<Copy>()) }
除了上述方法,也可以通過tasks.getByPath()方法來查找task,不過這種方式破壞了configuration avoidance和project isolation,因此不被推薦使用
配置Task
在創(chuàng)建了Task之后,我們常常需要配置Task
我們可以在查找到Task之后進行配置
tasks.named<Copy>("myCopy") { from("resources") into("target") include("**/*.txt", "**/*.xml", "**/*.properties") }
我們還可以將Task引用存儲在變量中,并用于稍后在腳本中進一步配置任務。
val myCopy by tasks.existing(Copy::class) { from("resources") into("target") } myCopy { include("**/*.txt", "**/*.xml", "**/*.properties") }
我們也可以在定義Task時進行配置,這也是最常用的一種
tasks.register<Copy>("copy") { from("resources") into("target") include("**/*.txt", "**/*.xml", "**/*.properties") }
將參數(shù)傳遞給Task構造函數(shù)
除了在Task創(chuàng)建后配置參數(shù),我們也可以將參數(shù)傳遞給Task的構建函數(shù),為了實現(xiàn)這點,我們必須使用@Inject注解
abstract class CustomTask @Inject constructor( private val message: String, private val number: Int ) : DefaultTask()
然后,我們可以創(chuàng)建一個Task,在參數(shù)列表的末尾傳遞構造函數(shù)參數(shù)。
tasks.register<CustomTask>("myTask", "hello", 42)
需要注意的是,在任何情況下,作為構造函數(shù)參數(shù)傳遞的值都必須是非空的。如果您嘗試傳遞一個null值,Gradle 將拋出一個NullPointerException指示哪個運行時值是null.
Task添加依賴
有幾種方法可以定義Task的依賴關系,首先我們可以通過名稱定義依賴項
project("project-a") { tasks.register("taskX") { dependsOn(":project-b:taskY") doLast { println("taskX") } } } project("project-b") { tasks.register("taskY") { doLast { println("taskY") } } }
其次我們也可以通過Task對象定義依賴項
val taskX by tasks.registering { doLast { println("taskX") } } val taskY by tasks.registering { doLast { println("taskY") } } taskX { dependsOn(taskY) }
還有一些更高端的用法,我們可以用provider懶加載塊來定義依賴項,在evaluated階段,provider被傳遞給正在計算依賴的task
provider塊應返回單個對象Task或Task對象集合,然后將其視為任務的依賴項,如下所示:taskx添加了所有以lib開頭的對象
val taskX by tasks.registering { doLast { println("taskX") } } // Using a Gradle Provider taskX { dependsOn(provider { tasks.filter { task -> task.name.startsWith("lib") } }) } tasks.register("lib1") { doLast { println("lib1") } }tasks.register("lib2") { doLast { println("lib2") } } tasks.register("notALib") { doLast { println("notALib") } }
Task排序
有時候,兩個task之間沒有依賴關系,但是對兩個task的執(zhí)行順序卻有所要求
任務排序和任務依賴之間的主要區(qū)別在于,排序規(guī)則不會影響將執(zhí)行哪些任務,只會影響它們的執(zhí)行順序。
任務排序在許多場景中都很有用:
- 強制執(zhí)行任務的順序:例如,build 永遠不會在clean 之前運行。
- 在構建的早期運行構建驗證:例如,在開始發(fā)布構建工作之前驗證我是否擁有正確的憑據(jù)。
- 通過在長時間驗證任務之前運行快速驗證任務來更快地獲得反饋:例如,單元測試應該在集成測試之前運行。
- 聚合特定類型的所有任務的結果的任務:例如測試報告任務組合所有已執(zhí)行測試任務的輸出。
gradle提供了兩個可用的排序規(guī)則:mustRunAfter 和 shouldRunAfter
當您使用mustRunAfter排序規(guī)則時,您指定taskB必須始終在taskA之后運行,這表示為taskB.mustRunAfter(taskA)
而shouldRunAfter規(guī)則理加弱化,因為在兩種情況下這條規(guī)則會被忽略。一是使用這條規(guī)則會導致先后順序成環(huán)的情況,二是當并行執(zhí)行task,并且任務的所有依賴關系都已經(jīng)滿足時,那么無論它的shouldRunAfter依賴關系是否已經(jīng)運行,這個任務都會運行。
因此您應該在排序有幫助但不是嚴格要求的情況下使用shouldRunAfter
示例如下:
val taskX by tasks.registering { doLast { println("taskX") } } val taskY by tasks.registering { doLast { println("taskY") } } // mustRunAfter taskY { mustRunAfter(taskX) } // shouldRunAfter taskY { shouldRunAfter(taskX) }
需要注意的是,B.mustRunAfter(A)或B.shouldRunAfter(A)并不意味著任務之間存在任何執(zhí)行依賴關系:
我們可以獨立執(zhí)行A或者任務B。排序規(guī)則僅在兩個任務都計劃執(zhí)行時才有效。
Task添加說明
您可以為Task添加說明。執(zhí)行時gradle tasks時會顯示此說明。
tasks.register<Copy>("copy") { description = "Copies the resource directory to the target directory." from("resources") into("target") include("**/*.txt", "**/*.xml", "**/*.properties") }
跳過Task
gradle提供了多種方式來跳過task的執(zhí)行
使用onlyIf
你可以通過onlyIf為任務的執(zhí)行添加條件,如果任務應該執(zhí)行,則應該返回 true,如果應該跳過任務,則返回 false
val hello by tasks.registering { doLast { println("hello world") } } hello { onlyIf { !project.hasProperty("skipHello") } }
Output of gradle hello -PskipHello
> gradle hello -PskipHello
> Task :hello SKIPPED
如上所示,hello任務被跳過了
使用 StopExecutionException
如果跳過任務邏輯不能使用onlyIf實現(xiàn),您可以使用StopExecutionException。如果某個Action拋出此異常,則跳過該Action的進一步執(zhí)行以及該任務的任何后續(xù)Action的執(zhí)行。構建繼續(xù)執(zhí)行下一個任務。
val compile by tasks.registering { doLast { println("We are doing the compile.") } } compile { doFirst { // Here you would put arbitrary conditions in real life. if (true) { throw StopExecutionException() } } } tasks.register("myTask") { dependsOn(compile) doLast { println("I am not affected") } }
禁用與啟用Task
每個任務都有一個enabled的標志位,默認為true。將其設置為false可以阻止執(zhí)行任何Task的執(zhí)行。禁用的任務將被標記為 SKIPPED。
val disableMe by tasks.registering { doLast { println("This should not be printed if the task is disabled.") } } disableMe { enabled = false }
Task超時
每個Task都有一個timeout屬性,可用于限制其執(zhí)行時間。當一個任務達到它的超時時間時,它的任務執(zhí)行線程被中斷。該任務將被標記為失敗。但是Finalizer Task任務仍將運行。
如果構建時使用了--continue參數(shù),其他任務可以在它之后繼續(xù)運行。不響應中斷的task不能超時。Gradle 的所有內(nèi)置task都會及時響應超時
Task支持增量編譯
任何構建工具的一個重要部分是避免重復工作。在編譯過程中,就是在編譯源文件后,除非發(fā)生了影響輸出的更改(例如源文件的修改或輸出文件的刪除),無需重新編譯它們。因為編譯可能會花費大量時間,因此在不需要時跳過該步驟可以節(jié)省大量時間。
Gradle 支持增量構建,當您運行構建時,有些Task被標記為UP-TO-DATE,這就是增量編譯生效了
那么Gradle增量編譯如何工作?自定義Task如何支持增量編譯?我們一起來看看
Task的輸入輸出
Task最基本的功能就是接受一些輸入,進行一系列運算后生成輸出。比如在編譯過程中,Java源文件是輸入,生成的classes文件是輸出。其他輸入可能包括諸如是否應包含調(diào)試信息之類的內(nèi)容。
task輸入的一個重要特征是它會影響一個或多個輸出,從上圖中可以看出。根據(jù)源文件的內(nèi)容和target jdk版本,會生成不同的字節(jié)碼。這使他們成為task輸入。
但是編譯期的一些其他屬性,比如編譯最大可用內(nèi)存,由memoryMaximumSize屬性決定,memoryMaximumSize對生成的字節(jié)碼沒有影響。因此,memoryMaximumSize不是task輸入,它只是一個內(nèi)部task屬性。
作為增量構建的一部分,Gradle 會檢查自上次構建以來是task的輸入或輸出有沒有發(fā)生變化。如果沒有,Gradle 可以認為task是最新的,因此跳過執(zhí)行其action。需要注意的是,除非task至少有一個task輸出,否則增量構建將不起作用
總得來說:
您需要告訴 Gradle 哪些task屬性是輸入,哪些是輸出。
如果task屬性影響輸出,請務必將其注冊為輸入,否則該任務將被認為是最新的而不是最新的。
相反,如果屬性不影響輸出,則不要將其注冊為輸入,否則任務可能會在不需要時執(zhí)行。
還要注意可能為完全相同的輸入生成不同輸出的非確定性task:不應將這些任務配置為增量構建,因為最新檢查將不起作用。
接下來讓我們看看如何將task屬性注冊為輸入和輸出。
自定義task類型
為了讓自定義task支持增量編譯,只需要以下兩個步驟
- 為每個task輸入和輸出創(chuàng)建類型化屬性(通過 getter 方法)
- 為每個屬性添加適當?shù)淖⒔?/li>
Gradle 支持四種主要的輸入和輸出類型:
- 簡單值
例如字符串和數(shù)字類型。更一般地說,任何一個實現(xiàn)了Serializable的類型。 - 文件系統(tǒng)類型
包括RegularFile,Directory和標準File類,也包括 Gradle 的FileCollection類型的派生類,以及任何可以被Project.file(java.lang.Object)和Project.files(java.lang.Object...)方法接收的參數(shù) - 依賴解析結果
這包括包含Artifact元數(shù)據(jù)的ResolvedArtifactResult類型和包含依賴圖的ResolvedComponentResult類型。請注意,它們僅支持包裝在Provider中. - 包裝類型
不符合其他幾個類型但具有自己的輸入或輸出屬性的自定義類型。task的輸入或輸出包裝在這些自定義類型中。
接下來我們看個例子
假設您有一個task處理不同類型的模板,例如 FreeMarker、Velocity、Moustache 等。它獲取模板源文件并將它們與一些模型數(shù)據(jù)結合以生成不同結果。
此任務將具有三個輸入和一個輸出:
- 模板源文件
- 模型數(shù)據(jù)
- 模板引擎
- 輸出文件的寫入位置
在編寫自定義task類時,我們很容易通過注解將屬性注冊為輸入或輸出
public abstract class ProcessTemplates extends DefaultTask { @Input public abstract Property<TemplateEngineType> getTemplateEngine(); @InputFiles public abstract ConfigurableFileCollection getSourceFiles(); @Nested public abstract TemplateData getTemplateData(); @OutputDirectory public abstract DirectoryProperty getOutputDir(); @TaskAction public void processTemplates() { // ... } } public abstract class TemplateData { @Input public abstract Property<String> getName(); @Input public abstract MapProperty<String, String> getVariables(); }
可以看出,我們定義了3個輸入,一個輸出
- templateEngine,表示使用什么模板引擎,我們傳入一個枚舉類型,枚舉類型都實現(xiàn)了Serializable,因此可作為輸入
- sourceFiles,表示源文件,我們傳入FileCollection作為輸入
- templateData,表示模型數(shù)據(jù),自定義類型,在它的內(nèi)部包裝了真正的輸入,通過@Nested注解表示
- outputDir,表示輸出目錄,表示單個目錄的屬性需要@OutputDirectory注解
當我們重復運行以上task之后,就可以看到以下輸出
> gradle processTemplates
> Task :processTemplates UP-TO-DATE
BUILD SUCCESSFUL in 0s
3 actionable tasks: 3 up-to-date
如上所示,task在執(zhí)行過程中會判斷輸入輸出有沒有發(fā)生變化,由于task的輸入輸出都沒有發(fā)生變化,該task可以直接跳過,展示為up-to-date
除了上述幾種注解,還有其他常用注解如@Internal,@Optional,@Classpath等,具體可查看文檔:Incremental build property type annotations
聲明輸入輸出的好處
一旦你聲明了一個task的正式輸入和輸出,Gradle 就可以推斷出關于這些屬性的一些事情。例如,如果一個task的輸入設置為另一個task的輸出,這意味著第一個task依賴于第二個,gradle可以推斷出這一點并添加隱式依賴
推斷task依賴關系
想象一個歸檔task,會將processTemplates task的輸出歸檔??梢钥吹綒w檔task顯然需要processTemplates首先運行,因此可能會添加顯式的dependsOn. 但是,如果您像這樣定義歸檔task:
tasks.register<Zip>("packageFiles") { from(processTemplates.map {it.outputs }) }
Gradle 會自動使packageFiles依賴processTemplates。它可以這樣做是因為它知道 packageFiles 的輸入之一需要 processTemplates 任務的輸出。我們稱之為推斷的task依賴。
上面的例子也可以寫成
tasks.register<Zip>("packageFiles2") { from(processTemplates) }
這是因為from()方法可以接受task對象作為參數(shù)。然后在幕后,from()使用project.files()方法包裝參數(shù),進而將task的正式輸出轉(zhuǎn)化為文件集合
輸入和輸出驗證
增量構建注解為 Gradle 提供了足夠的信息來對帶注解的屬性執(zhí)行一些基本驗證。它會在task執(zhí)行之前對每個屬性執(zhí)行以下操作:
- @InputFile- 驗證屬性是否有值,并且路徑是否對應于存在的文件(不是目錄)。
- @InputDirectory- 與@InputFile相同,但路徑必須對應于目錄。
- @OutputDirectory- 驗證路徑是否是個目錄,如果該目錄尚不存在,則創(chuàng)建該目錄。
如果一個task在某個位置產(chǎn)生輸出,而另一個任務task將其作為輸入使用,則 Gradle 會檢查消費者任務是否依賴于生產(chǎn)者任務。當生產(chǎn)者和消費者任務同時執(zhí)行時,構建就會失敗。
此類驗證提高了構建的穩(wěn)健性,使您能夠快速識別與輸入和輸出相關的問題。
您偶爾會想要禁用某些驗證,特別是當輸入文件可能實際上不存在時。這就是 Gradle 提供@Optional注釋的原因:您使用它來告訴 Gradle 特定輸入是可選的,因此如果相應的文件或目錄不存在,則構建不應失敗。
并行task
定義task輸入和輸出的另一個好處是:當使用--parallel選項時,Gradle 可以使用此信息來決定如何運行task。
例如,Gradle 將在選擇下一個要運行的任務時檢查task的輸出,并避免并發(fā)執(zhí)行寫入同一輸出目錄的任務。
同樣,當另一個task正在運行消耗或創(chuàng)建一些文件時,Gradle 將使用有關task銷毀哪些文件的信息(例如,由Destroys注釋)來避免運行刪除這些文件的task,反之亦然。
它還可以確定創(chuàng)建一組文件的task已經(jīng)運行,并且使用這些文件的task尚未運行,并且將避免在這中間運行刪除這些文件的task。
總得來說,通過以這種方式提供task的輸入和輸出信息,Gradle 可以推斷task之間的創(chuàng)建/消費/銷毀關系,并可以確保task執(zhí)行不會違反這些關系。
增量編譯原理解析
上面我們介紹了如何自定義一個支持增量編譯的task,那么它的原理是什么呢?
在第一次執(zhí)行task之前,Gradle 會獲取輸入的指紋。該指紋包含輸入文件的路徑和每個文件內(nèi)容的哈希值。Gradle 然后執(zhí)行task。如果任務成功完成,Gradle 會獲取輸出的指紋。該指紋包含一組輸出文件和每個文件內(nèi)容的哈希值。Gradle 會在下次執(zhí)行task時保留兩個指紋。
之后每次執(zhí)行task之前,Gradle 都會獲取輸入和輸出的新指紋。如果新指紋與之前的指紋相同,Gradle 會假定輸出是最新的并跳過該task。如果它們不相同,Gradle 將執(zhí)行task。Gradle 會在下次執(zhí)行task時保留兩個指紋。
如果文件的統(tǒng)計信息(即lastModified和size)沒有改變,Gradle 將重用上次運行的文件指紋。這意味著當文件的統(tǒng)計信息沒有更改時,Gradle 不會檢測到更改。
Gradle 還將task的代碼視為task輸入的一部分。當task、其操作或其依賴項在執(zhí)行之間發(fā)生變化時,Gradle 會認為該task已過期。
Gradle 了解文件屬性(例如,包含 Java classpath 的屬性)是否是順序敏感的。在比較此類屬性的指紋時,即使文件順序發(fā)生變化也會導致task過時。
請注意,如果task指定了輸出目錄,則自上次執(zhí)行以來添加到該目錄的任何文件都將被忽略,并且不會導致任務過期。這是因為不相關的任務可以共享一個輸出目錄而不會相互干擾。如果由于某種原因這不是您想要的行為,請考慮使用TaskOutputs.upToDateWhen(groovy.lang.Closure)
一些高端操作
上面介紹的內(nèi)容涵蓋了您將遇到的大多數(shù)用例,但有些場景需要特殊處理
將@OutputDirectory鏈接到@InputFiles
當您想將一個task的輸出鏈接到另一個task的輸入時,類型通常匹配,例如,F(xiàn)ile可以將輸出屬性分配給File輸入。
不幸的是,當您希望將一個task的@OutputDirectory中的文件作為另一個task的@InputFiles屬性(類型FileCollection)的源時,這種方法就會失效。
例如,假設您想使用 Java 編譯task的輸出(通過destinationDir屬性)作為自定義task的輸入,該task檢測一組包含 Java 字節(jié)碼的文件。這個自定義task,我們稱之為Instrument,有一個使用@InputFiles注解的classFiles屬性。您最初可能會嘗試像這樣配置task:
tasks.register<Instrument>("badInstrumentClasses") { classFiles.from(fileTree(tasks.compileJava.map { it.destinationDir })) destinationDir.set(file(layout.buildDirectory.dir("instrumented"))) }
這段代碼沒有明顯的問題,但是您如果實際運行的話可以看到compileJava并沒有執(zhí)行。在這種情況下,您需要通過dependsOn在instrumentClasses和compileJava之間添加顯式依賴。因為使用fileTree()意味著 Gradle 無法推斷task依賴本身。
一種解決方案是使用TaskOutputs.files屬性,如以下示例所示:
tasks.register<Instrument>("instrumentClasses") { classFiles.from(tasks.compileJava.map { it.outputs.files }) destinationDir.set(file(layout.buildDirectory.dir("instrumented"))) }
或者,您可以使用project.files(),project.layout.files(),project.objects.fileCollection()來代替project.fileTree()
tasks.register<Instrument>("instrumentClasses2") { classFiles.from(layout.files(tasks.compileJava)) destinationDir.set(file(layout.buildDirectory.dir("instrumented"))) }
請記住files(),layout.files()和objects.fileCollection()可以將task作為參數(shù),而fileTree()不能。
這種方法的缺點是源task的所有文件輸出都成為目標task的輸入文件。如果源task只有一個基于文件的輸出,就像JavaCompile一樣,那很好。但是如果你必須在多個輸出屬性中選擇一個,那么你需要明確告訴 Gradle 哪個task使用以下builtBy方法生成輸入文件:
tasks.register<Instrument>("instrumentClassesBuiltBy") { classFiles.from(fileTree(tasks.compileJava.map { it.destinationDir }) { builtBy(tasks.compileJava) }) destinationDir.set(file(layout.buildDirectory.dir("instrumented"))) }
當然你也可以通過dependsOn添加明確的task依賴,但是上面的方法提供了更多的語義,解釋了為什么compileJava必須預先運行。
禁用up-to-date檢查
Gradle 會自動處理對輸出文件和目錄的up-to-date檢查,但如果task輸出完全是另一回事呢?也許它是對 Web 服務或數(shù)據(jù)庫表的更新?;蛘哂袝r你有一個應該始終運行的task。
這就是doNotTrackState的作用,可以使用它來完全禁用task的up-to-date檢查,如下所示:
tasks.register<Instrument>("alwaysInstrumentClasses") { classFiles.from(layout.files(tasks.compileJava)) destinationDir.set(file(layout.buildDirectory.dir("instrumented"))) doNotTrackState("Instrumentation needs to re-run every time") }
如果你的自定義task需要始終運行,那么您也可以在任務類上使用注解@UntrackedTaskTask
提供自定義up-to-date檢查
Gradle 會自動處理對輸出文件和目錄的up-to-date檢查,但如果task輸出是對 Web 服務或數(shù)據(jù)庫表的更新。在這種情況下,Gradle 無法知道如何檢查task是否是up-to-date的。
這是就是TaskOutputs.upToDateWhen方法的作用,使用它我們就可以自定義up-to-date檢查的邏輯。例如,您可以從數(shù)據(jù)庫中讀取數(shù)據(jù)庫模式的版本號。或者,您可以檢查數(shù)據(jù)庫表中的特定記錄是否存在或已更改。
請注意,up-to-date檢查應該節(jié)省您的時間。不要添加比task的標準執(zhí)行花費更多時間的檢查。事實上,如果一個task經(jīng)常需要運行,因為它很少是up-to-date的,那么它可能根本不值得進行up-to-date檢查,如禁用up-to-date中所述。
一個常見的錯誤是使用upToDateWhen()而不是Task.onlyIf(). 如果您想根據(jù)與task輸入和輸出無關的某些條件跳過任務,那么您應該使用onlyIf(). 例如,如果您想在設置或未設置特定屬性時跳過task
Finalizer Task
我們常常使用dependsOn來在一個task之前做一些工作,但如果我們想要在task執(zhí)行之后做一些操作,該怎么實現(xiàn)呢?
這里我們可以用到finalizedBy方法
val taskX by tasks.registering { doLast { println("taskX") } } val taskY by tasks.registering { doLast { println("taskY") } } taskX { finalizedBy(taskY) }
如上所示,taskY將在taskX之后執(zhí)行,需要注意的是finalizedBy并不是依賴關系,就算taskX執(zhí)行失敗,taskY也將正常執(zhí)行
Finalizer task在構建創(chuàng)建的資源無論構建失敗還是成功都必須清理的情況下很有用,一個示例是在集成測試任務中啟動的 Web 容器,即使某些測試失敗,也應該始終關閉它。這樣看來finalizedBy類似java中的finally
要指定Finalizer task,請使用Task.finalizedBy(java.lang.Object...?)方法。此方法接受task實例、task名稱或Task.dependsOn(java.lang.Object...?)接受的任何其他輸入
總結
到這里這篇文章已經(jīng)相當長了,gradle自定義task上手非常簡單,但實際上有非常多的細節(jié),尤其是要支持增量編譯時??偟脕碚f,為了寫出高效的,可緩存的,不拖慢編譯速度的task,還是有必要了解一下這些知識的
參考資料
以上就是Android開發(fā)之Gradle 進階Tasks深入了解的詳細內(nèi)容,更多關于Android開發(fā)Gradle Tasks的資料請關注腳本之家其它相關文章!
相關文章
android編程實現(xiàn)系統(tǒng)圖片剪裁的方法
這篇文章主要介紹了android編程實現(xiàn)系統(tǒng)圖片剪裁的方法,涉及Android針對圖片的獲取、修改、保存等操作的相關技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-11-11詳解Android應用main函數(shù)的調(diào)用
Android常識,App主線程初始化了Looper,調(diào)用prepare的地方是ActivityThread.main函數(shù)。問題來了,App的main函數(shù)在哪兒調(diào)用,下面我們來一起學習一下吧2019-06-06Android中通過AsyncTask類來制作炫酷進度條的實例教程
這篇文章主要介紹了Android中通過AsyncTask來制作炫酷進度條的實例教程,借助AsyncTask類的線程操作方法來管理異步任務,需要的朋友可以參考下2016-05-05Android RxJava異步數(shù)據(jù)處理庫使用詳解
RxJava是一種異步數(shù)據(jù)處理庫,也是一種擴展的觀察者模式。對于Android開發(fā)者來說,使用RxJava時也會搭配RxAndroid,它是RxJava針對Android平臺的一個擴展,用于Android 開發(fā),它提供了響應式擴展組件,使用RxAndroid的調(diào)度器可以解決Android多線程問題2022-11-11Android使用Intent.ACTION_SEND分享圖片和文字內(nèi)容的示例代碼
這篇文章主要介紹了Android使用Intent.ACTION_SEND分享圖片和文字內(nèi)容的示例代碼的實例代碼,具有很好的參考價值,希望對大家有所幫助,一起跟隨小編過來看看吧2018-05-05windows10安裝adb/fastboot驅(qū)動超詳細圖文教程
這篇文章主要介紹了windows10安裝adb/fastboot超詳細圖文教程,安裝方法也很簡單,只要adb安裝成功,fastboot就安裝好了,文中給大家介紹了問題分析及解決方法,需要的朋友可以參考下2023-01-01