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

Posted

技术标签:

【中文标题】需要帮助了解我的 Android 应用程序中的内存泄漏【英文标题】:Need help to understand memory leak in my Android app 【发布时间】:2012-11-27 04:16:22 【问题描述】:

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

这是显示问题的堆转储:

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

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

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

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

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

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

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

【问题讨论】:

“......繁琐的构造函数传递引用”有助于避免此类问题。老实说,以这种方式使用静态肯定是造成这样的内存泄漏的一种方法,尤其是在对您的上下文进行静态引用的情况下。您需要显示调用 setData 的位置以及传递给它的内容。这可能会帮助您解决内存泄漏问题。 @Emile 好的,我添加了其他信息。 会不会是你的AsyncTask持有的private MainActivity mainActivity;出现内存泄漏?您是否尝试在AsyncTask 结束后将其设置为空? @ol_v_er 谢谢你的提示,我今晚在家的时候会试试看。 对不起,没用。结果相同。 【参考方案1】:

首先,我必须同意 Emile:

“......繁琐的构造函数传递引用”有助于避免 诸如此类的问题。老实说,以这种方式使用静力学肯定是一种 像这样创建内存泄漏的方法,尤其是使用静态 参考您的上下文。

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

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

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

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

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

【讨论】:

只有大约 20 - 30 个条目,但这些对象非常大,因为它们包含整个城市的 GeoJSON 数据。逻辑本身完美无缺。我多次没有任何数据,但你说得对:当我再次启动和停止 oder 时, JsonStringParser.parse 在线程 1 完成后再次启动,但逻辑可以处理。不幸的是,我没有找到杀死 JsonStringParser.parse 的方法。这将是我能做的最好的事情,因为这个解析大约需要 20 秒。我正在添加解析器的类体,也许有帮助。 从第二张内存图可以看出,不是zones列表是你的问题,因为它的大小仍然相同,并且大小与你报告的一致。您想知道AsyncTask 占用了 3.8 MB 内存,以及它的用途。但即使总共 6.8 MB 的内存听起来也不算多。大多数设备应该能够拥有至少 16 MB 的可用堆空间。 它总是在相同的位置崩溃。当我从 Url 读取或执行文件操作时。在我通过 readLine 读取 JSON 数据(一个非常大的一行)之前。然后我将它重构为逐块阅读。这有点帮助,但当我开始和停止它时没有帮助。我希望当这里的这个问题得到解决时我能摆脱它。我想知道为什么它会崩溃,因为我将堆设置为最小 16MB。最好的办法是杀死所有线程,然后重新启动它们,但我无法取消解析过程。我只是可以注意它不会产生不一致,至少这是可行的。 好吧,isCancelled()(AsyncTask 的)方法将在它被取消后立即返回 true(并且 doInBackground() 仍在运行)。因此,在解析步骤中检查它(如果可能的话)并在它为真时抛出异常可能是一个解决方案? 我已经实现了这个。我没有看到任何改进,因为垃圾收集需要这么长时间。它在解析时必须清理一万个对象。这是此过程中花费最多的时间。【参考方案2】:

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

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

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

如果调用了以下命令,然后您出于某种原因调用 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));

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

【讨论】:

“检查您是否没有从本地存储加载数据,向其中添加更多数据,然后将其保存回本地磁盘”到底是什么意思。我不明白那一步。 再次查看我不认为这是原因,除非您将数据修改到本地存储文件而不是覆盖它。你在任何地方都取消 httpclient 吗?【参考方案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 解析。

【讨论】:

我刚刚从我的 ApplicationContext 类中重构了我的上下文,但我仍然遇到同样的问题。也许我必须摆脱整个 ApplicationContext,但我会尝试 WeakReference。否则我没有其他可能。因为我不知道是否可以轻松地将我从 JsonParser 和 PersistenceManager 通过 AsyncTask 获得的整个 ArrayList 传递给所有使用它们的上层类。 我发现这个条目描述了不建议再使用 Wea​​kReference,但我不确定这是否仅适用于位图:github.com/nostra13/Android-Universal-Image-Loader/issues/53 我不能说 Dalvik 的这个新特性,但它绝对解决了我们使用 Wea​​kReferences 的大部分内存泄漏。这个想法是你永远不应该直接在 Context 上保留引用。 你真的改变了你的 AsyncTask 以使它在任何上下文(即活动)上都没有任何引用吗?我无法清楚地理解你做了什么。 我决定试试 RoboSpice。我不确定如何以干净的方式做到这一点。【参考方案4】:

我假设您修复了对 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() 当您“中断”应用程序时,或在您重用正在运行的副本时 重启应用。

【讨论】:

你说得对,我知道这就是问题所在,因为 GC 必须销毁数十万个对象。解决方案可能是不启动另一个 AsyncTask,而是“加入”正在运行的 AsyncTask。我已经找到了如何做到这一点,但它还不起作用。 为此,您也可以在 RoboSpice Motivation 源代码中找到示例。我们提供了 2 个实现来加入已启动的 AsyncTask。但是您真的应该考虑使用 RoboSpice,它为您完成了所有这些工作,并且它作为一种解决方案比破解 AsyncTask 并使其按照应有的方式工作以支持如此长时间运行的任务更具可重用性。你最终会得到工作代码,但没有任何东西可以轻松地在其他应用程序/活动中重用。 Bevor,由于 AsyncTask 始终将输出存储在 Application 对象中(需要进行一些小的更改),因此您可以简单地使用跟踪它运行的信号量。这样,当 Main Activity 加载时,只有在 AsyncTask 尚未运行时才启动它。 Bevor,添加到上面的评论中,告诉 mainActivity JsonStringParser.parse() 完成的简单方法如下:在 InitializationTask 中完全删除对 MainActivity 的引用并将其替换为 WEAK 引用应用程序上下文。确保 MainActivty 更新 onCreate() 中的引用,以便 InitializationTask 在完成处理时通知最新的 MainActivity。【参考方案5】:

我的最终解决方案

我现在自己找到了解决方案。当我多次启动和停止应用程序时,它运行稳定并且不会产生内存泄漏。这个解决方案的另一个优点是我能够踢掉所有这些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;

主活动 (我必须把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();

【讨论】:

这个解决方案很好。实际上它非常接近在这个线程上讨论过的东西:***.com/questions/3357477/…。然而,问题是如果没有大量的复制粘贴//样板代码,这个解决方案几乎不能被重用。它会起作用,但你不能真正利用这个解决方案。

以上是关于需要帮助了解我的 Android 应用程序中的内存泄漏的主要内容,如果未能解决你的问题,请参考以下文章

Android进程生命周期与ADJ

完全初学者;需要帮助正确设置我的 Android 5.0 工具栏

关于Android应用中图片占用内存浅谈

Android - 应用程序 (apk) 最大大小

Android安全Android中的“SpyLocker”恶意软件 - 您需要知道的内容

Google Play控制台预发布报告 - 内存