Jak zrychlit build Android aplikace
Nedávno jsem se zamyslel nad otázkou, která trápí téměř každého vývojáře. Jak docílit rychlejších buildů našich aplikací? Existují nějaké zaručené metody, díky kterým bych nemusel po každé změně v kódu čekat někdy i několik minut než uvidím, jestli je daná změna to pravé řešení mého problému?
Na internetu se člověk dozví opravdu spoustu zaručených tipů, které by měly build aplikací téměř až zázračně zrychlit, ale opravdu všechny fungují? Abych si odpověděl na tyto otázky, vybral jsem si jeden z našich nejrozsáhlejších projektů a rozhodl jsem se na něm prakticky otestovat všechny tipy na zrychlení build procesu, na které narazím.
Měření
Než se pustíme do samotného zrychlování, je nutné nejprve zjistit, jak pomalý (rychlý) vlastně můj build je a ideálně i kde jsou jeho slabá místa. Pro build Android aplikací je prakticky standardem Gradle. Ten nabízí hned několik možností, jak build proces profilovat a kontrolovat jeho průběh a výsledek. Mezi nejzákladnější způsoby určitě patří:
- Gradle log – v logu najdeme v průběhu buildu spoustu užitečných informací a například některé warningy nám rovnou můžou ukázat na konkrétní problémy. Zároveň vždycky nakonec uvidíme výslednou délku našeho buildu, což jako základní (a pro vývojáře vlastně i důležitá) informace často postačí.
- Gradle profiler je další možností, jak prozkoumat detaily svého buildu. Stačí přidat k příkazu pro build přepínač --profile a Gradle v průběhu buildu všechny detaily zapíše do úhledného HTML souboru, na který na konci procesu vypíše odkaz. Tento způsob je velmi jednoduchý na použití a přitom nabízí velmi detailní časové informace o každém jednotlivém tasku, který se během buildu vykonává, přehledně roztříděné mezi jednotlivé fáze build procesu.
- Možná poněkud méně známou možností profilování buildů je Gradle Build Scan. Je to služba, kterou Gradle nabízí jako součást svého portfolia pro větší firmy. Tato služba funguje na bázi pluginu, který se musí přidat do build procesu a poté build spustit s přepínačem --scan . Výsledky build scanu jsou po skončení buildu publikovány na stránkách Gradle pod unikátním kódem, který je na konci buildu zobrazen. Statistiky zobrazené na vygenerované stránce jsou krásně interaktivní, naprosto detailní a dokonce již samy obsahují některé tipy na zrychlení vašeho buildu. Nevýhodou však zůstává nutnost úpravy samotného build scriptu a dále pak také upload různých (možná i citlivých) údajů kamsi do internetu.
Důležité je také uvědomit si, co vlastně chceme měřit. Nejdůležitější veličinou je určitě délka inkrementálního buildu, který spouští často vývojáři po malých změnách v kódu. Délka takového buildu pak přímo ovlivňuje efektivitu vývojářů, a pokud je build proces nastaven tak, že po každé změně musí vývojáři na build čekat velmi dlouho, může v krajním případě zcela ochromit produktivitu. Naproti tomu stojí čistý build, který staví celou aplikaci ze všech závislostí zcela od základu. Takový build se většinou spouští na CI serverech (samozřejmě nejen tam) a jeho rychlost již není tak kritická (nespouští se většinou tak často), ale přesto ovlivňuje např. rychlost nasazení nové verze testerům atd.
První krůčky
Pravidlo číslo 1 při ladění výkonu jakéhokoliv softwaru vždy zní: pracovat pouze s aktuálními nástroji. Proto by mělo být samozřejmostí vždy, než začnu řešit skutečné důvody rychlosti (pomalosti) buildu, zkontrolovat v projektu verzi jak Gradle samotného, tak také Android gradle pluginu, který obstarává velkou část celého build procesu. Každá nová verze Gradle i jeho pluginů vždy v sekci "co je nového" slibuje spoustu oprav chyb a především zrychlení oproti minulé verzi. I kdyby to nebyl žádný velký zázrak, tak poslední stabilní verze nebude nejspíše pomalejší než ta předchozí. Jen pozor při upgradu o hodně verzí dopředu – to se již může objevit i nekompatibilita s danou verzí build skriptu nebo některé části kódu. Při pravidelných aktualizacích nástrojů pro build by se však tato situace objevit neměla.
Další užitečnou věcí, kterou je dobré zmínit na úvod je Gradle deamon. Pokud řešíme build na vývojářově počítači (kde probíhá build často a většinou také po pár malých změnách), dá se ušetřit pár drahocenných setin tak, že necháme proces Gradle běžet stále na pozadí, aby se nemusel pro každý build znovu startovat. Stačí přidat do properties Gradle org.gradle.daemon=true. Toto nastavení není vhodné používat na CI serverech, kde může probíhat i několik buildů najednou a důraz je kladen především na stabilitu.
Samozřejmostí by dále mělo být vyjmutí adresářů, které build ovlivňují z dohledu antivirů či jiných služeb, které jakkoliv zpomalují a ztěžují práci se soubory v daném umístění.
Poté, co jsme provedli všechny výše uvedené kroky, je na čase konečně spustit některý z profilerů a zkontrolovat detailně jeho výsledek. Může se stát, že například některá z knihoven nebo některý použitý plugin vykonávají nadbytečnou činnost, o které víme, že v našem buildu zaručeně není potřeba (někdy taková činnost může být i značně časově náročná). Pokud na takový task ve výsledcích z profileru narazíme, není nic jednoduššího, než vykonávání takového tasku vypnout pomocí přepínače -x nebo --exclude-task.
Po provedení předchozích úkonů trval čistý build mého testovacího projektu v průměru 108 s.
Gradle heap size
Jelikož Gradle build běží na JVM, jedním z nejdůležitějších nastavení buildu je množství paměti, kterou dostane k dispozici. S touto hodnotou se dá hýbat tak, aby proces buildu využil dostupnou pamět zařízení co nejefektivněji. Pokud máte na vašem zařízení paměti málo, může pro vás být výhodnější přidělit buildu paměti poněkud méně, aby vám zbyla paměť pro ostatní procesy vašeho počítače, a zabránit tak častějšímu swapování na disk. Pokud zase máte paměti dostatek, můžete build výrazně zrychlit jejím přidělením Gradlu. Zároveň je také nutné myslet na to, že pokud přidělíte Gradle buildu méně než 1536 MB paměti, bude nutné spustit část buildu zvanou dex jako samostatný proces, což znamená další zpomalení. (Gradle před tímto krokem sám varuje v logu).
Nastavení množství paměti je dáno Gradle property org.gradle.jvmargs. V některých tipech na internetu dále radí, že je nezbytné nastavit ještě také zvlášť množství dostupné paměti pro dex přes property android.dexOptions.javaMaxHeapSize, ale množství dostupné paměti pro dex je v případě, že dex běží v rámci procesu Gradle buildu stejně limitováno Gradle heap size, takže není nutné nastavovat zvlášť.
Jak jsem již zmiňoval množství dostupné paměti velmi ovlivňuje rychlost buildu a na mém testovacím projektu se rychlost čistého buildu pohybovala v závislosti na dostupné paměti takto:
Závislosti
V běžných projektech se často odkazujeme na větší či menší množství nejrůznějších knihoven. Každá závislost na externí knihovně prodlužuje rychlost buildu, takže nejjednodušší poučka je mít takových závislostí obecně co nejméně. Řešení těchto závislostí je nedílnou součástí každého buildu, proto je nutné mu věnovat zvýšenou pozornost. Určité problémy je možné zjistit už z výsledků profileru, ale existují zásady, které se dají použít vždy.
Repozitáře knihoven se při buildu prochází v pořadí, v jakém jsou v build scriptu definovány. Je proto dobré znát, odkud jaké knihovny pochází a upravit podle toho pořadí repozitářů. Například častou chybou je uvedení repozitářů mavenCentral() a jcenter(). Jcenter je totiž nadmnožinou mavenCentral a prohledávání obou těchto repozitářů tedy jen zbytečně prodlužuje build. Další čas se dá ušetřit tím, že nebudeme používat dynamické verze knihoven (např. místo com.android.tools.build:gradle:2.+ napíšeme přímo com.android.tools.build:gradle:2.3.1). Vyhledání přesné verze knihovny je obecně rychlejší.
Pokud víme, že žádné nové závislosti již do projektu přidávat nehodláme a nebudeme v nejbližší době ani měnit verze používaných knihoven. Můžeme pro jistotu zcela vypnout online vyhledávání závislostí a zcela se odkázat na lokální úložiště stažených závislostí pomocí přepínače --offline.
V mém případě jsem dokázal vyladěním řešení závislostí ušetřit průměrně pouze setiny, avšak vždy je důležité zkontrolovat výsledky profileru, zda se při řešení závislostí neděje něco nekalého.
Paralelizace
Gradle umí některé části buildu provádět paralelně. Bohužel je to jen několik fází buildu, ale například pokud se buildí projekt, který obsahuje více modulů, dokáže paralelizace ušetřit velké množství času. Existuje i nastavení "kolik vláken smí Gradle při buildu použít", avšak mně se vždy podařilo spustit maximálně přesný počet vláken, jako je jader procesoru (v mém případě 4).
Paralelizace buildu se spouští pomocí property org.gradle.parallel=true a v mém projektu, který obsahuje hned několik modulů ušetřila paralelizace na čistém buildu v průměru 20 s!
Gradle Build Cache
To je funkce dostupná od Gradle verze 3.5, která by měla příznivě ovlivňovat nejen inkrementální ale i čisté buildy (neplést s build cache Android pluginu, která je pro inkrementální buildy automaticky aktivní). Umožňuje Gradlu využívat společnou cache pro všechny buildy, takže by mělo být možné použít některé vybuilděné části z jakýchkoliv předchozích buildů na daném stroji. Tato funkce by měla najít uplatnění především na build serverech, kde probíhá například větší množství buildů podobných projektů nebo různých variant stejných projektů, jelikož dovoluje nastavit Gradle Build Cache i vzdálenou – sdílenou např. na dedikovaném serveru.
Ve svém projektu si jednoduše tuto funkci můžeme aktivovat pomocí přepínače --build-cache nebo pomocí property org.gradle.caching=true. V mém případě však po zapnutí této funkce k měřitelnému zrychlení čistých buildů nedošlo vůbec a u inkrementálních buildů došlo dokonce ke zpomalení, a to v průměru o 22 s! Tato funkce se mi tedy příliš neosvědčila.
dexOptions
Samostatnou kapitolou by mohly být nastavení části buildu, která se nazývá dex. Nastavují se jako properties android.dexOptions.. Ve všemožných radách na internetu jsou zmiňovány především následující:
- preDexLibraries (true) – Po čistém buildu se všechny použité knihovny rovnou vybuildí do formátu dex. Toto nastavení by mělo zrychlit inkrementální buildy na úkor mírného zpomalení čistých buildů – v mém případě to zpomalilo inkrementální build v průměru o 11 s. :-(
- maxProcessCount (číslo) – Počet vláken pro dex, který běží v rámci procesu Gradle buildu – můj build nezrychlilo.
- javaMaxHeapSize (číslo) – Jak jsem již zmiňoval velikost dostupné paměti pro dex je stejně limitována Gradle heap size a je lepší tuto paměť raději nastavit tam.
- incremental (true) – Povolí inkrementální dex – od Gradle verze 2.1 již stejně defaultně zapnuto.
Tipy pro dev prostředí
Existují také tipy, které mohou zrychlit inkrementální build, ale zároveň přímo ovlivňují build skript nebo přímo kód aplikace, proto by měly být použity s maximální opatrností a rozhodně by se nikdy neměly dostat do produkčního kódu.
- Pokud ještě nepoužíváte Android plugin 3.0 (nebo vyšší) a zároveň máte rozsáhlý projekt, který využívá multidex, můžete pro vaše dev buildy vyzkoušet zvednout minimální verzi Android SDK na 21, což by mělo umožnit použití rychlejšího multidexu. Od nové verze Android pluginu se však stejně používá zcela nový multidex, doporučuji proto spíš update projektu.
- V projektech se často používají různé mechanizmy, jak například spočítat code version nebo packagename. Tyto hodnoty jsou součástí Android manifestu, jehož změna při každém buildu tento build prodlužuje. Pro své dev prostředí se proto doporučuje mít tyto hodnoty napevno zadané v build skriptu. Tato úprava však v mém projektu ušetřila jen několik setin.
- Pokud se ve vašem projektu používá hodně verzí resources (např. pro různé jazyky nebo velikosti obrazovky), je možné pro dev verzi tyto resourcy ořezat na nutné minimum (např. jen anglickou verzi a xxhdpi – v konfiguraci Android pluginu resConfigs "en", "xxhdpi") a ušetřit tak čas při jejich zpracování během buildu. V mém projektu se tak podařilo zrychlit inkrementální build v průměru o 2.1 s. Toto nastavení však musí mít člověk neustále na paměti, aby se pak třeba nedivil, že daný string není přeložený. ;-)
Závěr (TL;DR)
Existuje spousta tipů, jak zrychlit build Android aplikací a jistě jsem nezmínil všechny, ale určitě jsem odhalil některé důležité a některé spíše prozatím experimentální. Za důležité bych určitě označil používání aktuálních verzí Gradle i Android pluginu. Pokud aktualizujete svůj projekt průběžně, nemůže se aktualizace nevyplatit. Další důležité nastavení se týká dostupné paměti pro build. Pokud váš počítač disponuje dostatkem paměti, je důležité dát tuto paměť buildu k dispozici – osvědčilo se mi přidělit i 4 GB. Pokud pracujete na projektu s více moduly určitě si zapněte paralelizaci buildu, která má na rychlost velmi příznivý vliv. Pro vývojové prostředí se také může vyplatit ořezat resources jen na nejnutnější varianty. Toto nastavení je však nutné provádět s opatrností a promyslet jeho možné následky. Před procesem optimalizace buildu i po něm je nejlepším pomocníkem profiler, který poskytne všechny potřebné informace a často může upozornit na případné problémy. Ostatní zkoušené možnosti nepřinesly očekávaný účinek a některé dokonce build značně zpomalily, proto určitě doporučuji v průběhu jakéhokoliv snažení o zrychlení neustálé měření rychlosti, protože ne každý tip ze Stack Overflow musí být nutně pravdivý. Dále se také vůbec nezmiňuji např. o instant runu Android Studia, který mi zatím v každé dostupné verzi přišel nespolehlivý, a proto jsem ho byl vždy donucen vypnout.
Doufám, že vám byl můj praktický test užitečný, a každopádně budu rád i za jakékoliv vaše postřehy a tipy v komentářích pod příspěvkem.