Android Samsung Lollipop 设备上的应用程序崩溃 NoClassDefFoundError

Android app crash NoClassDefFoundError on Samsung Lollipop devices

我们最近将应用程序的 minSdkVersion 从 16 (Jellybean) 提高到了 21 (Lollipop)。尽管我们主要使用调试版本对我们的应用程序进行了广泛的测试,但我们现在在应用程序启动时面临大量的生产崩溃,主要是在旧的三星设备上——(Note3 和 S4 是最崩溃的)并且总是在 Lollipop 上。

错误是

Fatal Exception: java.lang.NoClassDefFoundError: com.retailconvergence.ruelala.delegate.GoogleLoginDelegate
   at com.retailconvergence.ruelala.delegate.LifecycleDelegateManager.addDelegateOfType(LifecycleDelegateManager.java:48)
   at com.retailconvergence.ruelala.extensions.activity.LifecycleDelegateActivity.addDelegateOfType(LifecycleDelegateActivity.java:55)
   at com.retailconvergence.ruelala.activity.SplashActivity.setupDelegates(SplashActivity.java:198)
   at com.retailconvergence.ruelala.activity.SplashActivity.onCreate(SplashActivity.java:60)
   at android.app.Activity.performCreate(Activity.java:6288)
   at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1119)
   at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2646)
   at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2758)
   at android.app.ActivityThread.access0(ActivityThread.java:177)
   at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1448)
   at android.os.Handler.dispatchMessage(Handler.java:102)
   at android.os.Looper.loop(Looper.java:145)
   at android.app.ActivityThread.main(ActivityThread.java:5942)
   at java.lang.reflect.Method.invoke(Method.java)
   at java.lang.reflect.Method.invoke(Method.java:372)
   at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1399)
   at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1194)

SplashActivity 是应用程序的初始启动 activity。 class not found 是一个历史悠久的 class 不是新引入的东西。作为旁注,作为这个最新版本的一部分,我们升级到 Android Studio 3 并引入了 Kotlin 代码,但我认为这些与问题无关。我们没有在构建中使用混淆器。

我知道当 minSdkVersion 为 21 及以上时,构建会发生重大变化,这与使用 ART 而不是 Dalvik 有关,所以我想知道 Samsung Lollipop 设备是否仍然存在缺陷现在在主 dex 文件中寻找 class?

模块级build.gradle:

import java.text.SimpleDateFormat
import java.util.concurrent.TimeUnit

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'io.fabric'
apply plugin: 'spoon'

// Manifest version information
def versionMajor = 4
def versionMinor = 2
def versionPatch = 0
def versionBuild = 0  // bump for dogfood builds, public betas, etc.
ext.versionReleaseDate="OCT-13-2017"    // UPDATE THIS WHEN YOU BUMP THE VERSIONS ABOVE FOR A NEW RELEASE MMM-dd-yyy


repositories {
    mavenCentral()
    maven { url 'https://maven.fabric.io/public' }
    maven { url 'http://salesforce-marketingcloud.github.io/JB4A-SDK-Android/repository' }
    maven { url "https://maven.google.com" }
    maven { url "http://maven.tealiumiq.com/android/releases/" }
}

def getCountOfHoursSinceVersionUpdate() {
    def currentDate = new Date()
    def format = new SimpleDateFormat("MMM-dd-yyyy")
    def buildDate = (Date)format.parse(versionReleaseDate)
    return (Integer)((currentDate.getTime() - buildDate.getTime()) / TimeUnit.HOURS.toMillis(1))
}

android {
    compileSdkVersion 26
    buildToolsVersion '26.0.1'

    defaultConfig {
        targetSdkVersion 25

        /**
         * Increment versionCode by commit count
         */

        versionCode       versionMajor * 100000 + versionMinor * 10000 + versionPatch * 1000 + versionBuild + getCountOfHoursSinceVersionUpdate()


        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

        // Enabling multidex support. :(
        multiDexEnabled true

        def manifestPath = project(':').file('app/src/androidTest/AndroidManifest.xml')
        buildConfigField "String", "MANIFEST_PATH", "\"" + manifestPath + "\""

        def resPath = project(':').file('app/src/main/res/')
        buildConfigField "String", "RES_PATH", "\"" + resPath + "\""

        def assetPath = project(':').file('app/src/prod/assets/')
        buildConfigField "String", "ASSET_PATH", "\"" + assetPath + "\""

    }

    dexOptions {
        javaMaxHeapSize "8g"
        dexInProcess true // the magic line
    }


    flavorDimensions "debugDimension"

    /**
     * productFlavors override defaultConfig properties as well as force gradle to look in the new
     * folders that we have created to differentiate the build assets and manifests.
     * src/dev, src/prod
     */
    productFlavors {
        dev    {
            minSdkVersion 21
            applicationId "com.retailconvergence.ruelala.dev"
            versionName "${versionMajor}.${versionMinor}.0${versionPatch}"
            manifestPlaceholders = [optimizelyId: "optly4740131949"]
            dimension "debugDimension"
        }

        prod {
            minSdkVersion 21
            applicationId "com.retailconvergence.ruelala"
            versionName "${versionMajor}.${versionMinor}.${versionPatch}"
            manifestPlaceholders = [optimizelyId: "optly4752051515"]
            dimension "debugDimension"
        }
    }

    signingConfigs {
        prod {
            //the key is up a level, don't include in the modules
            storeFile file("../RueLaLaKeystore")
            storePassword "Boutiques"
            keyAlias "rue la la"
            keyPassword "Boutiques"
        }
    }

    buildTypes {
        release {
            signingConfig signingConfigs.prod


            ext.betaDistributionReleaseNotesFilePath = 'release_notes.txt'
            ext.betaDistributionGroupAliases = 'AndroidTesters'
            ext.betaDistributionNotifications = true
        }

        debug {
            versionNameSuffix '-dev'

            signingConfig signingConfigs.prod
            // to get coverage report, set testCoverageEnabled to true and run gradle task called createDevelopmentDebugAndroidTestCoverageReport
            // Note that test coverage doesn't seem to work on Samsung devices, other brand or emulator should work though
            testCoverageEnabled = false

            ext.betaDistributionReleaseNotesFilePath = 'release_notes.txt'
            ext.betaDistributionGroupAliases = 'AndroidTesters'
            ext.betaDistributionNotifications = true

        }
    }

    packagingOptions {
        exclude 'META-INF/LICENSE.txt'
        exclude 'META-INF/NOTICE.txt'
        exclude 'META-INF/LICENSE'
        exclude 'META-INF/NOTICE'
        exclude 'LICENSE.txt'
        exclude 'LICENSE'
        exclude 'READ.ME'
        exclude 'README'
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

configurations {
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])

    //include our modules
    compile project(':core')
    compile project(':data')

    //android
    final APP_COMPAT_VERSION = '26.1.0'

    compile "com.android.support:appcompat-v7:$APP_COMPAT_VERSION"
    compile "com.android.support:recyclerview-v7:$APP_COMPAT_VERSION"
    compile "com.android.support:design:$APP_COMPAT_VERSION"
    compile "com.android.support:multidex:1.0.0"
    compile "com.android.support:cardview-v7:$APP_COMPAT_VERSION"

    // google
    final PLAY_SERVICES_VERSION = '10.2.4'
    compile "com.google.android.gms:play-services-wallet:$PLAY_SERVICES_VERSION"
    compile "com.google.android.gms:play-services-location:$PLAY_SERVICES_VERSION"
    compile "com.google.android.gms:play-services-gcm:$PLAY_SERVICES_VERSION"
    compile "com.google.android.gms:play-services-plus:$PLAY_SERVICES_VERSION"
    compile "com.google.android.gms:play-services-identity:$PLAY_SERVICES_VERSION"
    compile "com.google.android.gms:play-services-analytics:$PLAY_SERVICES_VERSION"
    compile "com.google.android.gms:play-services-auth:$PLAY_SERVICES_VERSION"
    compile "com.google.android.gms:play-services-maps:$PLAY_SERVICES_VERSION"

    // facebook

    compile 'com.facebook.android:facebook-android-sdk:4.8.+'
    compile 'com.facebook.stetho:stetho:1.1.0'

    //markdown4j
    compile 'org.commonjava.googlecode.markdown4j:markdown4j:2.2-cj-1.0'

    //crashlytics
    compile('com.crashlytics.sdk.android:crashlytics:2.5.2@aar') {
        transitive = true;
    }

    //image zoom
    compile 'com.github.chrisbanes.photoview:library:1.2.3'

    //square
    compile 'com.squareup.picasso:picasso:2.5.2'
    compile 'com.makeramen:roundedimageview:2.2.1'


    debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.1'
    releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'
    testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'

    compile 'com.jakewharton:butterknife:8.6.0'
    annotationProcessor 'com.jakewharton:butterknife-compiler:8.6.0'

    // optimizely
    compile('com.optimizely:optimizely:1.4.2@aar') {
        transitive = true
    }

    //braintree
    compile 'com.braintreepayments.api:braintree:2.6.0'
    compile 'com.braintreepayments.api:data-collector:2.+'

    // guava
    compile 'com.google.guava:guava:19.0'

    // sticky headers
    compile 'com.github.mtotschnig:StickyListHeaders:2.7.1'

    // expandable recyclerview
    compile 'eu.davidea:flexible-adapter:5.0.0-rc2'

    //recyclerview animations
    compile 'jp.wasabeef:recyclerview-animators:2.2.3'

    // tooltip
    compile 'com.github.michaelye.easydialog:easydialog:1.4'

    // tealium
    compile 'com.tealium:library:5.3.0'

    // circle indicator
    compile 'me.relex:circleindicator:1.2.2@aar'

    //testing
    final HAMCREST_VERSION = '1.3'

    def jUnit = "junit:junit:4.12"

    // ExactTarget SDK
    compile ('com.salesforce.marketingcloud:marketingcloudsdk:5.0.5') {
        exclude module: 'android-beacon-library' //remove to use Proximity messaging
        exclude module: 'play-services-location' //remove to use Geofence or Proximity messaging
    }

    androidTestCompile jUnit

    // Unit tests dependencies
    testCompile jUnit
    testCompile "org.hamcrest:hamcrest-core:$HAMCREST_VERSION"
    testCompile "org.hamcrest:hamcrest-library:$HAMCREST_VERSION"
    testCompile "org.hamcrest:hamcrest-integration:$HAMCREST_VERSION"
    testCompile 'org.robolectric:robolectric:3.1'
    testCompile 'org.mockito:mockito-core:1.+'
    testCompile 'com.google.guava:guava:19.0'
    testCompile("com.android.support:support-v4:$APP_COMPAT_VERSION") {
        exclude module: 'support-annotations'
    }
    testCompile('org.powermock:powermock-api-mockito:1.6.4') {
        exclude module: 'objenesis'
    }
    testCompile('org.powermock:powermock-module-junit4:1.6.4') {
        exclude module: 'objenesis'
    }
    testCompile 'io.reactivex:rxandroid:1.0.1'
    testCompile 'io.reactivex:rxjava:1.1.0'

    // Espresso
    androidTestCompile('com.android.support.test:runner:0.5') {
        exclude module: 'support-annotations'
    }
    androidTestCompile('com.android.support.test:rules:0.5') {
        exclude module: 'support-annotations'
    }
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2') {
        exclude module: 'support-annotations'
    }
    androidTestCompile('com.android.support.test.espresso:espresso-intents:2.2.2') {
        exclude module: 'support-annotations'
    }
    androidTestCompile('com.android.support.test.espresso:espresso-web:2.2.2') {
        exclude module: 'support-annotations'
    }
    androidTestCompile('com.android.support.test.espresso:espresso-contrib:2.2.2') {
        exclude module: 'support-annotations'
        exclude module: 'recyclerview-v7'
        exclude module: 'appcompat-v7'
        exclude module: 'design'
        exclude module: 'support-v4'
    }

    // allows java 8 compile
    compile 'com.annimon:stream:1.1.2'

    // For taking screenshots
    androidTestCompile 'com.squareup.spoon:spoon-client:1.7.0'

    testCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.2'
    compile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.2'
    compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
}

apply plugin: 'com.google.gms.google-services'

spoon {
    noAnimations = true
    grantAllPermissions = true
}

apply plugin: 'devicefarm'

devicefarm {
    projectName "Rue Mobile"
    devicePool "Smoke Test Pool"
    useUnmeteredDevices()
    authentication {
        accessKey System.getenv("AWS_DEVICE_FARM_ACCESS_KEY")
        secretKey System.getenv("AWS_DEVICE_FARM_SECRET_KEY")
    }
}

解决此问题的方法是禁用预索引:

dexOptions {
    preDexLibraries false
}

在应用中 build.gradle。灵感来自这个 link 关于 Lollipop 上的 Picasso class not found 错误:see here

我不完全清楚为什么禁用 pre-dexing 可以解决问题,但我只能推断构建过程正在进行一些优化,影响 classes 在中的排序方式apk 的 dex 文件,然后影响这些三星棒棒糖设备上的应用程序安装。理论上 ART 应该处理所有这些,但显然 pre-dex 优化和一些 Lollipop 设备之间存在依赖关系。