LeakCanary原理解析

1. 内存泄露的定义

  • 传统定义:

    • 申请的内存忘记释放了。
  • Android(或 JVM)的内存泄露:

    • 短生命周期的对象被长生命周期的对象持有,导致短生命周期的对象不能被垃圾回收器释放。

2. 垃圾回收机制

Java 和其他语言采用不同的垃圾回收策略,主要包括两种:

2.1 引用计数法

  • 应用语言:

    • Python、Objective-C、Swift 等
  • 原理:

    • 用一个计数器记录对象被引用的次数,当引用计数降到 0 时,该对象被认为是垃圾对象。
  • 缺点:

    • 存在循环引用问题,可能导致垃圾对象无法被回收。

2.2 可达性分析法

  • 应用语言:

    • Java(JVM)
  • 原理:

    • JVM 从一组被称为 GC Roots 的对象出发,向下搜索所有可达对象。
    • 如果一个对象能被 GC Roots 引用到,则认为它不是垃圾;否则,即使对象间互相引用,也会被视为垃圾。
  • GC Roots 包括:

    • 线程栈中的局部变量(正在调用的方法中的参数和局部变量)
    • 存活的线程对象
    • JNI 的引用
    • Class 对象(因为 Android 加载的 Class 通常不会卸载)
    • 静态变量中引用的对象

3. 内存泄露问题的影响

  • 内存泄露不会立刻导致程序崩溃,但随着应用使用,被长生命周期对象持有的短生命周期对象无法回收,导致可用内存逐渐减少。
  • 当内存耗尽时,可能在应用的任何位置抛出 OutOfMemoryError,而每次错误堆栈可能都不同,增加问题排查难度。

4. LeakCanary 监听内存泄漏的整体流程和核心原理概要

LeakCanary的粗略流程就如下图所示:

1. 初始化和注册

  • Application 注册

    在 Application 的 onCreate() 方法中,先判断当前进程是否为 LeakCanary 分析进程,避免在该进程中初始化应用逻辑,然后调用 LeakCanary.install(this)

    kotlin 复制代码
    public class MyApp extends Application {
       @Override
       public void onCreate() {
           super.onCreate();
           if (LeakCanary.isInAnalyzerProcess(this)) {
               return;
           }
           LeakCanary.install(this);
       }
    }
  • 构建 RefWatcher
    LeakCanary.install() 内部会调用 refWatcher() 方法创建一个 AndroidRefWatcherBuilder,并设置用于接收内存泄漏分析结果的 listener(例如 DisplayLeakService),以及排除 Android SDK 或手机厂商修改引入的已知泄漏对象(excludedRefs)。

  • 注册生命周期回调

    buildAndInstall() 中,根据配置:

    • 注册 Activity 生命周期回调(通过 ActivityRefWatcher),在 Activity 的 onDestroyed() 时调用 refWatcher.watch(activity) 进行监控。
    • 注册 Fragment 生命周期回调(通过 FragmentRefWatcher.Helper),在 Fragment 销毁时同样监控相关对象。

2. 监控对象及弱引用机制

  • 监控目标

    LeakCanary 关注的对象通常是那些生命周期结束后应被回收的对象,如 Activity、Fragment、Service 或其他大对象。

  • 弱引用实现

    • 当调用 RefWatcher.watch(watchedReference, referenceName) 时,LeakCanary 为被观察对象创建一个自定义的弱引用 KeyedWeakReference(继承自 WeakReference),同时生成一个唯一的 key,并将这个 key 添加到内部的"怀疑名单"(retainedKeys)。
    • 此弱引用关联了一个 ReferenceQueue。当垃圾回收器回收目标对象后,该弱引用会被加入队列,从而可以通过轮询队列确认该对象是否真的被回收。
  • 检测回收情况

    • 在异步任务中(由 WatchExecutor 执行),首先调用 removeWeaklyReachableReferences() 从队列中移除已经回收对象对应的 key。
    • 如果 retainedKeys 中仍包含目标对象的 key,说明对象没有被回收,即可能存在内存泄漏。

3. 触发垃圾回收与堆转储

  • 主动触发 GC

    如果检测到被观察对象依然存在(即弱引用未被清除),LeakCanary 会主动调用 gcTrigger.runGc() 触发一次垃圾回收,再次清理弱引用。

  • 堆转储(Heap Dump)

    • 如果在再次检测后目标对象依然存在,则认为该对象未能正常释放,LeakCanary 开始执行堆转储,通过调用 heapDumper.dumpHeap() 获取 hprof 文件。
    • 为避免系统干扰,还会显示一个通知,并通过 Toast 等机制确认堆转储时机。

4. 堆文件分析与泄漏确认

  • 堆分析服务

    堆转储完成后,LeakCanary 会将转储文件、目标对象的 key 以及其他监控信息封装成一个 HeapDump 对象,并将其传递给监听器服务(如 DisplayLeakService)。

  • 独立进程分析

    在独立进程中,HeapAnalyzer 会加载 hprof 文件,对整个内存快照进行解析:

    • 查找 KeyedWeakReference 实例,验证目标对象是否真的存在。
    • 分析从 GC Roots 到该对象的引用路径,生成泄漏链(Leak Trace)。
    • 结合分析耗时等信息构建最终的 AnalysisResult
  • 结果展示

    分析结果最终通过 AbstractAnalysisResultService.sendResultToListener() 发送到指定的监听服务,开发者可以在 LeakCanary 提供的 UI 中查看详细的泄漏信息,也可以将结果上报服务器。

5. 总结

LeakCanary 的内存泄漏检测原理可以归纳为以下几点:

  • 注册与监控
    通过在 Application 中初始化,注册 Activity 和 Fragment 生命周期回调,在对象销毁时调用 watch() 监控待回收对象。
  • 弱引用与引用队列
    使用自定义的弱引用(KeyedWeakReference)与 ReferenceQueue 来检测对象是否已被垃圾回收,通过 retainedKeys 记录未回收的对象标识。
  • 主动 GC 与堆转储
    当检测到对象未被回收时,主动触发垃圾回收、转储堆内存数据,并启动独立进程进行详细分析。
  • 泄漏路径分析
    通过解析堆转储文件,生成对象的引用路径图,帮助定位导致内存泄漏的原因。

5. LeakCanary 原理详细解析

5.1 引用类型

在开始之前,我们需要先做一些前置知识的准备,了解四种引用类型。

  • 强引用:

    • 对象只要被强引用,就不会被垃圾回收。
  • 弱引用:

    • 可以通过 get() 方法获得引用的对象,若对象被垃圾回收,则返回 null
    • 在对象被回收前,弱引用会被放入关联的队列中,可用来判断对象是否被回收。
  • 软引用:

    • 类似弱引用,但在内存不足时才会被回收。
  • 虚引用:

    • 不能通过 get() 获得对象,主要用于跟踪对象回收的状态。

5.2 监控原理

LeakCanary 是一个用于检测内存泄露的工具,由于2.0以上的版本对其内容做了改动,为了方便我们理解原理,于是选取了1.6.3的版本,其内部的基础是:

  • LeakCanary 通过注册 Application 和 Activity 的生命周期回调,在 Activity 和 Fragment 销毁时开始观察其内存状态。
  • 1.x 版本的 LeakCanary 主要监控 support 包和 API 26 以上的 Fragment。

源码流程分析:

1. 完成LeakCanary的注册:

kotlin 复制代码
public class MyApp extends Application {
   @Override
   public void onCreate() {
       super.onCreate();
       // 判断是否在主进程中
       if (LeakCanary.isInAnalyzerProcess(this)) {
           // This process is dedicated to LeakCanary for heap analysis.
           // You should not init your app in this process.
           return;
       }
       // 使用LeakCanary
       LeakCanary.install(this);
   }
}

2. 进入LeakCanary#install(Application application)

install方法内部完成了整个LeakCanary的初始化,RefWatcher 字面意思就是引用观察者,

  • listenerServiceClass() 方法:允许指定一个自定义的 Service 类(继承自 AbstractAnalysisResultService),用于接收内存泄漏分析结果并发送通知。
  • excludedRefs:无法处理的内存泄漏,例如三方SDK的泄漏,AndroidExcludedRefs里边包含了一些AndroidSDK自带的内存泄漏,还有手机厂商修改带来的额外的内存泄漏
  • buildAndInstall() : 创建监察者。
kotlin 复制代码
/**
 * Creates a {@link RefWatcher} that works out of the box, and starts watching activity
 * references (on ICS+).
 */
public static @NonNull RefWatcher install(@NonNull Application application) {
  return refWatcher(application).listenerServiceClass(DisplayLeakService.class) // DisplayLeakService 可以自定义,可以把分析结果上送服务器。
      // 无法处理的内存泄漏,例如三方SDK的泄漏,AndroidExcludedRefs里边包含了一些AndroidSDK自带的内存泄漏,还有手机产商修改带来的额外的内存泄漏
      .excludedRefs(AndroidExcludedRefs.createAppDefaults().build()) 
      .buildAndInstall();
}

// 创建一个用于观察引用的对象
public static @NonNull AndroidRefWatcherBuilder refWatcher(@NonNull Context context) {
  return new AndroidRefWatcherBuilder(context);
}

// 手机厂商修改导致的内存泄漏
import static com.squareup.leakcanary.internal.LeakCanaryInternals.HUAWEI;
import static com.squareup.leakcanary.internal.LeakCanaryInternals.LENOVO;
import static com.squareup.leakcanary.internal.LeakCanaryInternals.LG;
import static com.squareup.leakcanary.internal.LeakCanaryInternals.MEIZU;
import static com.squareup.leakcanary.internal.LeakCanaryInternals.MOTOROLA;
import static com.squareup.leakcanary.internal.LeakCanaryInternals.NVIDIA;
import static com.squareup.leakcanary.internal.LeakCanaryInternals.SAMSUNG;
import static com.squareup.leakcanary.internal.LeakCanaryInternals.VIVO;

3. 深入AndroidRefWatcherBuilder#buildAndInstall()

  • 单例限制:首先检查是否已有安装的 RefWatcher,确保只在当前进程中调用一次。

  • 构建 RefWatcher:调用内部 build() 方法构建 RefWatcher 实例。

  • 监控配置

    • 如果不是 DISABLED(即未在 release 模式下禁用检测),则根据配置决定是否启用显示泄漏的 Activity(DisplayLeakActivity)。
    • 根据 watchActivities 和 watchFragments 配置,分别安装 Activity 和 Fragment 的生命周期监控器。
  • 保存实例:将构建好的 RefWatcher 保存到全局变量中,确保后续统一使用。

  • 返回实例:返回构建好的 RefWatcher,后续可以用于定制其他监控需求。

kotlin 复制代码
/**
 * 创建一个 RefWatcher 实例,并将该实例保存在 LeakCanary 内部,
 * 以便后续通过 LeakCanary.installedRefWatcher() 访问到该实例。
 * 同时,如果配置了监控 Activity 和 Fragment,则会自动注册生命周期回调,
 * 在 Activity 或 Fragment 销毁时开始监控它们是否会被正确回收。
 *
 * @throws UnsupportedOperationException 如果在同一进程中多次调用此方法(只允许初始化一次)
 */
public @NonNull RefWatcher buildAndInstall() {
  // 检查是否已经初始化过 RefWatcher,如果已经安装则抛出异常,防止重复初始化
  if (LeakCanaryInternals.installedRefWatcher != null) {
    throw new UnsupportedOperationException("buildAndInstall() should only be called once.");
  }
  
  // 调用内部 build() 方法构造 RefWatcher 实例
  RefWatcher refWatcher = build();
  
  // 如果构造的 RefWatcher 不是 DISABLED(即当前不是 release 版的空实现)
  if (refWatcher != DISABLED) {
    // 如果启用了显示泄漏分析 Activity 的功能(默认开启),则异步设置使得
    // 分析结果对应的 Activity 图标可以在应用列表中显示出来
    if (enableDisplayLeakActivity) {
      LeakCanaryInternals.setEnabledAsync(context, DisplayLeakActivity.class, true);
    }
  
    // 如果配置为监控 Activity,则安装 ActivityRefWatcher,
    // 通过注册 Application 的生命周期回调在 Activity 销毁时开始监控
    if (watchActivities) {
      ActivityRefWatcher.install(context, refWatcher);
    }
    // 如果配置为监控 Fragment,则安装 FragmentRefWatcher,
    // 通过注册 Activity 生命周期回调,在 Activity 创建时安装 Fragment 监控器
    if (watchFragments) {
      FragmentRefWatcher.Helper.install(context, refWatcher);
    }
  }
  
  // 将构造好的 RefWatcher 保存在全局变量中,保证在当前进程中唯一
  LeakCanaryInternals.installedRefWatcher = refWatcher;
  
  // 返回 RefWatcher 对象,这个对象后续还可以用于定制监控其他对象(例如大对象、Service等)
  return refWatcher;
}

/**
 * 设置一个自定义的 {@link AbstractAnalysisResultService} 用于接收内存泄漏分析结果。
 * 该方法将覆盖之前对 {@link #heapDumpListener(HeapDump.Listener)} 的设置。
 *
 * @param listenerServiceClass 用于监听堆转储分析结果并发送通知的 Service 类,
 *                             该类必须继承自 AbstractAnalysisResultService。
 * @return 当前 AndroidRefWatcherBuilder 对象,支持链式调用以进一步定制 LeakCanary 配置。
 */
public @NonNull AndroidRefWatcherBuilder listenerServiceClass(
    @NonNull Class<? extends AbstractAnalysisResultService> listenerServiceClass) {
  // 检查传入的 listenerServiceClass 是否为 DisplayLeakService 或其子类,
  // 如果是,则启用显示泄漏分析 Activity(即使分析结果的图标能够在 Launcher 中显示)
  enableDisplayLeakActivity = DisplayLeakService.class.isAssignableFrom(listenerServiceClass);
  
  // 创建一个新的 ServiceHeapDumpListener 实例,该 listener 用于在堆转储分析完成后接收结果,
  // 并调用 heapDumpListener() 将其设置到当前构建器中
  return heapDumpListener(new ServiceHeapDumpListener(context, listenerServiceClass));
}
  • ActivityRefWatcher: 在 Activity 被销毁后(调用 onDestroyed 后),自动将该 Activity 对象交给 RefWatcher 进行内存泄漏检测。具体作用如下:
  1. 注册生命周期回调

    • 通过调用 install() 方法,将一个包含自定义生命周期回调(这里继承自 ActivityLifecycleCallbacksAdapter)的对象注册到 Application 上。
    • 在这个回调中,重点关注 onActivityDestroyed(Activity activity) 方法,当某个 Activity 销毁时,自动调用 refWatcher.watch(activity)
  2. 自动检测内存泄漏

    • 当 Activity 被销毁后,RefWatcher 会对该 Activity 进行监控,判断其是否能被正常回收,避免出现内存泄漏情况。
  3. 便捷的安装和卸载

    • 提供 watchActivities()stopWatchingActivities() 方法,方便在运行时动态开启或停止对 Activity 生命周期的监控,确保不会重复注册回调。
kotlin 复制代码
public final class ActivityRefWatcher {

  public static void installOnIcsPlus(@NonNull Application application,
      @NonNull RefWatcher refWatcher) {
    install(application, refWatcher);
  }
  
  // 下边的代码想做的事情就是对已经调用了onDestroyed的Activity进行监视
  public static void install(@NonNull Context context, @NonNull RefWatcher refWatcher) {
    Application application = (Application) context.getApplicationContext();
    ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);
	  // application通过传入Activity的lifecycleCallbacks对象,就能够实现对所有Activity生命周期方法调用的监控,
    // 并且方法中还能够获取到Activity的对象,但是没有给Service添加这样的方法。
    application.registerActivityLifecycleCallbacks(activityRefWatcher.lifecycleCallbacks);
  }

  private final Application.ActivityLifecycleCallbacks lifecycleCallbacks =
      new ActivityLifecycleCallbacksAdapter() {
	      // 当某一个Activity的onDestroyed被调用的时候,就会触发这里。
        @Override public void onActivityDestroyed(Activity activity) {
	        // 对已经调用了onDestroyed进行监视。
          refWatcher.watch(activity);
        }
      };

  private final Application application;
  private final RefWatcher refWatcher;

  private ActivityRefWatcher(Application application, RefWatcher refWatcher) {
    this.application = application;
    this.refWatcher = refWatcher;
  }

  public void watchActivities() {
    // Make sure you don't get installed twice.
    stopWatchingActivities();
    application.registerActivityLifecycleCallbacks(lifecycleCallbacks);
  }

  public void stopWatchingActivities() {
    application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks);
  }
}
  • FragmentRefWatcher :LeakCanary 利用这部分代码自动注册 Fragment 生命周期监听器。通过在 Activity 创建时,安装针对 Fragment 的监控器,可以在 Fragment 销毁时(或其 View 销毁时)调用 refWatcher.watch(),从而检测这些对象是否在合适时机被垃圾回收。
  1. 针对不同类型的 Fragment:

    • 对于 Android O 及以上的原生 Fragment,使用 AndroidOFragmentRefWatcher
    • 对于 Support 包中的 Fragment,尝试通过反射加载并使用 SupportFragmentRefWatcher(前提是项目中包含了对应依赖,不过现在的项目应该很少存在Support包了)。
kotlin 复制代码
public interface FragmentRefWatcher {

  /**
   * 注册当前 Activity 中 Fragment 的生命周期回调,
   * 以便在 Fragment 的关键生命周期事件发生时进行内存泄漏检测。
   *
   * @param activity 当前要监控的 Activity
   */
  void watchFragments(Activity activity);

  final class Helper {

    // SupportFragmentRefWatcher 类名,用于检测是否引入了 LeakCanary 的支持包
    private static final String SUPPORT_FRAGMENT_REF_WATCHER_CLASS_NAME =
        "com.squareup.leakcanary.internal.SupportFragmentRefWatcher";

    /**
     * 安装 FragmentRefWatcher 的辅助类,
     * 根据当前环境和依赖情况,动态构造适用于不同 Fragment 类型的监控器,
     * 并注册 Activity 生命周期回调,在 Activity 创建时为其内部的 Fragment 注册监控。
     *
     * @param context     应用上下文
     * @param refWatcher  用于监控对象是否被回收的 RefWatcher 实例
     */
    public static void install(Context context, RefWatcher refWatcher) {
      // 用于存储构造出的 Fragment 监控器
      List<FragmentRefWatcher> fragmentRefWatchers = new ArrayList<>();

      // (1)原生 Fragment:Android 26(Oreo)及以上的系统支持对原生 Fragment 进行监控
      //     注意:在 Android 26 之前,自带的 Fragment 不支持内存泄漏检测,
      //           同时 AndroidX 包下的 Fragment 同样不支持 LeakCanary 的自动监控。
      if (SDK_INT >= O) {
        fragmentRefWatchers.add(new AndroidOFragmentRefWatcher(refWatcher));
      }

      try {
        // (2)Support Fragment:尝试通过反射加载 SupportFragmentRefWatcher 类,
        //     前提是项目引入了 com.squareup.leakcanary:leakcanary-support-fragment 依赖。
        Class<?> fragmentRefWatcherClass = Class.forName(SUPPORT_FRAGMENT_REF_WATCHER_CLASS_NAME);
        Constructor<?> constructor =
            fragmentRefWatcherClass.getDeclaredConstructor(RefWatcher.class);
        // 创建针对 Support 包下 Fragment 的监控器实例
        FragmentRefWatcher supportFragmentRefWatcher =
            (FragmentRefWatcher) constructor.newInstance(refWatcher);
        fragmentRefWatchers.add(supportFragmentRefWatcher);
      } catch (Exception ignored) {
        // 如果反射失败(例如依赖不存在),则忽略异常
      }

      // 如果没有任何 Fragment 监控器可用,则直接返回,不做安装
      if (fragmentRefWatchers.size() == 0) {
        return;
      }

      // 构造 Helper 实例,将创建好的 FragmentRefWatcher 列表传入
      Helper helper = new Helper(fragmentRefWatchers);

      // 获取全局 Application 实例,用于注册 Activity 生命周期回调
      Application application = (Application) context.getApplicationContext();
      // 注册一个 Activity 生命周期回调监听器,
      // 在 Activity 创建时,为该 Activity 内所有 Fragment 注册监控
      application.registerActivityLifecycleCallbacks(helper.activityLifecycleCallbacks);
    }

    // 定义 Activity 生命周期回调,用于在 Activity 创建时为其中的 Fragment 安装监控器
    private final Application.ActivityLifecycleCallbacks activityLifecycleCallbacks =
        new ActivityLifecycleCallbacksAdapter() {
          @Override 
          public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
            // 当 Activity 创建时,遍历所有已构造的 Fragment 监控器,
            // 并调用它们的 watchFragments 方法为当前 Activity 注册 Fragment 生命周期监听器
            for (FragmentRefWatcher watcher : fragmentRefWatchers) {
              watcher.watchFragments(activity);
            }
          }
        };

    // 存储所有构造的 FragmentRefWatcher 实例
    private final List<FragmentRefWatcher> fragmentRefWatchers;

    private Helper(List<FragmentRefWatcher> fragmentRefWatchers) {
      this.fragmentRefWatchers = fragmentRefWatchers;
    }
  }
}


/**
 * 针对 Android O 及以上版本原生 Fragment 的内存泄漏监控实现。
 * 监控内容包括:Fragment 自身和它的 View 在销毁时是否被正确回收。
 */
class AndroidOFragmentRefWatcher implements FragmentRefWatcher {

  private final RefWatcher refWatcher;

  AndroidOFragmentRefWatcher(RefWatcher refWatcher) {
    this.refWatcher = refWatcher;
  }

  // 定义 Fragment 生命周期回调,关注两个关键事件:
  // 1. Fragment 的 View 被销毁(onFragmentViewDestroyed)
  // 2. Fragment 实例被销毁(onFragmentDestroyed)
  private final FragmentManager.FragmentLifecycleCallbacks fragmentLifecycleCallbacks =
      new FragmentManager.FragmentLifecycleCallbacks() {
        @Override 
        public void onFragmentViewDestroyed(FragmentManager fm, Fragment fragment) {
          // 获取当前 Fragment 的 View 对象
          View view = fragment.getView();
          if (view != null) {
            // 当 Fragment 的 View 销毁时,调用 RefWatcher 监控该 View,
            // 以便检测该 View 是否最终能被垃圾回收,防止内存泄漏
            refWatcher.watch(view);
          }
        }
		
        @Override
        public void onFragmentDestroyed(FragmentManager fm, Fragment fragment) {
          // 当 Fragment 本身被销毁时,同样调用 RefWatcher 监控该 Fragment 对象
          refWatcher.watch(fragment);
        }
      };

  /**
   * 注册当前 Activity 中原生 Fragment 的生命周期回调,
   * 这样当 Activity 中的 Fragment 的关键生命周期事件发生时(例如销毁),
   * 就会自动调用相应回调进行内存泄漏检测。
   *
   * @param activity 当前需要监控 Fragment 的 Activity
   */
  @Override 
  public void watchFragments(Activity activity) {
    // 获取当前 Activity 的 FragmentManager
    FragmentManager fragmentManager = activity.getFragmentManager();
    // 注册 FragmentLifecycleCallbacks 回调,第二个参数 true 表示递归注册(监控嵌套的 Fragment)
    fragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true);
  }
}

4. RefWatcher内存泄漏检测核心类的分析

(1)内存泄漏的监察与捕捉:

关键点说明:

  • ReferenceQueue 的作用

    当被监控的对象(通过 KeyedWeakReference 包装)仅由弱引用持有时,一旦对象被垃圾回收,该弱引用会自动入队,从而通过 removeWeaklyReachableReferences() 方法将其 key 从怀疑名单中移除。

  • retainedKeys 集合

    用于保存所有待检测的对象的唯一标识(key)。如果某个 key 长时间未被移除,则表示对应对象没有被回收,可能存在内存泄漏。

  • 日志的分析与导出都在这个服务中处理,该服务运行在独立进程中:

js 复制代码
<service
    android:name="com.squareup.leakcanary.internal.HeapAnalyzerService"
    android:enabled="false"
    android:process=":leakcanary" />
  • ensureGone 方法
    核心检测逻辑:在一定时间内检查对象是否已被回收;如果未被回收,主动触发 GC,最终导出堆转储文件以进一步分析泄漏原因。
kotlin 复制代码
public final class RefWatcher {

  // 当 RefWatcher 被禁用时,使用一个空实现
  public static final RefWatcher DISABLED = new RefWatcherBuilder<>().build();
  
  // 任务执行器,用于异步执行检测任务
  private final WatchExecutor watchExecutor;
  // 用于检测调试器是否已连接,调试器可能会影响 GC 行为
  private final DebuggerControl debuggerControl;
  // 触发垃圾回收的工具
  private final GcTrigger gcTrigger;
  // 用于导出堆信息(hprof文件)的工具
  private final HeapDumper heapDumper;
  // 用于接收堆转储后分析结果的监听器
  private final HeapDump.Listener heapdumpListener;
  // 用于构建 HeapDump 对象(封装堆转储相关信息)的构造器
  private final HeapDump.Builder heapDumpBuilder;
  // 怀疑对象集合,用来存储待检测(可能泄漏)的对象的唯一标识(key)
  private final Set<String> retainedKeys;
  // 弱引用关联的队列,当被观察对象被回收后,其对应的弱引用会自动加入此队列
  private final ReferenceQueue<Object> queue;

  // 构造函数,初始化所有必需的组件和内部集合
  RefWatcher(WatchExecutor watchExecutor, DebuggerControl debuggerControl, GcTrigger gcTrigger,
      HeapDumper heapDumper, HeapDump.Listener heapdumpListener, HeapDump.Builder heapDumpBuilder) {
    this.watchExecutor = checkNotNull(watchExecutor, "watchExecutor");
    this.debuggerControl = checkNotNull(debuggerControl, "debuggerControl");
    this.gcTrigger = checkNotNull(gcTrigger, "gcTrigger");
    this.heapDumper = checkNotNull(heapDumper, "heapDumper");
    this.heapdumpListener = checkNotNull(heapdumpListener, "heapdumpListener");
    this.heapDumpBuilder = heapDumpBuilder;
    retainedKeys = new CopyOnWriteArraySet<>();
    // 创建一个 ReferenceQueue,用于跟踪被观察对象的弱引用是否被加入队列(即对象是否被回收)
    queue = new ReferenceQueue<>();
  }

  /**
   * 简化接口:使用空字符串作为引用名称
   */
  public void watch(Object watchedReference) {
    watch(watchedReference, "");
  }

  /**
   * 监控指定对象,观察它是否能够被 GC 回收。该方法会在Activity的OnDestroy 或者是Fragment的OnDestroyView 或者是Fragment的OnDestroy 执行后开始执行,
   * 该方法为非阻塞,实际检查任务由 watchExecutor 异步执行。
   *
   * @param referenceName 用于标识被观察对象的逻辑名称
   */
  public void watch(Object watchedReference, String referenceName) {
    // 如果当前 RefWatcher 已经被禁用,则直接返回,release包就会返回。
    if (this == DISABLED) {
      return;
    }
    checkNotNull(watchedReference, "watchedReference");
    checkNotNull(referenceName, "referenceName");
    // 记录监控开始时间(单位:纳秒),用于计算观察时长
    final long watchStartNanoTime = System.nanoTime();
    // 为被观察对象生成一个唯一标识 key
    String key = UUID.randomUUID().toString();
    // 将该 key 加入待观察的集合中,表示该对象应该在未来被回收
    retainedKeys.add(key);
    // 创建一个 KeyedWeakReference,将被观察对象包装为弱引用,并关联到 ReferenceQueue
    final KeyedWeakReference reference =
        new KeyedWeakReference(watchedReference, key, referenceName, queue);

    // 异步执行检测任务,确保对象能够被垃圾回收
    ensureGoneAsync(watchStartNanoTime, reference);
  }

  /**
   * 清除所有已监控的对象,停止监控
   */
  public void clearWatchedReferences() {
    retainedKeys.clear();
  }

  // 检查所有已监控对象是否已被回收后,判断待观察集合是否为空
  boolean isEmpty() {
    removeWeaklyReachableReferences();
    return retainedKeys.isEmpty();
  }

  HeapDump.Builder getHeapDumpBuilder() {
    return heapDumpBuilder;
  }

  Set<String> getRetainedKeys() {
    return new HashSet<>(retainedKeys);
  }

  // 异步检测:通过 watchExecutor 异步执行确保对象已被回收的检测任务
  private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
    watchExecutor.execute(new Retryable() {
      @Override public Retryable.Result run() {
        // 在子线程中检测对象是否已被回收
        return ensureGone(reference, watchStartNanoTime);
      }
    });
  }
  
  // watchExecutor的具体实现:

  // 以下是 watchExecutor 的执行逻辑,根据当前线程是否为主线程选择不同的等待方式
  @Override public void execute(@NonNull Retryable retryable) {
    if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
      // 如果在主线程,则等待主线程空闲后执行
      waitForIdle(retryable, 0);
    } else {
      // 否则直接在后台线程执行
      postWaitForIdle(retryable, 0);
    }
  }

  /**
   * 核心检测逻辑:确保被观察对象已经被垃圾回收
   * 1. 记录 GC 开始时间,并计算观察对象的存活时长
   * 2. 轮询 ReferenceQueue 移除已回收的引用(更新 retainedKeys)
   * 3. 如果对象仍未被回收,则主动触发 GC,再次检测
   * 4. 如果对象仍未被回收,则导出堆信息,并启动堆分析
   */
  @SuppressWarnings("ReferenceEquality") // 显式地检查引用相等性
  Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
    // 记录触发垃圾回收检测的时间点
    long gcStartNanoTime = System.nanoTime();
    // 计算从开始监控到触发 GC 的时长(毫秒)
    long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
    // 移除已被回收的对象对应的 key(从 ReferenceQueue 中轮询)
    removeWeaklyReachableReferences();

    // 如果调试器附加在进程上,可能会导致虚假泄漏,直接返回重试状态
    if (debuggerControl.isDebuggerAttached()) {
      return RETRY;
    }
    // 如果该对象已被回收(对应 key 已被移除),则返回 DONE 表示检测结束
    if (gone(reference)) {
      return DONE;
    }
    // 主动触发一次垃圾回收
    gcTrigger.runGc();
    // 再次清除 ReferenceQueue 中的已回收引用
    removeWeaklyReachableReferences();
    // 如果对象依然存在(未被回收),说明可能存在内存泄漏
    if (!gone(reference)) {
      long startDumpHeap = System.nanoTime();
      // 计算 GC 触发后至开始堆转储的耗时(毫秒)
      long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
      // 导出当前堆的快照到文件
      File heapDumpFile = heapDumper.dumpHeap();
      // 如果无法导出堆文件(例如权限问题),返回 RETRY 状态
      if (heapDumpFile == RETRY_LATER) {
        return RETRY;
      }
      // 计算堆转储耗时(毫秒)
      long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);

      // 构建一个 HeapDump 对象,包含堆文件、观察对象的 key、名称以及各个阶段的耗时信息
      HeapDump heapDump = heapDumpBuilder.heapDumpFile(heapDumpFile).referenceKey(reference.key)
          .referenceName(reference.name)
          .watchDurationMs(watchDurationMs)
          .gcDurationMs(gcDurationMs)
          .heapDumpDurationMs(heapDumpDurationMs)
          .build();
      // 通过监听器启动堆分析服务,进一步分析内存泄漏
      heapdumpListener.analyze(heapDump);

      // 分析服务内部的处理(例如:HeapAnalyzerService.runAnalysis)
      @Override public void analyze(@NonNull HeapDump heapDump) {
        checkNotNull(heapDump, "heapDump");
        HeapAnalyzerService.runAnalysis(context, heapDump, listenerServiceClass);
      }
    }
    // 对象已经被回收或者完成检测,返回 DONE
    return DONE;
  }

  // 判断目标对象是否已被回收:如果 retainedKeys 中不包含该对象的 key,则认为已被回收
  private boolean gone(KeyedWeakReference reference) {
    return !retainedKeys.contains(reference.key);
  }
  
	// 不断的从队列中取出对象,引用的观察对象的弱引用,回收之前,queue就是作为弱引用的构造传入的队列,在垃圾回收的时候,
	// 如果发现某个对象只被弱引用引用,那么这个对象就会被回收,并且在回收之前,会把这个弱引用添加到它构造方法中弱引用关联的队列中。
	// 在watch方法中,为观察的对象创建了一个弱引用,如果这个观察的目标被垃圾回收了,那么这里的弱引用就会被放入到队列当中。
	// 如果能够在这个里边取出弱引用,那么就说明观察的对象就被回收了,所以就从怀疑名单中给移除掉。如果经过了这一系列步骤。retainedKeys还存在着key,说明还没有垃圾回收掉。
  private void removeWeaklyReachableReferences() {
    // WeakReferences are enqueued as soon as the object to which they point to becomes weakly
    // reachable. This is before finalization or garbage collection has actually happened.
    KeyedWeakReference ref;

    while ((ref = (KeyedWeakReference) queue.poll()) != null) {
	  // 把观察对象的ID从retainedKeys移除掉
      retainedKeys.remove(ref.key);
    }
  }
}
(2)发现内存泄漏,就会打印内存堆栈:
java 复制代码
public File dumpHeap() {
  // 1. 创建一个用于存放堆转储文件的文件对象
  File heapDumpFile = leakDirectoryProvider.newHeapDumpFile();
  // 如果创建文件失败(返回 RETRY_LATER,代表无法生成文件),则直接返回
  if (heapDumpFile == RETRY_LATER) {
    return RETRY_LATER;
  }

  // 2. 创建一个 FutureResult 对象,用于等待 Toast 显示完成
  FutureResult<Toast> waitingForToast = new FutureResult<>();
  // 显示一个 Toast(提示用户正在进行堆转储),并将结果通过 waitingForToast 传递回来
  showToast(waitingForToast);
  // 等待最多 5 秒钟,等待 Toast 显示完成
  if (!waitingForToast.wait(5, SECONDS)) {
    CanaryLog.d("Did not dump heap, too much time waiting for Toast.");
    // 如果等待超时,则返回 RETRY_LATER,放弃本次堆转储
    return RETRY_LATER;
  }

  // 3. 构建一个通知,用于提示用户正在进行堆转储操作
  Notification.Builder builder = new Notification.Builder(context)
      .setContentTitle(context.getString(R.string.leak_canary_notification_dumping));
  // 通过内部工具方法构建通知对象
  Notification notification = LeakCanaryInternals.buildNotification(context, builder);
  // 获取系统的通知管理器
  NotificationManager notificationManager =
      (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
  // 生成一个基于系统时间的通知 ID
  int notificationId = (int) SystemClock.uptimeMillis();
  // 发布通知,提示用户堆转储正在进行
  notificationManager.notify(notificationId, notification);

  // 4. 从 waitingForToast 中获取之前显示的 Toast 对象
  Toast toast = waitingForToast.get();
  try {
    // 5. 调用 Debug.dumpHprofData() 方法进行堆转储,
    //    参数为堆转储文件的绝对路径,Framework 提供该能力,将堆数据写入文件
    Debug.dumpHprofData(heapDumpFile.getAbsolutePath());
    // 6. 堆转储成功后,取消之前显示的 Toast
    cancelToast(toast);
    // 7. 同时取消通知
    notificationManager.cancel(notificationId);
    // 返回生成的堆转储文件
    return heapDumpFile;
  } catch (Exception e) {
    // 如果在堆转储过程中出现异常,则记录日志
    CanaryLog.d(e, "Could not dump heap");
    // 并返回 RETRY_LATER,表示本次堆转储失败,需要重试
    return RETRY_LATER;
  }
}
(3)HeapAnalyzerService中的一个子线程用来处理分析结果
java 复制代码
@Override 
protected void onHandleIntentInForeground(@Nullable Intent intent) {
  // 如果 intent 为 null,则记录日志并忽略此次调用
  if (intent == null) {
    CanaryLog.d("HeapAnalyzerService received a null intent, ignoring.");
    return;
  }
  // 从 intent 中获取用于接收分析结果的 listener 的类名
  String listenerClassName = intent.getStringExtra(LISTENER_CLASS_EXTRA);
  // 从 intent 中获取 HeapDump 对象,HeapDump 包含堆转储文件、排除引用配置、计算保留内存大小的标志等
  HeapDump heapDump = (HeapDump) intent.getSerializableExtra(HEAPDUMP_EXTRA);

  // 创建一个 HeapAnalyzer 实例,用于分析堆转储文件
  // 参数包括:排除的引用列表、当前上下文(this),以及堆转储时使用的 ReachabilityInspector 类数组
  HeapAnalyzer heapAnalyzer =
      new HeapAnalyzer(heapDump.excludedRefs, this, heapDump.reachabilityInspectorClasses);
  // 开始堆转储分析,检查泄漏情况,返回一个 AnalysisResult 对象,
  // 其中包含泄漏路径、泄漏原因等信息
  AnalysisResult result = heapAnalyzer.checkForLeak(heapDump.heapDumpFile, heapDump.referenceKey,
      heapDump.computeRetainedHeapSize);
  // 将分析结果发送给指定的 listener 服务,最终展示给用户或者上报服务器,这个listener就是最开始我们注册的时候:refWatcher(application).listenerServiceClass(DisplayLeakService.class)

  AbstractAnalysisResultService.sendResultToListener(this, listenerClassName, heapDump, result);
}

  // 必须使用弱引用而不能使用虚引用,因为虚引用无法获取到引用的对象。但是下边的流程中我们是需要这个对象的。
  // 该方法用于在堆转储快照中查找与指定 key 对应的泄漏对象
// 这里查找的是通过 KeyedWeakReference 包装的对象的弱引用
private Instance findLeakingReference(String key, Snapshot snapshot) {
  // 在快照中查找 KeyedWeakReference 类(该类用于包装被监控对象的弱引用)
  ClassObj refClass = snapshot.findClass(KeyedWeakReference.class.getName());
  if (refClass == null) {
    // 如果堆快照中不存在该类,说明无法获取相关引用信息,抛出异常
    throw new IllegalStateException(
        "Could not find the " + KeyedWeakReference.class.getName() + " class in the heap dump.");
  }
  // 用于存储遍历过程中找到的所有 key 值,便于出错时打印调试信息
  List<String> keysFound = new ArrayList<>();
  // 遍历堆快照中所有 KeyedWeakReference 的实例
  for (Instance instance : refClass.getInstancesList()) {
    // 获取当前实例所有字段的值列表
    List<ClassInstance.FieldValue> values = classInstanceValues(instance);
    // 从字段列表中取出名称为 "key" 的字段的值
    Object keyFieldValue = fieldValue(values, "key");
    if (keyFieldValue == null) {
      // 如果当前实例的 key 字段为空,则记录 null 并继续下一个实例
      keysFound.add(null);
      continue;
    }
    // 将 key 字段值转换为字符串
    String keyCandidate = asString(keyFieldValue);
    // 如果转换后的 key 与传入的目标 key 相等,则说明找到了对应的弱引用
    // 返回该实例中保存被监控对象的字段 "referent" 的值,即真正可能泄漏的对象
    if (keyCandidate.equals(key)) {
      return fieldValue(values, "referent");
    }
    // 将遍历到的 keyCandidate 添加到 keysFound 列表中,便于调试输出
    keysFound.add(keyCandidate);
  }
  // 如果遍历完所有实例都没有找到匹配的 key,则抛出异常,并输出所有遍历到的 key 信息
  throw new IllegalStateException(
      "Could not find weak reference with key " + key + " in " + keysFound);
}

// 分析堆转储文件,检查是否存在内存泄漏,并返回一个 AnalysisResult 对象
// 参数:heapDumpFile - 堆转储文件;referenceKey - 被监控对象的唯一标识;computeRetainedSize - 是否计算保留内存大小
public @NonNull AnalysisResult checkForLeak(@NonNull File heapDumpFile,
      @NonNull String referenceKey,
      boolean computeRetainedSize) {
  // 记录分析开始的纳秒级时间戳
  long analysisStartNanoTime = System.nanoTime();

  // 如果堆转储文件不存在,则构造异常返回失败结果
  if (!heapDumpFile.exists()) {
    Exception exception = new IllegalArgumentException("File does not exist: " + heapDumpFile);
    return failure(exception, since(analysisStartNanoTime));
  }

  try {
    // 更新分析进度:正在读取堆转储文件
    listener.onProgressUpdate(READING_HEAP_DUMP_FILE);
    // 将堆转储文件映射为内存缓冲区,以便后续解析(采用内存映射文件方式)
    HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);
    // 创建 HprofParser 用于解析堆转储数据
    HprofParser parser = new HprofParser(buffer);
    // 更新进度:正在解析堆转储文件
    listener.onProgressUpdate(PARSING_HEAP_DUMP);
    // 解析堆转储数据,生成 Snapshot 对象,代表整个堆的快照
    Snapshot snapshot = parser.parse();
    // 更新进度:正在去重 GC Roots(垃圾回收根节点)
    listener.onProgressUpdate(DEDUPLICATING_GC_ROOTS);
    // 对 GC Roots 进行去重,简化后续分析过程
    deduplicateGcRoots(snapshot);
    // 更新进度:正在查找泄漏的引用
    listener.onProgressUpdate(FINDING_LEAKING_REF);
    // 在堆快照中查找与 referenceKey 对应的泄漏对象
    Instance leakingRef = findLeakingReference(referenceKey, snapshot);

    // 如果返回的泄漏对象为 null,说明在关键时间段内目标对象已经被回收
    // 这里视为误报(false alarm),返回 noLeak 结果,附带泄漏对象的类名和分析耗时
    if (leakingRef == null) {
      String className = leakingRef.getClassObj().getClassName();
      return noLeak(className, since(analysisStartNanoTime));
    }
    // 如果找到了泄漏对象,则进一步查找泄漏引用路径(Leak Trace),
    // 并返回包含详细泄漏信息的 AnalysisResult 对象
    return findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, computeRetainedSize);
  } catch (Throwable e) {
    // 如果在整个过程中出现异常,则返回失败的分析结果,并附上耗时信息
    return failure(e, since(analysisStartNanoTime));
  }
}

6. 一些细节的补充与2.0后版本的调整

6.1 正确触发 GC

不重写finalize方法,怎么知道对象是否被回收?三种弱引用都有一个同样的功能,当GC开始时候,如果垃圾回收处理器发现,这个弱引用所引用的对象没有被其他强引用引用到,那么在垃圾回收之前,就会把这个弱引用本身添加到队列当中。注意,这里是引用的对象,而不是弱引用引用的对象。这样就能判断这个对象能否被GC了,finalize方法并不可靠 ,它在GC调用的时候不一定会被调用。官方JDK的实现,也是通过这种方式做的。

  • 可以通过调用 dumpHprofData 获取 hprof 文件来进行堆内存分析。

6.2 LeakCanary2.0的初始化流程简单说明:

6.3 LeakCanary2.0与1.0的一些差别:

  • 2.0支持对View以及Service的内存泄漏的监听了,RootViewWatcher 通过监听根视图的添加和其 attach/detach 状态,这里还涉及到另一个库 Curtains,这个库的作用就是可以监听 window 的生命周期。在视图从窗口移除时启动延迟检查,检测它是否能被回收,从而实现对根视图内存泄漏的监控,而Service则是通过反射获取 ActivityThread 中的 Handler(mH)和它的 mCallback 字段,并用自定义的 Callback 替换来实现的:
kotlin 复制代码
private fun swapActivityThreadHandlerCallback(swap: (Handler.Callback?) -> Handler.Callback?) {
  val mHField =
    activityThreadClass.getDeclaredField("mH").apply { isAccessible = true }
  val mH = mHField[activityThreadInstance] as Handler

  val mCallbackField =
    Handler::class.java.getDeclaredField("mCallback").apply { isAccessible = true }
  val mCallback = mCallbackField[mH] as Handler.Callback?
  mCallbackField[mH] = swap(mCallback)
}

fun appDefaultWatchers(
  application: Application,
  reachabilityWatcher: ReachabilityWatcher = objectWatcher
): List<InstallableWatcher> {
  return listOf(
    ActivityWatcher(application, reachabilityWatcher),
    FragmentAndViewModelWatcher(application, reachabilityWatcher),
    RootViewWatcher(reachabilityWatcher),
    ServiceWatcher(reachabilityWatcher)
  )
}
  • 导入包发生变化,2.0有两种导入方式:
kotlin 复制代码
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.14' // 主进程分析
debugImplementation 'com.squareup.leakcanary:leakcanary-android-process:2.14' // 独立进程分析内存
泄漏

不同的配置方式会让LeakCanary运行在不同的进程中,下图就是独立进程:

  • 初始化时机: : Android 中 ContentProvider#onCreate() 会在 Application#onCreate() 之前执行, LeakCanary 2.0 利用了这一特性,实现了自动初始化,无需手动调用初始化代码。

  • 堆文件分析方式变化:

    • 1.x 版本: : 内部使用第三方库 haha 进行堆文件分析;
    • 2.x 版本: :改用第三方库 shark,内存占用大幅减少,分析速度大幅提升。
  • 分析流程内部逻辑的变化:

    • 延迟检查(与 1.x IdleHandler 的区别): 1.x 版本使用主线程空闲(IdleHandler)来触发检查;2.x 版本改为在主线程中延迟执行 5 秒(或其他指定时间),给系统足够的时间进行垃圾回收。
    • AppWatcher / ObjectWatcher 的引入1.x 版本: 主要通过 RefWatcher 对象来监控 Activity、Fragment 等,手动在 Activity/Fragment 销毁时调用 watch()2.x 版本: 引入了 AppWatcherObjectWatcher,更集中地管理所有被观察对象;对外只需调用 expectWeaklyReachable()watch(),内部会自动安排延迟检测并在确认对象未被回收后触发后续分析。
java 复制代码
@Synchronized
override fun expectWeaklyReachable(
  watchedObject: Any,
  description: String
) {
  // 如果监控功能未启用,则直接返回
  if (!isEnabled()) {
    return
  }
  // 先尝试移除已经被回收的对象,确保 watchedObjects 中只保留仍然存活的对象引用
  removeWeaklyReachableObjects()

  // 为要监控的对象生成一个唯一的 key
  val key = UUID.randomUUID().toString()
  // 记录当前时间(毫秒),用于后续判断对象存活时长
  val watchUptimeMillis = clock.uptimeMillis()

  // 创建一个 KeyedWeakReference,用弱引用的方式关联被监控对象
  // queue: ReferenceQueue,用于在对象被回收时将弱引用加入队列
  val reference = KeyedWeakReference(
    referent = watchedObject,
    key = key,
    description = description,
    watchUptimeMillis = watchUptimeMillis,
    queue = queue
  )

  // 打印调试日志,说明正在监控哪个对象(包括对象类型、描述信息以及生成的 key)
  SharkLog.d {
    "Watching " +
      (if (watchedObject is Class<*>) watchedObject.toString() else "instance of ${watchedObject.javaClass.name}") +
      (if (description.isNotEmpty()) " ($description)" else "") +
      " with key $key"
  }

  // 将弱引用对象存储到 watchedObjects 字典中,key 用于标识该对象
  watchedObjects[key] = reference

  // 与 1.x 不同:1.x 使用主线程的 IdleHandler 来等待系统空闲后检查;
  // 2.x 改用一个延迟任务(通常延迟 5 秒),给对象足够时间变为不可达并被 GC
  checkRetainedExecutor.execute {
    moveToRetained(key)
  }
}

@Synchronized
private fun moveToRetained(key: String) {
  // 再次尝试移除已经被 GC 回收的弱引用对象
  removeWeaklyReachableObjects()

  // 从 watchedObjects 中获取对应 key 的引用
  val retainedRef = watchedObjects[key]
  if (retainedRef != null) {
    // 如果还能找到该引用,说明对象仍然存活(尚未被 GC 回收)
    // 记录对象被确认为"保留"时的时间
    retainedRef.retainedUptimeMillis = clock.uptimeMillis()

    // 通知所有 OnObjectRetainedListener 监听器,表示有对象真正被保留了
    // 这通常意味着可能存在内存泄漏,需要后续进行进一步分析
    onObjectRetainedListeners.forEach { it.onObjectRetained() }
  }
}

// 在初始化(例如 AppWatcher.objectWatcher.addOnObjectRetainedListener(this))时
// 会将当前对象注册到 onObjectRetainedListeners 中,
// 当 moveToRetained 确认对象尚未被回收时,就会回调相应的监听器。

相关推荐
浪裡遊26 分钟前
uniapp常用组件
开发语言·前端·uni-app
五点六六六27 分钟前
Restful API 前端接口模型架构浅析
前端·javascript·设计模式
筱筱°30 分钟前
Vue 路由守卫
前端·javascript·vue.js
前端小张同学1 小时前
前端Vue后端Nodejs 实现 pdf下载和预览,如何实现?
前端·javascript·node.js
独孤求败Ace1 小时前
第59天:Web攻防-XSS跨站&反射型&存储型&DOM型&接受输出&JS执行&标签操作&SRC复盘
前端·xss
天空之枫1 小时前
node-sass替换成Dart-sass(全是坑)
前端·css·sass
SecPulse1 小时前
xss注入实验(xss-lab)
服务器·前端·人工智能·网络安全·智能路由器·github·xss
路遥努力吧1 小时前
el-input 不可编辑,但是点击的时候出现弹窗/或其他操作面板,并且带可清除按钮
前端·vue.js·elementui
绝顶少年1 小时前
确保刷新页面后用户登录状态不会失效,永久化存储用户登录信息
前端
初学者7.2 小时前
Webpack总结
前端·webpack·node.js