2012-12-09 42 views
12

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

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

这是详细信息列表,显示这问题有事情做与数组列表。

detail

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

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

下面是一个真正的崩溃图。我开始和停止,同时初始化多次应用:

crash report

[更新]
我通过存储在Android方面到我的ApplicationContext类,使PersistenceManager的非静态范围缩小一点。问题没有改变,所以我绝对相信这与我在全球存储Android上下文无关。它仍然是上图中的“问题可疑1”。所以我必须对这个巨大的列表做些什么,但是什么?我已经试图序列化它,但是非列化这个列表需要比20secs更长的时间,所以这不是一个选项。

现在我尝试了一些不同的东西。我踢出了整个ApplicationContext,所以我不再有任何静态引用。我试图在MainActivity中保存Zone对象的ArrayList。尽管我重构了至少需要让应用程序运行的部分,但是我甚至没有将Array或Activity传递给所需的所有类,但仍然存在相同的问题,所以我的猜测是区域对象本身在某种程度上是问题。或者我无法正确读取堆转储。看到下面的新图。这是一个简单的应用程序开始没有干扰的结果。

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

memory error 4 memory error 5

+0

“通过构造函数传递引用”是帮助避免这样的问题。老实说,以这种方式使用静态内存肯定是一种创建内存泄漏的方法,特别是对于上下文的静态引用。你需要显示你调用setData的地方以及你传递给它的内容。这可能会帮助您解决内存泄漏问题。 – Emile

+0

@Emile好吧,我添加了额外的信息。 – Bevor

+0

它可以是'私人MainActivity mainActivity;'由你的'AsyncTask'发生内存泄漏吗? 'AsyncTask'结束后,你是否试图将它设置为null? –

回答

0

我的最终解决方案

我现在发现我自己的解决方案。它运行稳定,并且在我开始和停止应用程序很多次时不会产生内存泄漏。这个解决方案的另一个优点是,我能够将所有这些零件踢出去。

的关键是我的ApplicationContext来坚持自己的InitializationTask参考。通过这种方法,我可以在新的MainActivity中恢复正在运行的AsyncTask,当我开始一个新的MainActivity。这意味着我永远不会启动多个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();

+0

该解决方案很好。其实它非常接近已经在这个线程上讨论的东西:http://stackoverflow.com/questions/3357477/is-asynctask-really-conceptually-flawed-or-am-i-just-missing-something。尽管如此,问题在于如果没有大量的复制粘贴/锅炉代码,这个解决方案就很难重用。它会工作,但你不能真正利用这个解决方案。 – Snicolas

3

首先,我有埃米尔同意:

The "..tedious passing references by constructor" is what helps avoid issues like this. Honestly, using statics in this way is certainly one way to create memory leaks like this, especially with a static reference to your context.

这也适用于在你的代码所有其他static方法。 static方法与全局函数没有什么不同。你正在建造一个充满static方法的大型意大利面条盘。特别是当他们开始分享某些状态时,它迟早会崩溃或创建一些难以理解的结果,而这些结果在适当的设计中不会得到,特别是在Android这样的高度多线程的平台的情况下。

还有一点我的眼睛是,请注意,在doInBackground完成之前将不会调用AsyncTaskonCancelled方法。因此,您的全球取消标志(ProcessNotification.isCancelled())或多或少不值钱(如果仅用于所示的代码段)。

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

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

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

+0

不超过20-30个条目,但这些对象真的很大,因为它们持有整个城市的GeoJSON数据。逻辑本身是完美的。我没有任何数据多次,但你是正确的一个:当我开始和停止奥德和再次,JsonStringParser.parse线程1完成后再次启动,但逻辑可以处理。不幸的是,我没有找到如何杀死JsonStringParser.parse的方法。这将是我可以做的最好的,因为这个解析大约需要20秒。我添加了解析器的类体,也许这很有帮助。 – Bevor

+0

从第二张内存图片中,您可以看到,“区域”列表不是您的问题,因为它仍具有相同的大小,并且大小与您报告的内容一致。你想知道什么'AsyncTask'占用了3.8MB的内存,以及它的用途。但总共6.8 MB的内存听起来不会太高。大多数设备应该能够拥有至少16 MB的可用堆空间。 – Stephan

+0

它总是在相同的位置崩溃。无论是从Url读取还是在进行文件操作时。在通过readLine阅读JSON数据之前(一个非常大的一行)。然后我将它重构为块式读取。这有一点帮助,但是当我开始并且停止很多时,这并没有什么帮助。当这个问题解决后,我希望我能摆脱它。我想知道为什么它会崩溃,因为我将堆设置为至少16MB。最好的办法是杀死所有线程,并重新启动它们,但是我无法取消解析过程。我可以保证它不会产生不一致,至少这是有效的。 – Bevor

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)); 
} 

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

+0

您的意思是“检查您是否从本地存储装载数据,向其添加更多数据,然后将其保存回本地磁盘”。我不明白这一步。 – Bevor

+0

看了再次我不认为这是原因,除非你修改数据本地存储文件,而不是覆盖它。你是否在任何地方使httpclient无效? – Emile

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解析。

+0

我只是重构了我的ApplicationContext类中的上下文,但我仍然遇到了同样的问题。也许我必须摆脱整个ApplicationContext,但我会给WeakReference一个尝试。否则,我没有其他的可能性。因为我不知道是否完全可能将我的整个ArrayList从JsonParser和PersistenceManager通过AsyncTask传递到所有使用它们的上层类。 – Bevor

+0

我发现这个条目描述了不推荐使用WeakReference,但我不确定这是否仅适用于BitMaps:https://github.com/nostra13/Android-Universal-Image-Loader/issues/53 – Bevor

+0

我不能说Dalvik的这个新功能,但它确实解决了我们大部分使用WeakReferences的内存泄漏问题。这个想法是,你不应该直接在Context上保留一个引用。 – Snicolas

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() 的方法,或者在重新启动应用程序时重新使用正在运行的副本。
+0

你是对的,我知道这是问题,因为GC必须销毁成千上万个对象。该解决方案可能不会启动另一个AsyncTask,而是“加入”正在运行的一个。我已经找到了如何做到这一点,但目前还无法解决。 – Bevor

+0

为此,您还可以在RoboSpice Motivation源代码中找到一个示例。我们提供了2个加入已启动的AsyncTask的实现。但是你应该真的考虑使用RoboSpice,它为你做了所有这些,并且它比作为解决AsyncTask的方式更容易重用,并且使它能够以它应该支持如此长时间运行的任务的方式工作。你只会得到有效的代码,但没有什么可以轻松地在其他应用程序/活动中重复使用。 – Snicolas

+0

Bevor,由于AsyncTask总是将输出存储在应用程序对象中(需要进行一些小的更改),因此您可以简单地使用跟踪其运行的信号量。这样,当主活动加载时,只有在AsyncTask尚未运行的情况下才能启动。 – gabriel