LeakCanary使用和原理

LeakCanary 是检测内存泄漏的工具

LeakCanary使用

引入包 debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.0' 就可以检测内存泄漏,不用进行初始化。

只在项目运行中出现,如果想要添加检测其他对象或者查看源码
引入包 implementation 'com.squareup.leakcanary:leakcanary-android:2.0'可以观察你想观察的任何对象AppWatcher.INSTANCE.getObjectWatcher().watch();

引发的问题 - 为什么添加 debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.0' application中不用初始化就可以检测内存泄漏 ?是在那里进行初始化的?

LeakCanary初始化的原理

在LeakCannary 中的AndroidManifest.xml 中

 <provider
        android:name="leakcanary.internal.AppWatcherInstaller$LeakCanaryProcess"
        android:authorities="${applicationId}.leakcanary-process.installer"
        android:process=":leakcanary"
        android:exported="false"/>

在 AppWatcherInstaller 类中的onCreate()中调用InternalAppWatcher.install()去初始化LeakCanary。

internal sealed class AppWatcherInstaller : ContentProvider() {

  /**
   * [MainProcess] automatically sets up the LeakCanary code that runs in the main app process.
   */
  internal class MainProcess : AppWatcherInstaller()

  /**
   * When using the `leakcanary-android-process` artifact instead of `leakcanary-android`,
   * [LeakCanaryProcess] automatically sets up the LeakCanary code
   */
  internal class LeakCanaryProcess : AppWatcherInstaller() {
 
	  override fun onCreate(): Boolean {
	    val application = context!!.applicationContext as Application
	     //provider的onCreate方法中,调用了install方法
	    InternalAppWatcher.install(application)
	    return true
	  }
	
	  override fun query(
	    uri: Uri,
	    strings: Array<String>?,
	    s: String?,
	    strings1: Array<String>?,
	    s1: String?
	  ): Cursor? {
	    return null
	  }
	
	  override fun getType(uri: Uri): String? {
	    return null
	  }
	
	  override fun insert(
	    uri: Uri,
	    contentValues: ContentValues?
	  ): Uri? {
	    return null
	  }
	
	  override fun delete(
	    uri: Uri,
	    s: String?,
	    strings: Array<String>?
	  ): Int {
	    return 0
	  }
	
	  override fun update(
	    uri: Uri,
	    contentValues: ContentValues?,
	    s: String?,
	    strings: Array<String>?
	  ): Int {
	    return 0
	  }
	}
	
	fun install(application: Application) {
	    SharkLog.logger = DefaultCanaryLog()
	    SharkLog.d { "Installing AppWatcher" }
	    checkMainThread()
	    if (this::application.isInitialized) {
	      return
	    }
	    InternalAppWatcher.application = application
	
	    val configProvider = { AppWatcher.config }
	    // 监控activity的内存泄漏
	    ActivityDestroyWatcher.install(application, objectWatcher, configProvider)
	     // 监控Fragment的内存泄漏
	    FragmentDestroyWatcher.install(application, objectWatcher, configProvider)
	    onAppWatcherInstalled(application)
  }

app 在打包的时候,会merge-Mainfest 引入其他的androidMainfest 这样在自己的项目中就会合并成一个AndroidMainfest.xml 文件。

继续探索,ContentProvider是什么时候被创建的(什么时候调用onCreate函数)?
探索源码,在ActivityThread 中有

	private void handleBindApplication(AppBindData data){
	...
            mInitialApplication = app;
            // don't bring up providers in restricted mode; they may depend on the
            // app's custom Application class
            if (!data.restrictedBackupMode) {
                if (!ArrayUtils.isEmpty(data.providers)) {
                    installContentProviders(app, data.providers);
                }
            }
            // Do this after providers, since instrumentation tests generally start their
            // test thread at this point, and we don't want that racing.
            try {
                mInstrumentation.onCreate(data.instrumentationArgs);
            }
            ...
     }
    ContentProviderHolder cph = installProvider(context, null, cpi,
                    false /*noisy*/, true /*noReleaseNeeded*/, true /*stable*/);
 private ContentProviderHolder installProvider(Context context,
            ContentProviderHolder holder, ProviderInfo info,
            boolean noisy, boolean noReleaseNeeded, boolean stable) {
            ...
                if (DEBUG_PROVIDER) Slog.v(
                    TAG, "Instantiating local provider " + info.name);
                // XXX Need to create the correct context for this provider.
                localProvider.attachInfo(c, info);
            } catch (java.lang.Exception e) {
                if (!mInstrumentation.onException(null, e)) {
                    throw new RuntimeException(
                            "Unable to get provider " + info.name
                            + ": " + e.toString(), e);
                }
                return null;
            }
        } 
        ...
    }
private void attachInfo(Context context, ProviderInfo info, boolean testing) {
        mNoPerms = testing;

        /*
         * Only allow it to be set once, so after the content service gives
         * this to us clients can't change it.
         */
        if (mContext == null) {
            mContext = context;
            if (context != null) {
                mTransport.mAppOpsManager = (AppOpsManager) context.getSystemService(
                        Context.APP_OPS_SERVICE);
            }
            mMyUid = Process.myUid();
            if (info != null) {
                setReadPermission(info.readPermission);
                setWritePermission(info.writePermission);
                setPathPermissions(info.pathPermissions);
                mExported = info.exported;
                mSingleUser = (info.flags & ProviderInfo.FLAG_SINGLE_USER) != 0;
                setAuthorities(info.authority);
            }
            //调用 ContentProvider 的onCreate
            ContentProvider.this.onCreate();
        }

得到的结论:
LeakCanary在2.0以后的版本中不需要在application完成初始化任务,LeakCanary2.0以后利用了ContentProvider 在 Application 被创建之前被加载的原理,在ContentProvider的onCreate完成了初始化任务。

LeakCanary2.0 用kotlin 写的
LeakCanary 主要有两个配置文件 LeakCanary 和 AppWatcher

/**
 * The entry point API for LeakCanary. LeakCanary builds on top of [AppWatcher]. AppWatcher
 * notifies LeakCanary of retained instances, which in turns dumps the heap, analyses it and
 * publishes the results.
 *
 * LeakCanary can be configured by updating [config].
 */
object LeakCanary {

  /**
  *  通过Config 设置LeakCanary的一些配置
  */
  data class Config(
    /**
    * 是否堆转储 默认为true
    *
    */
    val dumpHeap: Boolean = true,
    /**
     * If [dumpHeapWhenDebugging] is false then LeakCanary will not dump the heap
     * when the debugger is attached. The debugger can create temporary memory leaks (for instance
     * if a thread is blocked on a breakpoint).
     *
     * Defaults to false.
     */
    val dumpHeapWhenDebugging: Boolean = false,
    /**
     * When the app is visible, LeakCanary will wait for at least
     * [retainedVisibleThreshold] retained instances before dumping the heap. Dumping the heap
     * freezes the UI and can be frustrating for developers who are trying to work. This is
     * especially frustrating as the Android Framework has a number of leaks that cannot easily
     * be fixed.
     *
     * When the app becomes invisible, LeakCanary dumps the heap after
     * [AppWatcher.Config.watchDurationMillis] ms.
     *
     * The app is considered visible if it has at least one activity in started state.
     *
     * A higher threshold means LeakCanary will dump the heap less often, therefore it won't be
     * bothering developers as much but it could miss some leaks.
     *
     * Defaults to 5.
     */
    val retainedVisibleThreshold: Int = 5,

     /**
    * 观察的对象list 也可以自定义观察的对象
    *
    */
    val referenceMatchers: List<ReferenceMatcher> = AndroidReferenceMatchers.appDefaults,

    /**
     * List of [ObjectInspector] that provide LeakCanary with insights about objects found in the
     * heap. You can create your own [ObjectInspector] implementations, and also add
     * a [shark.AppSingletonInspector] instance created with the list of internal singletons.
     *
     * Defaults to [AndroidObjectInspectors.appDefaults]
     */
    val objectInspectors: List<ObjectInspector> = AndroidObjectInspectors.appDefaults,

    /**
     * Called on a background thread when the heap analysis is complete.
     * If you want leaks to be added to the activity that lists leaks, make sure to delegate
     * calls to a [DefaultOnHeapAnalyzedListener].
     *
     * Defaults to [DefaultOnHeapAnalyzedListener]
     */
    val onHeapAnalyzedListener: OnHeapAnalyzedListener = DefaultOnHeapAnalyzedListener.create(),

    /**
     * Extracts metadata from a hprof to be reported in [HeapAnalysisSuccess.metadata].
     * Called on a background thread during heap analysis.
     *
     * Defaults to [AndroidMetadataExtractor]
     */
    val metatadaExtractor: MetadataExtractor = AndroidMetadataExtractor,

    /**
     * Whether to compute the retained heap size, which is the total number of bytes in memory that
     * would be reclaimed if the detected leaks didn't happen. This includes native memory
     * associated to Java objects (e.g. Android bitmaps).
     *
     * Computing the retained heap size can slow down the analysis because it requires navigating
     * from GC roots through the entire object graph, whereas [shark.HeapAnalyzer] would otherwise
     * stop as soon as all leaking instances are found.
     *
     * Defaults to true.
     */
    val computeRetainedHeapSize: Boolean = true,

    /**
     * How many heap dumps are kept on the Android device for this app package. When this threshold
     * is reached LeakCanary deletes the older heap dumps. As several heap dumps may be enqueued
     * you should avoid going down to 1 or 2.
     *
     * Defaults to 7.
     */
    val maxStoredHeapDumps: Int = 7,

    /**
     * LeakCanary always attempts to store heap dumps on the external storage if the
     * WRITE_EXTERNAL_STORAGE is already granted, and otherwise uses the app storage.
     * If the WRITE_EXTERNAL_STORAGE permission is not granted and
     * [requestWriteExternalStoragePermission] is true, then LeakCanary will display a notification
     * to ask for that permission.
     *
     * Defaults to false because that permission notification can be annoying.
     */
    val requestWriteExternalStoragePermission: Boolean = false,

    /**
     * When true, [objectInspectors] are used to find leaks instead of only checking instances
     * tracked by [KeyedWeakReference]. This leads to finding more leaks and shorter leak traces.
     * However this also means the same leaking instances will be found in every heap dump for a
     * given process life.
     *
     * Defaults to false.
     */
    val useExperimentalLeakFinders: Boolean = false
  )

  /**
   * The current LeakCanary configuration. Can be updated at any time, usually by replacing it with
   * a mutated copy, e.g.:
   *
   * ```
   * LeakCanary.config = LeakCanary.config.copy(computeRetainedHeapSize = true)
   * ```
   */
  @Volatile
  var config: Config = if (AppWatcher.isInstalled) Config() else InternalLeakCanary.noInstallConfig
    set(newConfig) {
      val previousConfig = field
      field = newConfig
      logConfigChange(previousConfig, newConfig)
    }

  private fun logConfigChange(
    previousConfig: Config,
    newConfig: Config
  ) {
    SharkLog.d {
      val changedFields = mutableListOf<String>()
      Config::class.java.declaredFields.forEach { field ->
        field.isAccessible = true
        val previousValue = field[previousConfig]
        val newValue = field[newConfig]
        if (previousValue != newValue) {
          changedFields += "${field.name}=$newValue"
        }
      }
      "Updated LeakCanary.config: Config(${if (changedFields.isNotEmpty())
        changedFields.joinToString(", ") else "no changes"})"
    }
  }

  /**
   * Returns a new [Intent] that can be used to programmatically launch the leak display activity.
   */
  fun newLeakDisplayActivityIntent() = InternalLeakCanary.leakDisplayActivityIntent

  /**
   * Dynamically shows / hides the launcher icon for the leak display activity.
   * Note: you can change the default value by overriding the leak_canary_add_launcher_icon
   * boolean resource:
   *
   * ```
   * <?xml version="1.0" encoding="utf-8"?>
   * <resources>
   *   <bool name="leak_canary_add_launcher_icon">false</bool>
   * </resources>
   * ```
   */
  fun showLeakDisplayActivityLauncherIcon(showLauncherIcon: Boolean) {
    InternalLeakCanary.setEnabledBlocking(
        "leakcanary.internal.activity.LeakLauncherActivity", showLauncherIcon
    )
  }

  /**
   * Immediately triggers a heap dump and analysis, if there is at least one retained instance
   * tracked by [AppWatcher.objectWatcher]. If there are no retained instances then the heap will not
   * be dumped and a notification will be shown instead.
   */
  fun dumpHeap() = InternalLeakCanary.onDumpHeapReceived(forceDump = true)
}

AppWatcher 也有同样的配置文件

object AppWatcher {

  data class Config(
    /**
     * Whether AppWatcher should watch objects (by keeping weak references to them).
     *
     * Default to true in debuggable builds and false is non debuggable builds.
     */
    val enabled: Boolean = InternalAppWatcher.isDebuggableBuild,

    /**
     * Whether AppWatcher should automatically watch destroyed activity instances.
     *
     * Defaults to true.
     */
    val watchActivities: Boolean = true,

    /**
     * Whether AppWatcher should automatically watch destroyed fragment instances.
     *
     * Defaults to true.
     */
    val watchFragments: Boolean = true,

    /**
     * Whether AppWatcher should automatically watch destroyed fragment view instances.
     *
     * Defaults to true.
     */
    val watchFragmentViews: Boolean = true,

    /**
     * How long to wait before reporting a watched object as retained.
     *
     * Default to 5 seconds.
     */
    val watchDurationMillis: Long = TimeUnit.SECONDS.toMillis(5)
  )

  /**
   * The current AppWatcher configuration. Can be updated at any time, usually by replacing it with
   * a mutated copy, e.g.:
   *
   * ```
   * LeakCanary.config = LeakCanary.config.copy(enabled = false)
   * ```
   */
  @Volatile
  var config: Config = if (isInstalled) Config() else Config(enabled = false)
    set(newConfig) {
      val previousConfig = field
      field = newConfig
      logConfigChange(previousConfig, newConfig)
    }

  private fun logConfigChange(
    previousConfig: Config,
    newConfig: Config
  ) {
    SharkLog.d {
      val changedFields = mutableListOf<String>()
      Config::class.java.declaredFields.forEach { field ->
        field.isAccessible = true
        val previousValue = field[previousConfig]
        val newValue = field[newConfig]
        if (previousValue != newValue) {
          changedFields += "${field.name}=$newValue"
        }
      }
      "Updated AppWatcher.config: Config(${if (changedFields.isNotEmpty())
        changedFields.joinToString(", ") else "no changes"})"
    }
  }

  /**
   * The [ObjectWatcher] used by AppWatcher to detect retained objects.
   */
  val objectWatcher
    get() = InternalAppWatcher.objectWatcher

  /** @see [manualInstall] */
  val isInstalled
    get() = InternalAppWatcher.isInstalled

  /**
   * [AppWatcher] is automatically installed on main process start by
   * [leakcanary.internal.AppWatcherInstaller] which is registered in the AndroidManifest.xml of
   * your app. If you disabled [leakcanary.internal.AppWatcherInstaller] or you need AppWatcher
   * or LeakCanary to run outside of the main process then you can call this method to install
   * [AppWatcher].
   */
  fun manualInstall(application: Application) = InternalAppWatcher.install(application)

}

LeakCanary检测内存泄漏原理

原理

是利用了WeakRefrence + RefrenceQueue的机制(仅被弱引用持有的对象,当对象被回收时,会存入到引用队列中),从引用队列中不断的获取对象,将已确认被GC的对象剔除,剩余未被回收的对象则定义为可能泄露的对象,当达到一定的判断条件时,通知用户内存泄露

监控activity:

在LeakCanary初始化的时候,主要通过在Application中注册registerActivityLifecycleCallbacks绑定Activity生命周期回调,并在Activity destory时,调用ObjectWatcher的watch方法进行检测

internal class ActivityDestroyWatcher private constructor(
  private val objectWatcher: ObjectWatcher,
  private val configProvider: () -> Config
) {

  //activity destory 的时候,对Activity watch
  private val lifecycleCallbacks =
    object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
      override fun onActivityDestroyed(activity: Activity) {
        if (configProvider().watchActivities) {
          objectWatcher.watch(
              activity, "${activity::class.java.name} received Activity#onDestroy() callback"
          )
        }
      }
    }

  companion object {
    fun install(
      application: Application,
      objectWatcher: ObjectWatcher,
      configProvider: () -> Config
    ) {
      val activityDestroyWatcher =ActivityDestroyWatcher(objectWatcher, configProvider)
      //在application中注册 ActivityLifecycleCallbacks  
       application.registerActivityLifecycleCallbacks(activityDestroyWatcher.lifecycleCallbacks)
    }
  }
}

监控Fragment:

监控AndroidSupportFragmentDestroyWatcher类中,主要通过在Activity中注册FragmentLifecycleCallbacks回调,当触发onFragmentViewDestroyed回调时,通过ObjectWatcher#watch检查fragment.mView对象(即Fragment#onCreateView创建的View)。当触发onFragmentDestroyed回调时,检查fragment对象是否泄漏。

internal class AndroidSupportFragmentDestroyWatcher(
  private val objectWatcher: ObjectWatcher,
  private val configProvider: () -> Config
) : (Activity) -> Unit {

  private val fragmentLifecycleCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() {

    override fun onFragmentViewDestroyed(
      fm: FragmentManager,
      fragment: Fragment
    ) {
      val view = fragment.view
      if (view != null && configProvider().watchFragmentViews) {
        objectWatcher.watch(
            view, "${fragment::class.java.name} received Fragment#onDestroyView() callback " +
            "(references to its views should be cleared to prevent leaks)"
        )
      }
    }

    override fun onFragmentDestroyed(
      fm: FragmentManager,
      fragment: Fragment
    ) {
      if (configProvider().watchFragments) {
        objectWatcher.watch(
            fragment, "${fragment::class.java.name} received Fragment#onDestroy() callback"
        )
      }
    }
  }

  override fun invoke(activity: Activity) {
    if (activity is FragmentActivity) {
      val supportFragmentManager = activity.supportFragmentManager
      supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true)
    }
  }
}

核心代码类 ObjectWatcher

class ObjectWatcher constructor(
  private val clock: Clock,
  private val checkRetainedExecutor: Executor,
  /**
   * References passed to [watch].
   * 正在被观察的对象,此时还未泄漏
   */
  private val watchedObjects = mutableMapOf<String, KeyedWeakReference>()
  //引用队列 配合弱引用 定位泄漏的对象。
  private val queue = ReferenceQueue<Any>()
  //泄漏监听
  private val onObjectRetainedListeners = mutableSetOf<OnObjectRetainedListener>()

  /**
   * Watches the provided [watchedObject].
   *
   * @param description Describes why the object is watched.
   */
  @Synchronized fun watch(watchedObject: Any,description: String) {
    if (!isEnabled()) {
      return
    }
      
    //清除watchedObjects中不存在泄露的弱引用对象  
    removeWeaklyReachableObjects()
    val key = UUID.randomUUID().toString()
    val watchUptimeMillis = clock.uptimeMillis()
    val reference =KeyedWeakReference(watchedObject, key, description, watchUptimeMillis, queue)
    SharkLog.d {
      "Watching " +
          (if (watchedObject is Class<*>) watchedObject.toString() else "instance of ${watchedObject.javaClass.name}") +
          (if (description.isNotEmpty()) " ($description)" else "") +
          " with key $key"
    }

    //将本次watch的对象加入到watchedObjects 的map中
    watchedObjects[key] = reference
      
    //通过checkRetainedExecutor,5秒后执行moveToRetained
    // 5s 后进行
    checkRetainedExecutor.execute {
      //若依旧存在引用,则通知onObjectRetainedListeners
      moveToRetained(key)
    }
  }

  
 
  @Synchronized private fun moveToRetained(key: String) {
      //清除无需观察的对象
    removeWeaklyReachableObjects()
     //通过key 去找引用,若存在引用,这证明5s后,本该回收的对象未能回收,可能存在内存泄露
    val retainedRef = watchedObjects[key]
    if (retainedRef != null) {
      retainedRef.retainedUptimeMillis = clock.uptimeMillis()
      onObjectRetainedListeners.forEach { it.onObjectRetained() }
    }
  }

   
  private fun removeWeaklyReachableObjects() {
    // 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.
      
    //queue是一个 ReferenceQueue队列,当一个对象被GC掉之后,会被加入到这个队列当中,即queue中存在的对象,都可以认为是不会存在内存泄露的对象
    var ref: KeyedWeakReference?
    do {
      ref = queue.poll() as KeyedWeakReference?
      if (ref != null) {
        watchedObjects.remove(ref.key)
      }
    } while (ref != null)
  }
}

将要观察的对象放入 watchedObjects 这个map中( key值是生成的UID)

当对象销毁的时候,等待5秒(GC不是及时的,预留时间为5s,等待GC。MVP模式中,P等待网络请求,P持有Activity的引用,Activity销毁后,不能释放。可以手动配置这个时间为10s,10s后P回收,Activity回收,这时可认为长时间内不存在内存泄漏),去ReferenceQueue 中遍历,如果对象被回收,queue队列会存入这个引用,得到这个引用,从watchedObjects 移除。

在Activity 不可见的时候,会判断watchedObjects 保留的对象 如果大于0,会手动GC
在手动gc后发现
(1)如果不存在内存泄漏的对象,则不进行heap dump
(2)如果存在内存泄漏的对象个数小于 val retainedVisibleThreshold: Int = 5,,则不进行heap dump( config 配置 可以设置retainedVisibleThreshold)
就会dumpHeap 然后利用Shark(2.0以前是HAHA进行分析, 分析最短引用路径,进行通知。
onObjectRetainedListeners.forEach { it.onObjectRetained() } 最终会调用HeapDumpTrigger#checkRetainedObjects

 private fun checkRetainedObjects(reason: String) {
    val config = configProvider()
    // A tick will be rescheduled when this is turned back on.
    if (!config.dumpHeap) {
      SharkLog.d { "Ignoring check for retained objects scheduled because $reason: LeakCanary.Config.dumpHeap is false" }
      return
    }

    var retainedReferenceCount = objectWatcher.retainedObjectCount

    if (retainedReferenceCount > 0) {
      // 执行一次GC
      gcTrigger.runGc()
      retainedReferenceCount = objectWatcher.retainedObjectCount
    }
	// 如果内存泄漏对象数量在阈值内,不生成dump文件分析
    if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return

    if (!config.dumpHeapWhenDebugging && DebuggerControl.isDebuggerAttached) {
      onRetainInstanceListener.onEvent(DebuggerIsAttached)
      showRetainedCountNotification(
          objectCount = retainedReferenceCount,
          contentText = application.getString(
              R.string.leak_canary_notification_retained_debugger_attached
          )
      )
      scheduleRetainedObjectCheck(
          reason = "debugger is attached",
          rescheduling = true,
          delayMillis = WAIT_FOR_DEBUG_MILLIS
      )
      return
    }

    val now = SystemClock.uptimeMillis()
    val elapsedSinceLastDumpMillis = now - lastHeapDumpUptimeMillis
    if (elapsedSinceLastDumpMillis < WAIT_BETWEEN_HEAP_DUMPS_MILLIS) {
      onRetainInstanceListener.onEvent(DumpHappenedRecently)
      showRetainedCountNotification(
          objectCount = retainedReferenceCount,
          contentText = application.getString(R.string.leak_canary_notification_retained_dump_wait)
      )
      scheduleRetainedObjectCheck(
          reason = "previous heap dump was ${elapsedSinceLastDumpMillis}ms ago (< ${WAIT_BETWEEN_HEAP_DUMPS_MILLIS}ms)",
          rescheduling = true,
          delayMillis = WAIT_BETWEEN_HEAP_DUMPS_MILLIS - elapsedSinceLastDumpMillis
      )
      return
    }

    SharkLog.d { "Check for retained objects found $retainedReferenceCount objects, dumping the heap" }
    dismissRetainedCountNotification()
    // 创建dump文件,创建通知提示dump
    dumpHeap(retainedReferenceCount, retry = true)
  }

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值