需要帮助来了解我的Android应用中的内存泄漏

时间:2012-12-09 16:30:26

标签: android memory-leaks heap out-of-memory

我的应用运行正常,直到我在安装后的第一次启动中断初始化过程,只要初始化过程尚未完成,就退出并启动应用程序几次。处理逻辑和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);

但它没有效果。我已经尝试将zones属性放入另一个静态类,因为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;
    }
}

这是显示问题的堆转储:

Memory chart

这是详细列表,显示此问题与arraylist有关。

detail

任何想法在这里有什么问题?顺便说一句:由于没有详细信息,我不知道其他泄漏是什么。

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

这是一个真正的崩溃图。我在初始化时多次启动并停止了应用程序:

crash report

[UPDATE]
我没有将Android上下文存储到我的ApplicationContext类中并使PersistenceManager非静态,从而缩小了一点。这个问题没有改变,所以我完全相信它与全局存储Android上下文的事实无关。它仍然是上图中的“问题可疑1”。所以我必须对这个庞大的列表做些什么,但是什么呢?我已经尝试将其序列化,但是取消分配此列表需要的时间比20秒长,所以这不是一个选项。

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

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

memory error 4 memory error 5

5 个答案:

答案 0 :(得分:3)

首先,我必须同意埃米尔的意见:

  

“构造函数传递引文”是有助于避免的   像这样的问题。老实说,以这种方式使用静态肯定是一个   这样创建内存泄漏的方法,特别是静态的   参考你的背景。

这也适用于代码中的所有其他static方法。 static方法与全局函数没有什么不同。你正在那里建造一个充满static方法的大意大利面板。特别是当他们开始共享一些状态时,它迟早会崩溃或创建一些模糊的结果,而这些结果是你无法通过适当的设计得到的,特别是在存在高度多线程平台的情况下,如Android。

我还注意到的是,请注意onCancelled的{​​{1}}方法在AsyncTask完成之前不会被调用。因此,您的全局取消标记(doInBackground)或多或少没有价值(如果仅在显示的代码段落中使用)。

同样,根据您发布的记忆图像,ProcessNotification.isCancelled()列表中仅包含“31”项。该举多少钱?它增加了多少?如果它实际上增加了,那么结果可能是zones方法,这又是JsonStringParser.parse。如果它在某些缓存中保存了一个项目列表,并且控制逻辑无法正常工作(例如,在存在多个线程同时访问它的情况下),则每次调用它时都可能会向该缓存添加项目。

  • 猜测1:由于解析方法为static,因此在关闭应用程序时不会(必要)清除此数据。 static被初始化一次,并且出于本案例的目的,在(物理vm-)过程停止之前,永远不会去初始化。但是,即使应用程序已停止(see for example a wonderful explanation here),Android也不保证该进程被终止。因此,您可能会在(可能是解析)代码的某些static部分中累积一些数据。
  • 猜测2:由于您多次重新启动应用程序,因此后台线程并行运行多次(假设:每次重新启动应用程序时都会生成一个新线程。请注意,您的代码没有显示防范这个。)第一次解析仍在运行,另一个解析开始,因为全局static变量仍然没有值。全局函数zones可能不是线程安全的,并且会将多个数据多次放入最终返回的列表中,从而产生越来越大的列表。同样,通常不使用parse方法(并注意多线程)可以避免这种情况。

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

答案 1 :(得分:3)

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

这是一个真正的内存泄漏,因为Android会想要垃圾收集不再显示的Activity。你的AsyncTasks会阻止它。所有活动都保存在记忆中。

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

解决此问题的一种方法是使用WeakReference指向AsyncTask类中的活动。如果您仔细使用它们,则可以避免您的活动不被垃圾收集。

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

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

答案 2 :(得分:2)

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

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

根据你在这里显示的代码,似乎99%的20秒花在JsonStringParser.parse()中。

如果我看一下你的评论“在这里做了很多JSON解析”,我假设你的应用程序调用了JSONParser.something(),它会在20秒内停留。即使JsonStringParser是静态的,每次调用JsonStringParser.parse()都会创建一个JSONParser()的新副本,我的猜测是使用大量内存。

一个需要20秒的后台进程是一项非常重要的任务,而且在我看到的JSON解析器中,很多对象都会被创建和销毁,并且会消耗大量的周期。

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

总结:

  • 在第一个之前不要启动另一个JsonStringParser.parse() 被杀或完成。
  • 这意味着你必须找到一种方法来阻止JsonStringParser.parse() 当您“中断”应用程序时,或在您使用时重复使用正在运行的副本 重启应用程序。

答案 3 :(得分: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));
}

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

答案 4 :(得分:0)

我的最终解决方案

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

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

ApplicationContext中的新方法:

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

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

public static InitializationTask getInitializationTask() {
    return initializationTask;
}

<强> MainActivity
 (我必须将progressDialog放在这里,否则如果我停止并开始新的活动则不会显示):

@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();