Android的IPC机制(一)——AIDL的使用

综述

  IPC(interprocess communication)是指进程间通信,也就是在两个进程间进行数据交互。不同的操作系统都有他们自己的一套IPC机制。例如在Linux操作系统中可以通过管道、信号量、消息队列、内存共享、套接字等进行进程间通信。那么在Android系统中我们可以通过Binder来进行进程间的通信。当然除了Binder我们还可以使用Socket来进行进程间的通信。
  既然需要进程通信,那么就必须有多个进程。当然,在两个应用交互中必然出现多进程的情况。若是在一个应用中呢?我们可以通过给四大组件在AndroidMenifest中为他们指定android:process属性来实现不同的组件在不同进程中运行。下面就来介绍一下Android中进程间通信的实现方式。

AIDL简介

  AIDL是 Android Interface Definition Language的缩写。AIDL 是一种IDL 语言,用于生成可以在Android设备上两个进程之间进行 IPC的代码。如果在一个进程中(例如Activity)要调用另一个进程中(例如Service)对象的操作,就可以使用AIDL生成可序列化的参数。
  AIDL是面向接口的,像COM或Corba一样,但是更加轻量级。它是使用代理类在客户端和实现端传递数据。

AIDL用法

  首先我们创建一个AIDL文件,在AndroidStudio中当我们创建一个AIDL文件时会自动为我们创件一个AILD文件夹,用于存放AIDL文件。创建完之后重新rebuild会自动生成aidl实现类。
  这里写图片描述
  在下面的例子当中,我们将Service单独作为一个应用在系统中运行,在另一个用于访问Service的client也单独作为一个应用运行在系统中。这样保证了两个程序分别运行在两个进程中。并且使用了butterknife进行控件绑定。

AIDL简单用法

演示

  在Service中我们对客户端传来的两个整数做了一次加法运算并返回到客户端中。
这里写图片描述
  

AIDL代码

// ICalculate.aidl
package com.ljd.aidl;

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

interface ICalculate {
    /**
     * Demonstrates some basic types that you can use as parameters
     * and return values in AIDL.
     */
    int add(int first, int second);
}

服务端代码

package com.ljd.aidl.service;

import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.os.RemoteException;

import com.ljd.aidl.ICalculate;

public class CalculateService extends Service {
    public CalculateService() {
    }

    private Binder mBinder = new ICalculate.Stub(){

        @Override
        public int add(int first, int second) throws RemoteException {
            return first + second;
        }
    };

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

客户端代码

package com.ljd.aidl.activity;

import android.content.ComponentName;
import android.content.Context;
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.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Toast;

import com.ljd.aidl.ICalculate;
import com.ljd.aidl.client.R;

import butterknife.ButterKnife;
import butterknife.OnClick;

public class Demo1Activity extends AppCompatActivity {

    private final String TAG = "DEMO1";
    //是否已经绑定service
    private boolean mIsBindService;
    private ICalculate mCalculate;
    private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            Log.d(TAG,"bind success");
            Toast.makeText(Demo1Activity.this,"bind service success",Toast.LENGTH_SHORT).show();
            mCalculate = ICalculate.Stub.asInterface(service);
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
        //重新绑定Service防止系统将服务进程杀死而产生的调用错误。
            bindService();
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_demo1);
        ButterKnife.bind(this);
        mIsBindService = false;
    }

    @Override
    protected void onDestroy() {
        unbindService();
        ButterKnife.unbind(this);
        super.onDestroy();
    }

    @OnClick({ R.id.bind_demo1_btn,R.id.unbind_demo1_btn,R.id.calculate_btn})
    public void onClickButton(View v) {
        switch (v.getId()){
            case R.id.bind_demo1_btn:
                bindService();
                break;
            case R.id.unbind_demo1_btn:
                Toast.makeText(this,"unbind service success",Toast.LENGTH_SHORT).show();
                unbindService();
                break;
            case R.id.calculate_btn:
                if (mIsBindService && mCalculate != null ){
                    try {
                        int result = mCalculate.add(2,4);
                        Log.d(TAG,String.valueOf(result));
                        Toast.makeText(this,String.valueOf(result),Toast.LENGTH_SHORT).show();
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                } else {
                    Toast.makeText(this,"not bind service",Toast.LENGTH_SHORT).show();
                }
                break;
        }
    }

    private void bindService(){
        Intent intent = new Intent();
        intent.setAction("com.ljd.aidl.action.CALCULATE_SERVICE");
        bindService(intent,mConnection, Context.BIND_AUTO_CREATE);
        mIsBindService = true;
    }

    private void unbindService(){
        if(mIsBindService){
            mIsBindService = false;
            unbindService(mConnection);
        }
    }
}

AIDL高级用法

  对于上面的例子,在AIDL接口中只是使用了一些Java的基本类型,对于AIDL文件并不是所有的类型都是可用的,那么在AIDL中究竟有哪些类型可以使用呢?

AIDL语法规则

默认情况下AIDL支持以下数据类型:

  • 所有Java的基本数据类型(例如: int, long,double, char, boolean等)
  • String和CharSequence
  • List:AIDL实际接收到的是ArrayList,并且List里面所有元素都必须被AIDL支持
  • Map: AIDL实际接收到的是HashMap,并且Map里面所有元素都必须被AIDL支持

如果不是上面所述类型,我们必须要显示import进来,即使他们在同一个包中。当我们使用自定义的对象时必须实现Parcelable接口,Parcelable为对象序列化接口,效率比实现Serializable接口高。并且新建一个与该类同名的AIDL文件,声明他为Parcelable类型。

我们定义AIDL接口还需要注意以下几点:

  • 方法可以有多个或没有参数,可以有返回值也可以为void
  • 在参数中,除了基本类型以外,我们必须为参数标上方向in, out, 或者 inout
  • 在AIDL文件中只支持方法,不支持静态常量

演示

  在计算机商店中需要采购笔记本进行销售,在服务端中我们添加两台笔记本,在客户端中我们为商店加购一台dell笔记本。
这里写图片描述
  

实体类代码

  我们首先构建一个计算机实体类,包含笔记本的id,品牌,型号,并且实现Parcelable接口,在AndroidStudio中会为我们自动构造代码。
  

package com.ljd.aidl.entity;

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

public class ComputerEntity implements Parcelable{

    public int computerId;     //id
    public String brand;       //品牌
    public String model;       //型号

    public ComputerEntity(int computerId, String brand, String model) {
        this.brand = brand;
        this.computerId = computerId;
        this.model = model;
    }

    protected ComputerEntity(Parcel in) {
        computerId = in.readInt();
        brand = in.readString();
        model = in.readString();
    }

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

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

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

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(computerId);
        dest.writeString(brand);
        dest.writeString(model);
    }
}

AIDL代码

  在AIDL中对实体类进行声明,包名和文件名必须与实体类一致。在AndroidStudio中新建一个与实体类同名的AIDL文件会报错,需要先用一个其它名字,然后修改与实体类名一致即可。

package com.ljd.aidl.entity;

//包名必须和对用实体类的包名一致
// Declare any non-default types here with import statements
parcelable ComputerEntity;

  添加两个接口分别为添加一台笔记本和获取全部笔记本,在该文件中使用到了ComputerEntity类,显示的import进来。

package com.ljd.aidl;

import com.ljd.aidl.entity.ComputerEntity;
// Declare any non-default types here with import statements

interface IComputerManager {
    /**
     * Demonstrates some basic types that you can use as parameters
     * and return values in AIDL.
     */
     void addComputer(in ComputerEntity computer);
     List<ComputerEntity> getComputerList();
}

服务端代码

package com.ljd.aidl.service;

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

import com.ljd.aidl.IComputerManager;
import com.ljd.aidl.entity.ComputerEntity;

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class ComputerService extends Service {
    private CopyOnWriteArrayList<ComputerEntity> mComputerList = new CopyOnWriteArrayList<>();

    public ComputerService() {
    }

    private final IComputerManager.Stub mBinder = new IComputerManager.Stub() {
        @Override
        public void addComputer(ComputerEntity computer) throws RemoteException {
            mComputerList.add(computer);
        }

        @Override
        public List<ComputerEntity> getComputerList() throws RemoteException {
            return mComputerList;
        }
    };

    @Override
    public void onCreate() {
        super.onCreate();
        mComputerList.add(new ComputerEntity(0,"apple","macbookpro"));
        mComputerList.add(new ComputerEntity(1,"microsoft","surfacebook"));
        mComputerList.add(new ComputerEntity(2,"dell","XPS13"));
    }

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

  注意:在该类中使用了CopyOnWriteArrayList,CopyOnWriteArrayList能够自动进行线程同步。可是在AIDL中接收和返回的只能是ArrayList,其实AIDL支持的是抽象的List,在Binder中会按照List访问数据并最终形成一个ArrayList,所以在AIDL中返回的还是一个ArrayList。

客户端代码

package com.ljd.aidl.activity;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.RemoteException;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;

import com.ljd.aidl.IComputerManager;
import com.ljd.aidl.client.R;
import com.ljd.aidl.entity.ComputerEntity;

import java.util.List;

import butterknife.Bind;
import butterknife.ButterKnife;
import butterknife.OnClick;

public class Demo2Activity extends AppCompatActivity{

    @Bind(R.id.show_linear)
    LinearLayout mShowLinear;

    private boolean mIsBindService;
    private IComputerManager mRemoteComputerManager;
    private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
        @Override
        public void binderDied() {
            if(mRemoteComputerManager != null){
                mRemoteComputerManager.asBinder().unlinkToDeath(mDeathRecipient,0);
                mRemoteComputerManager = null;
                bindService();
            }
        }
    };
    private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mIsBindService = true;
            Toast.makeText(Demo2Activity.this,"bind service success",Toast.LENGTH_SHORT).show();
            mRemoteComputerManager = IComputerManager.Stub.asInterface(service);
            try {
                mRemoteComputerManager.asBinder().linkToDeath(mDeathRecipient,0);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            mRemoteComputerManager = null;
        }
    };
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_demo2);
        ButterKnife.bind(this);
        mIsBindService = false;
    }

    @Override
    protected void onDestroy() {
        unbindService();
        ButterKnife.unbind(this);
        super.onDestroy();
    }

    @OnClick({R.id.bind_demo2_btn,R.id.unbind_demo2_btn,R.id.test_demo2_btn,R.id.clear_demo2_btn})
    public void onClickButton(View v) {
        switch (v.getId()){
            case R.id.bind_demo2_btn:
                bindService();
                break;
            case R.id.unbind_demo2_btn:
                Toast.makeText(this,"unbind service success",Toast.LENGTH_SHORT).show();
                unbindService();
                break;
            case R.id.test_demo2_btn:
                if (!mIsBindService || mRemoteComputerManager == null){
                    Toast.makeText(this,"not bind service",Toast.LENGTH_SHORT).show();
                    return;
                }
                try {
                    List<ComputerEntity> computerList = mRemoteComputerManager.getComputerList();
                    for (int i =0;i<computerList.size();i++){
                        String str = "computerId:" + String.valueOf(computerList.get(i).computerId) +
                                " brand:" + computerList.get(i).brand +
                                " model:" + computerList.get(i).model ;
                        TextView textView = new TextView(this);
                        textView.setText(str);
                        mShowLinear.addView(textView);
                    }
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
                break;
            case R.id.clear_demo2_btn:
                mShowLinear.removeAllViews();
                break;
        }
    }

    private void bindService(){
        Intent intent = new Intent();
        intent.setAction("com.ljd.aidl.action.COMPUTER_SERVICE");
        mIsBindService = bindService(intent,mConnection, Context.BIND_AUTO_CREATE);
    }

    private void unbindService(){
        if(!mIsBindService){
            return;
        }
        mIsBindService = false;
        unbindService(mConnection);
    }
}

  由于Binder是有可能会意外死亡的,也就是Service所在进程被系统杀死,这时候我们调用Service的方法就会失败。在第一个例子中我们通过onServiceDisconnected方法中重新绑定服务。在这个例子中我们采用了另外一种方法,由于在Binder中提供了两个配对的方法linkToDeath和unlinkToDeath,通过linkToDeath可以给Binder设置一个死亡代理,Binder死亡时回调binderDied方法,在binderDied方法中我们重新绑定服务即可。

AIDL用法拓展

  当我们需要一种笔记本的时候,由于商店缺货,这时候我们会给卖家说一声,我所需要的这款笔记本到货后通知我。也就成了所谓的观察者模式。
  在Android系统中为我们提供了一个RemoteCallbackList,RemoteCallbackList是系统专门用来删除跨进程的listener接口,并且在RemoteCallbackList中自动实现了线程同步功能,下面看一下它的用法。

演示

  客户端注册服务以后,服务端每隔三秒会添加一台笔记本,并通知给客户端显示。
这里写图片描述

AIDL代码

  到货后的AIDL监听接口
  

package com.ljd.aidl;

import com.ljd.aidl.entity.ComputerEntity;
// Declare any non-default types here with import statements

interface IOnComputerArrivedListener {
    /**
     * Demonstrates some basic types that you can use as parameters
     * and return values in AIDL.
     */
    void onComputerArrived(in ComputerEntity computer);
}

  在IComputerManager接口中添加两个方法。显示importIOnComputerArrivedListener ,即使在同一个包下面。
  

// IComputerManagerObserver.aidl
package com.ljd.aidl;

import com.ljd.aidl.entity.ComputerEntity;
import com.ljd.aidl.IOnComputerArrivedListener;
// Declare any non-default types here with import statements

interface IComputerManagerObserver {
    /**
     * Demonstrates some basic types that you can use as parameters
     * and return values in AIDL.
     */
     void addComputer(in ComputerEntity computer);
     List<ComputerEntity> getComputerList();
     void registerUser(IOnComputerArrivedListener listener);
     void unRegisterUser(IOnComputerArrivedListener listener);
}

服务端代码

import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.os.RemoteCallbackList;
import android.os.RemoteException;

import com.ljd.aidl.IComputerManagerObserver;
import com.ljd.aidl.IOnComputerArrivedListener;
import com.ljd.aidl.entity.ComputerEntity;

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;

public class ComputerObserverService extends Service{
    public ComputerObserverService() {
    }

    private CopyOnWriteArrayList<ComputerEntity> mComputerList = new CopyOnWriteArrayList<>();
    private RemoteCallbackList<IOnComputerArrivedListener> mComputerArrivedListenerList = new RemoteCallbackList<>();
    private AtomicBoolean mIsServiceDestroy = new AtomicBoolean(false);

    private Binder mBinder = new IComputerManagerObserver.Stub(){

        @Override
        public void addComputer(ComputerEntity computer) throws RemoteException {
            mComputerList.add(computer);
        }

        @Override
        public List<ComputerEntity> getComputerList() throws RemoteException {
            return mComputerList;
        }

        @Override
        public void registerUser(IOnComputerArrivedListener listener) throws RemoteException {
            mComputerArrivedListenerList.register(listener);
        }

        @Override
        public void unRegisterUser(IOnComputerArrivedListener listener) throws RemoteException {
            mComputerArrivedListenerList.unregister(listener);
        }
    };

    @Override
    public void onCreate() {
        super.onCreate();
        mComputerList.add(new ComputerEntity(0,"apple","macbookpro"));
        mComputerList.add(new ComputerEntity(1,"microsoft","surfacebook"));
        mComputerList.add(new ComputerEntity(2,"dell","XPS13"));
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (!mIsServiceDestroy.get()){
                    try {
                        Thread.currentThread().sleep(3000);
                        ComputerEntity computer = new ComputerEntity(mComputerList.size(),"******","******");
                        mComputerList.add(computer);
                        final int COUNT = mComputerArrivedListenerList.beginBroadcast();
                        //通知所有注册过的用户
                        for (int i=0;i<COUNT;i++){
                            IOnComputerArrivedListener listener = mComputerArrivedListenerList.getBroadcastItem(i);
                            if (listener != null){
                                listener.onComputerArrived(computer);
                            }
                        }
                        mComputerArrivedListenerList.finishBroadcast();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }

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

    @Override
    public void onDestroy() {
        super.onDestroy();
        mIsServiceDestroy.set(true);
    }
}

  注意:RemoteCallbackList并不是一个List,所以我们不能像操作List一样操作RemoteCallbackList。并且遍历RemoteCallbackList时,beginBroadcast和finishBroadcast是配对使用的。

客户端代码


import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.RemoteException;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;

import com.ljd.aidl.IComputerManagerObserver;
import com.ljd.aidl.IOnComputerArrivedListener;
import com.ljd.aidl.client.R;
import com.ljd.aidl.entity.ComputerEntity;

import java.util.List;

import butterknife.Bind;
import butterknife.ButterKnife;
import butterknife.OnClick;

public class Demo3Activity extends AppCompatActivity {

    @Bind(R.id.show_demo3_linear)
    LinearLayout mShowLinear;

    private boolean mIsBindService;
    private static final int MESSAGE_COMPUTER_ARRIVED = 1;

    private IComputerManagerObserver mRemoteComputerManager;
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what){
                case MESSAGE_COMPUTER_ARRIVED:
                    ComputerEntity computer = (ComputerEntity)msg.obj;
                    String str = "computerId:" + String.valueOf(computer.computerId) +
                            " brand:" + computer.brand +
                            " model:" + computer.model ;
                    TextView textView = new TextView(Demo3Activity.this);
                    textView.setText(str);
                    mShowLinear.addView(textView);
                    break;
                default:
                    super.handleMessage(msg);
                    break;
            }

        }
    };

    private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
        @Override
        public void binderDied() {
            if(mRemoteComputerManager != null){
                mRemoteComputerManager.asBinder().unlinkToDeath(mDeathRecipient,0);
                mRemoteComputerManager = null;
                bindService();
            }
        }
    };

    private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mIsBindService = true;
            Toast.makeText(Demo3Activity.this,"bind service success",Toast.LENGTH_SHORT).show();
            mRemoteComputerManager = IComputerManagerObserver.Stub.asInterface(service);
            try {
                mRemoteComputerManager.asBinder().linkToDeath(mDeathRecipient,0);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            mRemoteComputerManager = null;
        }
    };

    private IOnComputerArrivedListener mOnComputerArrivedListener = new IOnComputerArrivedListener.Stub(){

        @Override
        public void onComputerArrived(ComputerEntity computer) throws RemoteException {
            mHandler.obtainMessage(MESSAGE_COMPUTER_ARRIVED,computer).sendToTarget();
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_demo3);
        ButterKnife.bind(this);
        mIsBindService = false;
    }

    @Override
    protected void onDestroy() {
        unbindService();
        ButterKnife.unbind(this);
        super.onDestroy();
    }

    @OnClick({R.id.bind_demo3_btn,R.id.unbind_demo3_btn,R.id.test_demo3_btn,R.id.clear_demo3_btn})
    public void onClickButton(View v){
        switch (v.getId()){
            case R.id.bind_demo3_btn:
                bindService();
                break;
            case R.id.unbind_demo3_btn:
                Toast.makeText(this,"unbind service success",Toast.LENGTH_SHORT).show();
                unbindService();
                break;
            case R.id.test_demo3_btn:
                if (!mIsBindService || mRemoteComputerManager == null){
                    Toast.makeText(this,"not bind service",Toast.LENGTH_SHORT).show();
                    return;
                }

                try {
                    ComputerEntity computer = new ComputerEntity(3,"hp","envy13");
                    mRemoteComputerManager.addComputer(computer);
                    List<ComputerEntity> computerList = mRemoteComputerManager.getComputerList();
                    for (int i =0;i<computerList.size();i++){
                        String str = "computerId:" + String.valueOf(computerList.get(i).computerId) +
                                " brand:" + computerList.get(i).brand +
                                " model:" + computerList.get(i).model ;
                        TextView textView = new TextView(this);
                        textView.setText(str);
                        mShowLinear.addView(textView);
                    }
                    mRemoteComputerManager.registerUser(mOnComputerArrivedListener);
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
                break;
            case R.id.clear_demo3_btn:
                mShowLinear.removeAllViews();
                break;
        }
    }

    private void bindService(){
        Intent intent = new Intent();
        intent.setAction("com.ljd.aidl.action.COMPUTER_OBSERVER_SERVICE");
        mIsBindService = bindService(intent,mConnection, Context.BIND_AUTO_CREATE);
    }

    private void unbindService(){
        if(!mIsBindService){
            return;
        }
        if (mRemoteComputerManager != null && mRemoteComputerManager.asBinder().isBinderAlive()){
            try {
                mRemoteComputerManager.unRegisterUser(mOnComputerArrivedListener);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
        unbindService(mConnection);
        mIsBindService = false;
    }
}

源码下载

  • 4
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值