分类 android 下的文章

Android 中的内存泄漏(Memory Leak)本质上是:长生命周期的对象持有了短生命周期对象(如 Activity/Fragment)的引用,导致短生命周期对象无法被垃圾回收器(GC)回收。

以下是 Android 开发中常见的内存泄漏类型、原理、示例以及检测和解决方法的系统性整理。


一、 Android 常见内存泄漏类型

1. 静态变量导致的泄漏 (Static Variable)

  • 原理:静态变量存储在方法区,其生命周期与 ClassLoader(通常等同于应用进程)一致。如果静态变量持有了 ActivityView 的引用,该对象将永远无法释放。
  • 例子

    public class MyUtils {
        // 错误:静态变量持有了 Activity 的 Context
        private static Context sContext;
    
        public static void setContext(Context context) {
            sContext = context;
        }
    }
    // 在 Activity 中调用 MyUtils.setContext(this); 导致 Activity 泄漏
  • 修正:尽量不存 Context。如果必须存,使用 context.getApplicationContext(),或使用 WeakReference

2. 单例模式导致的泄漏 (Singleton)

  • 原理:单例的生命周期贯穿整个应用。如果单例初始化时传入了 ActivityContext 并一直持有,该 Activity 就无法销毁。
  • 例子

    public class AppManager {
        private static AppManager instance;
        private Context context;
    
        private AppManager(Context context) {
            this.context = context; // 如果传入 Activity,泄漏发生
        }
    }
  • 修正:构造函数中强制转换:this.context = context.getApplicationContext();

3. 非静态内部类 / 匿名内部类 (Inner/Anonymous Class)

  • 核心原理:在 Java 中,非静态内部类和匿名内部类会隐式持有外部类(如 Activity)的强引用。如果这些内部类对象的生命周期比外部类长,外部类就无法回收。
  • 类型 A:Handler

    • 例子

      // 匿名内部类 Handler 持有 Activity 引用
      private final Handler mHandler = new Handler() { ... };
      // 发送延时消息,消息未处理完,Activity 关闭也无法回收
      mHandler.sendEmptyMessageDelayed(0, 10000);
    • 修正:使用 静态内部类 + WeakReference,且在 onDestroy 中调用 mHandler.removeCallbacksAndMessages(null)
  • 类型 B:Thread / AsyncTask

    • 例子

      new Thread(new Runnable() { // 匿名内部类 Runnable 持有 Activity
          @Override
          public void run() { SystemClock.sleep(20000); }
      }).start();
    • 修正:定义为静态内部类,或在 Activity 销毁时中断线程。
  • 类型 C:双括号初始化 (Double Brace Initialization)

    • 原理new HashMap() {{ ... }} 实际上创建了一个 HashMap匿名子类。这个子类会隐式持有外部类(Activity)的引用 this$0。如果这个 Map 被传递到 Activity 外部(如静态缓存、单例配置),Activity 就会泄漏。
    • 例子

      // 看起来是语法糖,实际是隐形炸弹
      public void initData() {
          Map<String, Object> map = new HashMap<String, Object>() {{
              put("key", "value"); // 这是一个匿名内部类的实例初始化块
          }};
          // 如果这个 map 被传给单例或静态变量,Activity 泄漏
          GlobalCache.getInstance().save(map);
      }
    • 修正

      • 使用普通写法:Map<String, Object> map = new HashMap<>(); map.put(...);
      • 使用 Kotlin:val map = mapOf("key" to "value")
      • 使用静态工具方法:Map.of(...) (Java 9+)

4. 资源未关闭 / 未注销 (Resource Not Closed)

  • 原理:使用了系统资源或注册了监听器,使用完毕后未关闭或注销。
  • 常见场景

    • BroadcastReceiver:忘记 unregisterReceiver
    • IO 流 / Cursor:数据库查询或文件读写后未 close()
    • EventBus / RxJavaonDestroy 中未取消订阅。
    • 属性动画:无限循环动画未调用 cancel(),View 被持有,导致 Activity 无法释放。

5. WebView 泄漏

  • 原理:WebView 系统实现复杂,会在 native 层持有 Context,且往往不随 Activity 销毁而立即彻底释放。
  • 修正

    • 不在 XML 中定义,改为代码动态 new WebView(getApplicationContext())
    • 销毁时:先从父容器 removeView,加载空内容 loadUrl("about:blank"),最后 destroy()
    • 多进程方案:将 WebView 放在独立进程,销毁时 System.exit(0)

二、 如何发现内存泄漏

1. LeakCanary (最推荐)

  • 介绍:Square 开源的检测库。
  • 原理:自动监测 Activity/Fragment 的销毁。若销毁后对象仍驻留堆内存,触发 GC 并分析引用链,通知开发者。
  • 优点:集成简单,实时报警,无需手动分析。

2. Android Studio Profiler (Memory Profiler)

  • 操作

    1. 运行 App,打开 Profiler -> Memory。
    2. 操作 App 后手动触发 GC。
    3. 点击 "Dump Java Heap"。
    4. 在结果中筛选 Activity 类,查看 Count(实例数)。若 Activity 已关闭但 Count > 0 且有关联引用,即为泄漏。

3. Lint 静态检查

  • Android Studio 自带的代码检查工具,能在编写代码时提示(例如:提示 "This Handler class should be static or leaks might occur")。

三、 总结:解决之道

解决的核心在于切断长生命周期对象对短生命周期对象的引用

  1. Context 敏感度

    • 原则:凡是跟 UI 渲染无关的(工具类、单例、全局配置),一律使用 Application Context
    • 警惕:严禁静态变量直接持有 ActivityView
  2. 内部类规范

    • 强制:Activity 中定义的 Handler、Thread、Runnable 尽量使用 静态内部类 (static class)
    • 引用:如果静态内部类需要引用 Activity,必须通过 WeakReference
    • 拒绝:禁止在 Activity 中随意使用 new ArrayList() {{ ... }} 这种双括号写法传递数据给外部。
  3. 生命周期对齐

    • 成对出现register 对应 unregisterbind 对应 unbind
    • 清理战场onDestroy 中必须移除 Handler 消息、取消 RxJava 订阅、停止动画。
  4. 工具常驻

    • 开发阶段集成 LeakCanary,将内存泄漏视为 Bug 处理,发现即修。

参数root
当参数root为null时,attachToRoot参数无效。且生成的view会丢失布局文件中的LayoutParams
当参数root不为null时,生成的view会根据root类型来保留布局文件中对应的LayoutParams

参数attachToRoot
attachToRoot为true时,返回的view就是root
attachToRoot为false时,返回的view是跟布局生成的view

问题:一个viewpager2使用了FragmentStateAdapter,当系统配置发生变化(切换横竖屏、切换深色模式、切换语言),会多出fragment实例,每切一次就会多一次。查看日志每次切换后获取fragment的时候,都会重新走FragmentStateAdapter的createFragment。而之前已经生成的fragment都会重建。所以造成上面的现象和问题。

原因:这个viewpager2是动态通过代码new创建的,改成直接写在xml布局中就正常了(一般写在xml中都会加一个id的)

根因:只有绑定id的viewpager2才能正常同步fragment。从xml加载的也需要加id才能。代码动态创建的,设置id后也能正确。

private void ensureFragment(int position) {
    long itemId = getItemId(position);
    if (!mFragments.containsKey(itemId)) {
        // TODO(133419201): check if a Fragment provided here is a new Fragment
        Fragment newFragment = createFragment(position);
        newFragment.setInitialSavedState(mSavedStates.get(itemId));
        mFragments.put(itemId, newFragment);
    }
}

FragmentStateAdapter源码显示,只有mFragments不包含的时候,才会从createFragment拿

@Override
public final void restoreState(@NonNull Parcelable savedState) {
    if (!mSavedStates.isEmpty() || !mFragments.isEmpty()) {
        throw new IllegalStateException(
                "Expected the adapter to be 'fresh' while restoring state.");
    }

    Bundle bundle = (Bundle) savedState;
    if (bundle.getClassLoader() == null) {
        /** TODO(b/133752041): pass the class loader from {@link ViewPager2.SavedState } */
        bundle.setClassLoader(getClass().getClassLoader());
    }

    for (String key : bundle.keySet()) {
        if (isValidKey(key, KEY_PREFIX_FRAGMENT)) {
            long itemId = parseIdFromKey(key, KEY_PREFIX_FRAGMENT);
            Fragment fragment = mFragmentManager.getFragment(bundle, key);
            mFragments.put(itemId, fragment);
            continue;
        }

        if (isValidKey(key, KEY_PREFIX_STATE)) {
            long itemId = parseIdFromKey(key, KEY_PREFIX_STATE);
            Fragment.SavedState state = bundle.getParcelable(key);
            if (containsItem(itemId)) {
                mSavedStates.put(itemId, state);
            }
            continue;
        }

        throw new IllegalArgumentException("Unexpected key in savedState: " + key);
    }

    if (!mFragments.isEmpty()) {
        mHasStaleFragments = true;
        mIsInGracePeriod = true;
        gcFragments();
        scheduleGracePeriodEnd();
    }
}

然后可以看见除了ensureFragment,只有在restoreState这个地方mFragments才会存fragment。关键是要从savedState拿到fragment。

继续跟源码可以发现adapter的restoreState方法是在viewpager2的restorePendingState方法中被调用的,其中传递的参数是mPendingAdapterState,继续看看mPendingAdapterState哪儿来的

@Override
protected void onRestoreInstanceState(Parcelable state) {
    if (!(state instanceof SavedState)) {
        super.onRestoreInstanceState(state);
        return;
    }

    SavedState ss = (SavedState) state;
    super.onRestoreInstanceState(ss.getSuperState());
    mPendingCurrentItem = ss.mCurrentItem;
    mPendingAdapterState = ss.mAdapterState;
}

mPendingAdapterState来自于viewpager2的onRestoreInstanceState。

protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
    if (mID != NO_ID) {
        Parcelable state = container.get(mID);
        if (state != null) {
            // Log.i("View", "Restoreing #" + Integer.toHexString(mID)
            // + ": " + state);
            mPrivateFlags &= ~PFLAG_SAVE_STATE_CALLED;
            onRestoreInstanceState(state);
            if ((mPrivateFlags & PFLAG_SAVE_STATE_CALLED) == 0) {
                throw new IllegalStateException(
                        "Derived class did not call super.onRestoreInstanceState()");
            }
        }
    }
}

可以看见只有当有id的时候才会执行onRestoreInstanceState

  1. 有需求在子进程中创建provider实例,需要添加android:multiprocess="true"或者直接指定android:process属性来指定进程
  2. 在主进程中,Provider 会在应用启动的时候自动初始化一次,创建实例,执行oncreate方法
  3. 系统不会自动在子进程中初始化 Provider,只有当子进程首次访问这个 Provider 时,系统才会在该进程中初始化一个新的 Provider 实例,只有创建实例时候才会执行oncreate方法
  4. 为了解决这个问题,可以主动初始化
public class ProviderUtils {
    public static void initializeProvider(Context context, ProviderInfo providerInfo) {
        try {
            // 通过 ContentResolver 触发 Provider 初始化
            Uri uri = Uri.parse("content://" + providerInfo.authority);
            ContentProviderClient client = context.getContentResolver()
                    .acquireContentProviderClient(uri);
            if (client != null) {
                // Provider 已初始化
                client.release();
            } else {
                // 尝试通过 call 方法触发初始化
                context.getContentResolver().call(
                    uri,
                    "init",
                    null,
                    null
                );
            }
        } catch (Exception e) {
            Log.e("ProviderUtils", "Failed to initialize provider: " + providerInfo.authority, e);
        }
    }

    // 初始化所有 Provider
    public static void initializeAllProviders(Context context) {
        List<ProviderInfo> providers = getAllProviders(context);
        for (ProviderInfo provider : providers) {
            initializeProvider(context, provider);
        }
    }
}

放在application oncreate或者其它合适的地方