Android中线程通讯类Handler

handler是线程通讯工具类。用于传递消息。它有两个队列:
1.消息队列
2.线程队列

消息队列使用sendMessage和HandleMessage的组合来发送和处理消息。
线程队列类似一段代码,或者说一个方法的委托,用户传递方法。使用post,postDelayed 添加委托,使用 removeCallbacks移除委托。
 

由上面的特性我们可以简单看出handler类似一个容器对象,它携带了消息的集合和委托的集合。java里没有委托delegate的概念,但是可以通过class来持有一个可执行的方法代理。
handler更像是一个传递者,在另外的线程里和主线程之间传递消息和可执行的代码。它不仅仅携带了数据,而且封装了一些操作行为,比如说在适当的时机(...)来执行线程队列里的“委托”的代码。

handler可能是和消息队列交互的,我们在new Handler实例化对象时,这个对象应该就和主线程的消息队列建立了关系。当我们使用handler.Post(runnabler1),发送一个委托的方法runnabler1代理给handler时,主消息队列会在适当的时候执行这个runnabler1里的委托方法,即执行了runnabler.run方法。

Handler与Thread的区别: 
Handler与调用者处于同一线程,如果Handler里面做耗时的动作,调用者线程会阻塞。Android UI操作不是线程安全的,并且这些操作必须在UI线程中执行。Android提供了几种基本的可以在其他线程中处理UI操作的方案,包括Activity 的runOnUiThread(Runnable),View的post以及1.5版本的工具类AsyncTask等方案都采用了 Handler,Handler的post对线程的处理也不是真正start一个新的线程,而是直接调用了线程的run方法,这正是google煞费苦心搞一套Handler的用意。 

Handler对于Message的处理不是并发的。一个Looper 只有处理完一条Message才会读取下一条,所以消息的处理是阻塞形式的。但是如果用不同的Looper则能达到并发的目的。Service 中,onStart的执行也是阻塞的。如果一个startService在onStart执行完成之前,再次条用startService也会阻塞。如果希望能尽快的执行onStart则可以在onStart中使用handler,因为Message的send是非阻塞的。如果要是不同消息的处理也是并发的,则可以用不同的Looper实例化Handler。 

1. Message Message消息 
理解为线程间交流的信息,处理数据后台线程需要更新UI,则发送Message内含一些数据给UI线程。

2. Handler Handler处理者 
是Message的主要处理者,负责Message的发送,Message内容的执行处理。后台线程就是通过传进来的Handler对象引用来sendMessage(Message)。而使用Handler,需要implement 该类的 handleMessage(Message) 方法,它是处理这些Message的操作内容,例如Update UI。 通常需要子类化Handler来实现handleMessage方法。

3. Message Queue Message Queue消息队列 
用来存放通过Handler发布的消息,按照先进先出执行。 每个message queue都会有一个对应的Handler。Handler会向message queue通过两种方法发送消息:sendMessage或post。这两种消息都会插在message queue队尾并按先进先出执行。但通过这两种方法发送的消息执行的方式略有不同:通过sendMessage发送的是一个message对象,会被Handler的handleMessage()函数处理;而通过post方法发送的是一个runnable对象,则会自己执行。 

4. Looper Looper是每条线程里的Message Queue的管家。 
Android没有Global的Message Queue,而Android会自动替主线程(UI线程)建立Message Queue,但在子线程里并没有建立Message Queue。所以调用Looper.getMainLooper()得到的主线程的Looper不为NULL,但调用Looper.myLooper()得到当前线程的Looper就有可能为NULL。

 

下面通过几个实例来理解:

Example1:

深入理解Android消息处理系统

Android系统中Looper负责管理线程的消息队列和消息循环。 可以通过Loop.myLooper()得到当前线程的Looper对象,通过Loop.getMainLooper()可以获得当前进程的主线程的Looper对象。
Android 系统的消息队列和消息循环都是针对具体线程的,一个线程可以存在(当然也可以不存在)一个消息队列和一个消息循环(Looper),特定线程的消息只能分 发给本线程,不能进行跨线程,跨进程通讯。但是创建的工作线程默认是没有消息循环和消息队列的,如果想让该线程具有消息队列和消息循环,需要在线程中首先 调用Looper.prepare()来创建消息队列,然后调用Looper.loop()进入消息循环。
 如下例所示:

Java代码   收藏代码
  1. class LooperThread extends Thread {    
  2.     public Handler mHandler;    
  3.     
  4.     public void run() {    
  5.         Looper.prepare();    
  6.     
  7.         mHandler = new Handler() {    
  8.             public void handleMessage(Message msg) {    
  9.                 // process incoming messages here    
  10.             }    
  11.         };    
  12.     
  13.         Looper.loop();    
  14.     }    
  15. }    

 这样你的线程就具有了消息处理机制了,在Handler中进行消息处理。

 

 Activity是一个UI线程,运行于主线程中,Android系统在启动的时候会为Activity创建一个消息队列和消息循环(Looper)。
     Handler的作用是把消息加入特定的(Looper)消息队列中,并分发和处理该消息队列中的消息。构造Handler的时候可以指定一个Looper对象,如果不指定则利用当前线程的Looper创建。

Activity、Looper、Handler的关系如下图所示:


 

一 个Activity中可以创建多个工作线程或者其他的组件,如果这些线程或者组件把他们的消息放入Activity的主线程消息队列,那么该消息就会在主 线程中处理了。因为主线程一般负责界面的更新操作,并且Android系统中的weget不是线程安全的,所以这种方式可以很好的实现Android界面 更新。在Android系统中这种方式有着广泛的运用。
     那么另外一个线程怎样把消息放入主线程的消息队列呢?答案是通过Handle对象,只要Handler对象以主线程的Looper创建,那么调用 Handler的sendMessage等接口,将会把消息放入队列都将是放入主线程的消息队列。并且将会在Handler主线程中调用该handler 的handleMessage接口来处理消息。

Java代码   收藏代码
  1. package com.simon;    
  2.     
  3. import android.app.Activity;    
  4. import android.os.Bundle;    
  5. import android.os.Message;    
  6. import android.util.Log;    
  7. import android.os.Handler;    
  8.     
  9. public class MyHandler extends Activity {    
  10.     static final String TAG = "Handler";    
  11.     Handler h = new Handler(){    
  12.         public void handleMessage (Message msg)    
  13.         {    
  14.             switch(msg.what)    
  15.             {    
  16.             case HANDLER_TEST:    
  17.                 Log.d(TAG, "The handler thread id = " + Thread.currentThread().getId() + "/n");    
  18.                 break;    
  19.             }    
  20.         }    
  21.     };    
  22.     
  23.     static final int HANDLER_TEST = 1;    
  24.     /** Called when the activity is first created. */    
  25.     @Override    
  26.     public void onCreate(Bundle savedInstanceState) {    
  27.         super.onCreate(savedInstanceState);    
  28.         Log.d(TAG, "The main thread id = " + Thread.currentThread().getId() + "/n");    
  29.     
  30.         new myThread().start();    
  31.         setContentView(R.layout.main);    
  32.     }    
  33.     
  34.     class myThread extends Thread    
  35.     {    
  36.         public void run()    
  37.         {    
  38.             Message msg = new Message();    
  39.             msg.what = HANDLER_TEST;    
  40.             h.sendMessage(msg);    
  41.             Log.d(TAG, "The worker thread id = " + Thread.currentThread().getId() + "/n");    
  42.         }    
  43.     }    
  44. }    

 在这个例子中我们主要是打印,这种处理机制各个模块的所处的线程情况。如下是我的机器运行结果:
09-10 23:40:51.478: DEBUG/Handler(302): The main thread id = 1
09-10 23:40:51.569: DEBUG/Handler(302): The worker thread id = 8
09-10 23:40:52.128: DEBUG/Handler(302): The handler thread id = 1
我们可以看出消息处理是在主线程中处理的,在消息处理函数中可以安全的调用主线程中的任何资源,包括刷新界面。工作线程和主线程运行在不同的线程中,所以必须要注意这两个线程间的竞争关系。
     上例中,你可能注意到在工作线程中访问了主线程handler对象,并在调用handler的对象向消息队列加入了一个消息。这个过程中会不会出现消息队列数据不一致问题呢?答案是handler对象不会出问题,因为handler对象管理的Looper对象是线程安全的,不管是加入消息到消息队列和从队列读出消息都是有同步对象保护的,具体请参考Looper.java文件。上例中没有修改handler对象,所以handler对象不可能会出现数据不一致的问题。
     通过上面的分析,我们可以得出如下结论:
1、如果通过工作线程刷新界面,推荐使用handler对象来实现。
2、注意工作线程和主线程之间的竞争关系。推荐handler对象在主线程中构造完成(并且启动工作线程之后不要再修改之,否则会出现数据不一致),然后在工作线程中可以放心的调用发送消息SendMessage等接口。
3、除了2所述的hanlder对象之外的任何主线程的成员变量如果在工作线程中调用,仔细考虑线程同步问题。如果有必要需要加入同步对象保护该变量。
4、handler对象的handleMessage接口将会在主线程中调用。在这个函数可以放心的调用主线程中任何变量和函数,进而完成更新UI的任务。
5、Android很多API也利用Handler这种线程特性,作为一种回调函数的变种,来通知调用者。这样Android框架就可以在其线程中将消息发送到调用者的线程消息队列之中,不用担心线程同步的问题。
     深入理解Android消息处理机制对于应用程序开发非常重要,也可以让你对线程同步有更加深刻的认识。

本例来源:http://www.apkbus.com/forum.php?mod=viewthread&tid=18169&ctid=41

 

Example2:

直接子类:
AsyncQueryHandler:一个帮助你处理异步ContentResolver查询的类
AsyncQueryHandler.WorkerHandler
HttpAuthHandler:Http认证要求用户界面必须处理
SslErrorHandler:SslErrorHandler.class负责处理SSL错误的类

一个Handler允许你发送和处理和线程的消息队列有关的消息和Runnable对象,每个线程实例都与一个单独的
线程和线程的消息队列有关,当你创建了一个新的Handler(句柄),它必然会创建一个thread/Message队列,
从这一点上,它会传送消息和Ruuuable到消息队列,并执行它们,因为它们从消息队列出来

使用Handler有两个主要的用途:
(1)在以后的某个点执行计划的messages和runnables
(2)在你自己所在线程不同的线程中进入一个action并执行

计划好的消息可以用 post(Runnable), postAtTime(Runnable, long), postDelayed(Runnable, long),
sendEmptyMessage(int), sendMessage(Message), sendMessageAtTime(Message, long),
和 sendMessageDelayed(Message, long) 这些方法。在后期的版本中允许插入一个Runnable对象
(当你接受到由message队列调用的对象);sendMessage函数允许你通过Handler的handleMessage(Message)
方法(要求你实现一个Handler的子类)来插入一个包含捆绑的数据的消息对象。

当发送消息的时候,你可以允许这些在消息队列中的item尽快的被处理,或者在被处理或者指定的时间内被
处理之前设置一个延迟,后两个允许你实现超时和其他时序行为。

当应用程序创建一个进程,它的主线程专门用于运行管理顶层的应用程序对象(活动,广播接收机等)和
他们所创建的任何床空的消息队列。您可以创建自己的线程,通过Handler来与你的主应用程序进行沟通。、
这是在post()和sendMessage()方法被调用之前要做的,而不是从你的新线程开始。计划的runnable和
message在适当的时候就会在Handler的message队列中并且被处理。

概览:
interface Handler.Callback :回调接口,你而已实例化一个Handler来避免不得不实现你子类的Handler

    Handler():当前线程在队列中与handler关联的默认构造方法
    Handler(Handler.Callback callback):当前线程和通过处理消息而得到的回调接口关联的构造方法
    Handler(Looper looper):使用提供的这个队列替换默认的那一个
    Handler(Looper looper,Handler.Callback callback):使用提供的这个队列替换默认的那一个并且通过处理消息而的得到一个回调接口

public void dispatchMessage (Message msg)处理系统信息
public final void dump (Printer pw, String prefix)
public final Looper  getLooper ()

public void handleMessage (Message msg)
    子类必须implements它才能接收消息
public final boolean hasMessages (int what, Object object)
    检查在发送消息期间是否在消息队列中有'what'和目标是'object'
public final boolean hasMessages (int what)
    检查在消息发送期间是否在消息队列中有'what'

public final Message  obtainMessage (int what, int arg1, int arg2)
    就像objectMessage()一样,除了它还要设置what,arg1,arg2等几个返回的参数
public final Message  obtainMessage ()
    从全局消息池返回一个新的消息。被创建和分配一个实例更有效。得到的消息通过handler设置这个实例
public final Message  obtainMessage (int what, int arg1, int arg2, Object obj)
    类似于obtainMessage(),除了要设置what,object,arg1,arg2这几个返回值
public final Message  obtainMessage (int what)
    类似于obtainMessage(),除了需要返回一个what参数
public final Message  obtainMessage (int what, Object obj)

public final boolean post (Runnable r)
    将Runnable对象r添加到消息队列中,它会运行在吸附在handler上的线程运行
    Parameters
        可以被执行的Runnable对象
    Returns
        如果Runnable对象成功插入到消息队列中就返回true. 如果失败返回false,一般是因为退出了循环处理消息队列

public final boolean postAtFrontOfQueue (Runnable r)
    向实现了Runnable接口的对象发送一个消息。使得Runnable对象r能够在消息队列的下一个迭代中继续执行。
    这个方法只能在非常特殊的情况下才有用---它很容易饿死在消息队列中,导致排序问题或者其他难以预料的负面影响

public final boolean postAtTime (Runnable r, Object token, long uptimeMillis)
    使Runnable对象r插入到消息队列中,通过uptimeMillis参数使其运行在一个特定的时间点上。时序是updateMillis().
    如果能成功添加到消息队列返回true,失败返回false,通常是因为退出了循环处理消息队列。注意,返回true并不代表Runnable对象可与被处理---如果如果超过了传送时间的户,消息就会被放弃,循环退出

public final boolean postAtTime (Runnable r, long uptimeMillis)
public final boolean postDelayed (Runnable r, long delayMillis)

public final void removeCallbacks (Runnable r)移除那些在消息队列等待的对象.
public final void removeCallbacks (Runnable r, Object token)
public final void removeCallbacksAndMessages (Object token)
public final void removeMessages (int what)移除那些在消息队列中以'what'所指向的消息
public final void removeMessages (int what, Object object)移除那些在消息队列中以'what'所指向的消息和目标为'object'的对象


public final boolean sendEmptyMessage (int what)发送一个带有what参数的消息
    如果消息成功的被放置到了消息队列,返回true,如果失败返回false,通常是由于退出了循环处理消息队列

public final boolean sendEmptyMessageAtTime (int what, long uptimeMillis)发送一个带有what参数的消息,在一个特定的时间点uptimeMillis时
public final boolean sendEmptyMessageDelayed (int what, long delayMillis)发送一个带有what参数的消息,在delayMillis时间时候发送
public final boolean sendMessage (Message msg)在当前时间内所有的等待消息的消息队尾插入一个消息,它将会在handleMessage(Message)中接收到,在附有handler的线程
public final boolean sendMessageAtFrontOfQueue (Message msg)public boolean sendMessageAtTime (Message msg, long uptimeMillis)
public final boolean sendMessageDelayed (Message msg, long delayMillis)

Java代码   收藏代码
  1. package com.loulijun.handlertest;  
  2.   
  3. import android.app.Activity;  
  4. import android.app.ProgressDialog;  
  5. import android.os.Bundle;  
  6. import android.os.Handler;  
  7. import android.os.Message;  
  8. import android.view.View;  
  9. import android.widget.Button;  
  10. import android.widget.Toast;  
  11.   
  12. public class HandlerTestActivity extends Activity {  
  13.     private Button start;  
  14.     ProgressDialog dialog = null;  
  15.      
  16.     @Override  
  17.     public void onCreate(Bundle savedInstanceState) {  
  18.         super.onCreate(savedInstanceState);  
  19.         setContentView(R.layout.main);  
  20.           
  21.         dialog = new ProgressDialog(HandlerTestActivity.this);  
  22.             dialog.setTitle("下载文件");  
  23.             dialog.setMessage("正在下载中...");  
  24.             dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);  
  25.             dialog.setIcon(android.R.drawable.ic_input_add);  
  26.             dialog.setIndeterminate(false);  
  27.             dialog.setCancelable(true);  
  28.           
  29.         start = (Button)findViewById(R.id.start);  
  30.         start.setOnClickListener(new Button.OnClickListener()  
  31.         {  
  32.   
  33.                         public void onClick(View v) {  
  34.                                 dialog.show();  
  35.                                 handler.post(updateThread);  
  36.                         }  
  37.                   
  38.         });  
  39.     }  
  40.       
  41.     Handler handler = new Handler()  
  42.     {  
  43.             public void handleMessage(Message msg)  
  44.             {  
  45.                     dialog.setProgress(msg.arg1);  
  46.                     handler.post(updateThread);  
  47.             }  
  48.     };  
  49.       
  50.     Runnable updateThread = new Runnable()  
  51.     {  
  52.             int i = 0;  
  53.             public void run()  
  54.             {  
  55.                     i = i + 1;  
  56.                     Message msg = handler.obtainMessage();  
  57.                     msg.arg1 = i;  
  58.                     try  
  59.                     {  
  60.                             Thread.sleep(100);  
  61.                     }catch(InterruptedException e)  
  62.                     {  
  63.                             e.printStackTrace();  
  64.                     }  
  65.                       
  66.                     handler.sendMessage(msg);  
  67.                     if( i == 100)  
  68.                     {  
  69.                             handler.removeCallbacks(updateThread);  
  70.                             dialog.dismiss();  
  71.                             Toast.makeText(getApplicationContext(), "下载完成!", Toast.LENGTH_SHORT).show();  
  72.                     }  
  73.             }  
  74.     };  
  75. }  

 分析:
程序中定义了一个Button和一个ProgressDialog,ProgressDialog是在源代码中实现的,一开始是不显示的,当用户点击“下载文件”这个按钮但时候,首先将ProgressDialog显示出来,然后用handler往线程队列中压入一个线程updateThread,此 时updateThread启动,它是用Runnable来实现的,先定义了一个变量i=0,执行run方法后开始让i的值自增长,并将Message对 象压入消息队列,通过hander调用obtainMessage()得到消息,然后为msg对象设置值,线程中设置sleep()的值为100,即每隔 0.1秒就执行一次,并通过handler调用sendMessage(msg)方法将消息发送出去,由handleMessage(Message msg)方法得到消息,设置进度条值,就这样连续执行,知道i=100的时候,将updateThread从线程队列中移除,并显示下载完成。

运行结果:



 

实例来源:

http://www.apkbus.com/forum.php?mod=viewthread&tid=14202&ctid=41

 

Example3:

点击开始后,数字开始从1开始累加



 

Java代码   收藏代码
  1. int  _number ;  
  2.    
  3.     // 执行的代码  
  4.   private Runnable run1 =  new Runnable(){  
  5.       public void run() {  
  6.          String text = "";  
  7.          text = ""+_number++;  
  8.          _txt1.setText(text);  
  9.            
  10.         // 再次传递一个Runnable对象,类似产生一种递归效果  
  11.          _handler.postDelayed(run1,1000);  
  12.    }};  

 上面已经看到 _handler.postDelayed方法了,这个方法就是把 run1这个被委托的内容方法,post传递给hander。主线程会会拿到这个handler,并在适当(空闲)时机执行它。

我们在开始按钮里写启动方法:
Java代码   收藏代码
  1. _btn1.setOnClickListener(new OnClickListener(){  
  2.    public void onClick(View arg0) {  
  3.     // 传递一个Runnable对象,1秒后执行该对象的run方法  
  4.     _handler.postDelayed(run1,1000);  
  5.  }});  

 在停止按钮里,写停止操作的方法

Java代码   收藏代码
  1. _btn2.setOnClickListener(new OnClickListener(){  
  2.    public void onClick(View v) {  
  3.     //移除回调  
  4.     _handler.removeCallbacks(run1);  
  5.    }  
  6. });  

 启动操作:就是把被委托的方法 runnable对象Post出去,即添加到handler的线程队列中去。

停止操作:从线程队列里 移除这个hander 我们还要注意一点,runnabler对象在执行run方法时,再次把自身( ruannabler对象)放进了线程队列,并延迟了1秒,使用了postDelay方法。于是整个就演变成:

1.窗体初始化:构建handler 和runnable对象

2.点击启动按钮,通过handler 发送(post)runnable对象。

3. 下面是我的推测:主线程的消息循环能检测到handler 对象的存在,发现它的线程队列里有未执行的 代码(被runnable对象携带),于是主线程取出这个runnabler对 象,执行了它的run方法。 执行后,把这个对象从线程队列里移除。

4.我们注意到“在我们写的runnable的run方法里,把自身又再次放到了线程队列”,也就是说,在上一步(第3步)中,刚刚吧 执行后的runnable对象移除,又再次放进去了,于是它会再次执行。由此产生了循环的效果,我们窗体的显示会在这个 移除,和 再次放置之间 更新视图,刷新了界面。于是我们看到视图中数字的递增变化。

5.点击取消按钮,强行将runnable从消息队列里移除,于是run方法不会被再次执行。产生了停止的效果。 我们看到:将委托的内容runnable对象 发送post后,该对象的run方法会执行。而在执行后,会自动将它移除。所以我们上面多次PostDelay才不会出错,要不然的话,可就执行个没完没了了。 于是我们再次猜测:这里的runnable对象,其实就是个携带方法的委托。hanler会在适当的时机执行它,而在执行它后,会通知系统内核来更新视图,重绘界面。 

 

Example4:

画一个圆出来,每隔0.1秒,圆向10移动10个像素。可以清楚展示利用Handler更新UI的流程。


Java代码   收藏代码
  1. package com.ray.handler;  
  2.   
  3. import android.content.Context;  
  4. import android.graphics.Canvas;  
  5. import android.graphics.Color;  
  6. import android.graphics.Paint;  
  7. import android.graphics.Point;  
  8. import android.graphics.drawable.Drawable;  
  9. import android.view.View;  
  10.   
  11. public class BounceView extends View {  
  12.     float x = 40;  
  13.       
  14.     public BounceView(Context context) {   
  15.          super(context);   
  16.     }   
  17.   
  18.     @Override   
  19.     protected void onDraw(Canvas canvas) {   
  20.             x+=10;  
  21.             Paint mPaint = new Paint();  
  22.             mPaint.setAntiAlias(true);  
  23.             mPaint.setColor(Color.GREEN);  
  24.             canvas.drawCircle(x, 4040, mPaint);  
  25.     }  
  26. }  
 
Java代码   收藏代码
  1. package com.ray.handler;  
  2.   
  3. import android.app.Activity;  
  4. import android.content.Context;  
  5. import android.graphics.Canvas;  
  6. import android.graphics.Color;  
  7. import android.graphics.Paint;  
  8. import android.os.Bundle;  
  9. import android.os.Handler;  
  10. import android.os.Message;  
  11. import android.view.View;  
  12. import android.view.Window;  
  13.   
  14. public class TestHandler extends Activity {  
  15.      protected static final int GUIUPDATEIDENTIFIER = 0x101;   
  16.            
  17.      Thread myRefreshThread = null;   
  18.      BounceView myBounceView = null;   
  19.    
  20.      Handler myHandler = new Handler() {  
  21.           public void handleMessage(Message msg) {   
  22.                switch (msg.what) {   
  23.                     case TestHandler.GUIUPDATEIDENTIFIER:   
  24.                          myBounceView.invalidate();  
  25.                          break;   
  26.                }   
  27.                super.handleMessage(msg);   
  28.           }   
  29.      };  
  30.      public void onCreate(Bundle savedInstanceState) {   
  31.           super.onCreate(savedInstanceState);   
  32.           this.requestWindowFeature(Window.FEATURE_NO_TITLE);   
  33.    
  34.           this.myBounceView = new BounceView(this);  
  35.           this.setContentView(this.myBounceView);   
  36.           new Thread(new myThread()).start();  
  37.      }   
  38.    
  39.      class myThread implements Runnable {   
  40.           public void run() {  
  41.                while (!Thread.currentThread().isInterrupted()) {    
  42.                        
  43.                     Message message = new Message();   
  44.                     message.what = TestHandler.GUIUPDATEIDENTIFIER;   
  45.                       
  46.                     TestHandler.this.myHandler.sendMessage(message);   
  47.                     try {   
  48.                          Thread.sleep(100);    
  49.                     } catch (InterruptedException e) {   
  50.                          Thread.currentThread().interrupt();   
  51.                     }   
  52.                }   
  53.           }   
  54.      }   
  55. }  
 

实例来源:http://www.apkbus.com/forum.php?mod=viewthread&tid=1606&ctid=41

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值