另辟蹊径创建移动应用:iOS和Android代码共享

移动开发 Android iOS
在苹果和安卓的应用商店中有成千上万各种用途的移动应用。iOS设备上的应用通常使用Objective-C工具库创建而成,而安卓设备上的应用则 基于Java语言。在这篇文章中我们将向您展示两种不太常用的使用Java和Xtend构建原生应用的方法,这两种方法能够帮助开发者在两个应用平台上共 享代码,从而简化开发工作。

[[120234]]

过去几年,移动应用席卷了整个世界,在工作和生活的方方面面改变着我们使用互联网的方式。创建移动应用的各种技术也随之兴起,各种开发流程也 将移动应用视为一等公民,开始考虑适应移动开发的流程。尽管已经让人感觉无处不在,真正的移动应用时代才刚刚开始。我们即将面对新一代的移动设备,如可穿戴设备或组成物联网的各种各样的移动装置。我们将面临全新的用于数据展示和命令接收的用户交互接口。我们也认识到越来越多的公司将真正采取移动优先的战 略。所有的这些都将对我们未来几年设计、开发和测试软件的方式产生巨大影响。

在苹果和安卓的应用商店中有成千上万各种用途的移动应用。iOS设备上的应用通常使用Objective-C工具库创建而成,而安卓设备上的应用则 基于Java语言。在这篇文章中我们将向您展示两种不太常用的使用Java和Xtend构建原生应用的方法,这两种方法能够帮助开发者在两个应用平台上共享代码,从而简化开发工作。

使用Java和RoboVM开发原生iOS应用

将安卓和iOS两个平台同时作为目标平台的移动应用开发者经常面临很多挑战。在比较这两个平台的原生应用开发环境时,例如分别由谷歌和苹果提供的开 发工具链,很容易就能够发现这两者之间有着本质的区别。谷歌所提供的安卓开发环境是基于Eclipse集成开发环境和Java程序设计语言的。而由苹果所 提供的iOS开发环境则是基于Xcode集成开发环境和Objective-C程序设计语言的。

这些平台间的差异导致代码无法重用。而且很少有开发者能够同时精通两个环境。最终就导致几乎每个跨平台的移动应用都需要为各个平台准备单独的开发团队并使用单独的代码库。

RoboVM是一个新的开源项目,旨在不影响开发者和应用用户体验的前提下解决上述问题。RoboVM项目的目标是在iOS设备上使用Java和其 他JVM语言,如Scala,Clojure和Kotlin。与其他类似的工具不同,RoboVM不会对开发者所使用的Java平台特性做任何限制,如反 射机制或文件I/O,并且还允许开发人员重用Java庞大的第三方库生态系统。RoboVM的独特之处还在于开发人员能够通过一个Java到 Objective-C的桥接器访问到完整的原生iOS API。这样,应用程序开发时,就能够用Java语言编写真正的原生用户交互界面并且能够获取到完整的硬件访问权限,同时使用的开发工具也是Java开发 人员所熟悉的Eclipse和Maven等。

使用RoboVM,进行跨平台开发将变得相对容易;同一组Java开发人员就有能力构建两个版本的移动应用程序并且代码库中的相当一部分代码都能够被共享。

如何开始?

RoboVM有多种调用方式,如命令行方式或使用Maven或Gradle,最容易上手的方式应该是使用RoboVM的Eclipse插件。

配置要求

安装RoboVM的Eclipse插件之前,请确保系统满足如下要求:

  • 一台运行Mac OS X 10.9操作系统的Mac电脑。
  • Oracle Java SE 7 JDK。
  • 从Mac应用商店下载的Xcode 5.x集成开发环境。

需要注意的是,必须使用Oracle Java SE 7 JDK运行Eclipse。Eclipse无法正常运行在苹果的Java 6 JVM之上。

安装RoboVM的Eclipse插件

系统满足所有的先决条件之后,安装插件是一项很简单的工作。从Eclipse的Help菜单中打开Eclipse Marketplace,搜索RoboVM并点击Install Now即可。

或者也可以使用如下更新站点

运行一个简单的iOS应用

接下来我们将创建一个简单的iOS应用。首先创建一个新的项目:File => New => Project......。在列表中选择RoboVM iOS Project向导。

在Project Name,Main Class和App name栏中输入IOSDemo,在App id一栏中输入org.robovm.IOSDemo。其他栏目保持默认值点击Finish。

然后,创建一个新的名为IOSDemo的类文件,省略包名。将下面的代码拷贝粘贴到新创建的文件中,替换Eclipse自动生成的代码。

  1. import org.robovm.apple.coregraphics.*; 
  2. import org.robovm.apple.foundation.*; 
  3. import org.robovm.apple.uikit.*; 
  4.  
  5. public class IOSDemo extends UIApplicationDelegateAdapter { 
  6.  
  7.     private UIWindow window = null
  8.     private int clickCount = 0
  9.  
  10.     @Override 
  11.     public boolean didFinishLaunching(UIApplication application, 
  12.             NSDictionary launchOptions) { 
  13.  
  14.         final UIButton button = UIButton.create(UIButtonType.RoundedRect); 
  15.         button.setFrame(new CGRect(115.0f, 121.0f, 91.0f, 37.0f)); 
  16.         button.setTitle("Click me!", UIControlState.Normal); 
  17.  
  18.         button.addOnTouchUpInsideListener(new UIControl.OnTouchUpInsideListener() { 
  19.             @Override 
  20.             public void onTouchUpInside(UIControl control, UIEvent event) { 
  21.                 button.setTitle("Click #" + (++clickCount), UIControlState.Normal); 
  22.  
  23.                 } 
  24.             }); 
  25.  
  26.             window = new UIWindow(UIScreen.getMainScreen().getBounds()); 
  27.             window.setBackgroundColor(UIColor.colorLightGray()); 
  28.             window.addSubview(button); 
  29.             window.makeKeyAndVisible(); 
  30.  
  31.             return true
  32.        } 
  33.        
  34.        public static void main(String[] args) { 
  35.             NSAutoreleasePool pool = new NSAutoreleasePool(); 
  36.             UIApplication.main(args, null, IOSDemo.class); 
  37.             pool.close(); 
  38.        } 

***,右键点击刚刚创建的项目,选择Run As... =>iOS Simulator App (iPhone),在iOS模拟器中启动应用程序。这样,应用程序就运行在一个模拟的iPhone上了。

[[120235]]

如果需要在真实的设备上运行应用程序,需要使用Run As... =>iOS Device App选项。需要注意的是,执行这一选项之前,所用到的设备需要进行相应的设置。设置的过程超出了本文的讨论范围。详细信息请参照苹果的官方文档

#p#

创建用于应用商店发布的IPA文件

如果应用商店的发布证书(Distribution Certificate)和应用描述文件(Provisioning Profile)已经准备妥当,创建用于提交到应用商店的IPA软件包只需要在Eclipse中右键点击RoboVM iOS项目,选择RoboVM Tools=>Package for App Store/Ad-Hoc distribution… ,填写对话框中相关信息即可。

完成上述操作后,将会在目标文件夹中生成一个后缀为.IPA的文件。使用Application Loader即可验证生成的IPA文件并将其提交到应用商店中。使用Spotlight可以很方便地定位到Application Loader应用。

从苹果网站上能够找到很多关于如何加入iOS开发者计划及如何创建用于应用商店发布的证书和应用描述文件的资源

底层实现机制

字节码编译器

RoboVM的核心是它的预编译器。预编译器可以通过命令行或者Maven、Gradle等构建工具或IDE调用。它可以将Java字节码翻译成用 于特定操作系统和CPU型号的机器码。一般来说会翻译成用于iOS系统和ARM处理器的机器码,不过RoboVM也支持将字节码转为运行在x86 CPU(32位)上的Mac OS X和Linux系统的机器码。

这种预编译的方法与Oracle Hotspot之类的传统JVM的工作机制有很大的区别。这些JVM通常会在运行时读取Java字节码,然后以某种方式执行包含在字节码的虚拟机指令。为 了加快这一进程,JVM采用了一种被称为即时编译的技术。简单来说,这个过程会在程序***次调用某个方法时,将这一方法的虚拟机指令翻译成当前系统所用的 CPU型号对应的机器码。

由于苹果内置于iOS中的技术限制,在iOS应用中使用任何形式的即时编译技术都是不可能的。唯一的替代方案就是使用解释器或像RoboVM中所用 的预编译技术,而解释器这种方式速度很慢并且十分耗电。预编译的过程发生在使用开发者的机器进行编译的时候,在iOS设备上运行时,生成的机器码就能够全 速运行,因此在速度上可以与由Objective-C编译生成的代码媲美,甚至可能会更快一些。

由于RoboVM预编译器消费Java字节码,而不是Java源代码,因此至少在理论上可以用于任何能够编译成字节码的JVM语言。目前已知的 RoboVM预编译器能够正常工作的JVM语言有Scala,Clojure和Kotlin。这种方法的另一个好处是,可在无需任何原始源代码的情况下, 在标准JAR文件中的第三方库上使用RoboVM,这样就可以在应用中使用专有的和闭源的库。

增量编译

即使是非常简单的RoboVM应用,例如IOSDemo应用,在***次启动时,都需要耗费较长一段时间。RoboVM编译器编译应用的过程是从应用 的main类开始。然后编译main类所用到的所有的类,之后再编译前面的类所用到的所有类,如此循环,直到应用所需的所有类均完成编译为止。在这一过程 中,标准的运行时类,如java.lang.Object和java.lang.String,也包含在编译的范围内。这是一个一次性的过程。 RoboVM会缓存已经编译过的类,只有一个类或与它有直接依赖关系的类已经发生了改变时,才会重新编译这个类。

增量编译和缓存目标文件的好处在于能够减少编译所耗费的时间。在生成的可执行文件中仅包含能够从Main类触及到的类可以降低可执行文件的大小。不 过,在某些情况下(如通过反射机制加载类时),RoboVM编译器无法决定是否应该对某个类进行编译。不过,可以给编译器下达指令,显式地将某个特定的类 或者所有符合某个条件的类包含在编译范围内。

基于安卓的运行时类库

任何的JVM虚拟机都需要运行时类库。这个类库为所有的Java程序提供标准的包和类,如java.lang.Object和 java.lang.String。RoboVM的运行时类库来自于安卓开源项目和已经被移植到RoboVM的非安卓专用的包中。这就意味着如果Java 或JVM代码中只用到了安卓标准包中的类,那么这些代码直接就能够在RoboVM上正常运行。

RoboVM现状

RoboVM目前仍在开发过程中,不过已经可以基本使用。1.0版本预计将在2014年年底之前发布。

在苹果应用商店中已经有至少50个基于RoboVM的应用。已知应用程序的***列表请参见这里

目前为止,大概有50%左右的iOS API可用在基于RoboVM的iOS应用中。在RoboVM的Wiki上可以查看到关于这些绑定的当前状态列表。截至现在,RoboVM上已经能够运行由Scala,Clojure和Kotlin所编写的代码。

关于RoboVM的文档,目前仍在完善过程中。在2014年晚些时候,1.0版本发布时,将会有较为完备的文档说明。

RoboVM的应用目前仍无法进行Debug。这一问题也将在今年的晚些时候解决。

限制

RoboVM只能够将已经完成预编译的类加载到应用中。这就意味着在RoboVM应用中,无法在运行时使用自定义的类加载器动态创建字节码并将其加载到应用中。也就是说,RoboVM无法支持运行时创建或修改类的技术。

更多相关信息

使用Xtend创建安卓应用

Xtend简介

Xtend 1是一种可以编译成可读Java源码的静态类型程序设计语言。这种语言本身是同类设计中的***典范,特别是在可 读性和强大的可扩展性方面,不过它也让Java的互操作性问题显而易见。这种语言鼓吹函数式编程风格和多分派、扩展方法、拉姆达表达式及编译期宏等特性。 与其他的Java替代品不同,Xtend本身并不包含庞大的标准库,而只是在标准的JDK上添加了一些扩展方法。Xtend还可以保证避免Java互操作 性问题的出现并且能够提供强大的IDE支持。

为什么安卓上的Java如此难用

Java代码往往十分冗长,特别是在安卓操作系统上。由于Android API的级别很低而且经常出现没有经过充分定义的类型(到处都是int类型)。另外一个烦恼就是无处不在的XML文件的使用和绑定。由于Android上 尚未支持Java 8,我们还不得不仔细阅读无处不在的匿名类。而且不幸的是,Java无法修剪代码以增强可读性,我们只能将代码与多余的符号、类型信息和样板习语 (boilerplate idioms)混杂在一起。

安卓对JVM语言的***要求

Java语言在安卓上的替代品必须要能够保证不增加任何运行时的系统开销,这就将所有的动态语言排除在外。另外,也不希望出现任何不必要的间接类型 转换。例如,代码中应该只使用Java和安卓类型,不应因为互操作性问题而需要来回转换。这不仅是处于性能方面的担心,在调试时也比较令人烦恼。***,安 卓系统限制每个应用只能够使用65536个方法。因此,寻找Java的替代品时,一定不能在应用中添加大的标准SDK,因为这样会大大减少开发人员所能使 用的方法数量。举例来说,使用Groovy的SDK会增加8000多个方法。

Xtend——安卓开发的***解决方案?

Xtend能够转化成地道的Java源代码,并且基本上只依赖于JDK和安卓系统的类。在运行时,也没有间接寻址、转换或者其他任何额外的开销。也 就是说,Xtend代码能够和Java的源代码有着基本一致的运行速度。另外,Xtend还包含一个经过精简的为安卓系统提供的运行时库,只有275kb 大小并且几乎包含了你所需要的一切。Xtend Eclipse插件与ADT(安卓开发工具)的整合也相当***,对于新的安卓构建系统3,甚至还提供了相应的Gradle插件2。接下来就让我们详细了解一下如何使用Xtend改善典型的安卓代码。

Hello安卓!

与往常一样,我们先看一个简单的Hello World示例程序:

  1. class HelloWorldActivity extends Activity {  
  2.  
  3.    override protected void onCreate(Bundle savedInstanceState) {  
  4.       super.onCreate(savedInstanceState)  
  5.     
  6.       val button = new Button(this
  7.       button.text = "Say Hello!"   
  8.       button.onClickListener = [  
  9.          Toast.makeText(context, "Hello Android from Xtend!", Toast.LENGTH_LONG).show  
  10.       ]  
  11.       val layout = new LinearLayout(this)  
  12.       layout.gravity = Gravity.CENTER  
  13.       layout.addView(button)  
  14.       contentView = layout  
  15.   }  
  16. }  

对于Java开发者来说,这个例子使用了类Java的编程风格,因此***眼看上去会非常熟悉。另外,你可能会注意到,示例中所用的API 100%来自于安卓SDK和JDK。

主要的区别在于:

  • 没有分号(分号是可选的)
  • 使用setter和getter访问对象属性
  • 属性的默认可见性(如,类默认是共有的)
  • 使用拉姆达表达式替代匿名类

在语言的特性方面,有很多地方可以深入探讨,不过在此之前,先让我们看一下如何将Xtend编译器与相应的Android构建过程整合在一起。

#p#

使用Gradle进行构建

对于目前最常用的三个构建系统:Maven,Gradle和Ant,Xtend都有相应的插件支持。谷歌最近为安卓项目引入了新的基于Gradle的构建系统。接下来我们看一下使用Gradle构建我们的“Hello World”项目需要做哪些工作。

本文假设你已经在系统中安装了***版本的Gradle和安卓SDK并且正确的设置了ANDROID_HOME环境变量。同时,你已经将Gradle的/bin目录添加到了PATH环境变量中。

接下来需要将构建脚本“build.gradle”添加到你的Eclipse Android项目的根目录下,build.gradle文件样例如下:

  1. buildscript { 
  2.    repositories { 
  3.       mavenCentral() 
  4.    } 
  5.    dependencies { 
  6.       classpath 'com.android.tools.build:gradle:0.8.+' 
  7.       classpath 'org.xtend:xtend-gradle-plugin:0.1.+'   
  8.    } 
  9.  
  10. apply plugin: 'android'  
  11. apply plugin: 'xtend-android'  
  12.  
  13. repositories {  
  14.   mavenCentral()  
  15. }  
  16.  
  17. dependencies {  
  18.   compile ('org.eclipse.xtend:org.eclipse.xtend.lib:2.6.+')  
  19. }  
  20.  
  21. android {  
  22.    compileSdkVersion 19  
  23.    buildToolsVersion "19.1.0" 
  24.    sourceSets {  
  25.       main {  
  26.          manifest {  
  27.             srcFile 'AndroidManifest.xml'  
  28.          }  
  29.          java {  
  30.             srcDir 'src'  
  31.          }  
  32.          res {  
  33.             srcDir 'res'  
  34.          }  
  35.          assets {  
  36.             srcDir 'assets'  
  37.          }  
  38.          resources {  
  39.             srcDir 'src'  
  40.          }  
  41.          aidl {  
  42.             srcDir 'src'  
  43.          }  
  44.       }  
  45.    }  
  46. }  

其主要工作就是导入并调用Maven和Xtend的构建插件。此外,我们将运行时库添加到项目中并告知Android插件我们正在使用 Eclipse风格的项目布局。上述工作完成后,在命令行窗口中进入项目的根目录并运行“gradle build”,Gradle将为你完成剩余的所有工作。

深入Xtend

除了语法糖之外,Xtend还附带了许多非常有用的语言特性,例如操作符重载,模板表达式和switch表达式。而且还可以通过结合不同的功能创建 新的特性。例如,假如你需要动态的UI,不能用静态的XML文件构建,而需要声明式的编写。Xtend为开发者提供了构建器语法(builder syntax)的支持。“Hello World”实例的UI实现代码如下:

  1. import static extension com.example.helloworld.UiBuilder.*  
  2.  
  3. class HelloWorldActivity extends Activity {  
  4.  
  5.    override protected void onCreate(Bundle savedInstanceState) {  
  6.       super.onCreate(savedInstanceState)  
  7.  
  8.       contentView = linearLayout [  
  9.          gravity = Gravity.CENTER  
  10.          addButton("Say Hello!") [  
  11.             onClickListener = [  
  12.                Toast.makeText(context,  
  13.                               "Hello Android from Xtend!",  
  14.                               Toast.LENGTH_LONG).show  
  15.             ]  
  16.         ]  
  17.     ]  
  18.  
  19.   }  

linearLayout(Context ctx, (LinearLayout)=>void initializer) 和button(ViewGroup group, String name, (Button)=>void initializer) 两个方法作为扩展被引入到Activity中。这两个方法将拉姆达函数作为其参数之一。传入拉姆达函数中的参数被称为implicit it,与this类似,implicit it不需要显式地解引用。如上所示,拉姆达函数,扩展方法和implicit it结合使用能够产生非常漂亮的构建器语法。通过Xtend也可以构建许多其他漂亮的API,从而以一种易读的声明式的方式编写代码。

来自于XML地狱的问候!

安卓开发者的相当一大部分日常工作就是配置和开发各种XML文件,用作国际化字符串的资源或用于各类视图的声明。安卓平台推荐使用XML文件,因为 平台已经为开发者提供了针对大型设备和SDK碎片化的解决方案。然而应用程序最终不可能只由静态视图和数据组成。开发者需要将所有的素材组合并为其赋予生 命。在安卓平台,通过R类来完成这些工作。这个自动生成的类包含了许多对应在XML文件中声明的各种元素的整型常量。假设一个视图XML文件中声明了如下 两个元素,点击Button可以更新TextView中的消息:

  1. <TextView android:id="@+id/message_view" 
  2. android:layout_weight="1" 
  3. android:layout_width="0dp" 
  4. android:layout_height="wrap_content" 
  5. android:hint="@string/empty" > 
  6. <Button 
  7. android:layout_width="wrap_content" 
  8. android:layout_height="wrap_content" 
  9. android:onClick="sayHello" 
  10. android:text="@string/hello_world" > 
  11. </Button>  

典型的安卓式开发方法是通过R类中生成的常量获取到TextView的控制权然后实现onClick的回调方法“sayHello”:

  1. class HelloWorldActivity extends Activity {  
  2.  
  3.    TextView messageView  
  4.  
  5.    override protected void onCreate(Bundle savedInstanceState) {  
  6.       super.onCreate(savedInstanceState)  
  7.       // set the view using the int constant  
  8.       contentView = R.layout.main  
  9.       // get a handle on the TextView  
  10.       messageView = findViewById(R.id.message_view) as TextView  
  11.    }  
  12.  
  13.    /**   
  14.     * Callback automagically called by Android   
  15.     */  
  16.    def void sayHello(View v) {  
  17.      messageView.text = "Hello Android from Xtend!"  
  18.    }  
  19. }  

上面一段安卓的典型代码中包含了不安全的类型转换,命名规范和各种样板文件。用Xtend我们能够做得更好。

你好,Xtendroid!

Xtendroid4是一个专门为安卓开发提供类库以及所谓的积极注解(active annotation)的小型项目。积极注解可以理解为编译时的宏,它能够参与到从Xtend到Java转化的编译过程中。你可以随意修改被注解的类,生成附加类型或使用这个钩子读写纯文本文件。

这样只要有一个注释,我们就知道要绑定哪个视图并且注释还可以帮助我们生成样板文件。除此之外,它还能够提供类型安全的元素访问方法和回调方法。下面一段代码就是用Xtendroid的@AndroidActivity注释编写的Activity类。

  1. @AndroidActivity(R.layout.main) class HelloWorldActivity {  
  2.  
  3.    /**   
  4.     * Type safe callback   
  5.     */  
  6.    override void sayHello(View v) {  
  7.       messageView.text = "Hello Android from Xtend!"  
  8.    }  
  9. }  

现在,这个Activity中只包含了我们想要加入的行为。其他的设置都是自动实现的,例如设置管道绑定、内容视图或扩展Activity的样板文件。而且现在一切都是类型安全的,IDE能够了解其中的来龙去脉并为开发者提供适当的自动完成建议。

此外,Xtendroid还能够为开发者处理JSON对象,资源文件或SQLite数据库提供便利。而且,积极注解以库的形式存在,因此通过自行开发或定制化已有库的方式,开发者可以很容易地构建更适于自己的库。

从下方1下载Eclipse并使用更新站点5安装ADT就可以开始自己亲自尝试上面所讲的内容。Xtendroid项目包含许多类似本文中所展示的示例。***祝大家能从中找到乐趣。

  1. Eclipse Xtend
  2. Xtend Gradle 插件
  3. Android Gradle 插件
  4. Xtendroid
  5. ADT 更新站点

关于作者

[[120236]]

Niklas Therning是开源项目RoboVM的创建者和的联合创始人——RoboVM项目的主要贡献者。他把如何合理地将Java引入iOS平台作为其使命。开始RoboVM项 目前,Niklas参与创建了SpamDrain反垃圾邮件服务,并且作为其承包商,主要从事Java EE和web应用程序开发的工作。Niklas持有位瑞典哥德堡查尔姆斯理工大学的计算机科学理学硕士学位。可以通过Twitter账号@robovm关注他。

[[120237]]

Sven Efftinge是一个充满激情的软件开发人员,他喜欢风筝冲浪运动、音乐和美食。他是Xtext项目的领导人。Xtext是一个程序设计语言、领域特定语言和JVM静态类型程序设计语言Xtend的开发框架。在位于Kiel的itemis公司,Sven领导一个研究部门。

过去几年,移动应用席卷了整个世界,在工作和生活的方方面面改变着我们使用互联网的方式。创建移动应用的各种技术也随之兴起,各种开发流程也 将移动应用视为一等公民,开始考虑适应移动开发的流程。尽管已经让人感觉无处不在,真正的移动应用时代才刚刚开始。我们即将面对新一代的移动设备,如可穿 戴设备或组成物联网的各种各样的移动装置。我们将面临全新的用于数据展示和命令接收的用户交互接口。我们也认识到越来越多的公司将真正采取移动优先的战 略。所有的这些都将对我们未来几年设计、开发和测试软件的方式产生巨大影响。

译文原文:另辟蹊径创建移动应用

查看英文原文:Unusual Ways to Create a Mobile App

 

责任编辑:闫佳明 来源: infoq
相关推荐

2011-12-29 09:44:14

云计算OwnCloud私有云

2021-03-01 10:15:24

数字货币人民币金融

2017-08-25 12:02:46

AI

2009-05-08 09:00:51

微软鲍尔默谷歌

2010-12-21 09:42:03

思科云计算

2015-04-01 10:22:22

一加ROM互联网

2016-04-11 16:49:46

白山带宽监测系统

2018-10-29 14:56:25

技术芯片系统

2022-08-25 15:05:23

AI论文

2014-12-04 12:00:50

移动设计产品失败

2019-11-08 13:53:47

量子芯片网络

2022-08-01 12:53:30

前端动画

2015-07-03 11:16:14

编程一个手镯

2012-10-08 10:08:45

移动WebFacebookAndroid

2022-03-10 15:21:26

算法人工智能宪法

2013-08-30 09:54:58

企业移动应用

2013-03-15 13:45:58

2017-02-14 21:10:09

2013-11-13 16:26:19

智能语音微软谷歌
点赞
收藏

51CTO技术栈公众号