Android 应用从 Google Play 下载时崩溃,但在本地开发中运行良好 - 为什么?

Android app crashes when downloaded from Google Play but works great in local dev - why?

我不确定是否有人可以对此提供帮助,但这确实很奇怪。我构建了一个 android 应用程序并在本地进行了测试,一切正常。然后,我将它发布到应用商店,它一直崩溃。

google 播放的堆栈跟踪显示:

java.lang.RuntimeException: An error occured while executing doInBackground()
    at android.os.AsyncTask.done(AsyncTask.java:300)
    at java.util.concurrent.FutureTask.finishCompletion(FutureTask.java:355)
    at java.util.concurrent.FutureTask.setException(FutureTask.java:222)
    at java.util.concurrent.FutureTask.run(FutureTask.java:242)
    at android.os.AsyncTask$SerialExecutor.run(AsyncTask.java:231)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:587)
    at java.lang.Thread.run(Thread.java:818)
Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'int java.lang.String.length()' on a null object reference
    at libcore.net.UriCodec.encode(UriCodec.java:132)
    at java.net.URLEncoder.encode(URLEncoder.java:57)
    at com.xxxx.yyyy.bbbb.SignedRequestsHelper.percentEncodeRfc3986(SignedRequestsHelper.java:120)
    at com.xxxx.yyyy.bbbb.SignedRequestsHelper.sign(SignedRequestsHelper.java:63)
    at com.xxxx.yyyy.SearchFragment$SearchAsyncTask.doInBackground(SearchFragment.java:385)
    at com.xxxx.yyyy.SearchFragment$SearchAsyncTask.doInBackground(SearchFragment.java:338)
    at android.os.AsyncTask.call(AsyncTask.java:288)
    at java.util.concurrent.FutureTask.run(FutureTask.java:237)
    ... 4 more

看来我的 SignedRequestsHelper.percentEncodeRfc3986() 方法有问题。一个字符串是 null。但请注意,在开发应用程序并在不同设备上对其进行测试时,不会发生此错误。只是上传到google play,然后从那里下载,才出现这个错误。

所以我查看了我的 SignedRequestHelper class,当时看起来像这样:

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Iterator;
import java.util.Map;
import java.util.SortedMap;
import java.util.TimeZone;
import java.util.TreeMap;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Base64;

public class SignedRequestsHelper {
    private static final String UTF8_CHARSET = "UTF-8";
    private static final String HMAC_SHA256_ALGORITHM = "HmacSHA256";
    private static final String REQUEST_URI = "/onca/xml";
    private static final String REQUEST_METHOD = "GET";

    // use xml-uk.amznxslt.com for xslt requests, or ecs.amazonaws.co.uk for others
    private String endpoint = "webservices.amazon.com"; // must be lowercase

    // change this so reads from properties file
    private String awsAccessKeyId = "xxxx";
    private String awsSecretKey = "xxx";

    private SecretKeySpec secretKeySpec = null;
    private Mac mac = null;

    public SignedRequestsHelper() throws UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeyException {
        byte[] secretyKeyBytes = awsSecretKey.getBytes(UTF8_CHARSET);
        secretKeySpec =
                new SecretKeySpec(secretyKeyBytes, HMAC_SHA256_ALGORITHM);
        mac = Mac.getInstance(HMAC_SHA256_ALGORITHM);
        mac.init(secretKeySpec);
    }

    public String sign(Map<String, String> params) {
        params.put("AWSAccessKeyId", awsAccessKeyId);
        params.put("Timestamp", timestamp());

        SortedMap<String, String> sortedParamMap =
                new TreeMap<String, String>(params);
        String canonicalQS = canonicalize(sortedParamMap);
        String toSign =
                REQUEST_METHOD + "\n"
                        + endpoint + "\n"
                        + REQUEST_URI + "\n"
                        + canonicalQS;

        String hmac = hmac(toSign);
        String sig = percentEncodeRfc3986(hmac);
        String url = "http://" + endpoint + REQUEST_URI + "?" +
                canonicalQS + "&Signature=" + sig;

        return url;
    }

    private String hmac(String stringToSign) {
        String signature = null;
        byte[] data;
        byte[] rawHmac;
        try {
            data = stringToSign.getBytes(UTF8_CHARSET);
            rawHmac = mac.doFinal(data);
            Base64 encoder = new Base64();
            signature = new String(encoder.encode(rawHmac));
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(UTF8_CHARSET + " is unsupported!", e);
        }
        return signature;
    }

    private String timestamp() {
        String timestamp = null;
        Calendar cal = Calendar.getInstance();
        DateFormat dfm = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
        dfm.setTimeZone(TimeZone.getTimeZone("GMT"));
        timestamp = dfm.format(cal.getTime());
        return timestamp;
    }

    private String canonicalize(SortedMap<String, String> sortedParamMap)
    {
        if (sortedParamMap.isEmpty()) {
            return "";
        }

        StringBuffer buffer = new StringBuffer();
        Iterator<Map.Entry<String, String>> iter =
                sortedParamMap.entrySet().iterator();

        while (iter.hasNext()) {
            Map.Entry<String, String> kvpair = iter.next();
            buffer.append(percentEncodeRfc3986(kvpair.getKey()));
            buffer.append("=");
            buffer.append(percentEncodeRfc3986(kvpair.getValue()));
            if (iter.hasNext()) {
                buffer.append("&");
            }
        }
        String cannoical = buffer.toString();
        return cannoical;
    }

    private String percentEncodeRfc3986(String s) {
        String out;
        try {
            out = URLEncoder.encode(s, UTF8_CHARSET)
                    .replace("+", "%20")
                    .replace("*", "%2A")
                    .replace("%7E", "~");
        } catch (UnsupportedEncodingException e) {
            out = s;
        }
        return out;
    }
}

并且我将 percentEncodeRfc3986() 方法更改为如下所示。只需删除最后两个替换标签:

private String percentEncodeRfc3986(String s) {
        String out;
        try {
            out = URLEncoder.encode(s, UTF8_CHARSET)
                    .replace("+", "%20");
        } catch (UnsupportedEncodingException e) {
            out = s;
        }
        return out;
    }

然后在本地测试了一下:可以。然后发布到google播放并下载:有效。

所以一切都很好,但我仍然在摸不着头脑,想知道为什么现在可以正常工作

这也让我很紧张,因为 在我的模拟器和测试设备上完美运行的代码在应用程序上传到 google play 时突然无法运行.这似乎是一件微不足道的事情......一些额外的替换导致了这个错误。

有人可以帮助我了解这里发生的事情吗? 或者至少为什么有时发布到 google play 的应用程序上的代码可能无法正常工作或执行本地开发人员?

编辑: 我的 build.gradle 文件:

android {
    compileSdkVersion 22
    buildToolsVersion "22.0.1"

    defaultConfig {
        applicationId "com.xxx.yyy"
        minSdkVersion 10
        targetSdkVersion 22
        versionCode 8
        versionName "1.2"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

好的,您缺少签名证书,只有发布版本才需要,这就是为什么您在调试时没有遇到它的原因。

要签署您的 Android 应用程序,您必须生成一个密钥库并将其存储在您的应用程序中。然后,您必须在 build.gradle 文件中再添加几行代码,告诉它您的密钥库的名称及其密码。最终结果将如下所示:

SigningConfigs {
        release {
            storeFile file("../keystore.jks")
            storePassword "7uSFX****"
            keyAlias "app"
            keyPassword "GQ****"
        }
    }

然后在您的 buildTypes 中指定这行代码将正确地签署您的应用程序。

signingConfig signingConfigs.release

请注意,最好还包含一个调试 buildType,这样当您调试您的应用程序时,您不必对其进行签名。像这样的东西(注意它丢失了 signingConfig signingConfigs.release):

debug {
            minifyEnabled false
            applicationIdSuffix ".debug"
            versionNameSuffix ".debug"
}

请按照指南为您的应用生成密钥库,这相对容易。我将在稍后编辑此 post,详细说明如何操作。

http://developer.android.com/tools/publishing/app-signing.html

经过一些测试后,我确定出现该错误的原因是因为我在最初发布我的应用程序时没有在 Android Studio 中更新我的 SDK 和其他更新。因为在我更新之后,我将我的代码恢复到它崩溃时的状态并且一切正常 - 这是有道理的,因为我真的完全没有理由让我的代码无法工作。

所以,是的,这里的教训是确保您所有的 SDK 工具和 Android Studio 在签署您的应用程序的发布版本之前都是最新的!