12

我的应用程序运行良好,直到我在安装后第一次启动时中断初始化过程,只要初始化过程尚未完成,就退出并启动应用程序几次。处理逻辑和 AsyncTask 可以很好地处理这个问题,所以我没有得到任何不一致的地方,但是堆有问题。当我在应用程序设置中执行此令人不安的退出和启动时,它会越来越多,这将导致 OutOfMemory 错误。通过使用 MAT 分析堆,我已经发现了一个泄漏,但我仍然有另一个泄漏,我还无法隔离。
背景信息:我将应用程序上下文、列表和时间戳存储在一个静态类中,以便能够从应用程序中任何位置的类访问它,而无需使用构造函数传递的繁琐引用。无论如何,这个静态类(ApplicationContext)一定有问题,因为它会由于区域列表而导致内存泄漏。区域对象是经过处理的 GeoJSON 数据。这是这个类的样子:

public class ApplicationContext extends Application {
    private static Context context;
    private static String timestamp;
    private static List<Zone> zones = new ArrayList<Zone>();

    public void onCreate()  {
        super.onCreate();
        ApplicationContext.context = getApplicationContext();
    }

    public static Context getAppContext() {
        return ApplicationContext.context;
    }

    public static List<Zone> getZones() {
        return zones;
    }

    public static void setData(String timestamp, List<Zone> zones) {
        ApplicationContext.timestamp = timestamp;
        ApplicationContext.zones = zones;
    }

    public static String getTimestamp() {
        return timestamp;
    }
}

我已经尝试像这样存储区域

ApplicationContext.zones = new ArrayList(zones);

但它没有效果。我已经尝试将 zone 属性放入另一个静态类,因为 ApplicationContext 在所有其他类之前加载(由于 AndroidManifest 中的条目),这可能导致这种行为,但这也不是问题。

setData 在我的“ProcessController”中被调用了两次。一次在 doUpdateFromStorage 中,一次在 doUpdateFromUrl(String) 中。这个类看起来像这样:

public final class ProcessController {
    private HttpClient httpClient = new HttpClient();

    public final InitializationResult initializeData()  {
        String urlTimestamp;
        try {
            urlTimestamp = getTimestampDataFromUrl();

            if (isModelEmpty())  {
                if (storageFilesExist())  {
                    try {
                        String localTimestamp = getLocalTimestamp();

                        if (isStorageDataUpToDate(localTimestamp, urlTimestamp))  {
                            return doDataUpdateFromStorage();
                        } 
                        else  {
                            return doDataUpdateFromUrl(urlTimestamp);
                        }
                    } 
                    catch (IOException e) {
                        return new InitializationResult(false, Errors.cannotReadTimestampFile());
                    }
                }
                else  {
                    try {
                        createNewFiles();

                        return doDataUpdateFromUrl(urlTimestamp);
                    } 
                    catch (IOException e) {
                        return new InitializationResult(false, Errors.fileCreationFailed());
                    }
                }
            }
            else  {
                if (isApplicationContextDataUpToDate(urlTimestamp))  {
                    return new InitializationResult(true, "");  
                }
                else  {
                    return doDataUpdateFromUrl(urlTimestamp);
                }
            }
        } 
        catch (IOException e1) {
            return new InitializationResult(false, Errors.noTimestampConnection());
        }
    }

    private String getTimestampDataFromUrl() throws IOException {
        if (ProcessNotification.isCancelled()) {
            throw new InterruptedIOException();
        }

        return httpClient.getDataFromUrl(FileType.TIMESTAMP);
    }

    private String getJsonDataFromUrl() throws IOException {
        if (ProcessNotification.isCancelled()) {
            throw new InterruptedIOException();
        }

        return httpClient.getDataFromUrl(FileType.JSONDATA);
    }

    private String getLocalTimestamp() throws IOException {
        if (ProcessNotification.isCancelled()) {
            throw new InterruptedIOException();
        }

        return PersistenceManager.getFileData(FileType.TIMESTAMP);
    }

    private List<Zone> getLocalJsonData() throws IOException, ParseException {
        if (ProcessNotification.isCancelled()) {
            throw new InterruptedIOException();
        }

        return JsonStringParser.parse(PersistenceManager.getFileData(FileType.JSONDATA));
    }

    private InitializationResult doDataUpdateFromStorage() throws InterruptedIOException {
        if (ProcessNotification.isCancelled()) {
            throw new InterruptedIOException();
        }

        try {
            ApplicationContext.setData(getLocalTimestamp(), getLocalJsonData());

            return new InitializationResult(true, "");
        } 
        catch (IOException e) {
            return new InitializationResult(false, Errors.cannotReadJsonFile());
        } 
        catch (ParseException e) {
            return new InitializationResult(false, Errors.parseError());
        }
    }

    private InitializationResult doDataUpdateFromUrl(String urlTimestamp) throws InterruptedIOException {
        if (ProcessNotification.isCancelled()) {
            throw new InterruptedIOException();
        }

        String jsonData;
        List<Zone> zones;
        try {
            jsonData = getJsonDataFromUrl();
            zones = JsonStringParser.parse(jsonData);

            try {
                PersistenceManager.persist(jsonData, FileType.JSONDATA);
                PersistenceManager.persist(urlTimestamp, FileType.TIMESTAMP);

                ApplicationContext.setData(urlTimestamp, zones);

                return new InitializationResult(true, "");
            } 
            catch (IOException e) {
                return new InitializationResult(false, Errors.filePersistError());
            }
        } 
        catch (IOException e) {
            return new InitializationResult(false, Errors.noJsonConnection());
        } 
        catch (ParseException e) {
            return new InitializationResult(false, Errors.parseError());
        }
    }

    private boolean isModelEmpty()  {
        if (ApplicationContext.getZones() == null || ApplicationContext.getZones().isEmpty())  {    
            return true;
        }

        return false;
    }

    private boolean isApplicationContextDataUpToDate(String urlTimestamp) { 
        if (ApplicationContext.getTimestamp() == null)  {
            return false;
        }

        String localTimestamp = ApplicationContext.getTimestamp();

        if (!localTimestamp.equals(urlTimestamp))  {
            return false;
        }

        return true;
    }

    private boolean isStorageDataUpToDate(String localTimestamp, String urlTimestamp) { 
        if (localTimestamp.equals(urlTimestamp))  {
            return true;
        }

        return false;
    }

    private boolean storageFilesExist()  {
        return PersistenceManager.filesExist();
    }

    private void createNewFiles() throws IOException {
        PersistenceManager.createNewFiles();
    }
}

也许这是另一个有用的信息,这个 ProcessController 是由我的 MainActivity 的 AsyncTask 在应用程序设置中调用的:

public class InitializationTask extends AsyncTask<Void, Void, InitializationResult> {
    private ProcessController processController = new ProcessController();
    private ProgressDialog progressDialog;
    private MainActivity mainActivity;
    private final String TAG = this.getClass().getSimpleName();

    public InitializationTask(MainActivity mainActivity) {
        this.mainActivity = mainActivity;
    }

    @Override
    protected void onPreExecute() {
        super.onPreExecute();

        ProcessNotification.setCancelled(false);

        progressDialog = new ProgressDialog(mainActivity);
        progressDialog.setMessage("Processing.\nPlease wait...");
        progressDialog.setIndeterminate(true); //means that the "loading amount" is not measured.
        progressDialog.setCancelable(true);
        progressDialog.show();
    };

    @Override
    protected InitializationResult doInBackground(Void... params) {
        return processController.initializeData();
    }

    @Override
    protected void onPostExecute(InitializationResult result) {
        super.onPostExecute(result);

        progressDialog.dismiss();

        if (result.isValid())  {
            mainActivity.finalizeSetup();
        }
        else  {
            AlertDialog.Builder dialog = new AlertDialog.Builder(mainActivity);
            dialog.setTitle("Error on initialization");
            dialog.setMessage(result.getReason());
            dialog.setPositiveButton("Ok",
                    new DialogInterface.OnClickListener() {

                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            dialog.cancel();

                            mainActivity.finish();
                        }
                    });

            dialog.show();
        }

        processController = null;
    }

    @Override
    protected void onCancelled() {
        super.onCancelled();

        Log.i(TAG, "onCancelled executed");
        Log.i(TAG, "set CancelNotification status to cancelled.");

        ProcessNotification.setCancelled(true);

        progressDialog.dismiss();

        try {
            Log.i(TAG, "clearing files");

            PersistenceManager.clearFiles();

            Log.i(TAG, "files cleared");
        } 
        catch (IOException e) {
            Log.e(TAG, "not able to clear files.");
        }

        processController = null;

        mainActivity.finish();
    }
}

这是 JSONParser 的主体。(更新:我将方法设置为非静态,但问题仍然存在。)我省略了 JSON 对象中的对象创建,因为我认为这不是错误:

public class JsonStringParser {
    private static String TAG = JsonStringParser.class.getSimpleName();

    public static synchronized List<Zone> parse(String jsonString) throws ParseException, InterruptedIOException {
        JSONParser jsonParser = new JSONParser();

        Log.i(TAG, "start parsing JSON String with length " + ((jsonString != null) ? jsonString.length() : "null"));
          List<Zone> zones = new ArrayList<Zone>();

        //does a lot of JSON parsing here

        Log.i(TAG, "finished parsing JSON String");

        jsonParser = null;

        return zones;
    }
}

这是显示问题的堆转储:

内存图

这是详细信息列表,表明此问题与数组列表有关。

细节

有什么想法吗?顺便说一句:我不知道其他泄漏是什么,因为没有详细信息。

可能很重要:此图显示了我一遍又一遍地不启动和停止应用程序时的状态。这是一个干净的开始的图表。但是当我多次启动和停止时,由于空间不足,可能会导致问题。

这是一个真实崩溃的图表。我在初始化时启动和停止了应用程序几次:

崩溃报告

[更新]
我通过不将 Android 上下文存储到我的 ApplicationContext 类并使 PersistenceManager 非静态来缩小范围。问题没有改变,所以我绝对确定它与我在全球存储 Android 上下文的事实无关。它仍然是上图中的“问题嫌疑人 1”。所以我必须对这个庞大的列表做点什么,但是什么?我已经尝试对其进行序列化,但取消序列化此列表所需的时间远远超过 20 秒,因此这不是一个选项。

现在我尝试了一些不同的东西。我踢出了整个 ApplicationContext 所以我没有任何静态引用了。我试图在 MainActivity 中保存 Zone 对象的 ArrayList。尽管我至少重构了使应用程序运行所需的部分,所以我什至没有将 Array 或 Activity 传递给我需要它的所有类,但我仍然以不同的方式遇到同样的问题,所以我的猜测是Zone 对象本身就是问题所在。或者我无法正确读取堆转储。请参阅下面的新图表。这是一个没有干扰的简单应用程序启动的结果。

[更新]
我得出的结论是没有内存泄漏,因为“内存是在一个实例中累积的”听起来不像是泄漏。问题是一遍又一遍地启动和停止会启动新的 AsyncTask,如一张图所示,因此解决方案是不启动新的 AsyncTask。我在 SO 上找到了一个可能的解决方案,但它还不适合我。

记忆错误 4 内存错误 5

4

5 回答 5

3

在您的 AsyncTask 中,您拥有对 Context 的引用:MainActivity。当您启动多个 AsyncTask 时,它们将由 ExecutorService 排队。因此,如果所有 AsyncTask 长时间运行,它们将是“活动的”(不是垃圾收集)。他们每个人都会在一个活动上保留一个参考。因此,您的所有活动也将保持活力。

这是一个真正的内存泄漏,因为 Android 将希望对不再显示的 Activity 进行垃圾收集。你的 AsyncTasks 会阻止这种情况。所有的活动都保存在记忆中。

我鼓励您尝试RoboSpice Motivations以了解有关此问题的更多信息。在这个应用程序中,我们解释了为什么不应该将 AsyncTasks 用于长时间运行的操作。仍然有一些解决方法可以让您使用它们,但它们很难实现。

解决此问题的一种方法是使用Wea​​kReference指向 AsyncTask 类中的活动。如果你小心使用它们,你可以避免你的活动不被垃圾收集。

实际上,RoboSpice是一个允许在服务内部执行网络请求的库。这种方法非常有趣,因为它将创建一个与您的活动无关的上下文(服务)。因此,您的请求可以根据需要进行,并且不会干扰 Android 的垃圾收集行为。

RoboSpice 有两个模块可用于处理 REST 请求。一个用于 Spring Android,另一个用于 Google Http Java Client。这两个库都将简化 JSON 解析。

于 2012-12-15T07:59:04.470 回答
3

首先,我必须同意 Emile:

“......繁琐的构造函数传递引用”有助于避免此类问题。老实说,以这种方式使用静态肯定是造成这样的内存泄漏的一种方法,尤其是在对您的上下文进行静态引用的情况下。

这也适用于代码中的所有其他static方法。static方法与全局函数并没有真正的不同。您正在那里构建一个充满static方法的大意大利面盘。尤其是当他们开始共享某些状态时,它迟早会崩溃或产生模糊的结果,这是通过适当的设计无法获得的,尤其是在 Android 等高度多线程的平台存在的情况下。

还引起我注意的是,请注意,在完成之前不会调用的onCancelled方法。因此,您的全局取消标志 ( ) 或多或少毫无价值(如果仅在显示的代码段落中使用)。AsyncTaskdoInBackgroundProcessNotification.isCancelled()

同样从您发布的记忆图像中,该zones列表“仅”包含 31 个项目。它应该持有多少?它增加了多少?如果它真的增加了,罪魁祸首可能在JsonStringParser.parse方法中,这又是static. 如果它在某个缓存中保存了项目列表并且控制逻辑无法正常工作(例如,在存在多个线程同时访问它的情况下),它可能会在每次调用时将项目添加到该缓存中。

  • 猜想一:由于解析方式是static,所以当应用程序关闭时,这些数据没有(必要)被清理。statics 被初始化一次,在这种情况下,在(物理 vm-)进程停止之前永远不会取消初始化。然而,即使应用程序停止,Android 也不保证进程会被终止(请参阅此处的精彩解释)。static因此,您可能会在(可能是解析)代码的某些部分中积累一些数据。
  • 猜测 2:由于您要多次重新启动应用程序,因此后台线程会并行运行多次(假设:每次重新启动应用程序时都会产生一个新线程。请注意,您的代码对此没有任何防范措施。)第一次解析仍在运行,由于全局zones变量仍然没有值,因此启动了另一个解析。全局函数parse可能不是线程安全的,并且将多个数据多次放入最终返回的列表中,从而产生越来越大的列表。同样,这通常可以通过没有static方法来避免(并注意多线程)。

(代码不完整,因此猜测,甚至可能还有其他东西潜伏在那里。)

于 2012-12-11T19:05:44.497 回答
2

我假设您修复了对 MainActivity 的引用,但我想提另一个问题...

您声明解析需要 20 秒。如果你“中断”应用程序,这个处理过程不会消失。

从您在此处显示的代码来看,这 20 秒的 99% 似乎都花在了 JsonStringParser.parse() 中。

如果我查看您的评论“在此处进行大量 JSON 解析”,我假设您的应用程序调用 JSONParser.something() 并保持 20 秒。尽管 JsonStringParser 是静态的,但每次调用 JsonStringParser.parse() 都会创建 JSONParser() 的新副本,我猜这会占用大量内存。

需要 20 秒的后台进程是一项非常艰巨的任务,在我使用 JSON 解析器时看到的情况,此时会创建和销毁大量对象,并消耗大量周期。

所以我认为你的根本原因是你启动了 JSONParser.something() 的第二个(或第三个或第四个)副本,因为它们每个都将独立执行并尝试分配许多内存块,并且运行时间甚至超过 20 秒因为他们将不得不共享 CPU 周期。多个 JSONParser 对象的组合内存分配会扼杀您的系统。

总结一下:

  • 在第一个被杀死或完成之前,不要启动另一个 JsonStringParser.parse()。
  • 这意味着您必须找到一种方法来在“中断”应用程序时停止 JsonStringParser.parse(),或者在重新启动应用程序时重新使用正在运行的副本。
于 2012-12-16T06:05:56.667 回答
1

我想我知道这怎么可能,但我的眼睛已经斜视了。

检查您是否没有从本地存储加载数据,向其中添加更多数据,然后将其保存回本地磁盘。

将以下方法与程序的其他部分结合起来。

如果调用了以下内容,然后您出于某种原因调用 getDatafromURL,那么我相信您会不断增长您的数据集。

这至少是我的出发点。加载、附加和保存。

ApplicationContext.setData(getLocalTimestamp(), getLocalJsonData());

private List<Zone> getLocalJsonData() throws IOException, ParseException {
    if (ProcessNotification.isCancelled()) {
        throw new InterruptedIOException();
    }

    return JsonStringParser.parse(PersistenceManager.getFileData(FileType.JSONDATA));
}

否则我认为问题在于您的解析代码,或者可能是您用来保存数据的静态类之一。

于 2012-12-12T08:58:49.433 回答
0

我的最终解决方案

我现在自己找到了解决方案。当我多次启动和停止应用程序时,它运行稳定并且不会产生内存泄漏。这个解决方案的另一个优点是我能够踢掉所有这些ProcessNotification.isCancelled()部分。

关键是在我的 ApplicationContext 中保存对我的 InitializationTask 的引用。使用这种方法,当我开始一个新的 MainActivity 时,我可以在一个新的 MainActivity 中恢复正在运行的 AsyncTask。这意味着我永远不会启动多个 AsyncTask,但我会将每个新 MainActivity 实例附加到当前正在运行的任务。旧的 Activity 将被分离。这看起来像这样:

ApplicationContext 中的新方法:

public static void register(InitializationTask initializationTask) {
    ApplicationContext.initializationTask = initializationTask;
}

public static void unregisterInitializationTask()  { 
    initializationTask = null;
}

public static InitializationTask getInitializationTask() {
    return initializationTask;
}

MainActivity
(我必须把progressDialog放在这里,否则如果我停止并启动一个新的Activity它就不会显示):

@Override
protected void onStart() {
    super.onStart();

    progressDialog = new ProgressDialog(this);
    progressDialog.setMessage("Processing.\nPlease wait...");
    progressDialog.setIndeterminate(true); // means that the "loading amount" is not measured.
    progressDialog.setCancelable(true);
    progressDialog.show();

    if (ApplicationContext.getInitializationTask() == null) {
        initializationTask = new InitializationTask();
        initializationTask.attach(this);

        ApplicationContext.register(initializationTask);

        initializationTask.execute((Void[]) null);
    } 
    else {
        initializationTask = ApplicationContext.getInitializationTask();

        initializationTask.attach(this);
    }
}

MainActivity 的“onPause”包含initializationTask.detach();progressDialog.dismiss();. finalizeSetup();也关闭对话框。

InitializationTask 包含另外两个方法:

public void attach(MainActivity mainActivity) {
    this.mainActivity = mainActivity;
}

public void detach() {
    mainActivity = null;
}

onPostExecute的任务调用ApplicationContext.unregisterInitializationTask();

于 2012-12-23T16:01:32.200 回答