liuzq(前端开发工程师)
本文是继上一篇《前端学习之iOS开发(一)》的续集,上一篇属于iOS开发的入门篇,主要内容为前端学习iOS开发的优势和对比学习。本文的内容为介绍在新的项目《有道口语大师》中的实际应用,主要包括第三方类库管理工具pods、sqlite、文件操作、网络请求、引导动画等在项目中的实践。
一、第三方类库管理工具CocoaPods
CocoaPods作为iOS开发的第三方类库管理工具,可以很好的解决第三方类库的检索、安装、更新等操作,功能与nodejs中的npm类似。
CocoaPods的安装和使用如下:
1、安装好Ruby环境
2、下载和安装命令(速度较慢):sudo gem install cocoapods
3、在与.xcodeproj同级目录下建立Podfile文件,文件内容如下:
文件中的内容用于指示下载哪些类库及指定类库版本号,例如,下载版本2.2.4的AFNetworking,不写版本的时候默认下载最新的版本,文件的意义同npm中的package.json。
使用 pod install 下载类库,使用该命令后会根据Podfile中的列表下载需要的类库。
类库下载过程中截图:
4、类库下载结束后会有如下提示
.xcworkspace文件是使用pod install后生成的新文件,在我们项目的根目录下,接下来就是使用.xcworkspace文件来打开我们的项目了,而不是使用.xcodeproj。双击.xcworkspace打开项目文件后会看到如下的目录结构:
其中红色部分就是我们通过pods下载的类库了,以AFNetworking类库为例,可以通过#import “AFNetworking.h”引入下载后的类库。
另外CocoaPods支持搜索命令,例如在终端中输入命令‘pod search AFNetworking’,便可以获得AFNetworking类库的各版本信息,这一点和命令‘npm search ’功能相同。
5、 CocoaPods使用总结,例如Podfile中的列表,大家可以看到下载了很多类库,但如果下载后的类库都添加到svn版本库中,这无疑会使我们的项目过于庞大,并且不利于其他同事checkout和管理,但我们可以不将下载后的类库添加到svn版本库中,只需添加PodFile到版本库中,这样需要checkout项目的同事就可以根据PodFile了解项目中使用的类库并通过pod命令下载依赖的类库。
关于CocoaPods的详细介绍和其他命令可以参见:https://github.com/CocoaPods/CocoaPods/wiki和http://cocoapods.org/。
最后前端开发者是不是觉得CocoaPods与npm有异曲同工之妙呢。
二、文件管理
1、NSBundle和NSDocumentDirectory
对于App有两个存储文件的目录结构,分别为:NSBundle和NSDocumentDirectory。
NSBundle目录中包含了程序会使用到的资源(图像、音频、视频、json文件等),这些资源都是随app第一次安装时进入到用户的手机中,并且存贮在app的安装包中。NSBundle目录是只读目录,也就是说在用户安装NSBundle后,里面的资源文件就不可修改了。所以如果资源文件是存储在NSBundle目录中,app上线后就算修改一个图片资源也要发布新的app版本。
NSDocumentDirectory是用户手机中应用程序沙盒中的目录,可用于存储在用户安装app后动态生成的文件,例如数据库文件,用户头像,从网络下载的其他资源等。下图为iOS模拟器中看到的《有道口语大师》应用程序沙盒中的目录结构:
其中data文件夹下存储的是语音评分的模型文件,lessonData文件夹下用于存储app中使用的数据包(json文件、图片、音频),OperationGifImages用于存储app中运营活动的图片文件,pandaDB用于存储用户数据库和头像,pandaResources用于存储即时更新的图片资源,从文件夹的描述上来看,大家应该明白NSDocumentDirectory目录与NSBundle目录的区别在于,前者用于存储动态更新的文件,因为前者是可以读写的,所以数据库文件和即时更新的文件都可以在这里存储。
在应用程序沙盒中除了Documents目录,还有Library目录,及tmp目录,但这两个目录都只适合临时存储。另外在向Documents目录下存储文件的时候一定要建立文件夹不要让文件散落到目录下,不然提交app的时候会被appleStore驳回。
下面简单介绍下如何获取到NSBundle和Documents目录,由于都是在api中有详细的讲解,这里只是想让初学者有个简单的了解:
(1)、获取NSBundle目录下的名为lessonData的压缩文件:
(2)、获得Documents根目录:
2、文件操作
这里不会重点讲文件的增删改查,对于增删改查操作只是一套api而已,读者可自行深入学习。这里主要讲为什么要将NSBundle中的部分资源文件拷贝到NSDocumentDirectory目录。这样可以让读者设计app的时候有更多的考虑。先看下我们的需求,在用户随app安装的时候会在NSBundle目录下存储一个lessonData.zip文件,该文件中包含json、图片、音频等资源文件。而且这些资源文件在app上线后会经常替换的,显然存在NSBundle目录下是不能达到即时更新的效果,所以我们要在app安装过程中将lessonData.zip文件解压到NSDocumentDirectory,这样做的好处是当json文件或者图片资源等有更新的时候可以通过网络请求立即更新NSDocumentDirectory中对应的文件。代码如下:
其中ZipArchive为用于解压的类库,可参见https://github.com/mattconnolly/ZipArchive。这里有个需要注意的地方是调用UnzipOpenFile打开文件之后一定要调用CloseZipFile2关闭文件,否则会引起内存泄露。
对于文件的增删改查可以通过下面的代码稍作了解,其中NSFileManager是Foundation库中负责处理文件的类。
(1)判断文件是否存在,不存在时创建:
(2)判断文件是否存在,存在时删除:
(3)循环拷贝NSBundle目录下文件到NSDocumentDirectory目录:
图中最后一条语句便是拷贝方法,但需要在拷贝前先判断目标文件是否已经在目标目录,如果文件已存在时进行拷贝会引发错误。
(4)要获取json文件中的内容可以通过如下方法:
三、网络请求(AFNetworking)
对于网络请求前端开发者应该很了解了,前端中可以使用ajax来实现异步请求。这里使用的是AFNetworking库。AFNetworking 是ios和mac os x下的网络框架,它是构建在Foundation URL Loading System之上的,封装了网络的抽象层,可以方便的使用,AFNetworking是一个模块化架构,拥有丰富的api。支持当前网络连接情况判断、GET、POST、POST Multi-Part格式的表单文件上传、下载及断点续传等操作。
分别用ajax和AFNetworking库实现POST请求的对比:
ajax:
AFNetworking:
可以看出使用方法上与ajax并无太多区别,只不过AFNetworking的方法名称等过于复杂,并在成功和失败的处理函数上使用的是object-c特有的代码块方式(block)。
这里着重讲下项目中利用AFNetworking完成下载数据压缩包的例子:
上面gif图中展示的边下载边显示下载进度的效果就是使用AFNetworking实现的,并且在下载前可以利用AFNetworking中的AFNetworkReachabilityManager先判断网络情况,然后在利用AFDownloadRequestOperation实现下载及获取下载进度的功能。
判断网络情况:
下载并获取下载进度:
下载处理的使用和ajax方法也很类似,只是多了一个用于获取下载进度的setProgressiveDownloadProgressBlock方法设置。
AFNetworking详细使用可参见:https://github.com/AFNetworking/AFNetworking
四、FMDatabase
在iOS开发中我们一般使用SQLite数据库,SQLite是一款轻型的数据库,是遵守ACID的关系型数据库管理系统,它的设计目标是嵌入式的,而且目前已经在很多嵌入式产品中使用了它,它占用资源非常的低,在嵌入式设备中,可能只需要几百K的内存就够了。对于这种不需要存储在web服务器上的,被称为“SQLite”的文件型数据库目前已被广泛使用,前端开发者应该并不陌生,因为HTML5的本地存储中使用的也是SQLite数据库。即使没有使用过也不必担心,对于iOS开发中使用的SQLite还是比较简单的,基本的sql语句就可以满足了 ,所以这里不需要有太多的数据库使用经验,对于一般项目来讲,大学的知识也就足够了。iOS中原生的SQLite API在使用上非常不便。于是,就出现了一系列将SQLite API进行封装的库,例如FMDB、PlausibleDatabase、sqlitepersistentobjects等,FMDB是一款简洁、易用的封装库,在《有道口语大师》项目中也是使用了FMDB。这里进行简单的介绍。
在下载FMDB文件后,工程中必须导入如下文件,并使用 libsqlite3.dylib 依赖包。
FMDB目录结构:
对于FMDB目录中的这三个文件做下功能描述:
FMDatabase : 创建SQLite数据库
FMResultSet :获取执行sql查询后的结果集
FMDatabaseQueue :在多个线程执行查询和更新时会使用这个类。
例如创建数据库:db = [FMDatabase databaseWithPath:database_path];其中database_path可以是第二小节讲到的应用程序沙盒中documents下的一个路径。
打开和关闭数据库:[db open]、[db close]
创建表:
可以使用FMResultSet对象的resultDictionary将结果集转为Dictionary:
通过intForColumn、stringForColumn、longForColumn等方法获取某一字段的值:
在使用过程中一定要注意[db open]和[db close]的配对使用,否则会造成内存泄露引起app崩溃。尤其注意某些数据库操作函数的嵌套使用。例如:
这段代码存在两个问题,在saveMissionScore:(NSString*)mid score:(NSNumber*)score方法中已经执行了[db open],但在unlockMissionId方法中又执行了[db open],另外在unlockMissionId中还执行了[db close],在unlockMissionId方法执行完毕时数据库已经处于关闭状态了,这时又去执行了sql操作,前面这两点都会引起数据库操作异常从而引起app崩溃。所以一定要确保[db open]和[db close]的配对使用。
开发者可以使用SQLite Professional、火狐浏览器的SQLite Manager等工具进行sql语句的验证及查看数据库文件。
对于数据库的增删改查语句这里就不展开介绍了,可以通过https://github.com/ccgus/fmdb和http://www.sqlite.org/inmemorydb.html去查看。
五、引导动画
目前很多app在启动后都会有一个引导动画,很多比较复杂和绚丽的动画会在一开始就吸引住用户。这一节主要是想通过app启动时的一个引导动画来学习iOS开发中的UIScrollView和动画的实现过程。先看下《有道口语大师》启动的引导动画:
实现分析:
1、对于UIScrollView的理解,前端人员可以理解为一个未设置overflow:hidden的div,当内容溢出容器后可以滚动显示出溢出的内容。
2、当我们想要知道div的滚动距离的时候肯定要监听div的scroll事件,在iOS的这个例子中当实现了UIScrollView的UIScrollViewDelegate协议后,我们就可以监听到UIScrollView的一切动作,包括横向、纵向滚动,及滚动的开始和结束。
3、通过如下设置便可实现UIScrollView的分页,假设设置分为3页:
其中guideControl是动画下面显示的分页器。
4、在前面3点介绍的iOS相关内容之后,剩下的就是我们前端擅长的动画操作了,只要实现UIScrollViewDelegate的scrollViewDidScroll:(UIScrollView *)scrollView方法,就可以监听到滚动的长度,该长度通过方法中的scrollView.contentOffset.x获得,然后通过该值来做相应的动画变化。其中涉及到的动画效果有CGAffineTransformMakeScale(缩放)、CGAffineTransformTranslate(移动)、CGAffineTransformRotate(旋转),这几个动画效果在CSS3中也有对应的实现方式,分别为:transform:scale(0.8,1)、translate(50px,50px)、transform:rotate(45deg),所以对于前端开发者并不难理解这些动画实现,接下来我们是不是也可以在webApp中实现一些native可以做的动画效果呢!
由于动画的代码还挺多,这里不贴出来了,可以看下如何通过scrollViewDidScroll:(UIScrollView *)scrollView方法获取到横向滚动距离,代码如下:
5、值得一说的是第二针的实现,读者仔细看第二针有很多小元素围绕着手机图片,如果这些小元素都拆分开再通过改变位置的方法实现显然不是最好的解决方案,代码会很复杂。那最后选择的方案是用一张大图,大图中只包含围绕着手机的小元素,并且将图片通过CGAffineTransformScale放大到8倍(这里的8倍是试出来的,主要是放大到足够大后保证小元素不会在屏幕上显示出来),滚动过程中再通过滚动的距离和图片放大后的宽度比例缩小图片,直到缩小到0。
六、使用GCD实现多线程(Grand Central Dispatch)
这个小节主要讲项目中一个例子,运用GCD实现异步操作。关于GCD的概念如下:
Grand Central Dispatch或者GCD,是一套低层API,提供了一种新的方法来进行并发程序编写。从基本功能上讲,GCD有点像NSOperationQueue,他们都允许程序将任务切分为多个单一任务然后提交至工作队列来并发地或者串行地执行。
与HTML5中的Web Workers目的相同,我们使用多线程大多数目的都是为了将某些耗时较长的处理交给后台线程去运行。例如下面这段js代码:
当num的值为100亿(不同的浏览器中有所差别)以上的值时,浏览器会弹出一个脚本运行时间过长的对话框,从而不得不终止当前计算。但如果将上述代码放在一个单独的js文件中,并通过Web Workers去执行,并在执行完毕后通过postMessage方法将执行结果发送给主线程,这样无论num为多大的值都可以正常计算并且不会影响用户的其他操作了。代码如下:
新建一个用于计算的sum.js:
在主线程中创建子线程并调用sum.js计算:
通过上面的对比例子之后,我们看下《有道口语大师》中实现清理app中的音频和图片资源的功能,并显示清理进度,实现效果如下:
读者可以根据下列代码及注释去理解,采用GCD的原因主要是删除文件过程是个耗时的操作,会阻塞UI线程,所以要创建一个异步线程在后台执行删除操作,再删除操作执行完毕后回到UI主线程更新删除进度,最后当删除操作全部完成后回到主线程中更新tableview中的被删除行。
其中dispatch_async为开启一个异步操作,第一个参数是指定一个gcd队列,第二个参数是分配一个处理事物的代码块到该队列,dispatch_get_global_queue(0, 0),指用了全局队列,一般来说系统本身会有3个队列:global_queue,current_queue,以及main_queue。 其中第一个参数0的值也是DISPATCH_QUEUE_PRIORITY_DEFAULT值,即可以写成:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),除此之外还有其他数值:DISPATCH_QUEUE_PRIORITY_HIGH,DISPATCH_QUEUE_PRIORITY_LOW,通过变量名成就可以理解出是设置异步队列的优先级,优先级的定义如下:
通过以上例子,最终我可以将代码简化为如下图所示的结构:
关于iOS中的多线程,除了GCD,读者还可以了解下使用NSThread、NSOperation和NSOperationQueue来实现多线程。
七、链式调用
前端开发者应该都很熟悉和喜欢使用jQuery的链式写法,例如如下代码:
在iOS开发中我们也可以通过返回self的方式实现链式调用,例如新建一个YDDictQuerier类:
现在就可以像使用jQuery一样采用链式写法了,代码如下:
八、内存监测与优化
本小节是讲在app启动后,如何通过Xcode随着不同的操作及app使用时间的增长来观察app所占用的内存。在前端开发中也有内存监测工具,例如chrome浏览器为开发者内置的内存跟踪工具,可以通过开发者工具的Profiles标签调出,该工具可以拍下当前JS的heap快照,并可以看到closure、array、object等的内存使用情况。在前端开发中例如:闭包上下文绑定后没有释放,定时器的处理函数没有及时释放(没有调用clearInterval方法)等都会引起内存泄露。在iOS开发中多数情况下也是因为没有及时释放不使用的内存引起app崩溃或由于内存占用过高导致app卡顿。当监测到app执行某一操作或到某一时间点的时候所占用的内存居高不下,并持续增长的话,基本确定app是有内存占用问题的。如下图在xcode6中提供的app内存监测界面:
图中显示的是当我们进入第四节中讲到的app引导动画界面后的内存变化情况,还记得在引导页面我们引入了一张很大的图片吧,由于这张图片的原因所以内存会变的很高,起初遇到这个问题,首先想到的就是在离开引导页面的时候删除图片引用就可以恢复内存使用量到较低的状态,但并没用达到预期效果,原因是虽然此时内存会居高不下,但这种由于引入大图片引起的内存过高是由于在引入图片后图片会缓存到内存中,这时即使使用代码强制删除图片引用,缓存中的图片也不会立即释放的,但稍后iOS系统会在图片不使用的阶段自动释放掉。但如果内存居高不下,持续增长的时候,开发者可以在对应的页面或执行的某个操作处视具体问题来查看是否有强引用、或某些耗时操作等需要优化,常出现的内存过高问题一般是在某一个controller或者view组件被移除或不需要引用的时候对应的dealloc方法由于强引用或其他原因没有执行引起的,可以通过在dealloc方法中设置断点或者打印log的方式查看相应的controller或view是否被释放,如果是强引用引起的视情况改为弱引用,或将耗时、耗内存的操作用上一节讲到的异步线程处理。
到这里本篇文章就结束了,主要介绍的是实现思路和类库的使用,并通过与前端开发进行对比学习加深理解和记忆,但并没有详细到代码级别,感兴趣的同学可以自己边看api边写demo来进一步学习。