Index

怎样减小iOS APP 或者 静态库的体积(精)

转载请注明粗处:http://elijahdou.github.io/

闲话不多收,直接上干货。

把打包好的.ipa文件的后缀改为.zip并解压。右键.appbundle 选择显示包内容。有些情况下,大一点的文件压缩后反而比小一点的文件压缩后的体积小,而我们真正关心的时候解压后的真实体积,所以一定要解压里面的资源文件,看解压后的size。从APP Store下载的.ipa文件要比自己本地打包的要大,因为APP Store对ipa包又做了加密处理。XcodeOrganizer windowEstimate Size功能能估计本地打包文件从APP Store下载时的大小。根据优化的28定律和常识,首先当然是多媒体资源的体积啦。

图片

  • 压缩图片 不重要的图片可适当采用 8bit PNG图片

    1.什么是矢量图 矢量图是由计算机的算法产生的,可以无限放大或缩小,不会有任何损失,通常由矢量软件制作。

    2.什么是位图 位图是由一个一个的小色块组成,放大后会看到那些小色块,同一面积内小色块越多,分辨率就越高。

    3.矢量图的优缺点 可以无限放大或缩小,不会影响图像素质,文件体积较小,编辑灵活。缺点是表达的色彩层次不清,整体观感效果不如位图

    4.位图的优缺点 不能放太大,减少文件分辨率后会影响图片质量,图片战胜空间较大,优点是能很细腻地表达图片的效果,图片表达效果非常好

    5.什么情况下用位图,什么情况下用矢量图 一些对图片要求高的用位图,例如照片。其他的尽量用矢量图。例如文字、表格、卡通图片等

  • 去掉无用的图片

  • 用代码绘制简单的纯色图片 用Sketch和PaintCode快速得到绘制代码

  • 如果不需要使用透明,可以用jpeg代替PNG。jpeg减少了些效率但更加小。需权衡性能,大小。
  • 对32位的图片,尽肯能的使用高压缩率,使用PS的“Save For Web”功能,可以有效的减小JPEG和PNG图片的尺寸。 默认情况下,在build时,PNG图像就被pngcrush压缩。

音频

视频

  • 视频也可以使用类似于音频的处理方法,音视频的压缩可以很大程度的压缩,但是要注意压缩的格式,是不是会增加编解码的负担,这要权衡考虑。

Assets

  • 检查bundle中的无用文件,不要打包到app或者静态库中。可以点击文件,在右侧的file inspector里面的target membership中取消勾选;或者在build phase里面的Copy Bundle Resources中去掉。
  • 确定 dead code(代码被定义但从未被调用)被剥离,build setting 里 DEAD_CODE_STRIPPING = YES。 去掉冗余的代码,即使一点冗余代码,编译后体积也是很可观的。

编译设置

  • Optimization Level 设置为Fastest, Smallest [-Os]Strip Debug Symbols During Copy设置为YES (COPY_PHASE_STRIP = YES),这样会减小接近一半的体积,但是在release版本,这些貌似是默认的配置,但是不妨也检查一下。 此外在debug版本最好在完成开发测试后,设置成这种模式,重新测试一遍,因为不同的编译设置可能会掩盖一些bug。

  • 设置IOS_DEPLOYMENT_TARGET 为想要运行系统的最低版本

  • 设置需要的arm 架构,设置 ARCHS = arm64可以消除armv6架构,潜在的减少近一半的容量。

    iOS Devices: ARM,尺寸,像素一览表

    1,如果想自己的app在各个机器都能够最高效率的运行,则需要将Build Active Architecture Only改为NO,Valid architectures选择对应的指令集:armv7 armv7s arm64。这个会为各个指令集编译对应的代码,因此最后的 ipa体积基本翻了3倍,Release版本必须NO。

    2,如果想让app体积保持最小,则现阶段应该选择Valid architectures为armv7,这样Build Active Architecture Only选YES或NO就无所谓了

其他

  • 将应用的中一些数据,如长字符串、表格等移到外部文件中,不要放在代码里面,这样能减小一些体积,因为外部文件的压缩率要比应用中的数据压缩率高。

参考资料

Reducing the size of my App

iOS微信安装包瘦身

iOS可执行文件瘦身方法

BDD行为驱动开发-学习总结

转载请注明出处:http://elijahdou.github.io/

BDD(行为驱动开发),写的很散,多包涵,还是要多学习一些敏捷开发,多用加深理解!!!

改变一个观念

大部分程序猿在开发的时候,都是直接构建业务代码,完成功能需求,而不是去先写测试代码。因为认为写单元测试之类的测试代码会增加两倍以上的代码量,繁琐不堪且不必要。但是在开发大型项目时,测试驱动开发能够有效的规避bug,理清业务需求,还能提高调试的速度。原因很简单,我们在写测试代码的时候要明确业务需求,尤其是业务需求模糊或者模棱两个的地方,我们必须要搞清楚了才能写测试代码;再者就是一键执行测试 检查模块功能的可用性,比常规的调试快多了吧。


对iOS开发而言测试方式的大概路线是单元测试(XCTest)、TDD、BDD。单元测试就是一系列的断言,网上很多资料,可以看一下,测试大型项目或者复杂逻辑时,力不从心。TDD(测试驱动开发)是敏捷开发的核心实践,基本思路就是通过测试来推动整个开发的进行,先把需求分析,设计,质量控制量化,根据需求排除模棱两可的需求,编写测试用例代码,然后根据测试用例编写产品代码,这就是所谓的TDD。

但是当我们使用TDD时,马上会出现一个问题,我要测试什么,只知道TDD测试驱动开发,却不知道测试什么。而BDD(行为驱动测试)从名字上就明确的告诉了我们要测试什么——行为。

什么叫行为:一个类对象的接口定义了其方法和依赖关系。这些方法和依赖关系决定了类对象如何与应用的其他部分交互 以及该类对象的功能,这些就是对象的行为。

那么“行为”怎么理解?

我的理解(纯粹是一家之言,每个人的理解不一样):

  • 第一步,需求分析,先明确要实现的类在业务需求上要实现的功能。依据“功能”,我们就能确定这个类的接口定义和依赖关系,这样我们就明确了该类的行为。

  • 开发者在编写测试代码的时候,要明确自己的定位,开发者是观察者,而类对象是被观察者,开发者要观察类对象的行为,这个行为包含两个方面:非互动行为 和 互动行为。非互动行为 就是不需要观察者与之互动就可以观察到的行为,可以理解为对象对自己施加的行为(可以理解为我们看到的它的长相,这是他自己打扮的结果);互动行为 就是观察者给被观察者一个信号,被观察者回复一个应答。 以UI控件的测试为例,先测试非互动行为,这个UI控件应该长成什么样子(即根据需求设计的UI,如title color等);然后是互动行为(如Button被点击的反应等)。
  • 行为定义包括两个方面,一是 接口定义的方法,二是 与其他类或者部分的依赖关系。测试的时候不要漏掉对依赖关系的测试。一些必须要处理的操作,但是属于内部实现,在当前对象的行为上体现不出来,应该怎么避免测试上的遗漏?这就是属于依赖关系,因为这些操作在当前的测试对象的行为上体现不出来,但是其他地方肯定要依赖这些操作(但这似乎又和不要测试内部实现有点矛盾)。

在iOS开发中,可用的BDD测试框架

OC版本的BDD框架: 基于宏定义和block实现

swift版本的BDD框架

在这三个OC的框架中,前两个最受欢迎,而从github上的star数量来说,Kiwi大约是Specta的两倍,从侧面说明了使用Kiwi的人最多。但是也有好多反应SpectaKiwi好用的,这主要是看个人的习惯和喜好。他们都是对XCTest的封装,所以不能同时集成到项目中使用。这两者简单使用过程来说,最直观的区别就是Kiwi的书写格式是消息发送,而Specta是点语法的链式表达。

Kiwi大约是Specta的主要区别(来源于多篇文章的总结):

  • Specta由github的RAC的那帮人维护,使用较多的黑魔法,一旦apple改变了测试的底层实现,可能会出现很多问题
  • 功能上来说,Kiwi功能要多一些Specta就是没有mock和验证功能Kiwi
  • 集成的方便程度,Kiwi只需要使用Pod集成一个Kiwi,而Specta还需要其他的三方库支持,如Expecta,因为Specta是轻量级的,需要其他框架配合完成更复杂的功能。
  • 使用的方便程度,这个因人而异,前者是OC的消息发送,后者是点语法的链式表达,个人倾向于点语法,整个表达结构和Masonry很相似。Specta的expectation语法有一个比Kiwi好的地方:每个变量都隐式boxing 如expect(items.count).to.equal(5)。不需要像Kiwi那样将5包装成NSNumber,theValue(5),so SpectaExpecta搭配使用效果更好。

Kiwi为例,学习BDD的过程

喵神的两篇文章,从测试的基础讲起,XCTest到TDD再到BDD再到Kiwi的学习使用,先学习这两篇,基本上就了解了BDD的Kiwi测试

接下来这一篇blog,很有价值,以为它用一个实际项目讲解了 RAC MVVMKiwi在项目中的实际使用。RACMVVM是我们下一步要用到的框架。

看完这三篇基本上就了解BDD,还有如下几篇可以看一下:


行为驱动开发大概三个步骤:

  • 选择最重要的行为,并编写行为的测试文件。此时,由于测试对象的类还没编写,所以编译失败。创建测试对象的类并编写类的伪实现,让编译通过。
  • 实现被测试类的行为,让测试通过。
  • 如果发现代码中有重复代码,重构被测试类来消除重复

简单说来,TDD的基本步骤就是“红→绿→大胆重构”。

先编写测试代码。这时因为还没有对应的产品代码,所以测试代码肯定是无法通过的。在大多数测试系统中,我们使用红色来表示错误,因此一个测试的初始状态应该是红色的。接下来我们需要使用最小的代价(最少的代码)来让测试通过。通过的测试将被表示为安全的绿色,于是我们回到了绿色的状态。接下来我们可以添加一些测试例,来验证我们的产品代码的实现是否正确。如果不幸新的测试例让我们回到了红色状态,那我们就可以修改产品代码,使其回到绿色。如此反复直到各种边界和测试都进行完毕,此时我们便可以得到一个具有测试保证,鲁棒性超强的产品代码。在我们之后的开发中,因为你有这些测试的保证,你可以大胆重构这段代码或者与之相关的代码,最后只需要保证项目处于绿灯状态,你就可以保证代码没重构没有出现问题。


测试中的原则,讲几个重要的,可测试 可重用之类的是必须的

  • 注重测试对象的行为,而不是内部实现,就是只测试接口和依赖关系,不测试内部实现
  • 单一性,一个测试文件只测试一个类
  • 小步前进,不要一次写好多测试代码,一起去实现业务,在一起测试
  • 及时重构,对结构不合理或者重复的代码,在测试通过后,应及时进行重构,并再次测试

知乎上有个问题TDD 与 BDD 仅仅是语言描述上的区别么?的一个回答如下:

BDD的核心价值是体现在正确的对系统行为进行设计,所以它并非一种行之有效的测试方法。它强调的是系统最终的实现与用户期望的行为是一致的、验证代码实现是否符合设计目标。但是它本身并不强调对系统功能、性能以及边界值等的健全性做保证,无法像完整的测试一样发现系统的各种问题。但BDD倡导的用简洁的自然语言描述系统行为的理念,可以明确的根据设计产生测试,并保障测试用例的质量。

这个回答提到的,也是我在学习BDD过程中想到的问题,有没有一种驱动测试的方法,能覆盖到所有边界,测试代码可重用性高。 至于性能测试 BDD是不是无能为力了? 看官如果有好的方法,评论一下。

iOS Good Practices(iOS最佳编程实践)最新版的中文翻译

本文转载自iOS Good Practices 最新版的中文翻译

  跟随iOS开发技术发展的潮流,我将持续维护本文档的中文版,如果你喜欢这个译本,也请给个 Star 鼓励下~~

  本文档的英文原版在这里,感谢Futurice团队卓越的工作,为我们提供这么优质的文档。

知识是人类进步的阶梯

翻译,喵 ~~

iOS开发的最佳实践

- 就像一个软件项目一样,这份文档如果我们不持续维护就会逐渐失效,我们鼓励大家参与到这个项目中来—仅需提交一个 issue 或发送一份 pull request

对其他移动平台感兴趣?我的Andriod开发最佳实践以及Windows App开发最佳实践可能会帮到你哦。

为什么写这个文档

iOS开发要上手比较困难,因为无论是 Objective-C 还是 Swift 在别处都没有广泛被应用,iOS 这个平台似乎对一切都有一套不同的叫法。当你尝试在真机上跑程序时难免会磕磕碰碰。这份持续更新的文档就是你的救星!无论你是Cocoa王国的新手,或是老练到只想知道”最佳做法”是什么,这份文档都值得一读。当然,内容仅供参考,你有理由采取不同的做法只要你愿意!

目录

如果你想阅读指定的小节,可以通过目录直接跳转

  1. 开始吧
  2. 常用库
  3. Architecture 架构
  4. 网络请求
  5. Stores 存储
  6. Assets 资源
  7. 编码风格
  8. Security 安全
  9. 诊断
  10. Analytics 统计分析
  11. 编译构建
  12. Deployment部署
  13. App内购(IAP)
  14. License
  15. More Ideas(计划)
  16. 译者

开始吧!

Xcode

Xcode是绝大多数 iOS 开发者选择的 IDE,也是 Apple 唯一一个官方支持的 IDE. 也有一些其他的选择,最著名的可能就是 AppCode了。但除非你已经对 iOS 游刃有余,否则还是用 Xcode 吧,尽管 Xcode 有一些缺点,但它现在还算是相当实用的!

要安装 Xcode ,只需在 Mac 的 AppStore 上下载即可。它自带最新版的 SDK 和 iOS 模拟器,其他版本可以在 Preferences > Downloads处安装。

创建工程

开始一个新的 iOS 项目时,一个常见的问题是:界面用代码写还是用 Storyboard、xib来画?现有的 App 中两种方式都占有相当的市场。就此我们需要考虑以下几点:

用代码写界面有啥好处?

  • Storyboard 的 XML 结构很复杂,所以如果用 Storyboard ,合并代码时很容易冲突,比起用代码来写,麻烦许多。
  • 用代码更容易构建和复用视图,从而使你的代码库更容易遵循 (Don’t Repeat Yourself)DRY原则
  • 用代码可以让所有的信息都集中在一处。但使用 Interface Builder 你得到处找对应的检查器,作死地各种点,才能找到你要设置的属性!

用 Storyboard 画界面有啥好处?

  • 对技术不太熟悉的人也可以画:调整颜色、布局约束等等,为项目作出直接贡献。不过做这些时,需要工程已经建好并了解一些基本的知识。
  • 开发迭代更快,因为不需要 build 工程就能预览到作出的改动,所见及所得:
    • Xcode 6中,在Storyboard里终于能看到自定义的字体和 UI 控件的样式了。这让你在设计时能更好地了解界面的最终外观。
    • 从 iOS8 开始(iOS7 部分兼容),你可以用SizeClasses 来设计同时支持各种屏幕尺寸的界面,省去了很多重复的工作。

gitignore文件

要为一个项目添加版本控制,最好第一步就添加一个恰当的.gitignore文件。这样一来不需要的文件(如用户配置、临时文件等)就不会进入 repository(版本仓库)了。可喜的是,Github 已经帮我们准备了 Objective-C版Swift版

Cocoapods

如果你准备在工程中引入外部依赖(例如第三方库),Cocoapods提供了快速而便捷的集成方法。安装方法如下:

sudo gem install cocoapods

首先进入你的工程目录,然后运行

pod init

这样会创建一个 Podfile 文件,在这里集中管理所有的依赖。添加你所需要的依赖然后运行

pod install

来安装这些库,并把它们和你的工程一起放进一个 workspace 里。在 commit 的时候,推荐把依赖库在你的 repo 里安装好之后再 commit ,最好不要让每个开发者 checkout后还要自己跑一下 pod install

注意:从此以后要用.workspace打开工程,不要再用.xcproject打开,否则代码编译不通过。

下面这条指令:

pod update

会把所有的 pod 都更新到 Podfile 允许的最新版本。你可以通过一系列的 语法来准确指定你对版本的要求。

项目结构:

把这些数以百计的源文件都保存在同一目录下,不根据工程结构来构建一个目录结构是无法想象的。你可以使用下面的结构:

|- Models
|- Views
|- Controllers
|- Stores
|- Helpers

首先,在 Xcode 的 Project Navigator (左边栏)里,把这些目录建立为group (小小的黄色”文件夹”),建在工程的同名 group 下。然后,把每一个 group 与工程路径下实际的文件夹链接起来,方法是:

  • Step 1、选中 group
  • Step 2、打开右边栏的 File Inspector
  • Step 3、点击小小的灰色文件夹 icon
  • Step 4、在工程目录下创建一个新的子文件夹,名称与 group 相同。

本地化

一开始就应该把所有的文案放在本地化文件里,这不仅有利于翻译,也能让你更快地找到面向用户的文本。你可以在 build scheme 里添加一个 launch 参数,指定在某种语言下启动 App,例如:

-AppleLanguages (Finnish)

对于更复杂的翻译,比如与名词的数量有关的复数形式(如 “1 person” 对应 “3 people”),你应该使用 .stringsdict格式来替换普通的 localizable.strings 文件。只要你能习惯这种奇葩的语法,你就拥有了一个强大的工具,你可以根据需要(如俄语或阿拉伯语的规则)把名词变为”一个”、”一些”、”少数”和”许多”等复数形式。

更多关于本地化的信息,请参考2012年2月的Helsink iOS会议的幻灯片。其中大部分演讲至少到2014年10月为止仍然不过时!

常量

创建被 prefix header 引入的一个 Constants.h 文件 不要用宏定义( #define ),用实际的常量定义

static CGFloat const XYZBrandingFontSizeSmall = 12.0f;
static NSString * const XYZAwesomenessDeliveredNotificationName = @"foo";

常量类型安全并有更明确的作用域(在所有没有引入的文件中不能使用),不能被重定义,并且可以在调试器中使用。

分支模型

App发布的时候把 Release 代码从原有的分支上隔离出来,并且加上适当的tag,是很好的做法,对于向公众分发(比如通过Appstore)的 app 这一点尤其重要。同时,涉及大量 commit 的 feature 应该在独立的分支上完成。 git-flow 是一个帮助你遵守这些规则的工具。它只是在 git 的分支和 tag 命令上简单加了一层包装,就可以帮助维护一套适当的分支结构,对于团队协作尤为有用。所有的开发都应该在 feature 对应的分支上完成(小改动在 develop 分支上完成),给 release 打上 app 版本的 tag,然后 commit 到 master 分支时只能用下面这条命令:

git flow release fininsh <version>


常用库

一般来说,在工程里添加外部依赖要谨慎。当然,眼下某个第三方库能漂亮地解决你的问题,但或许不久之后就陷入了维护的泥淖,最后随着下一版 OS 的发布全线崩溃。另一种情况是,原先只能通过引用外部库来实现的 feature,突然官方 API 也支持了。在设计良好的项目里,把第三方库替换为官方的实现花不了多少功夫,但在将来会大有裨益。永远要优先考虑用苹果官方的框架(也是最好的框架)来解决问题!

因此,这一章有意写得比较简短。下面介绍的第三方库主要用来减少模板代码(例如 Auto Layout)或者用来解决复杂的、需要大量测试的问题,例如计算日期。随着你对 iOS 越来越精通,务必要四处看看它们的源码,熟悉它们所使用的底层框架。你会发现做好这些就能减轻许多重担了。

AFNetworking

99.95% 的 iOS 开发者使用这个库,当 NSURLSession 自己本身也非常完善的时候, AFNetworking 仍然能凭借很多 App 需求的队列请求管理能力立于不败之地。

DateTools 日期工具

总的来说,不要自己计算日期DateTools 是一个经过彻底测试的开源库,你可以放心使用它来做这种事情。

Auto Layout 库

如果你更喜欢用代码写界面,你会用过 Apple 难用的 NSLayoutConstraint的工厂方法或者 Visual Format Language。前者很啰嗦,后者基于字符串不利于编译检查。

masonry 通过他自己的 DSL 来创建、更新和替换约束,利用语言丰富的操作符重载特性较优雅地实现了 AL。Swift 中一个类似的库是 Cartography。如果更加保守的话, FLKAutoLayout 是一个好的选择,它为原生API添加了一层简洁而不奇异的包装。


架构

  • Model-View-Controller-Store (MVCS)
    • 这是苹果默认的架构(MVC)上增加了一个 Store 层,用来吐出 Model, 处理网络请求、缓存等。
    • 每个 Store 暴露给 View Controller 的或是 RACSignal,或是返回值为 void,参数中带有自定义的 completion block的方法。
  • Model-View-ViewModel(MVVM)
    • MVVM 是为了解决“巨大的 view controller”而生,它把 UIViewController 的子类看做 View 层的一部分, 用 ViewModel 维护所有的状态来给 ViewController 瘦身。
    • 对于 Cocoa 开发者是一个很新的概念,但正在引起越来越多的关注。想了解更多请参考:Bob Spry 的 fantastic introduction.
  • View-Interactor-Presenter-Entity-Routing (VIPER).
    • 颇为奇特的架构,但项目大到即使使用 MVVM 都会太凌乱并且需要重点考虑项目可测性的情况下值得参考。

“通知”模式

以下是组建之间互发通知的一些常见手段:

  • Delegate (一对一) : Apple 官方经常用的模式(有些人认为用得太泛滥了)。主要用于回传,比如从模态框回传数据。
  • Callback blocks(一对一) : 耦合更松,同时能让相关联的代码在一起,并且消息发出者数量很多时比 Delegate 更方便。
  • Notification Center(一对多):可能是最常见的对象发送 events 给多个观察者的方法。耦合性非常松 - 没有任何对当前派发对象的引用的情况下,通知也能够在全局范围内被观察到。
  • Key-Value-Observing (KVO) : (一对多)。不需要被观测的对象主动”发出通知”,只需要被观测的键(属性)支持 Key-Value Coding (KVC)。这种模式比较含混,而且标准API比较繁复,所以一般不推荐使用。
  • Signal : (一对多)。是ReactiveCocoa的核心,它允许结合你的关键内容进行链式调用,用这种方法逃离回调深渊(嵌套过多的回调)

Models

要确保你的 Model 是不可变的,他们用来把远程 API 的语义和类型转换为 App 适用的语义和类型。Github的Mantle是个不错的选择。

Views

在自定义视图中使用 AutoLayout 时,推荐在初始化方法中创建并激活你的约束。如果你需要动态地改变你的约束,hold住(保留)他们(约束)的引用并在必要的时候关闭或激活他们。

只有在极少数情况下你需要重写 UIViewControllerupdateViewConstraints.如果这么做,要记得在View 类中加上如下代码:

Swift:

override class func requiresConstraintBasedLayout() -> Bool {
    return true;
}

Objective-C:

+ (BOOL)requiresConstraintBasedLayout {
    return YES;
}

不然,系统可能不会如期调用 -updateConstraints,而导致奇怪的 bug 。这一点上 Edward Huynh 提供的这个博客有更详细的解释。

Controllers

要使用依赖注入,也就是说,应该把 controller 需要的数据用参数传进来,而非把所有的状态都保持在单例中。后者仅当这些状态的确是全局状态的情况下才适用。

Swift:

let fooViewController = FooViewController(viewModel: fooViewModel)

Objective-C

FooViewController *fooViewController = [[FooViewController alloc] initWithViewModel:fooViewModel];

尽量避免在 view controller 中引入大量的本可以安全地放在其他地方实现的业务逻辑,这会让 view Controller 变得十分臃肿。Soroush Khanlou 有一篇 很好的博客 介绍了如何实现这种机制,而类似 MVVM 这样的程序架构将 view controller 当 views 对待,因此大大地减少了 view controller 的复杂度。


网络请求

传统方法:使用自定义回调 block

//GigStore.h
typedef void (^FetchGigsBlock)(NSArray *gigs, NSError *error);

- (void)fetchGigsForArtist:(Artist *)artist completion:(FetchGigsBlock)completion;

//GigStore.m
[GigStore sharedStore] fetchGigsForArtist:artist completion:^(NSArray *gigs, NSError *error) {
    if(!error) {
        //Do something with gigs
    }
    else {
        // :(
    }
};

这样虽可行,但如果要发起几个链式请求,很容易导致回调深渊。

Reactive 的方法:使用 RACSignal

如果你身陷回调深渊,可以看看 ReactiveCocoa(RAC).这是一个多功能、多用途的库,它可以改变整个 App 的写法。但你也可以仅在适合用它的时候,零散地用一下。

Teehan+lax以及NSHipster很好地介绍了 RAC 概念(以及整个 FRP 的概念)。

//GigStore.h

- (RACSignal *)gigsForArtist:(Artist *)artist;

//GigsViewController.m
[[[GigStore sharedStore] gigsForArtist:artist] subscribeNext:^(NSArray *gigs) {
                            // Do something with gigs
                        } error:^(NSError *error) {
                            // :(
                        }];

在这里我们可以把 gig(演出) 信号与其他信号结合,因此可以在展示 gig 之前做一些修改、过滤等处理。


存储

作为一个可以”在地面上移动”的移动应用,通常有某种存储模型把数据保存在某个地方,如硬盘上、本地数据库中或者远程的服务器上。在把模型对象的任意活动抽象出来的方面,Store 层也非常有用。

抓取数据通常是异步进行的,但它是意味着关闭后台请求还是从硬盘反序列化一个大文件呢?你的 Store 层的 API 必须通过提供某种延期机制反映出这种情况,就像同步返回数据将引起线程阻塞那样。

如果你使用 ReactiveCocoa, 通常会选择 SignalProducer 作为返回类型。举个栗子,获取某个艺术家的演出信息将会产生下面这样 Signature:

Swift + RAC 3:

func fetchGigsForArtist(artist: Artist) -> SignalProducer<[Gig], NSError> {
    //...
}

Objective-C + RAC 2:

- (RACSignal *)fetchGigsForArtist:(Artist *)artist {
    //...
}

这里,返回的 SignalProducer 仅仅是获取演出列表的一个”配方”。仅当被订阅者(如:一个 viewModel )启动时才会执行获取演出列表的实际的动作,在数据返回前取消订阅将会取消该网络请求。

如果你不想使用信号、”期货”或类似的机制来代表你未来的数据,你也可以使用常规的 block 回调。但要记住,block 块嵌套地进行链式调用,如在某个网络请求依赖于另一个的结果的情况下,就会迅速变得非常笨重 — 这种情况通常被称为“回调深渊“。


资源

Asset catalogs是管理你所有项目可视化资源的最好方式,他们可以同时管理通用的以及设备相关的(iPhoen4-inch,iPhone Retina,iPad 等)资源,并且会通过他们的名字自动分组。告诉你的设计师如何添加它们,(Xcode有内建的 Git 客户端)可以节省很多时间,否则你会很多时间从邮件或者其他渠道把它们复制到代码库中。同时,这样也可以让设计师即刻看到自己的改动,可以根据需求进行迭代。

Using Bitmap Images 使用位图

Asset catalog 只会暴露出一套图片的名字,省略了每张图片实际的文件名。这样类似 button_large@2x.png这类文件的命名空间仅限于 asset 内部,很好地避免了 asset 的命名冲突。然而,命名 asset 时遵循一些原则可以让生活更轻松:

IconCheckmarkHighlighted.png // Universal, non-Retina
IconCheckmarkHighlighted@2x.png // Universal, Retina
IconCheckmarkHighlighted~iPhone.png // iPhone, non-Retina
IconCheckmarkHighlighted@2x~iPhone.png // iPone, Retina
IconCheckmarkHighlighted-568@2x~iPhone.png // iPhone, Retina, 4-inch
IconCheckmarkHighlighted~iPad.png // iPad, non-Retina
IconCheckmarkhighlighted@2x~iPad.png // iPad, Retina

其中的 -568h@2x~iPhone以及~iPad这些标示符本省并不是必需的,但如果在文件名里加上它们,把文件拖动到 asset 时就能自动落到正确的”格子”上,因此能避免难以察觉的错误拖放。

Using Vector Images 使用矢量图

你可以把设计师设计的原始图矢量图(PDFs)放进 Asset catalog,让 Xcode 来自动生成位图。这样能减少工程的复杂度(减少文件的个数)。


编码风格

命名

Apple 非常注意在 API 中保持命名一致性,即便是非常冗长的命名也如此。做 cocoa 开发时要遵循 Apple的命名规范, 这样能让加入项目的新人轻松许多。

以下是几条看了就能用上的基本规则: 以动词开头的方法,表示它执行的操作会造成一些影响( 译者注:有时候是函数副作用 ),但是不返回任何值。 - (void)loadView; 或者 - (void)startAnimating;

以下注释来自“维基魔杖”

在计算机科学中,函数副作用指当调用函数时,除了返回函数值之外,还对主调用函数产生附加的影响。例如修改全局变量(函数外的变量)或修改参数。

函数副作用会给程序设计带来不必要的麻烦,给程序带来十分难以查找的错误,并且降低程序的可读性。严格的函数式语言要求函数必须无副作用。

任何以名字开头的方法,应该返回一个对象并且不能造成额外的影响 (即不带函数副作用)。 - (UINavigationItem *)navigationItem; + - (UILabel *)labelWithText:(NSString *)text;

尽可能地区分这两种方法有很多好处。比如当您转换数据的时候就不应该造成额外的影响 ( 译者注:即函数副作用。数据转换的时候即上面那些使用名字开头的方法,实际上是一种数据转换的方法),反过来也一样(没有函数副作用的函数应该返回某个对象,具体可参考严格意义上的函数式语言的要求)。这样的话可以让具有函数副作用的代码保持在一个小的比较集中的区域内,可以帮助理解代码并有利于 Debug.(类似我们的初始化全局变量的方法或者那些设置控制属性的方法等)

代码结构

Pragma marks是给方法分组很好的方法,特别是在 ViewController 中。下面是 swift/Objective-C 语言的一个在 viewController 中常见的结构:

Swift MARK 风格:

import someExternalFramework

class FooViewController : UIViewController, FoobarDelegate {
    let foo: Foo
    
    private let fooStringConstant = "FooConstant"
    private let floatConstant     = 1234.5
    
    //MARK: LifeCycle
    
    //Custom initializers go here
    
    //MARK: View LifeCycle
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // ...
    }
    
    //MARK: Layout
    private func makeViewConstaints() {
        // ...
    }
    
    //MARK: User Interaction
    
    func foobarButtonTapped () {
        // ...
    }
    
    //MARK:FoobarDelegate
    func foobar(foobar: Foobar didSomethingWithFoo foo: Foo) {
        // ...
    }
    
    //MARK: Additional Helpers
    private func displayNameForFoo(foo: Foo) {
        // ...
    }
}

Objective-C MARK风格:

#import "someModel.h"
#import "someView.h"
#import "someController.h"
#import "someStore.h"
#import "someHelper.h"
#import <someExternalLibrary/someExternalLibraryHeader.h>

static NSString * const XYZFooStringConstant = @"FoobarConstant";
static CGFloat const XYZFooFloatConstant = 1234.5;

@interface XYZFooViewController () <XYZBarDelegate>
@property (nonatomic, readonly, copy) Foo *foo;
@property (nonatomic, strong) UILabel *label; //译者加

@end

@implementation XYZFooViewController

#pragma mark - LifeCycle

- (instancetype)initWithFoo:(Foo *)foo;
- (void)dealloc;

#pragma mark - View LifeCycle

- (void)viewDidLoad;
- (void)viewWillAppear:(BOOL)animated;

#pragma mark - Layout

- (void)makeViewConstraints;

#pragma mark - Public Interface

- (void)startFooing;
- (void)stopFooing;

#pragma mark - User Interface

- (void)foobarButtonTapped;

#pragma mark - XYZFoobarDelegate

- (void)foobar:(Foobar *)foobar didSomethingWithFoo:(Foo *)foo;

#pragma mark - Internal Helpers

- (NSString *)displayNameForFoo:(Foo *)foo;

#pragma mark - Setter / Getter (译者加)

- (UILabel *)label;

@end

最重要的是让这些分块标记在工程里所有的类里保持一致!

其他的编程风格

Futurice(作者所在的公司)并没有公司范围的编码风格指南。不过,仔细研究一下其他开发社区的 Objective-C 风格指南会非常有用,尽管有些部分可能只对特定公司有效或比较主观。


安全

即使在这样一个时代,我们信任我们的便携设备,让其携带自己最私有的数据,但 app 的安全性仍然是一个经常被忽视的主题。尝试对数据安全性的设定找到一个良好的权衡,以下有一些简单的经久耐用的法则。另外,Apple 的 iOS安全指南是一个很好的入门教程。

数据存储

如果你的 app 需要存储敏感数据,比如用户名、密码、认证 “Token” 或者一些个人的用户信息,你需要将它们保存在本地且不允许从 App 外部进行读取。绝不能用 NSUserDefaults或别的存放在闪存的 plist 文件,也不能用 CoreData 来做,因为他们没有加密!绝大多数类似的情况,iOS KeyChain是你的救星。如果不习惯直接使用 C 的 APIs,你可以使用像 SSKeyChain或者 UICKeyChainStore 这样的一些封装。

在保存文件和密码时,确保正确而谨慎地选择恰当的安全等级。如果在设备锁定时(比如后台任务)你还需要访问文件,使用 “accessible after first unlock” 选项即可。其他的情况下,你应该要求设备在解锁之后才能访问数据。仅在需要使用敏感数据时才读取。

网络

确保任何时候与服务端的 HTTP 通信都是 TLS 加密的。为避免中间人攻击窃听你的加密数据,你可以设置证书约束(certificate pinning),像 AFNetworkingAlamofire这种流行的网络库都支持这样通信。

Logging(日志打印)

发布你的 app 之前,应特别小心地设置好合适的日志级别。构建的产品(ipa文件)绝不能(日志)记录登录密码、API的Tokens等类似的敏感信息,因为这很容易导致将他们泄露给公众。另一方面,记录基本的控制流程可以帮你定位用户所遇到的问题。

UserInterface(用户界面)

当使用 UITextField做密码输入时,记住设置它们的 secureTextEntry 属性为 true,以免明文显示密码。同时也应该关闭其”输入自动校正”的功能,并在任何合适的时刻清空密码,比如当 app 退到后台时。

当 app 退到后台时,清空剪切板可以避免密码或其他敏感数据被泄露。由于 iOS 可能需要你 app 的屏幕截图,以显示在 app 切换器中,所以在 applicationDidEnterBackground 方法返回前,应该确保 UI 上显示的所有敏感数据被清空。


诊断

编译警告

建议把编译警告都打开,并且像对待 error 一样对待 warning。这份幻灯片论证了这一点。幻灯片里同时还讲到了如何在特定文件或特定代码段中忽略特定的 warning。 一句话,在 build setting 的 “Other Warning Flags”中至少要加入以下两个值:

  • -Wall (开启非常多的额外的 warning)
  • -Wextra (开启许多额外的 warning)

同时打开 build setting 里的 “*Treat warnings as errors*

Clang静态分析器

Clang 编译器 (也就是 Xcode 适用的编译器) 有一个静态分析器(static analyer),用来执行代码控制流和数据流的分析,可以发现许多编译器检查不出来的问题。

你可以在 Xcode 的 Product —> Analyze里手动运行分析器。

分析器可以运行”shallow“和”deep“两种模式。后者要慢很多,但是有跨方法的控制流分析以及数据流分析,因此能发现更多问题。

建议:

  • 开启分析器的 全部检查(方法是在 build setting 的 “Static Analyzer” 部分开启所有选项)
  • 在 build setting 里,对 release 的 build 配置开启 “Analyzer during ‘Build’“。(真的,一定要这样做 — 你不会记得手动跑分析器的。)
  • 把 build setting 里的 “Model of Analysis for ‘Analyze’“设为 Shallow(faster)
  • 把 build setting 里的 “Model of Analysis for ‘Build’“设为 Deep

Faux Pas

由我们员工Ali Rantakari创作的 Faux Pas 是一个出色的静态 Error 检测工具。它能分析你的代码库,找出你全然不知的错误。在发布任何iOS (或 Mac)App之前务必要运行它一次!

Debugging

当 App 崩溃时,默认情况下 Xcode 不会进入 Debugger。要想进入 Debugger,需要添加一个 Exception Breakpoint (点击 Xcode 的 Debug Navigator 底部的”+”号),遇到 Exception 时就会暂停执行。在大部分情况下,你都能看到导致 Exception 的那行代码。 这种方法会捕捉到任何 Exception,包括已经做了处理的 Exception。如果Xcode经常停在良性的 Exception 上(比如第三方库),选择 Edit Breakpoint然后在 Exception 下拉菜单中选择 Objective-C可以减少这种情况的出现。

在 View 的 Debug 方面, RevealSpark Inspector是两个强大的可视化检查器,可以节约你大量的时间,尤其是用 AutoLayout 时想知道消失的视图去哪儿了的情况。 Xcode 也免费提供了一个类似的东西,不过只支持 iOS8 + ,并且还不够完善。

Profiling评测

Xcode 自带一套评测工具 “Instruments”。它包含了众多的评测工具:评测内存使用、CPU、网络连接、图像等等。它本身是个庞然大物,但一个比较简单直接的用途是用 Allocations Instrument 来检测内存泄露。只需在 Xcode 中选择 Product —> Profile,选择 Allocations instrument,点击 Record按钮,然后从 Allocation Summary中过滤一些有用的字符串,比如 app 中你自己写的类的类名前缀。在 Persistant一栏中的计数显示了每个对象有多少个实例。如果某个类的实例个数一直胡乱增长,说明有内存泄露。

众所周知的是 Instruments 有一个 Automation 工具可以把 UI 交互录制为 JavaScript 文件并重放。UI Auto Monkey是一个脚本,它可以借助 Automation 在你的 App 上随机点击、清扫、旋转,这对压力测试/浸泡测试很有帮助。

要格外注意的是,你在哪里以何种方式创建了巨耗资源的类。举个栗子,NSDateFormatter创建起来非常耗资源,当快速而连续这么做时,比如在 tableView:cellForRowAtIndePath:方法中,会真正减慢 App 的响应速度。你应该创建一个它的 static 实例,并在需要格式化日期时直接使用该实例。


统计分析

强烈推荐在你的 App 中添加一个统计分析的框架,它能帮助你看到用户实际上是怎么用你的 App 的。X 功能有价值吗?按钮 Y 太难找到了吗?要回答这些问题,可以把点击事件、计时以及其他可测的信息发送到一个能收集并可视化这些信息的服务上,比如 Google Tag Manager。Google Tag Manager 比 Google Analytics 更灵活一些,它在 App 和 Analytics 之间插了一个数据层,因此不须更新 app 就可以通过 web service 更改数据逻辑。

一种好的做法是加一个轻量的辅助类,比如 XYZAnalyticsHelper,用来把 App 内部的 model 和数据格式(XYZModel, NSTimeInterval等)翻译成以字符串为主的数据层。

Swift :

fun pushAddItemEventWithItem(item: Item, editMode: EditMode) {
    let editModeString = nameForEditMode(editMode)
    
    pushToDataLayer([
        "event" : "addItem",
        "itemIdentifier" : item.identifier,
        "editMode" : editModeString
    ])
}

Objective-C :

- (void)pushAddItemEventWithItem:(XYZItem *)item editMode:(XYZEditMode)editMode {
    NSString *editModeString = [self nameForEditMode:editMode];
    
    [self pushToDataLayer:@{
        @"event" : @"addItem",
        @"itemIdentifier" : item.identifier,
        @"editMode" : editModeString
    }];
}

这样做有一个额外的好处:在有必要时,可以清除掉整个统计分析框架,而 App 其余的部分不受任何影响。

CrashLogs崩溃日志

首先应该让 App 把崩溃日志发送到某个服务器上,这样你才能看得到。可以使用 PLCrashReporter结合自己的后台实现这个功能,但推荐使用已有的第三方服务,比如下面这些:

设置好这些之后,每次发布都要确保保存了 Xcode archive(.xcarchive).Archive 里包含编译出的二进制文件以及 Debug symbol( dSYM ),你需要这些数据来解析这个版本 App 的崩溃报告。


编译构建

编译配置

即使最简单的 App 也有不同的构建方式。 Xcode 提供的最基本的区别是DebugRelease模式。后者的编译时优化要强很多,代价是损失了 Debug 的可能性。苹果建议你开发时使用 Debug模式,提交到 AppStore 的包用 Release模式编译。默认的模式(在 Xcode 里的运行/停止按钮旁边的下拉菜单可以更改)就是这么设置的,Run 用 Debug, Archive 用 Release

不过对于真实的应用,这样还是过于简单。你可以— 不!是应该 — 有几套不同的环境,分别用于测试、更新和其他与服务相关的操作。每套环境都可以有自己的 base URL、log 级别、bundle identifier (这样就可以同时安装)、provision profile 等。因此,简单的 Debug/Release 不能满足需求。你可以在 Xcode 工程设置的 “Info” 一栏里添加更多的编译配置。

编译配置的xcconfig文件

编译配置一般是在 Xcode 的界面里设置的,不过你也可以使用配置文件(”.xcconfig 文件“)来设置。这样做的好处是:

  • 你可以添加注释来进行解释;
  • 你可以 #include其他编译文件,帮助避免重复:
    • 如果你有一些所有配置通用的设置,添加一个 Common.xcconfig 文件,然后把它 #include 到其他文件里;
    • 比如你想要加一个在 “Debug” 基础上开启编译优化的配置,只需 #include "MyApp_Debug.xcconfig",然后覆盖相应的设置
  • 合并和解决冲突更简单一些

更多关于本话题的信息,可以参考这些幻灯片

Targets

Targets 的概念比 project 低一个级别,即一个 project 可以有多个 targets,这些 targets 的设置 可以覆盖它的 project 的设置。粗略地说,每一个 target 对应着代码库上下文中的一个 app。举个栗子,你可能针对不同国家的 Appstore 有不同的 App (都是从同一个代码库编译出来的)。每一个 App 都需要 开发/staging(阶段性成果)/发布 的编译配置,因此用编译配置(build configurations)会比 target 更好一些。一个 App 对应只有一个 target 非常常见。

Schemes

Schemes 告诉 Xcode 在 Run、Test、Profile、Analyze 和 Archive 时分别应该干什么。基本上,以上每个操作的 Scheme 对应一个 target 和 一套编译配置。你也可以传递启动参数,比如 App 运行的语言(对于测试本地化很方便)或者设置一些 Debug 用的诊断标记。

Scheme 推荐的命名方式是 MyApp(<language>) [Environment]:

MyApp (English) [Development]
MyApp (German) [Development]
MyApp [Testing]
MyApp [Staging]
MyApp [AppStore]

大部分环境下,语言是不需要标明的,因为 App 有可能通过 Xcode 之外的途径安装,比如 TestFlight,这样启动参数就会被忽略,这种情况下,只能手动设置设备语言来测试本地化。


部署

将 app 安装到 iOS 设备上并不简单。那么我们在这里会介绍几个核心的概念,理解了这些概念会对你部署 app 有很大帮助。

Signing签名

只要你想把应用跑在真机上,你就需要在编译时用一个 Apple 颁发的 证书来签名。每一个证书对应一对公钥/私钥,私钥保存在你Mac的钥匙串中。证书有两种:

  • 开发证书:团队里的每个开发者都可以通过请求获得自己的开发证书。Xcode 可以自动完成这项工作,不过最好还是不要点击那个神奇的 “Fix issue”按钮,而是自己做一遍来理解这个过程到底做了什么。要把开发环境打的包安装到设备上就需要开发证书。
  • 分发证书:可以有多个,不过最好还是限制为每个组织一个,然后通过内部渠道分享它相关联的密钥。要发布到 AppStore 或者企业的内部 “Appstore”,需要这个证书。

Provisioning(证书)配置

除了证书之外,还有 Provisioning profiles(配置文件),它是关联证书与设备的一环。同样有两类,分别用于开发和发布:

  • Development provisioning profile (开发配置文件):它包括被授权安装/运行 App 的设备列表。同时它与一个或多个开发证书相关联,每一个开发证书对应一个可以使用这个 profile (配置文件)的开发者。这种 profile 可以与特定的 App 绑定,但对于开发的用途,大部分用通配的 profile 即可(AppID 以星号*结尾,比如 “net.senink.*“)。
  • Distribution provisioning profile (分发配置文件):有三种分发途径,每一种的使用情景都不同。每个 distribution profile 与一个分发证书相关联,证书过期即失效。
    • Ad-Hoc:与开发证书相同,它包含可以安装 App 的设备白名单。这种 profile 可以用来再每年最多100个设备上做 beta 测试(译者注:最近 Apple 放宽了限制:同种设备每年可以各有100个,即iPhone 100 ;iPad 100 ;iPhone touch 100 …),如果想通过规模更大的测试来改善设计及用户体验,可以使用 Apple 新推出的 TestFlight服务。Supertop 上对它的优势和问题做了很好的总结
    • AppStore:它没有包含设备列表,因为任何人都可以通过 Apple 的官方分发渠道安装。发布到 Appstore需要这种 profile。
    • Enterprise:和 Appstore 属于同一类型,没有设备白名单,任何人都可以通过企业内部的 “AppStore”来安装 App。

要把所有的证书和 profile 同步到你的设备上,在 Xcode 的 Preference 中的 Accounts里添加你的 Apple ID,然后双击团队(team)名称。底部有一个刷新按钮,但有时需要重启 Xcode 才能正常刷新。

DebuggingProvisioning配置文件的调试

有时你需要 Debug 一个 provisioning 问题。比如,Xcode 可能拒绝把包安装到设备上,因为设备不在(development 或 ad-hoc 的) profile 的设备列表上。这种情况下,你可以使用 CraigHockenberry 优秀的 Provisioning插件定位到~/Library/MobileDevice/Provisioning Profiles中,选择.mobileprovision文件然后按空格键启动 Finder 的快速搜索功能,它会展示出非常丰富的信息,包括:设备、授权、证书和 App ID 等。

上传

iTunes Connect是苹果 AppStore 上的 App 管理平台。上传一个包,Xcode 需要一个开发者账户的 Apple ID 来签名。如果你有多个开发者账户,想要分别上传他们的 App,可能遇到一些麻烦,因为不知道为什么 一个特定的 Apple ID只能与一个 iTunes Connect 账户相关联。替代的方法是,为每个 iTunes Connect 账户都创建一个新的 Apple ID,然后使用 Application Loader 代替 Xcode 来上传包。这样就把打包签名与上传 .ipa 文件的过程解耦了。

上传包之后,保持耐心,可能一个小时后这个版本的 App 才会出现在 Builds 一栏,当它出现后,你可以把它与 App 的版本信息关联起来,然后提交审核。


内购

验证 App 内购的收据时,请记得进行以下检查:

  • 真伪性: 购买收据是否确实来自 Apple;
  • 完整性: 收据有没有被篡改;
  • 应用匹配: 收据中的 Bundle ID 是否与你的 App 的 Bundle ID 相符;
  • 产品匹配: 收据的 product ID 是否与你预期的 product ID 相符;
  • 是否最新: 在这之前有没有见过相同的收据ID;

设计你的 IAP 系统时,尽量把售卖的内容存储再 server 端,然后仅当收到有效的、通过以上所有检查的收据后才把内容提供给 client 端。这样的设计防止了常规的盗版机制,并且— 既然验证是在 server 端进行的 — 你可以利用 Apple 的 HTTP 收据验证服务,而不是自己解析收据的 PKCS #7 / ASN.1格式文件。

关于这个问题,更多的信息请参考Futurice blog:Validating in-app purchases in your iOS app


授权

Futurice 署名 - 相同方式共享 4.0 国际许可协议(CC BY 4.0)


计划

  • 添加常用的编译警告
  • 添加如何使用Jenkins自动化打包分发
  • 添加一个跟测试相关的小节
  • 添加注意事项

译者

  KevinHM,喜欢就 Follow 吧,更多精彩将分享给您!

  文档的翻译也参考了iOS-good-practices-in-Chinese!

iOS和Android语音格式

转载请注明粗处:http://elijahdou.github.io/

见到有人讨论这个,就自己记录一下,方便以后使用。


iOS和Android支持的 即可以回放也可以录制的 音频类型

iOSAAC(MPEG-4 Advanced Audio Coding), Apple Lossless(ALAC), iLBC(internet Low Bitrate Codec, another format for speech), IMA4 (IMA/ADPCM), Linear PCM (uncompressed, linear pulse-code modulation), µ-law and a-law

Androidmp3, wma, ogg, AAC(enhanced AAC+, enhanced low delay AAC, AAC+, AAC-LC), AMR-NB, AMR-WB, WAV, MIDI

其中两个平台都支持的是AAC并且文件格式为MPEG-4。选择该格式是成本最低的方式,不需要再使用三方库转换。相对来说AAC对数据进行了压缩,文件较小,而且iOS平台上会有硬件加速,效率很高。

如果使用其他格式,那么需要使用三方库转换:libOpenCorelame

扩展延伸阅读

ios与android设备即时语音互通的录音格式

收集的Objective-C runtime博客及知识点总结(持续更新)

转载请注明出处:http://elijahdou.github.io/

这是在平时的学习和工作过程中,收集到的一些关于OC runtime的blog,讲的都很好,自己mark一下,方便回头查找、学习。文中的技术要点为各个blog的技术点的结论总结,抛却文中源码和解析过程,不想看原文的可以直接看要点干货。

关于category

深入理解Objective-C:Category 技术要点:

一 category是Objective-C 2.0之后添加的语言特性,category的主要作用:

  1. 为已经存在的类添加方法
  2. 把类的实现分开在几个不同的category文件里面,好处:
    • a)可以减少单个文件的体积
    • b)可以把不同的功能组织到不同的category里
    • c)可以由多个开发者共同完成一个类
    • d)可以按需加载想要的category 等等
  3. 声明私有方法
  4. 模拟多继承
  5. 把framework的私有方法公开

二 与extension对比

  • extension 在编译期决议,一般用来隐藏类的私有信息,你必须有一个类的源码才能为一个类添加extension,所以你无法为系统的类 比如NSString添加extension。而category在运行期决议,可以向任何类添加方法。
  • extension可以添加实例变量,而category是无法添加实例变量的(因为在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的。但是可以用关联属性来模拟添加属性~~~)。

三 被category覆盖掉的 类中的original method

  • category的方法没有“完全替换掉”原来类已经有的方法,而是将它放到了category的方法的后面。selector与方法的实现IMP是一个map结构的,map中的key就是selector,value就是IMP,可以理解为一个selector对应多个IMP,这多个IMP在一个链表中,在消息派发时,一旦匹配到IMP就不在向后查找,所以从表现上来说,category中方法好像覆盖了类中的original method。 正因为这样,我们仍然可以调用到类中的original method,只要顺着方法列表找到最后一个对应名字的方法,就可以调用原来类的方法(这是根据blog中的Code封装的一个方法):
- (IMP)ed_getOriginalMethodInClass:(Class)targetClass withSelector:(SEL)aSeclector
{
    if (targetClass) {
        unsigned int methodCount;
        Method *methodList = class_copyMethodList(targetClass, &methodCount);
        IMP lastImp = NULL;
        SEL lastSel = NULL;
        NSString *targetSELString = NSStringFromSelector(aSelector);
        for (NSUInteger i = 0; i < methodCount; i++) {
            Method method = methodList[i];
            NSString *methodName = [NSString stringWithCString:sel_getName(method_getName(method)) 
                                        encoding:NSUTF8StringEncoding];
            if ([targetSELString isEqualToString:methodName]) {
                lastImp = method_getImplementation(method);
                lastSel = method_getName(method);
            }
        }
    
        free(methodList);
        
        return lastIMP;
    }
    
    return NULL;
}

四 category和+load方法

  • 类中和category中均可以实现+load方法
  • 执行顺序: 先 类中,后 category中,多个category都实现了同名方法,那么会按照编译顺序执行。

小技巧:在Xcode中点击Edit Scheme,添加如下两个环境变量(可以在执行load方法以及加载category的时候打印log信息,更多的环境变量选项可参见objc-private.h): 设置环境变量 文件结构 编译顺序1 编译顺序2

设置如图一所示的环境变量就可以在启动应用的时候打印加载信息,文件结构如图二所示,分别有图三和图四两种文件顺序,那么编译使调用+load方法的顺序就不同,打印信息就不上了。

五 category和关联对象

  • 现在在category里面是无法为category添加实例变量的,虽然从原文blog的源码解析中可以看出apple有在category中实现添加property的意图,但是还没有实现。
  • 向category中添加属性,只能通过关联属性来做。举例代码如下:
@implementation MyClass (Category1)

+ (void)load
{
    NSLog(@"%@",@"load in Category1");
}

- (void)setName:(NSString *)name
{
    objc_setAssociatedObject(self,
                             "name",
                             name,
                             OBJC_ASSOCIATION_COPY);
}

- (NSString*)name
{
    NSString *nameObject = objc_getAssociatedObject(self, "name");
    return nameObject;
}

@end
  • 所有的关联对象都由AssociationsManager管理,而在对象的销毁逻辑里面,runtime的销毁对象函数objc_destructInstance里面会判断这个对象有没有关联对象,如果有,会调用_object_remove_assocations做关联对象的清理工作。

关于方法缓存

深入理解Objective-C:方法缓存 技术要点:

一 方法缓存

  • 类class的底层定义都是struct,类的定义里就有cache字段,类的所有缓存都存在metaclass上,所以每个类都只有一份方法缓存,而不是每一个类的object都保存一份
    struct _class_t {
      struct _class_t *isa;
      struct _class_t *superclass;
      void *cache;
      void *vtable;
      struct _class_ro_t *ro;
    };
  • 从父类取到的方法,也会存在类本身的方法缓存里。而当用一个父类对象去调用那个方法的时候,也会在父类的metaclass里缓存一份。
  • 类的方法缓存大小没有限制,为了防止快速无限增大,apple的实现方法会使缓存的大小增速慢一点,但是确实是没有上限的(具体可见原文)。
  • 为什么类的方法列表不直接做成散列表,而是使用list+单独缓存?

原文给出的可能原因:

  • 散列表是没有顺序的,Objective-C的方法列表是一个list,是有顺序的;Objective-C在查找方法的时候会顺着list依次寻找,并且category的方法在原始方法list的前面,需要先被找到,如果直接用hash存方法,方法的顺序就没法保证。
  • list的方法还保存了除了selector和IMP之外其他很多属性
  • 散列表是有空槽的,会浪费空间

关于runtime

Objective-C特性:Runtime, 这篇没有技术要点,要自己通读下来。推荐。