leakCanary 分析

一、leakCanary概念了解

1、leakCanary工作流程

LeakCannary 的主要原理,其實很簡單,大概可以分為以下幾步:

  • (1) 監(jiān)測Activity 的生命周期的 onDestroy() 的調(diào)用。
  • (2) 當某個 Activity 的 onDestroy() 調(diào)用后,便對這個 activity 創(chuàng)建一個帶 ReferenceQueue 的弱引用,并且給這個弱引用創(chuàng)建了一個 key 保存在 Set集合 中。
  • (3) 如果這個 activity 可以被回收,那么弱引用就會被添加到 ReferenceQueue 中。
  • (4) 等待主線程進入 idle(即空閑)后,通過一次遍歷,在 ReferenceQueue 中的弱引用所對應的 key 將從 retainedKeys 中移除,說明其沒有內(nèi)存泄漏。
  • (5) 如果 activity 沒有被回收,先強制進行一次 gc,再來檢查,如果 key 還存在 retainedKeys 中,說明 activity 不可回收,同時也說明了出現(xiàn)了內(nèi)存泄漏。
  • (6) 發(fā)生內(nèi)存泄露之后,dump內(nèi)存快照,分析 hprof 文件,找到泄露路徑(使用 haha 庫分析),發(fā)送到通知欄

LeakCanary對于內(nèi)存泄漏的檢測非常有效,但也并不是所有的內(nèi)存泄漏都能檢測出來。

  • 1、無法檢測出Service中的內(nèi)存泄漏問題
  • 2、如果最底層的MainActivity一直未走onDestroy生命周期(它在Activity棧的最底層),無法檢測出它的調(diào)用棧的內(nèi)存泄漏。
2c2aff757b430b65601240a19d326e5.png

2、java中的4中引用類型

  • 強引用:不會被GC回收
  • 軟引用:內(nèi)存不足的時候會被GC回收
  • 弱引用:當下次GC的時候會回收
  • 虛引用:任何情況都可以回收

二、leakCarcry分析

在分析Leak Canary原理之前,我們先來簡單了解WeakReference和ReferenceQueue的作用,為什么要了解這些知識呢?Leak Canary其實內(nèi)部就是使用這個機制來監(jiān)控對象是否被回收了,當然Leak Canary的監(jiān)控僅僅針對Activity和Fragment,所以這塊有引入了ActivityLifecycleCallBack,后面會說,這里的回收是指JVM在合適的時間觸發(fā)GC,并將回收的WeakReference對象放入與之關聯(lián)的ReferenceQueue中表示GC回收了該對象,Leak Canary通過上賣弄的檢測返現(xiàn)有些對象的生命周期本該已經(jīng)結束了,但是任然在占用內(nèi)存,這時候就判定是已經(jīng)泄露了,那么下一步就是開始解析析headump文件,分析引用鏈,至此就結束了,其中需要注意的是這是WeakReference.get方法獲取到的對象是null,所以Leak Canary使用了繼承WeakReference.類,并把傳入的對象作為成員變量保存起來,這樣當GC發(fā)生時雖然把WeakReference中引用的對象置為了null也不會把WeakReference中我們拓展的類的成員變量置為null,這樣我們就可以做其他的操作,比如:Leak Canary中把WeakReference存放在Set集合中,在恰當?shù)臅r候需要移除Set中的WeakReference的引用,這個機制Glide中的內(nèi)存緩存 也是使用了該機制,關于WeakReference和ReferenceQueue機制就不多說網(wǎng)上有很多可以了解一下。

1、WeakReference和ReferenceQueue機制

/**
 * 監(jiān)控對象被回收,因為如果被回收就會就如與之關聯(lián)的隊列中
 */
private void monitorClearedResources() {
    Log.e("tag", "start monitor");
    try {
        int n = 0;
        WeakReference k;
        while ((k = (WeakReference) referenceQueue.remove()) != null) {
            Log.e("tag", (++n) + "回收了:" + k + "   object: " + k.get());
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}


private ReferenceQueue<WeakRefrencesBean> referenceQueue = new ReferenceQueue<>();

class WeakRefrencesBean {
    private String name;

    public WeakRefrencesBean(String name) {
        this.name = name;
    }
}

new Thread() {
        @Override
        public void run() {
            monitorClearedResources();
        }
    }.start();

    new Thread() {
        @Override
        public void run() {
            while (true) {
                new WeakReference<WeakRefrencesBean>(new WeakRefrencesBean("aaaa"), referenceQueue);
            }
        }
    }.start();

輸出的日志:

1回收了:java.lang.ref.WeakReference@21f8376e   object: null
2回收了:java.lang.ref.WeakReference@24a74e0f   object: null
3回收了:java.lang.ref.WeakReference@39efe9c   object: null
4回收了:java.lang.ref.WeakReference@4ee20a5   object: null
3回收了:java.lang.ref.WeakReference@bf45c7a   object: null
4回收了:java.lang.ref.WeakReference@b94bc2b   object: null
5回收了:java.lang.ref.WeakReference@33eb6888   object: null

上面是一個監(jiān)控對象回收,因為如果對象被回收就把該對象加入如與之關聯(lián)的隊列中,接著開啟線程制造觸發(fā)GC,并開啟線程監(jiān)控對象回收,Leak Canary也是利用這個機制完成對一些對象本該生命周期已經(jīng)結束,還常駐內(nèi)存,就算觸發(fā)GC也不會回收,Leak Canary就判斷為泄漏,針對于內(nèi)存泄漏,我們知道有些對象是不能被GC回收的,JVM虛擬機的回收就是可達性算法,就是從GC Root開始檢測,如果不可達那么就會被第一次標志,再次GC就會被回收。

2、能夠作為 GC Root的對象

  • 虛擬機棧,在大家眼里也叫作棧(棧幀中的本地變量表)中引用的對象;
  • 方法區(qū)中類靜態(tài)屬性引用的對象;
  • 方法區(qū)中常量引用的對象;
  • 本地方法棧中JNI引用的對象;

3、Leak Canary是如何判斷Activity或Fragment的生命周期結束了呢?

  • Leak Canary是通過 Application的內(nèi)部類ActivityLifecycleCallbacks檢測Activity的生命周期是否結束了,如果回調(diào)了onActivityDestroyed方法,那么表示Activity的聲明周期已經(jīng)結束了,這時候就要執(zhí)行GC檢測了。
  • 對于Fragment是通過FragmentManager的內(nèi)部接口FragmentLifecycleCallbacks檢測Fragment的聲明周期的類似ActivityLifecycleCallbacks接口。

4、開始Leak Canary源碼解讀

步驟無非就是:
1、安裝,實際上就是做一些初始化的操作;
2、檢測時機,比如:回調(diào)onActivityDestroyed方法開始檢測;
3、UI的展示;

5、安裝

Leak Canary的地方就是 LeakCanary.install(this)方法開始,代碼如下:

一般我們使用Leak Canaryu都是在Application中調(diào)用:

public class ExampleApplication extends Application {
    @Override
    public void onCreate() {
      super.onCreate();
      setupLeakCanary();
    }

   protected void setupLeakCanary() {
     enabledStrictMode();
     if (LeakCanary.isInAnalyzerProcess(this)) {
         return;
      }
   LeakCanary.install(this);
  }
   ...
 }

在install方法之前有個判斷,這個判斷是用來判斷是否是在LeakCanary的堆統(tǒng)計進程(HeapAnalyzerService),也就是我們不能在我們的App進程中初始化LeakCanary,代碼如下:

/**
 * 當前進程是否是運行{@link HeapAnalyzerService}的進程中,這是一個與普通應用程序進程不同的進程。
 */
public static boolean isInAnalyzerProcess(@NonNull Context context) {
    Boolean isInAnalyzerProcess = LeakCanaryInternals.isInAnalyzerProcess;
    // 這里只需要為每個進程計算一次。
    if (isInAnalyzerProcess == null) {
        //把Context和HeapAnalyzerService服務作為參數(shù)傳進isInServiceProcess方法中
        isInAnalyzerProcess = isInServiceProcess(context, HeapAnalyzerService.class);
        LeakCanaryInternals.isInAnalyzerProcess = isInAnalyzerProcess;
    }
    return isInAnalyzerProcess;
}

在isInAnalyzerProcess方法中有調(diào)用了isInServiceProcess方法,代碼如下:

public static boolean isInServiceProcess(Context context, Class<? extends Service> serviceClass) {
    PackageManager packageManager = context.getPackageManager();
    PackageInfo packageInfo;
    try {
        packageInfo = packageManager.getPackageInfo(context.getPackageName(), GET_SERVICES);
    } catch (Exception e) {
        CanaryLog.d(e, "Could not get package info for %s", context.getPackageName());
        return false;
    }
    //主進程
    String mainProcess = packageInfo.applicationInfo.processName;


    //構造進程
    ComponentName component = new ComponentName(context, serviceClass);
    ServiceInfo serviceInfo;
    try {
        serviceInfo = packageManager.getServiceInfo(component, PackageManager.GET_DISABLED_COMPONENTS);
    } catch (PackageManager.NameNotFoundException ignored) {
        // Service is disabled.
        return false;
    }

    //判斷當前HeapAnalyzerService服務進程名和主進程名是否相等,如果相等直接返回false,因為LeakCanary不能再當前進程中運行
    if (serviceInfo.processName.equals(mainProcess)) {
        CanaryLog.d("Did not expect service %s to run in main process %s", serviceClass, mainProcess);
        // Technically we are in the service process, but we're not in the service dedicated process.
        return false;
    }

    int myPid = android.os.Process.myPid();
    ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
    ActivityManager.RunningAppProcessInfo myProcess = null;
    List<ActivityManager.RunningAppProcessInfo> runningProcesses;
    try {
        runningProcesses = activityManager.getRunningAppProcesses();
    } catch (SecurityException exception) {
        // https://github.com/square/leakcanary/issues/948
        CanaryLog.d("Could not get running app processes %d", exception);
        return false;
    }
    if (runningProcesses != null) {
        for (ActivityManager.RunningAppProcessInfo process : runningProcesses) {
            if (process.pid == myPid) {
                myProcess = process;
                break;
            }
        }
    }
    if (myProcess == null) {
        CanaryLog.d("Could not find running process for %d", myPid);
        return false;
    }

    return myProcess.processName.equals(serviceInfo.processName);
}

實際上LeakCanary最終會調(diào)用LeakCanaryInternals.isInServiceProcess方法,通過PackageManager、ActivityManager以及android.os.Process來判斷當前進程是否為HeapAnalyzerService運行的進程,因為我們不能在我們的App進程中初始化LeakCanary。

接下來我們開始LeakCanary真正的實現(xiàn),從LeakCanary.install(this)方法開始,代碼如下:

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

實際上這一步返回的RefWatcher的實現(xiàn)類AndroidRefWatcher,主要是做些關乎初始化的操作,這些不展開講,直接進入buildAndInstall()方法中,代碼如下:

public RefWatcher buildAndInstall() {

    //install()方法只能一次調(diào)用,多次調(diào)用將拋出異常
    if (LeakCanaryInternals.installedRefWatcher != null) {
        throw new UnsupportedOperationException("buildAndInstall() should only be called once.");
    }

    //初始化RefWatcher,這個東西是用來檢查內(nèi)存泄漏的,包括解析堆轉儲文件這些東西
    RefWatcher refWatcher = build();

    //如果RefWatcher還沒有初始化,就會進入這個分支
    if (refWatcher != DISABLED) {
        if (enableDisplayLeakActivity) {
            //setEnabledAsync最終調(diào)用了packageManager.setComponentEnabledSetting,
            // 將Activity組件設置為可用,即在manifest中enable屬性。
            // 也就是說,當我們運行LeakCanary.install(this)的時候,LeakCanary的icon才顯示出來
            LeakCanaryInternals.setEnabledAsync(context, DisplayLeakActivity.class, true);
        }


        //ActivityRefWatcher.install和FragmentRefWatcher.Helper.install的功能差不多,注冊了生命周期監(jiān)聽。
        // 不同的是,前者用application監(jiān)聽Activity的生命周期,后者用Activity監(jiān)聽也就是Activity回調(diào)onActivityCreated方法,
        // 然后獲取FragmentManager調(diào)用registerFragmentLifecycleCallbacks方法注冊,監(jiān)聽fragment的生命周期,
        // 而且用到了leakcanary-support-fragment包,兼容了v4的fragment。
        if (watchActivities) {
            ActivityRefWatcher.install(context, refWatcher);
        }
        if (watchFragments) {
            FragmentRefWatcher.Helper.install(context, refWatcher);
        }
    }
    LeakCanaryInternals.installedRefWatcher = refWatcher;
    return refWatcher;
}

在buildAndInstall方法中有幾點:

  • 首先會調(diào)用RefWatcherBuilder.build方法創(chuàng)建RefWatcher,RefWatcher是檢測內(nèi)存泄漏相關的;
  • 緊接著將Activity組件設置為可用,即在manifest中enable屬性,也就是說,當我們運行LeakCanary.install(this)的時候,LeakCanary的icon才在桌面才會顯示出來;
  • 然后就是ActivityRefWatcher.install和FragmentRefWatcher.Helper.install方法,注冊了Activity和Fragment的生命周期監(jiān)聽,不同的是,前者用application監(jiān)聽Activity的生命周期,后者用Activity監(jiān)聽也就是Activity回調(diào)onActivityCreated方法,然后通過Activity獲取FragmentManager調(diào)用并FragmentManager的registerFragmentLifecycleCallbacks方法注冊監(jiān)聽fragment的生命周期,而且用到了leakcanary-support-fragment包,兼容了v4的fragment。

RefWatcher類是用來監(jiān)控對象的引用是否可達,當引用變成不可達,那么就會觸發(fā)堆轉儲(HeapDumper),來看看RefWatcherBuilder.build方法的具體實現(xiàn),代碼如下:

public final RefWatcher build() {

    //如果已經(jīng)初始化了,直接返回RefWatcher.DISABLED表示已經(jīng)初始化了
    if (isDisabled()) {
        return RefWatcher.DISABLED;
    }

    if (heapDumpBuilder.excludedRefs == null) {
        heapDumpBuilder.excludedRefs(defaultExcludedRefs());
    }

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

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


    //創(chuàng)建堆轉儲對象
    HeapDumper heapDumper = this.heapDumper;
    if (heapDumper == null) {
        //返回的是HeapDumper.NONE,HeapDumper內(nèi)部實現(xiàn)類,
        heapDumper = defaultHeapDumper();
    }

    //創(chuàng)建監(jiān)控線程池
    WatchExecutor watchExecutor = this.watchExecutor;
    if (watchExecutor == null) {
        //默認返回 NONE
        watchExecutor = defaultWatchExecutor();
    }


    //默認的Gc觸發(fā)器
    GcTrigger gcTrigger = this.gcTrigger;
    if (gcTrigger == null) {
        gcTrigger = defaultGcTrigger();
    }

    if (heapDumpBuilder.reachabilityInspectorClasses == null) {
        heapDumpBuilder.reachabilityInspectorClasses(defaultReachabilityInspectorClasses());
    }
    
    //創(chuàng)建把參數(shù)構造RefWatcher
    return new RefWatcher(watchExecutor, debuggerControl, gcTrigger, heapDumper, heapDumpListener,
            heapDumpBuilder);
}

如上代碼知道,實際上是為了創(chuàng)建RefWatcher實例,和一些在檢測中的環(huán)境初始化,比如線程池、GC觸發(fā)器等等。

回到最初的biuldInstall方法中,知道監(jiān)控Activity和Fragment是查不到的所以這里就只分析Activity相關的,也就是ActivityRefWatcher.install方法,代碼如下:

public static void install(@NonNull Context context, @NonNull RefWatcher refWatcher) {
    Application application = (Application) context.getApplicationContext();
    ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);

    //注冊ActivityLifecycleCallbacks監(jiān)聽每一個Activity的生命周期
    application.registerActivityLifecycleCallbacks(activityRefWatcher.lifecycleCallbacks);
}

可以知道這里是使用的裝飾模式,使用ActivityRefWatcher對RefWatcher做了包裝,接著注冊ActivityLifecycleCallbacks監(jiān)聽每一個Activity的生命周期的onActivityDestroyed方法,這也就是檢測泄漏開始的地方,而在onActivityDestroyed方法方法中會調(diào)用refWatcher.watch方法把activity作為參數(shù)傳進去,代碼如下:

 private final Application.ActivityLifecycleCallbacks lifecycleCallbacks = new ActivityLifecycleCallbacksAdapter() {
    @Override
    public void onActivityDestroyed(Activity activity) {
        //當Activity被銷毀了,那么應該檢測是否內(nèi)存泄漏
        refWatcher.watch(activity);
    }
};

可以看到在Activity銷毀時會回調(diào)onActivityDestroyed方法,然后把該activity作為參數(shù)傳遞給refWatcher.watch(activity)方法,watch方法代碼如下:

public void watch(Object watchedReference, String referenceName) {
   
    ..........

    final long watchStartNanoTime = System.nanoTime();
    //給該引用生成UUID
    String key = UUID.randomUUID().toString();
    //給該引用的UUID保存至Set中,強引用
    retainedKeys.add(key);
    //KeyedWeakReference 繼承至WeakReference,由于KeyedWeakReference如果回收了,那么當中的對象通過get返回的是null,
    // 所以需要保存key和name作為標識,Glide也是此做法,KeyedWeakReference實現(xiàn)WeakReference
    final KeyedWeakReference reference = new KeyedWeakReference(watchedReference, key, referenceName, queue);

    //開始檢測
    ensureGoneAsync(watchStartNanoTime, reference);
}

在watch方法中有如下幾點:

  • 首先通過UUID生成表示該引用的Key,而這個Key會當做強引用保存到RefWatcher的成員變量Set集合中;
  • 接著創(chuàng)建KeyedWeakReference,而KeyedWeakReference 繼承至WeakReference,由于KeyedWeakReference如果回收了,那么當中的對象通過get返回的是null,所以為了能在GC之后拿到Key,需要將保存key和name作為KeyedWeakReference中,Glide也是此做法;
  • 接著調(diào)用ensureGoneAsync(watchStartNanoTime, reference)方法開始檢測是否有內(nèi)存泄漏;

ensureGoneAsync方法代碼如下:

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

在ensureGoneAsync方法中直接執(zhí)行線程池(AndroidWatchExecutor),而這個線程池就是在剛開始的時候LeakCanary.install方法中創(chuàng)建RefWatcher的子類AndroidRefWatcher的時候創(chuàng)建的,接著看看ensureGone方法,代碼如下:

Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
    //gc 開始的時間
    long gcStartNanoTime = System.nanoTime();
    long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);

    //從Set中移除不能訪問引用,意思就是GC之后該引用對象是否加入隊列了,如果已經(jīng)加入隊列說明不會造成泄漏的風險
    removeWeaklyReachableReferences();

    if (debuggerControl.isDebuggerAttached()) {
        // The debugger can create false leaks.
        return RETRY;
    }
    if (gone(reference)) {
        return DONE;
    }

    //嘗試GC
    gcTrigger.runGc();

    //從Set中移除不能訪問引用,意思就是GC之后該引用對象是否加入隊列了,如果已經(jīng)加入隊列說明不會造成泄漏的風險
    removeWeaklyReachableReferences();


    //到這一步說明該對象按理來說聲明周期是已經(jīng)結束了的,但是通過前面的GC卻不能回收,說明已經(jīng)造成了內(nèi)存泄漏,那么解析hprof文件,得到該對象的引用鏈,也就是要觸發(fā)堆轉儲
    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);

        HeapDump heapDump = heapDumpBuilder.heapDumpFile(heapDumpFile).referenceKey(reference.key)
                .referenceName(reference.name)
                .watchDurationMs(watchDurationMs)
                .gcDurationMs(gcDurationMs)
                .heapDumpDurationMs(heapDumpDurationMs)
                .build();
        //開始解釋堆轉儲文件
        heapdumpListener.analyze(heapDump);
    }
    return DONE;
}

在ensureGone方法中有如下幾點:

  • 調(diào)用removeWeaklyReachableReferences方法,從Set中移除不能訪問引用,意思就是GC之后該引用對象是否加入隊列了,如果已經(jīng)加入隊列說明不會造成泄漏的風險,就將引用從Set集合中移除;
  • 緊接著調(diào)用gcTrigger.runGc方法嘗試GC,看看能不能回收引用對象;
  • 再次調(diào)用removeWeaklyReachableReferences方法,從Set中移除不能訪問引用,意思就是GC之后該引用對象是否加入隊列了,如果已經(jīng)加入隊列說明不會造成泄漏的風險,也就是在手動觸發(fā)GC之后,再次檢測是否可以回收對象;
  • *最后通過gone(reference)方法檢測Set集合中是否還存在該對象,如果存在說明已經(jīng)泄漏了,就像前面說的,如果發(fā)生GC并且對象是可以被回收的,那么就會加入引用隊列, 最后到這一步說明該對象按理來說聲明周期是已經(jīng)結束了的,但是通過前面的GC卻不能回收,說明已經(jīng)造成了內(nèi)存泄漏,那么解析hprof文件,得到該對象的引用鏈,也就是要觸發(fā)堆轉儲。

在前面說很多檢測GC回收是怎么做到的呢,接下來看看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.
    //WeakReferences會在它們指向的對象變得無法訪問時排隊。 這是在完成或垃圾收集實際發(fā)生之前。
    //隊列不為null,說明該對象被收了,加入此隊列
    KeyedWeakReference ref;
    while ((ref = (KeyedWeakReference) queue.poll()) != null) {
        retainedKeys.remove(ref.key);
    }
}

這里直接使用一個while循環(huán)從隊列取出元素進行判斷,這里的queue.poll()是不會阻塞的,所以為什么LeakCanary會做兩次驗證的原因,為什么LeakCanary不使用queue.remove()方法呢?你想想queue.remove()方法是阻塞當前線程,從前面知道每次Activity或者Fragment銷毀回調(diào)生命周期方法都會創(chuàng)建一個KeyedWeakReference實例,也就是說如果不泄露就一直阻塞當前線程,這樣反而對造成不必要的開銷,我也是猜猜而已。

6、總結

  • LeakCanary是通過WeakReference+Reference機制檢測對象是否能被回收;
  • LeakCanary檢測的時機是當某組件的生命周期已經(jīng)結束,才會觸發(fā)檢測;

參考鏈接:http://m.itdecent.cn/p/fa9d4eae7f05

所以說LeakCanary針對Activity/Fragment的內(nèi)存泄漏檢測非常好用,但是對于以上檢測不到的情況,還得配合Android Monitor + MAT

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容