原创文章,转载请注明出处:http://blog.csdn.net/t5721654/article/details/7480696

Android系统本身提供了很多系统服务,如WindowManagerService,PowerManagerService等。下面描述一下添加一个系统服务的具体步骤。

1、  撰写一个aidl文件,定义服务的接口,将在编译过程中通过aidl工具生成对应的Java接口。一般系统服务的aidl文件都放在framework\base\core\java\android\os目录中。

以我所写的IMyTool.aidl为例。在.aidl中定义自己需要加入的方法,编写规则和java接口差不多,这里不多说。

2、  将aidl文件名添加到frameworks\base\目录下的Android.mk编译脚本文件中。

如:

LOCAL_SRC_FILES += \

core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl\

…\

core/java/android/os/IMyTool.aidl\

IMyTool.aidl即我加进去的aidl文件,加入后才能在make过程中编译到,否则将在后面的SystemServer添加系统服务时会报错提示找不到对应类。

3、  编写真正工作的服务类,继承IMyTool.Stub类(AIDL文件名.Stub,aidl生成的接口中的内部类,是一个Binder)。

服务类一般都放在framework\base\services\java\com\android\server目录中。

例如:

public class MyToolService extends IMyTool.Stub {

实现IMyTool.aidl中定义的接口。

}

4、  将自定义服务注册到SystemServer,使得开机过程中被添加。

在framework\base\services\java\com\android\server目录中的SystemServer中启动服务代码处加入:

try {

Slog.i(TAG, "MyToolService");

ServiceManager.addService(Context.MY_TOOL_SERVICE,new MyToolService(context));// MyToolService构造函数自己定义,一般都会用到Context

} catch(Throwable e) {

Slog.e(TAG, "Failure startingMyToolService", e);

}

上面代码中Context.MY_TOOL_SERVICE是自己在Context类中定义的常量,也就是给服务定义的名字,使用常量方便获取服务,而不需要记住注册服务时用的名字,且想换名字时只需改一个常量的值。

5、  由于在工程中添加了自己定义的类及常量,系统的api没有更新,因此需要先在工程中make clean然后make
update-api,执行完后会发现frameworks\base\api\current.xml文件中多出自己定义的一些东西。
current.xml这个文件包含了所有系统所有能被应用层使用的类及其方法等。

之后再使用make编出来的固件及jar包就能包含自定义的接口。

编译后如何使用:

将编出来的jar包通过lib方式导入工程。jar包位置:out\target\common\obj\JAVA_LIBRARIES\framework_intermediates\classes.jar

调用以下代码获取自定义服务:

IMyTool myTool = IMyTool.Stub.asInterface(ServiceManager.getService(MY_TOOL_SERVICE));

MY_TOOL_SERVICE即在Context中定义的常量。获取到myTool后就可以调用在aidl文件中定义的接口了。

Android Binder机制----实现自定义的系统服务

一.基于源码分析Binder机制:

Binder机制是Android系统中实现跨进程通信(IPC)的一种重要机制。可以说,Binder机制在android系统中无处不在,所以,要研究android源码,学好Binder机制极其重要。

在学习Binder机制之前,我们先试着摸索一下系统中一些相关的涉及到Binder机制的代码。

首先,先看看SystemServer.Java这个文件(基于android4.0源代码),该文件位于源码路径frameworks\base\services\java\com\android\server\SystemServer.java中,这个文件中有两个类,一个是SystemServer类(public),一个是线程类ServerThread。

SystemServer类的main函数中会先调用函数init1(),代码如下:

  1. public class SystemServer {
  2. ....
  3. public static void main(String[] args) {
  4. ....
  5. init1(args);
  6. }
  7. ....
  8. }

init1()是native函数,它的定义为native public static void init1(String[] args);它的内部会进行一些与Dalvik虚拟机相关的初始化工作,执行完初始化工作后,其内部会调用java端的SystemServer类的init2()函数。

而SystemServer类中的init2()函数的实现代码如下:

  1. public static final void init2() {
  2. Slog.i(TAG, "Entered the Android system server!");
  3. Thread thr = new ServerThread();
  4. thr.setName("android.server.ServerThread");
  5. thr.start();
  6. }

该函数实现创建和启动ServerThread线程。

我们再来看看ServerThread线程类的run()方法中的部分代码,如下:

  1. class ServerThread extends Thread {
  2. ....
  3. @Override
  4. public void run() {
  5. ....
  6. // Critical services...
  7. try {
  8. Slog.i(TAG, "Entropy Service");
  9. ServiceManager.addService("entropy", new EntropyService());
  10. Slog.i(TAG, "Power Manager");
  11. power = new PowerManagerService();
  12. ServiceManager.addService(Context.POWER_SERVICE, power);
  13. ....
  14. Slog.i(TAG, "Telephony Registry");
  15. ServiceManager.addService("telephony.registry", new TelephonyRegistry(context));
  16. ....
  17. }
  18. }
  19. }

在该线程类的run()函数中,很多的try{}代码块里,有如上类似的代码,它创建一个服务类,然后将初始化的服务类作为参数传进ServiceManager的addService函数中。也就是说,在frameworks\base\services\java\com\android\server源码路径里的服务类基本上都是在这里启动的。

而且这些服务类,一般都是继承于“I服务类名.Stub”(如IVibratorService.Stub,但有些继承Binder),I服务类名(如 IVibratorService)是由aidl文件自动生成的。要想看系统中这些aidl文件生成的类(编译生成),可以在编译后的源码中,out文件 目录下查找。比如,我要找IVibratorService.aidl生成的java文件:进入out目录,在Linux终端输入命令:find -name “IVibratorService.java”。这样就可以找到该类了。

我们再来看看ServiceManager这个类(源码所在路径\frameworks\base\core\java\android\os\ServiceManager.java):

  1. /** @hide */
  2. public final class ServiceManager {
  3. private static final String TAG = "ServiceManager";
  4. private static IServiceManager sServiceManager;
  5. private static HashMap<String, IBinder> sCache = new HashMap<String, IBinder>();
  6. private static IServiceManager getIServiceManager() {
  7. if (sServiceManager != null) {
  8. return sServiceManager;
  9. }
  10. // Find the service manager
  11. sServiceManager = ServiceManagerNative.asInterface(BinderInternal.getContextObject());
  12. return sServiceManager;
  13. }
  14. /**
  15. * Returns a reference to a service with the given name.
  16. *
  17. * @param name the name of the service to get
  18. * @return a reference to the service, or <code>null</code> if the service doesn't exist
  19. */
  20. public static IBinder getService(String name) {
  21. try {
  22. IBinder service = sCache.get(name);
  23. if (service != null) {
  24. return service;
  25. } else {
  26. return getIServiceManager().getService(name);
  27. }
  28. } catch (RemoteException e) {
  29. Log.e(TAG, "error in getService", e);
  30. }
  31. return null;
  32. }
  33. /**
  34. * Place a new @a service called @a name into the service
  35. * manager.
  36. *
  37. * @param name the name of the new service
  38. * @param service the service object
  39. */
  40. public static void addService(String name, IBinder service) {
  41. try {
  42. getIServiceManager().addService(name, service);
  43. } catch (RemoteException e) {
  44. Log.e(TAG, "error in addService", e);
  45. }
  46. }
  47. /**
  48. * Retrieve an existing service called @a name from the
  49. * service manager.  Non-blocking.
  50. */
  51. public static IBinder checkService(String name) {
  52. try {
  53. IBinder service = sCache.get(name);
  54. if (service != null) {
  55. return service;
  56. } else {
  57. return getIServiceManager().checkService(name);
  58. }
  59. } catch (RemoteException e) {
  60. Log.e(TAG, "error in checkService", e);
  61. return null;
  62. }
  63. }
  64. /**
  65. * Return a list of all currently running services.
  66. */
  67. public static String[] listServices() throws RemoteException {
  68. try {
  69. return getIServiceManager().listServices();
  70. } catch (RemoteException e) {
  71. Log.e(TAG, "error in listServices", e);
  72. return null;
  73. }
  74. }
  75. /**
  76. * This is only intended to be called when the process is first being brought
  77. * up and bound by the activity manager. There is only one thread in the process
  78. * at that time, so no locking is done.
  79. *
  80. * @param cache the cache of service references
  81. * @hide
  82. */
  83. public static void initServiceCache(Map<String, IBinder> cache) {
  84. if (sCache.size() != 0) {
  85. throw new IllegalStateException("setServiceCache may only be called once");
  86. }
  87. sCache.putAll(cache);
  88. }
  89. }

该类中提供一个getService(String name)的静态函数,参数name对应addService(String name, IBinder service)函数的name。name参数在SystemServer启动一个初始化的服务类,然后调用addService函数添加的时候指定(如ServiceManager.addService("entropy", new EntropyService()),name为“entropy”),那么当我们要在另外一个进程里(客户端)获取在SystemServer中启动运行的服务*(服务端),就需要调用ServiceManager类的getService(String name)函数了(如, 我们要在客户端获取EntropyService服务对象,那么就需要调用 ServiceManager.getService(“entropy”)),而通过getService获取的是一个IBinder,接着再通过调用 对应服务类的aidl文件生成的接口的内部类Stub的asInterface(iBinder)方法,(比如,IAccountManager mRemoteService = IAccountManager.Stub.asasInterface(iBinder)),参数iBinder就是通过 getService(String name)获取得到的对应的IBinder。这样,客户端就获取得到远程服务对象的代理,并不是服务类对象本身(具体后面会介绍)。

但是,我们发现,ServiceManager这个类是隐藏的(/** @hide */),也就是说在android SDK中不提供该类的接口。如果在开发的第三方应用中需要调用该类,只能通过反射来调用。

二.注册自定义的系统服务(Service类为客户端服务):

所谓的系统服务指可以使用getSystemService()方法获取的服务,即Binder服务,由继承Binder的类来实现。而我们开发第三方应用所常用的Service类服务为客户端服务。

当然,如果我们有需求,需要系统中注册运行一个自定义的系统服务,该服务在手机开机后执行一直运行着的工作(如,保存一些资源在该服务端,需要时,可以时 刻获取)。那么,通过上面的介绍,我们可以在SystemServer中注册我们自己编写的系统服务。比如:假如MyService是自己定义的需要注册 在SystemServer中的服务类,那么,我们可以在ServerThread类run函数中,try{}代码块添加如下代码: ServiceManager.addService("myservice", new MyService());

而 这个我们自定义的MyService,我们该如何去实现它,已到达类似其他系统服务类的效果呢。通过上面的介绍,我们知道,系统服务类一般都继承由 aidl文件生成的一个类的内部类(Stub)。抱着好奇的心态,我们不妨也类试试写一个IMyService的aidl文件,看看它生成的对应的 java文件中的代码实现。关于aidl文件的相关知识,可以去参考这篇博客:http://www.cnblogs.com/over140/archive/2011/03/08/1976890.html

下面的代码是在eclipse下的一个android工程项目中,建一个名为IMyService的后缀名aidl文件,然后eclipse会自动在工程的gen目录中生成的java代码;

IMyService.aidl文件中的定义:

interface IMyService
          {
             String getName();

void setName(String name);
          }

android工程gen目录中生成的IMyService.java文件代码:

  1. /*
  2. * This file is auto-generated.  DO NOT MODIFY.
  3. * Original file: D:\\WorkSpace\\RemoteServiceTest\\src\\com\\feixun\\hu\\IMyService.aidl
  4. */
  5. //IInterface是一个接口,提供一个asBinder()的方法声明,返回值为IBinder
  6. public interface IMyService extends android.os.IInterface {
  7. /** Local-side IPC implementation stub class. */
  8. //该内部类(一般称为桩)继承Biner,同时实现IMyService接口,也就说覆盖asBinder()方法
  9. public static abstract class Stub extends android.os.Binder implements
  10. IMyService {
  11. private static final java.lang.String DESCRIPTOR = "IMyService";
  12. /** Construct the stub at attach it to the interface. */
  13. public Stub() {
  14. this.attachInterface(this, DESCRIPTOR);
  15. }
  16. /**
  17. * Cast an IBinder object into an IMyService interface, generating a
  18. * proxy if needed.
  19. */
  20. /*客户端通过调用该方法实现跨进程访问,参数为对应获取得到的远程服务IBinder,
  21. * 一般是通过ServiceManager.getService获取
  22. */
  23. public static IMyService asInterface(android.os.IBinder obj) {
  24. if ((obj == null)) {
  25. return null;
  26. }
  27. android.os.IInterface iin = (android.os.IInterface) obj
  28. .queryLocalInterface(DESCRIPTOR);
  29. //若需要获取的服务为本地服务,则直接返回。
  30. if (((iin != null) && (iin instanceof IMyService))) {
  31. return ((IMyService) iin);
  32. }
  33. /*若不是本地进程所属服务(即跨进程服务),则返回创建的Proxy对象
  34. * 传递的参数obj,赋值给Binder驱动中的mRemote引用
  35. */
  36. return new IMyService.Stub.Proxy(obj);
  37. }
  38. public android.os.IBinder asBinder() {
  39. return this;
  40. }
  41. /*服务端重载的OnTransact,参数code用于标识客户端期望调用服务端的哪个函数
  42. * 参数data和reply均为包裹,都由客户端提供
  43. * data负责打包由客户端调用服务端函数时传进的参数
  44. * reply负责打包执行服务端函数后返回的结果数据,供客户端读取
  45. * flags定义执行IPC调用的模式,flags=等于0时为双向,即服务端执行完后返回一定的数据,
  46. * 为1时单向,即服务端执行完后不返回任何数据。
  47. */
  48. @Override
  49. public boolean onTransact(int code, android.os.Parcel data,
  50. android.os.Parcel reply, int flags)
  51. throws android.os.RemoteException {
  52. switch (code) {
  53. case INTERFACE_TRANSACTION: {
  54. reply.writeString(DESCRIPTOR);
  55. return true;
  56. }
  57. case TRANSACTION_getName: {
  58. //某种校验,与客户端的writeInterfaceToken()对应
  59. data.enforceInterface(DESCRIPTOR);
  60. /*调用MyService类的getName,该类实现IMyService.Stub
  61. * 即为真正实现功能的远程服务类,getName方法的具体实现
  62. * 在MyService中
  63. */
  64. java.lang.String _result = this.getName();
  65. reply.writeNoException();
  66. //将getName返回值_result写入reply
  67. reply.writeString(_result);
  68. return true;
  69. }
  70. case TRANSACTION_setName: {
  71. data.enforceInterface(DESCRIPTOR);
  72. java.lang.String _arg0;
  73. //读取客户端发来的data包裹,进行拆解得到客户端传进的相关参数
  74. _arg0 = data.readString();
  75. //setName方法没有返回值,所以不需要reply向客户端返回数据
  76. this.setName(_arg0);
  77. reply.writeNoException();
  78. return true;
  79. }
  80. }
  81. return super.onTransact(code, data, reply, flags);
  82. }
  83. //Binder驱动实现的代理机制,供远程客户端使用
  84. private static class Proxy implements IMyService {
  85. //远程服务端在Binder驱动中对应的Binder引用
  86. private android.os.IBinder mRemote;
  87. Proxy(android.os.IBinder remote) {
  88. //remote来自远程服务端对应的obj
  89. mRemote = remote;
  90. }
  91. public android.os.IBinder asBinder() {
  92. return mRemote;
  93. }
  94. public java.lang.String getInterfaceDescriptor() {
  95. return DESCRIPTOR;
  96. }
  97. public java.lang.String getName() throws android.os.RemoteException {
  98. //申请得到_data,_reply包裹对象(相当于创建对象)
  99. android.os.Parcel _data = android.os.Parcel.obtain();
  100. android.os.Parcel _reply = android.os.Parcel.obtain();
  101. java.lang.String _result;
  102. try {
  103. //与服务端的enforceInterface对应
  104. _data.writeInterfaceToken(DESCRIPTOR);
  105. //通过mRemote重载远程服务的onTransact方法,调用服务端的getName()函数
  106. mRemote.transact(Stub.TRANSACTION_getName, _data, _reply, 0);
  107. _reply.readException();
  108. //读取远程服务端getName函数返回的_result
  109. _result = _reply.readString();
  110. } finally {
  111. _reply.recycle();
  112. _data.recycle();
  113. }
  114. return _result;
  115. }
  116. public void setName(java.lang.String name)
  117. throws android.os.RemoteException {
  118. android.os.Parcel _data = android.os.Parcel.obtain();
  119. android.os.Parcel _reply = android.os.Parcel.obtain();
  120. try {
  121. _data.writeInterfaceToken(DESCRIPTOR);
  122. //往data包裹写入客户端传进的参数name,并发送到服务端
  123. _data.writeString(name);
  124. //通过mRemote重载远程服务的onTransact方法,调用服务端的setName()函数
  125. mRemote.transact(Stub.TRANSACTION_setName, _data, _reply, 0);
  126. _reply.readException();
  127. } finally {
  128. _reply.recycle();
  129. _data.recycle();
  130. }
  131. }
  132. }
  133. static final int TRANSACTION_getName = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
  134. static final int TRANSACTION_setName = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
  135. }
  136. public java.lang.String getName() throws android.os.RemoteException;
  137. public void setName(java.lang.String name)
  138. throws android.os.RemoteException;
  139. }

IMyService.java文件解析:
   1.该接口是由自定义的IMyService.aidl文件自动生成的,IMyService实现IInterface接口,IInterface提供一个asBinder()的方法声明,返回值为IBinder。

2.Stub类为IMyService接口的内部类,该内部类(一般称为桩)继承Biner,同时实现IMyService接口,也就说覆盖asBinder()方法。该类主要由服务端来使用。

3.Stub类中又有一个内部类Proxy(代理),该类就是实现跨进程访问的重要类。客户端跨进程获取的代理对象就是Proxy。

4.Proxy类中有一个mRemote,该mRemote为远程服务在Binder对象中对应的引用,通过该引用实现与远程服务的连接。

aidl文件编写完后,接下来就是实现功能代码块的系统服务类的编写了,这里我们将该系统服务实现类命名为MyService,该类的实现代码如下:

  1. import android.os.RemoteException;
  2. public class MyService extends IMyService.Stub
  3. {
  4. private String ServiceName;
  5. //重载IMyService接口的getName方法
  6. @Override
  7. public String getName() throws RemoteException
  8. {
  9. // TODO Auto-generated method stub
  10. return ServiceName;
  11. }
  12. //重载IMyService接口的setName方法
  13. @Override
  14. public void setName(String name) throws RemoteException
  15. {
  16. // TODO Auto-generated method stub
  17. ServiceName = name;
  18. }
  19. }

当然,自定义的系统服务类MyService中的函数,还可以通过调用一些native方法实现调用Android中间层的先关函数(JNI)。ok,系统服务类代码编写完后,接下来就是在ServerThread线程中注册该系统服务了,如下注册代码:

ServiceManager.addService("myservice", new MyService());

三.实现跨进程访问自定义的系统服务


统服务类的创建和注册已经完成,那么,该如何进行IPC调用呢。很显然,通过前面对IMyService.java文件的介绍和解析,我们知道,重点在于
IMyService.Stub的asInterfcace函数。比如,我们在另外一个进程中编写一个MyServiceManager类来实现获取远程
MyService服务,并实现调用相关的函数。代码如下:

  1. import android.os.IBinder;
  2. import android.os.ServiceManager;
  3. public class MyServiceManager
  4. {
  5. private static IMyService mRemoteService;
  6. public static IMyService getService()
  7. {
  8. if(mRemoteService == null)
  9. {
  10. //参数对应在ServerThread中注册系统服务时指定的参数,即"myservice"
  11. IBinder iBinder = ServiceManager.getService("myservice");
  12. mRemoteService = IMyService.Stub.asInterface(iBinder);
  13. }
  14. return mRemoteService;
  15. }
  16. public static String getName()
  17. {
  18. getService().getName();
  19. }
  20. public static String setName(String name)
  21. {
  22. getService().setName(name);
  23. }
  24. }

这样,不同进程的客户端就可以通过该类来实现与系统服务的连接和调用了。

注:上
面的相关类实现都是基于源码环境开发的,而不是第三方开发。
MyService.java,IMyService.java,MyServiceManager.java等文件是存放在源码路径
frameworks\base\core\java\android\MyProject中,MyProject是自己创建的文件夹,编译也都是在
Linux环境下基于源码编译的。

Android 添加系统服务的更多相关文章

  1. 【转】Android 添加系统服务

    Android系统本身提供了很多系统服务,如WindowManagerService,PowerManagerService等.下面描述一下添加一个系统服务的具体步骤. 1.  撰写一个aidl文件, ...

  2. 在Android 源码中添加系统服务

    Android系统本身提供了很多系统服务,如WindowManagerService,PowerManagerService等.下面描述一下添加一个系统服务的具体步骤. 1.定义自定义系统服务接口 撰 ...

  3. Android添加快捷方式(Shortcut)到手机桌面

    Android添加快捷方式(Short)到手机桌面 权限 要在手机桌面上添加快捷方式,首先需要在manifest中添加权限. <!-- 添加快捷方式 --> <uses-permis ...

  4. android 添加依赖的库文件

    Notpad: 2016-3-16: 1.android 添加依赖的库文件 右键自己的项目 -> properties ->android ->在Library处点击add -> ...

  5. Android 添加网络权限

    [Android 添加网络权限] <uses-permission Android:name="android.permission.INTERNET"></us ...

  6. android添加系统(服务、应用)

    1. 添加系统服务 1.1 添加方式1:(不加入servicemanager统一管理的) 看Android6.0.1 init.rc解析中的第2章和第3章 方式1: 1). 写一个测试脚本test.s ...

  7. nginx添加系统服务(start|stop|restart|reload)

    nginx添加系统服务 1.编写脚本,名为nginx #vim /etc/init.d/nginx #!/bin/bash#chkconfig: - 99 20 #description: Nginx ...

  8. Android 添加framework资源包

    为Android系统添加一个新的资源包 概述 传统的Android系统只有一个framework-res.apk资源包,第三方厂商在进行rom定制时会直接修改framework res资源,达到适配目 ...

  9. Android 添加键值并上报从驱动到上层

    转载:https://blog.csdn.net/weixin_43854010/article/details/94390803 Android 添加键值并上报从驱动到上层 平台 :RK3288 O ...

随机推荐

  1. PCL—低层次视觉—关键点检测(rangeImage)

    关键点又称为感兴趣的点,是低层次视觉通往高层次视觉的捷径,抑或是高层次感知对低层次处理手段的妥协. ——三维视觉关键点检测 1.关键点,线,面 关键点=特征点: 关键线=边缘: 关键面=foregro ...

  2. mongodb管理工具rockmongo

    mongodb的图像管理工具非常之多,我用的是rockmongo. RockMongo 是一个PHP5写的MongoDB管理工具. 主要特征: 使用宽松的New BSD License协议 速度快,安 ...

  3. 10个最佳的PHP图像操作库

    Thomas Boutell 以及众多的开发者创造了以GD图形库闻名的一个图形软件库,用于动态的图形计算. GD提供了对于诸如C, Perl, Python, PHP, OCaml等等诸多编程语言的支 ...

  4. 面试题_17_to_30_数据类型和 Java 基础面试问题

    17)Java 中应该使用什么数据类型来代表价格?(答案)如果不是特别关心内存和性能的话,使用BigDecimal,否则使用预定义精度的 double 类型. 18)怎么将 byte 转换为 Stri ...

  5. 1346. Intervals of Monotonicity(dp)

    1346 简单dp #include <iostream> #include<cstdio> #include<cstring> #include<algor ...

  6. 函数ut_2_log

    计算某个数的对数(最大的) 例如 16 计算后为 4 2的4次方为16 例如15 计算后为3 2的3次方为8 /******************************************** ...

  7. Linux LiveCD 的制作

    Knoppix,只需一张光盘, 就能够让我们在任何场所,随心所欲地使用 Linux1, 打破了操作系统只能先安装再使用的传统概念. Knoppix 最初的设计用途是教学,但由于这项技术很受欢迎,使得  ...

  8. Asp.Net操作FTP方法

    将用户上传的附件(文件.图片等)通过FTP方式传送到另外一台服务器上,从而缓解服务器压力 1.相关的文章如下: Discuz!NT中远程附件的功能实现[FTP协议] http://www.cnblog ...

  9. MyEclipse的快捷使用(含关联源码和Doc的方式)

    删除行代码 :在Eclipse中将光标移至待删除的行上,然后按Ctrl+d 组合键 快速导入包 :在Eclipse中将光标移至相应的类上面,按Ctrl+Shift+M 组合键 批量行注释 :Ctrl+ ...

  10. 物联网操作系统HelloX开发者入门指南

    HelloX开发者入门指南 HelloX是聚焦于物联网领域的操作系统开发项目,可以通过百度搜索"HelloX",获取详细信息.当前开发团队正在进一步招募中,欢迎您的了解和加入.如果 ...