我正在开发一个 Android 应用程序,我们正在调查内存使用情况。
查看来自 hprof 的堆转储,我们看到在 JarURLConnectionImpl 中的静态缓存中使用了近 2M(堆的 22%):
查看 JarURLConnectionImpl的源代码,似乎条目已添加到静态 jarCache 变量中,但从未删除。
如果它们确实从未被删除,那么我认为这是潜在的内存泄漏。
这是泄漏吗?有修复或解决方法吗?
我正在开发一个 Android 应用程序,我们正在调查内存使用情况。
查看来自 hprof 的堆转储,我们看到在 JarURLConnectionImpl 中的静态缓存中使用了近 2M(堆的 22%):
查看 JarURLConnectionImpl的源代码,似乎条目已添加到静态 jarCache 变量中,但从未删除。
如果它们确实从未被删除,那么我认为这是潜在的内存泄漏。
这是泄漏吗?有修复或解决方法吗?
这是一个丑陋的解决方法:
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 条目似乎并不经常重新创建,但它似乎偶尔会重新出现,因此仅运行此代码一次是不够的。
这绝对是一个令人讨厌的内存泄漏。我已经为它打开了一个问题,因为似乎没有其他人报告它。
感谢 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
}
}
}
}
}