关于ANR,OOM等问题分析流程的介绍

关于ARN和OOM的一些问题的分析流程,我主要是对博客进行了一个整理,解决了我的问题,特此记录学习。

首先对于底层的ANR(application not responding)和OOM(out of memory)的涉及到的一些底层Dalvik虚拟机知识。

参照这篇文章:http://blog.csdn.net/zanelove/article/details/44809565  这篇文章需要有一点数据结构基础和计算机组成原理基础才能很好的理解。

上面文章所未详解的一个ANR也就是Service造成的ANR,一句话概括,Service其实是运行在主线程中的,万变不离其宗,如果在Service中处理耗时的逻辑,也就是主线程中

进行耗时处理,就会导致程 序ANR,对于Service造成的ANR参照这篇文章 http://blog.csdn.net/guolin_blog/article/details/9797169 。

ANR可以理解为系统的一些限制,出现ANR提示,可以说是系统限制问题,但是归根到底还是因为自己的代码有问题被系统限制了。

OOM则是自己代码的问题,违反了Android的代码规范,最后造成系统为App分配的资源用尽问题,这是体现在开发者代码中的问题,在我们日常的开发中最容易犯得内存泄漏

问题大概有这几种(handler默认持有activity的应用,集合对象没有及时释放,静态内部类默认持有外部类的引用,静态变量持有activity的引用,线程未关闭,单例类持有

activity的引用,Bitmap没有调用recycle,或者文件流操作未关闭)等等,总结一下就是该释放的资源,GC无法释放,只会一味的去申请新的内存,因为Dalvik是有自己GC(垃

圾回收)的,在内存不足是,可以释放的资源应该得到释放,去满足正在申请新内存的资源,这是一个良性循环,所以高质量的代码基本不会出现内存溢出这种问题,我们在平

时的代码中一定要注意代码规范,这样才能很好的去维护一个项目关于内存泄漏的以下内容参考了以下博客

http://blog.csdn.net/a394268045/article/details/51735456#t1

Android中常见的内存泄漏汇总

集合类泄漏

集合类如果仅仅有添加元素的方法,而没有相应的删除机制,导致内存被占用。如果这个集合类是全局性的变量 (比如类中的静态属性,全局性的 map 等即有静态引用或 final 一直指向它),那么没有相应的删除机制,很可能导致集合所占用的内存只增不减。比如上面的典型例子就是其中一种情况,当然实际上我们在项目中肯定不会写这么 2B 的代码,但稍不注意还是很容易出现这种情况,比如我们都喜欢通过 HashMap 做一些缓存之类的事,这种情况就要多留一些心眼。

单例造成的内存泄漏
由于单例的静态特性使得其生命周期跟应用的生命周期一样长,所以如果使用不恰当的话,很容易造成内存泄漏。比如下面一个典型的例子,

public class AppManager {
    private static AppManager instance;
    private Context context;
    private AppManager(Context context) {
    this.context = context;
    }
    public static AppManager getInstance(Context context) {
    if (instance != null) {
        instance = new AppManager(context);
    }
    return instance;
    }
}

这是一个普通的单例模式,当创建这个单例的时候,由于需要传入一个Context,所以这个Context的生命周期的长短至关重要:

1、如果此时传入的是 Application 的 Context,因为 Application 的生命周期就是整个应用的生命周期,所以这将没有任何问题。
2、如果此时传入的是 Activity 的 Context,当这个 Context 所对应的 Activity 退出时,由于该 Context 的引用被单例对象所持有,其生命周期等于整个应用程序的生命周期,所以当前 Activity 退出时它的内存并不会被回收,这就造成泄漏了。

正确的方式应该改为下面这种方式:
public class AppManager {
    private static AppManager instance;
    private Context context;
    private AppManager(Context context) {
    this.context = context.getApplicationContext();// 使用Application 的context
    }
    public static AppManager getInstance(Context context) {
    if (instance != null) {
        instance = new AppManager(context);
    }
    return instance;
    }
}

或者这样写,连 Context 都不用传进来了:
在你的 Application 中添加一个静态方法,getContext() 返回 Application 的 context,
context = getApplicationContext();
   /**
     * 获取全局的context
     * @return 返回全局context对象
     */
    public static Context getContext(){
        return context;
    }
public class AppManager {
    private static AppManager instance;
    private Context context;
    private AppManager() {
    this.context = MyApplication.getContext();// 使用Application 的context
    }
    public static AppManager getInstance() {
    if (instance != null) {
        instance = new AppManager();
    }
    return instance;
    }
}

匿名内部类/非静态内部类和异步线程 
非静态内部类创建静态实例造成的内存泄漏 
有的时候我们可能会在启动频繁的Activity中,为了避免重复创建相同的数据资源,可能会出现这种写法:
public class MainActivity extends AppCompatActivity { 
private static TestResource mResource = null; 
@Override 
protected void onCreate(Bundle savedInstanceState) { 
super.onCreate(savedInstanceState); 
setContentView(R.layout.activity_main); 
if(mManager == null){ 
mManager = new TestResource(); 

//… 

class TestResource { 
//… 

}
这样就在Activity内部创建了一个非静态内部类的单例,每次启动Activity时都会使用该单例的数据,这样虽然避免了资源的重复创建,不过这种写法却会造成内存泄漏,因为非静态内部类默认会持有外部类的引用,而该非静态内部类又创建了一个静态的实例,该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Activity的引用,导致Activity的内存资源不能正常回收。正确的做法为:
将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Context,请按照上面推荐的使用Application 的 Context。当然,Application 的 context 不是万能的,所以也不能随便乱用,对于有些地方则必须使用 Activity 的 Context,对于Application,Service,Activity三者的Context的应用场景如下:
其中: NO1表示 Application 和 Service 可以启动一个 Activity,不过需要创建一个新的 task 任务队列。而对于 Dialog 而言,只有在 Activity 中才能创建


匿名内部类 
android开发经常会继承实现Activity/Fragment/View,此时如果你使用了匿名类,并被异步线程持有了,那要小心了,如果没有任何措施这样一定会导致泄露 
public class MainActivity extends Activity { 
… 
Runnable ref1 = new MyRunable(); 
Runnable ref2 = new Runnable() { 
@Override 
public void run() { 

}; 
… 
}
ref1和ref2的区别是,ref2使用了匿名内部类。我们来看看运行时这两个引用的内存:可以看到,ref1没什么特别的。但ref2这个匿名类的实现对象里面多了一个引用:

this$0这个引用指向MainActivity.this,也就是说当前的MainActivity实例会被ref2持有,如果将这个引用再传入一个异步线程,此线程和此Acitivity生命周期不一致的时候,就造成了Activity的泄露。

Handler 造成的内存泄漏

Handler 的使用造成的内存泄漏问题应该说是最为常见了,很多时候我们为了避免 ANR 而不在主线程进行耗时操作,在处理网络任务或者封装一些请求回调等api都借助Handler来处理,但 Handler 不是万能的,对于 Handler 的使用代码编写一不规范即有可能造成内存泄漏。另外,我们知道 Handler、Message 和 MessageQueue 都是相互关联在一起的,万一 Handler 发送的 Message 尚未被处理,则该 Message 及发送它的 Handler 对象将被线程 MessageQueue 一直持有。 
由于 Handler 属于 TLS(Thread Local Storage) 变量, 生命周期和 Activity 是不一致的。因此这种实现方式一般很难保证跟 View 或者 Activity 的生命周期保持一致,故很容易导致无法正确释放。 
举个例子:
public class SampleActivity extends Activity {
    private final Handler mLeakyHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
      // ...
    }
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // Post a message and delay its execution for 10 minutes.
    mLeakyHandler.postDelayed(new Runnable() {
      @Override
      public void run() { /* ... */ }
    }, 1000 * 60 * 10);
    // Go back to the previous Activity.
    finish();
    }
}
在该 SampleActivity 中声明了一个延迟10分钟执行的消息 Message,mLeakyHandler 将其 push 进了消息队列 MessageQueue 里。当该 Activity 被 finish() 掉时,延迟执行任务的 Message 还会继续存在于主线程中,它持有该 Activity 的 Handler 引用,所以此时 finish() 掉的 Activity 就不会被回收了从而造成内存泄漏(因 Handler 为非静态内部类,它会持有外部类的引用,在这里就是指 SampleActivity)。

修复方法:在 Activity 中避免使用非静态内部类,比如上面我们将 Handler 声明为静态的,则其存活期跟 Activity 的生命周期就无关了。同时通过弱引用的方式引入 Activity,避免直接将 Activity 作为 context 传进去,见下面代码:

public class SampleActivity extends Activity {
      /**
       * Instances of static inner classes do not hold an implicit
       * reference to their outer class.
       */
      private static class MyHandler extends Handler {
        private final WeakReference<SampleActivity> mActivity;
        public MyHandler(SampleActivity activity) {
          mActivity = new WeakReference<SampleActivity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
          SampleActivity activity = mActivity.get();
          if (activity != null) {
            // ...
          }
        }
      }

      private final MyHandler mHandler = new MyHandler(this);
      /**
       * Instances of anonymous classes do not hold an implicit
       * reference to their outer class when they are "static".
       */
      private static final Runnable sRunnable = new Runnable() {
          @Override
          public void run() { /* ... */ }
      };
      @Override
      protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // Post a message and delay its execution for 10 minutes.
        mHandler.postDelayed(sRunnable, 1000 * 60 * 10);
        // Go back to the previous Activity.
        finish();
      }
}

综述,即推荐使用静态内部类 + WeakReference 这种方式。每次使用前注意判空。
前面提到了 WeakReference,所以这里就简单的说一下 Java 对象的几种引用类型。
Java对引用的分类有 Strong reference, SoftReference, WeakReference, PhatomReference 四种。
在Android应用的开发中,为了防止内存溢出,在处理一些占用内存大而且声明周期较长的对象时候,可以尽量应用软引用和弱引用技术。
软/弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。利用这个队列可以得知被回收的软/弱引用的对象列表,从而为缓冲器清除已失效的软/弱引用。

假设我们的应用会用到大量的默认图片,比如应用中有默认的头像,默认游戏图标等等,这些图片很多地方会用到。如果每次都去读取图片,由于读取文件需要硬件操作,速度较慢,会导致性能较低。所以我们考虑将图片缓存起来,需要的时候直接从内存中读取。但是,由于图片占用内存空间比较大,缓存很多图片需要很多的内存,就可能比较容易发生OutOfMemory异常。这时,我们可以考虑使用软/弱引用技术来避免这个问题发生。以下就是高速缓冲器的雏形:
首先定义一个HashMap,保存软引用对象。private Map

尽量避免使用 static 成员变量
如果成员变量被声明为 static,那我们都知道其生命周期将与整个app进程生命周期一样。 
这会导致一系列问题,如果你的app进程设计上是长驻内存的,那即使app切到后台,这部分内存也不会被释放。按照现在手机app内存管理机制,占内存较大的后台进程将优先回收,yi’wei如果此app做过进程互保保活,那会造成app在后台频繁重启。当手机安装了你参与开发的app以后一夜时间手机被消耗空了电量、流量,你的app不得不被用户卸载或者静默。 
这里修复的方法是: 
不要在类初始时初始化静态成员。可以考虑lazy初始化。架构设计上要思考是否真的有必要这样做,尽量避免。如果架构需要这么设计,那么此对象的生命周期你有责任管理起来。

避免 override finalize()

1、finalize 方法被执行的时间不确定,不能依赖与它来释放紧缺的资源。时间不确定的原因是: 
虚拟机调用GC的时间不确定 
Finalize daemon线程被调度到的时间不确定 
2、finalize 方法只会被执行一次,即使对象被复活,如果已经执行过了 finalize 方法,再次被 GC 时也不会再执行了,原因是: 
含有 finalize 方法的 object 是在 new 的时候由虚拟机生成了一个 finalize reference 在来引用到该Object的,而在 finalize 方法执行的时候,该 object 所对应的 finalize Reference 会被释放掉,即使在这个时候把该 object 复活(即用强引用引用住该 object ),再第二次被 GC 的时候由于没有了 finalize reference 与之对应,所以 finalize 方法不会再执行。 
3、含有Finalize方法的object需要至少经过两轮GC才有可能被释放。 
详情见这里 深入分析过dalvik的代码 
资源未关闭造成的内存泄漏 
对于使用了BraodcastReceiver,ContentObserver,File,游标 Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。 
一些不良代码造成的内存压力 
有些代码并不造成内存泄露,但是它们,或是对没使用的内存没进行有效及时的释放,或是没有有效的利用已有的对象而是频繁的申请新内存。 
比如: 
Bitmap 没调用 recycle()方法,对于 Bitmap 对象在不使用时,我们应该先调用 recycle() 释放内存,然后才它设置为 null. 因为加载 Bitmap 对象的内存空间,一部分是 java 的,一部分 C 的(因为 Bitmap 分配的底层是通过 JNI 调用的 )。 而这个 recyle() 就是针对 C 部分的内存释放。 
构造 Adapter 时,没有使用缓存的 convertView ,每次都在创建新的 converView。这里推荐使用 ViewHolder。 

工具分析
Java 内存泄漏的分析工具有很多,但众所周知的要数 MAT(Memory Analysis Tools) 和 YourKit 了。由于篇幅问题,我这里就只对 MAT 的使用做一下介绍。–> MAT 的安装
MAT分析heap的总内存占用大小来初步判断是否存在泄露 
打开 DDMS 工具,在左边 Devices 视图页面选中“Update Heap”图标,然后在右边切换到 Heap 视图,点击 Heap 视图中的“Cause GC”按钮,到此为止需检测的进程就可以被监视。
Heap视图中部有一个Type叫做data object,即数据对象,也就是我们的程序中大量存在的类类型的对象。在data object一行中有一列是“Total Size”,其值就是当前进程中所有Java数据对象的内存总量,一般情况下,这个值的大小决定了是否会有内存泄漏。可以这样判断: 
进入某应用,不断的操作该应用,同时注意观察data object的Total Size值,正常情况下Total Size值都会稳定在一个有限的范围内,也就是说由于程序中的的代码良好,没有造成对象不被垃圾回收的情况。 
所以说虽然我们不断的操作会不断的生成很多对象,而在虚拟机不断的进行GC的过程中,这些对象都被回收了,内存占用量会会落到一个稳定的水平;反之如果代码中存在没有释放对象引用的情况,则data object的Total Size值在每次GC后不会有明显的回落。随着操作次数的增多Total Size的值会越来越大,直到到达一个上限后导致进程被杀掉。 
MAT分析hprof来定位内存泄露的原因所在 
这是出现内存泄露后使用MAT进行问题定位的有效手段。 


A)Dump出内存泄露当时的内存镜像hprof,分析怀疑泄露的类:
B)分析持有此类对象引用的外部对象
C)分析这些持有引用的对象的GC路径
D)逐个分析每个对象的GC路径是否正常

从这个路径可以看出是一个antiRadiationUtil工具类对象持有了MainActivity的引用导致MainActivity无法释放。此时就要进入代码分析此时antiRadiationUtil的引用持有是否合理(如果antiRadiationUtil持有了MainActivity的context导致节目退出后MainActivity无法销毁,那一般都属于内存泄露了)。 
MAT对比操作前后的hprof来定位内存泄露的根因所在 ,为查找内存泄漏,通常需要两个 Dump结果作对比,打开 Navigator History面板,将两个表的 Histogram结果都添加到 Compare Basket中去 

A) 第一个HPROF 文件(usingFile > Open Heap Dump ). 
B)打开Histogram view. 
C)在NavigationHistory view里 (如果看不到就从Window >show view>MAT- Navigation History ), 右击histogram然后选择Add to Compare Basket .
D)打开第二个HPROF 文件然后重做步骤2和3. 
E)切换到Compare Basket view, 然后点击Compare the Results (视图右上角的红色”!”图标)。
F)分析对比结果

可以看出两个hprof的数据对象对比结果。 
通过这种方式可以快速定位到操作前后所持有的对象增量,从而进一步定位出当前操作导致内存泄露的具体原因是泄露了什么数据对象。 
注意: 
如果是用 MAT Eclipse 插件获取的 Dump文件,不需要经过转换则可在MAT中打开,Adt会自动进行转换。 
而手机SDk Dump 出的文件要经过转换才能被 MAT识别,Android SDK提供了这个工具 hprof-conv (位于 sdk/tools下) 
首先,要通过控制台进入到你的 android sdk tools 目录下执行以下命令: 
./hprof-conv xxx-a.hprof xxx-b.hprof 
例如 hprof-conv input.hprof out.hprof 
此时才能将out.hprof放在eclipse的MAT中打开。 
Ok,下面将给大家介绍一个屌炸天的工具 – LeakCanary 。
使用 LeakCanary 检测 Android 的内存泄漏
什么是 LeakCanary 呢?为什么选择它来检测 Android 的内存泄漏呢?
别急,让我来慢慢告诉大家!

LeakCanary 是国外一位大神 Pierre-Yves Ricau 开发的一个用于检测内存泄露的开源类库。一般情况下,在对战内存泄露中,我们都会经过以下几个关键步骤:
1、了解 OutOfMemoryError 情况。 
2、重现问题。 
3、在发生内存泄露的时候,把内存 Dump 出来。 
4、在发生内存泄露的时候,把内存 Dump 出来。 
5、计算这个对象到 GC roots 的最短强引用路径。 
6、确定引用路径中的哪个引用是不该有的,然后修复问题。

很复杂对吧?
如果有一个类库能在发生 OOM 之前把这些事情全部都搞定,然后你只要修复这些问题就好了。LeakCanary 做的就是这件事情。你可以在 debug 包中轻松检测内存泄露。

一起来看这个例子(摘自 LeakCanary 中文使用说明,下面会附上所有的参考文档链接):
class Cat {
}

class Box {
Cat hiddenCat;
}
class Docker {
// 静态变量,将不会被回收,除非加载 Docker 类的 ClassLoader 被回收。
static Box container;
}
// …
Box box = new Box();
// 薛定谔之猫 
Cat schrodingerCat = new Cat(); 
box.hiddenCat = schrodingerCat; 
docker.Container = box;
创建一个RefWatcher,监控对象引用情况。 
// 我们期待薛定谔之猫很快就会消失(或者不消失),我们监控一下 
refWatcher.watch(schrodingerCat);
当发现有内存泄露的时候,你会看到一个很漂亮的 leak trace 报告:
GC ROOT static Docker.container 
references Box.hiddenCat 
leaks Cat instance 
我们知道,你很忙,每天都有一大堆需求。所以我们把这个事情弄得很简单,你只需要添加一行代码就行了。然后 LeakCanary 就会自动侦测 activity 的内存泄露了。
public class ExampleApplication extends Application {
@Override public void onCreate() {
super.onCreate();
LeakCanary.install(this);
}
}
然后你会在通知栏看到这样很漂亮的一个界面:以很直白的方式将内存泄露展现在我们的面前。


Demo
一个非常简单的 LeakCanary demo: 一个非常简单的 LeakCanary demo: https://github.com/liaohuqiu/leakcanary-demo
接入
在 build.gradle 中加入引用,不同的编译使用不同的引用:
dependencies { 
debugCompile ‘com.squareup.leakcanary:leakcanary-android:1.3’ 
releaseCompile ‘com.squareup.leakcanary:leakcanary-android-no-op:1.3’
}
如何使用
使用 RefWatcher 监控那些本该被回收的对象。
RefWatcher refWatcher = {…};
// 监控
refWatcher.watch(schrodingerCat);
LeakCanary.install() 会返回一个预定义的 RefWatcher,同时也会启用一个 ActivityRefWatcher,用于自动监控调用 Activity.onDestroy() 之后泄露的 activity。
在Application中进行配置 :
public class ExampleApplication extends Application {
public static RefWatcher getRefWatcher(Context context) {

ExampleApplication application = (ExampleApplication) context.getApplicationContext();
return application.refWatcher;
}
private RefWatcher refWatcher;
@Override public void onCreate() {
super.onCreate();
refWatcher = LeakCanary.install(this);
}
}
使用 RefWatcher 监控 Fragment:
public abstract class BaseFragment extends Fragment {
@Override public void onDestroy() {
super.onDestroy();
RefWatcher refWatcher = ExampleApplication.getRefWatcher(getActivity());
refWatcher.watch(this);
}
}
使用 RefWatcher 监控 Activity:
public class MainActivity extends AppCompatActivity {
......
@Override
protected void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_main);

        //在自己的应用初始Activity中加入如下两行代码
    RefWatcher refWatcher = ExampleApplication.getRefWatcher(this);
    refWatcher.watch(this);

   textView = (TextView) findViewById(R.id.tv);

    textView.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            startAsyncTask();
        }
    });
}
private void async() {

    startAsyncTask();
}
private void startAsyncTask() {
    // This async task is an anonymous class and therefore has a hidden reference to the outer
    // class MainActivity. If the activity gets destroyed before the task finishes (e.g. rotation)
    // the activity instance will leak.

    new AsyncTask<Void, Void, Void>() {
        @Override
   protected Void doInBackground(Void... params) {
            // Do some slow work in background
            SystemClock.sleep(20000);
            return null;
        }
    }.execute();
}
}

工作机制

1.RefWatcher.watch() 创建一个 KeyedWeakReference 到要被监控的对象。

2.然后在后台线程检查引用是否被清除,如果没有,调用GC。

3.如果引用还是未被清除,把 heap 内存 dump 到 APP 对应的文件系统中的一个 .hprof 文件中。

4.在另外一个进程中的 HeapAnalyzerService 有一个 HeapAnalyzer 使用HAHA 解析这个文件。

5.得益于唯一的 reference key, HeapAnalyzer 找到 KeyedWeakReference,定位内存泄露。

6.HeapAnalyzer 计算 到 GC roots 的最短强引用路径,并确定是否是泄露。如果是的话,建立导致泄露的引用链。

7.引用链传递到 APP 进程中的 DisplayLeakService, 并以通知的形式展示出来。

ok,这里就不再深入了,想要了解更多就到 作者 github 主页 这去哈。

总结

对 Activity 等组件的引用应该控制在 Activity 的生命周期之内; 如果不能就考虑使用 getApplicationContext 或者 getApplication,以避免 Activity 被外部长生命周期的对象引用而泄露。 
尽量不要在静态变量或者静态内部类中使用非静态外部成员变量(包括context ),即使要使用,也要考虑适时把外部成员变量置空;也可以在内部类中使用弱引用来引用外部类的变量。 
对于生命周期比Activity长的内部类对象,并且内部类中使用了外部类的成员变量,可以这样做避免内存泄漏: 
将内部类改为静态内部类 
静态内部类中使用弱引用来引用外部类的成员变量 
Handler 的持有的引用对象最好使用弱引用,资源释放时也可以清空 Handler 里面的消息。比如在 Activity onStop 或者 onDestroy 的时候,取消掉该 Handler 对象的 Message和 Runnable. 
在 Java 的实现过程中,也要考虑其对象释放,最好的方法是在不使用某对象时,显式地将此对象赋值为 null,比如使用完Bitmap 后先调用 recycle(),再赋为null,清空对图片等资源有直接引用或者间接引用的数组(使用 array.clear() ; array = null)等,最好遵循谁创建谁释放的原则。 
正确关闭资源,对于使用了BraodcastReceiver,ContentObserver,File,游标 Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时及时关闭或者注销。 
保持对对象生命周期的敏感,特别注意单例、静态对象、全局性集合等的生命周期。

关于MAT工具查找内存泄漏的文章,写的很详细。
1.http://blog.csdn.net/yxz329130952/article/details/50288145

相信看了以上文章你对ANR和OOM一定会有更深层次的理解




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值