AIDL自动生成源码分析以及根据自动生成的源码来自定义类实现IPC

前言

今天要写的就是AIDL的具体使用、AIDL自动生成源码分析以及根据自动生成的源码自定义类来实现IPC。这篇博客的着重点就在于源码分析和自定义类,至于使用过程中的细节并没有介绍。开始切入正题!

服务端进程使用

在服务端进程中我们定义AIDL文件,以学生类为例,我们对外接口就是IStudentManager.aidl文件,自定义类AIDL声明文件就是Student.aidl文件,对应我们实体类就是Student,三者代码如下:
Student.java:

package com.lgy.aidl.aidl;

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

public class Student implements Parcelable {

    private String mNo;
    private String mName;
    private String mSex;
    private String mTel;

    private Student(Parcel in) {
        mNo = in.readString();
        mName = in.readString();
        mSex = in.readString();
        mTel = in.readString();
    }

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

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

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

    @Override
    public void writeToParcel(Parcel parcel, int i) {
        parcel.writeString(mNo);
        parcel.writeString(mName);
        parcel.writeString(mSex);
        parcel.writeString(mTel);
    }

    public String getmNo() {
        return mNo;
    }

    public void setmNo(String mNo) {
        this.mNo = mNo;
    }

    public String getmName() {
        return mName;
    }

    public void setmName(String mName) {
        this.mName = mName;
    }

    public String getmSex() {
        return mSex;
    }

    public void setmSex(String mSex) {
        this.mSex = mSex;
    }

    public String getmTel() {
        return mTel;
    }

    public void setmTel(String mTel) {
        this.mTel = mTel;
    }
}

Student.aidl:

// Student.aidl
package com.lgy.aidl.aidl;

import com.lgy.aidl.aidl.Student;

parcelable Student;

IStudentManager.aidl:

// IStudentManager.aidl
package com.lgy.aidl.aidl;

import com.lgy.aidl.aidl.Student;

interface IStudentManager {

    List<Student>  getAllStudent();

    void addStudent(in Student stu);

}

此时我们程序的结构图:
这里写图片描述

我们在服务端进程中开启Service,在Service中返回给客户端进程IBinder对象,此时的IBinder对象就是我们刚刚IStudentManager.aidl文件生成源码中静态内部类Stub,客户端拿到服务端IBinder对象,就可以进行进程间的通信了。
Service代码:

package com.lgy.aidl;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;

import com.lgy.aidl.aidl.IStudentManager;
import com.lgy.aidl.aidl.Student;

import java.util.ArrayList;
import java.util.List;

public class MyService extends Service {

    private static final String TAG = "MyService";

    private ArrayList<Student> mList = new ArrayList<>();


    private IStudentManager.Stub mBinder = new IStudentManager.Stub() {
        @Override
        public List<Student> getAllStudent() throws RemoteException {
            Log.i(TAG,"客户端调用getAllStudent方法执行,学生人数" + mList.size());
            return mList;
        }

        @Override
        public void addStudent(Student stu) throws RemoteException {
            Log.i(TAG,"客户端调用addStudent方法执行之前学生人数" + mList.size());
            mList.add(stu);
            Log.i(TAG, "客户端调用addStudent方法执行之后学生人数" + mList.size());
        }
    };


    public MyService() {
    }

    @Override
    public void onCreate() {
        super.onCreate();
        Student stu1 = new Student("1","张三","男","13718447489");
        Student stu2 = new Student("2","李四","男","13699485793");
        Student stu3 = new Student("3","貂蝉","女","15896345892");
        mList.add(stu1);
        mList.add(stu2);
        mList.add(stu3);

        Log.i(TAG,"服务端Service启动!当前总共有" + mList.size() + "名学生");
    }

    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }
}

到此服务的进程代码编写完毕!

客户端进程使用

我们将服务端进程中所需要的AIDL文件和相关自定义的实体类,原封不动的移动到客户端进程程序中。我们在客户端进程中只需要绑定Service,获取拿到服务端的IBinder对象即可。

package com.lgy.aidl;

import android.app.Activity;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.support.v7.app.ActionBarActivity;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;

import com.lgy.aidl.aidl.IStudentManager;
import com.lgy.aidl.aidl.Student;

import java.util.ArrayList;

public class MainActivity extends Activity {

    public static final String TAG = "MainActivity";

    private IStudentManager mStudentManager;

    private ArrayList<Student> mList ;


    private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
            Log.i(TAG, "客户端进程连接成功");
            mStudentManager = IStudentManager.Stub.asInterface(iBinder);
            try {
                mList = (ArrayList<Student>) mStudentManager.getAllStudent();
                Log.i(TAG,"客户端进程连接成功之后,返回的学生人数" + mList.size());
            } catch (RemoteException e) {
                e.printStackTrace();
            }

            Student stu4 = new Student("4","Tom","男","15678958962");
            Student stu5 = new Student("5","lily","女","18963526354");


            try{
                mStudentManager.addStudent(stu4);
                mStudentManager.addStudent(stu5);
            }catch (RemoteException e){
                e.printStackTrace();
            }

            try {
                mList = (ArrayList<Student>) mStudentManager.getAllStudent();
                Log.i(TAG, "添加了2名学生后,返回的学生人数" + mList.size());
            } catch (RemoteException e) {
                e.printStackTrace();
            }

        }

        @Override
        public void onServiceDisconnected(ComponentName componentName) {

        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Intent intent = new Intent(this,MyService.class);
        bindService(intent,mConnection,BIND_AUTO_CREATE);
    }

    @Override
    protected void onDestroy() {
        unbindService(mConnection);
        super.onDestroy();
    }
}

好了,到此客户端进程程序就写好,我们运行一下程序看看我们打印的log,和我们预想的是否一致。
这里写图片描述
这里写图片描述

AIDL生成代码分析

现在我们分析一下我们写的AIDL文件生成的Java类:

/*
 * This file is auto-generated.  DO NOT MODIFY.
 * Original file: E:\\sunshine\\lgy\\aidl\\app\\src\\main\\aidl\\com\\lgy\\aidl\\aidl\\IStudentManager.aidl
 */
package com.lgy.aidl.aidl;

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

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

        /**
         * Cast an IBinder object into an com.lgy.aidl.aidl.IStudentManager interface,
         * generating a proxy if needed.
         */
        public static com.lgy.aidl.aidl.IStudentManager asInterface(android.os.IBinder obj) {
            if ((obj == null)) {
                return null;
            }
            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
            if (((iin != null) && (iin instanceof com.lgy.aidl.aidl.IStudentManager))) {
                return ((com.lgy.aidl.aidl.IStudentManager) iin);
            }
            return new com.lgy.aidl.aidl.IStudentManager.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_getAllStudent: {
                    data.enforceInterface(DESCRIPTOR);
                    java.util.List<com.lgy.aidl.aidl.Student> _result = this.getAllStudent();
                    reply.writeNoException();
                    reply.writeTypedList(_result);
                    return true;
                }
                case TRANSACTION_addStudent: {
                    data.enforceInterface(DESCRIPTOR);
                    com.lgy.aidl.aidl.Student _arg0;
                    if ((0 != data.readInt())) {
                        _arg0 = com.lgy.aidl.aidl.Student.CREATOR.createFromParcel(data);
                    } else {
                        _arg0 = null;
                    }
                    this.addStudent(_arg0);
                    reply.writeNoException();
                    return true;
                }
            }
            return super.onTransact(code, data, reply, flags);
        }

        private static class Proxy implements com.lgy.aidl.aidl.IStudentManager {
            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.lgy.aidl.aidl.Student> getAllStudent() throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                java.util.List<com.lgy.aidl.aidl.Student> _result;
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    mRemote.transact(Stub.TRANSACTION_getAllStudent, _data, _reply, 0);
                    _reply.readException();
                    _result = _reply.createTypedArrayList(com.lgy.aidl.aidl.Student.CREATOR);
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
                return _result;
            }

            @Override
            public void addStudent(com.lgy.aidl.aidl.Student stu) 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 ((stu != null)) {
                        _data.writeInt(1);
                        stu.writeToParcel(_data, 0);
                    } else {
                        _data.writeInt(0);
                    }
                    mRemote.transact(Stub.TRANSACTION_addStudent, _data, _reply, 0);
                    _reply.readException();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
            }
        }

        static final int TRANSACTION_getAllStudent = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
        static final int TRANSACTION_addStudent = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
    }

    **public java.util.List<com.lgy.aidl.aidl.Student> getAllStudent() throws android.os.RemoteException;
    public void addStudent(com.lgy.aidl.aidl.Student stu) throws android.os.RemoteException;**
}

从上面自动生成的代码来看,IStudentManager.java类,其实就是一个接口interface、实现了IInterface接口,在里面有2个我们定义的方法getAllStudent和addStudent(代码中粗色部分)。在该接口中有一个静态内部类Stub 继承Binder类、实现了IStudentManager接口。
现在我们从哪里讲呢?我们在客户端进程的编写过程中,在绑定Service的时候,创建连接的时候,我们有这么一句:

mStudentManager = IStudentManager.Stub.asInterface(iBinder);

好了,我们看看这个静态内部类Stub的静态内部方法asInterface是如何实现的?

if ((obj == null)) {
    return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin != null) && (iin instanceof com.lgy.aidl.aidl.IStudentManager))) {
    return ((com.lgy.aidl.aidl.IStudentManager) iin);
}
return new com.lgy.aidl.aidl.IStudentManager.Stub.Proxy(obj);

源码中首先判断参数IBinder是否为null, 接着调用Binder对象的queryLocalInterface方法,这个方法又是做什么的呢?
我们继续查看源代码:

/**
 * Use information supplied to attachInterface() to return the
 * associated IInterface if it matches the requested
 * descriptor.
 */
public IInterface queryLocalInterface(String descriptor) {
    if (mDescriptor.equals(descriptor)) {
        return mOwner;
    }
    return null;
}

从源代码中看很简单,判断descriptor字符串和自己内部的字符串相等还是不相等,返回的是一个mOwner对象,那么这个mOwner对象是IInterface接口,如下:

private int mObject;
private IInterface mOwner;
private String mDescriptor;

我们就要在去看源码中的构造函数Stub()如下:

        public Stub() {
            this.attachInterface(this, DESCRIPTOR);
        }

继续看attachInterface方法如下:

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

看到这里我想我们应该知道queryLocalInterface的含义了?就是查找当前进程是否和服务端进程在同一个程序进程中,如果在同一个进程的话,那么就直接返回找到自身mOwner IInterface对象 如果不在同一个进程的话,就返回给null。那么最终会返回给我们一个Stub类中的静态代理类Proxy。 也就是说代理类Proxy才是真正的实现IPC的关键!。
好了,我知道了这个代理类Proxy,那么他又是如何实现的呢?
第一:代理类也实现了IStudentManager接口,所以他也会有2个方法去实现。
第二:代理类中的构造和私有函数传递的就是我们IStudentManager.Stub.asInterface(iBinder); IBinder对象。

我们看看两个方法的实现:

@Override
            public java.util.List<com.lgy.aidl.aidl.Student> getAllStudent() throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                java.util.List<com.lgy.aidl.aidl.Student> _result;
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    mRemote.transact(Stub.TRANSACTION_getAllStudent, _data, _reply, 0);
                    _reply.readException();
                    _result = _reply.createTypedArrayList(com.lgy.aidl.aidl.Student.CREATOR);
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
                return _result;
            }
@Override
            public void addStudent(com.lgy.aidl.aidl.Student stu) 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 ((stu != null)) {
                        _data.writeInt(1);
                        stu.writeToParcel(_data, 0);
                    } else {
                        _data.writeInt(0);
                    }
                    mRemote.transact(Stub.TRANSACTION_addStudent, _data, _reply, 0);
                    _reply.readException();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
            }

在方法的源码实现中我们可以发现就是参数 是否回应,结果的返回都是序列化的Parcel。
在这2个方法中:
mRemote.transact(Stub.TRANSACTION_getAllStudent, _data, _reply, 0);
mRemote.transact(Stub.TRANSACTION_addStudent, _data, _reply, 0);
这2句才是真正的在多进程间数据通信。
在transact方法中:

/**
     * Default implementation rewinds the parcels and calls onTransact.  On
     * the remote side, transact calls into the binder to do the IPC.
     */
    public final boolean transact(int code, Parcel data, Parcel reply,
            int flags) throws RemoteException {
        if (false) Log.v("Binder", "Transact: " + code + " to " + this);
        if (data != null) {
            data.setDataPosition(0);
        }
        boolean r = onTransact(code, data, reply, flags);
        if (reply != null) {
            reply.setDataPosition(0);
        }
        return r;
    }

调用onTransact方法此时调用的是服务端的onTransact方法,因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_getAllStudent: {
                    data.enforceInterface(DESCRIPTOR);
                    java.util.List<com.lgy.aidl.aidl.Student> _result = this.getAllStudent();
                    reply.writeNoException();
                    reply.writeTypedList(_result);
                    return true;
                }
                case TRANSACTION_addStudent: {
                    data.enforceInterface(DESCRIPTOR);
                    com.lgy.aidl.aidl.Student _arg0;
                    if ((0 != data.readInt())) {
                        _arg0 = com.lgy.aidl.aidl.Student.CREATOR.createFromParcel(data);
                    } else {
                        _arg0 = null;
                    }
                    this.addStudent(_arg0);
                    reply.writeNoException();
                    return true;
                }
            }
            return super.onTransact(code, data, reply, flags);
        }

从onTransact源码中我们可以看到参数,是否回应,结果都会作为参数传递过来,然后调用相对应的方法,将结果放回对应的参数中去,客户端进程在获取拿到对应的结果返回。

_reply.readException();
_result = _reply.createTypedArrayList(com.lgy.aidl.aidl.Student.CREATOR);

从而整个过程也就完毕!(注意:客户端进程调用mRemote.transact方法和等待服务端onTransact结果 是同步的,也就是说在服务端进程不能做一些耗时的工作,否则客户端进程就会等待 从而可能会造成ANR现象)。

抛开AIDL,自定义代码实现

通过分析,我想我们应该已经有所认识了,那么我们不依靠AIDL文件生成对应的Java类的话,我们其实一样是可以实现IPC的通信。我做个实现步骤总结:

  1. 定义一个接口类A继承IInterface类,定义我们要实现的方法(按我们的例子就是要写2个方法)。
  2. 定义一个类B继承Binder类,实现刚刚定义的接口类A
  3. 定义我们B类的唯一标识DESCRIPTOR,构造函数实现
  4. 定义我们B类的asInterface静态方法
  5. 定义我们B类的静态内部类Proxy,实现接口类A
  6. 在我们定义B类中实现onTransact方法
    到此就是需要这6部就完成了自定义类似AIDLJava类文件。剩下的就是我们的使用。在使用的过程中和AIDL完全是一样的,只是这个时候需要用到的IStudentManager、Stub内部类,静态内部类Proxy都是我们自己定义的即可。
    看一看我按照以上6部写的类如下:
    IStudentManager:
package com.lgy.aidl;


import android.os.IInterface;
import android.os.RemoteException;

import com.lgy.aidl.aidl.Student;

import java.util.List;

public interface IStudentManager extends IInterface{

    public List<Student> getAllStudent() throws RemoteException;

    public void addStudent(Student stu) throws RemoteException;

}

IStudentStub:

package com.lgy.aidl;


import android.os.Binder;
import android.os.IBinder;
import android.os.IInterface;
import android.os.Parcel;
import android.os.RemoteException;

import com.lgy.aidl.aidl.*;

import java.util.List;

public class IStudentStub extends Binder implements IStudentManager{

    public static final String DESCRIPTOR = "com.lgy.aidl.IStudentStub";

    public static final int TRANSACTION_getAllStudent = FIRST_CALL_TRANSACTION + 0;
    public static final int TRANSACTION_addStudent = FIRST_CALL_TRANSACTION + 1;

    IStudentStub(){
        this.attachInterface(this,DESCRIPTOR);
    }

    public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
        switch (code) {
            case INTERFACE_TRANSACTION: {
                reply.writeString(DESCRIPTOR);
                return true;
            }
            case TRANSACTION_getAllStudent: {
                data.enforceInterface(DESCRIPTOR);
                List<Student> _result = this.getAllStudent();
                reply.writeNoException();
                reply.writeTypedList(_result);
                return true;
            }
            case TRANSACTION_addStudent: {
                data.enforceInterface(DESCRIPTOR);
                Student _arg0;
                if ((0 != data.readInt())) {
                    _arg0 = Student.CREATOR.createFromParcel(data);
                } else {
                    _arg0 = null;
                }
                this.addStudent(_arg0);
                reply.writeNoException();
                return true;
            }
        }
        return super.onTransact(code, data, reply, flags);
    }

    public static IStudentManager asInterface(IBinder binder){
        if (null == binder){
            return null;
        }

        IInterface iin = binder.queryLocalInterface(DESCRIPTOR);
        if (iin != null && iin instanceof IStudentStub){
            return (IStudentManager) iin;
        }
        return new Proxy(binder);
    }

    @Override
    public List<Student> getAllStudent() throws RemoteException {
        return null;
    }

    @Override
    public void addStudent(Student stu) throws RemoteException {

    }

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

    static class Proxy implements IStudentManager{

        private IBinder mRemote;

        Proxy(IBinder binder){
            mRemote = binder;
        }

        @Override
        public List<Student> getAllStudent() throws RemoteException {
            Parcel _data = Parcel.obtain();
            Parcel _reply = Parcel.obtain();
            List<Student> _result;
            try {
                _data.writeInterfaceToken(DESCRIPTOR);
                mRemote.transact(TRANSACTION_getAllStudent, _data, _reply, 0);
                _reply.readException();
                _result = _reply.createTypedArrayList(com.lgy.aidl.aidl.Student.CREATOR);
            } finally {
                _reply.recycle();
                _data.recycle();
            }
            return _result;
        }

        @Override
        public void addStudent(Student stu) throws RemoteException {
            Parcel _data = Parcel.obtain();
            Parcel _reply = Parcel.obtain();
            try {
                _data.writeInterfaceToken(DESCRIPTOR);
                if ((stu != null)) {
                    _data.writeInt(1);
                    stu.writeToParcel(_data, 0);
                } else {
                    _data.writeInt(0);
                }
                mRemote.transact(TRANSACTION_addStudent, _data, _reply, 0);
                _reply.readException();
            } finally {
                _reply.recycle();
                _data.recycle();
            }
        }

        @Override
        public IBinder asBinder() {
            return mRemote;
        }
    }
}

我们的使用在客户端和服务器端:

 private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
            Log.i(TAG, "客户端进程连接成功");
            mStudentManager = IStudentStub.asInterface(iBinder);
            try {
                mList = (ArrayList<Student>) mStudentManager.getAllStudent();
                Log.i(TAG,"客户端进程连接成功之后,返回的学生人数" + mList.size());
            } catch (RemoteException e) {
                e.printStackTrace();
            }

            Student stu4 = new Student("4","Tom","男","15678958962");
            Student stu5 = new Student("5","lily","女","18963526354");


            try{
                mStudentManager.addStudent(stu4);
                mStudentManager.addStudent(stu5);
            }catch (RemoteException e){
                e.printStackTrace();
            }

            try {
                mList = (ArrayList<Student>) mStudentManager.getAllStudent();
                Log.i(TAG, "添加了2名学生后,返回的学生人数" + mList.size());
            } catch (RemoteException e) {
                e.printStackTrace();
            }

        }

        @Override
        public void onServiceDisconnected(ComponentName componentName) {

        }
    };
    private IStudentStub mBinder = new IStudentStub(){
        @Override
        public List<Student> getAllStudent() throws RemoteException {
            Log.i(TAG,"客户端调用getAllStudent方法执行,学生人数" + mList.size());
            return mList;
        }

        @Override
        public void addStudent(Student stu) throws RemoteException {
            Log.i(TAG,"客户端调用addStudent方法执行之前学生人数" + mList.size());
            mList.add(stu);
            Log.i(TAG, "客户端调用addStudent方法执行之后学生人数" + mList.size());
        }

    };

我们在看一看运行的log日志是否和我们预期的一致呢?!
服务器端
这里写图片描述
再看一下我编写后的程序结构如下:
这里写图片描述
不知道写到这里大家是否能够理解,有问题大家一起探讨吧!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值