Apache Harmony的JarURLConnectionImpl中是否存在Android内存泄漏问题?

9
我正在开发一款Android应用程序,我们正在调查内存使用情况。
从hprof中的堆转储数据来看,我们发现在JarURLConnectionImpl的静态缓存中使用了近2M(占我们堆的22%):

enter image description here

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

如果它们从未被删除,那么可能会导致内存泄漏。

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


一个关于stackoverflow的相关帖子:https://dev59.com/Vl3Ua4cB1Zd3GeqP-TIU - emmby
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 https://dev59.com/zW3Xa4cB1Zd3GeqPjt0Z
    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
        }
    }

我会在每次创建活动时运行它,以使其每次执行。那个丑陋的哈希映射表条目似乎不会经常被重新创建,但是偶尔会重新出现,因此仅运行此代码一次是不够的。

这解决了我的问题,但是是否有一个适当的修复方法或者任何线索表明这是什么原因造成的?我能在我的代码中做一些其他的变通来避免使用hack吗? - aslakjo
在较新版本的Android中(不确定截止时间),类名为libcore.net.url.JarURLConnectionImpl - goldierox

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 https://dev59.com/zW3Xa4cB1Zd3GeqPjt0Z

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

我在我的应用程序中使用了这个解决方法已经将近一年了,但最近出现了一个“java.lang.IllegalStateException: Zip file closed”的错误,其中JarURLConnectionImpl是堆栈跟踪中唯一相关的代码。不确定这个解决方法是否是原因。 - Carl Anderson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接