了解Binder

熟悉AIDL的都知道,AIDL使用有比较固定的流程,可能大概也许是这样子的:1、书写一个aidl文件,比如叫作IMyAidlInterface.aidl,写上需要的接口方法,如果有自定义类型参数,必须实现Parcelable接口,并且增加对应的aidl文件;

2、新建一个Service,写一个类继承IMyAidlInterface.Stub类,对接口方法进行实现,并且在onBind()方法返回一个它的实例,本质上它是一个Binder对象;

3、在Activity中采用bindService()绑定服务,实现参数ServiceConnection的onServiceConnected()和onServiceDisconnected()方法;

4、在onServiceConnected()方法将IBinder实例采用IMyAidlInterface.Stub.asInterface()方法包装成IMyAidlInterface接口对象,得到对象之后我们就可以实现服务的方法调用了。

来个实例:

第一步、写个aidl文件,写上需要的接口方法。

// IStudentService.aidl
package com.example.administrator.study;
import com.example.administrator.study2.StudentInfo;

interface IStudentService {
    StudentInfo getStudentInfo();
    void setStudentInfo(in StudentInfo info);
    String getName();
    void setName(String name);
}

其中有自定义参数StudentInfo,它需要实现Parcelable接口,并且还需要增加StudentInfo.aidl文件。

// StudentInfo.java
package com.example.administrator.study2;

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

/**
* Created by Administrator on 2018/7/12 0012.
*/

public class StudentInfo implements Parcelable{
    private int age;
    private String grade;
    private int _class;


    public StudentInfo(int age, String grade, int _class) {
        this.age = age;
        this.grade = grade;
        this._class = _class;
    }

    @Override
    public String toString() {
        return "StudentInfo{" +
        "age=" + age +
        ", grade='" + grade + '\'' +
        ", _class=" + _class +
        '}';
    }

    protected StudentInfo(Parcel in) {
        age = in.readInt();
        grade = in.readString();
        _class = in.readInt();
    }

    public static final Creator<StudentInfo> CREATOR = new Creator<StudentInfo>() {
        @Override
        public StudentInfo createFromParcel(Parcel in) {
            return new StudentInfo(in);
        }

        @Override
        public StudentInfo[] newArray(int size) {
            return new StudentInfo[size];
         }
    };

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(age);
        dest.writeString(grade);
        dest.writeInt(_class);
    }
}
// IStudent.aidl
package com.example.administrator.study2;

parcelable StudentInfo;

StudentInfo需要对Parcelable接口进行实现,不过我们写好需要的属性之后AS是可以自动生成的。需要注意的是StudentInfo.java需要与StudentInfo.aidl在同一个包下,之所以需要在同一个包,是因为IStudentService.aidl在生成IStudentService.java的时候,会引入同名包下对应的类,比如上面的getStudentInfo()方法和setStudentInfo()方法在IStudentService.java中是这样的。

public com.example.administrator.study2.StudentInfo getStudentInfo() throws android.os.RemoteException;
public void setStudentInfo(com.example.administrator.study2.StudentInfo info) throws android.os.RemoteException;

如果com.example.administrator.study2包下没有对应的StudentInfo类,那么就会报编译错误。

第二步、写一个类继承IStudentService.Stub类,并且在Serviced的onBind()方法中返回它的实例。

public class StudentService extends Service{
    private StudentInfo mInfo;
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return new IStudentService.Stub() {

            @Override
            public com.example.administrator.study2.StudentInfo getStudentInfo() throws RemoteException {
                return mInfo;
            }

            @Override
            public void setStudentInfo(com.example.administrator.study2.StudentInfo info) throws RemoteException {
                mInfo = info;
            }

            public String getName() throws RemoteException {
                return mName;
            }

            @Override
            public void setName(String name) throws RemoteException {
                mName = name;
            }
        };
    }
}

第三步、在Activity中bindService绑定服务,实现ServiceConnection的ServiceConnected()方法,得到Binder对象,并且转成IStudentService接口对象,之后就可以进行方法调用了。

bindService(new Intent(this, StudentService.class), new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        iStudentService = IStudentService.Stub.asInterface(service);
        try {
            iStudentService.setStudentInfo(new StudentInfo(12,"大一",5));
            Toast.makeText(Main3Activity.this, iStudentService.getStudentInfo().toString(), Toast.LENGTH_SHORT).show();
        } catch (RemoteException e) {
            e.printStackTrace();
        }
     }

     @Override
     public void onServiceDisconnected(ComponentName name) {
        iStudentService = null
     }
}, BIND_AUTO_CREATE);

运行之后正常弹出Toast显示StudentInfo{age=12, grade='大一', _class=5},流程顺利。

 

以上代码很多人都非常熟悉了,是我们使用aidl实现跨进程通讯的方式。但其实aidl只是一种接口定义语言,它是用来生成Android进程间通信代码的,真正实现进程间通信的是Binder机制。那Binder到底在哪里,它做了什么,我们可以直接用Binder来进行进程间通讯吗,我也很想知道,接下来我们就来分析一下。

我们来看看自动生成的IStudentService.java类。

package com.example.administrator.study;

public interface IStudentService extends android.os.IInterface {
    /**
     * Local-side IPC implementation stub class.
     */
    public static abstract class Stub extends android.os.Binder implements com.example.administrator.study.IStudentService {
        private static final java.lang.String DESCRIPTOR = "com.example.administrator.study.IStudentService";

        /**
         * Construct the stub at attach it to the interface.
         */
        public Stub() {
            this.attachInterface(this, DESCRIPTOR);
        }

        /**
         * Cast an IBinder object into an com.example.administrator.study.IStudentService interface,
         * generating a proxy if needed.
         */
        public static com.example.administrator.study.IStudentService asInterface(android.os.IBinder obj) {
            if ((obj == null)) {
                return null;
            }
            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
            if (((iin != null) && (iin instanceof com.example.administrator.study.IStudentService))) {
                return ((com.example.administrator.study.IStudentService) iin);
            }
            return new com.example.administrator.study.IStudentService.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_getStudentInfo: {
                    data.enforceInterface(DESCRIPTOR);
                    com.example.administrator.study2.StudentInfo _result = this.getStudentInfo();
                    reply.writeNoException();
                    if ((_result != null)) {
                        reply.writeInt(1);
                        _result.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
                    } else {
                        reply.writeInt(0);
                    }
                    return true;
                }
                case TRANSACTION_setStudentInfo: {
                    data.enforceInterface(DESCRIPTOR);
                    com.example.administrator.study2.StudentInfo _arg0;
                    if ((0 != data.readInt())) {
                        _arg0 = com.example.administrator.study2.StudentInfo.CREATOR.createFromParcel(data);
                    } else {
                        _arg0 = null;
                    }
                    this.setStudentInfo(_arg0);
                    reply.writeNoException();
                    return true;
                }
                case TRANSACTION_getName: {
                    data.enforceInterface(DESCRIPTOR);
                    java.lang.String _result = this.getName();
                    reply.writeNoException();
                    reply.writeString(_result);
                    return true;
                }
                case TRANSACTION_setName: {
                    data.enforceInterface(DESCRIPTOR);
                    java.lang.String _arg0;
                    _arg0 = data.readString();
                    this.setName(_arg0);
                    reply.writeNoException();
                    return true;
                }
            }
            return super.onTransact(code, data, reply, flags);
        }

        private static class Proxy implements com.example.administrator.study.IStudentService {
            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 com.example.administrator.study2.StudentInfo getStudentInfo() throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                com.example.administrator.study2.StudentInfo _result;
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    mRemote.transact(Stub.TRANSACTION_getStudentInfo, _data, _reply, 0);
                    _reply.readException();
                    if ((0 != _reply.readInt())) {
                        _result = com.example.administrator.study2.StudentInfo.CREATOR.createFromParcel(_reply);
                    } else {
                        _result = null;
                    }
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
                return _result;
            }

            @Override
            public void setStudentInfo(com.example.administrator.study2.StudentInfo info) 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 ((info != null)) {
                        _data.writeInt(1);
                        info.writeToParcel(_data, 0);
                    } else {
                        _data.writeInt(0);
                    }
                    mRemote.transact(Stub.TRANSACTION_setStudentInfo, _data, _reply, 0);
                    _reply.readException();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
            }

            @Override
            public java.lang.String getName() throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                java.lang.String _result;
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    mRemote.transact(Stub.TRANSACTION_getName, _data, _reply, 0);
                    _reply.readException();
                    _result = _reply.readString();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
                return _result;
            }

            @Override
            public void setName(java.lang.String name) 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.writeString(name);
                    mRemote.transact(Stub.TRANSACTION_setName, _data, _reply, 0);
                    _reply.readException();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
            }
        }

        static final int TRANSACTION_getStudentInfo = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
        static final int TRANSACTION_setStudentInfo = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
        static final int TRANSACTION_getName = (android.os.IBinder.FIRST_CALL_TRANSACTION + 2);
        static final int TRANSACTION_setName = (android.os.IBinder.FIRST_CALL_TRANSACTION + 3);
    }

    public com.example.administrator.study2.StudentInfo getStudentInfo() throws android.os.RemoteException;

    public void setStudentInfo(com.example.administrator.study2.StudentInfo info) throws android.os.RemoteException;

    public java.lang.String getName() throws android.os.RemoteException;

    public void setName(java.lang.String name) throws android.os.RemoteException;
}

我们来分析一下这个类的结构。最外层是一个接口IStudentService,它继承了接口android.os.IInterface。IStudentService有四个方法,在文件的最后,跟我们在aidl文件中写的是一样的。

接口内有个静态内部类Stub,它继承了Binder类同时实现了IStudentService接口。

Stub类里面也有一个静态内部类Proxy,它仅仅实现了IStudentService接口。

它们三个虽然定义在一个文件内,但本质上是相互独立的。Proxy是运行在客户端的代码。Stub是运行在服务端的代码。aidl通信的实质就是客户端通过Proxy去发送请求调用,服务端通过Stub做出自己的响应处理。Proxy和Stub都实现了IStudentService接口,因此他们的调用可以“无缝连接”,即Proxy所发出的调用都可以在Stub中找到对应的处理方法。

一张图表示一下这个关系。

让我们来跟踪一下流程。

客户端中,bindService()后我们在onServiceConnected()方法中我们采用IStudentService.Stub.asInterface()将IBinder对象转化成了IStudentService对象

bindService(new Intent(this, StudentService.class), new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        iStudentService = IStudentService.Stub.asInterface(service);
        try {
            iStudentService.setStudentInfo(new StudentInfo(12,"大一",5));
            Toast.makeText(Main3Activity.this, iStudentService.getStudentInfo().toString(), Toast.LENGTH_SHORT).show();
        } catch (RemoteException e) {
            e.printStackTrace();
        }
     }

     @Override
     public void onServiceDisconnected(ComponentName name) {
        iStudentService = null
     }
}, BIND_AUTO_CREATE);

asInterface()的实现是这样子的。

/**
* Cast an IBinder object into an com.example.administrator.study.IStudentService interface,
* generating a proxy if needed.
*/
public static com.example.administrator.study.IStudentService asInterface(android.os.IBinder obj) {
    if ((obj == null)) {
        return null;
    }
    android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
    if (((iin != null) && (iin instanceof com.example.administrator.study.IStudentService))) {
        return ((com.example.administrator.study.IStudentService) iin);
    }
    return new com.example.administrator.study.IStudentService.Stub.Proxy(obj);
}

前面一部分是查询本进程有没有该接口对象,如果是同一个进程就会走 iin不为空的判断,我们先不管,来看最后一句。最后一句创建了Proxy对象,并且把IBinder对象传入,然后返回。

来到Proxy的构造方法,对参数IBinder的处理只是保存了它的引用而已,其实Proxy使用了代理模式,Proxy的方法将会通过IBinder的方法来实现。

private static class Proxy implements com.example.administrator.study.IStudentService {
    private android.os.IBinder mRemote;

    Proxy(android.os.IBinder remote) {
        mRemote = remote;
    }
    ……
}

asInterface()方法结束了,我们得到一个Proxy对象,接着我们调用setStudentInfo方法,给Service设置了一个StudentInfo对象。

@Override
public void onServiceConnected(ComponentName name, IBinder service) {
    IStudentService iStudentService = IStudentService.Stub.asInterface(service);
    try {
        iStudentService.setStudentInfo(new StudentInfo(12,"大一",5));
        Toast.makeText(Main3Activity.this, iStudentService.getStudentInfo().toString(), Toast.LENGTH_SHORT).show();
    } catch (RemoteException e) {
        e.printStackTrace();
    }
}

setStudentInfo在Proxy的实现是这样的。

@Override
public void setStudentInfo(com.example.administrator.study2.StudentInfo info) 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 ((info != null)) {
        _data.writeInt(1);
        info.writeToParcel(_data, 0);
    } else {
        _data.writeInt(0);
    }
        mRemote.transact(Stub.TRANSACTION_setStudentInfo, _data, _reply, 0);
        _reply.readException();
    } finally {
        _reply.recycle();
        _data.recycle();
    }
}

首先创建了两个Parcel对象,可以理解_data作为参数值,_reply作为返回值。首先用_data写了一些东西,我们先不看,写完之后调用了

mRemote.transact(Stub.TRANSACTION_setStudentInfo, _data, _reply, 0);

transact方法的声明如下。

/**
* Perform a generic operation with the object.
*
* @param code The action to perform. This should
* be a number between {@link #FIRST_CALL_TRANSACTION} and
* {@link #LAST_CALL_TRANSACTION}.
* @param data Marshalled data to send to the target. Must not be null.
* If you are not sending any data, you must create an empty Parcel
* that is given here.
* @param reply Marshalled data to be received from the target. May be
* null if you are not interested in the return value.
* @param flags Additional operation flags. Either 0 for a normal
* RPC, or {@link #FLAG_ONEWAY} for a one-way RPC.
*/
public boolean transact(int code, Parcel data, Parcel reply, int flags) throws RemoteException;

可以理解为code表示标识,data表示参数,reply表示返回值,flags是标志(不是0就是FLAG_ONEWAY,FLAG_ONEWAY的值是1)。

查看其它方法,流程也差不多,Proxy本身并没有提供具体逻辑,而是委托给Binder的transact()方法加上Parcel的方法实现。

transact方法的实现就不再细究,它会调用到本地方法,它的效果是引起服务端onTransact的回调。即我们在客户端调用transact方法之后,服务端Binder的onTransact就会得到调用,这个跨进程通信是Binder机制实现的。至于Binder是如何实现跨进程通信的,大概的原理是Android系统的内存空间是分为用户空间和内核空间,用户空间不同进程之间是不能共享的,而内核空间却是共享的。客户端进程向服务端进程发起通信,就是利用进程间可共享的内核空间来完成底层通信工作。对内核空间中驱动的交互往往采用ioctl等方法,客户端将Parcel发送给内核中的Binder Driver,Server会读取Binder Driver中的请求数据,如果是发送给自己的,解包Parcel对象,处理并将数据返回。大概是这样子,具体不是很懂,熟悉内核驱动的就会对这个非常熟了,想知道具体的可以详细查查这部分的知识。

接下来我们就来看看服务端是如何setStudentInfo()做出响应。前面说了transact()方法会引起服务端onTransact()的回调。

@Overridepublic boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {  
  
    switch (code) {        
        ……
        case TRANSACTION_setStudentInfo: {
            data.enforceInterface(DESCRIPTOR);
            com.example.administrator.study2.StudentInfo _arg0;
            if ((0 != data.readInt())) {
                _arg0 = com.example.administrator.study2.StudentInfo.CREATOR.createFromParcel(data);
            } else {
                _arg0 = null;
            }
            this.setStudentInfo(_arg0);
            reply.writeNoException();
            return true;
        }
        ……
    }
    return super.onTransact(code, data, reply, flags);
}

onTransact()也有跟transact()同样的四个参数,并且是一一对应的。首先对code进行switch处理,执行setStudentInfo()相关的逻辑。大致过程是提取参数data这个Parcel的数据,即setStudentInfo()时传入的StudentInfo对象,然后调用setStudentInfo()方法,这个方法在Stub中是个空实现,这就是为什么Stub是抽象的,它的方法要交给具体的子类实现,也就是我们在onBind方法中返回Binder对象。

@Override
public IBinder onBind(Intent intent) {
    return new IStudentService.Stub() {

        @Override
        public com.example.administrator.study2.StudentInfo getStudentInfo() throws RemoteException {
            return mInfo;
        }

        @Override
        public void setStudentInfo(com.example.administrator.study2.StudentInfo info) throws RemoteException {
            mInfo = info;
        }

        @Override
        public String getName() throws RemoteException {
            return mName;
        }

        @Override
        public void setName(String name) throws RemoteException {
            mName = name;
        }
    };
}

这样就实现了对应方法的调用,数据在其中的传递过程是采用Parcel的writeXXX和readXXX方法,内部实现也是native方法,并且它们在transact和onTransact的读取和写入的顺序是一一对应的。

所以总结一下Binder通信的过程就是,客户端采用Parcel写入各种数据,然后调用transact,此时会阻塞,等待服务端onTransact执行完成,接着从另一个Parcel中取得返回的数据。

服务端在onTransact中采用Parcel读取数据,然后执行具体逻辑,执行完后采用另一个Parcel写回返回值。

 

既然是这样,那是否我们实现Binder通信也可以不用借助aidl,直接把Binder拿来使用,调用它的transact方法实现进程间通信。答案当然是可以的。

首先直接在Service的onBind方法创建一个Binder对象,并且实现它的onTransact方法。

@Override
public IBinder onBind(Intent intent) {
    return new Binder(){
        @Override
        protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
            switch (code) {
                //get
                case 2333: {
                    reply.writeString(mName);
                    reply.writeInt(mAge);
                    return true;
                }
                //set
                case 2335: {
                    String name = data.readString();
                    int age = data.readInt();
                    mName = name;
                    mAge = age;
                    return true;
                }
            }
            return super.onTransact(code, data, reply, flags);
        }
    };
}

简简单单,set流程中我们从data读数据然后保存,get流程中我们直接往replay中写入数据。

客户端中,我们直接拿onServiceConnected的Binder来调用transact()方法。

@Override
public void onServiceConnected(ComponentName name, IBinder service) {
    //set过程
    Parcel _data = Parcel.obtain();
    Parcel _reply = Parcel.obtain();
    try {
        _data.writeString("zjb");
        _data.writeInt(88);
        service.transact(2335, _data, _reply, 0);
    } catch (RemoteException e) {
        e.printStackTrace();
    } finally {
        _reply.recycle();
        _data.recycle();
    }

    //get过程
    Parcel _data_2 = Parcel.obtain();
    Parcel _reply_2 = Parcel.obtain();
    try {
        service.transact(2333, _data_2, _reply_2, 0);
        String _name = _reply_2.readString();
        int _age = _reply_2.readInt();
        Toast.makeText(MainActivity.this, _name + " : " + _age, Toast.LENGTH_SHORT).show();
    } catch (RemoteException e) {
        e.printStackTrace();
    } finally {
        _reply_2.recycle();
        _data_2.recycle();
    }
}

set过程,创建两个Parcel对象,一个是用来传递数据的,一个是用来得到返回值的。把需要传递的参数采用Parcel的write方法一一写入,然后调用transact()方法,这就完成了set方法的调用。get过程,创建两个Parcel对象,因为没有数据传递,所以直接transact()获取到返回的Parcel _reply_2,采用readXXX()方法读取它的数据。

运行程序,正常弹出 zjb : 88 的Toast,证明调用成功了,我们也看到,采用这种方法,我们也可以在不自定义类型的情况下实现一次方法调用返回两个值。运行成功也证明aidl就是这样工作的,只不过它封装了流程而已。

补充一下,客户端得到的Binder和服务端返回的Binder是不是同一个对象,答案是:服务与Activity在同一个进程时是的,如果不在同个进程则不是。

虽然标题叫了解Binder,但只是说了一下它的用法,要具体了解,要深入的还有很多。不过知道这部分内容,当我们再看ActivityManagerService和ActivityThread通信的部分源码的时候就没有那么难理解了。这也是本文的一个小小目的吧。

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
千里马8年Android系统及应用开发经验,曾担任过美国unokiwi公司移动端技术总监兼架构师,对系统开发,性能优化,应用高级开发有深入的研究,Android开源定制ROM Lineage的贡献者之一,国内首家线下开辟培训Android Framework课程,拥有2年的Android系统培训经验。成为腾讯课堂专业负责android framework课程分享第一人,致力于提高国内android Framework水平Android Framework领域内是国内各大手机终端科技公司需要的人才,应用开发者都对Android系统充满着好奇,其中的binder是重中之重,都说无binderAndroidbinde是Android系统的任督二脉。课程水平循序渐进,由中级再到高级,满足各个层次水平的android开发者。1、灵活使用binder跨进程通信,在app端对它的任何api方法等使用自如2、可以单独分析android系统源码中任何binder部分,分析再也没有难度3、掌握binder驱动本质原理,及对应binder驱动怎么进行跨进程通信,及内存等拷贝方式数据等4、对binder从上层的java app端一直到最底层的内核binder驱动,都可以顺利理通5、针对系统开发过程中遇到的binder报错等分析方法,及binder bug案例学习6、针对面试官任何的binder问题都可以对答自如7、socket这种跨进程通信实战使用8、针对android源码中使用的socket源码轻松掌握9、android系统源码中最常见的socketpair中双向跨进程通信10、使用socket实现一个可以让app执行shell命令的程序
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值