前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >开源框架源码解析系列(2)——LeakCanary源码解析

开源框架源码解析系列(2)——LeakCanary源码解析

作者头像
老马的编程之旅
发布2022-06-22 13:20:39
1.8K0
发布2022-06-22 13:20:39
举报
文章被收录于专栏:深入理解Android

LeakCanary是一个开源的,可以用来检测activtiy或者fragment内存泄漏的框架,本篇我们来学习这个框架的源码。

1.LeakCanary接入

代码语言:javascript
复制
dependencies {
  // debugImplementation because LeakCanary should only run in debug builds.
  debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.0-alpha-2'
}

然后在application执行如下代码:

代码语言:javascript
复制
LeakCanary.install(this);

然后出现内存泄露时候就会出现通知弹窗

2.源码分析

2.1 LeakCanary#install

代码语言:javascript
复制
  public static @NonNull RefWatcher install(@NonNull Application application) {
    return refWatcher(application).listenerServiceClass(DisplayLeakService.class)
        .excludedRefs(AndroidExcludedRefs.createAppDefaults().build())
        .buildAndInstall();
  }

1.这里的DisplayLeakService就是内存泄漏时,执行弹出通知的逻辑 2.excludedRefs,排除Android SDK的内存泄露 3.调用buildAndInstall方法

2.2 AndroidRefWatcherBuilder#buildAndInstall

代码语言:javascript
复制
public @NonNull RefWatcher buildAndInstall() {
    if (LeakCanaryInternals.installedRefWatcher != null) {
      throw new UnsupportedOperationException("buildAndInstall() should only be called once.");
    }
    RefWatcher refWatcher = build();
    if (refWatcher != DISABLED) {
      if (enableDisplayLeakActivity) {
        LeakCanaryInternals.setEnabledAsync(context, DisplayLeakActivity.class, true);
      }
      if (watchActivities) {
        ActivityRefWatcher.install(context, refWatcher);
      }
      if (watchFragments) {
        FragmentRefWatcher.Helper.install(context, refWatcher);
      }
    }
    LeakCanaryInternals.installedRefWatcher = refWatcher;
    return refWatcher;
  }

1.调用build()方法创建一个RefWatcher,build方法其实就是创建RefWatcher的过程 2.如果watchActivities为true(默认true)则监听activity 3.如果watchFragments为true(默认true)则监听fragment

2.3 ActivityRefWatcher#install

代码语言:javascript
复制
 public static void install(@NonNull Context context, @NonNull RefWatcher refWatcher) {
    Application application = (Application) context.getApplicationContext();
    ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);

    application.registerActivityLifecycleCallbacks(activityRefWatcher.lifecycleCallbacks);
  }

1.创建ActivityRefWatcher 2.通过 application.registerActivityLifecycleCallbacks将ActivityRefWatcher注册进application的lifecycle中

代码语言:javascript
复制
 private final Application.ActivityLifecycleCallbacks lifecycleCallbacks =
      new ActivityLifecycleCallbacksAdapter() {
        @Override public void onActivityDestroyed(Activity activity) {
          refWatcher.watch(activity);
        }
      };

其实就是在每一个app的activity进行onDestroy的时候,通过RefWatcher.watch方法添加这个activity.

2.4 RefWatcher#watch

代码语言:javascript
复制
  public void watch(Object watchedReference, String referenceName) {
    if (this == DISABLED) {
      return;
    }
    checkNotNull(watchedReference, "watchedReference");
    checkNotNull(referenceName, "referenceName");
    final long watchStartNanoTime = System.nanoTime();
    String key = UUID.randomUUID().toString();
    retainedKeys.add(key);
    final KeyedWeakReference reference =
        new KeyedWeakReference(watchedReference, key, referenceName, queue);

    ensureGoneAsync(watchStartNanoTime, reference);
  }

1.随机生成一个key添加到集合retainedKeys中 2.构造一个KeyedWeakReference弱引用,参数为传入的activity,key,referenceName(默认为“”),还有一个ReferenceQueue队列 3.调用ensureGoneAsync方法

2.5 RefWatcher#ensureGoneAsync

代码语言:javascript
复制
private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
    watchExecutor.execute(new Retryable() {
      @Override public Retryable.Result run() {
        return ensureGone(reference, watchStartNanoTime);
      }
    });
  }

通过AndroidWatchExecutor执行一个任务Retryable,其实就是通过Handler post任务,然后会执行Retryable#run方法,会执行到RefWatcher#ensureGone方法

2.6 RefWatcher#ensureGone

代码语言:javascript
复制
 Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
    long gcStartNanoTime = System.nanoTime();
    long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
	//关键点1
    removeWeaklyReachableReferences();

    if (debuggerControl.isDebuggerAttached()) {
      // The debugger can create false leaks.
      return RETRY;
    }
    if (gone(reference)) {
      return DONE;
    }
    //关键点2
    gcTrigger.runGc();
    //关键点3
    removeWeaklyReachableReferences();
    //关键点4
    if (!gone(reference)) {
      long startDumpHeap = System.nanoTime();
      long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
	 //关键点5
      File heapDumpFile = heapDumper.dumpHeap();
      if (heapDumpFile == RETRY_LATER) {
        // Could not dump the heap.
        return RETRY;
      }
      long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);

      HeapDump heapDump = heapDumpBuilder.heapDumpFile(heapDumpFile).referenceKey(reference.key)
          .referenceName(reference.name)
          .watchDurationMs(watchDurationMs)
          .gcDurationMs(gcDurationMs)
          .heapDumpDurationMs(heapDumpDurationMs)
          .build();

      heapdumpListener.analyze(heapDump);
    }
    return DONE;
  }

1.通过removeWeaklyReachableReferences方法清除那些被回收的对象 RefWatcher#removeWeaklyReachableReferences

代码语言:javascript
复制
 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) {
      retainedKeys.remove(ref.key);
    }
  }

从之前ReferenceQueue队列中发现这个弱引用已经被添加了,说明已经被回收,因为垃圾回收的弱引用会放入这个队列中,则将这个弱引用的key从retainedKeys的集合中移除

2.生成内存快照之前先调用gcTrigger.runGc()方法进行一次GC回收,避免误判情况 GcTrigger#runGc

代码语言:javascript
复制
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 perform 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();
      }
    }
  };

调用Runtime.getRuntime().gc()进行gc,然后等待100毫秒,等待gc完成,等待系统将被回收后的各种引用添加到适合的队列中

3.再次调用一次removeWeaklyReachableReferences,确保gc后对象的key被移除出retainedKeys

4.调用gone方法,确认有没有activity内存泄漏

代码语言:javascript
复制
private boolean gone(KeyedWeakReference reference) {
    return !retainedKeys.contains(reference.key);
  }

其实就是判断activity的key还在不在retainedKeys中

5.通过heapDumper去抓去内存快照,这个类是创建RefWatcher时创建的,heapDumper时AndroidHeapDumper AndroidHeapDumper#dumpHeap

代码语言:javascript
复制
 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;
    }

    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);
    int notificationId = (int) SystemClock.uptimeMillis();
    notificationManager.notify(notificationId, notification);

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

1)首先post到主线程,通过在5S内尝试弹一个toast看是否成功,来决定是否立即获取内存快照,如果超过5S,说明主线程还有很多任务,延迟再进行快照 2)调用Debug.dumpHprofData(heapDumpFile.getAbsolutePath())进行快照,把生成的文件存到指定路径中

6.构建一个HeapDump对象,传入参数包括内存快照,activity的key

代码语言:javascript
复制
 HeapDump heapDump = heapDumpBuilder.heapDumpFile(heapDumpFile).referenceKey(reference.key)
          .referenceName(reference.name)
          .watchDurationMs(watchDurationMs)
          .gcDurationMs(gcDurationMs)
          .heapDumpDurationMs(heapDumpDurationMs)
          .build();

7.通过分析快照文件了解activity是否内存泄漏

代码语言:javascript
复制
heapdumpListener.analyze(heapDump);

heapdumpListener实现类为ServiceHeapDumpListener

2.7 ServiceHeapDumpListener#analyze

代码语言:javascript
复制
 @Override public void analyze(@NonNull HeapDump heapDump) {
    checkNotNull(heapDump, "heapDump");
    HeapAnalyzerService.runAnalysis(context, heapDump, listenerServiceClass);
  }

调用HeapAnalyzerService.runAnalysis

2.8 HeapAnalyzerService#runAnalysis

代码语言:javascript
复制
 public static void runAnalysis(Context context, HeapDump heapDump,
      Class<? extends AbstractAnalysisResultService> listenerServiceClass) {
    setEnabledBlocking(context, HeapAnalyzerService.class, true);
    setEnabledBlocking(context, listenerServiceClass, true);
    Intent intent = new Intent(context, HeapAnalyzerService.class);
    intent.putExtra(LISTENER_CLASS_EXTRA, listenerServiceClass.getName());
    intent.putExtra(HEAPDUMP_EXTRA, heapDump);
    ContextCompat.startForegroundService(context, intent);
  }

启动HeapAnalyzerService,这是个IntentService,IntentService自动运行再子线程,所以会回调到onHandleIntent中,onHandleIntent调用onHandleIntentInForeground

代码语言:javascript
复制
  @Override protected void onHandleIntentInForeground(@Nullable Intent intent) {
    if (intent == null) {
      CanaryLog.d("HeapAnalyzerService received a null intent, ignoring.");
      return;
    }
    String listenerClassName = intent.getStringExtra(LISTENER_CLASS_EXTRA);
    HeapDump heapDump = (HeapDump) intent.getSerializableExtra(HEAPDUMP_EXTRA);
	//关键1
    HeapAnalyzer heapAnalyzer =
        new HeapAnalyzer(heapDump.excludedRefs, this, heapDump.reachabilityInspectorClasses);
	//关键2
    AnalysisResult result = heapAnalyzer.checkForLeak(heapDump.heapDumpFile, heapDump.referenceKey,
        heapDump.computeRetainedHeapSize);
    //关键3
    AbstractAnalysisResultService.sendResultToListener(this, listenerClassName, heapDump, result);
  }

1.创建HeapAnalyzer 2.通过heapAnalyzer.checkForLeak方法检测是否泄漏 3.将结果通过AbstractAnalysisResultService.sendResultToListener方法进行设置

2.9 HeapAnalyzer#checkForLeak

代码语言:javascript
复制
 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);
      //关键1
      HprofParser parser = new HprofParser(buffer);
      listener.onProgressUpdate(PARSING_HEAP_DUMP);
      //关键2
      Snapshot snapshot = parser.parse();
      listener.onProgressUpdate(DEDUPLICATING_GC_ROOTS);
	  //关键3
      deduplicateGcRoots(snapshot);
      listener.onProgressUpdate(FINDING_LEAKING_REF);
      //关键4
      Instance leakingRef = findLeakingReference(referenceKey, snapshot);

      // False alarm, weak reference was cleared in between key check and heap dump.
      if (leakingRef == null) {
        String className = leakingRef.getClassObj().getClassName();
        return noLeak(className, since(analysisStartNanoTime));
      }
      //关键5
      return findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, computeRetainedSize);
    } catch (Throwable e) {
      return failure(e, since(analysisStartNanoTime));
    }
  }

1.创建HprofParser,这个时另一个库里的,用来解析.hprof文件的类 2.HprofParser.parse方法生成.hprof文件的内存Snapshot 3.调用deduplicateGcRoots(snapshot)方法删除重复的GcRoot

代码语言:javascript
复制
void deduplicateGcRoots(Snapshot snapshot) {
    // THashMap has a smaller memory footprint than HashMap.
    final THashMap<String, RootObj> uniqueRootMap = new THashMap<>();

    final Collection<RootObj> gcRoots = snapshot.getGCRoots();
    for (RootObj root : gcRoots) {
      String key = generateRootKey(root);
      if (!uniqueRootMap.containsKey(key)) {
        uniqueRootMap.put(key, root);
      }
    }

    // Repopulate snapshot with unique GC roots.
    gcRoots.clear();
    uniqueRootMap.forEach(new TObjectProcedure<String>() {
      @Override public boolean execute(String key) {
        return gcRoots.add(uniqueRootMap.get(key));
      }
    });
  }

4.调用findLeakingReference方法,找到内存泄漏完整路径

2.10 HeapAnalyzer#findLeakingReference

代码语言:javascript
复制
private Instance findLeakingReference(String key, Snapshot snapshot) {
    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.");
    }
    List<String> keysFound = new ArrayList<>();
    for (Instance instance : refClass.getInstancesList()) {
      List<ClassInstance.FieldValue> values = classInstanceValues(instance);
      Object keyFieldValue = fieldValue(values, "key");
      if (keyFieldValue == null) {
        keysFound.add(null);
        continue;
      }
      String keyCandidate = asString(keyFieldValue);
      if (keyCandidate.equals(key)) {
        return fieldValue(values, "referent");
      }
      keysFound.add(keyCandidate);
    }
    throw new IllegalStateException(
        "Could not find weak reference with key " + key + " in " + keysFound);
  }

1.在Snapshot中根据泄漏的 activity的弱引用找到ClassObj对象(封装好的引用实例) 2.根据key找到对应类,返回泄漏的引用

2.11 HeapAnalyzer#findLeakTrace

代码语言:javascript
复制
private AnalysisResult findLeakTrace(long analysisStartNanoTime, Snapshot snapshot,
      Instance leakingRef, boolean computeRetainedSize) {

    listener.onProgressUpdate(FINDING_SHORTEST_PATH);
    //关键1
    ShortestPathFinder pathFinder = new ShortestPathFinder(excludedRefs);
    ShortestPathFinder.Result result = pathFinder.findPath(snapshot, leakingRef);

    String className = leakingRef.getClassObj().getClassName();

    // False alarm, no strong reference path to GC Roots.
    if (result.leakingNode == null) {
      return noLeak(className, since(analysisStartNanoTime));
    }

    listener.onProgressUpdate(BUILDING_LEAK_TRACE);
    LeakTrace leakTrace = buildLeakTrace(result.leakingNode);

    long retainedSize;
    if (computeRetainedSize) {

      listener.onProgressUpdate(COMPUTING_DOMINATORS);
      // Side effect: computes retained size.
      snapshot.computeDominators();

      Instance leakingInstance = result.leakingNode.instance;

      retainedSize = leakingInstance.getTotalRetainedSize();

      // TODO: check O sources and see what happened to android.graphics.Bitmap.mBuffer
      if (SDK_INT <= N_MR1) {
        listener.onProgressUpdate(COMPUTING_BITMAP_SIZE);
        retainedSize += computeIgnoredBitmapRetainedSize(snapshot, leakingInstance);
      }
    } else {
      retainedSize = AnalysisResult.RETAINED_HEAP_SKIPPED;
    }

    return leakDetected(result.excludingKnownLeaks, className, leakTrace, retainedSize,
        since(analysisStartNanoTime));
  }

1.创建一个ShortestPathFinder,找到最短的泄漏路径 2.通过buildLeakTrace方法创建出泄漏最短路径的堆栈 3.最后封装成一个AnalysisResult对象进行返回

2.12 AbstractAnalysisResultService#sendResultToListener

在HeapAnalyzerService的onHandleIntentInForeground方法中,获取了AnalysisResult后,就会调用AbstractAnalysisResultService.sendResultToListener方法

代码语言:javascript
复制
public static void sendResultToListener(@NonNull Context context,
      @NonNull String listenerServiceClassName,
      @NonNull HeapDump heapDump,
      @NonNull AnalysisResult result) {
    Class<?> listenerServiceClass;
    try {
      listenerServiceClass = Class.forName(listenerServiceClassName);
    } catch (ClassNotFoundException e) {
      throw new RuntimeException(e);
    }
    Intent intent = new Intent(context, listenerServiceClass);

    File analyzedHeapFile = AnalyzedHeap.save(heapDump, result);
    if (analyzedHeapFile != null) {
      intent.putExtra(ANALYZED_HEAP_PATH_EXTRA, analyzedHeapFile.getAbsolutePath());
    }
    ContextCompat.startForegroundService(context, intent);
  }

分析完后,将堆栈文件IO写入,然后启动服务DisplayLeakService,参数传递路径

2.13 DisplayLeakService#onHeapAnalyzed

代码语言:javascript
复制
protected final void onHeapAnalyzed(@NonNull AnalyzedHeap analyzedHeap) {
    HeapDump heapDump = analyzedHeap.heapDump;
    AnalysisResult result = analyzedHeap.result;

    String leakInfo = leakInfo(this, heapDump, result, true);
    CanaryLog.d("%s", leakInfo);

    heapDump = renameHeapdump(heapDump);
    boolean resultSaved = saveResult(heapDump, result);

    String contentTitle;
    if (resultSaved) {
      PendingIntent pendingIntent =
          DisplayLeakActivity.createPendingIntent(this, heapDump.referenceKey);
      if (result.failure != null) {
        contentTitle = getString(R.string.leak_canary_analysis_failed);
      } else {
        String className = classSimpleName(result.className);
        if (result.leakFound) {
          if (result.retainedHeapSize == AnalysisResult.RETAINED_HEAP_SKIPPED) {
            if (result.excludedLeak) {
              contentTitle = getString(R.string.leak_canary_leak_excluded, className);
            } else {
              contentTitle = getString(R.string.leak_canary_class_has_leaked, className);
            }
          } else {
            String size = formatShortFileSize(this, result.retainedHeapSize);
            if (result.excludedLeak) {
              contentTitle =
                  getString(R.string.leak_canary_leak_excluded_retaining, className, size);
            } else {
              contentTitle =
                  getString(R.string.leak_canary_class_has_leaked_retaining, className, size);
            }
          }
        } else {
          contentTitle = getString(R.string.leak_canary_class_no_leak, className);
        }
      }
      String contentText = getString(R.string.leak_canary_notification_message);
      showNotification(pendingIntent, contentTitle, contentText);
    } else {
      onAnalysisResultFailure(getString(R.string.leak_canary_could_not_save_text));
    }

    afterDefaultHandling(heapDump, result, leakInfo);
  }

这里不详细分析了,就是将堆栈弹出通知的过程。

2.15 分析一下Fragment的检测流程

只是前面少数流程不一致,创建一个FragmentRefWatcher

代码语言:javascript
复制
Application application = (Application) context.getApplicationContext();
      application.registerActivityLifecycleCallbacks(helper.activityLifecycleCallbacks);
代码语言:javascript
复制
 private final Application.ActivityLifecycleCallbacks activityLifecycleCallbacks =
        new ActivityLifecycleCallbacksAdapter() {
          @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
            for (FragmentRefWatcher watcher : fragmentRefWatchers) {
              watcher.watchFragments(activity);
            }
          }
        };

当onActivityCreated方法回调时调watcher.watchFragments(activity)

SupportFragmentRefWatcher#watchFragments

代码语言:javascript
复制
@Override public void watchFragments(Activity activity) {
    if (activity instanceof FragmentActivity) {
      FragmentManager supportFragmentManager =
          ((FragmentActivity) activity).getSupportFragmentManager();
      supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true);
    }
  }

通过activity拿到FragmentManager,然后通过registerFragmentLifecycleCallbacks注册lifecycle回调

代码语言:javascript
复制
  private final FragmentManager.FragmentLifecycleCallbacks fragmentLifecycleCallbacks =
      new FragmentManager.FragmentLifecycleCallbacks() {

        @Override public void onFragmentViewDestroyed(FragmentManager fm, Fragment fragment) {
          View view = fragment.getView();
          if (view != null) {
            refWatcher.watch(view);
          }
        }

        @Override public void onFragmentDestroyed(FragmentManager fm, Fragment fragment) {
          refWatcher.watch(fragment);
        }
      };

主要是检测fragment的view和自身的内存泄漏,剩下的流程就和activity的完全一致了

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2019-06-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.LeakCanary接入
  • 2.源码分析
    • 2.1 LeakCanary#install
      • 2.2 AndroidRefWatcherBuilder#buildAndInstall
        • 2.3 ActivityRefWatcher#install
          • 2.4 RefWatcher#watch
            • 2.5 RefWatcher#ensureGoneAsync
              • 2.6 RefWatcher#ensureGone
                • 2.7 ServiceHeapDumpListener#analyze
                  • 2.8 HeapAnalyzerService#runAnalysis
                    • 2.9 HeapAnalyzer#checkForLeak
                      • 2.10 HeapAnalyzer#findLeakingReference
                        • 2.11 HeapAnalyzer#findLeakTrace
                          • 2.12 AbstractAnalysisResultService#sendResultToListener
                            • 2.13 DisplayLeakService#onHeapAnalyzed
                              • 2.15 分析一下Fragment的检测流程
                              领券
                              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档