语言规约
命名规约
- 【强制】命名约定通用准则:清晰、一致性、不能自我指涉
清晰:命名应该既清晰又简短,但拒绝为了追求简短而丧失清晰性,拒绝为了简洁进行随意缩写。
正例:
命名 | 说明 |
---|---|
insertObject:atIndex: | 好的命名 |
removeObjectAtIndex: | 好的命名 |
removeObject: | 这样命名也不错,因为方法将移除通过参数引用的对象。 |
destinationSelection | 好的命名 |
setBackgroundColor: | 好的命名 |
反例:
命名 | 说明 |
---|---|
insert:at: | 不清晰;插入什么?“at”表示什么? |
remove: | 不清晰:要移除什么? |
destSel | 不清晰,缩写含义不清 |
setBkgdColor: | 缩写含义不清 |
一致性:命名含义应该具有前后,全局的一致性,同个功能也应该使用同个名称。
命名 | 说明 |
---|---|
- (NSInteger)tag | 该方法同时定义在 NSView、 NSCell、 NSControl 这三个类里面。 |
不能自我指涉:除掩码常量、通知外,名称不应该自我指涉(self-reference)。
正例:
命名 | 说明 |
---|---|
NSString | 可以使用 |
NSUnderlineByWordMask | 掩码常量,可以使用Mask自我指涉 |
NSTableViewColumnDidMoveNotification | 通知常量,可以使用Mask自我指涉 |
反例:
命名 | 说明 |
---|---|
NSStringObject | NSString本身已经是Object了,不需要再在名字里显示指出 |
- 【强制】杜绝一切缩写,除以下已经长期使用形成共识的内容可以使用缩写。
命名 | 说明 |
---|---|
alloc | Allocate |
alt | Alternate |
app | Application. |
calc | Calculate |
dealloc | Deallocate |
func | Function |
horiz | Horizontal |
info | Information |
init | Initialize |
int | Integer |
max | Maximum |
min | Minimum |
msg | Message |
nib | Interface Builder archive |
pboard | Pasteboard |
rect | Rectangle |
Rep | Representation |
temp | Temporary |
vert | Vertical |
【强制】文件名、自定义类、Protocol禁止以以下系统前缀开头。
【AC,AB,AS,AL,AU,AV,CX,CF,CK,CN,CA,CB,NS,CF,CG,CI,CL,CM,MIDI,CM,CS,CT,CV,EK,EA,GC,GK,GLK,GSS,HK,HM,AD,CG,IN,GS,LA,MK,MA,MP,MT,MS,MF,MTL,MTK,MDL,MC,NE,NK,NC,AL,EAGL,PK,PH,CA,QL,RP,SF,SCN,SL,SF,SK,SC,TW,UI,UN,VS,VT,WC,WK】【参考】文件名、类、Protocol、常量、枚举等全局可见内容需要添加三个大写字符作为前缀,双字母前缀为 Apple 的类预留。尽管这个规范看起来有些古怪,但是这样做可以减少 Objective-C 没有命名空间所带来的问题。
【强制】方法名、参数名、成员变量、局部变量都采用小写字符开头,名称中的单词首字符要大写的小驼峰形式。另外,请不要在方法名称中使用前缀(category方法除外)。
fileExistsAtPath:isDirectory:
- 【强制】如果方法代表一个对象执行的动作,则其名称应该以一个动词开头:
- (void)invokeWithTarget:(id)target;
- (void)selectTabViewItem:(NSTabViewItem *)tabViewItem;
【强制】请不要使用 “do”或者 “does”作为名称的一部分,因为这些辅助性的动词不能为名称增加更多的含义。同时,请不要在动词之前使用副词或者形容词。
【强制】如果方法返回接收者的某个属性,则以属性名称作为方法名。如果方法没有间接地返回 一个或多个值,您也无须使用”get“这样的单词。
正例:
- (NSSize)cellSize;
反例:
- (NSSize)calcCellSize;
- (NSSize)getCellSize;
- 【强制】所有参数前面都应使用关键字。
正例:
- (void)sendAction:(SEL)aSelector to:(id)anObject forAllCells:(BOOL)flag;
反例:
- (void)sendAction:(SEL)aSelector :(id)anObject :(BOOL)flag;
- 【强制】如果您当前创建的方法比起它所继承的方法更有针对性,则您应该在已有的方法名称后 面添加关键字,并将其作为新方法的名称。
命名 | 说明 |
---|---|
- (instancetype)initWithFrame:(NSRect)frameRect; | NSView |
- (instancetype)initWithFrame:(NSRect)frameRect mode:(int)aMode cellClass:(Class)factoryId numberOfRows:(int)rowsHigh numberOfColumns:(int)colsWide; | NSMatrix 是 NSView 的子 类。 |
- 【推荐】请不要使用”and“来连接两个表示接受者属性的关键字。
虽然下面的例子使用”and“这个词感觉还不错,但是随着创建的方法所带有的关键字越来 越多,这种方式会引起问题
正例:
- (int)runModalForDirectory:(NSString *)path file:(NSString *) name types:(NSArray *)fileTypes;
反例:
- (int)runModalForDirectory:(NSString *)path andFile:(NSString *)name andTypes:(NSArray *)fileTypes;
- 【推荐】如果方法描述了两个独立的动作,请使用”and“把它们连接起来。
正例:
- (BOOL)openFile:(NSString *)fullPath withApplication:(NSString *)appName andDeactivate:(BOOL)flag;
- 【强制】您可以使用情态动词(在动词前冠以“can”,"should","will"等),使得方法的名称更加明 确,但是请不要使用“do”或“does”这样的情态动词。
正例:
- (void)setCanHide:(BOOL)flag;
- (BOOL)canHide;
- (void)setShouldCloseDocument:(BOOL)flag;
- (BOOL)shouldCloseDocument;
反例:
- (void)setDoesAcceptGlyphInfo:(BOOL)flag;
- (BOOL)doesAcceptGlyphInfo;
- 【强制】只有当方法间接地返回对象或者数值,您才需要在方法名称中使用 get"。这种格式只适 用于需要返回多个数据项的方法。
正例:
- (void)getLineDash:(float *)pattern count:(int*)count phase:(float *)phase;
【强制】下面是数条和方法参数命名相关的通用规则:
(1)和方法名称一样, 参数的名称也是以小写的字符开头,并且后续单词的首字符要大写。 例如:removeObject:(id)anObject。
(2)请不要在参数名称中使用"pointer"或者"ptr"。您应该使用参数的类型来声明参数是否是 一个指针。
(3)请不要使用一到两个字符的名称作为参数名。
(4)请不要使用只剩几个字符的缩写。【强制】方法名称的开头应标识出发送消息的对象所属的类:
正例:
- (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(int)row;
- (BOOL)application:(NSApplication *)sender openFile:(NSString
*)filename;
- 【强制】如果调用某个方法是为了通知委托某个事件已经发生或者即将发生, 则请在方法名称 中使用“did”或者“will”这样的助动词。
正例:
- (void)browserDidScroll:(NSBrowser *)sender;
- (NSUndoManager *)windowWillReturnUndoManager:(NSWindow *)window;
- 【强制】如果调用某个方法是为了要求委托代表其他对象执行某件事,当然,您也可以在方法名 称中使用“did”或者“will”,但我们倾向于使用“should”。
正例:
- (BOOL)windowShouldClose:(id)sender;
常量定义
- 【强制】请使用NS_ENUM枚举类型来表示一群相互关联的整数值常量。枚举项以枚举类型为前缀。
示例:
typedef NS_ENUM(NSInteger,NSMatrixMode) {
NSMatrixModeRadio = 0,
NSMatrixModeHighlight = 1,
NSMatrixModeList = 2,
NSMatrixModeTrack = 3
} ;
- 【强制】请使用NS_OPTIONS定义一组相互关联的位移枚举常量。位移枚举常量是可以组合使用的。枚举项以枚举类型为前缀。
示例:
typedef NS_OPTIONS(NSUInteger,NSMatrixModeMask) {
NSMatrixModeMaskBorderless = 0,
NSMatrixModeMaskTitled = 1 << 0,
NSMatrixModeMaskClosable = 1 << 1,
NSMatrixModeMaskMiniaturizable = 1 << 2,
NSMatrixModeMaskResizable = 1 << 3
};
- 【强制】请使用 const 来创建浮点值常量。如果某个整数值常量和其他的常量不相关,您也可以使用 const 来创建,否则,则应使用枚举类型。下面的声明展示了 const 常量的格式:
const float NSLightGray;
【强制】通常情况下,请不要使用#define 预处器理命令创建常量。对于整数值常量,请使用枚举类型创建,而对于浮点值常量,请使用const 修饰符创建。
【强制】有些符号,预处理器需要对其进行计算,以便决定是否要对某一代码块进行处理,则它 们应该使用大写字符表示。例如:
#ifdef DEBUG
- 【强制】推荐使用常量来代替字符串字面值和数字。常量应该用 static 声明为静态常量,而不要用 #define,除非它明确的作为一个宏来使用。
这样能够方便复用,而且可以快速修改而不需要查找和替换
正例:
static NSString * const ZOCCacheControllerDidClearCacheNotification = @"ZOCCacheControllerDidClearCacheNotification";
static const CGFloat ZOCImageThumbnailHeight = 50.0f;
反例:
#define CompanyName @"Apple Inc."
#define magicNumber 42
常量应该在头文件中以这样的形式暴露给外部:
extern NSString *const ZOCCacheControllerDidClearCacheNotification;
并在实现文件中为它赋值。
- 【强制】异常使用全局的 NSString 对象来标识,其名称按如下的方式进行组合:异常名称中的具有唯一性的那部分,其组成词应该拼写在一起, 并且每个单词的首字符要大写。
[Prefix] + [UniquePartOfName] + Exception
正例:
NSColorListIOException
NSColorListNotEditableException
NSDraggingException
NSFontUnavailableException
NSIllegalSelectorException
- 【强制】Notification消息使用全局的 NSString 对象进行标识,其名称按如下的方式组合:
[Name of associated class] + [Did | Will] + [UniquePartOfName] + Notification
正例:
NSApplicationDidBecomeActiveNotification
NSWindowDidMiniaturizeNotification
NSTextViewDidChangeSelectionNotification
NSColorPanelColorDidChangeNotification
类定义规约
- 【推荐】要尽可能地使用属性定义代替无修饰的实例变量。
正例:
@interface Item : NSObject
@property (nonatomic, copy) NSString* name;
@end
反例:
@interface Item : NSObject {
NSString* _name
}
@end
- 【推荐】如果需要自定义property的getter或setter方法时,请在声明property时一起声明掉。
@property(getter=my_XXX ,setter=my_setXXX) id xxx;
【推荐】对外暴露的属性,尽量定义为readonly。
【推荐】不建议使用@dynamic修饰属性,除非你真的知道自己在干什么。
【强制】在为某个类添加实例变量时,请记住下面几个因素:
(1)只暴露必须的对外接口或属性在.h中,其它private保留在.m里。
(2)请确保实例变量的名称能够扼要地描述它所保存的属性。【强制】不复写任何
+ (void) load
方法。所有的load方法的执行在Class的装载阶段,会延长App的启动时间.且如果存在稳定性问题,也没有可以修复的时机。【强制】
+(void)initialize
必须判断class类型或使用dispatch_once
防止执行多次.
由于任何继承类也会执行父类的initilize
,所以这里一定要做类型判断,或使用dispatch_once
来保障不会执行多次
if (self == [NSFoo class]) {
// the initializing code
}
- 【强制】不应该显式地调用
initialize
方法。如果需要触发初始化行为,则请调用一些无害的方法。
正例:
[NSImage self];
反例:
[NSImage initialize];
- 【强制】在property的getter方法里不能再显式或者隐式的调用同一个property的getter方法,会导致死循环。
反例:
@interface ALPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation ALPerson
- (void)setName:(NSString *)name {
self.name = name;//死循环!
}
@end
- 【推荐】方法调用,方法调用应尽量保持与方法声明的格式一致。当格式的风格有多种选择时,新的代码要与已有代码保持一致。
调用时所有参数应该在同一行:
[myObject doSomethingWith:arg1 name:arg2 error:arg3];
或者每行一个参数,以冒号对齐:
[myObject doSomethingWith:arg1
name:arg2
error:arg3];
方法定义与方法声明一样,当关键字的长度不足以以冒号对齐时,下一行都要以四个空格进行缩进。
[myObj short:arg1
longKeyword:arg2
evenLongerKeyword:arg3];
- 【推荐】使用
nonnull、nullable,__kindof
来修饰方入参数、返回值、属性
@property (nonatomic, strong, nonnull) Sark *sark;
@property (nonatomic, copy, readonly, nullable) NSArray *friends;
+ (nullable NSString *)friendWithName:(nonnull NSString *)name;
- 【强制】禁止从
designated initializer
里面调用一个secondary initializer
。如果这样,调用很可能会调用一个子类重写的init
方法并且陷入无限递归之中。
a. Objective-C 有指定初始化方法(designated initializer)
和间接(secondary initializer)
初始化方法的观念。designated
初始化方法是提供所有的参数,secondary
初始化方法是一个或多个,并且提供一个或者更多的默认参数来调用designated
初始化的初始化方法。
b. 一个类应该有且只有一个designated
初始化方法,其他的初始化方法应该调用这个designated
的初始化方法
c. 在希望提供你自己的初始化函数的时候,应该遵守这三个步骤来保证获得正确的行为:
(1)定义你的designated initializer
,确保调用了直接超类的designated initializer
。
(2)重载直接超类的designated initializer
。调用你的新的designated initializer
。
(3)为新的designated initializer
写文档。可以用编译器的指令__attribute__((objc_designated_initializer))
来标记。用编译器指令__attribute__((unavailable(Invoke the new designated initializer))
让父类的designated initializer
失效.
正例
@interface ZOCNewsViewController : UIViewController
- (instancetype)initWithNews:(ZOCNews *)news __attribute__((objc_designated_initializer));
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil __attribute__((unavailable("Invoke the designated initializer,call initWithNews:")));
- (instancetype)init __attribute__((unavailable("Invoke the designated initializer,call initWithNews:"));
@end
@implementation ZOCNewsViewController
- (id)initWithNews:(ZOCNews *)news
{
//调用直接父类的 designated initializer
self = [super initWithNibName:nil bundle:nil];
if (self) {
_news = news;
}
return self;
}
// 重载直接父类的 designated initializer
// 如果你没重载 initWithNibName:bundle: ,而且调用者决定用这个方法初始化你的类(这是完全合法的)。 initWithNews: 永远不会被调用,所以导致了不正确的初始化流程,你的类的特定初始化逻辑没有被执行。
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
// call the new designated initializer
return [self initWithNews:nil];
}
@end
反例
@implementation ParentObject
//designated initializer
- (instancetype)initWithURL:(NSString*)url title:(NSString*)title {
if (self = [super init]) {
_url = [url copy];
_title = [title copy];
}
return self;
}
//secondary initializer
- (instancetype)initWithURL:(NSString*)url {
return [self initWithURL:url title:nil];
}
@end
@interface ChildObject : ParentObject
@end
@implementation ChildObject
//designated initializer
- (instancetype)initWithURL:(NSString*)url title:(NSString*)title {
//在designated intializer中调用 secondary initializer,错误的
if (self = [super initWithURL:url]) {
}
return self;
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 这里会死循环
ChildObject* child = [[ChildObject alloc] initWithURL:@"url" title:@"title"];
}
@end
注释规约
【强制】头文件中的暴露的方法或者属性都必须添加注释
注释建议使用Xcode自带工具插入默认格式。
option+command+/
即可自动插入。【强制】自动生成的代码注释中的
placeholder
要替换掉【推荐】建议对于复杂难懂逻辑添加注释
代码组织规约
- 【推荐】当一个类功能很多时,建议使用Category的方式进行功能划分,这些Category可以放在同一个文件中。
示例:
@interface UIViewController (UIViewControllerRotation)
+ (void)attemptRotationToDeviceOrientation NS_AVAILABLE_IOS(5_0) __TVOS_PROHIBITED;
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation NS_DEPRECATED_IOS(2_0, 6_0) __TVOS_PROHIBITED;
@end
@interface UIViewController (UILayoutSupport)
@property(nonatomic,readonly,strong) id<UILayoutSupport> topLayoutGuide NS_AVAILABLE_IOS(7_0);
@property(nonatomic,readonly,strong) id<UILayoutSupport> bottomLayoutGuide NS_AVAILABLE_IOS(7_0);
@end
@interface UIViewController (UIKeyCommand)
- (void)addKeyCommand:(UIKeyCommand *)keyCommand NS_AVAILABLE_IOS(9_0);
- (void)removeKeyCommand:(UIKeyCommand *)keyCommand NS_AVAILABLE_IOS(9_0);
@end
- 【推荐】建议使用
#pragma marks -
来进行方法分组,提高可读性,具体样例如下,建议把生命周期,事件,property方法以及protocol方法进行区分。
示例:
#pragma mark - Lifecycle
- (instancetype)init {}
- (void)dealloc {}
- (void)viewDidLoad {}
- (void)viewWillAppear:(BOOL)animated {}
- (void)didReceiveMemoryWarning {}
#pragma mark - Custom Accessors
- (void)setCustomProperty:(id)value {}
- (id)customProperty {}
#pragma mark - IBActions
- (IBAction)submitData:(id)sender {}
#pragma mark - Public
- (void)publicMethod {}
#pragma mark - Private
- (void)privateMethod {}
#pragma mark - Protocol conformance
#pragma mark - UITextFieldDelegate
#pragma mark - UITableViewDataSource
#pragma mark - UITableViewDelegate
#pragma mark - NSCopying
- (id)copyWithZone:(NSZone *)zone {}
#pragma mark - NSObject
- (NSString *)description {}
【推荐】建议合理使用group或folder来组织工程结构,而不是全部放在source里,物理group与工程中group要对应
【推荐】过期方法,不要直接删除,先标记为
depcrated
。【推荐】建议类继承关系不要超过2层,并且抽取公共逻辑到父类,尽量避免父类,子类方法调用跳跃
【参考】尽量减少继承,可以考虑组合,
category,protocol
等方式【推荐】每个文件.m的方法数目不应该超过20个,每个方法的行数不应该超过200行。
每个方法应该只做一件事情。当函数过长时,它做的事情通常会不明确,后续会很难理解与维护。
- 【强制】函数内嵌套不能太深,一个函数内大括号里嵌套大括号不能超过三层。
超过三层已经很难理解一个函数的作用,可以将其中的一些逻辑抽离成一个单独的函数
【推荐】建议业务bundle使用统一的前缀来标识
【推荐】头文件中只暴露出需要给他人调用的类、方法及属性,私有类、方法、变量放在.m中
【强制】Release包必须关闭Log
【强制】必须清理工程中的所有warning
【推荐】长条件判断建议使用bool变量来代替
太长不容易调试,且不直观。
正例:
BOOL isConditionSatisfied = (1 == a.x && 3==b.y && 2 == c.x);
if (isConditionSatisfied){
doSomething()
}
反例:
if (a.x = 1 && b.y =3 && c.x = 2){
doSomething()
}
- 【推荐】条件判断,推荐加大括号,即使一行,容易导致的错误为,当 if 语句里面的一行被注释掉,下一行就会在不经意间成为了这个 if 语句的一部分。参考
正例:
if (!error) {
return success;
}
反对:
if (!error)
return success;
或
if (!error) return success;
【推荐】对三目运算使用时,要注意简化,
x=a?a:b
只要写成x=a?:b
即可;【推荐】编写
switch
语句的时候, 一定要实现default:
,防止外部异常调用,内部没有处理的情况,【强制】
switch
里每个case
里需要强制有break;
【强制】
switch
里每个case
里都要使用{}
所有代码括起来,就算只有一行;
最佳实践
多线程
【强制】自建线程必须命名。
【强制】多线程访问同一个对象时,必须注意临界区的保护
【强制】单例创建要使用线程安全模式,并且禁止在单例的
init
方法中使用dispatch_sync
来阻塞线程,极易出现死锁
正例:
+ (instancetype)sharedInstance {
static id sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
- 【强制】在多线程环境下使用懒加载方式加载变量,会有
crash
风险,必须加锁保护
正例:
//多线程环境下调用
- (NSCache *)contactCache
{
if (!_contactCache) {
@synchronized(self) {
if (!_contactCache) {
_contactCache = [[NSCache alloc] init];
_contactCache.name = @"contactCache";
}
}
}
return _contactCache;
}
- 【强制】
performSelector:withObject:afterDelay:
要在有Runloop
的线程里调用,否则调用无法生效。
说明:异步线程默认是没有
runloop
的,除非手动创建;而主线程是系统会自动创建Runloop
的。所以在异步线程调用是请先确保该线程是有Runloop
的。
【强制】禁止随意创建长驻线程,除非是在整个app运行周期内都必须存在且有任务运行的。
【推荐】
NSNotificationCenter
在iOS 8及更老系统上存在多线程bug,selector
执行到一半时可能会因为self
销毁而触发crash
,解决方案是在selector
里开始的地方引入下面的宏:
- (void)onMultiThreadNotificationTrigged:(NSNotification *)notify
{
__weak typeof(self) weakSelf = self;
__strong typeof(self) strongSelf = wself;
if (! weakSelf) {
return;
}
[strongSelf doSomething];
}
【推荐】在多线程应用中,
Notification
在哪个线程中post
,就在哪个线程中被转发,而不一定是在注册观察者的那个线程中。如果发送消息的不在主线程,而接受消息的回调里做了UI操作,需要让其在主线程执行。【推荐】仅当必须保证顺序执行时才使用
dispatch_sync
,否则容易出现死锁,应避免使用,可使用dispatch_async
。
反例:
// 禁止。出现死锁,报错:EXC_BAD_INSTRUCTION。原因:在主队列中同步的添加一个block到主队列中
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_block_t block = ^() {
NSLog(@"%@", [NSThread currentThread]);
};
dispatch_sync(mainQueue, block);
}
正例:
// 推荐
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_block_t block = ^() {
NSLog(@"%@", [NSThread currentThread]);
};
dispatch_async(mainQueue, block); //使用异步操作
}
- 【参考】使用
performSelector:withObject:afterDelay:
和cancelPreviousPerformRequestsWithTarget
组合的时候要小心
afterDelay
会增加receiver的引用计数,cancel
则会对应减一- 如果在
receiver
的引用计数只剩下1 (仅为delay)时,调用cancel
之后会立即销毁receiver
,后续再调用receiver
的方法就会crash
正例:
__weak typeof(self) weakSelf = self;
[NSObject cancelPreviousPerformRequestsWithTarget:self];
if (!weakSelf)
{
//NSLog(@"self被销毁");
return;
}
[self doOther];
【强制】禁止在非主线程中进行UI元素的操作
【强制】在主线程中禁止进行同步网络资源读取,使用
NSURLSession
进行异步获取【强制】如果需要进行大文件或者多文件的IO操作,禁止主线程使用,必须进行异步处理
【强制】对剪贴板的读取必须要放在异步线程处理,最新Mac和iOS里的剪贴板共享功能会导致有可能需要读取大量的内容,导致读取线程被长时间阻塞
正例:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
if (pasteboard.string.length > 0) {//这个方法会阻塞线程
NSString *text = [pasteboard.string copy];
[pasteboard setValue:@"" forPasteboardType:UIPasteboardNameGeneral];
if (text == nil || [text isEqualToString:@""]) {
return ;
}
dispatch_async(dispatch_get_main_queue(), ^{
[self processShareCode:text];
});
}
});
内存管理
【推荐】请慎重使用单例,避免造成产生不必要的常驻内存。
【推荐】单例初始化方法中尽量保证单一职责,尤其不要进行其他单例的调用。极端情况下,两个单例对象在各自的单例初始化方法中调用,会造成死锁。
【强制】
Delegate
需要用weak
进行引用。【强制】使用
block
时,需要在block
访问外部weak
修饰的self
,内部在重新strong
处理。避免RetainCycle
。【推荐】
strong
引用 子实例,weak
引用parent
,基础类型使用assign,NSString,NSArray,block
使用copy
【强制】对类添加属性时使用
copy
方式还是使用retain
方式规约:
- 对实现
NSCopying
协议的对象使用copy
方式。通常情况下,诸如NSString、NSURL, block,NSArray
这样的对象应该能被copy;- 像
UIView
的对象则应该可以被保持。strong
引用 子实例,weak
引用parent.
- 基础类型使用
assign
。
【强制】在
dealloc
中要记得要remove observer, callback=null
【强制】会循环使用的
Timer
(指定了repeat
参数为YES
),必须要在合适的时机调用invalidate
方法,否则会出现内存泄漏,在使用类的析构函数中调用Timer
的invalidate
方法为时已晚,因为timer
会对其传递的目标object
增加引用计数,若不调用invalidate
,使用类根本得不到析构。
对于指定了repeat
参数为NO
的Timer
,则可以不调用invalidate
方法。【强制】在
init
和dealloc
中不允许使用self
访问属性(父类属性除外),只允许通过"_变量名"直接访问。
- 容易出现重复创建对象,甚至
crash
问题- 在
init
和dealloc
阶段,self
是一个不完整的对象。- 由于
accessor
方法是可以被子类重写的,在调用父类init
初始化的时候,使用self
访问属性会调到子类重写的(如果有)getter
或setter
,这就出现了先于子类init
访问其属性或调用子类方法的情况,如果子类getter
或setter
中有一些特殊的处理逻辑,在某些极端情况下就可能出现行为不一致的问题。 由于在init
函数返回前,对象结构和结构是不稳定的,在init
函数内对任何方法的调用(尤其是public
方法)都应该慎之又慎。dealloc
同理。
【推荐】在非
init
和dealloc
方法中访问属性推荐通过getter
方法获取,不推荐直接使用“_变量名”。【推荐】在
init
中不需要直接使用的Property
,建议使用lazyloading
的方法创建。【强制】在创建大量临时的
UIImage
,或者Model
之类的对象的时,用@autoreleasepool
使autorelease
对象在结束时间释放,缓解内存的压力。比如:
正例:
NSMutableArray *dataList = [NSMutableArray new];
NSMutableArray *imageList = [NSMutableArray new];
[dataList enumerateObjectsUsingBlock:^(NSDictionary *dict, NSUInteger idx, BOOL *stop) {
@autoreleasepool {
NSData *data = dataList[idx];
UIImage *image = [[UIImage alloc] initWithData:data];
//可能对 image 进行一些处理,裁剪之类的
[imageList addObject:image];
}
}];
- 【强制】在使用到
UIScrollView,UITableView,UICollectionView
的Class
中,需要在dealloc
方法里手动的把对应的delegate, dataSouce
置为nil
- 防止在
scrollView
滑动时页面退出,delegate
释放,出现crash
问题- 苹果在iOS9上已经将以上类的
delegate
及datasource
由assign
改为了weak
,如果只支持9.0以上,则不需要手动置nil
- 【推荐】在
dealloc
中,避免将self
作为参数传递。如果被retain
住,到下个runloop
周期再次释放,则会造成多次释放crash
。
-(void)dealloc{
[self unsafeMethod:self];
//因为当前已经在self所指向对象的销毁阶段,如果在unsafeMethod:中将self放到了autorelease pool中,那么self会被retain住,计划下个runloop周期再进行销毁;但是dealloc运行结束后,self对象的内存空间就直接被回收了,self变成了野指针
//当到了下个runloop周期,self指向的对象实际上已经被销毁,会因为非法访问造成crash问题
}
【推荐】除非是非法参数等提前判断提前
return
的可以写在最前面。其他的return
建议有效返回值尽量只剩最后一个。提前return
时,要注意是否有对象没有被释放(常见的有CF对象
),是否有锁没有释放等配对问题。【强制】禁止一次性申请超过10MB的内存。
内存过高将会导致app被kill,并且没有crash堆栈。而申请大内存将会增加内存峰值,更容易出现内存过高而crash。
集合
包括,但不限于 NSMutableDictionay,NSMutableArray,NSMutableSet
【强制】插入对象需要做判空处理。
【强制】注意线程安全问题,必要时加锁,保障线程安全
【强制】先
copy
,再枚举操作,禁止对非临时变量的可变集合进行枚举操作,多线程情况下有可能因为可变集合在进行枚举时发生改变进而crash。
正例:
//正确的写法
- (void)checkAllValidItems{
[_arrayLock lock];
NSArray *array = [oldArray copy];
[_arrayLock unlock];
[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
//do something using obj
}];
}
反例:
//错误的写法
-(void)checkAllValidItems{
[self.allItems enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
//do something using obj
//如果在enumerate过程中其它线程对self.allItems进行了变更操作,这里就会引发crash
}];
}
【推荐】大部分情况下都不使用可变集合作为成员变量,如果确实需要进行集合的增删改操作,使用临时可变集合变量处理,之后再进行赋值操作。
【强制】禁止返回
mutable
对象,禁止mutable
对象作为入参传递。【推荐】如果使用
NSMutableDictionary
作为缓存,推荐使用NSCache
代替【推荐】容器类使用泛型来指定对象的类型
正例:
@property (readonly) NSArray<NSURL *> *imageURLs;
NSDictionary<NSString *, NSNumber *> *mapping = @{@"a": @1, @"b": @2};
反例:
@property (readonly) NSArray *imageURLs;
NSDictionary *mapping = @{@"a": @1, @"b": @2};
字符串
【推荐】当使用
keypaths:@"xx"
时候,尽量使用NSStringFromSelector(@selector(xx))
方式,防止某个key
被删除后没有编译感知【强制】取
substring
的时候要考虑emoji
字符的问题,防止截到中间crash
- (NSString *)dt_substringToIndex:(NSUInteger)index
{
//... 越界判断
NSRange wRange = [self rangeOfComposedCharacterSequencesForRange:NSMakeRange(0, index)];
return [self substringWithRange:wRange];
}
锁
【推荐】专锁专用,一个
lock
对象只负责一个任务。这样可以在逻辑上进行区分,也可以避免潜在的死锁问题【推荐】不同锁的使用场景:
(1)性能最好的属pthread_mutex、dispatch_semaphore
,另外dispatch_semaphore
在等待的时候会释放CPU资源,所以适合用在等待耗时较长的场景;
(2)@synchronized
是最简单易用的递归锁,不会有忘记unlock
的情况,但性能也是最低的,适合用在对性能要求不高的场景;
(3)其他的还有NSLock
,性能介于上面二者之间,也有对应的条件锁NSConditionLock
和递归锁NSRecursiveLock
,因为是Objective-C
对象,适合用在偏Objective-C
编程的场景,比如需要把锁存放在NSDictionary
中的场景。【强制】在使用锁的过程中如果要
return
,切记要先进行unlock
; 如果可能有exception
发生,那么需要在@finally
中进行锁的释放
正例:
- (void) exclusiveMethod1{
[self.lock lock];
if (condition == true){
//这里要记得unlcok,否则下次在进入这个方法就会发生线程被死锁的问题
[self.lock unlock];
return;
}
[self.lock unlock];
}
- (void) exclusiveMethod2{
[self.lock lock];
@try{
//异常发生
}@catch(NSException* ex){
}@finally{
//此处需要进行锁的回收
[self.lock unlock];
}
}
IO
【参考】尽量减少使用
NSUserDefault
.【推荐】
[[NSUserDefaults standardUserDefaults] synchronize]
会block
当前线程直到所有UserDefault
里的内容写回存储;如果内容过多,重复调用的话会严重影响性能。建议只有在合适的时候(比如退到后台)再进行持久化操作(此方法即将deprecated
,可以不再调用)【推荐】一些经常被读取的本地文件建议做好内存缓存,减少IO开销
【推荐】文件存储路径请遵循以下规则:
(1) Documents
目录:您应该将所有的应用程序数据文件写入到这个目录下。这个目录用于存储用户数据。该路径可通过配置实现iTunes共享文件。可被iTunes备份。
(2) AppName.app
目录:这是应用程序的程序包目录,包含应用程序的本身。由于应用程序必须经过签名,所以您在运行时不能对这个目录中的内容进行修改,否则可能会使应用程序无法启动。
(3) Library
目录:这个目录下有两个子目录:
Preferences
目录:包含应用程序的偏好设置文件。您不应该直接创建偏好设置文件,而是应该使用NSUserDefaults
类来取得和设置应用程序的偏好.
Caches
目录:用于存放应用程序专用的支持文件,保存应用程序再次启动过程中需要的信息。
可创建子文件夹。可以用来放置您希望被备份但不希望被用户看到的数据。该路径下的文件夹,除Caches
以外,都会被iTunes备份。
(4) tmp
目录:这个目录用于存放临时文件,保存应用程序再次启动过程中不需要的信息。该路径下的文件不会被iTunes备份。
UI
- 【推荐】不要在除了
viewDidLoad
方法之外调用ViewController
的self.view
来进行view
操作,特别是在一些系统通知之类的回调中,有可能造成self.view
创建出来之后没有被加入到当前层级,导致子view
的诡异问题.
- (void)didReceiveMemoryWarning{
[super didReceiveMemoryWarning];
[self.view doSomething]; //如果当VC已经被创建,但是view还没有加入到view层级中时(比如Tabbar初始化之后的非选中VC),此时接收到了内存警告,那么self.view会被直接创建,没有加入到层级,导致其子view可能处于异常的状态
}
- 【推荐】如果想要获取
app
的window
,不要view.window
来获取,可以使用[[UIApplication sharedApplication] keyWindow]
来获取。
说明:如果
view
不在展示时,获取window
会是nil
,而不是真正的app
所在的window
.
【强制】UI对象只允许在主线程访问。(避免在异步线程里释放,这样可以避免在
dealloc
时访问view
结构导致问题)【强制】禁止在
ViewController
的dealloc
方法中访问self.view
,会导致已经释放的view
被再次重建,可能会造成各种不可预知的问题【强制】显示带
textfield
的alert
之前,一定要确保键盘不在显示状态,否则会crash
参考文章
可以直接:[[[UIApplication sharedApplication].delegate window] endEditing:YES];
- 【强制】禁止使用
drawViewHierarchyInRect
截屏
原因:截屏会消耗大内存和耗性能,不建议使用该技术方案.
推荐使用snapshotViewAfterScreenUpdates
- 【推荐】不建议将
UIView
类的对象加入到NSDictionary, NSSet,
如有需要可以添加到NSMapTable
和NSHashTable
。
NSDictionary,NSSet
会对加入的对象做strong
引用,而NSMapTable、NSHashTable
会对加入的对象做weak
引用。
Category
- 【强制】
category
方法加自定义前缀。防止与其它人冲突。
正例:
@interface NSString(category)
- (NSString*)ali_stripWhiteSpace;
@end
反例:
@interface NSString(category)
- (NSString*)stripWhiteSpace;
@end
【强制】禁止
category
方法覆盖系统方法,防止出现方法调用的不确定性【推荐】对于一些提供
category
的工具库,建议根据不同类型功能拆分成不同的子bundle
,方便引用方按需引用,控制App
体积【强制】Category的源文件名称必须是“类名+扩展名.{h,m}”
正例:
NSString+ALiCategory.h
反例:
NSStringALiCategory.h
NSString_ALiCategory.h
异常
- 【强制】不要在
@finally
块中使用return
或者@throw
等导致方法执行中断的语句,会导致@try
内的return
失效
其它
【推荐】使用
Method swizzle
之前考虑是否有其他方法可以代替,禁止swizzle
其他基础库,二方三方库的方法【强制】
NSNotification
接口,userInfo
和object
的使用要规范。
说明:
object
通常是指发出notification
的对象,如果在发送notification
的同时要传递一些信息,请使用userInfo
,而不是object.
- 【推荐】在使用固定格式的
dateFormatter
时候,需要设置setLocale
为"en_US_POSIX"
,防止一些不同日历下格式异常,
示例:
NSDate* now = [NSDate date];
NSDateFormatter* fmt = [[NSDateFormatter alloc] init];
fmt.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
NSString* string = "1996-12-19T16:39:57-08:00";
NSDate* date = fmt.dateFromString(string);
【推荐】在使用
CTTelephonyNetworkInfo
的时候,务必使用全局的单例实例,这个类本身存在bug,如果有多实例会存在会导致小概率的crash。【强制】调用
block
时务必判断block
是否为nil
【推荐】调用
delegate
的optional
方法时,判断delegate
能否响应该方法,避免crash
【强制】禁止访问对象的结构体变量(使用->)
【强制】需要使用磁盘缓存的业务,务必提供清理缓存的能力
【强制】对于不确定对象类型的比较,可以使用
isEqual:
方法,其会对类型进行判断;对于确定对象类型的比较,比如NSString
,可以使用isEqualToString:
,其不对类型进行判断,但相比前者性能更好。
工程规约
版本管理规约
- 【建议】遵循语义化版本号规范,版本格式:主版本号.次版本号.修订号,版本号递增规则如下:
主版本号:当你做了不兼容的 API 修改,
次版本号:当你做了向下兼容的功能性新增,
修订号:当你做了向下兼容的问题修正。
先行版本号及版本编译信息可以加到“主版本号.次版本号.修订号”的后面,作为延伸。
【建议】App灰度使用四位版本号
【建议】业务方维护自己业务SDK的版本号,不要使用主App的版本号来做业务逻辑判断,如果有需要可以使用业务SDK的版本号来判断
分支管理
【建议】主分支Master
代码库应该有一个、且仅有一个主分支。所有提供给用户使用的正式版本,都在这个主分支上发布。【建议】开发分支Develop
日常开发分支在Develop,如果想正式对外发布,就在Master分支上,对Develop分支进行"合并"(merge)。【建议】临时性分支,按不同的需求,开启相应的临时分支,使用完以后,应该删除
功能(feature)分支
预发布(release)分支
修补bug(fixbug)分支
【强制】每次版本发布之后,都应该在代码仓库中对应的节点添加tag,保证版本的可回溯
【参考】在 Git 提交时可以使用 [添加],[修改],[删除],[修复],[更新]等前缀词语来表明当前的Commit 信息。
包管理
- 【参考】可以使用CocoaPods作为包管理工具
- 【强制】检查
podspec
的resource
选项,不要把Podfile、podspec、InfoPlist.strings、Info.plist
或者源文件等导出到使用方的工程中
Show Me The Code!
千言万语,不如直接看代码。这里我们简单写了一个Demo,用于展示规约中比较常见的一些条款的应用。
#import <Foundation/Foundation.h>
/**
性别的枚举值
- ALIPersonGenderMale: 男人
- ALIPersonGenderFemale: 女人
- ALIPersonGenderUnknown: 不知道是男是女
*/
typedef NS_ENUM(NSInteger, ALIPersonGender) {
ALIPersonGenderMale,
ALIPersonGenderFemale,
ALIPersonGenderUnknown
};
/**
人的基本类型
*/
@interface ALIPerson : NSObject
/**
名称
*/
@property(nonatomic, copy, nonnull) NSString *name;
/**
性别
*/
@property(nonatomic, assign) ALIPersonGender gender;
/**
初始化方法
@param aName 人的名字
@param aGender 人的性别
@return 返回人的实例
*/
- (nonnull instancetype)initWithName:(nonnull NSString *)aName gender:(ALIPersonGender)aGender;
@end
/**
一个人的体育运动
*/
@interface ALIPerson (Sports)
/**
扩展方法强调要有前缀,以免冲突
*/
- (void)alPlayBasketBallWithPerson:(nullable ALIPerson *)person;
@end
/**
程序员常用的语言枚举
- ALICoderLanguagesJava: Java
- ALICoderLanguagesC: C语言
- ALICoderLanguagesCPP: C++
- ALICoderLanguagesJavaScript: JavaScript
- ALICoderLanguagesPHP: PHP是世界上最好的语言
- ALICoderLanguagesRuby: ruby
- ALICoderLanguagesPython: python
- ALICoderLanguagesOC: Objective-C
*/
typedef NS_OPTIONS(NSUInteger, ALICoderLanguages){
ALICoderLanguagesNone = 0,
ALICoderLanguagesJava = 1 << 0,
ALICoderLanguagesC = 1 << 1,
ALICoderLanguagesCPP = 1 << 2,
ALICoderLanguagesJavaScript = 1 << 3,
ALICoderLanguagesPHP = 1 << 4,
ALICoderLanguagesRuby = 1 << 5,
ALICoderLanguagesPython = 1 << 6,
ALICoderLanguagesOC = 1 << 7
};
/**
程序员的基本协议
*/
@protocol ALICoderProtocol <NSObject>
/**
对大家说一句helloworld
@return 具体的helloworld内容
*/
- (nonnull NSString *)sayHelloWorld;
/**
喜欢的语言
@return 具体的语言,这里为了做事例用了mask
*/
- (ALICoderLanguages)preferLanguages;
@end
//Notification消息使用全局的 NSString 对象进行标识,其名称按如下的方式组合:
//[Name of associated class] + [Did | Will] + [UniquePartOfName] + Notification
//程序员即将写出一个bug的通知
extern NSString * _Nonnull const ALIChinaCoderWillWriteBugsNotification;
//程序员已经制造了一个bug的通知
extern NSString * _Nonnull const ALIChinaCoderDidWriteBugsNotification;
/**
程序员的代理,主要负责在程序员要写出bug的时候告诉代理,让代理处理下,是否要修复bug,或者打醒程序员
*/
@protocol ALIChinaCoderDelegate <NSObject>
/**
程序员即将写出bug的回调
@param coder 程序员本身
@param partner 程序员的搭档
*/
- (void)coder:(nonnull ALIPerson<ALICoderProtocol> *)coder willWriteBugsWithPartner:(nullable ALIPerson<ALICoderProtocol> *)partner;
/**
程序员已经写出bug的回调
@param coder 程序员本身
@param partner 程序员的搭档,我们一般认为跟妹子一起,由于程序员喜欢装逼,更容易写出bug
*/
- (void)coder:(nonnull ALIPerson<ALICoderProtocol> *)coder didWriteBugsWithPartner:(nullable ALIPerson<ALICoderProtocol> *)partner;
@end
/**
中国程序员
*/
@interface ALIChinaCoder : ALIPerson<ALICoderProtocol>
@property(nonatomic, weak, nullable) id<ALIChinaCoderDelegate> delegate;
/**
结对编程
@param partner 一起编程的程序员
*/
- (void)pairProgrammingWithPartner:(nullable ALIPerson<ALICoderProtocol> *)partner;
@end
/**
美国程序员
*/
@interface ALIUSACoder : ALIPerson<ALICoderProtocol>
@end
#import "ALIPerson.h"
@implementation ALIPerson
#pragma mark - Life Cycle
- (instancetype)initWithName:(NSString *)aName gender:(ALIPersonGender)aGender{
if (self = [super init]) {
_name = aName;
_gender = aGender;
}
return self;
}
@end
NSString * const ALIPersonNameYaoMing = @"姚明";
@implementation ALIPerson (Sports)
- (void)alPlayBasketBallWithPerson:(ALIPerson *)person{
if ([person.name isEqualToString:ALIPersonNameYaoMing]) {
NSLog(@"把球给我,我要回家!");
}else{
NSLog(@"看我装逼看我飞");
}
}
@end
//尽量声明变量而非用宏
NSString * const ALIChinaCoderNameNotFound = @"没人跟你说你好世界";
NSString * const ALIChinaCoderWillWriteBugsNotification = @"ALChinaCoderWillWriteBugsNotification";
NSString * const ALIChinaCoderDidWriteBugsNotification = @"ALChinaCoderDidWriteBugsNotification";
@implementation ALIChinaCoder
#pragma mark - ALCoderProtocol
- (NSString *)sayHelloWorld{
if (!self.name) {
return ALIChinaCoderNameNotFound;
}
return [NSString stringWithFormat:@"%@说你好世界",self.name];
}
- (ALICoderLanguages)preferLanguages{
return ALICoderLanguagesOC|ALICoderLanguagesJava|ALICoderLanguagesC;
}
#pragma mark - public
- (void)pairProgrammingWithPartner:(ALIPerson<ALICoderProtocol> *)partner{
switch (partner.gender) {
case ALIPersonGenderMale: {
NSLog(@"我去。。");
break;
}
case ALIPersonGenderFemale: {
[[NSNotificationCenter defaultCenter] postNotificationName:ALIChinaCoderWillWriteBugsNotification object:self];
[self.delegate coder:self willWriteBugsWithPartner:partner];
NSLog(@"哦也。。");
NSLog(@"看我手把手教妹子写代码");
[[NSNotificationCenter defaultCenter] postNotificationName:ALIChinaCoderDidWriteBugsNotification object:self];
[self.delegate coder:self didWriteBugsWithPartner:partner];
break;
}
default: {
NSLog(@"看情况再说。。");
break;
}
}
}
@end
NSString * const ALIUSACoderNameNotFound = @"No one say hello world";
@implementation ALIUSACoder
#pragma mark - ALICoderProtocol
- (NSString *)sayHelloWorld{
if (!self.name) {
return ALIUSACoderNameNotFound;
}
return [NSString stringWithFormat:@"%@ say Hello World",self.name];
}
- (ALICoderLanguages)preferLanguages{
return ALICoderLanguagesPHP;
}
@end