`
rensanning
  • 浏览: 3512466 次
  • 性别: Icon_minigender_1
  • 来自: 大连
博客专栏
Efef1dba-f7dd-3931-8a61-8e1c76c3e39f
使用Titanium Mo...
浏览量:37452
Bbab2146-6e1d-3c50-acd6-c8bae29e307d
Cordova 3.x入门...
浏览量:604194
C08766e7-8a33-3f9b-9155-654af05c3484
常用Java开源Libra...
浏览量:677862
77063fb3-0ee7-3bfa-9c72-2a0234ebf83e
搭建 CentOS 6 服...
浏览量:87184
E40e5e76-1f3b-398e-b6a6-dc9cfbb38156
Spring Boot 入...
浏览量:399731
Abe39461-b089-344f-99fa-cdfbddea0e18
基于Spring Secu...
浏览量:69037
66a41a70-fdf0-3dc9-aa31-19b7e8b24672
MQTT入门
浏览量:90417
社区版块
存档分类
最新评论

【转】Titanium 架构分析

阅读更多

【原文】Titanium 架构分析

虽然是对早期版本的分析,但是说的很深入,推荐大家看看!

 

 

一、分析的目标

  1. 了解Titanium产品的基本框架结构和特点
  2. 了解Titanium产品如何扩展本地API以及访问方式
  3. 了解Titanium产品中的动态语言之间如何相互调用

二、Titanium概述

2. 1 Titanium介绍

Titanium是一个Web应用程序运行环境,它支持不同的系统平台(WindowsLinuxMac),并且支持Web应用程序对本地APIs的访问。在基于Titanium平台上,用户可以快速开发和方便的部署应用程序,并且这些应用程序可以使用本地APIs实现许多普通Web应用程序无法完成的功能和特性。

2.2 Titanium特点

Titanium框架具有如下几个方面的特点:

  1. 支持多平台(LinuxMacWindows、移动设备)
  2. 使用Web技术加快软件开发速度
  3. 支持Web中内嵌多种编程语言
  4. 支持对本地APIs的访问
  5. 通过Appcelerator网络云服务,基于Titanium的应用可以更容易的打包、测试和部署
  6. 本地功能的模块化,可动态加载指定的功能模块
  7. 强大灵活的语言扩展,用户在Titanium框架中可以很方便的扩展多种动态语言

2.3 Titanium 框架结构

上图来自于Appcelerator官网,该图以iPhoneAndroid两个移动平台为例,描述了Titanium的总体框架结构。在Titanium框架中,Web应用程序可以很方便的访问设备UI组件。比如,可以在页面中使用Titanium提供的API控制导航条、工具栏、菜单,以及可以动态的向用户弹出对话框、警告框等。除此,之外Titanium API还支持本地功能模块的访问,即用户可以使用Titanium提供的APIs接口访问数据库、定位功能、文件系统功能、网络功能、媒体功能等。

不过该框架图,并没有将Titanium中对多种脚本语言的相互访问机制很好的表现出来。但是,这一机制却又是Titanium框架的一个比较重要的功能特性。

三、Titanium构建

Titanium的构建过程使用scons管理(http://www.scons.org/)。scons是一个开源的软件构建工具,使用Python语言来描述软件构建规则。通过Titanium的源码级构建和Titanium的构建规则两个方面,可以了解Titanium运行环境由那些部分组成、这些模块和模块之间的关系是什么。

[注]以下所有的测试和分析内容均是以Linux平台上Desktop版本的Titanium代码为基础。

  1. 构建Titanium所依赖的库和环境
  • Ruby 1.8.x 开发包
  • Python 2.5.x开发包
  • scons构建工具
  • git 版本管理工具
    1. Ubuntu 9.04上构建Titanium所需的支持包
      sudo apt-get install build-essential ruby rubygems libzip-ruby \
      scons libxml2-dev libgtk2.0-dev python-dev ruby-dev \
      libdbus-glib-1-dev libnotify-dev libgstreamer0.10-dev \
      libxss-dev libcurl4-openssl-dev 
      
      sudo apt-get install git-core
    2. 获取Titanium源码
      git clone git://github.com/marshall/titanium
      cd titanium
    3. 获取Kroll源码
      git submodule init
      git submodule update
      cd kroll
      git checkout master
    4. 构建Titanium测试程序
      cd ..
      scons debug=1
    5. 运行
scons testapp debug=1 run=1

有关Titanium构建相关的信息,可以访问以下页面获得:

http://wiki.github.com/marshall/titanium/build-instructions

3.2 Titanium构建规则分析

3.2.1 版本需求

 

构建过程所需的库/程序版本
Python 2.5
Ruby 1.8
Scons 1.2
kroll 源码版本 12/30/99
titanium_desktop 源码版本 12/30/99
WebKit版本 libwebkittitanium-1.0.so.2.7.0
   

 

3.2.2 默认配置项

 

默认配置项
配置 备注
PRODUCT_VERSION 0.7.0  
INSTALL_PREFIX /usr/local  
PRODUCT_NAME Titanium  
CONFIG_FILENAME tiapp.xml  
BUILD_DIR build  
THIRD_PARTY_DIR kroll/thirdparty  
DISTRIBUTION_URL api.appcelerator.net  
CRASH_REPORT_URL api.appcelerator.net/p/v1/app-crash-report  
GLOBAL_NS_VARNAME Titanium 定义了全局Titanium对象名称
     

3.2.3 scons编译参数

 

Scons编译参数
debug 0表示release版本,1表示debug版本
clean 清除构建的工程
qclean 清除构建的工程
run 运行TestApp
run_with 带参数运行TestApp,好像Linux平台上没用

3.2.4 构建规则文件

 

构建规则文件
kroll/SConscript.thirdparty Titanium所需的第三方支持文件规则
installation/SConscript Titanium安装器构建规则
kroll/SConscript 构建kroll库规则
modules/SConscript 构建语言支持模块规则
apps/SConscript 构建TestApp规则
SConscript.dist 构建SDK规则
SConscript.docs 构建APIs文档规则
SConscript.test 构建测试程序规则

3.2.5 核心库和程序构建规则

 

/程序 规则
build/linux/runtime/template/kboot kroll/boot/breakpad/common/*.c

 

kroll/boot/breakpad/common/*.cc

kroll/boot/breakpad/client/*.cc

kroll/boot/breakpad/processor/*.cc

kroll/boot/breakpad/client/linux/handler/*.cc

kroll/boot/breakpad/common/linux/*.cc

build/linux/runtime/libkroll.so kroll/api/*.cpp

 

kroll/api/config/*.cpp

kroll/api/binding/*.cpp

kroll/api/utils/*.cpp

kroll/api/utils/poco/*.cpp

kroll/api/utils/linux/*.cpp

kroll/api/net/proxy_config.cpp

kroll/api/net/*_linux.cpp

build/linux/runtime/libkhost.so kroll/host/linux/host.cpp

 

kroll/host/linux/linux_job.cpp

/linux/modules/api/libapimodule.so poco third library(http://pocoproject.org/)

 

kroll/modules/api/*.cpp

build/linux/modules/javascript/libjavascriptmodule.so poco third library(http://pocoproject.org/)

 

webkittitanium-1.0 third library

kroll/modules/javascript/*.cpp

build/linux/modules/ruby/librubymodule.so poco third library(http://pocoproject.org/)

 

libruby third library

kroll/modules/ruby/*.cpp

build/linux/modules/php/libphpmodule.so poco third library(http://pocoproject.org/)

 

kroll/modules/php/*.cpp

   

四、Titanium静态分析

该部分主要是说明整个Titanium的阅读工作量、弄清楚Titanium中定义的核心对象的功能作用,以及各个模块之间的关系是什么。

4.1 代码统计

这里,将Titanium项目代码分成kroll和功能模块扩展两部分代码来统计,数据如下两表所示:

 

Kroll模块代码量统计
Language Files Blank Comment Code Scale Equiv
C/C++ Header 1168 35490
63506
111461
1.00
111461
HTML 386 1252 16112 51375 1.9 97612.5
C++ 162 6401 7046 33133 1.51 50030.83
Javascript 47 3273 1598 13214 1.48 19556.72
CSS 3 554 41 2720 1 2720
Object C 6 359 312 1400 2.96 4144
Python 10 260 185 1206 4.2 5065.2
Shell 11 56 157 234 3.81 891.54
Make 3 30 29 93 2.5 232.5
Assembly 1 15 39 57 0.25 14.25
Ruby 1 10 0 54 4.2 226.8
Yaml 1 0 0 12 0.9 10.8
SUM 1802 47938 89263 217012 1.35 293546.95
titanium_desktop模块(排除Kroll模块)
Language Files Blank Comment Code Scale Equiv
Javascript 118 5801 3276 28678 1.48 42443.44
C++ 125 4690 5169 27320 1.51 41253.2
C/C++ Header 159 1647 3443 7682 1 7682
HTML 49 347 39 3715 1.8 7058.5
Ruby 29 673 643 3227 4.2 13553.4
CSS 5 542 41 2655 1 2655
Python 45 601 664 2632 4.2 11054.4
C 1 167 237 1925 0.77 1482.25
Shell 13 60 158 251 3.81 956.31
PHP 5 37 1 179 3.5 626.5
XML 5 0 8 151 1.9 286.9
Object C 2 31 15 119 2.96 352.24
SUM 556 14596 13694 78534 1.65 129404.14

4.2 核心对象的介绍

 

对象 基类 说明
AccessorBoundObject StaticBoundObject settergetter的封装,当用户访问想访问XXX属性时,该对象会调用setXXX方法或者getXXX方法。目前Titanium中主要是JSTitanium对象使用AccessortBoundObject封装
AccessorBoundMethod StaticBoundMethod 用于通过属性的方式访问方法,由该对象封装的方法,会自动的导出settergetter方法
AccessorBoundList StaticBoundList 用于以属性的方式访问list对象,由该对象封装的list,会自动导出settergetter
ArgList   对参数列表对象的封装
Blob   对数据封装,可以描述任何数据
Tuplex   对元组对象的封装
DelegateStaticBoundObject KObject 用于对全局访问对象的封装,目前Titanium中只有UITitanium JS对象使用该对象封装
KList KObject 封装List对象
KMethod KObject 对方法的封装,所有扩展语言的函数,都需要用该对象封装
KEventObject AccessorBoundObject 描述事件对象,JS中可以通过该对象,向主线程发送事件。比如重新载入页面、弹出对话框。
KEventMethod KEventObject 对事件方法的封装,目前只有ti.Process模块使用该对象
KObject ReferenceCounted 所有的其他类型语言对象和方法都是继承该类,这样可以按照相同的方法处理不同语言对象和方法
StaticBoundList KList 静态列表,使用内部map绑定属性
StaticBoundMethod KMethod 静态方法
StaticBoundObject KObject 静态对象,继承该对象可以很方便的设置对象的属性、方法。

 

每个StaticBoundObject内部,都保存着一个StringShareValuemap成员属性。

Value ReferenceCounted 描述对象类型
     

4.3 模块之间的关系

从整体框架结构上来看,可以将Titanium分成三个部分,最上层是WebKit以及针对WebKit的扩展(修改很少),中间层是kroll可以将其看成是一个中间件,最下层是个个模块的扩展。模块之间的关系如图所示:

以下从WebKitKroll和模块扩展三个部分来说明

WebKit: WebKit引擎解析页面数据发现<script>标签,或者当用户触发了页面中某个与脚本函数相关的控件时,WebCore会将相应的脚本代码片段传递给JavascriptCore解析执行。如果对比Tinanium修改的WebKit代码和原始的WebKit代码(http://www.webkit.org)会发现,tinaniumWebKit的修改是及小的。主要是作了两个方面的工作:首先,tinanium扩展了KURL的处理,增加了ti://, app://等私有协议的支持。再者,在WebKit/gtk/webkit/目录中,添加了几个接口函数(主要是用来处理扩展的协议和注册解析器),其中最重要的是webkit_titanium_add_script_evaluator,该接口在Kroll模块的script类中会被调用,用来向WebKit引擎注册一个Evaluator Proxy

KrollBase Module:这部分主要的职责是负责Javascript的方法、对象和PythonRubyPHP等语言之间相互转换、事件处理,以及模块动态加载。Kroll模块中,定义了一个host对象,这个对象是整个TestApp的主线程,UI初始化、WebKit初始化和事件处理都是在host中完成的。host对象中保存了一个全局对象表,该表会在WebKit引擎、Python引擎、Ruby引擎之间以KObject中间对象形式相互传递,最终达到不同语言之间的相互调用。

API Extension:这里扩展了大量的与系统平台功能相关的APIWeb应用使用。其中最重要的一个对象是ti.UI,该模块负责UI相关的资源、事件处理、GTK主界面的创建、Tininum JS对象的创建。

五、Titanium动态分析

下面从6个方面以TestApp为例,来分析Titanium的主要特性和功能。

5.1 TestApp初始化

TestApp的启动过程有个自启动过程。首先,TestApp启动后会创建一个Application对象,该对象会从Mainifest文件中获取App相关的资源,并且保存在一个全局变量中。然后,TestApp会设置几个系统环境变量:

KR_BOOTSTRAPPED: 描述是否已经初始化环境变量以及构建Application对象

KR_HOME:描述运行程序的HOME路径

KR_RUNTIME:描述运行时资源路径

KR_MODULES:描述需加载模块信息

LD_LIBRARY_PATH:描述模块所在的文件夹路径

最后,TestApp会使用exec系统调用将自己自启,然后通过之前设置 KR_BOOTSTRAPPED环境变量判断是否进入下一阶段的初始化过程。如果 KR_BOOTSTRAPPED设置为YES,则会首先将其unset,然后启动LinuxHost

titanium框架中,使用动态库的方式将模块之间的关系解耦合。在TestApp启动的第二阶段中,StartHost(kroll/boot/boot_linux.cpp)会根据之前设置的 KR_RUNTIME路径信息,找到libkhost.so动态库,然后从libkhost.so中获取Execute函数指针,并且调用(这里有个问题,如果多个实列同时运行,有可能KR_RUNTIME尚未unset就启动第二个应用,则会出现TestApp异常)。

libkhost.so动态库中的Execute方法,首先创建一个Host实例,在这里是LinuxHost对象,然后调用该对象的Run方法进入一个循环。这个循环是整个TestApp的主循环,主要负责模块的动态发现和加载,事件处理。在LinuxHost的实现中,会维护一个job队列,通过定时器的方式,每隔250ms的时间会去检测该job队列中是否有job存在(LinuxJob描述)。如果,事件存在则会一次性将所有的事件取出,并且清空事件队列,然后一个个的执行job对象的Execute方法。

TestApp的两次初始化过程如下图所示:

5.2 模块初始化

TestApp程序创建LinuxHost对象,并且执行Run方法之后,会首先扫描KR_MODULES环境变量中指定的模块,并且从 LD_LIBRARY_PATH定义的路径信息中寻找到这些动态库模块,并且加载(调用相应模块的Initialize方法)。LinuxHost首先加载的是基本模块(APIPythonModuleRubyModulePHPModuleJavascriptModule)。

pythonModule为例,描述一个完整的加载过程:

  1. Host::LoadModules从环境变量中获取到python动态库的路径信息
  2. 调用Host::FindBasicModules方法,将libpythonmodule.so文件加载进来,然后调用PythonModuleInitialize方法
  3. PythonModule::Initialize首先会向全局属性表中创建一个PythonPythonEaluator对象的关联。前面也说到,Host对象会保存一个全局属性表,这个表中使用KObject中间对象形式,将JAVASCRIPTPYTHONRUBYPHP等语言定义的对象封装,并且保存在该表中。运行时,可以使用Host对象的GetGlobalObject方法获取。
  4. 调用Script::AddScriptEvaluator静态方法将PythonEvaluator对象放入Script对象中维护的一个Ealuator列表中。当JavascriptCore引擎发现<script>标签会遍历这个evaluator链表,通过MIME类型找到相应的解析器实例,然后将代码片段传递给相应的解析器处理(这样就可以支持HTML代码中内嵌多种语言)。

这里还有一个模块比较特殊需要仔细说明,即ti.UI模块。该模块负责WebKit引擎初始化,GTK窗口创建以及UI事件的处理。加载过程类似PythonModule,首先Host对象找到libtiuimodule.so动态库,然后调用Initialize方法初始化,ti.UI模块中的Initialize方法只做了两件事情,创建了一个APIBinding对象,然后将“API”属性和APIBinding关联起来,保存在全局属性表中。接下来,当Host::LoadModules方法加载完毕所有的动态库后,会调用Host::StartModules来启动模块(在整个TestApp运行中,只有ti.UI模块的Start方法被重载了,而其他模块在StartModules方法被执行时,什么事情都没有做)。UIModule::Start方法做了三个非常重要的操作:1、创建GtkUIBinding对象,并且将“UI”和该对象绑定,存放在全局属性表中。2、调用ScriptEvaluator::Initialzie()使用WebKit扩展中导出的webkit_titanium_add_script_evaluator函数,将自己注册到JavascriptCore中。3、创建初始化WebView

至此,WebKit引擎已经初始化完毕、UI界面已经初始化完毕、相应语言的解析器以及JAVACRIPT API扩展对象已经添加到全局属性表中。但是至此,页面中是无法正常访问Titanium对象的。

整个过程如下图所示:

5.3 Titanium对象的注册

Javascript中的Titanium并没有通过硬编码的方式定义该名称,而是在构建的过程中通过变量的方式指定的。在构建规则中有GLOBAL_NS_VARNAME变量,该变量名称会作为编译参数传递,代码通过改变量的定义,来确定Javascript可见的Titanium主对象的名称是什么。

webView构建完毕后,Titanium Js主对象并不存在,只有当第一次WebKit遇到脚本代码时,这个对象才被创建。当WebKit引擎碰到脚本对象时,会调用JavascriptCoreinitScipt方法,初始化Javascript引擎。在此JavascriptCore会将我们之前调用webkit_titanium_add_script_evaluator增加的Evaluator代理和JavascriptCore解析关联起来。当引擎和资源都初始化完毕,会向FrameClient发送object avaliable通知,会调用FrameLoader::dispatchWindowObjectAvaliable()方法。这个方法会导致UserWindow::RegisterJSContext()方法的调用。然后UserWindow对象会创建一个DelegateStaticBoundObject对象来描述Titanium对象,并且将之前初始化完毕的Titanium API对象(KObject)与Titanium对象关联起来,然后将其放入到全局属性表中。这样,之后Web应用程序就可以从全局属性表中访问到Titanium对象了。但是Titanium对象中有哪些子属性是不知道的,这是运行时才去确定。

这个初始化过程如下图所示:

5.4 事件系统

Titanium的事件系统分成两个部分来说明,一部分是如何获取事件,另一部分是如何向事件系统中增加新事件。

linuxHost::RunLoop循环被调用后,立即会注册一个定时器,每隔250ms调用一次main_thread_job_handler函数。该函数首先通过GetJobs方法获取当前系统中未处理的事件,并且依次的调用事件对象(LinuxJob)的Execute方法执行。如果系统没有未决的事件存在,则立即返回。

事件的添加和触发均通过LinuxHost::InvokeMethodOnMainThread()方法完成。该函数会创建一个LinuxJob对象,并且插入到事件队列尾中。事件的触发有多种可能性,可以是由Javascript代码触发,也可以是内部事件触发,同样也有可能是UI事件触发。

由于Titanium扩展的JS APIC层上都会与相应的C方法对应,当Web应用程序调用相应的JS方法,对应的C方法会被调用,该方法则会使用LinuxHost::InvokeMethodOnMainThread()方法,向主线程发送事件处理请求。

对于UI来说,在Linux平台上所有的UI相关的事件首先是被GTK的应用框架截获,当有UI事件到来时,ti.UI模块会创建对应事件的KEvent对象(在event.h中定义),然后调用KEvent对象的Fire方法,触发事件。该方法会间接的使用LinuxHost::InvokeMethodOnMainThread方法向LinuxHost事件队列注册事件。

这个过程如下图所示:

5.5 访问Titanium对象属性和方法

比如,有一段Javascript脚本中调用Titanium.API获取Titanium对象的API属性,当JavascriptCore解析到这部分代码时,并不知道这个对象是什么类型的,完全当作一个抽象的JSValue对象来对待,因为对于Javascript引擎来说,并不需要时刻知道对象是什么,有那些属性和方法,只有到运行时才会去用查询该对象中是否存在指定的属性或者方法(JavascriptCore内部维护了一张属性表,通过查询方式获取相应属性或者函数的处理函数指针。这点V8做的就比较高明,用类的方式描述,动态将对应JS的方法和属性转换成C++的成员函数和成员变量,大量的减少了访问时间)。由于API这个对象是由Kroll创建的,是一个KObject对象,在挂载到Javascript 全局属性表之前,Titanium会调用KObjectToJSValue方法,将KObject对象转换成一个JSObject对象,并且设置了几个比较重要的回调函数:

  1. HasPropertyCallback: 当查询属性时候被调用
  2. GetPropertyCallback: 当获取指定属性时被调用
  3. SetPropertyCallback: 当设置指定属性时被调用

javascript访问用Titanium对象的API属性时,通过JSValue.Get方法会调用到GetPropertyCallback函数,该函数会查询KObject对象中(这里是说的Titanium)是否有API这个属性,如果有则转换成JSValue对象,并且返回给Javascript引擎。

如果这里访问的是一个属性的方法,过程和访问对象是一样的,只不过最后创建的是一个Function Js对象,而非Object对象。当Javascript访问这个返回的Function对象时,Javascript引擎会调用callAsFunction方法,而该方法会引发CallAsFunctionCallback回调函数被调用(该函数是静态函数在KMethodToJSValue函数中注册)。这样通过CallAsFunctionCallback这个回调接口,调用实际的KObjectCall方法,执行实际的处理函数。

属性访问过程如下图所示:

5.6 JavascriptPythonRuby动态语言间的相互调用

Titanium框架中引入了一个比较有意思的特性,即支持多种语言之间的相互调用。从实现技术角度来说,Titanium的多语言支持的设计思想,是在学习了WebKitBinding机制而发展过来的。主要用到了JavascriptCore引擎可以动态注册Evaluator的机制。HTML语言中定义了<script>标签,用于内嵌脚本语言,该标签有个子属性type,通过该属性可以让浏览器引擎区分是什么类型的脚本。加入我们有如下的脚本代码:

<script type=”text/python” src=”xxx.py”></script>

首先,WebCore引擎会解析HTML页面数据,当发现有<script>标签出现,则会创建HTMLScriptElement,对于script有两种处理情况,一种是如上代码通过src包含一个脚本路径,还有一种情况是定义一段代码,通过控件或者超连接的方式以事件方式触发。如果是第一种情况,则会在创建HTMLScriptElement的时引发ScriptElementData::requestScript方法的调用。如果是第二种情况,则会在触发相应事件时候调用FrameLoader::executeScript方法执行脚本。最终都会调用JavascriptCore中的EvaluatorAdapter::evaluate()。由于在初始化ti.UI模块时,我们已经注册了自己的evaluatorScriptEvaluator),因此会将获取的脚本信息传递给ScriptEvaluator,在该对象中,会通过Script::Evaluate()方法,根据传递下来脚本的MIME类型(也就是scripttext字段定义的类型)派发给注册的不同解析器去执行。

这里以Javascript调用Python代码,并且Python代码中又调用了ruby代码为例子说明其调用过程。

Python代码:

def abc():

ruby_fun()

Ruby代码:

def ruby_func()

end

pythonEvaluator::Evaluate()被调用后,首先将保存的全局JS对象表转换成Python可识别的对象字典(KMethodToPyObject完成,转换成PyKMethodTypePython对象)。这样在之后编译的Python代码中就可以访问到这些对象。然后将Python代码使用Python编译器编译,并且将编译后的函数对象转换成KObject对象,插入到全局的JS对象表中(abc)。这样,Javscript,和其他语言都可以识别该对象。同样,对Ruby函数的处理,也会首先将全局JS对象表中的KObject对象转换成Ruby的对象,然后对Ruby函数进行编译,将新生成的Ruby函数对象(ruby_fun)转换成KObject对象,然后从新更新JS全局对象转换表。至此,全局JS对象表中就新增了两个KObject对象:ruby_fun, abc

javascript访问该abc函数对象时,按照通常方式首先调用CaAsFunctionCallback,该函数会调用KObjectCall方法,由于该KObject实际上就是一个KPythonMethod对象,因此KPythonMethod对象的Call方法会被调用。之前我们注册的Python方法是一个PyKMethodType,该类型中定义了一个方法回调函数,当Python方法被调用时,该回调函数(PyKMethod_call)会被调用。这个例子中,在Python代码里调用了ruby_fun,因此当PyKMethod_call被调用时,首先将调用的KObject对象(实际上是一个对Ruby函数对象的封装)转换成KMethod对象,然后调用Call方法。这样就通过间接调用,调用到KRubyMethodCall方法,使得Ruby函数得到执行。

整个过程如下图所示:

六、参考资源

http://www.appcelerator.com/

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics