Android Asynchronous HTTPClient的实现和优化

大家知道Android对UI线程的反应时间要求很高,超过5秒钟直接ANR掉,根本不给你机会多等。


而Android应用与后端系统的交互是最基本的需求之一,如何实现高效的Asynchronous HTTPClient,确保UI线程在启动任务后交由后端异步处理与服务器端的通信,尤为关键。


Google过几个方案,要么太复杂要么不符合要求,基本都淘汰了,最后发现这一版本的实现不错,就拿来用了。

链接:Android Asynchronous HTTPClient tutorial


后来发现了几个严重的问题,罗列如下:

1. 启用单独的线程后,简直如脱缰的野马,难以驾驭。

现象是:在调试的时候经常发现某个线程死掉(比如在服务器down掉的时候,由于线程无法连接而挂掉)

后果是:只能关掉模拟器,甚至还要重启eclipse,否者两者通信出现问题,再也不能继续联机调试


2. 异常的处理非常弱,Activity层难以捕捉并加以处理。

这个问题跟实现的机制有一定的关系,此实现根本就没提供好的异常处理机制,以便捕捉、反馈、处理合理的可预见性的异常,诸如:

1)UnknownHostException – 谁能确保手机的网络连接一直正常,信号一直满格?

2)HttpResponseException – 后端500的错误,说不定就蹦出来了

3)SocketTimeoutException 超时也是太正常不过了,如果人家在荒山野岭(no 3G)摆弄超大的通信请求

4)诸如此类吧

所以改造就再说难免了。下面我贴出相关代码(import就省了吧这里),并加以简单注释说明,方面大家的理解。


首先定义AsyncHttpClient.java。这里的重点是超时的设置。另外我加了个cancelRequest,用以在切换Activity后取消掉原有Activity发出的所有的异步请求,因为一般情况下,切换了Activity后是不能再更新那个UI了,否则会抛出异常,直接导致应用crash掉,不过话说回来,这个cancel我发现好像不是那么给力(any feedback?)。

Java代码 复制代码 收藏代码
  1. public class AsyncHttpClient {
  2. private static DefaultHttpClient httpClient;
  3. public static int CONNECTION_TIMEOUT = 2*60*1000;
  4. public static int SOCKET_TIMEOUT = 2*60*1000;
  5. private static ConcurrentHashMap<Activity,AsyncHttpSender> tasks = new ConcurrentHashMap<Activity,AsyncHttpSender>();
  6. public static void sendRequest(
  7. final Activity currentActitity,
  8. final HttpRequest request,
  9. AsyncResponseListener callback) {
  10. sendRequest(currentActitity, request, callback, CONNECTION_TIMEOUT, SOCKET_TIMEOUT);
  11. }
  12. public static void sendRequest(
  13. final Activity currentActitity,
  14. final HttpRequest request,
  15. AsyncResponseListener callback,
  16. int timeoutConnection,
  17. int timeoutSocket) {
  18. InputHolder input = new InputHolder(request, callback);
  19. AsyncHttpSender sender = new AsyncHttpSender();
  20. sender.execute(input);
  21. tasks.put(currentActitity, sender);
  22. }
  23. public static void cancelRequest(final Activity currentActitity){
  24. if(tasks==null || tasks.size()==0) return;
  25. for (Activity key : tasks.keySet()) {
  26. if(currentActitity == key){
  27. AsyncTask<?,?,?> task = tasks.get(key);
  28. if(task.getStatus()!=null && task.getStatus()!=AsyncTask.Status.FINISHED){
  29. Log.i(TAG, "AsyncTask of " + task + " cancelled.");
  30. task.cancel(true);
  31. }
  32. tasks.remove(key);
  33. }
  34. }
  35. }
  36. public static synchronized HttpClient getClient() {
  37. if (httpClient == null){
  38. //use following code to solve Adapter is detached error
  39. //refer: http://stackoverflow.com/questions/5317882/android-handling-back-button-during-asynctask
  40. BasicHttpParams params = new BasicHttpParams();
  41. SchemeRegistry schemeRegistry = new SchemeRegistry();
  42. schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
  43. final SSLSocketFactory sslSocketFactory = SSLSocketFactory.getSocketFactory();
  44. schemeRegistry.register(new Scheme("https", sslSocketFactory, 443));
  45. // Set the timeout in milliseconds until a connection is established.
  46. HttpConnectionParams.setConnectionTimeout(params, CONNECTION_TIMEOUT);
  47. // Set the default socket timeout (SO_TIMEOUT)
  48. // in milliseconds which is the timeout for waiting for data.
  49. HttpConnectionParams.setSoTimeout(params, SOCKET_TIMEOUT);
  50. ClientConnectionManager cm = new ThreadSafeClientConnManager(params, schemeRegistry);
  51. httpClient = new DefaultHttpClient(cm, params);
  52. }
  53. return httpClient;
  54. }
  55. }
public class AsyncHttpClient {	private static DefaultHttpClient httpClient;		public static int CONNECTION_TIMEOUT = 2*60*1000;	public static int SOCKET_TIMEOUT  = 2*60*1000;		private static ConcurrentHashMap<Activity,AsyncHttpSender> tasks = new ConcurrentHashMap<Activity,AsyncHttpSender>();			public static void sendRequest(			final Activity currentActitity,			final HttpRequest request,			AsyncResponseListener callback) {				sendRequest(currentActitity, request, callback, CONNECTION_TIMEOUT, SOCKET_TIMEOUT);	}		public static void sendRequest(			final Activity currentActitity,			final HttpRequest request,			AsyncResponseListener callback,			int timeoutConnection,			int timeoutSocket) {				InputHolder input = new InputHolder(request, callback);		AsyncHttpSender sender = new AsyncHttpSender();		sender.execute(input);		tasks.put(currentActitity, sender);	}		public static void cancelRequest(final Activity currentActitity){		if(tasks==null || tasks.size()==0) return;		for (Activity key : tasks.keySet()) {		    if(currentActitity == key){		    	AsyncTask<?,?,?> task = tasks.get(key);		    	if(task.getStatus()!=null && task.getStatus()!=AsyncTask.Status.FINISHED){			    	Log.i(TAG, "AsyncTask of " + task + " cancelled.");		    		task.cancel(true);		    	}		    	tasks.remove(key);		    }		}	} 	public static synchronized HttpClient getClient() {		if (httpClient == null){						//use following code to solve Adapter is detached error			//refer: http://stackoverflow.com/questions/5317882/android-handling-back-button-during-asynctask			BasicHttpParams params = new BasicHttpParams();						SchemeRegistry schemeRegistry = new SchemeRegistry();			schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));			final SSLSocketFactory sslSocketFactory = SSLSocketFactory.getSocketFactory();			schemeRegistry.register(new Scheme("https", sslSocketFactory, 443));						// Set the timeout in milliseconds until a connection is established.			HttpConnectionParams.setConnectionTimeout(params, CONNECTION_TIMEOUT);			// Set the default socket timeout (SO_TIMEOUT) 			// in milliseconds which is the timeout for waiting for data.			HttpConnectionParams.setSoTimeout(params, SOCKET_TIMEOUT);						ClientConnectionManager cm = new ThreadSafeClientConnManager(params, schemeRegistry);			httpClient = new DefaultHttpClient(cm, params);			}		return httpClient;	} }

然后是AsyncHttpSender。这里我用了InputHolder和OutputHolder来进行对象传递,简单包装了下:

Java代码 复制代码 收藏代码
  1. /**
  2. * AsyncHttpSender is the AsyncTask implementation
  3. *
  4. * @author bright_zheng
  5. *
  6. */
  7. public class AsyncHttpSender extends AsyncTask<InputHolder, Void, OutputHolder> {
  8. @Override
  9. protected OutputHolder doInBackground(InputHolder... params) {
  10. HttpEntity entity = null;
  11. InputHolder input = params[0];
  12. try {
  13. HttpResponse response = AsyncHttpClient.getClient().execute((HttpUriRequest) input.getRequest());
  14. StatusLine status = response.getStatusLine();
  15. if(status.getStatusCode() >= 300) {
  16. return new OutputHolder(
  17. new HttpResponseException(status.getStatusCode(), status.getReasonPhrase()),
  18. input.getResponseListener());
  19. }
  20. entity = response.getEntity();
  21. Log.i(TAG, "isChunked:" + entity.isChunked());
  22. if(entity != null) {
  23. try{
  24. entity = new BufferedHttpEntity(entity);
  25. }catch(Exception e){
  26. Log.e(<span style="background-color: rgb(255, 255, 255);">TAG</span>, e.getMessage(), e);
  27. //ignore?
  28. }
  29. }
  30. } catch (ClientProtocolException e) {
  31. Log.e(<span style="background-color: rgb(255, 255, 255);">TAG</span>, e.getMessage(), e);
  32. return new OutputHolder(e, input.getResponseListener());
  33. } catch (IOException e) {
  34. Log.e(<span style="background-color: rgb(255, 255, 255);">TAG</span>, e.getMessage(), e);
  35. return new OutputHolder(e, input.getResponseListener());
  36. }
  37. return new OutputHolder(entity, input.getResponseListener());
  38. }
  39. @Override
  40. protected void onPreExecute(){
  41. Log.i(<span style="background-color: rgb(255, 255, 255);">TAG</span>, "AsyncHttpSender.onPreExecute()");
  42. super.onPreExecute();
  43. }
  44. @Override
  45. protected void onPostExecute(OutputHolder result) {
  46. Log.i(<span style="background-color: rgb(255, 255, 255);">TAG</span>, "AsyncHttpSender.onPostExecute()");
  47. super.onPostExecute(result);
  48. if(isCancelled()){
  49. Log.i(<span style="background-color: rgb(255, 255, 255);">TAG</span>, "AsyncHttpSender.onPostExecute(): isCancelled() is true");
  50. return; //Canceled, do nothing
  51. }
  52. AsyncResponseListener listener = result.getResponseListener();
  53. HttpEntity response = result.getResponse();
  54. Throwable exception = result.getException();
  55. if(response!=null){
  56. Log.i(<span style="background-color: rgb(255, 255, 255);">TAG</span>, "AsyncHttpSender.onResponseReceived(response)");
  57. listener.onResponseReceived(response);
  58. }else{
  59. Log.i(<span style="background-color: rgb(255, 255, 255);">TAG</span>, "AsyncHttpSender.onResponseReceived(exception)");
  60. listener.onResponseReceived(exception);
  61. }
  62. }
  63. @Override
  64. protected void onCancelled(){
  65. Log.i(<span style="background-color: rgb(255, 255, 255);">TAG</span>, "AsyncHttpSender.onCancelled()");
  66. super.onCancelled();
  67. //this.isCancelled = true;
  68. }
  69. }
/** * AsyncHttpSender is the AsyncTask implementation *  * @author bright_zheng * */public class AsyncHttpSender extends AsyncTask<InputHolder, Void, OutputHolder> {	@Override	protected OutputHolder doInBackground(InputHolder... params) {		HttpEntity entity = null;		InputHolder input = params[0];		try {			HttpResponse response = AsyncHttpClient.getClient().execute((HttpUriRequest) input.getRequest());			StatusLine status = response.getStatusLine();				        if(status.getStatusCode() >= 300) {	        	return new OutputHolder(	        			new HttpResponseException(status.getStatusCode(), status.getReasonPhrase()),	        			input.getResponseListener());	        }	        			entity = response.getEntity();			Log.i(TAG, "isChunked:" + entity.isChunked());            if(entity != null) {            	try{            		entity = new BufferedHttpEntity(entity);            	}catch(Exception e){            		Log.e(TAG, e.getMessage(), e);            		//ignore?            	}            }					} catch (ClientProtocolException e) {			Log.e(TAG, e.getMessage(), e);			return new OutputHolder(e, input.getResponseListener());		} catch (IOException e) {			Log.e(TAG, e.getMessage(), e);			return new OutputHolder(e, input.getResponseListener());		}		return new OutputHolder(entity, input.getResponseListener());	}		@Override    protected void onPreExecute(){		Log.i(TAG, "AsyncHttpSender.onPreExecute()");		super.onPreExecute();	}		@Override	protected void onPostExecute(OutputHolder result) {		Log.i(TAG, "AsyncHttpSender.onPostExecute()");		super.onPostExecute(result);				if(isCancelled()){			Log.i(TAG, "AsyncHttpSender.onPostExecute(): isCancelled() is true");			return; //Canceled, do nothing		}				AsyncResponseListener listener = result.getResponseListener();		HttpEntity response = result.getResponse();		Throwable exception = result.getException();		if(response!=null){			Log.i(TAG, "AsyncHttpSender.onResponseReceived(response)");			listener.onResponseReceived(response);		}else{			Log.i(TAG, "AsyncHttpSender.onResponseReceived(exception)");			listener.onResponseReceived(exception);		}	}		@Override    protected void onCancelled(){		Log.i(TAG, "AsyncHttpSender.onCancelled()");		super.onCancelled();		//this.isCancelled = true;	}}
Java代码 复制代码 收藏代码
  1. /**
  2. * Input holder
  3. *
  4. * @author bright_zheng
  5. *
  6. */
  7. public class InputHolder{
  8. private HttpRequest request;
  9. private AsyncResponseListener responseListener;
  10. public InputHolder(HttpRequest request, AsyncResponseListener responseListener){
  11. this.request = request;
  12. this.responseListener = responseListener;
  13. }
  14. public HttpRequest getRequest() {
  15. return request;
  16. }
  17. public AsyncResponseListener getResponseListener() {
  18. return responseListener;
  19. }
  20. }
/** * Input holder *  * @author bright_zheng * */public class InputHolder{	private HttpRequest request;	private AsyncResponseListener responseListener;		public InputHolder(HttpRequest request, AsyncResponseListener responseListener){		this.request = request;		this.responseListener = responseListener;	}			public HttpRequest getRequest() {		return request;	}	public AsyncResponseListener getResponseListener() {		return responseListener;	}}
Java代码 复制代码 收藏代码
  1. public class OutputHolder{
  2. private HttpEntity response;
  3. private Throwable exception;
  4. private AsyncResponseListener responseListener;
  5. public OutputHolder(HttpEntity response, AsyncResponseListener responseListener){
  6. this.response = response;
  7. this.responseListener = responseListener;
  8. }
  9. public OutputHolder(Throwable exception, AsyncResponseListener responseListener){
  10. this.exception = exception;
  11. this.responseListener = responseListener;
  12. }
  13. public HttpEntity getResponse() {
  14. return response;
  15. }
  16. public Throwable getException() {
  17. return exception;
  18. }
  19. public AsyncResponseListener getResponseListener() {
  20. return responseListener;
  21. }
  22. }
public class OutputHolder{	private HttpEntity response;	private Throwable exception;	private AsyncResponseListener responseListener;		public OutputHolder(HttpEntity response, AsyncResponseListener responseListener){		this.response = response;		this.responseListener = responseListener;	}		public OutputHolder(Throwable exception, AsyncResponseListener responseListener){		this.exception = exception;		this.responseListener = responseListener;	}	public HttpEntity getResponse() {		return response;	}	public Throwable getException() {		return exception;	}		public AsyncResponseListener getResponseListener() {		return responseListener;	}	}

再来看看我们的Call back接口定义, AsyncResponseListener.java:

Java代码 复制代码 收藏代码
  1. /**
  2. * The call back interface for
  3. *
  4. * @author bright_zheng
  5. *
  6. */
  7. public interface AsyncResponseListener {
  8. /** Handle successful response */
  9. public void onResponseReceived(HttpEntity response);
  10. /** Handle exception */
  11. public void onResponseReceived(Throwable response);
  12. }
/** * The call back interface for   *  * @author bright_zheng * */public interface AsyncResponseListener {	/** Handle successful response */	public void onResponseReceived(HttpEntity response);		/** Handle exception */	public void onResponseReceived(Throwable response);}

以及抽象Call back的实现,AbstractAsyncResponseListener.java:

Java代码 复制代码 收藏代码
  1. /**
  2. * Abstract Async Response Listener implementation
  3. *
  4. * Subclass should implement at lease two methods.
  5. * 1. onSuccess() to handle the corresponding successful response object
  6. * 2. onFailure() to handle the exception if any
  7. *
  8. * @author bright_zheng
  9. *
  10. */
  11. public abstract class AbstractAsyncResponseListener implements AsyncResponseListener{
  12. public static final int RESPONSE_TYPE_STRING = 1;
  13. public static final int RESPONSE_TYPE_JSON_ARRAY = 2;
  14. public static final int RESPONSE_TYPE_JSON_OBJECT = 3;
  15. public static final int RESPONSE_TYPE_STREAM = 4;
  16. private int responseType;
  17. public AbstractAsyncResponseListener(){
  18. this.responseType = RESPONSE_TYPE_STRING; // default type
  19. }
  20. public AbstractAsyncResponseListener(int responseType){
  21. this.responseType = responseType;
  22. }
  23. public void onResponseReceived(HttpEntity response){
  24. try {
  25. switch(this.responseType){
  26. case RESPONSE_TYPE_JSON_ARRAY:{
  27. String responseBody = EntityUtils.toString(response);
  28. Log.i(<span style="background-color: rgb(255, 255, 255);">TAG</span>, "Return JSON String: " + responseBody);
  29. JSONArray json = null;
  30. if(responseBody!=null && responseBody.trim().length()>0){
  31. json = (JSONArray) new JSONTokener(responseBody).nextValue();
  32. }
  33. onSuccess(json);
  34. break;
  35. }
  36. case RESPONSE_TYPE_JSON_OBJECT:{
  37. String responseBody = EntityUtils.toString(response);
  38. Log.i(<span style="background-color: rgb(255, 255, 255);">TAG</span>, "Return JSON String: " + responseBody);
  39. JSONObject json = null;
  40. if(responseBody!=null && responseBody.trim().length()>0){
  41. json = (JSONObject) new JSONTokener(responseBody).nextValue();
  42. }
  43. onSuccess(json);
  44. break;
  45. }
  46. case RESPONSE_TYPE_STREAM:{
  47. onSuccess(response.getContent());
  48. break;
  49. }
  50. default:{
  51. String responseBody = EntityUtils.toString(response);
  52. onSuccess(responseBody);
  53. }
  54. }
  55. } catch(IOException e) {
  56. onFailure(e);
  57. } catch (JSONException e) {
  58. onFailure(e);
  59. }
  60. }
  61. public void onResponseReceived(Throwable response){
  62. onFailure(response);
  63. }
  64. protected void onSuccess(JSONArray response){}
  65. protected void onSuccess(JSONObject response){}
  66. protected void onSuccess(InputStream response){}
  67. protected void onSuccess(String response) {}
  68. protected void onFailure(Throwable e) {}
  69. }
/** * Abstract Async Response Listener implementation *  * Subclass should implement at lease two methods. * 1. onSuccess() to handle the corresponding successful response object * 2. onFailure() to handle the exception if any *  * @author bright_zheng * */public abstract class AbstractAsyncResponseListener implements AsyncResponseListener{	public static final int RESPONSE_TYPE_STRING = 1;	public static final int RESPONSE_TYPE_JSON_ARRAY = 2;	public static final int RESPONSE_TYPE_JSON_OBJECT = 3;	public static final int RESPONSE_TYPE_STREAM = 4;	private int responseType;		public AbstractAsyncResponseListener(){		this.responseType = RESPONSE_TYPE_STRING; // default type	}		public AbstractAsyncResponseListener(int responseType){		this.responseType = responseType;	}		public void onResponseReceived(HttpEntity response){		try {			switch(this.responseType){		        case RESPONSE_TYPE_JSON_ARRAY:{		        	String responseBody = EntityUtils.toString(response);			        	Log.i(TAG, "Return JSON String: " + responseBody);		        	JSONArray json = null;		        	if(responseBody!=null && responseBody.trim().length()>0){		        		json = (JSONArray) new JSONTokener(responseBody).nextValue();		        	}		    		onSuccess(json);		        	break;		        }		        case RESPONSE_TYPE_JSON_OBJECT:{		        	String responseBody = EntityUtils.toString(response);			        	Log.i(TAG, "Return JSON String: " + responseBody);		        	JSONObject json = null;		        	if(responseBody!=null && responseBody.trim().length()>0){		        		json = (JSONObject) new JSONTokener(responseBody).nextValue();		        	}		    		onSuccess(json);			        	break;		        }		        case RESPONSE_TYPE_STREAM:{		        	onSuccess(response.getContent());		        	break;		        }		        default:{		        	String responseBody = EntityUtils.toString(response);		        	onSuccess(responseBody);		        }         			}	    } catch(IOException e) {	    	onFailure(e);	    } catch (JSONException e) {	    	onFailure(e);		}		}		public void onResponseReceived(Throwable response){		onFailure(response);	}		protected void onSuccess(JSONArray response){}		protected void onSuccess(JSONObject response){}		protected void onSuccess(InputStream response){}		protected void onSuccess(String response) {}	protected void onFailure(Throwable e) {}}

这样我们使用起来就非常清晰、简单了。

下面贴个简单的客户端用法代码片段:

1、这个是把服务器端响应当stream用的,用以诸如文件、图片下载之类的场景:

Java代码 复制代码 收藏代码
  1. AsyncHttpClient.sendRequest(this, request,
  2. new AbstractAsyncResponseListener(AbstractAsyncResponseListener.RESPONSE_TYPE_STREAM){
  3. @Override
  4. protected void onSuccess(InputStream response){
  5. Bitmap bmp = null;
  6. try {
  7. //bmp = decodeFile(response, _facial.getWidth());
  8. bmp = BitmapFactory.decodeStream(response);
  9. //resize to fit screen
  10. bmp = resizeImage(bmp, _facial.getWidth(), true);
  11. candidateCache.put(candidate_id, bmp);
  12. ((ImageView) v).setImageBitmap(bmp);
  13. dialog.dismiss();
  14. } catch (Exception e) {
  15. onFailure(e);
  16. }
  17. }
  18. @Override
  19. protected void onFailure(Throwable e) {
  20. Log.i(TAG, "Error: " + e.getMessage(), e);
  21. updateErrorMessage(e);
  22. dialog.dismiss();
  23. }
  24. });
AsyncHttpClient.sendRequest(this, request,          		new AbstractAsyncResponseListener(AbstractAsyncResponseListener.RESPONSE_TYPE_STREAM){						@Override			protected void onSuccess(InputStream response){				Bitmap bmp = null;				try {					//bmp = decodeFile(response, _facial.getWidth());					bmp = BitmapFactory.decodeStream(response);										//resize to fit screen					bmp = resizeImage(bmp, _facial.getWidth(), true);	        							candidateCache.put(candidate_id, bmp);	        		((ImageView) v).setImageBitmap(bmp);	        			        		dialog.dismiss();				} catch (Exception e) {					onFailure(e);				}			}						@Override			protected void onFailure(Throwable e) {				Log.i(TAG, "Error: " + e.getMessage(), e);				updateErrorMessage(e);								dialog.dismiss();			}					});

2、这个是把服务器端响应当JSON用的,用以诸如获取基本文本信息之类的场景:

Java代码 复制代码 收藏代码
  1. // Async mode to get hit result
  2. AsyncHttpClient.sendRequest(this, request,
  3. new AbstractAsyncResponseListener(AbstractAsyncResponseListener.RESPONSE_TYPE_JSON_ARRAY){
  4. @Override
  5. protected void onSuccess(JSONArray response){
  6. Log.i(TAG, "UploadAndMatch.onSuccess()...");
  7. candidates = response;
  8. if(candidates!=null && candidates.length()>0){
  9. hit_count = candidates.length();
  10. Log.i(TAG, "HIT: " + hit_count);
  11. updateStatus(String.format(context.getString(R.string.msg_got_hit), hit_count));
  12. //update UI
  13. refreshCurrentUI(1);
  14. }else{
  15. Log.i(TAG, "No HIT!");
  16. updateStatus(context.getString(R.string.msg_no_hit));
  17. //update UI
  18. refreshCurrentUI(0);
  19. }
  20. }
  21. @Override
  22. protected void onFailure(Throwable e) {
  23. Log.e(TAG, "UploadAndMatch.onFailure(), error: " + e.getMessage(), e);
  24. updateErrorMessage(e);
  25. //update UI
  26. refreshCurrentUI(-1);
  27. }
  28. });  
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值