Android 内存泄漏的调查

145 阅读6分钟

可能会造成内存泄漏的情况如下: 1.静态变量:如果一个对象被声明为静态变量,它将一直存在于内存中,即使不再被使用。静态变量的生命周期是跟随进程的,进程销毁,静态变量占用的内存就会被系统回收。
如果将一个Activity或者Fragment的实例赋值给一个静态变量,这个静态变量会持有对Activity或者Fragment的引用,导致无法被垃圾回收。解决方法是使用弱引用或者在不需要时及时释放引用

① 静态字段能放:
全局配置、不可变常量、与生命周期无关的轻量单例、WeakReference。
② 静态字段千万别放 短生命周期对象
Activity、Fragment、Service、View、Drawable、Bitmap、大型缓存列表、任何 Context(除非 ApplicationContext)。
在不需要时及时释放引用:
public static Activity sActivity;
①静态字段持有Fragment引用,在页面销毁时,将静态字段持有Fragment置空
②在Activity销毁时,将sActivity置空

  1. 匿名内部类:匿名内部类会持有外部类的引用,如果没有正确释放,可能导致外部类无法被垃圾回收。可以考虑使用静态内部类或弱引用来解决该问题。 (监听这类型的就解注册)
    例如:MapCruiseManager.getInstance().setMapCruiseListener(mapCruiseListener);
    这个 mapCruiseListener 是 MapIndexFragment 的一个匿名内部类实例,它隐式持有外部类 MapIndexFragment 的引用
    如果 MapIndexFragment 被销毁但没有调用 MapCruiseManager.getInstance().setMapCruiseListener(null),就会导致以下引用链:
    MapCruiseManager单例 -> mapCruiseListener(匿名内部类实例) -> MapIndexFragment实例

  2. 资源未关闭:在使用一些需要手动关闭的资源(如文件、数据库、网络连接等)时,务必在不再需要时及时关闭或释放这些资源。例如数据库cursor.close

  3. 单例模式:如果单例对象持有了其他对象的引用,并且这些对象不再需要时没有被正确释放,可能导致内存泄漏。确保在不再需要时及时释放相关的对象引用。
    单例写法中不能传入短生命周期对象,比如activity,和静态变量那个类似

  4. 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);
    }
}
  1. 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 <- this0SearchRouteResultFragment0 SearchRouteResultFragment2 <- native 层。
Java层向native层注册了一个匿名内部类实现的监听器。虽然页面退出时,删除了监听器,但native层一直持有监听器,导致页面泄露。
二、解决方案

  1. native 层问题反馈给业务层处理
  2. 客户端中,将监听器从匿名内部类改为静态内部类,在这个静态内部类持有外层页面的弱引用。在创建监听器实例的时候,将页面this引用传入构造函数创建页面的弱引用。这样就使用弱引用的页面调用外部类的对象了
  3. 这样打断了强引用链,在页面退出时可以正常销毁。

以前的写法:

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

blog.csdn.net/weixin_4110…