可能会造成内存泄漏的情况如下:
1.静态变量:如果一个对象被声明为静态变量,它将一直存在于内存中,即使不再被使用。静态变量的生命周期是跟随进程的,进程销毁,静态变量占用的内存就会被系统回收。
如果将一个Activity或者Fragment的实例赋值给一个静态变量,这个静态变量会持有对Activity或者Fragment的引用,导致无法被垃圾回收。解决方法是使用弱引用或者在不需要时及时释放引用。
① 静态字段能放:
全局配置、不可变常量、与生命周期无关的轻量单例、WeakReference。
② 静态字段千万别放 短生命周期对象
Activity、Fragment、Service、View、Drawable、Bitmap、大型缓存列表、任何 Context(除非 ApplicationContext)。
在不需要时及时释放引用:
public static Activity sActivity;
①静态字段持有Fragment引用,在页面销毁时,将静态字段持有Fragment置空
②在Activity销毁时,将sActivity置空
-
匿名内部类:匿名内部类会持有外部类的引用,如果没有正确释放,可能导致外部类无法被垃圾回收。可以考虑使用静态内部类或弱引用来解决该问题。 (监听这类型的就解注册)
例如:MapCruiseManager.getInstance().setMapCruiseListener(mapCruiseListener);
这个 mapCruiseListener 是 MapIndexFragment 的一个匿名内部类实例,它隐式持有外部类 MapIndexFragment 的引用
如果 MapIndexFragment 被销毁但没有调用 MapCruiseManager.getInstance().setMapCruiseListener(null),就会导致以下引用链:
MapCruiseManager单例 -> mapCruiseListener(匿名内部类实例) -> MapIndexFragment实例 -
资源未关闭:在使用一些需要手动关闭的资源(如文件、数据库、网络连接等)时,务必在不再需要时及时关闭或释放这些资源。例如数据库cursor.close
-
单例模式:如果单例对象持有了其他对象的引用,并且这些对象不再需要时没有被正确释放,可能导致内存泄漏。确保在不再需要时及时释放相关的对象引用。
单例写法中不能传入短生命周期对象,比如activity,和静态变量那个类似 -
Handler引起的泄漏:Handler会持有外部类的引用,如果Handler没有被正确释放,可能导致外部类无法被垃圾回收。可以使用弱引用或使用removeCallbacksAndMessages()方法来解决该问题。
public class MainActivity extends AppCompatActivity {
private final Handler handler = new Handler(); // 隐式持有 Activity
private void doSomething() {
handler.postDelayed(() -> {
Toast.makeText(this, "hello", Toast.LENGTH_SHORT).show();
}, 30_000);
}
@Override
protected void onDestroy() {
super.onDestroy();
// 关键:移除所有回调
handler.removeCallbacksAndMessages(null);
}
}
- Context引起的泄漏:如果持有Context的对象没有被正确释放,可能导致Activity或其他Context相关的对象无法被垃圾回收。在不需要时及时释放对Context的引用。
比如单例模式把activity当成context传入,长生命周期对象拿住了短周期对象,短周期对象周期被拉长,就会导致泄漏
长寿命对象(单例、静态、线程、缓存)只能握 ApplicationContext;
非要握 Activity,就用弱引用或 LifecycleObserver,
并在 onDestroy() 里立刻置 null / remove / clear(),
PS:在安卓开发中,传入Context参数时,通常建议使用getApplicationContext()方法而不是使用Activity的实例,Application的生命周期与应用程序的生命周期一致,Activity的实例可能会在应用程序的生命周期结束后仍然保留对Context的引用,导致内存泄漏。
内存泄漏的原因:对象无法被GC回收(可以自动清空堆中不再使用的对象)
内存泄漏的本质是垃圾对象到GCRoot是可达的,换一种思路可以理解为就是引用者与被引用者的生命周期不一致
栈和堆的区别:
-
栈中存储的是方法调用的信息,包括方法的参数、局部变量、方法的返回地址等。
-
栈采用先进后出(FILO)的数据结构,即后进栈的数据先出栈。
-
栈的大小是固定的,当栈空间不足时会抛出 StackOverflowError 错误。
-
堆是一种共享的内存区域,用于存储对象实例。
-
所有的对象实例都存储在堆中,包括通过关键字
new创建的对象。 -
堆的大小是动态分配的,当堆空间不足时会触发垃圾回收机制进行垃圾回收,释放不再使用的对象占用的空间。
java7大类:
| 类别 | 是否有名字 | 持有外部引用 | 作用域 | 典型用途 |
|---|---|---|---|---|
| 顶级类 | ✅ | ❌ | 整个包 | 任何公共类 |
| 静态内部类 | ✅ | ❌ | 外部类内部 只能访问外部类的 static 成员 | 工具、Builder |
| 普通内部类 | ✅ | ✅ | 外部类内部 | 适配器、迭代器 |
| 局部内部类 | ✅ | ✅ | 方法/代码块内 | 一次性算法 |
| 匿名内部类 | ❌ | ✅ | 语句级 | 事件监听、回调 View.OnClickListener、Runnable、Comparator等 |
| 枚举 | ✅ | ❌ | 顶级或内部 | 常量、状态 |
| 记录类 | ✅ | ❌ | 顶级或内部 | 不可变数据载体 |
public class Outer { // ① 顶级类(Top-Level Class)
private int outerVal = 10;
/* ② 静态内部类 */
public static class StaticInner {
void show() { System.out.println("静态内部类不持有外部引用"); }
}
/* ③ 普通内部类(成员内部类)*/
public class Inner {
void show() { System.out.println("普通内部类可以访问 outerVal=" + outerVal); }
}
/* ④ 局部内部类(方法里)*/
public void method() {
class LocalInner { // 方法内部
void show() { System.out.println("局部内部类"); }
}
new LocalInner().show();
}
/* ⑤ 匿名内部类 */
public void anonymous() {
Runnable r = new Runnable() { // 没有名字,直接 new
@Override
public void run() { System.out.println("匿名内部类"); }
};
new Thread(r).start();
}
/* ⑥ 枚举(特殊的顶级类)*/
public enum Color { RED, GREEN, BLUE }
/* ⑦ 记录类(Java 16+,Android 14+ 已支持)*/
public record Point(int x, int y) {}
}
内存泄漏实践
弱引用:
只要 GC 发现对象只被 WeakReference 关联,就会立即回收它,不管内存是否充足
WeakReference只要 GC 就回收 /SoftReference内存不足时才回收
导航项目里没用到过软引用
一、问题分析
泄露时的引用链:SearchRouteResultFragment <- this2 <- native 层。
Java层向native层注册了一个匿名内部类实现的监听器。虽然页面退出时,删除了监听器,但native层一直持有监听器,导致页面泄露。
二、解决方案
- native 层问题反馈给业务层处理
- 客户端中,将监听器从匿名内部类改为静态内部类,在这个静态内部类持有外层页面的弱引用。在创建监听器实例的时候,将页面this引用传入构造函数创建页面的弱引用。这样就使用弱引用的页面调用外部类的对象了。
- 这样打断了强引用链,在页面退出时可以正常销毁。
以前的写法:
LineSearchEtaListener lineSearchEtaListener = new LineSearchEtaListener() {
@Override
public void onLineSearchEtaCallback(LineSearchEtaEvent lineSearchEtaEvent) {
if (lineSearchEtaEvent == LineSearchEtaEvent.SUCC) {
List<LineSearchEtaResult> resultList = LineSearchEtaManager.getInstance().getResult();
int index = -1;
for (int j = 0; j < mData.size(); j++) {
if (!mData.get(j).isHaveETA()) {
index = j;
break;
}
}
if (index == -1) {
return;
}
RouteBase currentRouteBase = InteractiveUtil.getProvider(ROUTE_PROVIDER, IRouteProvider.class).getCurrentRouteBase();
int currentTime = ETA.getInstance().getRemainingTime();
long currentLength = currentRouteBase.getLength();
for (int i = 0; i < resultList.size(); i++) {
int estimatedTime = resultList.get(i).getTime();
mData.get(index + i).setMoreTime(estimatedTime - currentTime);
int length = (int) resultList.get(i).getDistance();
mData.get(index + i).setLength((int) (length - currentLength));
mData.get(index + i).setHaveETA(true);
}
mAdapter.setNessaryCostMoreTime(true);
mAdapter.setmData(mData);
}
}
};
修改后的代码:
private MyLineSearchEtaListener lineSearchEtaListener = new MyLineSearchEtaListener(this);
private static class MyLineSearchEtaListener implements LineSearchEtaListener {
private WeakReference<SearchRouteResultFragment> mFragmentRef = null;
public MyLineSearchEtaListener(SearchRouteResultFragment fragment) {
mFragmentRef = new WeakReference<>(fragment);
}
@Override
public void onLineSearchEtaCallback(LineSearchEtaEvent lineSearchEtaEvent) {
if (mFragmentRef == null || mFragmentRef.get() == null) {
return;
}
SearchRouteResultFragment fragment = mFragmentRef.get();
if (lineSearchEtaEvent == LineSearchEtaEvent.SUCC) {
List<LineSearchEtaResult> resultList = LineSearchEtaManager.getInstance().getResult();
int index = -1;
for (int j = 0; j < fragment.mData.size(); j++) {
if (!fragment.mData.get(j).isHaveETA()) {
index = j;
break;
}
}
if (index == -1) {
return;
}
RouteBase currentRouteBase = InteractiveUtil.getProvider(ROUTE_PROVIDER, IRouteProvider.class).getCurrentRouteBase();
int currentTime = ETA.getInstance().getRemainingTime();
long currentLength = currentRouteBase.getLength();
for (int i = 0; i < resultList.size(); i++) {
int estimatedTime = resultList.get(i).getTime();
fragment.mData.get(index + i).setMoreTime(estimatedTime - currentTime);
int length = (int) resultList.get(i).getDistance();
fragment.mData.get(index + i).setLength((int) (length - currentLength));
fragment.mData.get(index + i).setHaveETA(true);
}
fragment.mAdapter.setNessaryCostMoreTime(true);
fragment.mAdapter.setmData(fragment.mData);
}
}
}
上面这种注册的监听,被注册的类是业务层的自己无法改 所以采用这种改法,如果是底层有取消注册的接口可以直接解注册即可,或者调用底层接口设置Listerner为null