良好的编码规范不但可以提高项目的可维护性和可读性,而且也可以减少一些不必要的错误。个人极力推荐日常编码过程中尽量可以遵守 Apple 编码指南:Apple CodingGuidelines。下面是个人在开发过程中的一些总结。
单个源文件中保留相同的代码风格
// 不推荐
@property (nonatomic, strong) NSString *aString;
@property (strong, nonatomic) UIView *aView;
- (void)method1 {
}
- (void)method2
{
}
// 推荐
@property (nonatomic, strong) NSString *aString;
@property (nonatomic, strong) UIView *aView;
- (void)method1
{
}
- (void)method2
{
}
属性修饰符尽量不要省略
// 不推荐
@property (nonatomic) NSNumber *aNumber;
// 推荐
@property (nonatomic, strong) NSNumber *aNumber;
合理的使用空格、换行和缩进,保持代码美观。建议参考 Apple 空格使用风格
// 不推荐
-(void)myMethod{
if(1==a){
NSLog(@"%@",@"666");
}
b=1;
a=b>1?b:2;
c=a+b;
d ++;
}
// 推荐
- (void)myMethod {
if (1 == a) {
NSLog(@"%@", @"666");
}
b = 1;
a = (b > 1) ? b : 2;
c = a + b;
d++;
}
变量命名和格式如果拿捏不准的话,可以参考 Apple 风格。
// 不推荐【大多数人】
@property (nonatomic, assign) BOOL canScroll;
// 推荐【Apple】
@property (nonatomic, assign) BOOL scrollEnable;
代码对齐,让代码美如画
// 不推荐
static NSString *const kMyStringIsLongLong = @"kMyString";
static NSString *const kYourString = @"kYourString";
static NSString *const kHeString = @"kHeString";
// 推荐
static NSString *const kMyStringIsLongLong = @"kMyStringIsLongLong";
static NSString *const kYourString = @"kYourString";
static NSString *const kHeString = @"kHeString";
方法名过长时注意换行,以:
进行对齐。此外方法的参数最好不要超过 6 个,方法实现控制在 200 行以内,太长会导致可读性变差
// 不推荐
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onEnterBackground) name: UIApplicationDidEnterBackgroundNotification object:nil];
// 推荐
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(onEnterBackground)
name: UIApplicationDidEnterBackgroundNotification
object:nil];
规则:[Name of associated class] + [Did | Will] + [UniquePartOfName] + Notification
// 示例
UIKIT_EXTERN NSNotificationName const UIApplicationDidEnterBackgroundNotification;
集合类带上存储类型
// 不推荐
@property (nonatomic, strong) NSMutableArray *childViewControllers;
// 推荐
@property (nonatomic, strong) NSMutableArray<UIViewController *> *childViewControllers;
block 里面的代码尽量不要超过 5 行,最好抽成一个方法。这样可以防止遗漏 weakSelf 可能导致的循环引用问题,其次由于是弱引用,所以 weakSelf 可能随时被释放,这样还可以避免 block 里面方法调用行为不确定的问题
// 不推荐
__weak typeof(self) weakSelf = self;
[self queryMoreGameLivesWithCompletion:^(NSArray *rsp, BOOL success) {
// weakSelf 可能随时被释放,所以下面的方法调用行为是不确定
weakSelf.rsp = rsp;
[weakSelf doSomething];
[weakSelf doSomething2];
}];
// 推荐
[self queryMoreGameLivesWithCompletion:^(NSArray *rsp, BOOL success) {
// 调用行为确定
[weakSelf handleMoreGameLivesWithRsp:rsp success:success];
}];
- (void)handleMoreGameLivesWithRsp:(id)rsp success:(BOOL)success
{
self.rsp = rsp;
[self doSomething];
[self doSomething2];
}
建议最好为集合类创建安全方法,这样防止越界和插入空对象等异常【注意区分外放和内测版本】。例如为 NSArray 增加一个 safe 前缀完全方法
@implementation NSArray (Safe)
- (id)safeObjectAtIndex:(NSUInteger)index
{
// 增加内侧版本是为了可以在测试阶段屏蔽保护,做到及时发现问题,这样可以避免引起一些业务上的异常
if (INTERNAL_VERSION) {
return [self objectAtIndex:index];
} else {
if (self.count > index) {
return [self objectAtIndex:index];
}
return nil;
}
}
@end
NSArray *infos = [NSArray array];
// 不推荐
id model = [infos objectAtIndex:index];
//推荐
id model = [infos safeObjectAtIndex:index];
UICollectionView 注册默认 Cell 和 SupplementaryView,防止发生异常
[collectionView registerClass:[UICollectionViewCell class]
forCellWithReuseIdentifier:@"UICollectionViewCell"];
[collectionView registerClass:[UICollectionReusableView class]
forSupplementaryViewOfKind:UICollectionElementKindSectionHeader
withReuseIdentifier:@"header"];
[collectionView registerClass:[UICollectionReusableView class]
forSupplementaryViewOfKind:UICollectionElementKindSectionFooter
withReuseIdentifier:@"footer"];
delegate 方法建议声明为 @required,未实现方法时编译器会发出警告,有利于及时发现问题。如果为可选,在调用代理方法加上 respondsToSelector
判断
// @optional
if ([self.delegate respondsToSelector:@selector(myViewDidClick)]) {
[self.delegate myViewDidClick];
}
无 default 分支时,当有分支未实现时,编译器会发出警告,有利于及时发现问题。如果你对于警告不是很敏感的话,建议加上 default 分支,并且在 default 分支加上 NSAssert (断言),这样可以避免不必要的崩溃和及时发现问题。
关键路径输出日志有利于问题的发现与定位。例如一些较难重现的野指针崩溃,往往这些 Crash 日志的调用栈全是系统调用,此时如何是好?当然是结合用户日志进行综合分析,这样可以通过日志定位户当时在哪个页面和用户的操作路径,这些都是有利于提高崩溃重现概率的点。
如需要重新布局只需要调用 setNeedsLayout
即可,避免耗尽当次循环时间。除非需要同步布局才使用 layoutIfNeeded 更新布局
// 不推荐
- (void)setUserInfo:(id)userInfo
{
_userinfo = userInfo;
[self setNeedsLayout];
[self layoutIfNeeded];
}
// 推荐
- (void)setUserInfo:(id)userInfo
{
_userinfo = userInfo;
[self setNeedsLayout];
}
在 layoutSubviews(View) 和 viewDidLayoutSubviews(ViewController) 这个两个方法中进行视图布局。如果使用自动布局的话,请在初始化方法中完成
@implementation View
- (void) layoutSubviews
{
[super layoutSubviews];
// UI 布局
}
@end
@implementation ViewController
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
// UI 布局
}
@end
例如一些广告轮播视图在其不可见时应该停止轮播,防止额外的开销,提高整体 App 的性能
// 视图是否可见判断条件
if (viewController.isViewLoaded && viewController.view.window) {
// viewController is visible
}
if (view.window) {
//view is visible
}
// 推荐
- (void)didMoveToWindow
{
[super didMoveToWindow];
if (self.window) {
[self startTimer];
} else {
[self stopTimer];
}
}
使用 @[ ] 和 @{ } 创建集合对象时要确保容器内的 object 和 key 不为空。曾经遇到过 imageWithName:
返回为空导致的崩溃。
// 不推荐
@[[UIImage imageWithName:@"icon"]];
// 推荐
[NSArray alloc] initWithObjects:[UIImage imageWithName:@"icon"], nil];
[NSDictionary alloc] initWithObjectsAndKeys:@(1) : @"key", nil];
-
应该继承 UIView,而不是 UICollectionViewCell 或者 UITableViewCell 等特定类。这样有利于项目其他模块复用
-
如果视图层级过于复杂,建议将视图进行分层,例如分为:top / middle / bottom,这样不仅使得视图的层级结构更加清晰,而且有利于问题定位
-
复杂视图建议使用代码创建,而不是 storyboard 或者 xib。有利于后续维护和后来者接手。当然自己维护还好,但是后来者的话,看到满屏的线条和模糊不清的层级结构,只能说剪不断理还乱
1. 该继承谁?
// 不推荐
@interface MomentView : UICollectionViewCell
@end
// 推荐
@interface MomentView : UIView
@end
2. 代码 or Xib,分层 ?
// 不推荐
@interface MomentView : UIView
@property (nonatomic, weak) IBOutlet UIImageView *coverImageView;
@property (nonatomic, weak) IBOutlet UIImageView *iconImageView;
@property (nonatomic, weak) IBOutlet UILabel *nameLabel;
@property (nonatomic, weak) IBOutlet UILabel *fansLabel;
@property (nonatomic, weak) IBOutlet UICollectionView *picturesView;
@property (nonatomic, weak) IBOutlet UILabel *titleLabel;
@property (nonatomic, weak) IBOutlet UIButton *commentBtn;
@property (nonatomic, weak) IBOutlet UIButton *likeBtn;
@property (nonatomic, weak) IBOutlet UIButton *shareBtn;
@end
// 推荐
@interface MomentView : UIView
// top
@property (nonatomic, strong) UIView *topContainerView;
@property (nonatomic, strong) UIImageView *coverImageView;
@property (nonatomic, strong) UIImageView *iconImageView;
@property (nonatomic, strong) UILabel *nameLabel;
@property (nonatomic, strong) UILabel *fansLabel;
// bottom
@property (nonatomic, strong) UIView *bottomContainerView;
@property (nonatomic, strong) UICollectionView *picturesView;
@property (nonatomic, strong) UILabel *titleLabel;
@property (nonatomic, strong) UIButton *commentBtn;
@property (nonatomic, strong) UIButton *likeBtn;
@property (nonatomic, strong) UIButton *shareBtn;
@end
如果是为了查找集合内的某个特定对象,查找完毕后记住 break, 防止多余循环。我相信大多少人都知道是应该这样做,但是我就是忘了呀,所以在 bug 不多的时候多回去看看自己的代码,或者是互相 review 代码。
// 不推荐
UIView *videoPreView = nil;
for (UIView *subview in self.subviews) {
if ([subview isKindOfClass:[VideoView class]]) {
videoPreView = subview;
}
}
// 推荐
UIView *videoPreView = nil;
for (UIView *subview in self.subviews) {
if ([subview isKindOfClass:[VideoView class]]) {
videoPreView = subview;
break;
}
}
给 Category 增加自定义关联属性时避免直接声明 property ,应该使用对应的存取方法代替。这样可以避免由于归档时将该变量存入本地数据库,防止版本更新可能导致数据类型不对的异常。例如:V1.0 myObject 是 NSNumber,在 V2.0时由于 xx 原因改为 NSString。如果测试没有覆盖到,那么上线你会哭
@interface NSObject (Extension)
// 不推荐
@property (nonatomic, strong) NSNumber *myObject;
// 推荐
- (NSObject *)myObject;
- (void)setMyObject:(NSObject *)myObject;
@end
使用第三方库时应该独立封装一个中间层进行管理,避免直接调用,不然以后更换第三方库的时候会崩溃。
// 举例 SDWebImage
// 不推荐
[self.coverImgView sd_setImageWithURL:[NSURL URLWithString:@"http://xxx.com/my.png"]
placeholderImage:[UIImage imageNamed:@"default"]];
// 推荐
@interface UIImageView (MyWebCache)
- (void)cc_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder;
@end
[self.coverImgView cc_setImageWithURL:[NSURL URLWithString:@"http://xxx.com/my.png"]
placeholderImage:[UIImage imageNamed:@"default"]];
使用 reloadSections 时一定要确保其他 section 的数据不会改变,如果性能影响不大的话,建议使用 reloadData
当用到多层 for 循环时一定要慎重考虑,想想有没有更好的方法。因为往往他就是性能杀手
// 好好想想有没有更好的方法
for (int a = 0; a < MAX; a++) {
for (int b = 0; b < MAX; b++) {
for (int c = 0; c < MAX; c++) {
// do something
}
}
}
常量定义建议使用 static const 代替 #define。 define 只是纯粹的文本替换,没有类型安全检测, 而且宏重定义编译器并不会报错。虽然编译器有类型检测警告,但是你忽略了呢?那必定是强势背锅。
//1. define
// 类型错误 警告 ⚠️
#define MY_STRING @666
// 重定义 警告 ⚠️
#import "People.h"
#define MY_STRING @"myString"
@interface People : NSObject
@end
// 有一天你的同事隔壁老王也定义一个全局同名的 MY_STRING 宏,此时你的宏就有可能被覆盖,杯具就开始了
#define MY_STRING @"geBiLaoWang"
//2. static const
// 类型错误 --> Error!!!
static NSString *const kMyString = @1;
// 变量重名 --> Error!!!
static NSString *const kMyString = @"myString";
extern static NSString *const kMyString;
#import "People.h"
static NSString *const kMyString = @"peopleString";
使用 CGRectGetXxx 代替 view.frame.xx.xx,极力推荐给 UIView 扩展 MaxX 等方法
// 不推荐
self.frame.size.width;
self.frame.origin.x;
self.frame.origin.y;
// 推荐
CGRectGetWidth(self.frame);
CGRectGetMinX(self.frame);
CGRectGetMinY(self.frame);
// 极力推荐
@implementation UIView (Layout)
- (CGFloat)maxX
{
return CGRectGetMaxX(self.frame);
}
@end
建议文本计算使用 sizeWithAttributes 代替 sizeToFit。原因是 sizeToFit 开销大,会影响滑动性能。 sizeToFit 不但把文本宽高计算出来,而且还帮你把 frame 都一起设置了。由于你还是需要手动设置 frame 的,所以相当于多设置一次 frame , 而且 sizeToFit 背后做的事情远不止这些,具体使用 instruments 查看其调用栈。
// 不推荐
- (void) layoutSubviews
{
[super layoutSubviews];
[self.titleLabel sizeToFit];
self.titleLabel.frame = CGRectMake(6, 6, CGRectGetWidth(self.titleLabel.frame), 6);
}
// 推荐
- (void) layoutSubviews
{
[super layoutSubviews];
CGFloat title_w = ceil([_titleLabel.text sizeWithAttributes:@{NSFontAttributeName : _titleLabel.font}].width);
self.titleLabel.frame = CGRectMake(6, 6, title_w, 6);
}
在一些对性能要求较高的业务中,使用frame 布局时不应该用 .x .y .top .bottom 等便捷方法,这样会导致多次设置视图 frame,导致多余开销,影响交互性能。正确的姿势应该是一次性直接 setFrame:
@interface UIView (Frame)
@property (nonatomic, assign) CGFloat x;
@property (nonatomic, assign) CGFloat y;
@property (nonatomic, assign) CGFloat widtf;
@property (nonatomic, assign) CGFloat height;
@end
UIView *aView = [[UIView alloc] init];
// 不推荐
aView.x = 2;
aView.y = 6;
aView.width = 66;
aView.height = 66;
// 推荐
aView.frame = CGRectMake(2, 6, 66, 66);
当 UIView 发生 Misaligned (像素未对齐)时怎么办?快用 CGRectIntegral( )
, 如果视图的 frame 已是整数值了,确定不会有 Misaligned,那问题很大可能出在其 superView 上,此时只需要对其父视图做一次 CGRectIntegral 即可。Misaligned 经常会出现在 UILabel 和 UIImageView 上。
Misaligned
1. 影响:
视图或图片的点数(point),不能换算成整数的像素值(pixel),导致显示视图的时候需要对没对齐的边缘进行额外混合计算,影响性能
2. 表现
洋红色: UIView 的 frame 像素不对齐,即不能换算成整数像素值
黄色: UIImageView 的图片像素大小与其 frame.size 不对齐,图片发生了缩放造成
// 示例
CGFloat title_w = [_titleLabel.text sizeWithAttributes:@{NSFontAttributeName : _titleLabel.font}].width;
self.titleLabel.frame = CGRectIntegral(CGRectMake(6, 6, title_w, 6));
Tip
至于 image 的话,如果是本地图片,UIImageView 尺寸大小设置为 image 尺寸即可。 如果是从服务器下载的图片,那将下载到的图片缩放到与 UIImageView 对应的尺寸,再显示出来, 但是要注意图片缩放也是一笔额外开销,所以对于频繁变动的 image 不建议这样做。因为图片缩放可能影响更大
基于之前的列表优滑动化经验发现 Misaligned 对滑动性能影响较小,可有可无,所以就让他去吧。 如果是几乎不会变动的才考虑这样做,并且把处理后的图片缓存起来
当一个文件引入的头文件 很多 时,建议按照头文件类型进行分类,如果你是个追求极致的人,甚至还可以严格按照头文件长度再次排序。如下
// controller
#import "Controller1.h"
#import "Controller2.h"
#import "Controller3.h"
// model
#import "Model1.h"
#import "Model2.h"
// view
#import "View1.h"
#import "View2.h"
// other
#import "Other1.h"
使用 @class 和 @protocol 代替 #import ,有利于加快编译速度
// 不推荐
#import "Dog.h"
// 推荐
@class Dog;
@interface People : NSObject
@property (nonatomic, strong) Dog *dog;
@end
如果公有属性是只读的,建议加上修饰符 readonly
@interface People : NSObject
// 不推荐
@property (nonatomic, strong) NSString *name;
// 推荐
@property (nonatomic, strong, readonly) NSString *name;
@end
如果一个对象的多个属性之间有依赖关系,此时最好不要依赖外部属性更新顺序,因为万一其他人使用到该对象就很容易出问题。而是封装成一个方法给调用者,直接在内部处理其依赖关系。
@interface MyView : UIView
// 不推荐
@property (nonatomic, strong) NSString *title;
@property (nonatomic, assign) int type;
@property (nonatomic, assign) CGFloat topMargin;
MyView *my = [MyView new];
my.title = @"";
my.type = 0;
my.topMargin = 0;
// 例如要求必须先设置 type 才能确定 topMargin。如果是外部结局的话,那就没次使用时必须注意 type 要在 topMargin 前赋值
// 下面顺序会导致视图表现异常
my.topMargin = 0;
my.type = 0;
// 推荐
// 将外部的 3 个属性隐藏在内部,只能通过此方法进行更新
// 如果一定要通过属性赋值更新的话,那就只能重写 setter 方法来处理属性之间的依赖关系
// 具体要使用哪种方式可以根据具体情况具体分析
- (void)setTitle:(NSString *)title
type:(int)type
topMargin:(CGFloat) topMargin
{
self.title = @"";
self.type = 0;
self.topMargin = 0;
}
@end
如何合并多个服务器请求?这里可以合理 位掩码(BitMask),好处就是代码简洁易懂,计算速度快等
typedef NS_OPTIONS(NSUInteger, InfoQueryOptions) {
InfoQueryNone = 0,
InfoQueryMine = 1 << 0, // mine
InfoQueryFans = 1 << 1 // fans
};
@property (nonatomic, assign) InfoQueryOptions queryOptions;
__weak typeof(self) weakSelf = self;
[self queryInfoMineWithCompletion:^(id *rsp, BOOL success) {
weakSelf.queryOptions |= InfoQueryMine;
[weakSelf reloadDataIfNeed];
}];
[self queryInfoFansWithCompletion:^(id *rsp, BOOL success) {
weakSelf.queryOptions |= InfoQueryFans;
[weakSelf reloadDataIfNeed];
}];
- (void)reloadDataIfNeed
{
if ([weakSelf isInfoQueryFinished]) {
//一顿操作
}
}
- (BOOL)isInfoQueryFinished
{
return (self.queryOptions & InfoQueryMine == InfoQueryMine) &&
(self.queryOptions & InfoQueryFans == InfoQueryFans)
}
如何处理 Massive viewControlle(MVC 痛点),暂且定义大于 1500 行代码就算臃肿。这边推荐 Extension 和 Category 进行功能模块划分。该方法同样适用于其他类
@interface MyViewController : UIViewController
@end
1. CollectionView 模块 (DataSource / Delegate)
@interface MyViewController ()
// 引入主类必要的一些属性或方法
- (void)reloadDataIfNeed;
@end
@interface MyViewController (CollectionView) <UICollectionViewDataSource, UICollectionViewDelegateFlowLayout>
- (void)setupCollectionView;
@end
2. 数据请求模块
@interface MyViewController ()
// 引入主类必要的一些属性或方法
@property (nonatomic, assign) BOOL isQuerying;
@property (nonatomic, strong) NSArray<InfoModel *> *sectionsData;
@end
@interface MyViewController (DataQuery)
- (void)queryMineList;
- (void)queryfansList;
@end
3. 数据上报模块
@interface MyViewController ()
// 引入主类必要的一些属性或方法
@end
@interface MyViewController (EventReport)
@end