Android Interface Definition Language (AIDL)——翻译自developer.android.com

35 篇文章 0 订阅

Android 接口定义语言(AIDL)

AIDL类似你可能接触过的其他的IDL。它可以让你定义编程接口,使得客户端和服务端可以达成一致,从而使用IPC(interprocess communication)相互通信。

在Android里面,一个进程通常不能访问其他进程的内存。所以说,他们需要将对象分解为操作系统可以理解的基本的部分,从而使得这些对象为你突破界限。然而突破界限的代码太繁杂,所以Android使用AIDL替你打理。


提示:只有当你想要不同app的客户端都可以对你的service IPC,并且你的service需要多线程处理的时候,你才有必要使用AIDL。如果你不需要跨进程并发IPC,那么你可以通过拓展一个Binder来创建你的接口。或者你需要IPC但是不需要处理多线程,你可以构建使用Messenger来构建一个接口。无论怎样,确保你在开始AIDL之前立即Bound Service。


当你设计你的AIDL接口之前,确保你对AIDL接口的调用都是直接函数调用。你不要预想这些调用从哪些线程里面出现。重点在于,调用来自于一个本地的进程还是一个一个远程的进程。特别是:

- 来自本地进程的调用,会在发起这个调用的线程中执行。如果这是你的主UI界面线程,这个线程会继续在AIDL接口中运行。如果是另一个线程,那就是在你的service中执行的代码的线程。这样的话,只有本地线程在访问srevice的时候,你才可以完全控制哪一个线程在其中执行。(如果是这种情况,你根本不应该使用AIDL,而应该通过构建一个Binder来创建接口)。

- 来自远程进程的调用会被从你的线程池当中派遣,这个线程池是该平台在你自己的进程中维护的。你必须准备好迎接来自位置线程的通信,而且是多个 通信同时。换句话说,AIDL的构建必须要是彻底的线程安全。

- oneway(单向)关键字指定了远程调用的样子。当使用该关键字的时候,远程的调用不会阻塞。它仅仅是传送业务数据并且即刻返回。这个接口的构建最终把它作为一个来自Binder线程池的常规调用,就如同一个正常的远程调用一样。如果oneway关键字被用作一个本地调用则没有影响,调用依旧是同步的。


定义一个AIDL接口

你必须在一个.aidl文件中使用java编程语言的语法来进行定义AIDL接口,然后把它存储在运行service的源代码和其他要bind service的源代码中(在 src/目录下)。


但你构建包含.aidl文件的每一个app时,android SDK工具会基于.aidl产生一个IBinder,并把它存储在项目的gen/目录下。service必须恰当地构建IBinder接口。然后客户端的应用可以bind service并且调用IBinder中的方法来实现IPC。


使用AIDL来创建绑定的service,遵循以下几个步骤:

1.创建.aidl 文件

这个文件使用方法签名(就是函数的声明)定义一个编程接口。

2.构建接口 

Android SDK工具基于.aidl文件产生一个Java编程语言的接口。这个接口有一个叫做Stub的内部抽象类,他拓展了Binder并且实现了AIDL接口中的方法。你必须拓展Stub类并且实现这些方法。

3.向客户端暴露这些方法

构建一个Service并且重写onBinde方法来返回你构建的Stub类。

警告:任何在你发布了第一个版本的AIDL之后所做的修改必须要向后兼容,以防止破坏其他使用你的服务的应用。因为你的.aidl文件一定会被其他的app复用来访问你的service的接口,你必须维护并支持原始的接口。


大标题

1.创建.aidl文件

AIDL使用一个简单的语法,它可以让你用一个或者多个方法声明一个接口,接口可以接收参数和返回数值。参数和返回值可以是任何的类型,AIDL生成的其他类型都可以。

你必须使用Java编程语言来构建.aidl文件。每一个.aidl文件都必须定义只一个接口,并且请求指定接口的定义和方法签名。

默认的话,AIDL支持一下的数据的类型:

- 所有Java编程语言中的基本类型。(比如说 int,long,char,boolean等等)

- String

- CharSequence

- List

   所有List中的缘分都必须是列表中支持的类型,或者是其他的AIDL生成的接口,以及你声明的可打包类型。一个List可以随意地被用作一个泛型(比如说,List<String>)。另一边实际接收的具体类型通常是一个ArrayList,尽管接口中定义的是List类型。

- Map

Map中的所有元素也必须是列表中所支持的类型,或者是其他的AIDL中生成的接口,或者是你声明的可打包类型。 泛型的 maps(比如说Map<String,Integer>)形式的就不被支持。另一边接收的实际的具体的类通常是一个HashMap,尽管接口中定义的是Map类型。


你必须必须为每一个上面没有提及的附加类型包含一个import声明,尽管他们和你的接口定义在同一个包中。

当你定义你的服务接口时,你要知道:

- 方法可以接受0个或者多个的参数,返回一个值或者void。

- 所有的非基本参数都需要一个方向标签来指明这些数据将怎样流动。in,out,或者inout。

    基本类型默认是in类型,不能是其他的。

警告:你必须对真正需要的类型的方向有所限制,因为封装传送(marshalling)参数是很耗费资源的。

- 所有的在.aidl中的代码注释都被包含在了生成的IBinder接口中(除了在import和pachage声明之前的注释)。

- 只能提供方法,你不能暴露AIDL中的静态部分。

下面是一个.aidl文件的例子:

// IRemoteService.aidl
package com.example.android;

// Declare any non-default types here with import statements

/** Example service interface */
interface IRemoteService {
    /** Request the process ID of this service, to do evil things with it. */
    int getPid();

    /** Demonstrates some basic types that you can use as parameters
     * and return values in AIDL.
     */
    void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
            double aDouble, String aString);
}
只要把你的.ail文件存在你的项目的src/目录下,当你构建你的app时,sdk工具会在你的项目的gen/目录下生成IBinder接口文件。生成的文件的名字和.aidl的文件名对应,但是有一个java拓展名。(比如IRemoteService.aild和IRemoteService.java)。


如果你使用Android Studio,增量构建(incremental build)立即产生一个binder class。如果你不使用这个IDE,那么Gradle工具在你下一次构建你的应用时生成bind class——在你完成.aidl的编辑以后,你要尽早地使用 gradle assembleDebug(或者gradle assmbleRlease)来构建你的项目。这样的话你的代码就可以链接到生成的class了。

2.构建接口

当你(build)构建你的app时,Android SDK工具为你生成一个以.aidl文件的命名的.java接口文件。这个生成的接口包含一个名字为Stub的子类,它是它的父接口的抽象构造接口(比如说,YourInterface.stub),并且声明了.aidl文件中的所有的方法。

<这个cast 是借用的冶金中铸造注模的概念,把液态的金属倒入成型的模范这个过程叫cast。编程中把一段数据装成某个数据类型,让数据具有某个类型的过程叫做cast。
比如让4个字节变成一个int类型,把int变成4个char这种过程。
基本上和“类型转换”同义,不过cast在c++语言中是从对象封装的视角看这个动作。
所以有动态cast,静态cast等多种cast。>

提示:Stub 也定义了一下帮助者的方法,比如说asInterface,它接收一个IBinder参数(通常是传递给onServiceConnected回调方法的那个参数),并且返回一个stub的接口实例。想了解更多的其中的类型转换的细节,请查看Calling an IPC Method。

想要构建从.aidl中生成的接口,就要拓展生成的Binder接口(比如说YourInterface.Stub),并且构建从.aidl文件中继承的方法。

下面是一个使用匿名对象构建一个叫做IRemoterService(在上面的IRemoteService.aidl中定义的)的接口的例子。


private final IRemoteService.Stub mBinder = new IRemoteService.Stub() {
    public int getPid(){
        return Process.myPid();
    }
    public void basicTypes(int anInt, long aLong, boolean aBoolean,
        float aFloat, double aDouble, String aString) {
        // Does nothing
    }
};
现在mBinder 是一个Stub类的(一个Binder)实例,它为这个服务定义了一个RPC接口。下一步将会把这个接口呈现给客户端,这样他们可以和service通信了。

在构建AIDL接口时有几个规则你要知道:

- 接到的调用不能保证都在主线程中运行,所以你需要在一开始就考虑多线程,并且保证线程安全。

- 默认而言,RPC调用都是同步的。如果你发现服务处理一个请求花费了可观的时间,那么你不应该在activity的主线程中调用它,因为这样就可能会让app挂起(Android 系统可能会现实ANR对话框)——你需要从客户端的子线程中调用。

- 你抛出的所有异常都不会被发送到调用者那里。


3.向客户端呈现接口

一旦你为service构建好了接口,你需要向客户端暴露呈现它们才能让他们bind。要呈现service的接口,你应该extend Service并且构建一个onBind来返回构建Stub的类的实例(上面说的那个)。下面是一个向客户端呈现IRemoteService的例子。

public class RemoteService extends Service {
    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    public IBinder onBind(Intent intent) {
        // Return the interface
        return mBinder;
    }

    private final IRemoteService.Stub mBinder = new IRemoteService.Stub() {
        public int getPid(){
            return Process.myPid();
        }
        public void basicTypes(int anInt, long aLong, boolean aBoolean,
            float aFloat, double aDouble, String aString) {
            // Does nothing
        }
    };
}

这样的话当一个客户端(比如activity)调用bindService来连接服务,客户端的onServiceConnected回调方法从service的onBind方法中接收一个实例。


客户端必须可以访问接口类,所以如果客户端和server在不同的app中,那么他们各自都应该在src/下有.aidl的副本(它生成了android.os.Binder接口,让用户可以访问AIDL方法)。


当客户端在onServiceConneted回调方法中接收到IBinder的实例以后,他必须调用YourServiceInterface.stub.asInterface(Service)来将返回的参数转换为YourServiceInnterface的类型,例如:

IRemoteService mIRemoteService;
private ServiceConnection mConnection = new ServiceConnection() {
    // Called when the connection with the service is established
    public void onServiceConnected(ComponentName className, IBinder service) {
        // Following the example above for an AIDL interface,
        // this gets an instance of the IRemoteInterface, which we can use to call on the service
        mIRemoteService = IRemoteService.Stub.asInterface(service);
    }

    // Called when the connection with the service disconnects unexpectedly
    public void onServiceDisconnected(ComponentName className) {
        Log.e(TAG, "Service has unexpectedly disconnected");
        mIRemoteService = null;
    }
};
更多的示例代码,请参考RemoteSErvice.java-ApiDemos.


通过IPC来传递对象

你已经可以通过IPC接口来跨进程传递一个class了。但是,你需要确保你的代码可以从另一端的IPC通道获得,并且你的class需要实现Pacelable接口。实现Pacelable接口很重要,只有这样Android系统才能分解对象来跨进程。


要创建一个支持Pacelable协议的类,你必须这么做:

1.实现Parcelable接口。

2.要实现一个记录当前对象状态并将其写入Parcel的方法,writeToParcel。

3.将一个叫做CREATOR的静态对象放入你的类,这个对象要实现Parcelable.Creator接口。

4.最后,创建一个声明你的parcelable类的.aidl文件(如下面的Rect.aidl文件)。

如果你使用了一个客户构建的进程,那么不要把.aidl文件放到你的构建里面,如同C语言一样,这个.aidl不会被编译。

AIDL 在其代码中使用这些方法和域来装箱和散出你的对象。

例如,下面是一个Rect.aidl文件,创建了一个parcelabel的Rect类。

package android.graphics;

// Declare Rect so AIDL can find it and knows that it implements
// the parcelable protocol.
parcelable Rect;
下面是一个Rect class实现Parcelable协议的例子。

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

public final class Rect implements Parcelable {
    public int left;
    public int top;
    public int right;
    public int bottom;

    public static final Parcelable.Creator<Rect> CREATOR = new
Parcelable.Creator<Rect>() {
        public Rect createFromParcel(Parcel in) {
            return new Rect(in);
        }

        public Rect[] newArray(int size) {
            return new Rect[size];
        }
    };

    public Rect() {
    }

    private Rect(Parcel in) {
        readFromParcel(in);
    }

    public void writeToParcel(Parcel out) {
        out.writeInt(left);
        out.writeInt(top);
        out.writeInt(right);
        out.writeInt(bottom);
    }

    public void readFromParcel(Parcel in) {
        left = in.readInt();
        top = in.readInt();
        right = in.readInt();
        bottom = in.readInt();
    }
}
在Rect类中的编集很简单。看看其他的Parcled的方法来了解一下其他类型可以写入Parcel的数值。

警告:不要忽视跨进程接收数据对于安全的影响。在本例中,Rect从Parcel中读取了4个数字,但是你应该自己判断这些数字是不是在可用的范围内。 可以参考Sercurity andPermission来获得更多的关于防止恶意软件侵害的内容。


调用一个IPC方法

下面是一个主动的类要通过AIDL调用远程必须做的:

1.在src/目录下有一个.aidl文件。

2.声明一个IBinder接口的实例(在AIDL中生成的接口)。

3.实现SErviceConnection。

4.调用Context.bindService(),传入你构架的ServiceConnection。

5.在你构建的onServiceConnction当中,你将会接到一个叫做service的IBinder对象。调用UourInterfaceName.Stub.asInstance((IBinder)service)来将返回的参数转化为你的接口的类型。

6.可以调用接口中定义的方法了,但是你呀一直都捕获DeadObjectException异常,他们会在连接断开的时候抛出。这是远程方法可能抛出的唯一之中异常。

7.要断开连接,用你的接口实例调用Context.unbindService方法。


下面是关于调用IPC服务的一些注解。

- 对象被当做是跨越进程的引用。

- 你可以在方法参数中放入匿名对象。


更多的关于bind service的资料请看BoundService文档。

下面是一个示例代码,展示了一个AIDL生成的service。来自于ApiDemos项目的Rmote Service例子。

public static class Binding extends Activity {
    /** The primary interface we will be calling on the service. */
    IRemoteService mService = null;
    /** Another interface we use on the service. */
    ISecondary mSecondaryService = null;

    Button mKillButton;
    TextView mCallbackText;

    private boolean mIsBound;

    /**
     * Standard initialization of this activity.  Set up the UI, then wait
     * for the user to poke it before doing anything.
     */
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.remote_service_binding);

        // Watch for button clicks.
        Button button = (Button)findViewById(R.id.bind);
        button.setOnClickListener(mBindListener);
        button = (Button)findViewById(R.id.unbind);
        button.setOnClickListener(mUnbindListener);
        mKillButton = (Button)findViewById(R.id.kill);
        mKillButton.setOnClickListener(mKillListener);
        mKillButton.setEnabled(false);

        mCallbackText = (TextView)findViewById(R.id.callback);
        mCallbackText.setText("Not attached.");
    }

    /**
     * Class for interacting with the main interface of the service.
     */
    private ServiceConnection mConnection = new ServiceConnection() {
        public void onServiceConnected(ComponentName className,
                IBinder service) {
            // This is called when the connection with the service has been
            // established, giving us the service object we can use to
            // interact with the service.  We are communicating with our
            // service through an IDL interface, so get a client-side
            // representation of that from the raw service object.
            mService = IRemoteService.Stub.asInterface(service);
            mKillButton.setEnabled(true);
            mCallbackText.setText("Attached.");

            // We want to monitor the service for as long as we are
            // connected to it.
            try {
                mService.registerCallback(mCallback);
            } catch (RemoteException e) {
                // In this case the service has crashed before we could even
                // do anything with it; we can count on soon being
                // disconnected (and then reconnected if it can be restarted)
                // so there is no need to do anything here.
            }

            // As part of the sample, tell the user what happened.
            Toast.makeText(Binding.this, R.string.remote_service_connected,
                    Toast.LENGTH_SHORT).show();
        }

        public void onServiceDisconnected(ComponentName className) {
            // This is called when the connection with the service has been
            // unexpectedly disconnected -- that is, its process crashed.
            mService = null;
            mKillButton.setEnabled(false);
            mCallbackText.setText("Disconnected.");

            // As part of the sample, tell the user what happened.
            Toast.makeText(Binding.this, R.string.remote_service_disconnected,
                    Toast.LENGTH_SHORT).show();
        }
    };

    /**
     * Class for interacting with the secondary interface of the service.
     */
    private ServiceConnection mSecondaryConnection = new ServiceConnection() {
        public void onServiceConnected(ComponentName className,
                IBinder service) {
            // Connecting to a secondary interface is the same as any
            // other interface.
            mSecondaryService = ISecondary.Stub.asInterface(service);
            mKillButton.setEnabled(true);
        }

        public void onServiceDisconnected(ComponentName className) {
            mSecondaryService = null;
            mKillButton.setEnabled(false);
        }
    };

    private OnClickListener mBindListener = new OnClickListener() {
        public void onClick(View v) {
            // Establish a couple connections with the service, binding
            // by interface names.  This allows other applications to be
            // installed that replace the remote service by implementing
            // the same interface.
            Intent intent = new Intent(Binding.this, RemoteService.class);
            intent.setAction(IRemoteService.class.getName());
            bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
            intent.setAction(ISecondary.class.getName());
            bindService(intent, mSecondaryConnection, Context.BIND_AUTO_CREATE);
            mIsBound = true;
            mCallbackText.setText("Binding.");
        }
    };

    private OnClickListener mUnbindListener = new OnClickListener() {
        public void onClick(View v) {
            if (mIsBound) {
                // If we have received the service, and hence registered with
                // it, then now is the time to unregister.
                if (mService != null) {
                    try {
                        mService.unregisterCallback(mCallback);
                    } catch (RemoteException e) {
                        // There is nothing special we need to do if the service
                        // has crashed.
                    }
                }

                // Detach our existing connection.
                unbindService(mConnection);
                unbindService(mSecondaryConnection);
                mKillButton.setEnabled(false);
                mIsBound = false;
                mCallbackText.setText("Unbinding.");
            }
        }
    };

    private OnClickListener mKillListener = new OnClickListener() {
        public void onClick(View v) {
            // To kill the process hosting our service, we need to know its
            // PID.  Conveniently our service has a call that will return
            // to us that information.
            if (mSecondaryService != null) {
                try {
                    int pid = mSecondaryService.getPid();
                    // Note that, though this API allows us to request to
                    // kill any process based on its PID, the kernel will
                    // still impose standard restrictions on which PIDs you
                    // are actually able to kill.  Typically this means only
                    // the process running your application and any additional
                    // processes created by that app as shown here; packages
                    // sharing a common UID will also be able to kill each
                    // other's processes.
                    Process.killProcess(pid);
                    mCallbackText.setText("Killed service process.");
                } catch (RemoteException ex) {
                    // Recover gracefully from the process hosting the
                    // server dying.
                    // Just for purposes of the sample, put up a notification.
                    Toast.makeText(Binding.this,
                            R.string.remote_call_failed,
                            Toast.LENGTH_SHORT).show();
                }
            }
        }
    };

    // ----------------------------------------------------------------------
    // Code showing how to deal with callbacks.
    // ----------------------------------------------------------------------

    /**
     * This implementation is used to receive callbacks from the remote
     * service.
     */
    private IRemoteServiceCallback mCallback = new IRemoteServiceCallback.Stub() {
        /**
         * This is called by the remote service regularly to tell us about
         * new values.  Note that IPC calls are dispatched through a thread
         * pool running in each process, so the code executing here will
         * NOT be running in our main thread like most other things -- so,
         * to update the UI, we need to use a Handler to hop over there.
         */
        public void valueChanged(int value) {
            mHandler.sendMessage(mHandler.obtainMessage(BUMP_MSG, value, 0));
        }
    };

    private static final int BUMP_MSG = 1;

    private Handler mHandler = new Handler() {
        @Override public void handleMessage(Message msg) {
            switch (msg.what) {
                case BUMP_MSG:
                    mCallbackText.setText("Received from service: " + msg.arg1);
                    break;
                default:
                    super.handleMessage(msg);
            }
        }

    };
}


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
提供的源码资源涵盖了Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 适合毕业设计、课程设计作业。这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。 所有源码均经过严格测试,可以直接运行,可以放心下载使用。有任何使用问题欢迎随时与博主沟通,第一时间进行解答!
提供的源码资源涵盖了小程序应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 适合毕业设计、课程设计作业。这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。 所有源码均经过严格测试,可以直接运行,可以放心下载使用。有任何使用问题欢迎随时与博主沟通,第一时间进行解答!
提供的源码资源涵盖了Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 适合毕业设计、课程设计作业。这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。 所有源码均经过严格测试,可以直接运行,可以放心下载使用。有任何使用问题欢迎随时与博主沟通,第一时间进行解答!

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值