Android 源码系列之<十四>从源码的角度深入理解LeakCanary

转载请注明出处:http://blog.csdn.net/llew2011/article/details/52958567

在上边文章Android 源码系列之<十三>从源码的角度深入理解LeakCanary的内存泄露检测机制(中)由于篇幅原因仅仅向小伙伴们讲述了在Android开发中如何使用LeakCanary来检测应用中出现的内存泄露,并简单的介绍了LeakCanary的相关配置信息。根据上篇文章的介绍我们知道LeakCanary为了不给APP进程造成影响所以新开启了一个进程,在新开启的进程中做内存泄露检测,这篇文章将要带领小伙伴们从源码的角度出发深入了解一下LeakCanary的内存泄露检测机制,希望能给小伙伴们一点帮助,如果你对LeakCanary的原理非常熟悉了,请跳过本文(*^__^*) ……

当在项目中引入了LeakCanary后,就是进行LeakCanary的安装操作,初始化操作流程如下所示:

public class ExampleApplication extends Application {    @Override    public void onCreate() {        super.onCreate();        // 判断是否是LeakCanary的分析进程        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);    }}

安装LeakCanary前先判断当前进程是否是HeapAnalyzerService所在的远程分析进程,如果是分析进程就直接返回因为分析进程只是来分析堆信息是否存在内存泄露,否则调用LeakCanary的install()方法,该方法就是入口,我们跟进去,源码如下:

/** * Creates a {@link RefWatcher} that works out of the box, and starts watching activity * references (on ICS+). */public static RefWatcher install(Application application) {    return refWatcher(application).listenerServiceClass(DisplayLeakService.class)            .excludedRefs(AndroidExcludedRefs.createAppDefaults().build())            .buildAndInstall();}

install()方法内部通过链式调用最终返回了一个RefWatcher对象,该对象就是来监听哪些对象是否发生内存泄露的,refWatcher()方法返回AndroidRefWatcherBuilder对象以后都是调用该对象的链式方法,其中excludeRefs()方法表示排除掉由Android SDK引发的内存泄露,因为Android SDK引发的内存泄露并非我们程序造成如果检测到是Android SDK引发的就不会报告给用户(如果想详细了解SDK引发的内存泄露信息可自行查看AndroidExcludedRefs类,该类有详细说明)。最后调用的是buildAndInstall()方法,该方法源码如下:

/** * Creates a {@link RefWatcher} instance and starts watching activity references (on ICS+). */public RefWatcher buildAndInstall() {    RefWatcher refWatcher = build();    if (refWatcher != DISABLED) {        LeakCanary.enableDisplayLeakActivity(context);        ActivityRefWatcher.installOnIcsPlus((Application) context, refWatcher);    }    return refWatcher;}

buildAndInstall()方法中调用build()方法获取一个RefWatcher对象refWatcher并最终把refWatcher返回,当时在返回之前做了一个判断,若refWatcher不是DISABLED对象时就先后调用了LeakCanary的静态enableDisplayLeakActivity()方法和ActivityRefWatcher的静态installOnIcsPlus()方法。为什么要判断refWatcher对象是不是DISABLED呢?是因为在打包的时候如果当前模式是DEBUG模式那么refWatcher就不是DISABLED,否则就是DISABLED。这是间接的判断打包模式来决定时候安装LeakCanary库。

LeakCanary的静态方法enableDisplayLeakActivity()是动态的设定DisplayLeakActivity为可用状态,因为在上篇文章中介绍过DisplayLeakActivity默认是不可用的。ActivityRefWatcher的installOnIcsPlus()方法是真正的安装LeakCanary库的,该方法源码如下:

public static void installOnIcsPlus(Application application, RefWatcher refWatcher) {    if (SDK_INT < ICE_CREAM_SANDWICH) {        // 如果当前SDK版本号小于4.0则直接返回        // If you need to support Android < ICS, override onDestroy() in your base activity.        return;    }    ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);    activityRefWatcher.watchActivities();}

installOnIcsPlus()方法先判断当前SDK版本号,如果SDK版本号低于4.0则什么都不做就直接返回了,那也就是说LeakCanary库在Android4.0之前是没法直接使用的,若要在4.0之前使用就需要在基类BaseActivity的onDestroy()方法中调用RefWatcher的watch()方法来监控。接着是创建了一个ActivityRefWatcher实例对象并调用该对象的watchActivities()方法,根据方法名字就知道是监控Activity的,跟进源码看一下该方法,源码如下:

public void watchActivities() {    // Make sure you don't get installed twice.    // 开始监控Activity之前先尝试移除已经添加过的回调,确保只添加一次    stopWatchingActivities();    application.registerActivityLifecycleCallbacks(lifecycleCallbacks);}public void stopWatchingActivities() {    // 移除Application中已经添加的回调接口    application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks);}

通过阅读watchActivities()方法发现原来LeakCanary是巧妙的利用了Android 4.0之后的API,因为在4.0之后Google给Application添加了Activity的生命周期回调接口,如果我们注入了该回调接口,那么当Activity的声明周期发生变化的时候就会回调相关方法。我们看一下注入的回调接口lifecycleCallbacks,源码如下:

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) {                // 每当Activity销毁时系统都会回调该方法                ActivityRefWatcher.this.onActivityDestroyed(activity);            }        };

在lifecycleCallbacks()回调方法中仅仅在onActivityDestroyed()方法中回调了ActivityRefWatcher的onActivityDestroyed()方法并把要销毁的Activity传递进来,因此我们可以猜测当Activity被销毁的时候都会触发onActivityDestroyed()方法,LeakCanary就是通过监控传递进来的Activity通过判断它的应用关系来检测是否是发生了内存泄露。

到这里我们已经知道了LeakCanary检测Activity的时机,接下来看看LeakCanary是如何判断Activity存在内存泄露的。onActivityDestroyed()方法就是检测Activity是否发生内存泄露的,其源码如下:

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

onActivityDestroyed()方法中辗转调用了refWatcher的watch()方法(该方法可以监控所有的Java对象),watch()方法又调用其重载方法,代码如下:

/** * 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();    String key = UUID.randomUUID().toString();    retainedKeys.add(key);    final KeyedWeakReference reference =            new KeyedWeakReference(watchedReference, key, referenceName, queue);    ensureGoneAsync(watchStartNanoTime, reference);}

watch()方法中先是校验当前程序是否是DEBUG模式,如果是就直接返回否则对传递进来的参数进行非空校验,校验通过之后通过生成一个唯一的UUID并存储在retainKeys中,然后根据这个UUID初始化一个KeyedWeakReference实例对象reference,reference中保留了当前UUID和name,然后调用了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的execute()方法,经过分析watchExecutor是AndroidWatchExecutor的实例对象,也就是说最终执行的是AndroidWatchExecutor的execute()方法,我们进入该方法看看:

@Overridepublic void execute(Retryable retryable) {    // 确保在主线程中执行    if (Looper.getMainLooper().getThread() == Thread.currentThread()) {        waitForIdle(retryable, 0);    } else {        postWaitForIdle(retryable, 0);    }}

在AndroidWatchExecutor的execute()方法中首先判断当前线程是不是主线程,如果是主线程就直接调用waitForIdle()方法,否则调用postWaitForIdle()方法切换到主线后再执行waitForIdle()方法。由此可见execute()方法是保证操作在主线程中进行,我们进入waitForIdle()方法看一下,源码如下所示:

void waitForIdle(final Retryable retryable, final int failedAttempts) {    // This needs to be called from the main thread.    // 当前方法只能在主线程中调用    Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {        @Override        public boolean queueIdle() {            postToBackgroundWithDelay(retryable, failedAttempts);            return false;        }    });}

waitForIdle()方法中通过调用Looper的myQueue()方法拿到当前主线程的消息队列MessageQueue对象,接着调用MessageQueue对象的addIdleHandler()方法给当前消息队列添加一个IdleHandler实例对象,IdleHandler是一个接口,该接口中定义的queueIdle()方法的返回值为boolean类型,当返回true表示继续保持IdleHandler实例对象否则移除该实例对象。需要说明的是:addIdleHandler()方法的目的是给当前消息队列添加一个回调接口,当当前消息队列中没有可执行的消息时就会回调该IdleHandler的queueIdle()方法,说白了就是等到主线程空闲的时候就会回调queueIdle()方法,queueIdle()方法返回了false表明该方法只被执行一次。在queueIdle()方法中调用了postToBackgroundWithDelay()方法,我们跟进postToBackgroundWithDelay()方法中看一下,该方法源码如下:

private void postToBackgroundWithDelay(final Retryable retryable, final int failedAttempts) {    long exponentialBackoffFactor = (long) Math.min(Math.pow(2, failedAttempts), maxBackoffFactor);    long delayMillis = initialDelayMillis * exponentialBackoffFactor;    backgroundHandler.postDelayed(new Runnable() {        @Override        public void run() {            // 切换到子线程中执行,retryable.run()方法            Retryable.Result result = retryable.run();            if (result == RETRY) {                // 如果retryable的run方法返回类型为RETRY,则循环以上流程                postWaitForIdle(retryable, failedAttempts + 1);            }        }    }, delayMillis);}

postToBackgroundWithDelay()方法又切换到切换到子线程中执行Retryable的run()方法,如果run()方法返回值为RETRY就继续调用postWaitForIdle()方法循环以上流程否则本次调用结束,由于Retryable的run()方法中调用的是RefWatcher的ensureGone()方法,我们进入该方法看一下,其源码如下所示:

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;    }    // gone()方法检测是否包含当前reference    if (gone(reference)) {        return DONE;    }    // 触发垃圾回收器进行回收操作    gcTrigger.runGc();    // 再次尝试移除操作    removeWeaklyReachableReferences();    // 若gone()方法返回false,表示有内存泄露    if (!gone(reference)) {        long startDumpHeap = System.nanoTime();        long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);        // 把堆内存相关信息映射成文件        File heapDumpFile = heapDumper.dumpHeap();        if (heapDumpFile == RETRY_LATER) {            // Could not dump the heap.            return RETRY;        }        long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);        // 分析堆内存的映射文件,判断是否发生内存泄露        heapdumpListener.analyze(                new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs, watchDurationMs,                        gcDurationMs, heapDumpDurationMs));    }    return DONE;}

在ensureGone()方法中先尝试从缓存队列中移除相应软引用,然后再调用gone()方法检测缓存集合中是否还包含传递进来的弱引用,如果还包含就表明可能发生了内存泄露,这时候调用gcTrigger.runGc()操作出发垃圾回收器进行内存回收,之后再调用gone()方法进行检测,如果gone()方法依然返回false就调用heapDumper的dumpHeap()方法把堆内存映射成文件,接着就是启动调用heapdumpListener的analyze()方法来分析堆映射文件,源码如下:

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

analyze()方法中直接调用了HeapAnalyzerService的静态方法runAnalysis(),HeapAnalyzerService是IntentService的子类,其主要特点是执行完毕后会自行销毁,如果你对IntentService不熟悉建议阅读一下我先前写的文章Android 源码系列之<七>从源码的角度深入理解IntentService及HandlerThread,在这篇文章中有对IntentService做详细解说。我们看一下HeapAnalyzerService的源码,如下所示:

/** * This service runs in a separate process to avoid slowing down the app process or making it run * out of memory. */public final class HeapAnalyzerService extends IntentService {    private static final String LISTENER_CLASS_EXTRA = "listener_class_extra";    private static final String HEAPDUMP_EXTRA = "heapdump_extra";    public static void runAnalysis(Context context, HeapDump heapDump,                                   Class<? extends AbstractAnalysisResultService> listenerServiceClass) {        Intent intent = new Intent(context, HeapAnalyzerService.class);        intent.putExtra(LISTENER_CLASS_EXTRA, listenerServiceClass.getName());        intent.putExtra(HEAPDUMP_EXTRA, heapDump);        // 启动HeapAnalyzerService服务        context.startService(intent);    }    public HeapAnalyzerService() {        super(HeapAnalyzerService.class.getSimpleName());    }    @Override    protected void onHandleIntent(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);        // 初始化HeapAnalyzer实例对象heapAnalyzer        HeapAnalyzer heapAnalyzer = new HeapAnalyzer(heapDump.excludedRefs);        // 调用heapAnalyzer对象的checkForLeak()方法检测内存泄露        AnalysisResult result = heapAnalyzer.checkForLeak(heapDump.heapDumpFile, heapDump.referenceKey);        // 把检测结果传送给AbstractAnalysisResultService        AbstractAnalysisResultService.sendResultToListener(this, listenerClassName, heapDump, result);    }}

HeapAnalyzerService的静态runAnalysis()方法启动了HeapAnalyzerService服务,该服务启动后会调用onHandleIntent()方法,在onHandleIntent()方法执行完毕后HeapAnalyzerService服务的生命周期也就结束了。在onHandleIntent()方法中通过HeapAnalyzer对象的checkForLeak()方法来检测分析是否发生了内存泄露。我们看一下checkForLeak()方法,其源码如下:

/** * Searches the heap dump for a {@link KeyedWeakReference} instance with the corresponding key, * and then computes the shortest strong reference path from that instance to the GC roots. */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对象        HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);        // 根据HprofBuffer对象生成一个解析器parser        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));    }}

在checkForLeak()方法中主要是利用square的另外一个开源库HAHA来做内存检测的,该库的GitHub地址为:https://github.com/square/haha。HAHA会根据生成的内存快照文件来分析引用路径从而确定是否发生了内存泄露,如果你想更为详细的了解HAHA的工作原理请自行到github上查看源码,这里由于篇幅关系就不再详述了。总之通过HAHA来分析内存泄露,然后把分析结果返回,最后把分析结果传递给AbstractAnalysisResultService的sendResultToListener()方法,该方法源码如下:

public abstract class AbstractAnalysisResultService extends IntentService {    private static final String HEAP_DUMP_EXTRA = "heap_dump_extra";    private static final String RESULT_EXTRA = "result_extra";    public static void sendResultToListener(Context context, String listenerServiceClassName,                                            HeapDump heapDump, AnalysisResult result) {        Class<?> listenerServiceClass;        try {            // 加载listenerServiceClassName类类,listenerServiceClassName为DisplayLeakService            listenerServiceClass = Class.forName(listenerServiceClassName);        } catch (ClassNotFoundException e) {            throw new RuntimeException(e);        }        // 由于DisplayLeakService是AbstractAnalysisResultService的实现类        Intent intent = new Intent(context, listenerServiceClass);        intent.putExtra(HEAP_DUMP_EXTRA, heapDump);        intent.putExtra(RESULT_EXTRA, result);        context.startService(intent);    }    public AbstractAnalysisResultService() {        super(AbstractAnalysisResultService.class.getName());    }    @Override    protected final void onHandleIntent(Intent intent) {        HeapDump heapDump = (HeapDump) intent.getSerializableExtra(HEAP_DUMP_EXTRA);        AnalysisResult result = (AnalysisResult) intent.getSerializableExtra(RESULT_EXTRA);        try {            // 可以在onHeapAnalyzed()方法中上报内存泄露等操作            onHeapAnalyzed(heapDump, result);        } finally {            //noinspection ResultOfMethodCallIgnored            // 最后把该内存映射文件删除掉,该轮分析也就结束了            heapDump.heapDumpFile.delete();        }    }    /**     * Called after a heap dump is analyzed, whether or not a leak was found.     * Check {@link AnalysisResult#leakFound} and {@link AnalysisResult#excludedLeak} to see if there     * was a leak and if it can be ignored.     * <p>     * This will be called from a background intent service thread.     * <p>     * It's OK to block here and wait for the heap dump to be uploaded.     * <p>     * The heap dump file will be deleted immediately after this callback returns.     */    protected abstract void onHeapAnalyzed(HeapDump heapDump, AnalysisResult result);}

AbstractAnalysisResultService也是IntentService类型,它是一个抽象类,对外提供了抽象方法onHeapAnalyzed(),目的是根据分析结果让开发者可以自己处理,比如可以把内存泄露信息存储或者是上传到服务器等。在AbstractAnalysisResultService的sendResultToListener()方法中启动了DisplayLeakService,需要注意的是DisplayLeakService是AbstractAnalysisResultService的实现类并重写了抽象方法onHeapAnalyzed()方法,该类的源码如下:

/** * Logs leak analysis results, and then shows a notification which will start {@link * DisplayLeakActivity}. * <p> * You can extend this class and override {@link #afterDefaultHandling(HeapDump, AnalysisResult, * String)} to add custom behavior, e.g. uploading the heap dump. */public class DisplayLeakService extends AbstractAnalysisResultService {    @Override    protected final void onHeapAnalyzed(HeapDump heapDump, AnalysisResult result) {        // 获取分析的泄露信息        String leakInfo = leakInfo(this, heapDump, result, true);        CanaryLog.d(leakInfo);        boolean resultSaved = false;        boolean shouldSaveResult = result.leakFound || result.failure != null;        if (shouldSaveResult) {            // 保存            heapDump = renameHeapdump(heapDump);            resultSaved = saveResult(heapDump, result);        }        PendingIntent pendingIntent;        String contentTitle;        String contentText;        if (!shouldSaveResult) {            contentTitle = getString(R.string.leak_canary_no_leak_title);            contentText = getString(R.string.leak_canary_no_leak_text);            pendingIntent = null;        } else if (resultSaved) {            // 创建PendingIntent            pendingIntent = DisplayLeakActivity.createPendingIntent(this, heapDump.referenceKey);            if (result.failure == null) {                String size = formatShortFileSize(this, result.retainedHeapSize);                String className = classSimpleName(result.className);                if (result.excludedLeak) {                    contentTitle = getString(R.string.leak_canary_leak_excluded, className, size);                } else {                    contentTitle = getString(R.string.leak_canary_class_has_leaked, className, size);                }            } else {                contentTitle = getString(R.string.leak_canary_analysis_failed);            }            contentText = getString(R.string.leak_canary_notification_message);        } else {            contentTitle = getString(R.string.leak_canary_could_not_save_title);            contentText = getString(R.string.leak_canary_could_not_save_text);            pendingIntent = null;        }        // New notification id every second.        // 弹出一个Notification        int notificationId = (int) (SystemClock.uptimeMillis() / 1000);        showNotification(this, contentTitle, contentText, pendingIntent, notificationId);        // 最后调用afterDefaultHandling()方法        afterDefaultHandling(heapDump, result, leakInfo);    }    private boolean saveResult(HeapDump heapDump, AnalysisResult result) {        File resultFile = new File(heapDump.heapDumpFile.getParentFile(),                heapDump.heapDumpFile.getName() + ".result");        FileOutputStream fos = null;        try {            fos = new FileOutputStream(resultFile);            ObjectOutputStream oos = new ObjectOutputStream(fos);            oos.writeObject(heapDump);            oos.writeObject(result);            return true;        } catch (IOException e) {            CanaryLog.d(e, "Could not save leak analysis result to disk.");        } finally {            if (fos != null) {                try {                    fos.close();                } catch (IOException ignored) {                }            }        }        return false;    }    private HeapDump renameHeapdump(HeapDump heapDump) {        String fileName =                new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss_SSS'.hprof'", Locale.US).format(new Date());        File newFile = new File(heapDump.heapDumpFile.getParent(), fileName);        boolean renamed = heapDump.heapDumpFile.renameTo(newFile);        if (!renamed) {            CanaryLog.d("Could not rename heap dump file %s to %s", heapDump.heapDumpFile.getPath(),                    newFile.getPath());        }        return new HeapDump(newFile, heapDump.referenceKey, heapDump.referenceName,                heapDump.excludedRefs, heapDump.watchDurationMs, heapDump.gcDurationMs,                heapDump.heapDumpDurationMs);    }    /**     * You can override this method and do a blocking call to a server to upload the leak trace and     * the heap dump. Don't forget to check {@link AnalysisResult#leakFound} and {@link     * AnalysisResult#excludedLeak} first.     */    protected void afterDefaultHandling(HeapDump heapDump, AnalysisResult result, String leakInfo) {        // 空实现,我们可以重写该方法,把结果上传到服务器等    }}

DisplayLeakService中主要操作就是把堆内存映射文件保存了下来,然后创建一个Notification并弹出,用来通知我们应用发生了内存泄露,当我们点击了Notification就可以看到有关内存的泄露的详细信息了,这就是LeakCanary的主要流程了。这里可以借用这篇文章的流程图:

好了,有关LeakCanary的源码分析就到这里了,通过分析源码可以更深层次的理解其原理和思想,所以在平时开发中如果有使用了那些好的第三方开源库建议小伙伴们都要读一读别人的源码,看看别人是怎么实现的,通过阅读源码可以更好的提升自己水平,最后感谢收看。

在前进的路上,主动搬开别人脚下的绊脚石,有时往往也是为自己铺路。

Android 源码系列之&lt;十四&gt;从源码的角度深入理解LeakCanary

相关文章:

你感兴趣的文章:

标签云: