Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

App is compiled every time Gradle runs, taking significant time

When developing with Eclipse, if I've ran/debugged an app before and have not changed its source code, it's reasonably fast to run/debug the same app again.

However, with Android Studio and Gralde, every time when I try to run/debug an app, gradle build would always run, adding an additional 15~45 seconds of delay to the time when the app is launched (and sometimes up to 70 seconds on a 4 year-old HP i7 laptop).

Therefore, the question is: is there any way skip Android Studio's gradle build phase, or at least reduce the amount of time it would take to run/debug?


Note: I've already configured gradle.properties as follows:

org.gradle.parallel=true
org.gradle.daemon=true
org.gradle.configureondemand=true

Edit: My gradle build is probably more complex than most projects, as it has 7 different flavors (will expand to ~20) and 3 build types, and also contain Groovy code to change APK name (insert current date), and automatically insert tasks to increment the version code and version name depending on current buildType. Here's the complete build.gradle (modified to hide customer names):

import java.text.SimpleDateFormat

apply plugin: 'com.android.application'

def appendVersionNameVersionCode(applicationVariants) {
    applicationVariants.all { variant ->
        variant.outputs.each { output ->
            def outputFile = output.outputFile
            if (outputFile != null) {
                def PREFIX = "My_APP_"
                if (outputFile.name.endsWith('.apk') && !outputFile.name.startsWith(PREFIX)) {
                    def names = variant.baseName.split("-");
                    def apkName = PREFIX+names[0]+"_";
                    if(names[1].equals(android.buildTypes.debugEx.name)) {
                        apkName += 'debugEx_'
                    } else {
                        apkName += new SimpleDateFormat("YYYYMMdd").format(new Date())
                    }
                    if(variant.name.toLowerCase().contains(android.buildTypes.release.name)) {
                        if (outputFile.name.contains('unsigned')) {
                            apkName += "-unsigned"
                        } else {
                            apkName += "_SIGNED"
                        }
                    }
                    if (!variant.outputs.zipAlign) {
                        apkName += "-unaligned"
                    }
                    apkName += ".apk"
                    println outputFile.name+" --> " + apkName
                    output.outputFile = new File(outputFile.parent, apkName)
                }
            }
        }
    }
}

def retrieveVersionCode(variantName) {
    def manifestFile = file("src/$variantName/AndroidManifest.xml")
    def pattern = Pattern.compile("versionCode=\"(\\d+)\"")
    def manifestText = manifestFile.getText()
    def matcher = pattern.matcher(manifestText)
    matcher.find()
    return Integer.parseInt(matcher.group(1))
}

def retrieveVersionName(variantName) {
    def manifestFile = file("src/$variantName/AndroidManifest.xml")
    def pattern = Pattern.compile(Pattern.quote("versionName=\"") + "(.*?)"+ Pattern.quote("\""))
    def manifestText = manifestFile.getText()
    def matcher = pattern.matcher(manifestText)
    matcher.find()
    return matcher.group(1)
}

android {
    compileSdkVersion 21
    buildToolsVersion "21.1.0"

    lintOptions {
        abortOnError false
        absolutePaths false
        lintConfig file("lint.xml")
    }

    defaultConfig {
        applicationId "com.app.sportcam"
        minSdkVersion 8
        targetSdkVersion 21
    }

    if(project.hasProperty("app.signing")
            && new File(project.property("app.signing")+'.gradle').exists()) {
        apply from: project.property("app.signing")+'.gradle';
    } else {
        println 'Warning, signing credential not found: ' + project.property("app.signing")+'.gradle'
    }

    buildTypes {
        all {
                buildConfigField 'String', 'IP', '"192.168.1.1"'
                buildConfigField 'String', 'RTSP_IP', '"rtsp://"+IP+"/"'

                //debugging
                buildConfigField 'boolean', 'DEBUG_DETAILED', 'false'
                buildConfigField 'boolean', 'DEBUG_UI_STATE', 'false'
                buildConfigField 'boolean', 'INTERNAL_DEBUG', 'false'
                buildConfigField 'boolean', 'ENABLE_VIEWSERVER', 'false'
                buildConfigField 'boolean', 'INJECT_PTP_PROPERTIES', 'false'

                //functional
                buildConfigField 'boolean', 'ENABLE_TIME_LIMIT', 'false'
                buildConfigField 'boolean', 'HIDE_ACTIONBAR_ON_LANDSCAPE', 'false'
                buildConfigField 'boolean', 'ENABLE_VIDEO_DOWNLOAD', 'true'
                buildConfigField 'boolean', 'ENABLE_VIDEO_DOWNLOAD_PROGRESS', 'true'
                buildConfigField 'boolean', 'ENABLE_VIDEO_DOWNLOAD_CANCEL', 'false'
                buildConfigField 'boolean', 'SET_TIME', 'true'
                buildConfigField 'boolean', 'SHOULD_SET_CAMERA_MODE_WHEN_TURNING_RECORDING_OFF', 'false'
                buildConfigField 'boolean', 'SHOULD_SET_CAMERA_MODE_ON_CONNECTION', 'false'

            appendVersionNameVersionCode(applicationVariants)
        }

        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }

        //for customers' testing
        debug {
                buildConfigField 'boolean', 'ENABLE_TIME_LIMIT', 'true'
        }

        //for internal testing
        debugEx {
                buildConfigField 'boolean', 'DEBUG_DETAILED', 'true'
                buildConfigField 'boolean', 'INTERNAL_DEBUG', 'true'
                buildConfigField 'boolean', 'ENABLE_VIEWSERVER', 'true'
                buildConfigField 'boolean', 'INJECT_TEST_PROPERTIES', 'true'

            debuggable true
            signingConfig signingConfigs.debug
            applicationIdSuffix ".debug"
        }
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_7
        targetCompatibility JavaVersion.VERSION_1_7
    }

    def time=Calendar.getInstance()
    time.add(Calendar.MONTH, 3)
    println 'Debug build expiry date='+time.getTime()

    productFlavors {
        // default BuildConfig variables
        all {
            buildConfigField 'long', 'TIME_LIMIT', time.getTimeInMillis()+'l'

            buildConfigField 'boolean', 'ADD_ABOUT', 'true'

            buildConfigField 'boolean', 'FORCE_DEVICE_CHECK', 'false'

            buildConfigField 'boolean', 'SHOW_CUR_SELECTION_PREF', 'true'
            buildConfigField 'boolean', 'SHOW_CUR_SELECTION_ONSCREEN', 'false'

            buildConfigField 'boolean', 'NO_WIFI_SCREEN', 'true'
            buildConfigField 'boolean', 'NO_STREAMING', 'false'
            buildConfigField 'boolean', 'NO_GALLERY', 'false'

            buildConfigField 'boolean', 'INIT_IN_START', 'true'

            buildConfigField 'boolean', 'CUSTOM_FUNCTIONS', 'true'

            buildConfigField 'boolean', 'ENABLE_TIMEOUT_CONTINUE', 'false'

            buildConfigField 'boolean', 'TRANSPARENT_BOTTOM_BAR', 'false'

            buildConfigField 'int', 'LOGO_TIMING', '1000'
        }

        default {
            //mandatory
            buildConfigField 'int', 'CUSTOMER_NAME_PREFIX', '0xFF'

            buildConfigField 'boolean', 'ADD_ABOUT', 'false'

            applicationId = 'com.app.default'
            def variantName='DEFAULT'
            versionCode retrieveVersionCode(variantName)
            versionName retrieveVersionName(variantName)
        }

        Customer_1 {
            //mandatory
            buildConfigField 'int', 'CUSTOMER_NAME_PREFIX', '0x0B'

            buildConfigField 'boolean', 'FORCE_DEVICE_CHECK', 'true'

            applicationId 'com.app.c1'
            def variantName='c1'
            versionCode retrieveVersionCode(variantName)
            versionName retrieveVersionName(variantName)
        }

        Customer_2 {
            //mandatory
            buildConfigField 'int', 'CUSTOMER_NAME_PREFIX', '0xFF' //TODO not final

            buildConfigField 'boolean', 'SHOW_CUR_SELECTION_ONSCREEN', 'true'

            applicationId 'com.app.c2'
            def variantName='c2'
            versionCode retrieveVersionCode(variantName)
            versionName retrieveVersionName(variantName)
        }

        Customer_3 {
            //mandatory
            buildConfigField 'int', 'CUSTOMER_NAME_PREFIX', '0x12'
            buildConfigField 'int', 'LOGO_TIMING', '3000'

            applicationId = 'com.app.c3'
            def variantName='c3'
            versionCode retrieveVersionCode(variantName)
            versionName retrieveVersionName(variantName)
        }

        Customer_4 {
            //mandatory
            buildConfigField 'int', 'CUSTOMER_NAME_PREFIX', '0x02'

            applicationId = 'com.app.c4'
            def variantName='c4'
            versionCode retrieveVersionCode(variantName)
            versionName retrieveVersionName(variantName)
        }

        Customer_5 {
            //mandatory
            buildConfigField 'int', 'CUSTOMER_NAME_PREFIX', '0x04'

            applicationId = 'com.app.c5'
            def variantName='c5'
            versionCode retrieveVersionCode(variantName)
            versionName retrieveVersionName(variantName)
        }

        Customer_6 {
            //mandatory
            buildConfigField 'int', 'CUSTOMER_NAME_PREFIX', '0xFF'

            applicationId = 'com.app.c6'
            def variantName='c6'
            versionCode retrieveVersionCode(variantName)
            versionName retrieveVersionName(variantName)
        }

        Customer_7 {
            //mandatory
            buildConfigField 'int', 'CUSTOMER_NAME_PREFIX', '0x14'

            buildConfigField 'boolean', 'FORCE_DEVICE_CHECK', 'true'

            applicationId = 'com.app.c7'
            def variantName='c7'
            versionCode retrieveVersionCode(variantName)
            versionName retrieveVersionName(variantName)
        }
    }

    sourceSets{
        main {
            res.srcDirs = ['src/main/res']
        }

        default {
            res.srcDir 'src/_Strings_/Standard'
            res.srcDir 'src/_Strings_/xx'
        }

        Customer_1 {
            res.srcDir 'src/_Strings_/Standard'
            res.srcDir 'src/_Strings_/xx'
        }

        Customer_2 {
            res.srcDir 'src/_Strings_/Standard'
            res.srcDir 'src/_Strings_/xx'
        }

        Customer_3 {
            res.srcDir 'src/_Strings_/Standard'
            res.srcDir 'src/_Strings_/xx'
            res.srcDir 'src/_Strings_/yy'
        }

        Customer_4 {
            res.srcDir 'src/_Strings_/Standard'
            res.srcDir 'src/_Strings_/xx'
        }

        Customer_5 {
            res.srcDir 'src/_Strings_/xx'
            res.srcDir 'src/_Strings_/zz'
        }

        Customer_6 {
            res.srcDir 'src/_Strings_/xx'
            res.srcDir 'src/_Strings_/aa'
        }

        Customer_7 {
            res.srcDir 'src/_Strings_/Standard'
            res.srcDir 'src/_Strings_/xx'
        }
    }
}

import java.util.regex.Pattern
def variantNameRegex = Pattern.quote("generate") + "(.*?)"+ Pattern.quote("BuildConfig")
Pattern patternVariantName = Pattern.compile(variantNameRegex);
tasks.whenTaskAdded { task ->
    //TODO disables lint
    if (task.name.startsWith("lint")) {
        println 'Disables lint task: '+task.name
        task.enabled = false
    }

    def m = patternVariantName.matcher(task.name)
    if (m.find()) {
        def variantName = m.group(1)
        def isRelease=false
        if (variantName.endsWith('Debug')) {
            variantName = variantName.substring(0, variantName.lastIndexOf('Debug'))
        } else if (variantName.endsWith('Release')) {
            variantName = variantName.substring(0, variantName.lastIndexOf('Release'))
            isRelease=true;
        } else {
            return
        }

        def taskIncVerCode="increaseVersionCode$variantName"
        if(!project.hasProperty(taskIncVerCode)) {
            project.task(taskIncVerCode) << {
                def manifestFile = file("src/$variantName/AndroidManifest.xml")
                def pattern = Pattern.compile("versionCode=\"(\\d+)\"")
                def manifestText = manifestFile.getText()
                def matcher = pattern.matcher(manifestText)
                matcher.find()
                def versionCode = Integer.parseInt(matcher.group(1))
                def manifestContent = matcher.replaceAll("versionCode=\"" + ++versionCode + "\"")
                manifestFile.write(manifestContent)
            }
        }
        task.dependsOn taskIncVerCode

        if(isRelease) {
            def taskIncVerName="increaseVersionName$variantName"
            if(!project.hasProperty(taskIncVerName)) {
                project.task(taskIncVerName) << {
                    def manifestFile = file("src/$variantName/AndroidManifest.xml")
                    def patternVersionNumber = Pattern.compile("versionName=\"(\\d+)\\.(\\d+)\\.(\\d+)\"")
                    def manifestText = manifestFile.getText()
                    def matcherVersionNumber = patternVersionNumber.matcher(manifestText)
                    matcherVersionNumber.find()
                    def majorVersion = Integer.parseInt(matcherVersionNumber.group(1))
                    def minorVersion = Integer.parseInt(matcherVersionNumber.group(2))
                    def pointVersion = Integer.parseInt(matcherVersionNumber.group(3))
                    def mNextVersionName = majorVersion + "." + minorVersion + "." + (pointVersion + 1)
                    def manifestContent = matcherVersionNumber.replaceAll("versionName=\"" + mNextVersionName + "\"")
                    manifestFile.write(manifestContent)
                }
            }
            task.dependsOn taskIncVerName
        }
    }
}

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    compile 'com.android.support:support-v4:21.0.0'
    compile files('libs/eventbus.jar')
    compile files('libs/libGoogleAnalyticsServices.jar')
    compile files('libs/trove-3.0.3.jar')
}

Here's the Gradle console output, generated by executing Run twice without any gralde/code modifications:

Executing tasks: [:ptp_app_base:assembleCustomer_6DebugEx]

Parallel execution with configuration on demand is an incubating feature.
Debug build expiry date=Mon Mar 16 10:39:02 CST 2015
Disables lint task: lintVitalCustomer_1Release
Disables lint task: lintVitalCustomer_2Release
Disables lint task: lintVitalDefaultRelease
Disables lint task: lintVitalCustomer_3Release
Disables lint task: lintVitalCustomer_4Release
Disables lint task: lintVitalCustomer_5Release
Disables lint task: lintVitalCustomer_6Release
Disables lint task: lintVitalCustomer_7Release
Disables lint task: lint
Disables lint task: lintCustomer_1DebugEx
Disables lint task: lintCustomer_1Debug
Disables lint task: lintCustomer_1Release
Disables lint task: lintCustomer_2DebugEx
Disables lint task: lintCustomer_2Debug
Disables lint task: lintCustomer_2Release
Disables lint task: lintDefaultDebugEx
Disables lint task: lintDefaultDebug
Disables lint task: lintDefaultRelease
Disables lint task: lintCustomer_3DebugEx
Disables lint task: lintCustomer_3Debug
Disables lint task: lintCustomer_3Release
Disables lint task: lintCustomer_4DebugEx
Disables lint task: lintCustomer_4Debug
Disables lint task: lintCustomer_4Release
Disables lint task: lintCustomer_5DebugEx
Disables lint task: lintCustomer_5Debug
Disables lint task: lintCustomer_5Release
Disables lint task: lintCustomer_6DebugEx
Disables lint task: lintCustomer_6Debug
Disables lint task: lintCustomer_6Release
Disables lint task: lintCustomer_7DebugEx
Disables lint task: lintCustomer_7Debug
Disables lint task: lintCustomer_7Release
ptp_app_base-Customer_1-debugEx.apk --> MY_APP_Customer_1_debugEx_.apk
ptp_app_base-Customer_1-debug.apk --> MY_APP_Customer_1_20141216.apk
ptp_app_base-Customer_1-release.apk --> MY_APP_Customer_1_20141216_SIGNED.apk
ptp_app_base-Customer_2-debugEx.apk --> MY_APP_Customer_2_debugEx_.apk
ptp_app_base-Customer_2-debug.apk --> MY_APP_Customer_2_20141216.apk
ptp_app_base-Customer_2-release.apk --> MY_APP_Customer_2_20141216_SIGNED.apk
ptp_app_base-default-debugEx.apk --> MY_APP_default_debugEx_.apk
ptp_app_base-default-debug.apk --> MY_APP_default_20141216.apk
ptp_app_base-default-release.apk --> MY_APP_default_20141216_SIGNED.apk
ptp_app_base-Customer_3-debugEx.apk --> MY_APP_Customer_3_debugEx_.apk
ptp_app_base-Customer_3-debug.apk --> MY_APP_Customer_3_20141216.apk
ptp_app_base-Customer_3-release.apk --> MY_APP_Customer_3_20141216_SIGNED.apk
ptp_app_base-Customer_4-debugEx.apk --> MY_APP_Customer_4_debugEx_.apk
ptp_app_base-Customer_4-debug.apk --> MY_APP_Customer_4_20141216.apk
ptp_app_base-Customer_4-release.apk --> MY_APP_Customer_4_20141216_SIGNED.apk
ptp_app_base-i3-debugEx.apk --> MY_APP_i3_debugEx_.apk
ptp_app_base-i3-debug.apk --> MY_APP_i3_20141216.apk
ptp_app_base-i3-release.apk --> MY_APP_i3_20141216_SIGNED.apk
ptp_app_base-i5-debugEx.apk --> MY_APP_i5_debugEx_.apk
ptp_app_base-i5-debug.apk --> MY_APP_i5_20141216.apk
ptp_app_base-i5-release.apk --> MY_APP_i5_20141216_SIGNED.apk
ptp_app_base-Customer_7-debugEx.apk --> MY_APP_Customer_7_debugEx_.apk
ptp_app_base-Customer_7-debug.apk --> MY_APP_Customer_7_20141216.apk
ptp_app_base-Customer_7-release.apk --> MY_APP_Customer_7_20141216_SIGNED.apk
:ptp_app_base:preBuild
:ptp_app_base:compileCustomer_6DebugExNdk UP-TO-DATE
:ptp_app_base:preCustomer_6DebugExBuild
:ptp_app_base:checkCustomer_6DebugExManifest
:ptp_app_base:preCustomer_4DebugBuild
:ptp_app_base:preCustomer_4DebugExBuild
:ptp_app_base:preCustomer_4ReleaseBuild
:ptp_app_base:preCustomer_5DebugBuild
:ptp_app_base:preCustomer_5DebugExBuild
:ptp_app_base:preCustomer_5ReleaseBuild
:ptp_app_base:preCustomer_6DebugBuild
:ptp_app_base:preCustomer_6ReleaseBuild
:ptp_app_base:preDefaultDebugBuild
:ptp_app_base:preDefaultDebugExBuild
:ptp_app_base:preDefaultReleaseBuild
:ptp_app_base:preCustomer_3DebugBuild
:ptp_app_base:preCustomer_3DebugExBuild
:ptp_app_base:preCustomer_3ReleaseBuild
:ptp_app_base:preCustomer_7DebugBuild
:ptp_app_base:preCustomer_7DebugExBuild
:ptp_app_base:preCustomer_7ReleaseBuild
:ptp_app_base:preCustomer_1DebugBuild
:ptp_app_base:preCustomer_1DebugExBuild
:ptp_app_base:preCustomer_1ReleaseBuild
:ptp_app_base:preCustomer_2DebugBuild
:ptp_app_base:preCustomer_2DebugExBuild
:ptp_app_base:preCustomer_2ReleaseBuild
:ptp_app_base:prepareComAndroidSupportSupportV42100Library UP-TO-DATE
:ptp_app_base:prepareCustomer_6DebugExDependencies
:ptp_app_base:compileCustomer_6DebugExAidl UP-TO-DATE
:ptp_app_base:compileCustomer_6DebugExRenderscript UP-TO-DATE
:ptp_app_base:generateCustomer_6DebugExBuildConfig
:ptp_app_base:generateCustomer_6DebugExAssets UP-TO-DATE
:ptp_app_base:mergeCustomer_6DebugExAssets UP-TO-DATE
:ptp_app_base:generateCustomer_6DebugExResValues UP-TO-DATE
:ptp_app_base:generateCustomer_6DebugExResources UP-TO-DATE
:ptp_app_base:mergeCustomer_6DebugExResources UP-TO-DATE
:ptp_app_base:processCustomer_6DebugExManifest UP-TO-DATE
:ptp_app_base:processCustomer_6DebugExResources UP-TO-DATE
:ptp_app_base:generateCustomer_6DebugExSources
:ptp_app_base:compileCustomer_6DebugExJava
Note: Some input files use or override a deprecated API.
Note: Recompile with -Xlint:deprecation for details.
Note: Some input files use unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

:ptp_app_base:preDexCustomer_6DebugEx UP-TO-DATE
:ptp_app_base:dexCustomer_6DebugEx
:ptp_app_base:processCustomer_6DebugExJavaRes UP-TO-DATE
:ptp_app_base:validateDebugSigning
:ptp_app_base:packageCustomer_6DebugEx
:ptp_app_base:zipalignCustomer_6DebugEx
:ptp_app_base:assembleCustomer_6DebugEx

BUILD SUCCESSFUL

Total time: 30.303 secs

The current build script is probably not the most performant, so both tips on how to skip rebuild or speeding up rebuild would be appreciated.


Edit 2: I noticed most of the gradle build time is spent on:

  1. compile[app]Java
  2. dex[app]
  3. package[app]

These steps appear to run despite the fact that nothing has changed since the last build.


Edit 3: Original title was "How to skip Gradle build when running/debugging with Android Studio", changed to better reflect the symptom of the issue and the remedy.

like image 219
Kai Avatar asked Dec 15 '14 13:12

Kai


1 Answers

The cause is stupidity on my part:

I set a BuildConfig field to be the current time in milliseconds, which caused the resulting BuildConfig.java to be different every time Gradle runs, causing the whole compiling/dexing/packaging phases to run.

Edit:

For me the problem was caused by executing script similar to this:

productFlavors {
    all {
        buildConfigField 'long', 'TIME_LIMIT', System.currentTimeMillis() + 'l'
    }
    ...
}

Since System.currentTimeMillis() would be different every time, it means every time Gradle runs it will find the source code has changed, therefore triggering a cascade of actions. It is solved by changing the script to:

def time = Calendar.getInstance()
time.set(Calendar.HOUR, 1);
time.set(Calendar.MINUTE, 1);
time.set(Calendar.SECOND, 1);
time.set(Calendar.MILLISECOND, 1);
productFlavors {
    // default BuildConfig variables
    all {
        buildConfigField 'long', 'TIME_LIMIT', time.getTimeInMillis() + 'l'
    }
    ...
}

The above script means the exact same time is produced when run in the same day. therefore if nothing else was changed then the previous generated APK will be reused with no recompilation necessary.

like image 127
Kai Avatar answered Nov 11 '22 04:11

Kai