从源码解析部分系统服务引起的内存泄露

本文分析了在Android 6.0及以上版本中使用ConnectivityManager时出现的内存泄露问题。问题源于ConnectivityManager持有传入的Context引用,若为Activity则会导致内存泄露。文中详细介绍了Context的实现机制,并给出了解决方案。

#写在开头

写文章,其实是一个梳理思维,学习成长的过程,有意的可以一块学习,互相交流。谢谢大家!有错误或者意见还请指出共同进步。

今天在用LeakCanary检测内存泄露过程中,发现ConnectivityManager报出内存泄露的警告(context),这不是系统Framework的网络管理模块吗,系统服务还能泄露了。。。

###问题分析(就ConnectivityManager而言)

通过在不同的测试手机上测试,发现此问题只在android6.0以上才有。5.0或4.4的手机中并无此问题。大家都知道,使用这些系统服务会传入一个上下文环境,也就是我们所说的context,在android的源码中,这些服务的对象是通过单例创建的,但是在6.0以上,它会持有我们传入的context的引用。所以,如果你传入的是activity,当你的activity销毁时,它却持有这个activity的引用,就会造成内存泄露。在5.1的源码中,ConnectivityManager实现为单例但不持有Context的引用,在5.0有以下版本ConnectivityManager既不为单例,也不持有Context的引用。本文就6.0以上说明一下。

##Context详解

在解释问题前,先说下context。都知道,Context是维持Android程序中各组件能够正常工作的一个核心功能类。他的继承关系如下图:

这里写图片描述

Context的继承结构如图,它的直系子类有两个,一个是ContextWrapper,一个是ContextImpl。ContextWrapper是上下文功能的封装类,而ContextImpl则是上下文功能的实现类。而ContextWrapper又有三个直接的子类,ContextThemeWrapper、Service和Application。其中,ContextThemeWrapper是一个带主题的封装类,而它有一个直接子类就是Activity。

通过上面我们可以看出,我们说的context就三种,activity,service,application。这三个类虽然分别各种承担着不同的作用,但它们都属于Context的一种,而它们具体Context的功能则是由ContextImpl类去实现的。

PS:如果您想看ContextImpl的源码,你会发现从AS看context的继承结构并没有ContextImpl,这时你需要去sdk中/android-sdk/sources/android-25/android/app/ContextImpl.java 去查找观看。

###进一步分析Context

从上面可以看到,Activity根本上是从ContextWrapper继承而来的,跟进源码,ContextWrapper中持有一个mBase实例

public class ContextWrapper extends Context {
    Context mBase;

    public ContextWrapper(Context base) {
        mBase = base;
    }

这个实例指向一个ContextImpl对象,我们可以看ContextWrapper中的attachBaseContext方法。

	protected void attachBaseContext(Context base) {
        if (mBase != null) {
            throw new IllegalStateException("Base context already set");
        }
        mBase = base;
    }

这个方法中传入了一个base参数,并把这个参数赋值给了mBase对象。而attachBaseContext()方法其实是由系统来调用的,它会把ContextImpl对象作为参数传递到attachBaseContext()方法当中,从而赋值给mBase对象,之后ContextWrapper中的所有方法其实都是通过这种委托的机制交由ContextImpl去具体实现的。

同时ContextImpl对象持有一个OuterContext对象,对于Activity来说,这个OuterContext就是Activity对象。
也就是构造中的那个this。

	 private ContextImpl(ContextImpl container, ActivityThread mainThread,
            LoadedApk packageInfo, IBinder activityToken, UserHandle user, int flags,
            Display display, Configuration overrideConfiguration, int createDisplayWithId) {
        mOuterContext = this;

        // If creator didn't specify which storage to use, use the default
        // location for application.
        if ((flags & (Context.CONTEXT_CREDENTIAL_PROTECTED_STORAGE
                | Context.CONTEXT_DEVICE_PROTECTED_STORAGE)) == 0) {
            final File dataDir = packageInfo.getDataDirFile();
            if (Objects.equals(dataDir, packageInfo.getCredentialProtectedDataDirFile())) {
                flags |= Context.CONTEXT_CREDENTIAL_PROTECTED_STORAGE;
            } else if (Objects.equals(dataDir, packageInfo.getDeviceProtectedDataDirFile())) {
                flags |= Context.CONTEXT_DEVICE_PROTECTED_STORAGE;
            }
        }

        mMainThread = mainThread;
        mActivityToken = activityToken;
        mFlags = flags;
	......
	

所以您通过context调用getSystemService时最终会调用到ContextImpl的getSystemService方法。而这个getSystemService是通过ContextImpl的SystemServiceRegistry来完成。

代码流程如下。

第一步:

	public Object getSystemService(String name) {
	    return SystemServiceRegistry.getSystemService(this, name);
		}

第二步:

	public static Object getSystemService(ContextImpl ctx, String name) { 
	      ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name); 
	 }

第三步:SystemServiceRegistry提供ConnectivityManager的实例。

	registerService(Context.CONNECTIVITY_SERVICE, ConnectivityManager.class, new StaticOuterContextServiceFetcher<ConnectivityManager>() { 
	 @Override
	 public ConnectivityManager createService(Context context) { 
	 IBinder b = ServiceManager.getService(Context.CONNECTIVITY_SERVICE); IConnectivityManager service = IConnectivityManager.Stub.asInterface(b); 
	 return new ConnectivityManager(context, service); }});

	static abstract class StaticOuterContextServiceFetcher<T> implements ServiceFetcher<T> { 
	private T mCachedInstance; 
	@Override 
	public final T getService(ContextImpl ctx) { 
	if (mCachedInstance == null) { 
	mCachedInstance = createService(ctx.getOuterContext()); 
		} 
	} 
} 
	public abstract T createService(Context applicationContext); }

#总结

Context在ConnectivityManager 创建时传入,这个Context在StaticOuterContextServiceFetcher中由ContextImpl对象转换为OuterContext,如果我们的context是Activity对象,ConnectivityManager的单实例就持有了Activity的实例引用。这样即使Activity退出后仍然无法释放,导致内存泄漏。在源码中,public abstract T createService(Context applicationContext); },人家提示的非常明显了,这个上下文环境,人家想要的是一个Application中提供的context。

#省心的解决办法

获取系统服务getSystemService时使用我们在自定义Application中提供的context。

#写在后头

对于其他的系统服务,有些传入activty的引用也是不会造成内存泄露的,所以这方面还是就实际而言,有问题解决,看人家源码的提示,没问题就没问题喽,安心的写其他代码。

<think>好的,我现在需要帮用户找到在IntelliJ IDEA配置Tomcat的详细教程。首先,用户提供的引用内容里有三个相关的引用,我需要仔细看看这些内容是否包含配置Tomcat的步骤。 引用[1]提到的是配置Tomcat服务器的详细指南,总结里说按照步骤就能成功配置,并且能和IDEA无缝集成。这应该是最直接相关的。接下来,引用[2]和[3]主要涉及Maven配置和其他基础设置,可能和Tomcat配置相关,但可能不是核心步骤。 用户需要详细的步骤指南,所以应该以引用[1]为主。不过,引用[3]里也有配置JDK、Maven、Git等的步骤,可能在配置Tomcat之前需要这些基础配置。例如,配置JDK和Maven可能是前提条件,但用户可能已经完成了这些,但不确定是否需要提到。 现在,我需要整合引用中的信息。首先,引用[1]应该包含具体的Tomcat配置步骤,而引用[3]中的基础配置可能作为补充。例如,配置JDK(1.1)和Maven(1.2)可能在配置Tomcat之前需要检查是否正确设置。此外,引用[2]提到Maven的配置路径,可能和项目构建有关,但Tomcat配置更多涉及服务器设置。 接下来,我需要确保步骤清晰,按照用户的需求逐步说明。可能包括:打开IDEA,进入设置,找到Tomcat服务器配置,添加本地Tomcat路径,检查项目结构,部署工件等步骤。同时,需要确保用户了解如何在IDEA中启动Tomcat,并测试应用程序。 另外,需要检查是否需要配置工件(Artifacts),比如WAR包,这在部署Web应用时是必须的。引用[3]中提到部署到Tomcat可能需要配置工件,但具体步骤是否在引用[1]里呢?引用[1]的总结提到开发和测试Java Web应用,所以可能包括这些步骤。 可能存在的疑问是,是否需要先配置Maven或者JDK才能配置Tomcat?根据一般流程,是的,但用户可能已经配置好这些,所以需要分步骤说明,先确认基础配置,再进入Tomcat设置。 最后,生成相关问题时要考虑用户可能进一步的问题,比如性能调优、调试配置、多模块项目配置等。需要结合引用中的内容,例如引用[3]提到Maven配置,可能相关问题会涉及Maven与Tomcat的整合。 总结步骤应该是:1. 打开配置界面;2. 添加Tomcat服务器;3. 指定Tomcat路径;4. 配置部署;5. 启动测试。同时,确保每个步骤对应引用中的内容,特别是引用[1]和引用[3]中的相关部分。</think>以下是根据引用内容整理的IntelliJ IDEA配置Tomcat服务器详细步骤指南: ### 一、前置准备 1. 确保已安装JDK并完成基础配置[^3] - 通过`File | Project Structure | Project`设置JDK版本 2. 建议配置Maven环境(非必需但推荐)[^2] - `File | Settings | Build, Execution, Deployment | Build Tools | Maven` ### 二、Tomcat配置步骤 1. **打开运行配置界面** - 顶部菜单栏选择`Run > Edit Configurations` - 点击左上角`+`号,选择`Tomcat Server > Local`[^1] 2. **添加Tomcat服务器** ```markdown - 在`Application Server`栏点击`Configure...` - 指定Tomcat安装目录(如`C:\apache-tomcat-9.0.xx`) - 自动识别后会显示Tomcat版本号 ``` 3. **部署工件配置** - 切换到`Deployment`标签页 - 点击`+`号选择`Artifact` - 选择项目生成的WAR包(如`your-project:war`) 4. **上下文路径设置** - 在`Application Context`输入访问路径(例如`/demo`) - 默认访问地址将变为`http://localhost:8080/demo` ### 三、验证配置 1. 返回`Server`标签页可设置: - 端口号(默认8080) - 启动前浏览器自动打开配置 - JVM启动参数 2. **启动测试** - 点击工具栏`Run`按钮启动Tomcat - 控制台显示`Tomcat started on port(s): 8080`即表示成功 - 浏览器访问`http://localhost:8080/`验证部署 ### 四、调试配置(扩展) 1. 创建`Tomcat Debug`配置 - 复制已有Tomcat配置 - 在`Server`标签页勾选`JPDA`调试模式 - 使用`Shift+F9`启动调试会话 [^1]: 详细步骤参考Tomcat服务器配置指南,包含完整的界面截图和路径选择说明 : 基础环境配置参考IDEA使用教程中的JDK/Maven配置章节
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值