下划线和self.的区别
1.通过self. 访问,包含了set和get方法。
通过下划线是获取自己的实例变量,不包含set和get的方法。
2.self.是对属性的访问,而下划线是对局部变量的访问。在使用self.时是调用一个getter方法。会使引用计数加一,而下划线不会使用引用计数器加一的。
所有被声明为属性的成员,在iOS5之前需要使用编译指令@synthesize 来告诉编译器帮助生成属性的getter和setter方法,之后这个指令可以不用人为的指定了,默认情况下编译器会帮助我们生成。编译器在生成getter,setter方法时首先查找当前的类中用户是否定义了属性的getter,setter方法,如果有,则编译器会跳过,不会再生成,使用用户定义的方法。
总结:
使用self.是更好的选择,因为这样可以兼容懒加载,同时也避免了使用下滑线的时候忽略了self这个指针,后者容易在Block中造成循环引用。也因为,使用 _是获取不到父类的属性的,它只是对局部变量的访问。self方法实际上是用了get和set方法间接调用,下划线方法是直接对变量操作。
参考文献:
OC中下划线和self.的区别
const和#define的区别
define
#define
语法称之为预处理命令。#define
声明主要用于将常量(或字符串)赋予有意义的名字,比如当你在编写一个日历程序时,可以定义:
#define MONTHS_PER_YEAR 12
Tips:
1.通常情况下,习惯将预处理的常量名全大写,单词之间用下划线隔开
(与正常变量区分)。
2.如果是定义常量,若这个常量的适用范围局限于.m,那么习惯性在常量名前
加k,若常量在类之外可见,则通常以类名为前缀。
预处理代码起的作用实际上相当于在编译之前!!!
预处理代码起的作用实际上相当于在编译之前!!!
预处理代码起的作用实际上相当于在编译之前!!!
重要事情说三遍。也就是说宏使用过多会增加编译时间
而一个常量在栈中开辟空间是很高效的。
使用宏有以下好处:
1.增强代码可读性
2.方便全局使用和修改一些方法和参数
3.增强复用性
const常量
const 如果是声明常量,仅仅是想要在.m文件中使用,那么一定要同时使用static 和const来声明,若不加系统在编译时会自动为它加一个extern (外部符号),此时,若另一个编译单元出现了同名变量就会报错,并且很难查找错误。
这是OC程序员最不愿意看到的错误。。。也是很难查找的错误,跟修改已经创建的类的文件名的错误差不多。
总结:
- 多用类型常量,少用#define预处理指令(出自《Effective Objective-C》第四条原则)
- 声明const的字符串时,开头用k标识。推荐k+模板名字首字母大写+作用名称 防止和其他的重复
- 苹果的API中,大多数字符串,也是用以下这种方式(如以Key,style,Type结尾的一些参数)
// Constants.h
FOUNDATION_EXPORT NSString *const MyFirstConstant;
FOUNDATION_EXPORT NSString *const MySecondConstant;
// Constants.m
NSString *const MyFirstConstant = @"FirstConstant";
NSString *const MySecondConstant = @"SecondConstant";
或者
extern NSString * const PPRememberMusicList;
extern NSString * const PPLoadMusicAtListLoad;
extern NSString * const PPAfterPlayingMusic;
extern NSString * const PPGotoStartupAfterPlaying;
NSString * const PPRememberMusicList = @"Remember Music List";
NSString * const PPLoadMusicAtListLoad = @"Load music when loading list";
NSString * const PPAfterPlayingMusic = @"After playing music";
NSString * const PPGotoStartupAfterPlaying = @"Go to startup pos. after playing";
参考文献:
Define与Const的使用
#define详解
预编译
程序会在预编译之前还会有一些操作, 比如:删除反斜线+换行符的组合, 将各种形式的注释用空格替代等等。
接着是预编译阶段,预编译在处理#define的时候,会从#开始一直执行到遇到的第一个换行符(写代码的时候换行的作用)为止。
所以可知#define只会允许定义一行的宏,但是因为预编译之前会删除反斜线+换行符的组合,所以我们可以利用反斜线+换行符来定义多行宏,在将删除反斜线和换行符的组合后,在预编译阶段的逻辑上#define定义就成了一行的宏了。
#define
在预处理阶段只进行文本的替换(相当于把代码拷贝粘贴),不会进行具体的计算。
#define
作用在预编译时期,其真正的效果就是代码替换,而且是直接替换(内联函数!!!),这个和函数有着很大的区别。
宏(#define)的基本语法:
#define 宏名 主体;
↓ ↓ ↓
#define PI 3.1415926
宏(#define)的使用:
类对象宏:
类对象宏一般用来定义数据常量或字符串常量。
#define PI 3.1415926
#define NAME @"SunSatan"
类函数宏:
类函数宏就是宏名类似函数名,宏的主体为函数,并可以帮助主体函数接受参数,使用起来就像是函数一样。
#define Log(x) NSLog(@"this is test: x = %@", x)
类函数宏和函数的区别是, 类函数宏的参数不指定类型, 具体的参数类型在调用宏的时候由传入的参数决定,这就可能会遇到类型错误的问题。
举个栗子:
#define power(x) x*x
int x = 2;
int pow_1 = power(x);
int pow_2 = power(x+1);
猜猜pow_1和pow_2的值分别为多少?
结果是pow_1=4 ,pow_2=5。
这个结果是不是和预想的不一样,pow_2不应该等于9吗?因为宏真正的效果就是代码替换,所以在预编译后原来的代码变为了:
#define power(x) x*x
int x = 2;
int pow_1 = x*x; //2*2=4
int pow_2 = x+1*x+1;//2+1*2+1=5
这样看是不是就能看出问题,在代码替换后优先级不对了。
所以想要得到正确的结果的宏定义如下:
#define power(x) (x)*(x)
这个栗子就是告诉大家定义类函数宏的时候就真的要小心,不然得到结果可能会出乎我们的意料。
宏(#define)中操作符的使用:
#:字符串化操作符,需要放置在参数前,将类函数宏中传入的参数用""括起来变成了c语言的字符串。
#define Log(x) #x
代码替换后:
Log(x) -> "x"
所以以下两种使用方法都没问题,打印信息都是 x
NSLog(@"%s", Log(x)); -> NSLog(@"%s", "x");
NSLog(@"%@",@Log(x)); -> NSLog(@"%@",@"x");
##:符号连接操作符,需要放置在参数前,参数就会和##前面的符号连接起来。
#define single(name) +(instancetype)share##name;
代码替换后:
single(SunSatan) -> +(instancetype)shareSunSatan;
宏(#define)的好处:
使用宏的好处是不言自明的,可以少写很多重复的代码,修改一处就可以完成全局修改,在节省工作量的同时,大大增加代码可读性。
如果想成为一个能写出漂亮优雅代码的开发者,宏绝对是必不可少的技能。
使用宏(#define)的建议:
多多使用类函数宏对开发者而言是非常灵活、方便的,可以抽离很多重复使用的冗长的代码段,增加代码可读性,节省工作量。
使用但不是滥用,应有节制。
使用大量的宏,每次修改宏都需要重新替换,导致编译时间久。
不建议使用类对象宏,因为类对象宏定义的是常量,既然都要定义为常量,那么我们应该使用const来修饰。
相对于定义常量而言,const要比宏效率高很多,而且宏不做检查,只是代码替换,而const会进行编译检查,const要比宏更安全。所以应尽可能的使用const来代替类对象宏。
参考文献:
https://blog.csdn.net/qq_36557133/article/details/86476686
NSAssert(断言)
NSAssert
断言(NSAssert)是一个宏,在开发过程中使用NSAssert可以及时发现程序中的问题。
NSAssert声明如下:
#define NSAssert(condition, desc, ...)
- condition:条件表达式。条件成立时,运行后面程序;不成立时,抛出带有desc描述的异常信息。
- desc:异常描述,通常为NSString类型对象。用于描述条件表达式不成立的错误信息和参数的占位符。
- ...:desc字符串中的参数。
假设我们需要判断变量值是否大于5,我们可以用如下代码进行判断。
int i = 6;
NSAssert(i>5, @"i must bigger than 5");
运行后,控制台没有任何输出。
把变量i的值改为2,如下所示:
int i = 2;
NSAssert(i>5, @"i must bigger than 5");
运行demo,demo会崩溃。在控制台输出如下信息:
*** Assertion failure in -[ViewController viewDidLoad], /Users/ad/Library/Mobile Documents/com~apple~CloudDocs/file2016/NSAssert0709/NSAssert0709/ViewController.m:23
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'i must bigger than 5'
通过控制台输出的信息,可以得到崩溃发生于:ViewController类的viewDidLoad方法中,该文件位于/Users/ad/Library/Mobile Documents/comappleCloudDocs/file2016/NSAssert0709/NSAssert0709/ViewController.m,导致崩溃的代码位于第23行,崩溃原因为:i must bigger than 5。
使用NSAssert时可以对输出信息进行传值。
int i = 2;
NSAssert1(i>5, @"The real value is %i", i);
输出为:
*** Assertion failure in -[ViewController viewDidLoad], /Users/ad/Library/Mobile Documents/com~apple~CloudDocs/file2016/NSAssert0709/NSAssert0709/ViewController.m:23
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'The real value is 2'
NSAssert用于Objective-C,NSCAssert用于C语言中。
NSAssert1的desc带有一个参数,NSAssert2的desc带有两个参数,……,NSAssert5带有五个参数。断言可以带有零至五个参数。
也许你会好奇为什么desc中不使用[NSString stringWithFormat:...]格式,而要有五个NSAssert?因为NSAssert()的实现就是一个宏,因此,要处理异常信息中不同数量参数,就要有多个宏,所以就有了NSAssert(condition, dest)、NSAssert1(condition, formatDest, arg1)、NSAssert2(condition, formatDest, arg1, arg2)...NSAssert5(...)。
NSAssert和NSLog都可以在控制台输出,但NSAssert输出后程序立即crash,控制台也会输出程序遇到错误的位置等信息,而NSLog只用于输出信息。
NSParameterAssert
如果需要判断传入参数是否符合要求,可以使用NSParameterAssert。
- (NSString *)processItem:(NSUInteger)index {
NSParameterAssert(index
如果传入参数index大于myArray数组内元素数量,则程序会崩溃。控制它输出如下:
*** Assertion failure in -[ViewController processItem:], /Users/ad/Library/Mobile Documents/com~apple~CloudDocs/file2016/NSAssert0709/NSAssert0709/ViewController.m:31
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid parameter not satisfying: index
崩溃信息中会告诉你哪一行代码出错了,崩溃原因是什么。
NSParameterAssert用于Objective-C,NSCParameterAssert用于C语言中。
断言默认只存在于debug版
从Xcode 4.2开始,release版默认关闭了断言。也就是当编译发布版时,任何调用NSAssert的地方都被空行替换。所以,不要在NSAssert内执行任何有效操作。
如果想要在发布版中使用NSAssert,可以在Build Setting中的Enable Foundation Assertions中修改。
总结
对来源于系统内部的可靠数据使用断言,即用断言来处理绝不应该发生的情况。不要对外部不可靠数据(如用户输入、文件、网络读取等)使用断言,即对于外部不可靠数据或预期会发生的情况应当使用错误处理。同时要避免把需要执行的代码放到断言中,断言可以看成可执行的注释。
来源于系统外部的数据都是不可信的,需要严格检查(通常是错误处理)才能放行到系统内部,这相当于一个守卫。而对于系统内部的交互(如调用其他方法),如果每次也都要去处理输入的数据,也就相当于系统没有可信的边界,会让代码变的臃肿。事实上,在系统内部传递给方法正确的参数是调用者的责任,调用者应该确保传递给所调用方法的数据是符合要求的。这样就隔离了不可靠的外部环境和可靠的内部环境,降低复杂度。
但在开发阶段,代码极可能存在缺陷,有可能是处理外部数据的逻辑不周全或调用内部方法的代码存在错误,最终造成调用失败。这个时候,断言就可以发挥作用,用来确诊到底哪一部分问题导致程序出错。在清理了所有缺陷后,内外有别的信用体系就建立起来了。等到发行版的时候,这些断言就没有存在的必要了。
单元测试可能是一个更好的方法,但有些情况下(如复杂的算法过程中),我们希望在代码中执行检查,这时断言将更有效。
参考文献:
https://www.jianshu.com/p/3f16e7aaf7ef
switch case
OC中的switch case语句,在每个case后都要加break。不然会将该case下面的case也全部执行,直到遇到break。
Swift的case,只要有内容,就可以省略break。
在 OC 中的 switch
- 格式: switch(需要匹配的值) case 匹配的值: 需要执行的语句 break;
- 可以穿透
- 可以不写default
- default位置可以随便放
- 在case中定义变量需要加大括号, 否则作用域混乱(仔细了解了一下在C或者C++中,只要是在任何一对花括号 “{ }”中定义的对象,那么该对象的作用域就局限在这对花括号里面,上面的代码的错误就出现在这儿了。)
- 不能判断对象类型, 只能判断基本类型中的整数(不能判断NSString)
在 Swift 中的 Switch
- OC必须是整数, 在 Swift 中可以是对象的类型也可以是 double类型
- 不可以穿透
- 可以不写break
- 不能不写default
- default位置只能在最后
- 在case中定义变量不用加大括号
@interface
.h中的@interface
供其他的class调用。是public
.m中的@interface
也可以叫做class extension。是.h中@interface的补充。但是对其他class是不开放的。是private
@Interface的写法
@interface 类名: 父类 <协议1, 协议2>
@interface和类名中间一个空格
类名后紧跟:之后空格加上父类协议之间用,空格分割
写法模板
方法的参数在一排显示
方法之间保留一行
第一个方法和@interface保留空行
最后一个方法和@end保留空行
总结:一般把public的方法和变量放到.h中。将private的变量放到.m中。而private的方法在.m中可以不声明,直接使用。
Copy
当我们使用自定义对象的 copy
方法时会报错,例如我们自定了一个 Person
类并实例化一个对象,并调用其 copy
方法:
Person *p = [[Person alloc] init];
p.name = @"小明";
Person *p1 = [p copy];
当执行到 [p copy] 时,将会报错,错误如下:
-[Person copyWithZone:]: unrecognized selector sent to instance 0x100612d20
原因是没有实现 copyWithZone
方法,是因为在 copy
方法中调用了 copyWithZone
方法,所以必须实现 copyWithZone
方法才能够使用 copy
方法。
要实现 copyWithZone
方法,我们首先需要在 Person.h
文件中让 Person 类遵循 NSCopying
协议
@interface Person : NSObject
然后在 Person.m 文件中实现 copyWithZone 方法:
- (id)copyWithZone:(NSZone *)zone{
// 如果不实现下面的代码将会变成浅拷贝
Person *copy = [[[self class]allocWithZone:zone]init];
copy.name = self.name;
return copy;
}
实现后运行以下代码:
Person *p = [[Person alloc] init];
p.name = @"小明";
Person *p1 = [p copy];
NSLog(@"%p,%p,%@,%@",p,p1,p.name,p1.name);
打印的结果是
0x1006562b0,0x10065a490,小明,小明
可以看出通过以上代码调用 copy 方法拷贝是深拷贝。
参考文献:
https://juejin.im/post/6844903648380583950
dealloc
ARC下,可以不用显式调用父类的[super dealloc],结果仍然会调用,因为在编译期间编译器会自动添加 [super dealloc]。
如果子类不实现dealloc方法,会自动调用父类的dealloc。如果子类实现了dealloc方法,会先调用子类的dealloc,在调用父类的dealloc。
参考文献:
https://blog.csdn.net/s3590024/article/details/53943074
#import
#import
是OC中对#include的改进版本,#import
不需要写条件编译语句就可以确保引用的文件只会被包含一次,不会陷入递归包含的问题。
#import的顺序
#import <系统库>
#import <第三方库>
#import “其他类”
// 例:
#import
#import
#import "GBOrderEmptyView.h"
尽量按照先系统类 第三方类 自己写的类顺序导入 中间不能有空格
@Class的写法
// 建议写法
@class class1, class2;
// 不建议写法
@class UIPress;
@class UIPressesEvent;
#import<>与#import""
<>是指从系统库中引用头文件,也就是从系统库目录下查找,如果找不到,则结束查找。
""是先从用户目录下查找文件,如果找不到,则继续在系统库目录下查找文件。
一般而言,<>引入的是系统库的文件,""引入的是本地工程的文件。
#import与@class
#import
会包含这个类的所有信息,包含各种变量和方法;而@class
则会告诉编译器,其后面的名称是一个类的名称,现在无需知道该类的定义,后面会告诉使用者的。
在类的声明文件(.h文件)中,一般只需要知道被引用的类的名称就可以了,不需要知道其具体实现,所以在.h文件中一般使用@class
来声明这个名称是类的名称;而在类的实现文件里面,因为会用到这个引用类的内部的实体变量和方法,所以需要使用#import
来包含这个所引用类的头文件。
#import
对比#include
的一大优势就是不会重复引入相同的类。所以,不要在当前类的头文件中使用#import
引入其他的类,因为如果引入类的头文件中也import了其他的杂七杂八的类,那么当前类就会引入许多根本用不到的类,这势必会增加编译时间。
所以,在头文件中是用#import
导入引入类,会导致如下两个问题:
1,可能会引入许多根本用不到的内容,增加编译时间;
2,容易引起循环导入,进而导致编译错误。
因此,我们在类的头文件中少使用import引入其他的头文件,而是使用@class
来声明一个类。
在@interface中的大括号中声明和@property区别
一共有三种方式
方式一:直接在@interface中的大括号中声明。
@interface MyTest : NSObject{
NSString *mystr;
}
方式二:在@interface中声明,然后再在@property中声明。
@interface MyTest :NSObject{
NSString *_mystr;
}
@property (strong, nonatomic)NSString *mystr;
随后在.m文件中加入
@synthesize mystr =_myStr;
方式三:直接用@property声明
@interface MyTest :NSObject{
}
@property (strong, nonatomic)NSString *mystr;
随后在.m文件中加入(也可以不加,xcode会自动生成)
@synthesize mystr= _myStr;
首先来说一下方式一根方式三的区别,使用方式一声明的成员变量是只能在自己类内部使用的,而不能在类的外部使用,(就是通过类名. 点的方式是显示不出来的),方式三则相反,它可以在类的外部访问,在类的内部可以通过下划线+变量名或者self.变量名的方式来访问。
方式二的写法是一种过时的声明变量的方式,xcode在早期@systhesize没有自动合成属性器之前,需要手写
getter与setter方法,下划线从风格上表明这是类的内部变量,要是需要直接使用变量则需要使用get或者set的方式。
在XCode目前有了自动合成属性器后,编译器会自动帮我们生成一个以下划线开头的的实例变量,所以我们不必去同时声明属性与变量。我们可以直接用@property的方式来声明一个成员属性,在.m文件中使不使用@systhesize都无所谓,xcode会自动帮你生成getter与setter.
目前推荐方式三,这是是苹果开发模板所推荐的,也可以在.m文件中不加@systhesize。
参考文献:
https://blog.csdn.net/baidu_31071595/article/details/50830325
数组
二维数组
首先,OC中是没有二维数组的。二维数组是通过一位数组的嵌套实现的。
通过字面量创建和使用二维数组(推荐)
// 1.字面量创建二维数组并访问(推荐)
NSArray *array2d = @[
@[@11,@12,@13],
@[@21,@22,@23],
@[@31,@32,@33]
];
// 字面量访问方式(推荐)
NSLog(@"array2d[2][2]:%@",array2d[2][2]);
// 数组对象函数访问
NSLog(@"array2d[2][2]:%@",[[array2d objectAtIndex:2] objectAtIndex:2]);
协议
在.h文件中声明协议,并且声明代理
协议需要继承于 基协议
@protocol Define
// 协议方法
// @required:表明某方法必须实现
@required
-(void)update:(int)value; //这个方法必须实现
// @optional:表明某个方法可选择实现
@optional
-(void) download; //这个方法可以选择实现,也可以不实现
@end
声明协议对象
@property (nonatomic, weak) id viewDelegate;
调用协议时,需要先判断协议是否实现。
if ([self.viewDelegate respondsToSelector:@selector(headerViewClicked:isOpen:)]) {
// 判断有实现之后,在调用协议
}
为什么声明协议对象时,要用id<协议名>
因为这个协议中定义了一些基本的方法,由于我们使用的所有类都继承NSObject
这个基类,而这个基类遵守了
这个协议,那么也就实现了其中的那些方法,这些方法当然可以由NSObject
及其子类对象调用,但是在不知道遵守者类型的时候需要用到id <协议名>
这样的指针,这个指针在编译期并不知道自己指向哪个对象,唯一能调用的便是协议中的方法,然而有时候又需要用一些基本的方法,比如要辨别id <协议名>
这个指针所指的对象属于哪个类,就要用到-isMemberOf:
这个方法,而这个方法是
这个协议中的方法之一,所以,我们自定义的协议都需要继承
。
中的方法在NSObject
基类中实现了,那么无需再关心实现了,直接调用
中的方法吧。
参考文献:
https://www.jianshu.com/p/614300d8bb1e
for循环
1.1 最常用的循环方式
NSArray *testArray = @[@1,@2,@3,@4];
for(int a = 0; a < testArray.count; a++){
NSLog(@"%@",testArray[a]);
}
这段代码最大的问题就是循环每进行一次我们都会调用数组的计数方法. 数组的总数是不会改变的,因此每次都去调用一下这种做法是多余的。常用的优化如下:
NSArray *testArray = @[@1,@2,@3,@4];
NSUInteger count = testArray.count;
for(int a = 0; a < count; a++){
NSLog(@"%@",testArray[a]);
}
1.2 快速枚举
快速枚举是在 Objective-C 2.0 中作为传统的NSEnumerator的更便利的替代方法而引入的,新的方法顺便带来了一种新的循环语法, for…in 循环。
NSArray *testArray = @[@1,@2,@3,@4];
for(id num in testArray){
NSLog(@"%@",num);
}
要注意的是使用for in快速枚举NSMutableArray这类可变对象时要注意不能对容器进行修改,否则会导致遍历器抛出异常导致程序崩溃。
对上述三个循环进行性能测试
我们这里用命令行运行这段代码以排除任何干扰最终结果的隐藏在幕后的保留或者排除处理。
clang -framework Foundation main.m -o main
static const NSUInteger arrayItems = 10000000;
NSMutableArray *array = [NSMutableArray arrayWithCapacity:arrayItems];
for (int i = 0; i < arrayItems; i++) [array addObject:@(i)];
array = [array copy];
CFTimeInterval start = CFAbsoluteTimeGetCurrent();
// 常用的for循环
for (NSUInteger i = 0; i < [array count]; i++){
id object = array[i];
}
CFTimeInterval forLoop = CFAbsoluteTimeGetCurrent();
NSLog(@"For loop: %g", forLoop - start);
//优化过的常用for循环
NSUInteger count = [array count];
for (NSUInteger i = 0; i < count; i++){
id object = array[i];
}
CFTimeInterval forLoopWithCountVar = CFAbsoluteTimeGetCurrent();
NSLog(@"Optimized for loop: %g", forLoopWithCountVar - forLoop);
CFTimeInterval enumeratorLoop = CFAbsoluteTimeGetCurrent();
// 快速枚举
for (id object in array){
}
CFTimeInterval forInLoop = CFAbsoluteTimeGetCurrent();
NSLog(@"For…in loop: %g", forInLoop - enumeratorLoop);
}
运行结果如下:
由此可以看出for in的性能最高,优化了的普通循环也要比正常的快很多。基于我们所见,如果所有其它的因素都一样的话,在循环遍历数组时你应该尝试去使用for...in循环。至于 NSSet 和 NSDictionary,也可以测试一下,结果是一样的。
参考文献:
https://www.jianshu.com/p/a1d6d52589fe
NSUInteger, NSInteger, int, NSNumber区别
- 当需要使用int类型的变量的时候,可以像写C的程序一样,用int,也可以用NSInteger,但更推荐使用NSInteger,因为这样就不用考虑设备是32位的还是64位的。(以后使用NSInteger,因为苹果现在必须让程序支持64位的,所以以后不要在使用int 来定义变量了)
- 苹果的官方文档中总是推荐用NSInteger。NSInteger是一个封装,它会识别当前操作系统的位数,自动返回最大的类型。当你不知道你的操作系统是什么类型的时候,你通常会想要使用NSInteger,所以或许你想要你的int类型范围尽可能的大,用NSInteger,32位系统NSInteger是一个int,即32位,但当时64位系统时,NSInteger便是64位的。——所以就是一般推荐用NSInteger的。
- NSUInteger是无符号的,即没有负数,NSInteger是有符号的。
- 有人说既然都有了NSInteger等这些基础类型了为什么还要有NSNumber?它们的功能当然是不同的。
NSInteger是基础类型,但是NSNumber是一个类。如果想要存储一个数值,直接用NSInteger是不行的,比如在一个Array里面这样用:
NSArray *array = [[NSArray alloc]init];
[array addObject:3];//会编译错误
这样是会引发编译错误的,因为NSArray里面放的需要是一个类,但‘3’不是。这个时候需要用到NSNumber:
NSArray *array = [[NSArray alloc]init];
[array addObject:[NSNumber numberWithInt:3]];
Cocoa提供了NSNumber类来包装(即以对象形式实现)基本数据类型。
例如以下创建方法:
(NSNumber *) numberWithChar: (char) value;
(NSNumber *) numberWithInt: (int) value;
(NSNumber *) numberWithFloat: (float) value;
(NSNumber *) numberWithBool: (BOOL) value;
将基本类型数据封装到NSNumber中后,就可以通过下面的实例方法重新获取它:
(char) charValue;
(int) intValue;
(float) floatValue;
(BOOL) boolValue;
(NSString *) stringValue;
参考文献:
https://www.jianshu.com/p/5a7f3d0c428b
https://www.jianshu.com/p/2abaedc8e8a4
字典
初始化:
1、初始化方法
//Value值xiaochen key值xc
NSDictionary *dic = [[NSDictionary alloc]initWithObjectsAndKeys:@"xiaochen",@"xc",@"xiaoyun",@"xy",@"xiaohong",@"xh",nil];
2、遍历构造器
//Value值xiaochen key值xc
NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:@"xiaochen",@"xc",@"xiaoyun",@"xy",@"xiaohong",@"xh",nil];
3、字面量
//字面量key值在前 value在后
NSDictionary *dicts = @{@"name":@"xiaochen",@"gender":@"man",@"age":@"19"};
获取字典长度
NSUInteger i = dicts.count;
根据Key值获取value值
//value 只要是对象就可以,但是key必须要遵从NSCoying协议,字符串默认是遵从的
NSString *str = [dicts objectForKey:@"name"];
NSLog(@"name = %@",str); // name = xiaochen
NSString *str1 = [dicts objectForKey:@"gender"];
NSLog(@"gender = %@",str1); //gender = man
NSString *str2 = [dicts objectForKey:@"age"];
NSLog(@"age = %@",str2); //age = 19
获取所有的Key值和所有的value值
NSArray *arr = [dicts allKeys]; //获取所有的Key值
NSLog(@"%@",arr);
/*(
name,
age,
gender
)*/
NSArray *ar =[dicts allValues];//获取所有的value值
NSLog(@"%@",ar);
/*(
xiaochen,
19,
man
)*/
可变字典
初始化
NSMutableDictionary *dic = [[NSMutableDictionary alloc]initWithCapacity:10];
遍历构造器
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:10];
字面量
NSMutableDictionary *dicts = [@{@"key1":@"value1",@"key2":@"value2"}mutableCopy];
添加元素
[dicts setObject:@"value3"forKey:@"key3"];
修改value值
[dicts setObject:@"value10"forKey:@"key1"];
删除
[dicts removeObjectForKey:@"key1"];
增与改:
[self.sourceData setValue:@[] forKey:key];
self.sourceData[key] = @[];
赋值其他字典
// 复制后不可变
dic0 = [dic1 copy];
// 复制后可变
dic0 = [dic1 mutableCopy];
集合
特点:无序的 有互异性
初始化方法
NSSet *set1 =[[NSSet alloc]initWithObjects:@"dic",@"bb",@"cc",@"aa",@"bb",@"cc",nil];
遍历构造器
NSSet *set2 =[NSSet setWithObjects:@"aa",@"bb",@"cc",@"dd",@"se",nil];
获取集合中的元素
NSString *str= [set2 anyObject];//随机获取元素
可变集合
初始化方法
NSMutableSet *set = [[NSMutableSet alloc] initWithCapacity:10];
添加元素
[set addObject:@"dic"];
删除集合元素
[set removeObject:@"ff"];
参考文献:
https://www.jianshu.com/p/53e19fa1da17
ViewController相关
1.1 preferredInterfaceOrientationForPresentation
// 默认的屏幕方向(当前ViewController必须是通过模态出来的UIViewController(模态带导航的无效)方式展现出来的,才会调用这个方法)
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
return UIInterfaceOrientationPortrait;
}
1.2 屏幕旋转
控制页面旋转的方式可以总结为两种,第一种是通过全局设置来控制,第二种是页面自己单独控制。
一:修改全局设置来实现
第一种是通过勾选方向让页面支持旋转。
第二种是通过修改info.plist文件
Supported interface orientations
设置选项,增加方向让页面支持旋转。
第三种是通过在
AppDelegate
中实现- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window
方法让页面支持旋转。
- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window {
//返回你需要的方向
return UIInterfaceOrientationMaskPortrait;
}
这三种方式中,前面两种是一样的,需要注意的是第三种方式,它的优先级是最高的,也就是你通过AppDelegate这种方式来控制全局旋转,同时勾选或者修改了plist选项,最终会以AppDelegate中支持的方向为准。
全局控制这种方式,通常用在所有页面都需要支持全屏的情况下,如果要让某个页面支持,大部分页面不支持,又该怎么处理呢?在这里利用runtime动态替换方法和分类的特性,来实现单独控制页面旋转,经过封装后,一句话就可以达到让页面支持或者不支持旋转。
AppDelegate分类代码中实现- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window
方法,利用分类的特性来完成AppDelegate需要实现的代码
- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window {
//返回你需要的方向
return UIInterfaceOrientationMaskPortrait;
}
UIViewController分类中利用runtime动态替换方法实现控制页面旋转,这里使用week是因为页面销毁的时候需要将其他控制器的方向还原,不被当前页面修改方向后影响。
- (void)isNeedRotation:(BOOL)needRotation{
AppDelegate * appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
__weak __typeof(self) weakSelf = self;
IMP originalIMP = method_getImplementation(class_getInstanceMethod([appDelegate class], @selector(application:supportedInterfaceOrientationsForWindow:)));
IMP newIMP = imp_implementationWithBlock(^(id obj, UIApplication *application, UIWindow *window){
if (!weakSelf) {
class_replaceMethod([appDelegate class], @selector(application:supportedInterfaceOrientationsForWindow:), originalIMP, method_getTypeEncoding(class_getInstanceMethod([appDelegate class], @selector(application:supportedInterfaceOrientationsForWindow:))));
}
return needRotation ? UIInterfaceOrientationMaskAll : UIInterfaceOrientationMaskPortrait;
});
class_replaceMethod([appDelegate class], @selector(application:supportedInterfaceOrientationsForWindow:), newIMP, method_getTypeEncoding(class_getInstanceMethod([appDelegate class], @selector(application:supportedInterfaceOrientationsForWindow:))));
}
二:通过每个页面单独控制页面旋转
通过每个页面单独控制页面旋转,首先必须打开全局方向设置,设置需要旋转的方向,然后根据页面不同的创建方式(push或者present)和不同根控制器(UITabBarController或者UINavigationController),可以分出三种情况。
第一种情况,页面通过UINavigationController+push创建,在这种情况下,需要在UINavigationController实现以下方法就可以使页面支持旋转。
// 是否支持自动转屏
- (BOOL)shouldAutorotate {
return [self.topViewController shouldAutorotate];
}
// 支持哪些屏幕方向
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
return [self.topViewController supportedInterfaceOrientations];
}
第二种情况,页面通过UITabBarController+push创建,在这种情况下,需要在UITabBarController,UINavigationController都实现以下方法才可以让页面支持旋转。其中需要注意的是UITabBarController中,需要利用runtime动态替换系统方法,防止没有初始值造成越界。
+ (void)load {
SEL selectors[] = {
@selector(selectedIndex)
};
for (NSUInteger index = 0; index < sizeof(selectors) / sizeof(SEL); ++index) {
SEL originalSelector = selectors[index];
SEL swizzledSelector = NSSelectorFromString([@"cl_" stringByAppendingString:NSStringFromSelector(originalSelector)]);
Method originalMethod = class_getInstanceMethod(self, originalSelector);
Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector);
if (class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))) {
class_replaceMethod(self, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
}
- (NSInteger)cl_selectedIndex {
NSInteger index = [self cl_selectedIndex];
if (index > self.viewControllers.count){
return 0;
}else{
return index;
}
}
- (BOOL)shouldAutorotate {
return [self.selectedViewController shouldAutorotate];
}
- (NSUInteger)supportedInterfaceOrientations {
return [self.selectedViewController supportedInterfaceOrientations];
}
第三种情况,页面通过present创建,需要在present出来的页面实现以下方法才可以让页面支持旋转。
// 是否支持自动转屏
- (BOOL)shouldAutorotate {
return NO;
}
// 支持哪些屏幕方向
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
return UIInterfaceOrientationMaskPortrait;
}
// 默认的屏幕方向(当前ViewController必须是通过模态出来的UIViewController(模态带导航的无效)方式展现出来的,才会调用这个方法)
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
return UIInterfaceOrientationPortrait;
}
页面自己单独控制旋转,需要每个页面都写重复的代码,很多时候都是利用基类来实现,但是需要子页面继承,对代码还是有一定的影响。既不想写基类,又不想每个页面单独写代码,又该怎么来实现呢?在这里还是利用分类的特性,分别创建UITabBarController,UINavigationController,UIViewController的分类来实现。
UITabBarController分类中的代码。
// 是否支持自动转屏
- (BOOL)shouldAutorotate {
UIViewController *vc = self.viewControllers[self.selectedIndex];
if ([vc isKindOfClass:[UINavigationController class]]) {
UINavigationController *nav = (UINavigationController *)vc;
return [nav.topViewController shouldAutorotate];
} else {
return [vc shouldAutorotate];
}
}
// 支持哪些屏幕方向
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
UIViewController *vc = self.viewControllers[self.selectedIndex];
if ([vc isKindOfClass:[UINavigationController class]]) {
UINavigationController *nav = (UINavigationController *)vc;
return [nav.topViewController supportedInterfaceOrientations];
} else {
return [vc supportedInterfaceOrientations];
}
}
// 默认的屏幕方向(当前ViewController必须是通过模态出来的UIViewController(模态带导航的无效)方式展现出来的,才会调用这个方法)
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
UIViewController *vc = self.viewControllers[self.selectedIndex];
if ([vc isKindOfClass:[UINavigationController class]]) {
UINavigationController *nav = (UINavigationController *)vc;
return [nav.topViewController preferredInterfaceOrientationForPresentation];
} else {
return [vc preferredInterfaceOrientationForPresentation];
}
}
UINavigationController分类中的代码。
// 是否支持自动转屏
- (BOOL)shouldAutorotate {
return [self.topViewController shouldAutorotate];
}
// 支持哪些屏幕方向
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
return [self.topViewController supportedInterfaceOrientations];
}
// 默认的屏幕方向(当前ViewController必须是通过模态出来的UIViewController(模态带导航的无效)方式展现出来的,才会调用这个方法)
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
return [self.topViewController preferredInterfaceOrientationForPresentation];
}
UIViewController分类中的代码。
/**
* 默认所有都不支持转屏,如需个别页面支持除竖屏外的其他方向,请在viewController重写下边这三个方法
*/
// 是否支持自动转屏
- (BOOL)shouldAutorotate {
return NO;
}
// 支持哪些屏幕方向
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
return UIInterfaceOrientationMaskPortrait;
}
// 默认的屏幕方向(当前ViewController必须是通过模态出来的UIViewController(模态带导航的无效)方式展现出来的,才会调用这个方法)
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
return UIInterfaceOrientationPortrait;
}
参考文献:
https://juejin.im/post/6844903528087945230
生命周期
viewWillLayoutSubviews和viewDidLayoutSubviews
loadView、viewDidLoad及viewDidUnload的关系
- loadView
调用时间:
每次访问UIViewController的view(比如controller.view、self.view)而且view为nil,loadView方法就会被调用。
作用:
loadView方法是用来负责创建UIViewController的view
默认实现:
1、它会先去查找与UIViewController相关联的xib文件,通过加载xib文件来创建UIViewController的view。
如果在初始化UIViewController指定了xib文件名,就会根据传入的xib文件名加载对应的xib文件
[[MJViewController alloc] initWithNibName:@"MJViewController" bundle:nil];
如果没有明显地传xib文件名,就会加载跟UIViewController同名的xib文件
[[MJViewController alloc] init]; // 加载MJViewController.xib
2、如果没有找到相关联的xib文件,就会创建一个空白的UIView,然后赋值给UIViewController的view属性,大致如下
self.view = [[[UIView alloc] initWithFrame:[UIScreen mainScreen].applicationFrame] autorelease];
// applicationFrame的值是:{{x = 0, y = 20}, {width = 320, height = 460}}
- viewDidLoad
调用时间:
无论你是通过xib文件还是重写loadView方法创建UIViewController的view,在view创建完毕后,最终都会调用viewDidLoad方法
作用:
一般我们会在这里做界面上的初始化操作,比如往view中添加一些子视图、从数据库或者网络加载模型数据装配到子视图中。 - viewDidUnload
调用时间:
当内存警告时,UIViewController就会收到didReceiveMemoryWarning消息。didReceiveMemoryWarning方法的默认实现是:如果当前UIViewController的view不在应用程序的视图层次结构(View Hierarchy)中,即view的superview为nil的时候,就会将view释放,并且调用viewDidUnload方法(此方法iOS6之后以及不在调用。苹果在文档中建议,应该将回收内存的相关操作移到另一个回调函数:didReceiveMemoryWarning
。但在实际使用中,你不需要做任何以前 viewDidUnload 的事情,更不需要把以前 viewDidUnload 的代码移动到 didReceiveMemoryWarning 方法中。)
作用:
一般在释放资源,主要是释放界面元素相关的资源,将相关的实例都赋值为nil
参考文献:
https://www.cnblogs.com/mjios/archive/2013/02/26/2933667.html
https://blog.devtang.com/2013/05/18/goodbye-viewdidunload/
block闭包
定义:
闭包是一个函数(或指向函数的指针),再加上该函数执行的外部的上下文变量(有时候也称作自由变量)。
无参无返回值的定义和使用
//无参无返回值 定义 和使用
void (^MyBlockOne)(void) = ^{
NSLog(@"无参无返回值");
};
// 调用
MyBlockOne();
无参有返回值的定义和使用
// 无参有返回值
int (^MyBlockTwo)(void) = ^{
NSLog(@"无参有返回值");
return 2;
};
// 调用
int res = MyBlockTwo();
有参无返回值的定义和使用
//有参无返回值 定义
void (^MyBlockThree)(int a) = ^(int a){
NSLog(@"有参无返回值 a = %d",a);
};
// 调用
MyBlockThree(10);
有参有返回值的定义和使用
//有参有返回值
int (^MyBlockFour)(int a) = ^(int a){
NSLog(@"有参有返回值 a = %d",a);
return a * 2;
};
MyBlockFour(4);
block 示例
// 声明
@property (nonatomic, copy) void(^testBlock)();
// 实现
__weak typeof(self) weakSelf = self;
self.testBlock = ^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// weakSelf为nil时,并不会崩溃。也不会调用testBlockAction
[weakSelf testBlockAction];
});
};
// 调用
self.testBlock();
- (void)testBlockAction {
DDLogDebug(@"testBlock Action", nil);
}
还有一种情况,在函数的参数传递闭包:
+ (void)testFunc:(UIViewController *)fromViewController completion:(void (^)(BOOL success, id _Nonnull responseObject, NSError * _Nonnull error))completion;
// 或者
void DispatchOnce(void (^block)());
闭包中,weakSelf为nil时,并不会崩溃。weakSelf的方法也不会调用。
如果需要weakSelf不能为nil,且在block执行完毕后,才置为nil。就需要对weakSelf做一次 __strong。
__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[NSThread sleepForTimeInterval:1];
NSLog(@"%@",strongSelf);
});
};
}
使用了__strong在dispatch_async 里边 block 代码执行完毕之前,对self有一个引用,防止对象(self)提前被释放。而作用域一过,strongSelf不存在了,对象(self)也会被释放。
除了weakSelf解决block强引用外,还可以使用__block
。使用 __block
关键字设置一个指针 vc 指向 self,重新形成一个 self → block → vc → self
的循环持有链。在调用结束后,将 vc 置为 nil,就能断开循环持有链,从而令 self 正常释放。
__block UIViewController *vc = self;
self.block = ^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@", vc.name);
vc = nil;
});
};
self.block();
首先,block 本身不允许修改外部变量的值。但被 __block 修饰的变量会被存在了一个栈的结构体当中,成为结构体指针。当这个对象被 block 持有,就将“外部变量”在栈中的内存地址放到堆中,进而可以在 block 内部修改外部变量的值。
还有一种方式可以避免强引用环形成。就是将 self 以传参的形式传入 block 内部,这样 self 就不会被 block 持用,也就不会形成循环持有链。
self.block = ^(UIViewController *vc){
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@", vc.name);
});
};
self.block(self);
关于weakify 和strongify
如果使用了RAC的话,建议使用:
@weakify(self);
[@[] enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
@strongify(self);
}];
否则就使用:
__weak typeof(self) weakSelf = self;
[@[] enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
__strong typeof(weakSelf) strongSelf = weakSelf;
}];
全局函数和全局变量
全局函数
// .h文件
/// 国际化
NSString * LDNewLocalizedString(NSString *key, ...);
/// 快捷生成UIimage
/// @param name 图片名
UIImage *__nullable LDNewUIImage(NSString *__nullable name);
/// 单例
void DispatchOnce(void (^block)());
// .m文件
UIImage *__nullable LDNewUIImage(NSString *__nullable name) {
return [LDNewTool getUIImage:name];
}
void DispatchOnce(void (^block)()) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
block();
});
}
已弃用属性的写法
@property (nonatomic, strong) UILabel * label_bottomWord __deprecated_msg("");
NSLog打印各种数据的方式
与Swift不同,OC打印需要指明数据的类型,所以便有此统计
// 整型占位符说明
%d : 十进制整数, 正数无符号, 负数有 “-” 符号;
%o : 八进制无符号整数, 没有 0 前缀;
%x : 十六进制无符号整数, 没有 0x 前缀;
%u : 十进制无符号整数;
%hd : 短整型
%ld , %lld : 长整型
%zd : 有符号 NSInteger型专用输出,在iOS开发中应牢记。
%tu : 无符号NSUInteger的输出
%lu : sizeof(i)内存中所占字节数
// 字符占位符说明
%c : 单个字符输出;
%s : 输出字符串;
// 浮点占位符说明
%f : 以小数形式输出浮点数, 默认 6 位小数; 如:CGFloat
%e : 以指数形式输出浮点数, 默认 6 位小数;
%g : 自动选择 %e 或者 %f 各式;
// 其它形式占位符
%p : 输出十六进制形式的指针地址;
%@ : 输出 Object-C 对象; 如:NSString *
// 占位符附加字符
– l : 在整型 和 浮点型占位符之前, %d %o %x %u %f %e %g 代表长整型 和 长字符串;
– n(任意整数) : %8d 代表输出8位数字, 输出总位数;
– .n : 浮点数 限制小数位数, %5.2f 表示 5位数字 2位小数, 字符串 截取字符个数;
– - : 字符左对齐;
参考文献:
https://www.cnblogs.com/mukekeheart/p/11280604.html
书写规范
nil的判断
// 正确示例
if (objc) {
}
// 错误示例
if (nil == objc) {
}
编码规范
- 所有的方法之间空一行。
- 所有的代码块之间空一行,删除多余的注释。
- 所有自定义的方法需要给出注释。
- 尽量使用懒加载,在控制器分类时有提及和要求,其它自定义类按照控制器格式分类,没有的分类不写即可。
- 代码后的’{‘不需要独占一行,包括方法之后,if,switch等。
- 必须要统一的要求,属性的定义请按照下图property之后,空一格,括号之后空一格,写上类名,空一格之后跟上*和属性名。
@property (nonatomic, strong) UITableView *tableView;///< 注释
@property (nonatomic, strong) DeliveryModel *delivery;///< 注释
@property (nonatomic, strong) DeliveryLookAdapter *lookAdapter;///< 注释
@property (nonatomic, strong) DeliveryLookAPIManager *lookManager;///< 注释
- 遵循一般代码规范,多模仿苹果API。
- 删除不用的代码。
- 如果有方法一直不会用到,请删除(除工具类)。
- 没有执行任何业务逻辑的方法,请删除或给予注释,删除多余的资。源或文件,添加必要的注释。
- 比较大的代码块需要给出注释。
- 方法尽量控制最多五十行。如果超过就精简代码 就分开方法写,方便之后进行热修复 代码重构。
- 不允许外接修改的属性要设置readonly
- 头文件引入的其他类 要使用@class,不使用#import引入
结构
- life cycle
- 自定义方法
- event response
- Delegate方法实现
- getter和setter全部放到最后
在函数分组和protocol/delegate实现中使用#pragma mark -来分类方法,要遵循以下一般结构:
#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/Event Response
- (IBAction)submitData:(id)sender {}
- (void)someButtonDidPressed:(UIButton*)button
#pragma mark - Protocol conformance
#pragma mark - UITextFieldDelegate
#pragma mark - UITableViewDataSource
#pragma mark - UITableViewDelegate
#pragma mark - Public
- (void)publicMethod {}
#pragma mark - Private
- (void)privateMethod {}
#pragma mark - NSCopying
- (id)copyWithZone:(NSZone *)zone {}
#pragma mark - NSObject
- (NSString *)description {}
空格
- 缩进使用4个空格,确保在Xcode偏好设置来设置。(raywenderlich.com使用2个空格)
- 方法大括号和其他大括号(
if
/else
/switch
/while
等.)总是在同一行语句打开但在新行中关闭。
// 正确写法
if (user.isHappy) {
//Do something
} else {
//Do something else
}
// 错误写法
if (user.isHappy)
{
//Do something
}
else {
//Do something else
}
下划线
在初始化方法里,应该使用_variableName来避免getter/setter潜在的副作用。
其他位置使用实例变量时,都应该使用self.来访问和改变。
局部变量 不应该包含下划线。
属性特性
所有属性特性应该显式地列出来,有助于阅读代码。
NSString应该使用copy
而不是 strong
的属性特性。
为什么?即使你声明一个NSString的属性,有人可能传入一个NSMutableString的实例,然后在你没有注意的情况下修改它。
// 正确写法
@property (nonatomic,copy) NSString *tutorialName;
// 错误写法
@property (nonatomic,strong) NSString *tutorialName;
@property关键词的使用
对象 strong
基本变量assign
XIB控件 代理 weak
字符串和block使用 copy
对于一些弱引用对象使用weak
对于需要赋值内存对象 copy
字面值
NSString
, NSDictionary
, NSArray
, 和 NSNumber
的字面值应该在创建这些类的不可变实例时被使用。请特别注意nil
值不能传入NSArray
和NSDictionary
字面值,因为这样会导致crash。
应该:
NSArray *names = @[@"Brian", @"Matt", @"Chris", @"Alex", @"Steve", @"Paul"];
NSDictionary *productManagers = @{@"iPhone": @"Kate", @"iPad": @"Kamal", @"Mobile Web": @"Bill"};
NSNumber *shouldUseLiterals = @YES;
NSNumber *buildingStreetNumber = @10018;
不应该:
NSArray *names = [NSArray arrayWithObjects:@"Brian", @"Matt", @"Chris", @"Alex", @"Steve", @"Paul", nil];
NSDictionary *productManagers = [NSDictionary dictionaryWithObjectsAndKeys: @"Kate", @"iPhone", @"Kamal", @"iPad", @"Bill", @"Mobile Web", nil];
NSNumber *shouldUseLiterals = [NSNumber numberWithBool:YES];
NSNumber *buildingStreetNumber = [NSNumber numberWithInteger:10018];
点符号语法
点语法是一种很方便封装访问方法调用的方式。当你使用点语法时,通过使用getter或setter方法,属性仍然被访问或修改。
点语法的使用必须满足两个条件:1是属性;2是有对应的getter和setter方法
一般情况下,满足条件的属性用点语法。而方法则使用中括号。
变量
变量尽量以描述性的方式来命名。单个字符的变量命名应该尽量避免,除了在for()
循环。
星号表示变量是指针。例如, NSString *text
既不是 NSString* text
也不是 NSString * text
,除了一些特殊情况下常量。
私有变量应该尽可能代替实例变量的使用。尽管使用实例变量是一种有效的方式,但更偏向于使用属性来保持代码一致性。
通过使用'back'属性(_variable,变量名前面有下划线)直接访问实例变量应该尽量避免,除了在初始化方法(init, initWithCoder:, 等…),dealloc 方法和自定义的setters和getters。
// 正确写法
@interface RWTTutorial : NSObject
@property (nonatomic, copy) NSString *tutorialName;
@end
// 错误写法
@interface RWTTutorial : NSObject {
NSString *tutorialName;
}
常量
常量是容易重复被使用和无需通过查找和代替就能快速修改值。常量应该使用static来声明而不是使用#define,除非显式地使用宏。
// 正确写法
static NSString * const RWTAboutViewControllerCompanyName = @"RayWenderlich.com";
static CGFloat const RWTImageThumbnailHeight = 50.0;
// 错误写法
#define CompanyName @"RayWenderlich.com"
#define thumbnailHeight 2
枚举类型
当使用``enum时,推荐使用新的固定基本类型规格,因为它有更强的类型检查和代码补全。现在SDK有一个宏
NS_ENUM()```来帮助和鼓励你使用固定的基本类型。
typedef NS_ENUM(NSInteger, RWTLeftMenuTopItemType) {
RWTLeftMenuTopItemMain,
RWTLeftMenuTopItemShows,
RWTLeftMenuTopItemSchedule
};
你也可以显式地赋值(展示旧的k-style常量定义):
typedef NS_ENUM(NSInteger, RWTGlobalConstants) {
RWTPinSizeMin = 1,
RWTPinSizeMax = 5,
RWTPinCountMin = 100,
RWTPinCountMax = 500,
};
旧的k-style常量定义应该避免除非编写Core Foundation C的代码。
// 错误写法
enum GlobalConstants {
kMaxPinSize = 5,
kMaxPinCount = 500,
};
case 语句
大括号在case语句中并不是必须的,除非编译器强制要求。当一个case语句包含多行代码时,大括号应该加上。
有很多次,当相同代码被多个cases使用时,一个fall-through应该被使用。一个fall-through就是在case最后移除'break'语句,这样就能够允许执行流程跳转到下一个case值。为了代码更加清晰,一个fall-through需要注释一下。
switch (condition) {
case 1:
// ** fall-through! **
case 2:
// code executed for values 1 and 2
break;
default:
// ...
break;
}
当在switch使用枚举类型时,'default'是不需要的。例如:
RWTLeftMenuTopItemType menuType = RWTLeftMenuTopItemMain;
switch (menuType) {
case RWTLeftMenuTopItemMain:
// ...
break;
case RWTLeftMenuTopItemShows:
// ...
break;
case RWTLeftMenuTopItemSchedule:
// ...
break;
}
私有属性
私有属性应该在类的实现文件中的类扩展(匿名分类)中声明,命名分类(比如RWTPrivate
或private
)应该从不使用除非是扩展其他类。匿名分类应该通过使用
@interface RWTDetailViewController ()
@property (nonatomic, strong) GADBannerView *googleAdView;
@property (nonatomic, strong) ADBannerView *iAdView;
@property (nonatomic, strong) UIWebView *adXWebView;
@end
布尔值
Objective-C使用YES和NO。因为true和false应该只在CoreFoundation,C或C++代码使用。既然nil解析成NO,所以没有必要在条件语句比较。不要拿某样东西直接与YES比较,因为YES被定义为1和一个BOOL能被设置为8位。
这是为了在不同文件保持一致性和在视觉上更加简洁而考虑。
应该:
if (someObject) {}
if (![anotherObject boolValue]) {}
不应该:
if (someObject == nil) {}
if ([anotherObject boolValue] == NO) {}
if (isAwesome == YES) {} // Never do this.
if (isAwesome == true) {} // Never do this.
如果BOOL属性的名字是一个形容词,属性就能忽略"is"前缀,但要指定get访问器的惯用名称。例如:
@property (assign, getter=isEditable) BOOL editable;
返回布尔值时,要返回YES
或NO
。不能返回逻辑运算的数
// 正确写法
- (BOOL)isBold {
return ([self fontTraits] & NSFontBoldTrait) ? YES : NO;
}
// 正确,逻辑操作符可以直接转化为BOOL
- (BOOL)isValid {
return [self stringValue] != nil;
}
// 错误写法
// 不要将其它类型转化为BOOL返回。因为在这种情况下,BOOL变量只会取值的最后一个字节来赋值,这样很可能会取到0(NO)。但是,一些逻辑操作符比如`&&`,`||`,`!`的返回是可以直接赋给BOOL的
- (BOOL)isBold {
return [self fontTraits] & NSFontBoldTrait;
}
- (BOOL)isValid {
return [self stringValue];
}
条件语句
条件语句主体为了防止出错应该使用大括号包围,即使条件语句主体能够不用大括号编写(如,只用一行代码)。这些错误包括添加第二行代码和期望它成为if语句;还有,even more dangerous defect可能发生在if语句里面一行代码被注释了,然后下一行代码不知不觉地成为if语句的一部分。除此之外,这种风格与其他条件语句的风格保持一致,所以更加容易阅读。
应该:
if (!error) {
return success;
}
不应该:
if (!error)
return success;
或:
if (!error) return success;
三元操作符
当需要提高代码的清晰性和简洁性时,三元操作符?:才会使用。单个条件求值常常需要它。多个条件求值时,如果使用if语句或重构成实例变量时,代码会更加易读。一般来说,最好使用三元操作符是在根据条件来赋值的情况下。
Non-boolean的变量与某东西比较,加上括号()会提高可读性。如果被比较的变量是boolean类型,那么就不需要括号。
应该:
NSInteger value = 5;
result = (value != 0) ? x : y;
BOOL isHorizontal = YES;
result = isHorizontal ? x : y;
不应该
result = a > b ? x = c > d ? c : d : y;
Init方法
Init方法应该遵循Apple生成代码模板的命名规则。返回类型应该使用instancetype
而不是id
- (instancetype)init {
self = [super init];
if (self) {
// ...
}
return self;
}
不要使用new语法。
尽管很多时候能用new
代替alloc init
方法,但这可能会导致调试内存时出现不可预料的问题。Cocoa的规范就是使用alloc init
方法,使用new
会让一些读者困惑。
类构造方法
当类构造方法被使用时,它应该返回类型是instancetype而不是id。这样确保编译器正确地推断结果类型。
@interface Airplane
+ (instancetype)airplaneWithType:(RWTAirplaneType)type;
@end
方法
在方法签名中,应该在方法类型(-/+ 符号)之后有一个空格。在方法各个段之间应该也有一个空格(符合Apple的风格)。在参数之前应该包含一个具有描述性的关键字来描述参数。
多个参数的话 务必说明参数的作用
不要用and
来连接两个参数,通常and
用来表示方法执行了两个相对独立的操作(从设计上来说,这时候应该拆分成两个独立的方法): 用with。
如果一个函数有特别多的参数或者名称很长,应该将其按照:来对齐分行显示:
-(id)initWithModel:(IPCModle)model
ConnectType:(IPCConnectType)connectType
Resolution:(IPCResolution)resolution
AuthName:(NSString *)authName
Password:(NSString *)password
MAC:(NSString *)mac
AzIp:(NSString *)az_ip
AzDns:(NSString *)az_dns
Token:(NSString *)token
Email:(NSString *)email
Delegate:(id)delegate;
函数调用时,和书写格式一样。可以按照函数的长短来选择写在一行或者分成多行。分多行写时,要按照:
对齐各个参数。
语法糖
应该使用可读性更好的语法糖来构造NSArray,NSDictionary等数据结构,避免使用冗长的alloc,init方法。
如果构造代码写在一行,需要在括号两端留有一个空格,使得被构造的元素于与构造语法区分开来:
//正确,在语法糖的"[]"或者"{}"两端留有空格
NSArray *array = @[ [foo description], @"Another String", [bar description] ];
NSDictionary *dict = @{ NSForegroundColorAttributeName : [NSColor redColor] };
//不正确,不留有空格降低了可读性
NSArray* array = @[[foo description], [bar description]];
NSDictionary* dict = @{NSForegroundColorAttributeName: [NSColor redColor]};
命名规范
清晰 命名应该尽可能的清晰和简洁,但在Objective-C中,清晰比简洁更重要。由于Xcode强大的自动补全功能,我们不必担心名称过长的问题。
黄金路径
当使用条件语句编码时,不要嵌套太多if语句,多个返回语句也是OK。
应该:
- (void)someMethod {
if (![someOther boolValue]) {
return;
}
//Do something important
}
不应该:
- (void)someMethod {
if ([someOther boolValue]) {
//Do something important
}
}
错误处理
当方法通过引用来返回一个错误参数,判断返回值而不是错误变量。
应该:
NSError *error;
if (![self trySomethingWithError:&error]) {
// Handle Error
}
不应该:
NSError *error;
[self trySomethingWithError:&error];
if (error) {
// Handle Error
}
在成功的情况下,有些Apple的APIs记录垃圾值(garbage values)到错误参数(如果non-NULL),那么判断错误值会导致false负值和crash。
单例模式
单例对象应该使用线程安全模式来创建共享实例。
+ (instancetype)sharedInstance {
static id sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
换行符
换行符是一个很重要的主题,因为它的风格指南主要为了打印和网上的可读性。
self.productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:productIdentifiers];
一行很长的代码应该分成两行代码,下一行用两个空格隔开。
self.productsRequest = [[SKProductsRequest alloc]
initWithProductIdentifiers:productIdentifiers];
参考文献:
https://www.jianshu.com/p/cb0269b88d8a
https://juejin.im/post/6844903524887691277