Android工具:LeakCanary—內存泄露檢測神器

一、LeakCanary簡介
LeakCanary是Square公司開源的一個檢測內存的泄露的函數庫,可以方便地和你的項目進行集成,在Debug版本中監控Activity、Fragment等的內存泄露;
LeakCanary集成到項目中之後,在檢測到內存泄露時,會發送消息到系統通知欄。點擊後打開名稱DisplayLeakActivity的頁面,並顯示泄露的跟蹤信息,Logcat上面也會有對應的日誌輸出。同時如果跟蹤信息不足以定位時,DisplayLeakActivity還爲開發者默認保存了最近7個dump文件到App的目錄中,可以使用MAT等工具對dump文件進行進一步的分析;
二、內存泄漏簡介
在瞭解了LeakCanary的接入方式後,我們肯定着急想見識見識LeakCanary的威力。在跟大家演示LeakCanary檢測和處理構成之前,大家應該明應該對內存泄露有基本的瞭解和認識;
1.爲什麼會產生內存泄漏?
當一個對象不需要使用本該回收時,有另外一個正在使用的對象持有它的引用,從而導致它不能回收停留在堆內存中,這就產生了內存泄漏;
2.內存泄露對程序產生的影響?
內存泄漏是造成應用程序OOM的主要原因之一。Android系統爲每個應用程序分配有限的內存,當應用中內存泄漏較多時,就難免會導致應用所需要的內存超出系統分配限額,從而導致OOM應用Crash;
3.Android常見的內存泄露?
相信內存泄露對大家都早有耳聞,但是它不像一些Java異常情況,會立即造成程序的Crash,卻有讓大家比較“陌生”。下面我們就列舉出日常開發中常見的內存泄露類型,讓大家對內存泄露的認識不僅僅停留在“有所耳聞 ”的層面;
 單例造成:由於單例靜態特性使得單例的生命週期和應用的生命週期一樣長,如果一個對象(如Context)已經不使用了,而單例對象還持有對象的引用造成這個對象不能正常被回收;
 非靜態內部類創建靜態實例造成:在Acitivity內存創建一個非靜態內部類單例,避免每次啓動資源重新創建。但是因爲非靜態內部類默認持有外部類(Activity)的引用,並且使用該類創建靜態實例。造成該實例和應用生命週期一樣長,導致靜態實例持有引用的Activity和資源不能正常回收;
 Handler造成:子線程執行網絡任務,使用Handler處理子線程發送消息。由於handler對象是非靜態匿名內部類的對象,持有外部類(Activity)的引用。在Handler-Message中Looper線程不斷輪詢處理消息,當Activity退出還有未處理或者正在處理的消息時,消息隊列中的消息持有handler對象引用,handler又持有Activity,導致Activity的內存和資源不能及時回收;
 線程造成:匿名內部類Runnalbe和AsyncTask對象執行異步任務,對當前Activity隱式引用。當Activity銷燬之前,任務還沒有執行完,將導致Activity的內存和資源不能及時回收;
 資源未關閉造成的內存泄露:對於使用了BroadcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等資源的使用,應該在Activity銷燬時及時關閉或者註銷,否則這些資源將不會被回收,造成內存泄露;
三、LeakCanary接入
下面我們還是以QProject項目進行演示如何在項目中接入LeakCanary,項目目錄如下:

1.添加LeakCanary依賴
在主項目main模塊的build.gradle文件中添加LeakCanary相關依賴;
/main/build.gradle文件
apply plugin: 'com.android.application'
android {
    ...  ...
}
dependencies {
    ... ...
    //添加leakcanary相關的依賴
    //在release和test版本中,使用的是LeakCanary的no-op版本,也就是沒有實際代碼和操作的Wrapper版本,只包含LeakCanary和RefWatcher類的空實現,這樣不會對生成的APK包體積和應用性能造成影響
    debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5'
    releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
    testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
    ... ...
    compile project(':test')
}
2.初始化LeakCanary
在主項目main模塊的QApplication的onCreate()方法中初始化LeakCanary;
/main/src/main/java/com/qproject/main/QApplication.java文件
public class QAplication extends Application{
    @Override
    public void onCreate() {
        super.onCreate();
        ... ...
        //初始化LeakCanary
        if (LeakCanary.isInAnalyzerProcess(this)) {
            return;
        }
        LeakCanary.install(this);
    }
}
OK,到這裏我們就完成了一個項目的LeankCanary的簡單接入;
提示1:集成LeakCanary後,構建和安裝apk的時候報錯如下:
Error:Error converting bytecode to dex:
Cause: com.android.dex.DexException: Multiple dex files define Lcom/squareup/leakcanary/LeakCanary;
Error:Execution failed for task ':main:transformClassesWithDexForDebug'.
> com.android.build.api.transform.TransformException: com.android.ide.common.process.ProcessException: java.util.concurrent.ExecutionException: java.lang.UnsupportedOperationException
處理1:添加依賴debugCompile 'com.squareup.haha:haha:2.0.3',修改依賴的版本爲1.4-beta2;
四、LeakCanary檢測
通過對一些常見的內存泄露的學習,我們已經對內存泄露有所見聞了。那麼下面我們通過LeakCanary工具,讓大家感受下它在你的日常開發中的真實存在。我們以常見內存—單例造成的內存泄露爲例進行實踐;
1.單例內存泄露模擬
test/src/main/com/qproject/test/TestManager.java
public class TestManager {
    //單例靜態特性使得單例的生命週期和應用的生命週期一樣長
    private static TestManager instance;
    private Context context;

    /**
     * 傳入的Context的生命週期很重要:
     *   如果傳入的是Application的Context,則生命週期和單例生命週期一樣長;
     *   如果傳入的是Activity的Context,由於該Context和Activity的生命週期一樣長,當Activity退出的時候它的內存不會被回收,因爲單例對象持有它的引用;
     */
    private TestManager(Context context) {
        this.context = context;
    }

    public static TestManager getInstance(Context context) {
        if (instance == null) {
            instance = new TestManager(context);
        }
        return instance;
    }
}
test/src/main/com/qproject/test/leakcanary/LeakCanaryActivity.java
public class LeakCanaryActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_leakcanary);
        //獲取單例對象,退出Activity即可模擬出內存泄露
        TestManager testManager = TestManager.getInstance(this);
    }
}
2.檢測消息通知
運行App到LeakCanaryActivit頁面並退出,在檢測到內存泄露的時候,會發送消息到系統通知欄;
 
3.查看檢測詳情
點擊通知消息,打開名爲DisplayLeakActivity的頁面,並顯示泄漏的跟蹤信息;

4.查看LogCat日誌
除了以上泄漏信息的顯示,Logcat上面也會有對應的日誌輸出;
//內存泄露對象com.qproject.test.leakcanary.LeakCanaryActivity
12-25 07:50:51.710 4941-5795/com.qproject.main D/LeakCanary: * com.qproject.test.leakcanary.LeakCanaryActivity has leaked:
//static com.qproject.test.TestManager.instance的com.qproject.test.TestManager.context引用了回收的內存對象
12-25 07:50:51.710 4941-5795/com.qproject.main D/LeakCanary: * GC ROOT static com.qproject.test.TestManager.instance
12-25 07:50:51.710 4941-5795/com.qproject.main D/LeakCanary: * references com.qproject.test.TestManager.context
//內存泄露對象大小,Reference Key,Device和Android Version等信息
12-25 07:50:51.710 4941-5795/com.qproject.main D/LeakCanary: * leaks com.qproject.test.leakcanary.LeakCanaryActivity instance
12-25 07:50:51.710 4941-5795/com.qproject.main D/LeakCanary: * Retaining: 46 KB.
12-25 07:50:51.710 4941-5795/com.qproject.main D/LeakCanary: * Reference Key: 3d74d294-70dc-4447-a9a2-64e656ea86b8
12-25 07:50:51.710 4941-5795/com.qproject.main D/LeakCanary: * Device: Genymotion Android PREVIEW - Google Nexus 5X - 7.0.0 - API 24 - 1080x1920 vbox86p
12-25 07:50:51.710 4941-5795/com.qproject.main D/LeakCanary: * Android Version: 7.0 API: 24 LeakCanary: 1.5 00f37f5
12-25 07:50:51.710 4941-5795/com.qproject.main D/LeakCanary: * Durations: watch=5038ms, gc=137ms, heap dump=2390ms, analysis=27325ms
//內存泄露對象詳細信息
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: * Details:
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: * Class com.qproject.test.TestManager
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   static instance = com.qproject.test.TestManager@316184816 (0x12d898f0)
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   static $classOverhead = byte[308]@316175745 (0x12d87581)
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: * Instance of com.qproject.test.TestManager
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   static instance = com.qproject.test.TestManager@316184816 (0x12d898f0)
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   static $classOverhead = byte[308]@316175745 (0x12d87581)
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   context = com.qproject.test.leakcanary.LeakCanaryActivity@315059712 (0x12c76e00)
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   shadow$_klass_ = com.qproject.test.TestManager
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   shadow$_monitor_ = 0
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: * Instance of com.qproject.test.leakcanary.LeakCanaryActivity
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   static $classOverhead = byte[2228]@316203009 (0x12d8e001)
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   mDelegate = android.support.v7.app.AppCompatDelegateImplN@315842128 (0x12d35e50)
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   mEatKeyUpEvent = false
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   mResources = null
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   mThemeId = 2131230884
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   mCreated = true
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   mFragments = android.support.v4.app.FragmentController@316183584 (0x12d89420)
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   mHandler = android.support.v4.app.FragmentActivity$1@316163360 (0x12d84520)
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   mNextCandidateRequestIndex = 0
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   mOptionsMenuInvalidated = false
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   mPendingFragmentActivityResults = android.support.v4.util.SparseArrayCompat@316172368 (0x12d86850)
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   mReallyStopped = true
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   mRequestedPermissionsFromFragment = false
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   mResumed = false
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   mRetaining = false
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   mStopped = true
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   mStartedActivityFromFragment = false
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   mStartedIntentSenderFromFragment = false
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   mExtraDataMap = android.support.v4.util.SimpleArrayMap@316171864 (0x12d86658)
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   mActionBar = null
12-25 07:50:51.712 4941-5795/com.qproject.main D/LeakCanary: |   mActionModeTypeStarting = 0
12-25 07:50:51.712 4941-5795/com.qproject.main D/LeakCanary: |   mActivityInfo = android.content.pm.ActivityInfo@315841984 (0x12d35dc0)
12-25 07:50:51.712 4941-5795/com.qproject.main D/LeakCanary: |   mActivityTransitionState = android.app.ActivityTransitionState@316207336 (0x12d8f0e8)
12-25 07:50:51.712 4941-5795/com.qproject.main D/LeakCanary: |   mApplication = com.qproject.main.QAplication@314916416 (0x12c53e40)
12-25 07:50:51.712 4941-5795/com.qproject.main D/LeakCanary: |   mCalled = true
12-25 07:50:51.712 4941-5795/com.qproject.main D/LeakCanary: |   mChangeCanvasToTranslucent = false
12-25 07:50:51.712 4941-5795/com.qproject.main D/LeakCanary: |   mChangingConfigurations = false
12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: |   mComponent = android.content.ComponentName@315998320 (0x12d5c070)
12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: |   mConfigChangeFlags = 0
12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: |   mCurrentConfig = android.content.res.Configuration@316178888 (0x12d881c8)
12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: |   mDecor = null
12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: |   mDefaultKeyMode = 0
12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: |   mDefaultKeySsb = null
12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: |   mDestroyed = true
12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: |   mDoReportFullyDrawn = false
12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: |   mEatKeyUpEvent = false
12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: |   mEmbeddedID = null
12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: |   mEnableDefaultActionBarUp = false
12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: |   mEnterTransitionListener = android.app.SharedElementCallback$1@1887062680 (0x707a4a98)
12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: |   mExitTransitionListener = android.app.SharedElementCallback$1@1887062680 (0x707a4a98)
12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: |   mFinished = true
12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: |   mFragments = android.app.FragmentController@316183536 (0x12d893f0)
12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: |   mHandler = android.os.Handler@316163296 (0x12d844e0)
12-25 07:50:51.714 4941-5795/com.qproject.main D/LeakCanary: |   mHasCurrentPermissionsRequest = false
12-25 07:50:51.714 4941-5795/com.qproject.main D/LeakCanary: |   mIdent = 20356640
12-25 07:50:51.714 4941-5795/com.qproject.main D/LeakCanary: |   mInstanceTracker = android.os.StrictMode$InstanceTracker@316183552 (0x12d89400)
12-25 07:50:51.714 4941-5795/com.qproject.main D/LeakCanary: |   mInstrumentation = android.app.Instrumentation@314950632 (0x12c5c3e8)
12-25 07:50:51.714 4941-5795/com.qproject.main D/LeakCanary: |   mIntent = android.content.Intent@316215416 (0x12d91078)
12-25 07:50:51.714 4941-5795/com.qproject.main D/LeakCanary: |   mLastNonConfigurationInstances = null
12-25 07:50:51.714 4941-5795/com.qproject.main D/LeakCanary: |   mMainThread = android.app.ActivityThread@314966272 (0x12c60100)
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mManagedCursors = java.util.ArrayList@316171816 (0x12d86628)
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mManagedDialogs = null
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mMenuInflater = null
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mParent = null
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mReferrer = java.lang.String@316215864 (0x12d91238)
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mResultCode = 0
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mResultData = null
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mResumed = false
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mSearchEvent = null
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mSearchManager = null
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mStartedActivity = false
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mStopped = true
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mTaskDescription = android.app.ActivityManager$TaskDescription@316163328 (0x12d84500)
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mTemporaryPause = false
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mTitle = java.lang.String@315129824 (0x12c87fe0)
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mTitleColor = 0
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mTitleReady = true
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mToken = android.os.BinderProxy@315867328 (0x12d3c0c0)
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mTranslucentCallback = null
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mUiThread = java.lang.Thread@1959751680 (0x74cf7000)
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mVisibleBehind = false
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mVisibleFromClient = true
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mVisibleFromServer = true
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mVoiceInteractor = null
12-25 07:50:51.716 4941-5795/com.qproject.main D/LeakCanary: |   mWindow = com.android.internal.policy.PhoneWindow@315116864 (0x12c84d40)
12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: |   mWindowAdded = true
12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: |   mWindowManager = android.view.WindowManagerImpl@316172152 (0x12d86778)
12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: |   mInflater = com.android.internal.policy.PhoneLayoutInflater@316010352 (0x12d5ef70)
12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: |   mOverrideConfiguration = null
12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: |   mResources = android.content.res.Resources@316235992 (0x12d960d8)
12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: |   mTheme = android.content.res.Resources$Theme@316183728 (0x12d894b0)
12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: |   mThemeResource = 2131230884
12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: |   mBase = android.app.ContextImpl@316155392 (0x12d82600)
12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: |   shadow$_klass_ = com.qproject.test.leakcanary.LeakCanaryActivity
12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: |   shadow$_monitor_ = 1316364430
12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: * Excluded Refs:
12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: | Field: android.view.Choreographer$FrameDisplayEventReceiver.mMessageQueue (always)
12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: | Thread:FinalizerWatchdogDaemon (always)
12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: | Thread:main (always)
12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: | Thread:LeakCanary-Heap-Dump (always)
12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: | Class:java.lang.ref.WeakReference (always)
12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: | Class:java.lang.ref.SoftReference (always)
12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: | Class:java.lang.ref.PhantomReference (always)
12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: | Class:java.lang.ref.Finalizer (always)
12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: | Class:java.lang.ref.FinalizerReference (always)
5.獲取dump日誌文件
如果覺得跟蹤信息不足以定位時,DisplayLeakActivity還爲開發者默認保存了最近7個dump文件到APP的目錄中,可以使用MAT等工具對dump文件進行進一步的分析;
vbox86p:/data/data/com.qproject.main/files/leakcanary # ls
2016-12-25_07-50-51_718.hprof 2016-12-25_07-50-51_718.hprof.result
D:\>adb pull ./data/data/com.qproject.main/files/leakcanary/2016-12-25_07-50-51_ 718.hprof
[100%] ./data/data/com.qproject.main/f...akcanary/2016-12-25_07-50-51_718.hprof
Android Studio->View->Tool Windows->Captures,打開Captures窗口,將pull獲取的hprof文件剪切到Capture中文件的目錄下,雙擊打開即可;

具體的分析過程,這裏就不重點講述,大家去查詢MAT相關的資料;
五、檢測其他對象
如果想監聽其他的對象(例如Fragment ),可以通過RefWatcher的實例來實現;
 
六、LeakCanary原理
1.RefWatcher.watch()函數會爲被監控的對象創建一個KeyedWeakReference弱引用對象,是WeakReference對的子類,增加了鍵值對信息,後面會根據指定的鍵key找到弱引用對象;
2.在後臺線程AndroidWatchExecutor中,檢查KeyedWeakReference弱引用是否已經被清楚。如果還存在,則觸發一次垃圾回收之後。垃圾回收之後,如果弱引用對象依然存在,說明發生了內存泄露;

新技術,新未來!歡迎大家關注“1024工場”微信服務號,時刻關注我們的最新的技術訊息!(甭客氣!盡情的掃描或者長按!)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章