关于Binder在Java上工作的一点愚见(一)

Binder作为android作为IPC(进程间通信)的核心,在整个android框架中复用了很多遍几乎只要关于进程之间通信都要涉及Binder,可以说到处都有Binder影子,所以说对android下面感兴趣,Binder就是必备的前置工具。

Binder是什么呢?
首先我们要知道在Linux中进程通信的方法有一下几种:管道(Pipe),信号与跟踪(Signal and Trace),插口(socket),报文队列(Message),共存内存(Share Memory),信号量(Semaphore)。

而Binder作为Android的通信手段同样遵循Service-Client的通信模型。同样使用同样模型的socket的传输的速率慢,开销大。而使用管道,消息队列来发送就要进行数据从发送方缓存拷贝到内核开辟的内存,再从内核的内存拷贝接受方的缓存中。来回的数据拷贝了两次。

如果搭建协议使用信号量,共享内存,则会大大的增加android框架的复杂度。

在安全性能方面只要知道了socket的ip,管道的名字就能被第三方的恶意程序访问另一端。

Binder在这些中间做了折中,它像socket采用了C/S通信模型,但是却不是完全使用一个公开的ip去访问另一端,而是使用类似管道思想。对于Binder而言,Binder可以看成Service提供某个特定服务的访问接入点,Client可以通过这个地址访问Service。对于Client而言,可以把Binder看成通向Service的管道。

本文只总结在Java框架中封装好Binder机制是怎么运行的以及大概要怎么写一个Binder,更加深层次的Native层次还没有这个能力去解析。

要学习Binder怎么使用最好是看看官方怎么写,aidl做为进程通信的方案之一,其实aidl是通过在gen文件夹生成aidl对应的.java文件进行进程间通信。这个文件其实就是一个自动生成的Binder使用方案。

Binder做为进程间通信,想要传输数据的话,必须要将数据序列化。在android里面并不是使用Serializable而是使用Android提供的Parcelable。

要传输数据,就先需要做一个序列化的类:

import android.os.Parcel;
import android.os.Parcelable;

public class Book implements Parcelable{
    public int bookId;
    public String bookName;

    public Book(int bookId,String bookName){
        this.bookId = bookId;
        this.bookName = bookName;
    }

    @Override
    public int describeContents() {
        // TODO Auto-generated method stub
        return 0;
    }

    @Override
    public void writeToParcel(Parcel out, int flags) {
        // TODO Auto-generated method stub
        out.writeInt(bookId);
        out.writeString(bookName);
    }

    public static final Parcelable.Creator<Book> CREATOR = new Parcelable.Creator<Book>() {

        @Override
        public Book createFromParcel(Parcel in) {
            // TODO Auto-generated method stub
            return new Book(in);
        }

        @Override
        public Book[] newArray(int size) {
            // TODO Auto-generated method stub
            return new Book[size];
        }

    };

    private Book(Parcel in){
        bookId = in.readInt();
        bookName = in.readString();
    }

    public String toString(){
        return bookName;
    }

}

序列化详细的东西,开新的一章再说。

让我们先建立三个aidl:

文件 Book.aidl

package com.example.bindertest.aidl;

parcelable Book;

文件IBookManager.aidl

package com.example.bindertest.aidl;

import com.example.bindertest.aidl.Book;
import com.example.bindertest.aidl.IOnNewBookArrived;

interface IBookManager{
    List<Book> getBookList();
    void addBook(in Book book);
    void registerListener(IOnNewBookArrived listener);
    void unregisterListener(IOnNewBookArrived listener);
}

文件IOnNewBookArrived.aidl:

package com.example.bindertest.aidl;

import com.example.bindertest.aidl.Book;

interface IOnNewBookArrived{
    void onNewBookArrived(in Book book);
}

这里我们要注意aidl只会支持,简单的类型,如基本类型呢和List等。

现在在gen目录下,生成了两个.java文件,取出其中一个文件来看看,官方是怎么写Binder的。

看文件IBookManager.java开始分析起:

package com.example.bindertest.aidl;
public interface IBookManager extends android.os.IInterface
{
/** Local-side IPC implementation stub class. */
public static abstract class Stub extends android.os.Binder implements com.example.bindertest.aidl.IBookManager
{
private static final java.lang.String DESCRIPTOR = "com.example.bindertest.aidl.IBookManager";
/** Construct the stub at attach it to the interface. */
public Stub()
{
this.attachInterface(this, DESCRIPTOR);
}
/**
 * Cast an IBinder object into an com.example.bindertest.aidl.IBookManager interface,
 * generating a proxy if needed.
 */
public static com.example.bindertest.aidl.IBookManager asInterface(android.os.IBinder obj)
{
if ((obj==null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin!=null)&&(iin instanceof com.example.bindertest.aidl.IBookManager))) {
return ((com.example.bindertest.aidl.IBookManager)iin);
}
return new com.example.bindertest.aidl.IBookManager.Stub.Proxy(obj);
}
@Override public android.os.IBinder asBinder()
{
return this;
}
@Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
{
switch (code)
{
case INTERFACE_TRANSACTION:
{
reply.writeString(DESCRIPTOR);
return true;
}
case TRANSACTION_getBookList:
{
data.enforceInterface(DESCRIPTOR);
java.util.List<com.example.bindertest.aidl.Book> _result = this.getBookList();
reply.writeNoException();
reply.writeTypedList(_result);
return true;
}
case TRANSACTION_addBook:
{
data.enforceInterface(DESCRIPTOR);
com.example.bindertest.aidl.Book _arg0;
if ((0!=data.readInt())) {
_arg0 = com.example.bindertest.aidl.Book.CREATOR.createFromParcel(data);
}
else {
_arg0 = null;
}
this.addBook(_arg0);
reply.writeNoException();
return true;
}
case TRANSACTION_registerListener:
{
data.enforceInterface(DESCRIPTOR);
com.example.bindertest.aidl.IOnNewBookArrived _arg0;
_arg0 = com.example.bindertest.aidl.IOnNewBookArrived.Stub.asInterface(data.readStrongBinder());
this.registerListener(_arg0);
reply.writeNoException();
return true;
}
case TRANSACTION_unregisterListener:
{
data.enforceInterface(DESCRIPTOR);
com.example.bindertest.aidl.IOnNewBookArrived _arg0;
_arg0 = com.example.bindertest.aidl.IOnNewBookArrived.Stub.asInterface(data.readStrongBinder());
this.unregisterListener(_arg0);
reply.writeNoException();
return true;
}
}
return super.onTransact(code, data, reply, flags);
}
private static class Proxy implements com.example.bindertest.aidl.IBookManager
{
private android.os.IBinder mRemote;
Proxy(android.os.IBinder remote)
{
mRemote = remote;
}
@Override public android.os.IBinder asBinder()
{
return mRemote;
}
public java.lang.String getInterfaceDescriptor()
{
return DESCRIPTOR;
}
@Override public java.util.List<com.example.bindertest.aidl.Book> getBookList() throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
java.util.List<com.example.bindertest.aidl.Book> _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
mRemote.transact(Stub.TRANSACTION_getBookList, _data, _reply, 0);
_reply.readException();
_result = _reply.createTypedArrayList(com.example.bindertest.aidl.Book.CREATOR);
}
finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
@Override public void addBook(com.example.bindertest.aidl.Book book) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
try {
_data.writeInterfaceToken(DESCRIPTOR);
if ((book!=null)) {
_data.writeInt(1);
book.writeToParcel(_data, 0);
}
else {
_data.writeInt(0);
}
mRemote.transact(Stub.TRANSACTION_addBook, _data, _reply, 0);
_reply.readException();
}
finally {
_reply.recycle();
_data.recycle();
}
}
@Override public void registerListener(com.example.bindertest.aidl.IOnNewBookArrived listener) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeStrongBinder((((listener!=null))?(listener.asBinder()):(null)));
mRemote.transact(Stub.TRANSACTION_registerListener, _data, _reply, 0);
_reply.readException();
}
finally {
_reply.recycle();
_data.recycle();
}
}
@Override public void unregisterListener(com.example.bindertest.aidl.IOnNewBookArrived listener) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeStrongBinder((((listener!=null))?(listener.asBinder()):(null)));
mRemote.transact(Stub.TRANSACTION_unregisterListener, _data, _reply, 0);
_reply.readException();
}
finally {
_reply.recycle();
_data.recycle();
}
}
}
static final int TRANSACTION_getBookList = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
static final int TRANSACTION_addBook = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
static final int TRANSACTION_registerListener = (android.os.IBinder.FIRST_CALL_TRANSACTION + 2);
static final int TRANSACTION_unregisterListener = (android.os.IBinder.FIRST_CALL_TRANSACTION + 3);
}
public java.util.List<com.example.bindertest.aidl.Book> getBookList() throws android.os.RemoteException;
public void addBook(com.example.bindertest.aidl.Book book) throws android.os.RemoteException;
public void registerListener(com.example.bindertest.aidl.IOnNewBookArrived listener) throws android.os.RemoteException;
public void unregisterListener(com.example.bindertest.aidl.IOnNewBookArrived listener) throws android.os.RemoteException;
}

上面我们可以观察到有一个和aidl名字相同的接口,里面有一个Stub的抽象类,在Stub中有一个Proxy的内部类。
同时翻开源码可以看到,Binder类里面也有相似的构造,Binder类里面有一个BinderProxy的内部类。

我们一步步的开始解析里面的方法:
首先在aidl中声明的IBookManager的接口继承了IInterface的抽象类,同时里面声明了接口方法,以及方法对应的标志位。

public interface IBookManager extends android.os.IInterface

看看继承的IInterface是什么,很简单:

package android.os;

/**
 * Base class for Binder interfaces.  When defining a new interface,
 * you must derive it from IInterface.
 */
public interface IInterface
{
    /**
     * Retrieve the Binder object associated with this interface.
     * You must use this instead of a plain cast, so that proxy objects
     * can return the correct result.
     */
    public IBinder asBinder();
}

只有一个获取IBinder对象的接口,而Binder恰恰是继承于IBinder。为什么不直接调用Binder的对象呢?这是因为Binder的对象不一定是本地获取,也有可能从远程获取Binder对象。

接口的里面就是,一个抽象类Stub,继承了Binder和接口IBookManager。Stub的内部类的构造器,这个Stub的抽象内部类的构造器:

private static final java.lang.String DESCRIPTOR = "com.example.bindertest.aidl.IBookManager";
public Stub()
{
this.attachInterface(this, DESCRIPTOR);
}

这个函数的作用如下:

    private IInterface mOwner;
    private String mDescriptor;
    public void attachInterface(IInterface owner, String descriptor) {
        mOwner = owner;
        mDescriptor = descriptor;
    }

其实就是通过这个函数获取对应的文件描述符,以及IInterface对象,这个对象上面已经提过是获取Binder对象。

获取完对象之后当然是通过判断Binder判断是否在本地还是远程,再返回IBookMananger这个接口。

public static com.example.bindertest.aidl.IBookManager asInterface(android.os.IBinder obj)
{
if ((obj==null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin!=null)&&(iin instanceof com.example.bindertest.aidl.IBookManager))) {
return ((com.example.bindertest.aidl.IBookManager)iin);
}
return new com.example.bindertest.aidl.IBookManager.Stub.Proxy(obj);
}

如果Binder传进来为空则返回空。如果Binder有东西接着判断是本地还是远程的,通过函数obj.queryLocalInterface(DESCRIPTOR),看看这个函数在android下面是怎么写的,要注意queryLocalInterface分为两个:

来自Binder:

    public IInterface queryLocalInterface(String descriptor) {
        if (mDescriptor.equals(descriptor)) {
            return mOwner;
        }
        return null;
    }

来自BinderProxy:

    public IInterface queryLocalInterface(String descriptor) {
        return null;
    }

这个函数通过文件描述符来判断IInterface返回的Binder是否是本地的,是则返回原来的this,不是则返回空。

接着判断假如iin不为空且实例化,则返回转型为IBookManager的iin。如果iin为空,那么代表这时候的obj是来自于BinderProxy。
这样Proxy中获取的Binder对象其实就是BinderProxy,那么调用的方法就是这个内部类中相应的方法,这就完成了调用服务器的函数转化为调用客户端的函数了。

@Override public android.os.IBinder asBinder()
{
return this;
}

上面是返回当前Binder对象。

@Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
{
switch (code)
{
case INTERFACE_TRANSACTION:
{
reply.writeString(DESCRIPTOR);
return true;
}
case TRANSACTION_getBookList:
{
data.enforceInterface(DESCRIPTOR);
java.util.List<com.example.bindertest.aidl.Book> _result = this.getBookList();
reply.writeNoException();
reply.writeTypedList(_result);
return true;
}
case TRANSACTION_addBook:
{
data.enforceInterface(DESCRIPTOR);
com.example.bindertest.aidl.Book _arg0;
if ((0!=data.readInt())) {
_arg0 = com.example.bindertest.aidl.Book.CREATOR.createFromParcel(data);
}
else {
_arg0 = null;
}
this.addBook(_arg0);
reply.writeNoException();
return true;
}
case TRANSACTION_registerListener:
{
data.enforceInterface(DESCRIPTOR);
com.example.bindertest.aidl.IOnNewBookArrived _arg0;
_arg0 = com.example.bindertest.aidl.IOnNewBookArrived.Stub.asInterface(data.readStrongBinder());
this.registerListener(_arg0);
reply.writeNoException();
return true;
}
case TRANSACTION_unregisterListener:
{
data.enforceInterface(DESCRIPTOR);
com.example.bindertest.aidl.IOnNewBookArrived _arg0;
_arg0 = com.example.bindertest.aidl.IOnNewBookArrived.Stub.asInterface(data.readStrongBinder());
this.unregisterListener(_arg0);
reply.writeNoException();
return true;
}
}
return super.onTransact(code, data, reply, flags);
}

接下来这一段,就是真正处理Binder,实现跨进程的方法。这个方法运行在服务端的Binder线程池中,当客户端发起请求的时候会通过系统底层封装好的方法之后交由此方法来处理。
让我们翻开源码看看究竟是怎么样一个过程:

    // Entry point from android_util_Binder.cpp's onTransact
    private boolean execTransact(int code, long dataObj, long replyObj,
            int flags) {
        Parcel data = Parcel.obtain(dataObj);
        Parcel reply = Parcel.obtain(replyObj);
        // theoretically, we should call transact, which will call onTransact,
        // but all that does is rewind it, and we just got these from an IPC,
        // so we'll just call it directly.
        boolean res;
        // Log any exceptions as warnings, don't silently suppress them.
        // If the call was FLAG_ONEWAY then these exceptions disappear into the ether.
        try {
            res = onTransact(code, data, reply, flags);
        } catch (RemoteException e) {
            if ((flags & FLAG_ONEWAY) != 0) {
                Log.w(TAG, "Binder call failed.", e);
            } else {
                reply.setDataPosition(0);
                reply.writeException(e);
            }
            res = true;
        } catch (RuntimeException e) {
            if ((flags & FLAG_ONEWAY) != 0) {
                Log.w(TAG, "Caught a RuntimeException from the binder stub implementation.", e);
            } else {
                reply.setDataPosition(0);
                reply.writeException(e);
            }
            res = true;
        } catch (OutOfMemoryError e) {
            // Unconditionally log this, since this is generally unrecoverable.
            Log.e(TAG, "Caught an OutOfMemoryError from the binder stub implementation.", e);
            RuntimeException re = new RuntimeException("Out of memory", e);
            reply.setDataPosition(0);
            reply.writeException(re);
            res = true;
        }
        checkParcel(this, code, reply, "Unreasonably large binder reply buffer");
        reply.recycle();
        data.recycle();

        // Just in case -- we are done with the IPC, so there should be no more strict
        // mode violations that have gathered for this thread.  Either they have been
        // parceled and are now in transport off to the caller, or we are returning back
        // to the main transaction loop to wait for another incoming transaction.  Either
        // way, strict mode begone!
        StrictMode.clearGatheredViolations();

        return res;
    }
}

虽然我暂时不知道从native下面怎么上来,但是不妨碍我们的理解。
注释上面说了, Entry point from android_util_Binder.cpp’s onTransact,从android_util_Binder.cpp的onTransact中循环调用了Java层次的execTransact().在execTransact中将传进来的长整形转化为Parcel序列化数据,再将Parcel的数据传入我们覆写的onTransact()中去,倘若出现了错误则返回Parcel数据队列的第0位,在reply中写入错误代码。

接着看看我们覆写的onTransact()的函数中就是,就是处理传过来的code,做Parcel的读取写入处理。这些code就是我们定义的:

static final int TRANSACTION_getBookList = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
static final int TRANSACTION_addBook = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
static final int TRANSACTION_registerListener = (android.os.IBinder.FIRST_CALL_TRANSACTION + 2);
static final int TRANSACTION_unregisterListener = (android.os.IBinder.FIRST_CALL_TRANSACTION + 3);

这里面FIRST_CALL_TRANSACTION就是在IBinder中定义好的参数,作为第一个可以被用户识别的命令,下面的函数是按照之前在AIDL声明的顺序逐个+1,当然不按照这个顺序也行,但是要在客户端定义好。

接着判断当每一个code的来临,即处理每一个自己声明的函数,进行处理。在本地服务端的时候,做以下几个事情:

case INTERFACE_TRANSACTION:
{
reply.writeString(DESCRIPTOR);
return true;
}
case TRANSACTION_getBookList:
{
data.enforceInterface(DESCRIPTOR);
java.util.List<com.example.bindertest.aidl.Book> _result = this.getBookList();
reply.writeNoException();
reply.writeTypedList(_result);
return true;
}
case TRANSACTION_addBook:
{
data.enforceInterface(DESCRIPTOR);
com.example.bindertest.aidl.Book _arg0;
if ((0!=data.readInt())) {
_arg0 = com.example.bindertest.aidl.Book.CREATOR.createFromParcel(data);
}
else {
_arg0 = null;
}
this.addBook(_arg0);
reply.writeNoException();
return true;
}

服务端的工作:
1.传来的是文件描述符的时候,在data中读入文件描述符。
2.传来的的是操作的时候,先读取data里面的文件描述符,reply写入本地的数据,调用本地在继承的子类中定义好的对应函数。若为Parcelable类型时,则要调用com.example.bindertest.aidl.Book.CREATOR.c-reateFromParcel(data);若无返回值的话,reply中不需要写入返回值。

如果都不是则交还给原生函数onTransact来处理。

源码如下:

    /**
     * Default implementation is a stub that returns false.  You will want
     * to override this to do the appropriate unmarshalling of transactions.
     *
     * <p>If you want to call this, call transact().
     */
    protected boolean onTransact(int code, Parcel data, Parcel reply,
            int flags) throws RemoteException {
        if (code == INTERFACE_TRANSACTION) {
            reply.writeString(getInterfaceDescriptor());
            return true;
        } else if (code == DUMP_TRANSACTION) {
            ParcelFileDescriptor fd = data.readFileDescriptor();
            String[] args = data.readStringArray();
            if (fd != null) {
                try {
                    dump(fd.getFileDescriptor(), args);
                } finally {
                    try {
                        fd.close();
                    } catch (IOException e) {
                        // swallowed, not propagated back to the caller
                    }
                }
            }
            // Write the StrictMode header.
            if (reply != null) {
                reply.writeNoException();
            } else {
                StrictMode.clearGatheredViolations();
            }
            return true;
        }
        return false;
    }

上面的代码操作有两个,第一个和我们自定义的操作一样读入文件描述符,如果code==DUMP_TRANSACTION,则调用dump函数,这个函数最后会调用BinderProxy中dump函数,这个函数为空,作用是为了让我们打印object的状态给输入的流。

在这里就将服务端的Binder解析结束了。接下来就是远程客户端的解析:

private android.os.IBinder mRemote;
Proxy(android.os.IBinder remote)
{
mRemote = remote;
}
@Override public android.os.IBinder asBinder()
{
return mRemote;
}
public java.lang.String getInterfaceDescriptor()
{
return DESCRIPTOR;
}

首先是这三个由于在上面的Binder从最早的判断返回了,知道这是获取Binder以及文件描述符就OK了。这也就引出了当我们要远程调用接口的时候,要做一次asInterface的判断,是IBookManager的类还是内部类。

接下来才是最重要的工作,作为客户端首先要接受来自Parcelable的数据,之后需要返回再返回。
取出一截来说明:

@Override public java.util.List<com.example.bindertest.aidl.Book> getBookList() throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
java.util.List<com.example.bindertest.aidl.Book> _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
mRemote.transact(Stub.TRANSACTION_getBookList, _data, _reply, 0);
_reply.readException();
_result = _reply.createTypedArrayList(com.example.bindertest.aidl.Book.CREATOR);
}
finally {
_reply.recycle();
_data.recycle();
}
return _result;
}


@Override public void addBook(com.example.bindertest.aidl.Book book) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
try {
_data.writeInterfaceToken(DESCRIPTOR);
if ((book!=null)) {
_data.writeInt(1);
book.writeToParcel(_data, 0);
}
else {
_data.writeInt(0);
}
mRemote.transact(Stub.TRANSACTION_addBook, _data, _reply, 0);
_reply.readException();
}
finally {
_reply.recycle();
_data.recycle();
}
}

我们要在Proxy中实现每一个在aidl也就是Binder中作为通信角色的函数。
首先要声明两个Parcel的类去获取传过来的Parcel参数,以及定义一个需要返回类型。
第一步:
_data.writeInterfaceToken(DESCRIPTOR);的作用是写入要传去服务端的IInterface的数据。这个文件描述符就相当于socket中的公开IP一样。
reply则是里面
可以看见在addBook中_data.writeInterfaceToken(DESCRIPTOR);加入了一个协议,加入Book不为空,data写入1;相对应的在服务端我们可以看到对应的code一个判断,data不为0的时候创建一个Parcelable的序列出来。
第二步:
Binder.Proxy类型的mRemote调用transact这个方法。这个方法也就是客户端传到服务端的核心方法,调用的是BinderProxy这个内部类的方法。
看看这个源码是怎么写的:

    public boolean transact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
        Binder.checkParcel(this, code, data, "Unreasonably large binder buffer");
        return transactNative(code, data, reply, flags);
    }

很遗憾,在源码中,这里面已经将Binder的工作封装到了transactNative(code, data, reply, flags)函数中去了,但是我们可以推测,这是将Parcel数据传进native层次,在服务端又通过execTransact提取出来。

继续分析,reply在服务端写入了异常,在客户端一样也要读出来。官方的解释是不能让潜在的异常逐步增大。

最后再回收Parcel资源。这就将Binder分析结束了。

下面是总结的分析图:
Binder在Java框架的流程

下一篇再继续说Binder的使用。

应该没人转载这篇渣渣写的东西吧,还是说一声转载请写出出处,菜鸟Y

感谢任玉刚大神的android开发艺术探索,以及罗老师的:老罗的Binder学习计划

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值