一步步拆解 LeakCanary

1,990 阅读11分钟

本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布

java 源码系列 - 带你读懂 Reference 和 ReferenceQueue

blog.csdn.net/gdutxiaoxu/…

一步步拆解 LeakCanary

blog.csdn.net/gdutxiaoxu/…

前言

内存泄露,一直是我们性能优化方面的重点。今天,就让我们一起来拆解 LeakCanary,一步步理解它的原理

原理概览

讲解 LeakCannary 原理之前,我们先来说一下它的主要原理,给大家吃颗定心丸,其实挺简单的,大概可以分为以下几步:

  • 监听 Activity 的生命周期
  • 在 onDestroy 的时候,创建相应的 Refrence 和 RefrenceQueue,并启动后台进程去检测
  • 一段时间之后,从 RefrenceQueue 读取,若读取不到相应 activity 的 Refrence,有可能发生泄露了,这个时候,再促发 gc,一段时间之后,再去读取,若在从 RefrenceQueue 还是读取不到相应 activity 的 refrence,可以断定是发生内存泄露了
  • 发生内存泄露之后,dump,分析 hprof 文件,找到泄露路径(使用 haha 库分析),发送到通知栏

原理分析

LeakCanary#Install

public static RefWatcher install(Application application) {
  return refWatcher(application).listenerServiceClass(DisplayLeakService.class)
      .excludedRefs(AndroidExcludedRefs.createAppDefaults().build())
      .buildAndInstall();
}

listenerServiceClass 方法

public AndroidRefWatcherBuilder listenerServiceClass(
    Class<? extends AbstractAnalysisResultService> listenerServiceClass) {
  return heapDumpListener(new ServiceHeapDumpListener(context, listenerServiceClass));
}

public final class ServiceHeapDumpListener implements HeapDump.Listener {

  private final Context context;
  private final Class<? extends AbstractAnalysisResultService> listenerServiceClass;

  public ServiceHeapDumpListener(Context context,
      Class<? extends AbstractAnalysisResultService> listenerServiceClass) {
     // 启动后台服务监听
    setEnabled(context, listenerServiceClass, true);
    // 启动 HeapAnalyzerService ,用来分析 dump 文件
    setEnabled(context, HeapAnalyzerService.class, true);
    this.listenerServiceClass = checkNotNull(listenerServiceClass, "listenerServiceClass");
    this.context = checkNotNull(context, "context").getApplicationContext();
  }

  ----  
}

listenerServiceClass() 方法绑定了一个后台服务 DisplayLeakService,这个服务主要用来分析内存泄漏结果并发送通知。你可以继承并重写这个类来进行一些自定义操作,比如上传分析结果等。

RefWatcherBuilder.excludedRefs

public final T excludedRefs(ExcludedRefs excludedRefs) {
  this.excludedRefs = excludedRefs;
  return self();
}
AndroidExcludedRefs.java
/**
 * This returns the references in the leak path that can be ignored for app developers. This
 * doesn't mean there is no memory leak, to the contrary. However, some leaks are caused by bugs
 * in AOSP or manufacturer forks of AOSP. In such cases, there is very little we can do as app
 * developers except by resorting to serious hacks, so we remove the noise caused by those leaks.
 */
public static ExcludedRefs.Builder createAppDefaults() {
  return createBuilder(EnumSet.allOf(AndroidExcludedRefs.class));
}

public static ExcludedRefs.Builder createBuilder(EnumSet<AndroidExcludedRefs> refs) {
  ExcludedRefs.Builder excluded = ExcludedRefs.builder();
  for (AndroidExcludedRefs ref : refs) {
    if (ref.applies) {
      ref.add(excluded);
      ((ExcludedRefs.BuilderWithParams) excluded).named(ref.name());
    }
  }
  return excluded;
}

excludedRefs() 方法定义了一些对于开发者可以忽略的路径,意思就是即使这里发生了内存泄漏,LeakCanary 也不会弹出通知。这大多是系统 Bug 导致的,无需用户进行处理。

AndroidRefWatcherBuilder.buildAndInstall

buildAndInstall 所做的工作,调用 build 构建 refWatcher,判断 refWatcher 是否 DISABLED,若不是 DISABLED 状态,调用 install 方法,并将 refWatcher 返回回去

/**
 * Creates a {@link RefWatcher} instance and starts watching activity references (on ICS+).
 */
public RefWatcher buildAndInstall() {
  // 构建 refWatcher 对象
  RefWatcher refWatcher = build();
  // 判断是否 DISABLED,若不是 DISABLED 状态,调用 
  if (refWatcher != DISABLED) {
    LeakCanary.enableDisplayLeakActivity(context);
    ActivityRefWatcher.install((Application) context, refWatcher);
  }
  return refWatcher;
}

了解 build 方法 之前,我们先来看一下 RefWatcherBuilder 是什么东东?

RefWatcherBuilder

public class RefWatcherBuilder<T extends RefWatcherBuilder<T>> {

  private ExcludedRefs excludedRefs;
  private HeapDump.Listener heapDumpListener;
  private DebuggerControl debuggerControl;
  private HeapDumper heapDumper;
  private WatchExecutor watchExecutor;
  private GcTrigger gcTrigger;



  /** Creates a {@link RefWatcher}. */
  public final RefWatcher build() {
    if (isDisabled()) {
      return RefWatcher.DISABLED;
    }

    ExcludedRefs excludedRefs = this.excludedRefs;
    if (excludedRefs == null) {
      excludedRefs = defaultExcludedRefs();
    }

    HeapDump.Listener heapDumpListener = this.heapDumpListener;
    if (heapDumpListener == null) {
      heapDumpListener = defaultHeapDumpListener();
    }

    DebuggerControl debuggerControl = this.debuggerControl;
    if (debuggerControl == null) {
      debuggerControl = defaultDebuggerControl();
    }

    HeapDumper heapDumper = this.heapDumper;
    if (heapDumper == null) {
      heapDumper = defaultHeapDumper();
    }

    WatchExecutor watchExecutor = this.watchExecutor;
    if (watchExecutor == null) {
      watchExecutor = defaultWatchExecutor();
    }

    GcTrigger gcTrigger = this.gcTrigger;
    if (gcTrigger == null) {
      gcTrigger = defaultGcTrigger();
    }

    return new RefWatcher(watchExecutor, debuggerControl, gcTrigger, heapDumper, heapDumpListener,
        excludedRefs);
  }
  
  ----


build 方法看到这里你是不是有一种很眼熟的感觉,没错,它运用了建造者模式,与我们 Android 中的 AlertDialog.build 同出一辙。 建造者模式(Builder)及其应用

RefWatcherBuilder 主要有几个重要的成员变量

  • watchExecutor : 线程控制器,在 onDestroy() 之后并且主线程空闲时执行内存泄漏检测
  • debuggerControl : 判断是否处于调试模式,调试模式中不会进行内存泄漏检测
  • gcTrigger : 用于 GC,watchExecutor 首次检测到可能的内存泄漏,会主动进行 GC,GC 之后会再检测一次,仍然泄漏的判定为内存泄漏,进行后续操作
  • heapDumper : dump 内存泄漏处的 heap 信息,写入 hprof 文件
  • heapDumpListener : 解析完 hprof 文件,进行回调,并通知 DisplayLeakService 弹出提醒
  • excludedRefs : 排除可以忽略的泄漏路径

接下来,我们一起来看一下 ActivityRefWatcher.install 方法

ActivityRefWatcher.install((Application) context, refWatcher);
public final class ActivityRefWatcher {

  /** @deprecated Use {@link #install(Application, RefWatcher)}. */
  @Deprecated
  public static void installOnIcsPlus(Application application, RefWatcher refWatcher) {
    install(application, refWatcher);
  }

  public static void install(Application application, RefWatcher refWatcher) {
    new ActivityRefWatcher(application, refWatcher).watchActivities();
  }

  private final Application.ActivityLifecycleCallbacks lifecycleCallbacks =
      new Application.ActivityLifecycleCallbacks() {
        @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
        }

        @Override public void onActivityStarted(Activity activity) {
        }

        @Override public void onActivityResumed(Activity activity) {
        }

        @Override public void onActivityPaused(Activity activity) {
        }

        @Override public void onActivityStopped(Activity activity) {
        }

        @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
        }

        @Override public void onActivityDestroyed(Activity activity) {
          ActivityRefWatcher.this.onActivityDestroyed(activity);
        }
      };

  private final Application application;
  private final RefWatcher refWatcher;

  /**
   * Constructs an {@link ActivityRefWatcher} that will make sure the activities are not leaking
   * after they have been destroyed.
   */
  public ActivityRefWatcher(Application application, RefWatcher refWatcher) {
    this.application = checkNotNull(application, "application");
    this.refWatcher = checkNotNull(refWatcher, "refWatcher");
  }

  void onActivityDestroyed(Activity activity) {
    refWatcher.watch(activity);
  }

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

  public void stopWatchingActivities() {
    application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks);
  }
}

install 来说,主要做以下事情

  • 创建 ActivityRefWatcher,并调用 watchActivities 监听 activity 的生命周期
  • 在 activity 被销毁的时候,会回调 lifecycleCallbacks 的 onActivityDestroyed 方法,这时候会调用 onActivityDestroyed 去分析,而 onActivityDestroyed 方法又会回调 refWatcher.watch(activity)

我们回到 refWatcher.watch 方法

public void watch(Object watchedReference) {
  watch(watchedReference, "");
}

/**
 * Watches the provided references and checks if it can be GCed. This method is non blocking,
 * the check is done on the {@link WatchExecutor} this {@link RefWatcher} has been constructed
 * with.
 *
 * @param referenceName An logical identifier for the watched object.
 */
public void watch(Object watchedReference, String referenceName) {
  if (this == DISABLED) {
    return;
  }
  checkNotNull(watchedReference, "watchedReference");
  checkNotNull(referenceName, "referenceName");
  final long watchStartNanoTime = System.nanoTime();
  // 保证 key 的唯一性
  String key = UUID.randomUUID().toString();
  // 添加到 set 集合中
  retainedKeys.add(key);
  // 穿件 KeyedWeakReference 对象
  final KeyedWeakReference reference =
      new KeyedWeakReference(watchedReference, key, referenceName, queue);

  ensureGoneAsync(watchStartNanoTime, reference);
}


  • retainedKeys : 一个 Set 集合,每个检测的对象都对应着一个唯一的 key,存储在 retainedKeys 中
  • KeyedWeakReference : 自定义的弱引用,持有检测对象和对用的 key 值

我们先来看一下 KeyedWeakReference ,可以看到 KeyedWeakReference 继承于 WeakReference,并定义了 key,name 字段

final class KeyedWeakReference extends WeakReference<Object> {
  public final String key;
  public final String name;

  KeyedWeakReference(Object referent, String key, String name,
      ReferenceQueue<Object> referenceQueue) {
    super(checkNotNull(referent, "referent"), checkNotNull(referenceQueue, "referenceQueue"));
    this.key = checkNotNull(key, "key");
    this.name = checkNotNull(name, "name");
  }
}
  • key 对应的 key 值名称
  • referenceQueue 引用队列,当结合 Refrence 使用的时候,垃圾回收器回收的时候,会把相应的对象加入到 refrenceQueue 中。

弱引用和引用队列 ReferenceQueue 联合使用时,如果弱引用持有的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。即 KeyedWeakReference 持有的 Activity 对象如果被垃圾回收,该对象就会加入到引用队列 queue 中。具体的可以参考我的这一篇博客 java 源码系列 - 带你读懂 Reference 和 ReferenceQueue

ensureGoneAsync 方法

private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
  watchExecutor.execute(new Retryable() {
    @Override public Retryable.Result run() {
      return ensureGone(reference, watchStartNanoTime);
    }
  });
}

ensureGoneAsync 这个方法,在 watchExecutor 的回调里面执行了 ensureGone 方法,watchExecutor 是 AndroidWatchExecutor 的实例。

接下来,我们一起来看一下 watchExecutor,主要关注 execute 方法

watchExecutor

public final class AndroidWatchExecutor implements WatchExecutor {

  static final String LEAK_CANARY_THREAD_NAME = "LeakCanary-Heap-Dump";
  private final Handler mainHandler;
  private final Handler backgroundHandler;
  private final long initialDelayMillis;
  private final long maxBackoffFactor;

  public AndroidWatchExecutor(long initialDelayMillis) {
    mainHandler = new Handler(Looper.getMainLooper());
    HandlerThread handlerThread = new HandlerThread(LEAK_CANARY_THREAD_NAME);
    handlerThread.start();
    backgroundHandler = new Handler(handlerThread.getLooper());
    this.initialDelayMillis = initialDelayMillis;
    maxBackoffFactor = Long.MAX_VALUE / initialDelayMillis;
  }

  @Override public void execute(Retryable retryable) {
    // 当前线程是主线程
    if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
      waitForIdle(retryable, 0);
    } else { // 当前线程不是主线程
      postWaitForIdle(retryable, 0);
    }
  }

   --------
}

execute 方法,首先判断是否是主线程,如果是主线程,调用 waitForIdle 方法,等待空闲的时候执行,如果不是主线程,调用 postWaitForIdle 方法。我们一起来看一下 postWaitForIdle 和 waitForIdle 方法。

  // 调用 mainHandler 的 post 方法,,确保在主线程中执行
  void postWaitForIdle(final Retryable retryable, final int failedAttempts) {
    mainHandler.post(new Runnable() {
      @Override public void run() {
        waitForIdle(retryable, failedAttempts);
      }
    });
  }

 // 当当前线程 looper 空闲的时候执行
  void waitForIdle(final Retryable retryable, final int failedAttempts) {
    // This needs to be called from the main thread.
    // 当 looper 空闲的时候,会回调 queueIdle 方法
    Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
      @Override public boolean queueIdle() {
        postToBackgroundWithDelay(retryable, failedAttempts);
        return false;
      }
    });
  }

可以看到 postWaitForIdle 方法其实是 调用 mainHandler 的 post 方法,,确保在主线程中执行,之后再 runnable 的 run 方法在调用 waitForIdle 方法。而 waitForIdle 方法是在等当前 looper 空闲之后,执行 postToBackgroundWithDelay 方法

  void postToBackgroundWithDelay(final Retryable retryable, final int failedAttempts) {
  // 取 Math.pow(2, failedAttempts), maxBackoffFactor 的最小值,maxBackoffFactor = Long.MAX_VALUE / 5,
  // 第一次执行的时候 failedAttempts 是 0 ,所以 exponentialBackoffFactor 是1
    long exponentialBackoffFactor = (long) Math.min(Math.pow(2, failedAttempts), maxBackoffFactor);
    // initialDelayMillis 的默认值是 5
    long delayMillis = initialDelayMillis * exponentialBackoffFactor;
    // 所以第一次延迟执行的时候是 5s,若
    backgroundHandler.postDelayed(new Runnable() {
      @Override public void run() {
        Retryable.Result result = retryable.run();
        // 过 result == RETRY,再次调用 postWaitForIdle,下一次的 delayMillis= 上一次的  delayMillis *2;
        // 正常情况下,不会返回 RETRY,当 heapDumpFile == RETRY_LATER (即 dump heap 失败的时候),会返回 RETRY
        if (result == RETRY) {
          postWaitForIdle(retryable, failedAttempts + 1);
        }
      }
    }, delayMillis);
  }

postToBackgroundWithDelay 方法有点类似递归,正常情况下,若 retryable.run() 返回的结果不等于 RETRY,只会执行一次。若 retryable.run() 返回 RETRY,则会执行多次,退出的条件是 retryable.run() 返回结果不等于 RETRY;

delay 的时间 取 Math.pow(2, failedAttempts), maxBackoffFactor 两个数的最小值,maxBackoffFactor = Long.MAX_VALUE / 5,而,第一次执行的时候 failedAttempts 是 0 ,所以 exponentialBackoffFactor 是 1,即 delayMillis = initialDelayMillis * exponentialBackoffFactor= 5*1=5;

因此,综合上面的例子,第一次执行的时间是 activity destroy 之后 5s。

OK,我们回到 ensureGone 方法,这才是我们的重点

@SuppressWarnings("ReferenceEquality") // Explicitly checking for named null.
Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
  long gcStartNanoTime = System.nanoTime();
  long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);

   // 移除已经被回收的引用
  removeWeaklyReachableReferences();

  if (debuggerControl.isDebuggerAttached()) {
    // The debugger can create false leaks.
    return RETRY;
  }
  // 判断 reference,即 activity 是否内回收了,若被回收了,直接返回
  if (gone(reference)) {
    return DONE;
  }
  // 调用 gc 方法进行垃圾回收
  gcTrigger.runGc();
   // 移除已经被回收的引用
  removeWeaklyReachableReferences();
  // activity 还没有被回收,证明发生内存泄露
  if (!gone(reference)) {
    long startDumpHeap = System.nanoTime();
    long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
    // dump heap,并生成相应的 hprof 文件
    File heapDumpFile = heapDumper.dumpHeap();
    
    if (heapDumpFile == RETRY_LATER) {// dump the heap 失败的时候
      // Could not dump the heap.
      return RETRY;
    }
    long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
    // 分析 hprof 文件
    heapdumpListener.analyze(
        new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs, watchDurationMs,
            gcDurationMs, heapDumpDurationMs));
  }
  return DONE;
}


removeWeaklyReachableReferences 方法

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;
  // 遍历 queue ,并从 retainedKeys set 集合中移除
  while ((ref = (KeyedWeakReference) queue.poll()) != null) {
    retainedKeys.remove(ref.key);
  }
}


gone(reference) 方法,判断 retainedKeys set 集合,是否还含有 reference,若没有,证明已经被回收了;若含有,可能已经发生内存泄露。因为我们知道 refrence 被回收的时候,会被加进 queue 里面,值调用 gone 方法判断的时候,我们已经遍历 queue 移除掉 retainedKeys 里面的 refrence,若含有,证明 refrence 没有被回收,之所以说可能发生内存泄露,是因为 gc 回收器可能还没有回收。

private boolean gone(KeyedWeakReference reference) {
  return !retainedKeys.contains(reference.key);
}


gcTrigger.runGc() 的主要作用是促发 gc,进行回收。

  GcTrigger DEFAULT = new GcTrigger() {
    @Override public void runGc() {
      // Code taken from AOSP FinalizationTest:
      // https://android.googlesource.com/platform/libcore/+/master/support/src/test/java/libcore/
      // java/lang/ref/FinalizationTester.java
      // System.gc() does not garbage collect every time. Runtime.gc() is
      // more likely to perfom a gc.
      Runtime.getRuntime().gc();
      enqueueReferences();
      System.runFinalization();
    }

    private void enqueueReferences() {
      // Hack. We don't have a programmatic way to wait for the reference queue daemon to move
      // references to the appropriate queues.
      try {
        Thread.sleep(100);
      } catch (InterruptedException e) {
        throw new AssertionError();
      }
    }
  };

ok,我们在回到 ensureGoneAsync 方法,整理一下它的流程

  • Activity onDestroy 5s 之后,检测 activity 的弱引用 refrence 有没有被回收,若被回收,证明没有发生内存泄露,若没有被回收,继续下面流程
  • 调用 gcTrigger.runGc() 促发垃圾回收机器进行回收
  • 再次检测 activity 的弱引用 refrence 有没有被回收,若被回收,证明没有发生内存泄露,若没有被回收,则认为发生内存泄露
  • dump heap,生成 hprof。
  • 分析 hprof 文件,找到泄露路径,发送到通知栏

关于如何 dump 和 如何解析hprof

关于如何 dump

这里主要是调用 AndroidHeapDumper 的 dumpHeap 方法,而里面比较重要的是调用 Debug.dumpHprofData 生成 hprof 文件。

AndroidHeapDumper#dumpHeap

@SuppressWarnings("ReferenceEquality") // Explicitly checking for named null.
@Override public File dumpHeap() {
  File heapDumpFile = leakDirectoryProvider.newHeapDumpFile();

  if (heapDumpFile == RETRY_LATER) {
    return RETRY_LATER;
  }

  FutureResult<Toast> waitingForToast = new FutureResult<>();
  showToast(waitingForToast);

  if (!waitingForToast.wait(5, SECONDS)) {
    CanaryLog.d("Did not dump heap, too much time waiting for Toast.");
    return RETRY_LATER;
  }

  Toast toast = waitingForToast.get();
  try {
    Debug.dumpHprofData(heapDumpFile.getAbsolutePath());
    cancelToast(toast);
    return heapDumpFile;
  } catch (Exception e) {
    CanaryLog.d(e, "Could not dump heap");
    // Abort heap dump
    return RETRY_LATER;
  }
}

如何解析hprof

当发生了泄漏就会生成 HeapDump 对象然后就会进入下面这个方法去启动 HeapAnalyzerServiceService 来进行分析

@Override public void analyze(HeapDump heapDump) {
    checkNotNull(heapDump, "heapDump");
    HeapAnalyzerService.runAnalysis(context, heapDump, listenerServiceClass);
  }

关于如解析 hprof,请自行了解 haha 库的用法即原理

public AnalysisResult checkForLeak(File heapDumpFile, String referenceKey) {
    long analysisStartNanoTime = System.nanoTime();

    if (!heapDumpFile.exists()) {
      Exception exception = new IllegalArgumentException("File does not exist: " + heapDumpFile);
      return failure(exception, since(analysisStartNanoTime));
    }

    try {
      HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);
      HprofParser parser = new HprofParser(buffer);
      Snapshot snapshot = parser.parse();
      deduplicateGcRoots(snapshot);

      Instance leakingRef = findLeakingReference(referenceKey, snapshot);

      // False alarm, weak reference was cleared in between key check and heap dump.
      if (leakingRef == null) {
        return noLeak(since(analysisStartNanoTime));
      }

      return findLeakTrace(analysisStartNanoTime, snapshot, leakingRef);
    } catch (Throwable e) {
      return failure(e, since(analysisStartNanoTime));
    }
  }

经过解析之后机会把数据传递到 DisplayLeakService ,Service 会根据传入进来的数据发送通知栏通知,当你点击对应的通知进入DisplayLeakActivity界面就能显示泄漏日志了。


总结:

LeakCanary 的原理总结如下

  • 监听 Activity 的生命周期
  • 在 onDestroy 的时候,创建相应的 Refrence 和 RefrenceQueue,并启动后台进程去检测
  • 一段时间之后,从 RefrenceQueue 读取,若读取不到相应 activity 的 Refrence,有可能发生泄露了,这个时候,再促发 gc,一段时间之后,再去读取,若在从 RefrenceQueue 还是读取不到相应 activity 的 refrence,可以断定是发生内存泄露了
  • 发生内存泄露之后,dump,分析 hprof 文件,找到泄露路径(使用 haha 库分析)

其中,比较重要的是如何确定是否发生内存泄露,而如何确定发生内存泄露最主要的原理是通过 Refrence 和 RefrenceQueue。悄悄地提醒你一下,面试必备。

最后,用一张图片来表示 leakCannary 的执行流程,该图片来自 深入理解 Android 之 LeakCanary 源码解析

image

java 源码系列 - 带你读懂 Reference 和 ReferenceQueue

blog.csdn.net/gdutxiaoxu/…

一步步拆解 LeakCanary

blog.csdn.net/gdutxiaoxu/…

最后的最后

卖一下广告,欢迎大家关注我的微信公众号,扫一扫下方二维码或搜索微信号 stormjun,即可关注。 目前专注于 Android 开发,主要分享 Android开发相关知识和一些相关的优秀文章,包括个人总结,职场经验等。

image