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 确认对象尚未被回收时,就会回调相应的监听器。

相关推荐
崔庆才丨静觅4 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60614 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了5 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅5 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅5 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅5 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment6 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅6 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊6 小时前
jwt介绍
前端
爱敲代码的小鱼6 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax