iOS程序的Build过程
2014-03-14 11:45
309 查看
转自:http://beyondvincent.com/blog/2013/11/21/123-build-process/
注1:本文由破船译自The Build Process。
注2:
下面开始吧:
本文目录如下所示:
解密Build日志
Build过程的控制
工程文件
小结
当我们进行开发时,如果需要运行程序,只要在Xcode中点击运行按钮(这个按钮看起来有点像在播放音乐),过一会,我们的程序就会运行在设备或者模拟器上了,看似简单的操作过程,不过在这背后隐藏了许多步骤!当然,有时候也会遇到一些错误。
本文,我将从稍微高一点的角度来解读整个Build过程,并探索一下Build过程与Xcode界面上显示的project setting有多大关系。当然,为了更加深入的了解每一步实际执行的任务,我会适当的引入一些别的文章。
为了了解Xcode build过程的内部工作原理,我们首先把突破点放在完整的log文件上。打开
默认情况下,XCode会把大量的log信息隐藏起来,你只需要点击选中某条log,然后点击右边的展开按钮,就能看到该条log的详细信息了。当然,你也可以选中一条或者多条日志,然后通过Cmd+C,就能将相关的所有文本信息拷贝到粘贴板上。另外,还可以通过菜单Editor中的
在我这儿的示例中,将近有10000行log信息(当然,大多数信息是由OpenSSL带来的,并非来自我们的代码)。下面我们就开始吧!
首先,你可能会发现输出的log信息,被工程中对应的target分割开了:
在我这的工程中有好几个依赖项:如包含在Pods中的AFNetworking 和 SSZipArchive, 已经以子工程形式存在的OpenSSL等。
针对这里的每个target,Xcode都会执行一些列的操作,以将相关的源代码转换为机器可读的二进制(于所选平台相关)。我们来亲密接触一下第一个targetSSZipArchive吧。
在这个target的log输出中,我们可以看到每个任务执行的详细情况。例如,第一个是处理一个预编译头文件(为了增加其可读性,我省略了许多细节):
在build过程中,每个任务都会出现类似上面的这些log信息,我们就通过上面的log信息了解详情吧。
每个log都会以这样的一行来对任务进行描述。
接着下面带缩进的这3行会被输出。此处,修改了工作路径,并对PANG和PATH环境变量进行设置。
这里才是真正焕发出魔力的地方。为了处理一个
-x标示符用来指定语言,此时是
目标架构指定为
标示#defines的内容已经被添加了。
-c标示符用来告诉clang具体如何运行。-c意味着:运行预处理器、词法分析、类型检查LLVM的生成和优化,以及特定target相关汇编代码的生成阶段,最后,运行这个汇编代码以生成.o目标文件。
输入文件。
输出文件。
虽然有大量的log信息,不过我不会把每个log信息都做详解。我们的目的是让你了解在build过程中,完整的了解什么工具被调用,以及都使用了什么参数。
针对这个target,虽然只有一个.pch文件,但实际上这里对objective-c-header文件处理了两次。下面来看看log信息告诉我们的详细情况:
可以看到,build了两种target:armv7和armv7s,所以clang为每种架构处理了一次这个文件。
紧接着预编译头文件的处理之后,我们可以找到SSZipArchive target相关的其它一些任务:
通过名称,我们基本能够知道个大概:
上面这些类似的步骤会出现在工程中所有其它的依赖项中。
当所有的依赖项都准备好了,就可以开始构建我们程序的target了。针对该target输出的log信息包含了之前没有出现过的内容,这些内容非常有价值:
在上面的任务中,可能Ld不能一眼看出是什么意思,此处它是一个linker工具,跟libtool类似。实际上libtool会简单的调用ld和lipo。而ld用来构建可执行文件。更多编译和链接相关的文章可以看看 Daniel 和 Chris写的。
上面这些步骤,实际上都会调用相关的命令行工具来做实际的工作,这跟之前我们看的步骤ProcessPCH类似。至此,我将不会继续介绍这些log信息了,我将带来大家从另外一个不同的角度来继续探索这些任务:Xcode是如何知道哪些任务需要被执行?
当你选中在Xcode 5中的一个工程时,project editor会在顶部显示出6个tabs:General, Capabilities, Info, Build Settings, Build Phases 以及 Build Rules。如下图所示:
其中最后3项与build过程的相关度最大。
Build Phases代表着将代码构建为一个可执行文件的规则。它描述了build过程中必须执行的不同任务。
首先,指定了target的依赖项。这将告诉build系统在当前target可以build之前,必须先build target的依赖项。实际上这并不属于真正的build phase,在这里,Xcode只不过将其与build phase显示到一块罢了。
接着是一个CocoaPods相关的脚本需要在build phase执行——更多CocoaPods相关信息可以查看Michele的文章。
然后在
rules和build settings中研究。在
当编译结束,下一步就是将所有的内容链接到一块:
executables。
当链接完成之后,build phase中最后需要处理的就是将静态资源(例如图片和字体)拷贝到app bundle中。需要注意的是,如果图片资源是PNG格式,那么不仅仅对其进行拷贝,还会做一些优化(如果build settings中的PNG优化是打开的)。
虽然静态资源的拷贝是build phase中的最后一步,但这并不代表build过程已经完成了。例如,还没有进行code signing(这并不是build phase考虑的范畴),code signing属于build步骤中的最后一步
至此,你已经完全可以掌控build phases相关内容(先不考虑默认的设置项),例如,你可以在build phases中添加运行自定义脚本,就像CocoaPods使用的一样,来做额外的工作。当然也可以添加一些资源的拷贝任务,当你需要将某些确定的资源拷贝到制定的target目录中,这非常有用。
另外你可以通过定制build phase来添加带有水印(包括版本号和commit hash)的app icon。只需要在build phase中添加一个
然后可以使用ImageMagick来修改app icon。这里有一个完整的示例,可以参考。
如果你希望编写的代码比较简洁点,那么可以添加一个
Build rules指定了不同文件类型该如何编译。一般来说,开发者并不需要修改这里面的内容。如果你需要对特定的文件类型添加处理方法,那么可以在此处天剑一条新的规则。
一条build rule指定了其应用于那种文件类型,该文件类型是如何被处理的,以及输出内容被放置到何处。比方说,我们创建了一条预处理规则,该规则将Objective-C的实现文件当做输入,然后解析文件内部的注释内容,最后再输出一个.m文件,文件中包含了生成的代码。由于我们不能将.m文件既当做输入又当做输出,所以我使用了.mal后缀,定制的build rule如下所示:
上面的规则应用于所有后缀为*.mal的文件,这些文件会被自定义的脚本处理(调用我们的预处理器,并附带上输入和输出参数)。最后,该规则告诉build system在哪里可以找到此规则的输出文件。
由于这里的输出是一个.m文件,那么build使这些.m文件会被编译处理(就如刚开始介绍的那些预处理步骤)。
在脚本中,我使用了少量的变量来指定正确的路径和文件名。在苹果的Build
Setting Reference.文档中可以找到所有可用的变量。build过程中,要想观察所有已存在的环境变量,你可以添加一个
至此,我们已经了解到build phases是如何被用来定义build 过程的步骤,以及build rules是如何指定哪些文件类型在编译阶段需要被预处理。在build settings中,我们可以配置每个任务(之前在build log输出中看到的任务)的详细内容。
在这里,你会发现build 过程的每一个阶段,都有许多选项:从编译、链接一直到code signing和packaging。注意,settings被分割为不同的部分,大部分会于build phases有关联,有时候也会指定编译的文件类型。
这些选项基本都有不错的文档介绍,你可以在右边面板中的quick help inspector或者 Build
Setting Reference中查看到。
上面我们介绍的所有内容都被保存在工程文件(.pbxproj)中,除了其它一些工程相关信息(例如file groups),我们很少会深入该文件内部,除非在代码merge时发生冲突,或许会进去看看。
我建议你用文本编辑器打开一个工程文件,从头到尾的看一遍里面的内容。它的可读性非常高,里面的许多内容一看就知道什么意思了,不会存在太大的问题。通过阅读并完全理解工程文件,这对于合并工程文件的冲突非常有帮助。
首先,我们来看看文件中叫做
根据这个ID(1793817C17A9421F0078255E),我们可以找到main工程的定义:
在这部分section中包含了一些keys,顺从这些key,我们可以了解到更多关于这个工程文件的组成。例如,
file group。如果你按照这个思路,你可以快速了解到在.pbxproj文件中工程的结构。下面我要来介绍一些与build过程相关的内容。其中
target的定义:
根据第一个id,我们找到一个target的定义:
其中
setting tab中所有选项存储的位置:
而
phases。这非常容易识别出来(Xcode中的参数使用了它们原本真正的名字,并以C风格进行注释)。
rules。
没那么吓人,不是吗?工程中剩下的内容就留给你去当做练习来了解吧。只需要顺着ID走,即可,一旦你找到了敲门,理解了Xcode中工程设置的不同section,那么对于merge工程文件的冲突时,将变得非常简单。甚至可以在GitHub中就能阅读工程文件,而不用将工程文件clone到本地,并用Xcode打开。
当今的软件是都用其它复杂的一些软件和资源开发出来的,例如library和build工具等。反过来,这些工具是构建于底层架构的,这犹如剥洋葱一样,一层包着一层。虽然这样一层一层的,给人感觉太复杂,但是你完全可以去深入了解它们,这非常有助于你对软件的深入理解,实际上当你了解之后,这并没有想象中的那么神奇,只不过它是一层一层堆砌起来的,每一层都是基于下一层构建起来的。
在这里,我们只是轻微的探究了一下build过程,当我们点击Xcode中的允许按钮时,并没必要深入了解内部具体发生了什么。只需要了解到build的过程,以及可控的一些操作顺序即可。当然,要想进一步深入了解,可以试着阅读其它一些文章。
注1:本文由破船译自The Build Process。
注2:
1 2 3 4 5 6 7 8 9 10 | 本文将轻度解密Xcode build日志,还原iOS程序build的过程。 另外将介绍如何对build过程的控制,进而定制出自己希望的流程, 例如通过Build phase的定制,给app icon打水印(包括版本号和日期)。 通过对工程文件的解读,为你揭开工程文件(.pbxproj)与 build settings的关系。这对于解决工程文件的merge冲突非常有帮助。 PS:实际上各开发平台的build过程都比较相似,如果你熟悉了 某个平台的build过程,那么同样的原理也适用于别的平台。 可以说换汤不换药,本质是一样的。 |
本文目录如下所示:
解密Build日志
Build过程的控制
工程文件
小结
当我们进行开发时,如果需要运行程序,只要在Xcode中点击运行按钮(这个按钮看起来有点像在播放音乐),过一会,我们的程序就会运行在设备或者模拟器上了,看似简单的操作过程,不过在这背后隐藏了许多步骤!当然,有时候也会遇到一些错误。
本文,我将从稍微高一点的角度来解读整个Build过程,并探索一下Build过程与Xcode界面上显示的project setting有多大关系。当然,为了更加深入的了解每一步实际执行的任务,我会适当的引入一些别的文章。
解密Build日志
为了了解Xcode build过程的内部工作原理,我们首先把突破点放在完整的log文件上。打开Log Navigator,从列表中选择一个Build,Xcode就会通过很漂亮的一种格式将log文件显示出来。如下图所示:
默认情况下,XCode会把大量的log信息隐藏起来,你只需要点击选中某条log,然后点击右边的展开按钮,就能看到该条log的详细信息了。当然,你也可以选中一条或者多条日志,然后通过Cmd+C,就能将相关的所有文本信息拷贝到粘贴板上。另外,还可以通过菜单Editor中的
Copy transcript for shown results将所有的log信息复制到粘贴板上。
在我这儿的示例中,将近有10000行log信息(当然,大多数信息是由OpenSSL带来的,并非来自我们的代码)。下面我们就开始吧!
首先,你可能会发现输出的log信息,被工程中对应的target分割开了:
1 2 3 4 5 6 7 8 9 101112 | Build target Pods-SSZipArchive ... Build target Makefile-openssl ... Build target Pods-AFNetworking ... Build target crypto ... Build target Pods ... Build target ssl ... Build target objcio |
针对这里的每个target,Xcode都会执行一些列的操作,以将相关的源代码转换为机器可读的二进制(于所选平台相关)。我们来亲密接触一下第一个targetSSZipArchive吧。
在这个target的log输出中,我们可以看到每个任务执行的详细情况。例如,第一个是处理一个预编译头文件(为了增加其可读性,我省略了许多细节):
1 2 3 4 5 6 7 8 9 101112 | (1) ProcessPCH /.../Pods-SSZipArchive-prefix.pch.pch Pods-SSZipArchive-prefix.pch normal armv7 objective-c com.apple.compilers.llvm.clang.1_0.compiler (2) cd /.../Dev/objcio/Pods setenv LANG en_US.US-ASCII setenv PATH "..." (3) /.../Xcode.app/.../clang (4) -x objective-c-header (5) -arch armv7 ... configuration and warning flags ... (6) -DDEBUG=1 -DCOCOAPODS=1 ... include paths and more ... (7) -c (8) /.../Pods-SSZipArchive-prefix.pch (9) -o /.../Pods-SSZipArchive-prefix.pch.pch |
每个log都会以这样的一行来对任务进行描述。
接着下面带缩进的这3行会被输出。此处,修改了工作路径,并对PANG和PATH环境变量进行设置。
这里才是真正焕发出魔力的地方。为了处理一个
.pch文件,调用了clang,并且附带了大量的选项。这行log信息显示出了所有的调用参数,我们稍微看几个参数吧:
-x标示符用来指定语言,此时是
objective-c-header。
目标架构指定为
armv7。
标示#defines的内容已经被添加了。
-c标示符用来告诉clang具体如何运行。-c意味着:运行预处理器、词法分析、类型检查LLVM的生成和优化,以及特定target相关汇编代码的生成阶段,最后,运行这个汇编代码以生成.o目标文件。
输入文件。
输出文件。
虽然有大量的log信息,不过我不会把每个log信息都做详解。我们的目的是让你了解在build过程中,完整的了解什么工具被调用,以及都使用了什么参数。
针对这个target,虽然只有一个.pch文件,但实际上这里对objective-c-header文件处理了两次。下面来看看log信息告诉我们的详细情况:
1 2 | ProcessPCH /.../Pods-SSZipArchive-prefix.pch.pch Pods-SSZipArchive-prefix.pch normal armv7 objective-c ... ProcessPCH /.../Pods-SSZipArchive-prefix.pch.pch Pods-SSZipArchive-prefix.pch normal armv7s objective-c ... |
紧接着预编译头文件的处理之后,我们可以找到SSZipArchive target相关的其它一些任务:
1 23 | CompileC ... Libtool ... CreateUniversalBinary ... |
CompileC用来编译.m和.c文件,
Libtool根据目标文件创建出一个库,而
CreateUniversalBinary则将上一阶段产生的两个.a文件(对应着两个不同的架构)合并为一个通用的二进制文件(可以运行在armv7和armv7s上)。
上面这些类似的步骤会出现在工程中所有其它的依赖项中。
当所有的依赖项都准备好了,就可以开始构建我们程序的target了。针对该target输出的log信息包含了之前没有出现过的内容,这些内容非常有价值:
1 2 3 4 5 6 7 8 9 101112 | PhaseScriptExecution ... DataModelVersionCompile ... Ld ... GenerateDSYMFile ... CopyStringsFile ... CpResource ... CopyPNGFile ... CompileAssetCatalog ... ProcessInfoPlistFile ... ProcessProductPackaging /.../some-hash.mobileprovision ... ProcessProductPackaging objcio/objcio.entitlements ... CodeSign ... |
上面这些步骤,实际上都会调用相关的命令行工具来做实际的工作,这跟之前我们看的步骤ProcessPCH类似。至此,我将不会继续介绍这些log信息了,我将带来大家从另外一个不同的角度来继续探索这些任务:Xcode是如何知道哪些任务需要被执行?
Build过程的控制
当你选中在Xcode 5中的一个工程时,project editor会在顶部显示出6个tabs:General, Capabilities, Info, Build Settings, Build Phases 以及 Build Rules。如下图所示:其中最后3项与build过程的相关度最大。
Build Phases
Build Phases代表着将代码构建为一个可执行文件的规则。它描述了build过程中必须执行的不同任务。首先,指定了target的依赖项。这将告诉build系统在当前target可以build之前,必须先build target的依赖项。实际上这并不属于真正的build phase,在这里,Xcode只不过将其与build phase显示到一块罢了。
接着是一个CocoaPods相关的脚本需要在build phase执行——更多CocoaPods相关信息可以查看Michele的文章。
然后在
Compile Sources中指定了所有必须进行编译的文件。更多相关内容我们将在build
rules和build settings中研究。在
Compile Sources中指定的文件将根据这些rule和setting被处理。
当编译结束,下一步就是将所有的内容链接到一块:
Link Binary with Libraries。在这里面列出了所有的静态库和动态库,这些库会与上面编译阶段生成的目标文件进行链接。实际上静态库和动态库的处理过程有非常大的区别,相关内容可以参考Daniel的文章Mach-O
executables。
当链接完成之后,build phase中最后需要处理的就是将静态资源(例如图片和字体)拷贝到app bundle中。需要注意的是,如果图片资源是PNG格式,那么不仅仅对其进行拷贝,还会做一些优化(如果build settings中的PNG优化是打开的)。
虽然静态资源的拷贝是build phase中的最后一步,但这并不代表build过程已经完成了。例如,还没有进行code signing(这并不是build phase考虑的范畴),code signing属于build步骤中的最后一步
Packaging。
定制Build Phases
至此,你已经完全可以掌控build phases相关内容(先不考虑默认的设置项),例如,你可以在build phases中添加运行自定义脚本,就像CocoaPods使用的一样,来做额外的工作。当然也可以添加一些资源的拷贝任务,当你需要将某些确定的资源拷贝到制定的target目录中,这非常有用。另外你可以通过定制build phase来添加带有水印(包括版本号和commit hash)的app icon。只需要在build phase中添加一个
Run Script,然后用下面的命令来获取版本号和commit hash:
1 2 | version=`/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "${INFOPLIST_FILE}"` commit=`git rev-parse --short HEAD` |
如果你希望编写的代码比较简洁点,那么可以添加一个
Run Script,如果一个源文件超过指定行数,就发出警告。如下代码所示,设置的行数为200。
1 | find "${SRCROOT}" \( -name "*.h" -or -name "*.m" \) -print0 | xargs -0 wc -l | awk '$1 > 200 && $2 != "total" { print $2 ":1: warning: file more than 200 lines" }' |
Build Rules
Build rules指定了不同文件类型该如何编译。一般来说,开发者并不需要修改这里面的内容。如果你需要对特定的文件类型添加处理方法,那么可以在此处天剑一条新的规则。一条build rule指定了其应用于那种文件类型,该文件类型是如何被处理的,以及输出内容被放置到何处。比方说,我们创建了一条预处理规则,该规则将Objective-C的实现文件当做输入,然后解析文件内部的注释内容,最后再输出一个.m文件,文件中包含了生成的代码。由于我们不能将.m文件既当做输入又当做输出,所以我使用了.mal后缀,定制的build rule如下所示:
上面的规则应用于所有后缀为*.mal的文件,这些文件会被自定义的脚本处理(调用我们的预处理器,并附带上输入和输出参数)。最后,该规则告诉build system在哪里可以找到此规则的输出文件。
由于这里的输出是一个.m文件,那么build使这些.m文件会被编译处理(就如刚开始介绍的那些预处理步骤)。
在脚本中,我使用了少量的变量来指定正确的路径和文件名。在苹果的Build
Setting Reference.文档中可以找到所有可用的变量。build过程中,要想观察所有已存在的环境变量,你可以添加一个
Run Scriptbuild phase,并勾选上
Show environment variables in build log。
Build Settings
至此,我们已经了解到build phases是如何被用来定义build 过程的步骤,以及build rules是如何指定哪些文件类型在编译阶段需要被预处理。在build settings中,我们可以配置每个任务(之前在build log输出中看到的任务)的详细内容。在这里,你会发现build 过程的每一个阶段,都有许多选项:从编译、链接一直到code signing和packaging。注意,settings被分割为不同的部分,大部分会于build phases有关联,有时候也会指定编译的文件类型。
这些选项基本都有不错的文档介绍,你可以在右边面板中的quick help inspector或者 Build
Setting Reference中查看到。
工程文件
上面我们介绍的所有内容都被保存在工程文件(.pbxproj)中,除了其它一些工程相关信息(例如file groups),我们很少会深入该文件内部,除非在代码merge时发生冲突,或许会进去看看。我建议你用文本编辑器打开一个工程文件,从头到尾的看一遍里面的内容。它的可读性非常高,里面的许多内容一看就知道什么意思了,不会存在太大的问题。通过阅读并完全理解工程文件,这对于合并工程文件的冲突非常有帮助。
首先,我们来看看文件中叫做
rootObject的entry。在我的工程中,如下所示:
1 | rootObject = 1793817C17A9421F0078255E /* Project object */; |
1 23 | /* Begin PBXProject section */ 1793817C17A9421F0078255E /* Project object */ = { isa = PBXProject; ... |
mainGroup指向了root
file group。如果你按照这个思路,你可以快速了解到在.pbxproj文件中工程的结构。下面我要来介绍一些与build过程相关的内容。其中
targetkey指向了build
target的定义:
1 23 | targets = ( 1793818317A9421F0078255E /* objcio */, 170E83CE17ABF256006E716E /* objcio Tests */, ); |
1 2 3 4 5 6 7 8 9 101112 | 1793818317A9421F0078255E /* objcio */ = { isa = PBXNativeTarget; buildConfigurationList = 179381B617A9421F0078255E /* Build configuration list for PBXNativeTarget "objcio" */; buildPhases = ( F3EB8576A1C24900A8F9CBB6 /* Check Pods Manifest.lock */, 1793818017A9421F0078255E /* Sources */, 1793818117A9421F0078255E /* Frameworks */, 1793818217A9421F0078255E /* Resources */, FF25BB7F4B7D4F87AC7A4265 /* Copy Pods Resources */, ); buildRules = ( ); dependencies = ( 1769BED917CA8239008B6F5D /* PBXTargetDependency */, 1769BED717CA8236008B6F5D /* PBXTargetDependency */, ); name = objcio; productName = objcio; productReference = 1793818417A9421F0078255E /* objcio.app */; productType = "com.apple.product-type.application"; }; |
buildConfigurationList指向了可用的配置项,一般包括
Debug和
Release。根据debug对应的id,我们可以找到build
setting tab中所有选项存储的位置:
1 23 | 179381B717A9421F0078255E /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 05D234D6F5E146E9937E8997 /* Pods.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = YES; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; CODE_SIGN_ENTITLEMENTS = objcio/objcio.entitlements; ... |
buildPhases属性则简单的列出了在Xcode中定义的所有build
phases。这非常容易识别出来(Xcode中的参数使用了它们原本真正的名字,并以C风格进行注释)。
buildRules属性是空的:因为在该工程中,我没有自定义build
rules。
dependencies列出了在Xcode build phase tab中列出的target依赖项。
没那么吓人,不是吗?工程中剩下的内容就留给你去当做练习来了解吧。只需要顺着ID走,即可,一旦你找到了敲门,理解了Xcode中工程设置的不同section,那么对于merge工程文件的冲突时,将变得非常简单。甚至可以在GitHub中就能阅读工程文件,而不用将工程文件clone到本地,并用Xcode打开。
小结
当今的软件是都用其它复杂的一些软件和资源开发出来的,例如library和build工具等。反过来,这些工具是构建于底层架构的,这犹如剥洋葱一样,一层包着一层。虽然这样一层一层的,给人感觉太复杂,但是你完全可以去深入了解它们,这非常有助于你对软件的深入理解,实际上当你了解之后,这并没有想象中的那么神奇,只不过它是一层一层堆砌起来的,每一层都是基于下一层构建起来的。在这里,我们只是轻微的探究了一下build过程,当我们点击Xcode中的允许按钮时,并没必要深入了解内部具体发生了什么。只需要了解到build的过程,以及可控的一些操作顺序即可。当然,要想进一步深入了解,可以试着阅读其它一些文章。
相关文章推荐
- UIAccelerometer 加速计 UIAcceleration
- UIScrollView文档
- UILable自适应宽度
- [IOS]UIButton&UIImageView导圆角设边框
- HDFS之SequenceFile和MapFile
- 为什么判断UITextField判断为空不能用isEqualToString:@""
- Distinct Subsequences 挖坑待填 想不大明白这个最优子结构。。
- 理解Scroll View
- 给easyui datagrid添加搜索栏
- 将像素绘制到屏幕上去
- event.returnValue和return false的区别
- Android 如何在自定义界面上启用输入法 (How to enable inputmethod for the custom UI)
- 扩大UIButton的点击区域
- iOS开发 贝塞尔曲线UIBezierPath
- php里include,include_once, require和require_once的区别
- easyui 关于渲染的问题。
- 10G日志报错Heap size 2119K exceeds notification threshold (2048K)
- 关于ios7UI适配问题
- iOS 7 UI Transition Guide - Scope the Project | 研究工程
- iOS 7 UI Transition Guide - Before you start | iOS 7 界面过渡指导,在你开始前