Android中IPC机制的原理是什么

发布时间:2021-06-28 16:03:00 作者:Leah
来源:亿速云 阅读:241

Android中IPC机制的原理是什么,针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。

IPC是 Inter-Proscess  Communication的缩写,含义为进程间的通讯或者跨进程通讯,是指两个进程之间进行数据交换的过程。按操作系统的中的描述,线程是CPU调度最小的单元,同时线程是一种有限的系统资源,而进程是指一个执行单元,在PC和移动设备上指一个程序或者一个应用。一个进程可以包含多个线程,因此进程和线程是包含于被包含的关系。

IPC的使用场景就必须提到多进程,只有面对多进程这种场景下,才需要考虑进程间通讯。多进程的情况分为两种:***种是一个应用因为某些原因自身需要采用多进程模式来实现,原因有很多,应用特殊原因需要运行的单独的进程中,或者为了加大一个应用可使用内存所以需要通过多进程来获取多分内存空间。另外一种情况是:当前应用需要向其他应用获取数据,由于是两个应用,所以必须采取跨进程方式来获取所需要数据。

Android中的多进程模式

开启Android多进程模式很简单,就是给四大组件(Activity,Service,Receiver,ContentProvider)在AndroidMenifest中指定android:process属性。另外还有一种非常规的做法,那就是通过JNI在native层去fork一个新的进程。

给process指定多进程有两种不同的形式

进程名以 “:”的含义是指要在进程名前面附加上当前的包名,这个进程属于当前应用的私有进程,其他应用不可以和他跑在同一个进程。

这种属于全局进程,其他应用可以通过ShareUID方式可以和它跑在同一个进程,我们都知道系统会为每个应用分配一个唯一的UID,具有相同UID的应用才能共享数据。两个应用通过ShareUID跑在同一个进程,是需要相同的ShareUID并且签名相同才可以。不管它们是不是跑在同一个进程中,具有相同ShareUID的它们可以访问对方的私有数据,如:data目录、组件信息等。当然如果是在同一个进程中,除了data目录、组件信息还能共享内存数据。

多进程运行机制

我们知道Android为每一个应用分配了一个独立的虚拟机,或者说为每一个进程都分配了一个独立的虚拟机,不同的虚拟机在内存分配上有不同的地址空间,这就导致在不同的虚拟机访问同一个类的对象会产生多分副本。

所有运行在不同进程中的四大组件,只要它们之间需要通过内存来共享数据,都会共享失败,这也是多进程所带来的主要影响,一般来说,使用多进程会造成如下几方面的问题。

IPC基础概念介绍

Serializable是Java所提供的一个序列号接口,它是一个空接口,为对象提供标准的序列化和反序列化操作。使用Serializable相当简单,只需要实现Serializable接口并声明一个serialVersionUID,其实这个serialVersionUID也不是必需的,如果不声明这个serialVersionUID也是可以实现序列化的,但是这将会对反序列化过程产生影响。

//序列化  r user = new User("xia","123455");           ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("cache.txt"));           out.write(user);           out.close();           //反序列化           ObjectInputStream in = new ObjectInputStream(new FileInputStream("cache.txt"));           User newUser = (User)in.readObject();           in.close();

serialVersionUID是用来辅助序列化和反序列化过程的,原则上序列化后的数据中serialVersionUID只有和当前类serialVersionUID相同才能够正常的被反序列化。serialVersionUID的详细工作机制是这样的:序列化的时候系统会把当前类的serialVersionUID写入序列化的文件中(也可能是其他中介),但反序列化的时候会去检测文件中的serialVersionUID,看它是否和当前类的serialVersionUID一致,如果一致就说明序列化的版本和当前版本是相同的,这个时候可以成功的反序列化,否则就说明当前类和序列化的类相比发生了某些变换,比如成员变量的数量、类型发生了改变,这个时候无法正常反序列化。

一般来说,我们应该手动指定serialVersionUID的值,如1L,也可以根据自身结构自动去生成它的hash值,这样序列化和反序列化时两者的serialVersionUID是相同的。如果不指定serialVersionUID的值,反序列化时当前类有所改变,比如增加或者删除了某些成员变量,那么系统就会重新计算当前类型的hash值并把它赋值给serialVersionUID,这个时候当前类的serialVersionUID就和序列化数据中的serialVersionUID不一致,于是反序列化失败,程序就会出现crash。所以避免反序列化过程的失败。比如当版本升级后,我们很可能删除了某个成员变量也可能增加了一些新的成员变量,这个时候序列化过程仍然能够成功,程序可以***限度地恢复数据,相反,如果不指定serialVersionUID的话,程序则会挂掉。当然我们还要考虑另外一种情况,如果类的结构发生了非常规性的改变,比如修改了类名,修改了成员变量的类型,这个时候尽管serialVersionUID验证通过,但是反序列化还是会失败,因为类结构有了毁灭性的改变,根本无法从老版本的数据中还原出一个新的类结构对象。

静态成员变量属于类不属于对象,所以不会参与序列化过程,其次用transient关键字标记的成员变量不参与序列化配置。

- Parceable 接口

Parceable也是一个接口,只有实现这个接口,一个类的对象就可以实现序列化并可以通过Intent和Binder传递。

public class User implements Parcelable {            public int UserId;           public String userName;           public boolean isMale;            protected User(Parcel in) {                 //从序列化后的对象中创建原始对象               UserId = in.readInt();               userName = in.readString();               isMale = in.readByte() != 0;           }            public static final Creator<User> CREATOR = new Creator<User>() {               @Override               public User createFromParcel(Parcel in) {                     //从序列化后的对象中创建原始对象                   return new User(in);               }                @Override               public User[] newArray(int size) {                     //创建指定长度的原始对象数组                   return new User[size];               }           };            @Override           public int describeContents() {             /**                 返回当前对象的内容描述。如果含有文件描述符,返回1,否则返回0,几乎所有情况都返回0             */               return 0;           }            @Override           public void writeToParcel(Parcel dest, int flags) {                 /**将当前对象写入序列化结构中,其中flags,标识有0或1                  为1时标识当前对象需要作返回值返回,不能立即释放资源,几乎所有情况          都为0**/               dest.writeInt(UserId);               dest.writeString(userName);               dest.writeByte((byte) (isMale ? 1 : 0));           }       }

Parcel内部包装了可序列化的数据,可以在Binder中自由传输,从上述代码中可以看出,在序列化过程中需要实现的功能有序列化、反序列化和内部描述序列化功能由writeParcel方法完成,最终是通过Parcel中的一系列write方法来完成的。反序列化功能由CREATOR来完成,其内部标明了如何创建序列化对象和数组,并通过Parcel一系列read方法来完成反序列化过程;内容描述功能由describeContents来完成,几乎所有情况下这个方法都应该返回0,仅当当前对象中存在文件描述符时,此方法返回1.系统已经提供了许多实现Parcelable接口的类,它们都是可以直接序列化的,如:Intent、Bundle、Bitmap等,同时List  和 Map也可以序列化,前提时它们里面每个元素都是可序列化的。

既然Parcelable 和Serializable 都可以用于Intent间的数据传递,那么如何选择了。

- Serializable是Java中的序列化接口,其使用起来简单但是开销大,序列化和反序列化过程都需要大量的 I/O操作。

-  Parcelable是Android中的序列化方式,更适用于在Android平台上,它的缺点就是用起来稍微麻烦,但效率很高,这是Android推荐方式,因此,***Parcelable。但通过Parcelable将对象序列化到存储设备中或将对象序列化后通过网络传输也都是可以的,但是这个过程会稍显复杂,因此这种情况下建议使用Serializable。

- Binder

Binder是一个非常复杂,这里只是介绍下Binder的使用及上层实现原理。

Binder是Android中的一个类,它实现了IBinder的接口。从IPC角度来说,Binder是Android中一种跨进程的通讯方式,Binder还可以理解为一种虚拟物理设备,它的设备驱动是  /dev/binder,该通讯方式在Linux中没有;从Android  Framework,角度来说,Binder是ServiceManger连接各种Manger(ActivityManger  、WindowManger,等等)和相应的MangeSrervice的桥梁;从Android应用层来说,Binder是客户端和服务端进行通讯的媒介,当bindSrervice的时候,服务端会返回一个包含了服务端业务调用的Binder对象,通过这个Binder对象,客户端就可以用获取服务端提供的服务或者数据,这里的服务包括普通服务和基于AIDL的服务。普通Srervice中的Binder不涉及进程间通信,下面通过AIDL来分析Binder的工作过程。

//Book.java     public class Book implements Parcelable{           int id;           String type;            public Book(int id, String type) {               this.id = id;               this.type = type;           }            @Override           public String toString() {               return "Book{" +                       "id=" + id +                       ", type='" + type + '\'' +                       '}';           }        @Override       public int describeContents() {           return 0;       }        @Override       public void writeToParcel(Parcel dest, int flags) {           dest.writeInt(this.id);           dest.writeString(this.type);       }        protected Book(Parcel in) {           this.id = in.readInt();           this.type = in.readString();       }        public static final Creator<Book> CREATOR = new Creator<Book>() {           @Override           public Book createFromParcel(Parcel source) {               return new Book(source);           }            @Override           public Book[] newArray(int size) {               return new Book[size];           }       };    }        ```java       // Book.aidl       package com.example.xiahao.myapplication;       parcelable Book;        // IBookManager.aidl       package com.example.xiahao.myapplication;        // Declare any non-default types here with import statements       import com.example.xiahao.myapplication.Book;        interface IBookManager {            List<Book> getBookList();              void addBook(in Book book);       }上面三个文件中,Book.java是一个表示图书信息的类,它实现了Parcelable接口。Book.aidl 是Book类在AIDL中的声明。IBookManager.aidl是我们定义的一个接口,里面有两个方法 getBookList() 和addBook(),其中getBookList用于从远程服务端获取图书列表,而addBook是添加一本书。虽然Book类已经和IBookManager位于相同的包中,但IBookManager仍然需要导入Book类,这就是AIDL的特殊之处。builde的项目,系统为我们在gen目录下生产IBookManage.java的类,接下来我们需要根据这个系统生成的IBookManag类来分析Binder的工作原理  /*        * This file is auto-generated.  DO NOT MODIFY.        * Original file: /Users/xiahao/Documents/WorkSpace/AndroidStudioProjects/MyApplication/app/src/main/aidl/com/example/xiahao/myapplication/IBookManager.aidl        */       }

可以看到根据IBookManager.aidl系统为我们生成了IBookManager.java这个类,它继承了IInterface这个接口,同时它自己也还是个接口,所以可以在Binder中传输的接口都需要继承IInterface接口。

首先,它声明了两个方法getBookList 和 addBook  ,这就是我们在IBookManger.aidl中所声明的方法,同时它还声明了两个整数的id分别用于标识这两个方法,这两个id用标识在transact过程客户端请求的到底是哪个方法。接着,还声明了一个内部类Stub,这个Stub就是一个Biner类,当客户端和服务端都位于同一个进程中,方法调用不会走跨进程的transact过程,而当两者位于不同的进程中,方法需要走transact过程,这个逻辑由Stub的内部代理类  Proxy来完成。所以这个接口的实现核心就是它的内部类Stud和Stub的内部代理类 Proxy。

Binder的唯一标识,一般用于当前的Binder的类名表示,比如本例中的 “com.example.xiahao.IBookManger”

用于将服务端的Binder对象转换成客户端所需要的AIDL接口的类型对象,这种转换时区分进程的,如果客户端和服务端位于同一进程,那么此方法返回的就是服务端的Stub对象本身,否则返回的时系统封装后的Stub.proxy对象。

此方法用于返回当前的Binder对象

这个方法运行在服务端的Binder线程池中,当客户端发起跨进程请求的时,远程请求会通过系统底层封装后交由此方法来处理。该方法的原型为

@Override

public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel  reply, int flags)

服务端通过code可以确定客户端请求的目标方法是什么,接着从data中取出目标方法所需要的参数(如果目标方法有参数的话),然后执行目标方法,当目标方法执行完毕后,就向reply中写入返回值(如果目标方法有返回值的话),onTransact方法的执行过程就是这样的。需要注意的时,如果此方法返回false,那么客户端的请求就会失败,因此我们可以利用这个特性来做权限验证,毕竟我们也不希望随便一个进程都能远程调用我们的服务。

这个方法运行在客户端,当客户端调用此方法时,它的内部实现是这样的:首先创建该方法所需要的的输入型Prcel对象 _data、输出型Prcel对象  _reply和返回值对象List;然后把该方法的参数信息写入  _data中(如果有参数的话);接着调用transact方法来发起RPC(远程过程调用)请求,同时当前线程挂起;然后服务端onTransact方法会被调用,直到RPC过程返回后,当前线程继续执行,并  _reply中取出RPC过程的返回结果;***返回 _reply中的数据。

Proxy#getBookList

这个方法运行在客户端,它的执行过程和getBookList是一样的,addBook没有返回值,所以他不需要从  _reply中取出返回值。注意:当客户端发起远程请求时,由于当前线程会被挂起直至服务器返回数据,所以如果一个远程的方法是很耗时的话,那么不能再UI线程中发起次远程请求;其次,由于服务端的Binder方法运行在Binder的线程池中,所以Binder方法不管是否耗时都应该采用同步的方式实现,因为它已经运行在一个线程中了。为了更好的说明Binder,下面给出一个工作机制的图:

Android中IPC机制的原理是什么

接下来,介绍下Binder的两个很重要的方法 linkTodeath 和  unlinkTodeath,如果服务端的Binder连接断裂 (称之为 Binder  死亡),会导致我们远程调用失败。更为关键的时,如果我们不知道Binder的连接已经断裂,那么客户端的功能就会受到影响。为此我们可以给Binder设置一个死亡代理,当Binder死亡时,我们就会收到通知,这个时候我们就可以给Binder设置一个死亡代理,这个时候就可以重新发起连接请求从而恢复连接。

声明一个IBinder.DeathRecipient对象,IBinder.DeathRecipient是一个接口,其内部只有一个binderDied,我们需要实现这个方法,当binder死亡的时候,系统就会回调binderDied方法,然后我们就可以移除之前绑定的binder代理并重新绑定远程服务:

//销毁代理类,重启服务  private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {               @Override               public void binderDied() {                   Log.w(TAG, "binder: deed");                   mIBinderPool.asBinder().unlinkToDeath(mDeathRecipient, 0);                   mIBinderPool = null;                   connectBinderPoolService();               }           };

在客户端绑定远程服务成功后,给binder设置死亡代理

mIBinderPool = IBinderPool.Stub.asInterface(iBinder);                   try {                       mIBinderPool.asBinder().linkToDeath(mDeathRecipient,0);                   } catch (RemoteException e) {                       e.printStackTrace();                   }

其中linkDeath的第二个参数是个标记位,我们直接设为0即可。经过上面的两个步骤就给我们的binder设置了死亡代理,当binder死亡的时候我们就可以收到通知了。另外Binder的方法isBinderAlive也可以判断Binder是否死亡。

Android中的IPC方式

四大组件中的三大组件(Activity、Service、Receiver)都是支持Intent中传递Bundle数据的,由于Bundle实现了Pracel接口,所以它可以很方便的在不同的进程间传输。基于这一点,当我们调用了另一个进程中的Activity、Service、Receiver时,我们就可以在Bundle中附加我们需要传输给远程进程的信息并通过Intent发生出去。当然,被传输的数据必须能够被序列化,比如基本类型,实现了Pracelable、Serialzable接口以及一些Android支持的特殊对象。具体可以看Bundle这个类!

两个进程通过读/写同一个文件交换数据,比如A进程把数据写入文件,B进程通过读取这个文件来获取数据。由于Android系统基于Linux,所以并发读/写文件没有限制性,甚至两个线程对同一个文件进行写操作都是允许的,尽管这会出现问题,除了交换基本信息之外,我们可以序列化一个对象到文件系统中的同时从另一个进程恢复这个对象。文件共享的局限性是,并发读/写问题,如果并发读/写,读出的数据可能不是***的,如果是并发写的话就更严重了。

SharePreferences也属于文件的一种,但是由于系统对它的读/写有一定的缓存策略,即在内存中会有一份SharePreferences文件的缓存,因此在多进程的情况下,系统对它的读/写变得不可靠,当面对高并发读/写数据就很有很大几率丢失数据,不建议在进程间通信中使用SharePreferences。

通过Messenger可以在不同进程中传递Message对象,在Message中放入我们需要传入的数据,就可以实现数据的进程间传递了。Messenger是一种轻量级的IPC方案,它的底层实现是AIDL,并对AIDI做了封装,使得可以很方便的进行进程间通信。同时,由于处理一个请求,因此在服务端我们不用考虑线程同步的问题。

private static final String TAG = "MessengerService";               private static class MessengerServiceHandler extends Handler {                  @Override                  public void handleMessage(Message msg) {                      switch (msg.what) {                          case Constants.MSG_FROM_CLIENT:                              Log.i(TAG, "server form client\t" + msg.getData().getString("msg"));                              Messenger client = msg.replyTo;                              Message replyMessage = Message.obtain(null, Constants.MSG_FROM_SERVICE);                              Bundle bundle = new Bundle();                              bundle.putString("reply", "收到消息,我是服务端!");                              replyMessage.setData(bundle);                              try {                                  client.send(replyMessage);                              } catch (RemoteException e) {                                  e.printStackTrace();                              }                              break;                          default:                              super.handleMessage(msg);                      }                  }              }               private final Messenger mMessenger = new Messenger(new MessengerServiceHandler());               @Override              public IBinder onBind(Intent intent) {                  return mMessenger.getBinder();              }

在AndroidManifest 配置服务 android:process=”:remote”

- 客户端

private Messenger mMessenger;               private static final String TAG = "MainActivity";                @Override               protected void onCreate(Bundle savedInstanceState) {                   super.onCreate(savedInstanceState);                   setContentView(R.layout.activity_main);                   Intent intent = new Intent(this, MessengerService.class);                   bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);               }                private ServiceConnection mServiceConnection = new ServiceConnection() {                   @Override                   public void onServiceConnected(ComponentName name, IBinder service) {                       mMessenger = new Messenger(service);                       Message message = Message.obtain(null, Constants.MSG_FROM_CLIENT);                       Bundle bundle = new Bundle();                       bundle.putString("msg", "我是客户端");                       message.setData(bundle);                         //当客户端发送消息的时候,需要把接受服务端回复的Messenger通过Message的replytTo参数传递给服务端。                       message.replyTo = mGetReplyMessenger;                       try {                           mMessenger.send(message);                       } catch (RemoteException e) {                           e.printStackTrace();                       }                   }                    @Override                   public void onServiceDisconnected(ComponentName name) {                    }               };            private Messenger mGetReplyMessenger = new Messenger(new MessengerHandler());            private static class MessengerHandler extends Handler {               @Override               public void handleMessage(Message msg) {                   switch (msg.what) {                       case Constants.MSG_FROM_SERVICE:                           Log.i(TAG, "receive msg from Service\t" + msg.getData().getString("reply"));                           break;                       default:                           super.handleMessage(msg);                   }               }           }            @Override           protected void onDestroy() {               super.onDestroy();               unbindService(mServiceConnection);           }       ```

在Messenger中进行数据传递必须将数据放入Message中,而Messenger和Message都是实现了Parcelable接口,因此可以跨进程传输。实际上:通过Messenger来传输Message,Message中能使用的载体只有what、arg1、arg2、Bundle已经replyTo。Message的另一个字段object在同一进程中是很实用的,但是在进程间通信时候,非系统的Parcelable对象无法通过object字段来传输。但可以实用Bundle,Bundle可以支持大量的数据类型。

使用AIDL

用Messenger来进行进程间通信时是以串行的方式处理客户端发来的消息,如果大量的消息同时发送到服务端,服务端仍然只能一个个处理,如果大量的并发请求,那么用Messenger就不太合适了。同时Messenger的作用主要是为了传递消息,很多时候我们可能需要跨进程调用服务端方法,这个时候我们就可以用AIDL了。

大致步骤如下:

服务端首先要创建一个Service用来监听客户端的连接,然后创建一个AIDL文件,将暴露给客户端的接口在这个AIDL文件中声明,***在Service中实现这个AIDL文件即可。

首先绑定服务端的Service,绑定成功后,将服务端返回的Binder对象转成AIDL接口所属的类型,接着就可以调用AIDL中的方法。

// IBookManager.aidl  com.example.xiahao.myapplication;  // Declare any non-default types here with import statements import com.example.xiahao.myapplication.Book; import com.example.xiahao.myapplication.IOnNewBookArrivedListener;  interface IBookManager {       List<Book> getBookList();      void addBook(in Book book); }

在AIDL文件中,并不是所有的类型都支持的,支持的类型如下:

- 基本数据类型(int 、long 、char、 boolean 、double等);

- string 和CharSequence

- List 只支持ArrayList,里面每个元素必须能够被AIDL支持

- Map 只支持HashMap ,里面每个元素必须能够被AIDL支持,包括key 和value

- Parcelable:所有实现了Parcelable接口的对象

- AIDL:所有AIDL接口本身也可以在AIDL文件中使用。

以上6中类型,其中自定义Parcelable对象和AIDL文件必须显示的import进来,不管是否和当前的AIDL位于同一文件中。另外,如果AIDL用到了自定义的Parcelable对象必须新建一个和它同名的的AIDL文件,上面我们用到了Book,所以必须创建Book.aidl.

// Book.aidl  package com.example.xiahao.myapplication;  parcelable Book;

为了方便开发,建议把所以AIDL相关类和文件全部放入同一包中,当客户端是另外一个应用时,我们可以直接把整个包复制到客户端工程中。后面会给出一个书上的例子:具体包含,基本的AIDL调用,注册解注册,权限验证,断开重连,binder连接池一个服务处理多个AIDL的调用。

- 使用ContentProvide

ContentProvide专门用来应用之间的通讯,和Messenger一样,ContentProvide底层也是Binder,虽然底层Binder但使用要比AIDL简单多,因为系统帮我们做了封装。

- 使用Socket

通过Socket进行进程间的通讯,它分为流式套接字和用户数据套接字两种,分别对应网络协议层中的TCP和UDP协议。TCP是面向连接的协议,提供稳定的双向的通讯功能,TCP的建立需要经过  “三次握手”才能完成,为提供稳定的的数据传输功能,其本身提供了超时重连机制,因此具有很高的稳定性。而UDP是无连接的,提供不稳定的单向的通讯功能,当然UDP也可以实现双向通讯功能。在性能上,UDP具有更好的效率,其缺点就是不保证数据一定能够正确传输,尤其是在网络拥塞的情况下。

- 选用合适的IPC方式

给出书中的一张表格《Android开发艺术探讨》

名称 优点 缺点 适用场景

Bundle 简单易用 只能传输Bundle支持的数据类型 四大组件的进程间通信

文件共享 简单易用 不适合高并发场景,并且无法做到进程间的即时通讯 无并发访问情形,交换简单的数据实时性不高的场景

AIDL 功能强大,支持一对多并发通信,支持实时通信 使用稍复杂,需要处理好线程同步 一对多通信且RPC需要Messenger  功能一般,支持一对多串行通信,支持实时通信 不能很好的处理高并发情形,不支持RPC,数据通过Message进行传输,因此只能传输Bundle支持的数据类型  低并发的一对多即时通讯,无RPC需要,或者无需返回结果的RPC需求

ContentProvider 在数据访问功能很强大,支持一对多并发数据共享,可通过call方法扩展其他操作  可以理解为受约束的AIDL,主要提供数据的CRUD操作 一对多的进程间的数据共享

Socket 功能强大,可以通过网络传输字节流,并支持一对多并发实时通信 实现细节稍微有点繁琐,不支持直接的RPC 网络数据交换

关于Android中IPC机制的原理是什么问题的解答就分享到这里了,希望以上内容可以对大家有一定的帮助,如果你还有很多疑惑没有解开,可以关注亿速云行业资讯频道了解更多相关知识。

推荐阅读:
  1. MySQL中复制机制的原理是什么
  2. Java中SPI 机制的原理是什么

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

android

上一篇:Android中怎么利用TextView实现数字跳动效果

下一篇:Android中怎么利用 Input子系统监听线程的启动

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》