iOS最佳实践
译者注
原标题是iOS Good Practices,应该翻译成 iOS 良好实践/优秀实践的,不过好拗口,而且已经发出去了,暂且就这样吧。
以下是正文
就像软件一样,如果我们不持续改进这份文档,它就会落伍。我们希望大家都来帮助我们改进它 —— 只要开一个 issue 或者发送一个 pull request!
为什么阅读本文档
跳进了 iOS 的坑真是麻烦。无论是 Swift 还是 Objective-C, 都没有在其他地方广泛使用,而且这个平台对每个东西都几乎有它自己的命名方式,并且连要在真机上调试都充满了坎坷。无论你是刚刚入门 Cocoa 还是想纠正自己开发习惯的开发者,都能从本文档获益。不过下面写的仅仅是建议,所以如果你有一个更好的方案,那就试试吧!
入门
Xcode
项目初始化
开始iOS开发的时候,一个常见的问题是用代码写所有的 view 还是使用 Interface Builder(Storyboards 或者 XIB )。两种方案都久经考量。但是,有下面几个考虑:
为什么使用代码?
- Storyboard 因为它的复杂的 XML 结构容易带来版本冲突,这让代码合并变得困难。
- 容易用代码结构化以及复用 view,让你的代码变得 。
- 所有的信息汇集一处。在 Interface Builder 里面你需要点击所有的检查器来寻找你要找的东西。
为什么用 Storyboard?
- 为了更少的技术要求,Storyboard 使用了一个很好的直接贡献于项目的方法,比如,通过调整颜色或者布局的 constraints。然而,它要求一个项目已经做好配置,并且开发者有一些时间掌握基础
- 当你不用构建项目也能看到变化的时候,集成更快了
- 在 Xcode6 里面,自定义的文字和 UI 元素在 storyboard 里面都可以可视化表示,比你在代码里面修改好多了
- 从iOS8开始, 允许你设计不同类型设备的屏幕,不用重复一些工作
忽略文件
CocoaPods
sudo gem install cocoapods
要开始使用,仅仅需要在你的 iOS 项目目录下运行:
pod init
它会创建一个 Podfile, 会管理你所有的依赖,在 Podfile 中加入你的依赖后,允许
pod install
注意在之后,你需要打开 .xcworkspace
而不是 .xcproject
,否则你的代码就不能被编译了,命令:
pod update
项目结构
为了组织目录里面的上百个源代码文件,最好根据你的架构来设置一些文件结构。比如,你可以用这样的:
├─ Models
├─ Views
├─ Controllers
├─ Stores
├─ Helpers
首先,将他们创建为 Group(黄色的目录),用 Xcode 的项目导航里面的你的项目中。然后对每个项目里的文件,将它么连接到真实的文件目录 —— 通过打开它右边的文件检查器,点击小小的目录图标,在你的项目目录下创建一个和 group 同名的子目录。
本地化
一开始就应该把所有的显示给用户的字符串放进本地化文件。这样不仅仅为了翻译方便,同时也便于查找用户看见的文本。你可以在 build Scheme 中加入一个启动参数来指定特定的语言
-AppleLanguages (Finnish)
常量
创建被 prefix header 引入的一个 Constants.h
文件。
不要用宏定义(用 #define
),用实际的常量定义
static CGFloat const XYZBrandingFontSizeSmall = 12.0f;
static NSString * const XYZAwesomenessDeliveredNotificationName = @"foo";
常量类型安全,并且有更明确的作用域(并不是在所有没有被引入的文件中能使用)。不能被重定义,并且可以在调试器中使用。
分支模型
git flow release finish <version>
常用库
总得来说,来把一个第三方库加入到你的项目中需要慎重考虑。确实,一个灵巧的库或许能帮助你解决现在的问题,但是可能在之后陷入维护的噩梦,比如在下一个系统版本改变一些东西之后,或者一个第三方库的场景变成了官方的API。不过在一个良好的设计的代码中,切换实现是很轻松的。尽量考虑用 Apple 的广泛(而且优秀的)框架吧~
这个小节尽量保持剪短。库的特性是为了减少模板代码(比如 AutoLayout)或者解决复杂的需要很多测试的问题,比如日期计算。当你在 iOS 中更加专业的时候,挖掘这里的源代码,通过它们底层的 Apple 框架来认识他们,你会发现这些可以做大量工作。
AFNetworking
99.95% 的 iOS开发者使用这个网络库,当 NSURLSession
自己本身也非常完善的时候, AFNetworking
仍然能凭借很多 app 需要的队列请求管理功能立足于不败之地。
DateTools 日期工具
Auto Layout 库
Architecture 架构
-
- 这是 Apple 默认的架构(MVC),通过扩展一表示 Model 实例的存储层以及处理网络,缓存等内容。
- 每一个存储通过
RACSignal
s 或者void
返回值的自定义 block 方法来返回。
-
- 通过 "massive view controllers": MVVM 认为
UIViewController
子类是 view 的一部分,并且保持精简,通过在 viewmodel里面维持状态。 - 对于 Cocoa 开发者是非常新的概念,但是
- 通过 "massive view controllers": MVVM 认为
-
- 值得在大型项目中一看的架构,在 MVVM 都显得复杂,而且需要关注测试的时候。
事件模式
这里有一些通知其他对象的常用方法:
- Delegation (委托): (一对一) Apple 经常用它(或者说,太多了)。用它来执行回调,比如, Model View 做一个回调
- Callback blocks (回调代码块): (一对一) 可以更加解耦,可以维护类似的相关代码段。同时在有很多 sender 的时候比委托有更好的扩展性。
- Notification Center (通知): (一对多) 最常用的对象来向第一个观察者发送事件的方法。非常解耦合 - 通知甚至可以全局地进行观察,而不用引用派发对象
- Key-Value Observing (KVO,键值编码): (一对多) 不需要观察者来明确发送的时间,就像 Key-Value Coding (KVC) 符合观察的键(属性)。通常不推荐使用,因为他不自然的特性以及繁琐的API。
- Signals(信号): (一对多) 的核心, 允许链接和组合你的内容, 提供了防止 的一个方法。
Models
Views
当用 Auto Layout 布局你的 view 的时候,确保在你父类中加入了下面的代码:
+ (BOOL)requiresConstraintBasedLayout
{
return YES;
}
否则当系统没有调用 -updateConstraints
的时候,你可能会遇到奇怪的 bug。
Controllers
建议使用依赖注入,比如:传递任何需要的对象作为参数,而不是在一个单例中保持所有的状态。后一种方法仅仅在状态是 真的 全局的时候适用。
+ [[FooDetailsViewController alloc] initWithFoo:(Foo *)foo];
网络
传统的方式:使用回调 block
// GigStore.h
typedef void (^FetchGigsBlock)(NSArray *gigs, NSError *error);
- (void)fetchGigsForArtist:(Artist *)artist completion:(FetchGigsBlock)completion
// GigsViewController.m
[[GigStore sharedStore] fetchGigsForArtist:artist completion:^(NSArray *gigs, NSError *error) {
if (!error) {
// Do something with gigs
}
else {
// :(
}
];
这样运行,但是如果你有多个组合的网络请求的时候,就会进入回调嵌套的地狱。
Reactive 的方法: 使用 RAC 信号
// GigStore.h
- (RACSignal *)gigsForArtist:(Artist *)artist;
// GigsViewController.m
[[GigStore sharedStore] gigsForArtist:artist]
subscribeNext:^(NSArray *gigs) {
// Do something with gigs
} error:^(NSError *error) {
// :(
}
];
它允许通过信号和其他信号的结合,在展示它们之前做一些改变。
Assets 资源
Using Bitmap Images 使用位图
Asset catalogs 仅仅暴露了图片的名称,图片集里面的抽象的名字。这可以避免资源名字的冲突,就像 button_large@2x.png
的文件的命名空间在它的图片集里面。遵守一些命名规则可以让生活更美好:
IconCheckmarkHighlighted.png // Universal, non-Retina
IconCheckmarkHighlighted@2x.png // Universal, Retina
IconCheckmarkHighlighted~iphone.png // iPhone, non-Retina
IconCheckmarkHighlighted@2x~iphone.png // iPhone, Retina
IconCheckmarkHighlighted-568h@2x~iphone.png // iPhone, Retina, 4-inch
IconCheckmarkHighlighted~ipad.png // iPad, non-Retina
IconCheckmarkHighlighted@2x~ipad.png // iPad, Retina
修饰后缀 -568h
, @2x
, ~iphone
and ~ipad
是不必要的,但是有他们在文件里面,当把文件拖进去的时候,Xcode会正确地处置它们。这避免赋值错误。
使用向量图
代码风格
命名
尽管命名约定很长,但是Apple一如既往地在 API中 遵守了命名原则。
这里有一些你应该使用的基本原则:
一个方法用 动词 开头的表示它做了一些副作用,但是不会返回任何东西:
- (void)loadView;
- (void)startAnimating;
任何以 名词 开头的方法,没有副作用而且返回了它指的对象:
- (UINavigationItem *)navigationItem;
+ (UILabel *)labelWithText:(NSString *)text;
尽量分离两者,比如,不要在操作数据的时候产生副作用,反之亦然。这会让你的副作用包含到代码更小的细分粒度里面,让调试变得非常困难。
结构
#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, copy, readonly) Foo *foo;
@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 Interaction
- (void)foobarButtonTapped;
#pragma mark - XYZFoobarDelegate
- (void)foobar:(Foobar *)foobar didSomethingWithFoo:(Foo *)foo;
#pragma mark - Internal Helpers
- (NSString *)displayNameForFoo:(Foo *)foo;
@end
最重要的一点是在你项目的类中保持一致性
其他风格指南
我们公司没有任何公司级别的代码风格指南,详细看看其他开发者的 Objective-C 风格指南很有用,即使一些内容是公司相关的或者过于激进了。
诊断
编译警告
简单的来说,至少需要在 _“Other Warning Flags” 编译设置里面定义下面的值:
-
-Wall
(增加很多的警告) -
-Wextra
(增加更多的警告)
同时打开 “Treat warnings as errors”
Clang 静态分析
Clang 编译器(Xcode使用的)有一个 静态分析器 来进行你的代码控制和数据流的分析,来检测编译器不能检测的许多错误。
你可以通过在 Xcode 里面手动运行 Product → Analyze 菜单项来手动执行代码分析
分析器可以用浅或者深的模式允许,后者更加慢,但是可以从跨函数的控制流和数据流上分析更多问题
推荐:
- 打开 所有 分析器检查 (通过在 building setting 中打开所有 “Static Analyzer” 选项)
- 在 release 的编译设置里面打开 “Analyze during ‘Build’” 来让分析器自动在发布的版本构建的时候允许。(这样你就不需要记住要手动运行了)
- 把 “Mode of Analysis for ‘Analyze’” 设置为 Shallow (faster)
- 把 “Mode of Analysis for ‘Build’” 设置为 to Deep
调试
当你的 App 崩溃的时候,Xcode 不会默认进入到调试器里面。为了调试,你需要增加一个异常断点(在 Xcode 的 Debug 导航中点 “+”),来在异常发生的时候退出执行。在很多情况下,你需要看看触发这些异常的代码。它会捕捉任何异常,即使是已经处理的。如果 Xcode 在 一个第三方库里面中断执行,比如,你可能需要通过选择 Edit Breakpoint 并且设置 Exception 为 Objective-C.。
分析
Xcode 有一个叫 Instruments 的分析工具,它包括了
许多分析内存,CPU,网络通讯,图形以及更多的工具,它有点复杂的,但是它的追踪内存泄漏的时候还是蛮直观的。只需要在 Xcode 中 选择 Product > Profile,选择 Allocations, 点击 Record 按钮并且用一些有用的字符串过滤申请空间的信息,比如你自己的app的类名。它会在固定的列中统计,并且告诉你每个对象有多少实例。到底是什么类一直增加实例导致内存泄漏。
统计
一个好的实践是创建一个简单的 helper 类,比如 XYZAnalyticsHelper
,处理 app 内部 model 以及数据格式 (XYZModel, NSTimeInterval, …)的变换,来适配字符串为主的数据层,
- (void)pushAddItemEventWithItem:(XYZItem *)item editMode:(XYZEditMode)editMode
{
NSString *editModeString = [self nameForEditMode:editMode];
[self pushToDataLayer:@{
@"event": "addItem",
@"itemIdentifier": item.identifier,
@"editMode": editModeString
}];
}
另外的优点是,你在必要的时候可以替换整个统计框架,而不用改变 app 其他部分。
Crash Logs 崩溃日志
当你配置好后,确保你 保存了 the Xcode archive (.xcarchive
) 对于每一个 app 放出的版本。这个 归档中包含了构建的app的二进制以及调试符号(dSYM
),你需要用每个版本特定的app把你的 Crash 报告符号化。
构建
构建设置
每一个简单的 app 都可以不同的方式构建,最基本的分离是 Xcode 给你 debug 和 release 之间的构建方案。后者在编译的时候有更多的优化,可能会导致你需要多调试一些问题。 Apple 建议你在开发的时候用 debug 模式,在打包的时候用 release 设置。这是默认的 Scheme (Play 和 Stop 后面的下拉菜单),运行Run 的时候会 用 debug 设置而运行 Archive 的时候会使用 release。
关于构建设置的 xcconfig
文件
通常构建设置是 Xcode GUI定义的,但是你同样可以用 configuration settings files (“.xcconfig
files”),优点是:
- 你可以注释
- 你可以
#include
其他构建设置文件, 能帮助你减少重复:- 如果你有一些适用于所有构建设置的设置, 增加一个
Common.xcconfig
并且在其他构建设置文件里面#include
- 如果你,比如,希望有一个 “Debug” 构建设置文件,允许编译器优化,你只需要
#include "MyApp_Debug.xcconfig"
来重载其他设置
- 如果你有一些适用于所有构建设置的设置, 增加一个
- 冲突解决和合并变得更轻松
Targets
一个 target 是处于比项目更低一级的级别。比如,一个项目可能有多个target,可能重载它的项目设置。简单地说,每个 target 和一个 app相当。比如,你可能有几个因为国家区分的 app (从同样的代码编译)来提交到 App Store。每个会有 development/staging/release 的构建,所以最好通过构建设置而不是 target来区分。一个 app 只有一个 target 是很少见的。
Schemes
Schemes 告诉 Xcode 在你点击 Run, Test, Profile, Analyze 或者 Archive 操作的时候应该怎么做。它们把这些操作映射到一个 target 和一个构建设置中。你可以传递启动参数,比如 app 需要允许的语言(为了测试本地化)或者一些了为了调试用的诊断标志。
一个建议的 Scheme 命名是 MyApp (<Language>) [Environment]
:
MyApp (English) [Development]
MyApp (German) [Development]
MyApp [Testing]
MyApp [Staging]
MyApp [App Store]
对于大多数环境来说语言部分是不必要的,app 会可能会以非 Xcode 的方式安装,比如用 TestFlight, 启动参数会被忽略。这个情况下,为了测试本地化需要手动设置设备的语言。
部署
部署一个软件到 iOS 设备上并不直观。但是有一些核心观点,只要理解了,对你有很大的帮助。
签名
当你需要在真实设备上运行软件的时候,你需要用一个 Apple 认证的 证书 签名。每一个证书是连接到一个 公、私 密钥对,私钥会保存在你的 Mac 的 KeyChain 里面,证书有两种类型
- 开发证书: 每个组的开发者都有自己的证书,而且它通过请求特到。Xcode可以帮你完成,但是最好不用点击 "Fix issue" 来完成,而是理解它到底做了什么事情。在部署开发版本到设备上的时候需要这个证书。
- 发布证书: 可以有多个,但是最好每一个组织有一个,并且通过内部渠道共享。在提交 App 到 App Store 或者你的企业的内部 App Store的时候需要这个证书。
描述文件
除了证书,还有 描述文件, 它把设备和证书连接起来。而且,它分成开发和发布两种类型。
- Development provisioning profile: 开发描述文件, 它包含了一个包含所有能安装这个 app 的设备列表。它连接了一个或者多个开发者允许这个描述文件使用的证书。描述文件可以确认特定的 app,但是对于大多数开发目的,它特别适合用一个通配符描述文件,也就是 Apple ID 以一个 星号 (*)结尾的。
-
Distribution provisioning profile: 发布描述文件 本身,对于不同使用目的也有不同的类型。每一个发布描述文件 链接到一个发布证书,并且在证书过期的时候失效。
- Ad-Hoc: 就像开发证书一个,它包含了一个 app 可以安装的设备的白名单。这个描述文件可以用来做 beta 版本,每年可以测试 100 个设备。为了做更细致的测试以及升级到 1000 个测试用户,你可以使用 Apple 最近发布的 服务, Supertop 提供了一个 .
- App Store: 这个 profile 没有列出设备,任何人都可以通过苹果官方的发布来安装,所有 App Store 发布的 App都需要这个证书
- Enterprise: 就像 App Store,没有设备白名单,任何通过企业内部网络的人都可以从内部“应用商店”安装。应用商店可能只是一个带连接的网站。这个描述文件只允许企业账号使用。
想同步所有的证书和描述文件到你的机器,可以去 Xcode 的 Preferences 的 Accounts下,添加你的 Apple ID, 然后双击你的 Team 名称。然后底部会有一个刷新按钮,有时候你需要重启 Xcode 来让东西显示出来。
调试描述文件
上传
在上传二进制后,耐心等待,可能要花上一个小时。当你的 App 出现后,可以链接到对应的App版本并且提交审核
应用内购买
当验证一个 App内购的 receipt的时候,记住做以下步骤:
- Authenticity: receipt 是来自 Apple 的
- Integrity: receipt 没有被篡改
- App match: receipt 的 App bundle ID 和你的 App bundle ID 一致
- Product match: receipt 里面产品 ID和你期望的一致
- Freshness: 你之前没有验证一样的 receipt ID
如果可能,把你的 IAP 存储销售相关的内容存储在服务器端,并且只在一个合法的经过上述检查的 receipt。这样的设计避免了常见的盗窃机制,同时,因为服务器做了验证,所以你可以使用 Apple 的 HTTP 验证服务来取代你自己的 PKCS #7
/ ASN.1
格式。