9

我正在开发一个 Android 应用程序,我们正在调查内存使用情况。

查看来自 hprof 的堆转储,我们看到在 JarURLConnectionImpl 中的静态缓存中使用了近 2M(堆的 22%):

在此处输入图像描述

查看 JarURLConnectionImpl的源代码,似乎条目已添加到静态 jarCache 变量中,但从未删除。

如果它们确实从未被删除,那么我认为这是潜在的内存泄漏。

这是泄漏吗?有修复或解决方法吗?

4

2 回答 2

5

这是一个丑陋的解决方法:

private static HashMap<URL,JarFile> jarCache;


static {
    try {
        Class<?> jarURLConnectionImplClass = Class.forName("org.apache.harmony.luni.internal.net.www.protocol.jar.JarURLConnectionImpl");
        final Field jarCacheField = jarURLConnectionImplClass.getDeclaredField("jarCache");
        jarCacheField.setAccessible(true);
        //noinspection unchecked
        jarCache = (HashMap<URL, JarFile>) jarCacheField.get(null);
    } catch(Exception e) {
        // ignored
    }
}

然后,定期运行以下命令:

    // HACK http://stackoverflow.com/questions/14610350/android-memory-leak-in-apache-harmonys-jarurlconnectionimpl
    if( jarCache!=null ) {
        try {
            for (
                final Iterator<Map.Entry<URL, JarFile>> iterator = jarCache.entrySet().iterator(); iterator.hasNext(); ) {
                final Map.Entry<URL, JarFile> e = iterator.next();
                final URL url = e.getKey();
                if (Strings.toString(url).endsWith(".apk")) {
                    Log.i(TAG,"Removing static hashmap entry for " + url);
                    try {
                        final JarFile jarFile = e.getValue();
                        jarFile.close();
                        iterator.remove();
                    } catch( Exception f ) {
                        Log.e(TAG,"Error removing hashmap entry for "+ url,f);
                    }
                }
            }
        } catch( Exception e ) {
            // ignored
        }
    }

我在创建活动时运行它,因此每次创建我的一项活动时都会执行它。丑陋的 hashmap 条目似乎并不经常重新创建,但它似乎偶尔会重新出现,因此仅运行此代码一次是不够的。

于 2013-02-01T00:29:09.427 回答
4

这绝对是一个令人讨厌的内存泄漏。我已经为它打开了一个问题,因为似乎没有其他人报告它。

感谢 emmby 的“丑陋解决方法”,这很有帮助。一种更安全的方法(尽管可能会影响性能)是完全禁用 URLConnection 缓存。由于 URLConnection.defaultUseCaches 标志是静态的,并且正如您可能猜到的那样,它是每个实例的 useCaches 标志的默认值,因此您可以将其设置为 false 并且没有更多实例将缓存它们的连接。这将影响 URLConnection 的所有实现,因此它可能会产生比预期更远的影响,但我认为这是一个合理的权衡。

您可以像这样创建一个简单的类,并在应用程序的 onCreate() 中尽早实例化它:

public class URLConnectionNoCache extends URLConnection {

    protected URLConnectionNoCache(URL url) {
        super(url);
        setDefaultUseCaches(false);
    }

    public void connect() throws IOException {
    }
}

有趣的是,由于这发生在您的应用程序加载并运行之后,系统库应该已经被缓存,这只会阻止进一步缓存,所以这可能提供了最好的权衡:不缓存您的 apk,同时允许缓存系统 jar 的性能优势。

在这样做之前,我确实对 emmby 的解决方案进行了一些修改,使其成为一个独立的类,该类创建一个后台线程来定期清除缓存。我将其限制为仅清除应用程序的 apk,尽管如果需要可以放宽。这里要担心的是您正在修改可能正在使用的对象,这通常不是一件好事。如果您确实想走这条路,您只需要使用上下文调用 start() 方法,例如在您的应用程序的 onCreate() 中。

package com.example;

import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.jar.JarFile;
import java.util.regex.Pattern;

import android.content.Context;

// hack to remove memory leak in JarURLConnectionImpl
// from http://stackoverflow.com/questions/14610350/android-memory-leak-in-apache-harmonys-jarurlconnectionimpl

public class JarURLMonitor {
    private static JarURLMonitor instance;
    private Pattern pat;
    private Field jarCacheField;
    public volatile boolean stop;

    private static final long CHECK_INTERVAL = 60 * 1000;


    public static synchronized void start(Context context) {
        if (instance == null) {
            instance = new JarURLMonitor(context);
        }
    }

    public static synchronized void stop() {
        if (instance != null) {
            instance.stop = true;
        }
    }

    private JarURLMonitor(Context context) {
        // get jar cache field
        try {
            final Class<?> cls = Class.forName("libcore.net.url.JarURLConnectionImpl");
            jarCacheField = cls.getDeclaredField("jarCache");
            jarCacheField.setAccessible(true);
        }
        catch (Exception e) {
            // log
        }

        if (jarCacheField != null) {
            // create pattern that matches our package: e.g. /data/app/<pkgname>-1.apk
            pat = Pattern.compile("^.*/" + context.getPackageName() + "-.*\\.apk$");

            // start background thread to check it
            new Thread("JarURLMonitor") {
                @Override
                public void run() {
                    try {
                        while (!stop) {
                            checkJarCache();
                            Thread.sleep(CHECK_INTERVAL);
                        }
                    }
                    catch (Exception e) {
                        // log
                    }
                }
            }.start();
        }
    }

    private void checkJarCache() throws Exception {
        @SuppressWarnings("unchecked")
        final HashMap<URL, JarFile> jarCache = (HashMap<URL, JarFile>)jarCacheField.get(null);

        final Iterator<Map.Entry<URL, JarFile>> iterator = jarCache.entrySet().iterator();
        while (iterator.hasNext()) {
            final Map.Entry<URL, JarFile> entry = iterator.next();
            final JarFile jarFile = entry.getValue();
            final String file = jarFile.getName();
            if (pat.matcher(file).matches()) {
                try {
                    jarFile.close();
                    iterator.remove();
                }
                catch (Exception e) {
                    // log
                }
            }
        }
    }
}
于 2013-09-14T04:08:27.410 回答