crashapplication

地址://http://onewayonelife.iteye.com/blog/1147533

CrashHandler

Java代码 复制代码  收藏代码
  1. package org.wp.activity;   
  2.   
  3. import java.io.File;   
  4. import java.io.FileOutputStream;   
  5. import java.io.FilenameFilter;   
  6. import java.io.PrintWriter;   
  7. import java.io.StringWriter;   
  8. import java.io.Writer;   
  9. import java.lang.Thread.UncaughtExceptionHandler;   
  10. import java.lang.reflect.Field;   
  11. import java.util.Arrays;   
  12. import java.util.Properties;   
  13. import java.util.TreeSet;   
  14.   
  15. import android.content.Context;   
  16. import android.content.pm.PackageInfo;   
  17. import android.content.pm.PackageManager;   
  18. import android.content.pm.PackageManager.NameNotFoundException;   
  19. import android.os.Build;   
  20. import android.os.Looper;   
  21. import android.util.Log;   
  22. import android.widget.Toast;   
  23.   
  24. /**  
  25.  *   
  26.  *   
  27.  * UncaughtExceptionHandler:线程未捕获异常控制器是用来处理未捕获异常的。   
  28.  *                           如果程序出现了未捕获异常默认情况下则会出现强行关闭对话框  
  29.  *                           实现该接口并注册为程序中的默认未捕获异常处理   
  30.  *                           这样当未捕获异常发生时,就可以做些异常处理操作  
  31.  *                           例如:收集异常信息,发送错误报告 等。  
  32.  *   
  33.  * UncaughtException处理类,当程序发生Uncaught异常的时候,由该类来接管程序,并记录发送错误报告.  
  34.  */  
  35. public class CrashHandler implements UncaughtExceptionHandler {   
  36.     /** Debug Log Tag */  
  37.     public static final String TAG = "CrashHandler";   
  38.     /** 是否开启日志输出, 在Debug状态下开启, 在Release状态下关闭以提升程序性能 */  
  39.     public static final boolean DEBUG = true;   
  40.     /** CrashHandler实例 */  
  41.     private static CrashHandler INSTANCE;   
  42.     /** 程序的Context对象 */  
  43.     private Context mContext;   
  44.     /** 系统默认的UncaughtException处理类 */  
  45.     private Thread.UncaughtExceptionHandler mDefaultHandler;   
  46.        
  47.     /** 使用Properties来保存设备的信息和错误堆栈信息 */  
  48.     private Properties mDeviceCrashInfo = new Properties();   
  49.     private static final String VERSION_NAME = "versionName";   
  50.     private static final String VERSION_CODE = "versionCode";   
  51.     private static final String STACK_TRACE = "STACK_TRACE";   
  52.     /** 错误报告文件的扩展名 */  
  53.     private static final String CRASH_REPORTER_EXTENSION = ".cr";   
  54.        
  55.     /** 保证只有一个CrashHandler实例 */  
  56.     private CrashHandler() {   
  57.     }   
  58.   
  59.     /** 获取CrashHandler实例 ,单例模式 */  
  60.     public static CrashHandler getInstance() {   
  61.         if (INSTANCE == null)   
  62.             INSTANCE = new CrashHandler();   
  63.         return INSTANCE;   
  64.     }   
  65.        
  66.     /**  
  67.      * 初始化,注册Context对象, 获取系统默认的UncaughtException处理器, 设置该CrashHandler为程序的默认处理器  
  68.      *   
  69.      * @param ctx  
  70.      */  
  71.     public void init(Context ctx) {   
  72.         mContext = ctx;   
  73.         mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();   
  74.         Thread.setDefaultUncaughtExceptionHandler(this);   
  75.     }   
  76.        
  77.     /**  
  78.      * 当UncaughtException发生时会转入该函数来处理  
  79.      */  
  80.     @Override  
  81.     public void uncaughtException(Thread thread, Throwable ex) {   
  82.         if (!handleException(ex) && mDefaultHandler != null) {   
  83.             // 如果用户没有处理则让系统默认的异常处理器来处理   
  84.             mDefaultHandler.uncaughtException(thread, ex);   
  85.         } else {   
  86.             // Sleep一会后结束程序   
  87.             // 来让线程停止一会是为了显示Toast信息给用户,然后Kill程序   
  88.             try {   
  89.                 Thread.sleep(3000);   
  90.             } catch (InterruptedException e) {   
  91.                 Log.e(TAG, "Error : ", e);   
  92.             }   
  93.             android.os.Process.killProcess(android.os.Process.myPid());   
  94.             System.exit(10);   
  95.         }   
  96.     }   
  97.   
  98.     /**  
  99.      * 自定义错误处理,收集错误信息 发送错误报告等操作均在此完成. 开发者可以根据自己的情况来自定义异常处理逻辑  
  100.      *   
  101.      * @param ex  
  102.      * @return true:如果处理了该异常信息;否则返回false  
  103.      */  
  104.     private boolean handleException(Throwable ex) {   
  105.         if (ex == null) {   
  106.             return true;   
  107.         }   
  108.         final String msg = ex.getLocalizedMessage();   
  109.         // 使用Toast来显示异常信息   
  110.         new Thread() {   
  111.             @Override  
  112.             public void run() {   
  113.                 // Toast 显示需要出现在一个线程的消息队列中   
  114.                 Looper.prepare();   
  115.                 Toast.makeText(mContext, "程序出错啦:" + msg, Toast.LENGTH_LONG).show();   
  116.                 Looper.loop();   
  117.             }   
  118.         }.start();   
  119.         // 收集设备信息   
  120.         collectCrashDeviceInfo(mContext);   
  121.         // 保存错误报告文件   
  122.         String crashFileName = saveCrashInfoToFile(ex);   
  123.         // 发送错误报告到服务器   
  124.         sendCrashReportsToServer(mContext);   
  125.         return true;   
  126.     }   
  127.   
  128.     /**  
  129.      * 收集程序崩溃的设备信息  
  130.      *   
  131.      * @param ctx  
  132.      */  
  133.     public void collectCrashDeviceInfo(Context ctx) {   
  134.         try {   
  135.             // Class for retrieving various kinds of information related to the   
  136.             // application packages that are currently installed on the device.   
  137.             // You can find this class through getPackageManager().   
  138.             PackageManager pm = ctx.getPackageManager();   
  139.             // getPackageInfo(String packageName, int flags)   
  140.             // Retrieve overall information about an application package that is installed on the system.   
  141.             // public static final int GET_ACTIVITIES   
  142.             // Since: API Level 1 PackageInfo flag: return information about activities in the package in activities.   
  143.             PackageInfo pi = pm.getPackageInfo(ctx.getPackageName(), PackageManager.GET_ACTIVITIES);   
  144.             if (pi != null) {   
  145.                 // public String versionName The version name of this package,   
  146.                 // as specified by the <manifest> tag's versionName attribute.   
  147.                 mDeviceCrashInfo.put(VERSION_NAME, pi.versionName == null ? "not set" : pi.versionName);   
  148.                 // public int versionCode The version number of this package,    
  149.                 // as specified by the <manifest> tag's versionCode attribute.   
  150.                 mDeviceCrashInfo.put(VERSION_CODE, pi.versionCode);   
  151.             }   
  152.         } catch (NameNotFoundException e) {   
  153.             Log.e(TAG, "Error while collect package info", e);   
  154.         }   
  155.         // 使用反射来收集设备信息.在Build类中包含各种设备信息,   
  156.         // 例如: 系统版本号,设备生产商 等帮助调试程序的有用信息   
  157.         // 返回 Field 对象的一个数组,这些对象反映此 Class 对象所表示的类或接口所声明的所有字段   
  158.         Field[] fields = Build.class.getDeclaredFields();   
  159.         for (Field field : fields) {   
  160.             try {   
  161.                 // setAccessible(boolean flag)   
  162.                 // 将此对象的 accessible 标志设置为指示的布尔值。   
  163.                 // 通过设置Accessible属性为true,才能对私有变量进行访问,不然会得到一个IllegalAccessException的异常   
  164.                 field.setAccessible(true);   
  165.                 mDeviceCrashInfo.put(field.getName(), field.get(null));   
  166.                 if (DEBUG) {   
  167.                     Log.d(TAG, field.getName() + " : " + field.get(null));   
  168.                 }   
  169.             } catch (Exception e) {   
  170.                 Log.e(TAG, "Error while collect crash info", e);   
  171.             }   
  172.         }   
  173.     }   
  174.        
  175.     /**  
  176.      * 保存错误信息到文件中  
  177.      *   
  178.      * @param ex  
  179.      * @return  
  180.      */  
  181.     private String saveCrashInfoToFile(Throwable ex) {   
  182.         Writer info = new StringWriter();   
  183.         PrintWriter printWriter = new PrintWriter(info);   
  184.         // printStackTrace(PrintWriter s)   
  185.         // 将此 throwable 及其追踪输出到指定的 PrintWriter   
  186.         ex.printStackTrace(printWriter);   
  187.   
  188.         // getCause() 返回此 throwable 的 cause;如果 cause 不存在或未知,则返回 null。   
  189.         Throwable cause = ex.getCause();   
  190.         while (cause != null) {   
  191.             cause.printStackTrace(printWriter);   
  192.             cause = cause.getCause();   
  193.         }   
  194.   
  195.         // toString() 以字符串的形式返回该缓冲区的当前值。   
  196.         String result = info.toString();   
  197.         printWriter.close();   
  198.         mDeviceCrashInfo.put(STACK_TRACE, result);   
  199.   
  200.         try {   
  201.             long timestamp = System.currentTimeMillis();   
  202.             String fileName = "crash-" + timestamp + CRASH_REPORTER_EXTENSION;   
  203.             // 保存文件   
  204.             FileOutputStream trace = mContext.openFileOutput(fileName, Context.MODE_PRIVATE);   
  205.             mDeviceCrashInfo.store(trace, "");   
  206.             trace.flush();   
  207.             trace.close();   
  208.             return fileName;   
  209.         } catch (Exception e) {   
  210.             Log.e(TAG, "an error occured while writing report file...", e);   
  211.         }   
  212.         return null;   
  213.     }   
  214.        
  215.     /**  
  216.      * 把错误报告发送给服务器,包含新产生的和以前没发送的.  
  217.      *   
  218.      * @param ctx  
  219.      */  
  220.     private void sendCrashReportsToServer(Context ctx) {   
  221.         String[] crFiles = getCrashReportFiles(ctx);   
  222.         if (crFiles != null && crFiles.length > 0) {   
  223.             TreeSet<String> sortedFiles = new TreeSet<String>();   
  224.             sortedFiles.addAll(Arrays.asList(crFiles));   
  225.   
  226.             for (String fileName : sortedFiles) {   
  227.                 File cr = new File(ctx.getFilesDir(), fileName);   
  228.                 postReport(cr);   
  229.                 cr.delete();// 删除已发送的报告   
  230.             }   
  231.         }   
  232.     }   
  233.   
  234.     /**  
  235.      * 获取错误报告文件名  
  236.      *   
  237.      * @param ctx  
  238.      * @return  
  239.      */  
  240.     private String[] getCrashReportFiles(Context ctx) {   
  241.         File filesDir = ctx.getFilesDir();   
  242.         // 实现FilenameFilter接口的类实例可用于过滤器文件名   
  243.         FilenameFilter filter = new FilenameFilter() {   
  244.             // accept(File dir, String name)   
  245.             // 测试指定文件是否应该包含在某一文件列表中。   
  246.             public boolean accept(File dir, String name) {   
  247.                 return name.endsWith(CRASH_REPORTER_EXTENSION);   
  248.             }   
  249.         };   
  250.         // list(FilenameFilter filter)   
  251.         // 返回一个字符串数组,这些字符串指定此抽象路径名表示的目录中满足指定过滤器的文件和目录   
  252.         return filesDir.list(filter);   
  253.     }   
  254.   
  255.     private void postReport(File file) {   
  256.         // TODO 使用HTTP Post 发送错误报告到服务器   
  257.         // 这里不再详述,开发者可以根据OPhoneSDN上的其他网络操作   
  258.         // 教程来提交错误报告   
  259.     }   
  260.   
  261.     /**  
  262.      * 在程序启动时候, 可以调用该函数来发送以前没有发送的报告  
  263.      */  
  264.     public void sendPreviousReportsToServer() {   
  265.         sendCrashReportsToServer(mContext);   
  266.     }   
  267. }  
package org.wp.activity;

import java.io.File;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.Thread.UncaughtExceptionHandler;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Properties;
import java.util.TreeSet;

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Build;
import android.os.Looper;
import android.util.Log;
import android.widget.Toast;

/**
 * 
 * 
 * UncaughtExceptionHandler:线程未捕获异常控制器是用来处理未捕获异常的。 
 *                           如果程序出现了未捕获异常默认情况下则会出现强行关闭对话框
 *                           实现该接口并注册为程序中的默认未捕获异常处理 
 *                           这样当未捕获异常发生时,就可以做些异常处理操作
 *                           例如:收集异常信息,发送错误报告 等。
 * 
 * UncaughtException处理类,当程序发生Uncaught异常的时候,由该类来接管程序,并记录发送错误报告.
 */
public class CrashHandler implements UncaughtExceptionHandler {
	/** Debug Log Tag */
	public static final String TAG = "CrashHandler";
	/** 是否开启日志输出, 在Debug状态下开启, 在Release状态下关闭以提升程序性能 */
	public static final boolean DEBUG = true;
	/** CrashHandler实例 */
	private static CrashHandler INSTANCE;
	/** 程序的Context对象 */
	private Context mContext;
	/** 系统默认的UncaughtException处理类 */
	private Thread.UncaughtExceptionHandler mDefaultHandler;
	
	/** 使用Properties来保存设备的信息和错误堆栈信息 */
	private Properties mDeviceCrashInfo = new Properties();
	private static final String VERSION_NAME = "versionName";
	private static final String VERSION_CODE = "versionCode";
	private static final String STACK_TRACE = "STACK_TRACE";
	/** 错误报告文件的扩展名 */
	private static final String CRASH_REPORTER_EXTENSION = ".cr";
	
	/** 保证只有一个CrashHandler实例 */
	private CrashHandler() {
	}

	/** 获取CrashHandler实例 ,单例模式 */
	public static CrashHandler getInstance() {
		if (INSTANCE == null)
			INSTANCE = new CrashHandler();
		return INSTANCE;
	}
	
	/**
	 * 初始化,注册Context对象, 获取系统默认的UncaughtException处理器, 设置该CrashHandler为程序的默认处理器
	 * 
	 * @param ctx
	 */
	public void init(Context ctx) {
		mContext = ctx;
		mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
		Thread.setDefaultUncaughtExceptionHandler(this);
	}
	
	/**
	 * 当UncaughtException发生时会转入该函数来处理
	 */
	@Override
	public void uncaughtException(Thread thread, Throwable ex) {
		if (!handleException(ex) && mDefaultHandler != null) {
			// 如果用户没有处理则让系统默认的异常处理器来处理
			mDefaultHandler.uncaughtException(thread, ex);
		} else {
			// Sleep一会后结束程序
			// 来让线程停止一会是为了显示Toast信息给用户,然后Kill程序
			try {
				Thread.sleep(3000);
			} catch (InterruptedException e) {
				Log.e(TAG, "Error : ", e);
			}
			android.os.Process.killProcess(android.os.Process.myPid());
			System.exit(10);
		}
	}

	/**
	 * 自定义错误处理,收集错误信息 发送错误报告等操作均在此完成. 开发者可以根据自己的情况来自定义异常处理逻辑
	 * 
	 * @param ex
	 * @return true:如果处理了该异常信息;否则返回false
	 */
	private boolean handleException(Throwable ex) {
		if (ex == null) {
			return true;
		}
		final String msg = ex.getLocalizedMessage();
		// 使用Toast来显示异常信息
		new Thread() {
			@Override
			public void run() {
				// Toast 显示需要出现在一个线程的消息队列中
				Looper.prepare();
				Toast.makeText(mContext, "程序出错啦:" + msg, Toast.LENGTH_LONG).show();
				Looper.loop();
			}
		}.start();
		// 收集设备信息
		collectCrashDeviceInfo(mContext);
		// 保存错误报告文件
		String crashFileName = saveCrashInfoToFile(ex);
		// 发送错误报告到服务器
		sendCrashReportsToServer(mContext);
		return true;
	}

	/**
	 * 收集程序崩溃的设备信息
	 * 
	 * @param ctx
	 */
	public void collectCrashDeviceInfo(Context ctx) {
		try {
			// Class for retrieving various kinds of information related to the
			// application packages that are currently installed on the device.
			// You can find this class through getPackageManager().
			PackageManager pm = ctx.getPackageManager();
			// getPackageInfo(String packageName, int flags)
			// Retrieve overall information about an application package that is installed on the system.
			// public static final int GET_ACTIVITIES
			// Since: API Level 1 PackageInfo flag: return information about activities in the package in activities.
			PackageInfo pi = pm.getPackageInfo(ctx.getPackageName(), PackageManager.GET_ACTIVITIES);
			if (pi != null) {
				// public String versionName The version name of this package,
				// as specified by the <manifest> tag's versionName attribute.
				mDeviceCrashInfo.put(VERSION_NAME, pi.versionName == null ? "not set" : pi.versionName);
				// public int versionCode The version number of this package, 
				// as specified by the <manifest> tag's versionCode attribute.
				mDeviceCrashInfo.put(VERSION_CODE, pi.versionCode);
			}
		} catch (NameNotFoundException e) {
			Log.e(TAG, "Error while collect package info", e);
		}
		// 使用反射来收集设备信息.在Build类中包含各种设备信息,
		// 例如: 系统版本号,设备生产商 等帮助调试程序的有用信息
		// 返回 Field 对象的一个数组,这些对象反映此 Class 对象所表示的类或接口所声明的所有字段
		Field[] fields = Build.class.getDeclaredFields();
		for (Field field : fields) {
			try {
				// setAccessible(boolean flag)
				// 将此对象的 accessible 标志设置为指示的布尔值。
				// 通过设置Accessible属性为true,才能对私有变量进行访问,不然会得到一个IllegalAccessException的异常
				field.setAccessible(true);
				mDeviceCrashInfo.put(field.getName(), field.get(null));
				if (DEBUG) {
					Log.d(TAG, field.getName() + " : " + field.get(null));
				}
			} catch (Exception e) {
				Log.e(TAG, "Error while collect crash info", e);
			}
		}
	}
	
	/**
	 * 保存错误信息到文件中
	 * 
	 * @param ex
	 * @return
	 */
	private String saveCrashInfoToFile(Throwable ex) {
		Writer info = new StringWriter();
		PrintWriter printWriter = new PrintWriter(info);
		// printStackTrace(PrintWriter s)
		// 将此 throwable 及其追踪输出到指定的 PrintWriter
		ex.printStackTrace(printWriter);

		// getCause() 返回此 throwable 的 cause;如果 cause 不存在或未知,则返回 null。
		Throwable cause = ex.getCause();
		while (cause != null) {
			cause.printStackTrace(printWriter);
			cause = cause.getCause();
		}

		// toString() 以字符串的形式返回该缓冲区的当前值。
		String result = info.toString();
		printWriter.close();
		mDeviceCrashInfo.put(STACK_TRACE, result);

		try {
			long timestamp = System.currentTimeMillis();
			String fileName = "crash-" + timestamp + CRASH_REPORTER_EXTENSION;
			// 保存文件
			FileOutputStream trace = mContext.openFileOutput(fileName, Context.MODE_PRIVATE);
			mDeviceCrashInfo.store(trace, "");
			trace.flush();
			trace.close();
			return fileName;
		} catch (Exception e) {
			Log.e(TAG, "an error occured while writing report file...", e);
		}
		return null;
	}
	
	/**
	 * 把错误报告发送给服务器,包含新产生的和以前没发送的.
	 * 
	 * @param ctx
	 */
	private void sendCrashReportsToServer(Context ctx) {
		String[] crFiles = getCrashReportFiles(ctx);
		if (crFiles != null && crFiles.length > 0) {
			TreeSet<String> sortedFiles = new TreeSet<String>();
			sortedFiles.addAll(Arrays.asList(crFiles));

			for (String fileName : sortedFiles) {
				File cr = new File(ctx.getFilesDir(), fileName);
				postReport(cr);
				cr.delete();// 删除已发送的报告
			}
		}
	}

	/**
	 * 获取错误报告文件名
	 * 
	 * @param ctx
	 * @return
	 */
	private String[] getCrashReportFiles(Context ctx) {
		File filesDir = ctx.getFilesDir();
		// 实现FilenameFilter接口的类实例可用于过滤器文件名
		FilenameFilter filter = new FilenameFilter() {
			// accept(File dir, String name)
			// 测试指定文件是否应该包含在某一文件列表中。
			public boolean accept(File dir, String name) {
				return name.endsWith(CRASH_REPORTER_EXTENSION);
			}
		};
		// list(FilenameFilter filter)
		// 返回一个字符串数组,这些字符串指定此抽象路径名表示的目录中满足指定过滤器的文件和目录
		return filesDir.list(filter);
	}

	private void postReport(File file) {
		// TODO 使用HTTP Post 发送错误报告到服务器
		// 这里不再详述,开发者可以根据OPhoneSDN上的其他网络操作
		// 教程来提交错误报告
	}

	/**
	 * 在程序启动时候, 可以调用该函数来发送以前没有发送的报告
	 */
	public void sendPreviousReportsToServer() {
		sendCrashReportsToServer(mContext);
	}
}

 

CrashApplication

Java代码 复制代码  收藏代码
  1. package org.wp.activity;   
  2.   
  3. import android.app.Application;   
  4.   
  5. /**  
  6.  * 在开发应用时都会和Activity打交道,而Application使用的就相对较少了。  
  7.  * Application是用来管理应用程序的全局状态的,比如载入资源文件。  
  8.  * 在应用程序启动的时候Application会首先创建,然后才会根据情况(Intent)启动相应的Activity或者Service。  
  9.  * 在本文将在Application中注册未捕获异常处理器。  
  10.  */  
  11. public class CrashApplication extends Application {   
  12.     @Override  
  13.     public void onCreate() {   
  14.         super.onCreate();   
  15.         CrashHandler crashHandler = CrashHandler.getInstance();   
  16.         // 注册crashHandler   
  17.         crashHandler.init(getApplicationContext());   
  18.         // 发送以前没发送的报告(可选)   
  19.         crashHandler.sendPreviousReportsToServer();   
  20.     }   
  21. }  
package org.wp.activity;

import android.app.Application;

/**
 * 在开发应用时都会和Activity打交道,而Application使用的就相对较少了。
 * Application是用来管理应用程序的全局状态的,比如载入资源文件。
 * 在应用程序启动的时候Application会首先创建,然后才会根据情况(Intent)启动相应的Activity或者Service。
 * 在本文将在Application中注册未捕获异常处理器。
 */
public class CrashApplication extends Application {
	@Override
	public void onCreate() {
		super.onCreate();
		CrashHandler crashHandler = CrashHandler.getInstance();
		// 注册crashHandler
		crashHandler.init(getApplicationContext());
		// 发送以前没发送的报告(可选)
		crashHandler.sendPreviousReportsToServer();
	}
}

 

在AndroidManifest.xml中注册

Xml代码 复制代码  收藏代码
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <manifest xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     package="org.wp.activity" android:versionCode="1" android:versionName="1.0">  
  4.     <application android:icon="@drawable/icon" android:label="@string/app_name"  
  5.         android:name=".CrashApplication" android:debuggable="true">  
  6.         <activity android:name=".MainActivity" android:label="@string/app_name">  
  7.             <intent-filter>  
  8.                 <action android:name="android.intent.action.MAIN" />  
  9.                 <category android:name="android.intent.category.LAUNCHER" />  
  10.             </intent-filter>  
  11.         </activity>  
  12.     </application>  
  13.     <uses-sdk android:minSdkVersion="8" />  
  14. </manifest>  

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值