iOS编写高质量代码
前言
github:https://github.com/koknine
这是一篇读书笔记,快速记录各种高效率编程的技巧和方法。这些方法是为了提升编码质量和效率,高质量代码利于后期的维护和更新,毕竟不能一份代码到永远。
由于是记录形式,当然不能把整篇内容都写下来,只记录关键性的内容,长期更新。
正文
Objective-C
使用了消息机制代替调用方法。
区别:使用消息结构的语言,其运行时缩影执行的代码由运行环境来决定。而使用函数调用的语言,则又编译器决定。
头文件中少引用其他文件
在头文件中使用@Class
代替直接引用其他头文件
多使用字面量语法
NSNumber *intNumber = @1; NSNumber *floatNumber = @2.5f; NSNumber *doubleNumber = @3.1415926; NSNumber *boolNumber = @YES; NSNumber *charNumber = @'a'; int a = 3; float b = 2.1; NSNumber *c = @(a*b); NSArray *animals = @[@"cat",@"dog",@"monkey"]; NSString *dog = animals[1]; NSDictionary *dataDict = @{ @"firstName" : @"aa", @"lastName" : @"bb", @"age" : @20 }; NSString *lastName = dataDict[@"lastName"]; NSMutableArray *mutableArray = animals.mutableCopy;
多用类型常量,少用#define
预处理
如果只在本类使用的常量,使用static const
关键字来定义常量。
如果多个类都需使用到某一常量,则需将常量定义成公开的,具体方式是在类的声明文件中使用extern const
关键字声明常量,在类的实现文件中使用const
关键字定义常量,这样任何类只要导入了声明常量的头文件就可以直接使用定义好的常量了。
在.h
文件中声明
extern NSString *const XFExternalConst;
在.m文件中
描述
NSString *const XFExternalConst = @"ko";
为避免冲突,一般都用类名做前缀。
用枚举表示状态、选项、状态码
枚举只是一种常量命名方式,某个对象所经历的各种状态可以定义为一个枚举集。
编译器会为枚举分配一个独有的编号,从0开始每个递增加1.实现枚举所用的数据类型取决于编译器,不过其二进制位的个数必须能完全表示枚举编号才行。
enum ConnectionState { ConnectionStateDisconnected, ConnectionStateConnecting, ConnectionStateConnected};typedef enum ConnectionState ConnectingState;
还可以不使用编译器所分配的编号,手工指定某个枚举成员所对应的值。
还有一种情况应该使用枚举类型,那就是定义选项的时候。若这些选项可以彼此组合,则更应该如此。只要枚举定义的对,各选项之间就可以通过“按位或操作符”来组合。
凡是需要以按位或操作来组合的枚举都应该用NS_OPTIONS
宏,如果没有组合需求,就用NS_ENUM
宏。
typedef NS_ENUM(NSInteger, UIViewAnimationTransition) { UIViewAnimationTransitionNone, UIViewAnimationTransitionFlipFromLeft, UIViewAnimationTransitionFlipFromRight, UIViewAnimationTransitionCurlUp, UIViewAnimationTransitionCurlDown,};typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) { UIViewAutoresizingNone = 0, UIViewAutoresizingFlexibleLeftMargin = 1 << 0, UIViewAutoresizingFlexibleWidth = 1 << 1, UIViewAutoresizingFlexibleRightMargin = 1 << 2, UIViewAutoresizingFlexibleTopMargin = 1 << 3, UIViewAutoresizingFlexibleHeight = 1 << 4, UIViewAutoresizingFlexibleBottomMargin = 1 << 5};
枚举在switch
语句里面的时候,不需要加default
分支。
属性的概念
基本方法就不描述了。
@dynamic
关键字,表示不要自动创建实现属性所有的实例变量,也不要为其创建存取方法。即使编译器没有发现定义存取方法,也不会报错,它相信这些方法能在运行期找到。
属性的四种特质
原子性
默认情况下,编译器合成的方法会锁定机制保持atomic
。如果使用nonatomic
,则不使用同步锁。
读写权限
readwrite
的属性具有getter
和setter
方法
readonly
的属性仅具有getter
方法
内存管理语义
assign
只针对“纯量类型”,比如CGFloat
或者NSInteger
strong
表示该属性定义了一种拥有关系
。为这种属性设置新值时,设置方法会先保留新值,并释放旧值,然后将新值设置上去
weak
表示该属性定义另一种非拥有关系
。为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。和assign
类似,然而在属性所指的对象遭到摧毁时,属性值也会清空
unsafe_unretained
这个和assign
相同,但是它适用于对象类型
,该特质表达一种非拥有关系
,当目标对象遭到摧毁时,属性值不会自动清空,是不安全的
copy
表达的所属关系和strong
类型。然后设置方法并不保留新值,而是将其拷贝。当属性类型为NSString *
时,经常用此特质来保护其封装性,因为传递给设置方法的新值可能指向一个NSMutableString
类的实例。如果不是拷贝的花,那么设置完属性以后,字符串的值可能会在对象不知情的情况下遭人更改。所以这个时候需要拷贝一份不可变的字符串。
方法名
getter=<name>
指定getter
的方法名。如果属性是Boolean
型,在方法名加上is
前缀,就可以用这个方法来指定。
setter=<name>
指定setter
的方法名。这个不常见。
在对象内部尽量直接访问实例变量
懒加载是重写getter
方法
理解对象等同性
的概念
按照==
操作符比较出来的结果未必是我们想要的,因为该操作符比较出来的是两个指针本身,而不是指针所指的对象。应该是用NSObject
协议中声明的isEqual
方法来判断两个对象的等同性。来办来说两个类型不同的对象总是不相等的。
NSString *oneStr = @"aaa 21";NSString *twoStr = [NSString stringWithFormat:@"aaa %d",21];BOOL equalA = (oneStr == twoStr);//NOBOOL equalB = [oneStr isEqual:twoStr];//YESBOOL equalC = [oneStr isEqualToString:twoStr];//YES
两个用于判断等同性的关键方法
(BOOL)isEqual:(id)object;
@property (readonly) NSUInteger hash;
默认实现是:当且仅当其指针值完全相等时,这两个对象才相等。
几个要点
若想监测对象的等同性,提供
isEqual:
与 hash 方法相同的对象必须具有相同的哈希码,但是两个哈希码相同的对象却未必相同
不要盲目逐个检测每条属性,而是应该依照具体需求来制定监测方案
编写 hash 方法时,应该是用计算速度快而且碰撞低的算法
以“类族模式”隐藏实现细节
核心套路就是类似UIButton
,创建的时候传入一个枚举值,根据枚举值来创建子类。(这里的笔记是我看懂以后写的,不知道的朋友先搜索一下工厂模式
,其实就是那个意思)
在既有类中使用关联对象存放自定义数据
有时候需要在对象中存放相关信息,这时候我们通常会从对象所属的类中继承一个子类,然后改用这个子类对象。然而并非所有情况下都能这么做,有时候类的实例可能由某种机制创建。Objective-C
有一种强大机制叫关联对象
。
这种机制要小心使用,因为会使代码失控。
理解objc_msgSend
的作用
原型
void objc_msgSend(id self, SEL cmd, ...)
一个例子
id returnValue = [someObject messageName:parameter];
编译器会把它转换为以下函数
id returnValue = objc_msgSend(someObject,@selector(messageName:),parameter);
为了完成调用方法,该方法需要在接受者所属的类中搜寻其方法列表
,如果能找到,就跳转过去。如果找不到,就沿着继承体系向上继续查找,等找到合适的再跳转。如果最终还是找不到,就执行消息转发
的操作。
一些边界情况,则交由另一些函数处理
objc_msgSend_stret
如果待发送的消息要返回结构体,可交此函数处理。objc_msgSend_fpret
如果消息返回的是浮点数,可交由此函数处理。ojbc_msgSendSuper
如果要给超类发送消息,例如[super message:parameter]
,那么就就交由此函数处理。
理解消息转发机制
消息转发分为两大阶段。
第一阶段先问接受者,所属的类,看其是否能动态添加方法,以处理当前这个unknown selector
,这称为dynamic method resolution
。
第二阶段涉及full forwarding mechanism
。如果运行期系统已经把第一阶段执行完了,那么接受者自己就无法再以动态新增方法的手段来响应包含该selector
的消息了。此时,运行期系统会请求接受者以其他手段来处理与消息相关的方法调用。然后又分两部。
首先,让接受者看看有没有其他对象能处理这条消息。如果有,就转发给那个对象。
如果没有,就会启动完整的消息转发机制,运行期系统会把消息有关的全部细节封装到NSInvocation
对象中,再给接受者最后一次机会,让它设法解决当前还未处理的这条消息。
动态方法解析
对象收到无法解读的消息后,先调用
+ (BOOL)resolveInstanceMethod:(SEL)sel
该方法的参数就是那个未知的selector
,返回Boolean
类型,表示这个类是否能新增一个实例方法用来处理这个selector
。在继续走下去之前,这有个机会新增一个处理的方法。
如果尚未实现的不是实例方法而是类方法,则调用
+ (BOOL)resolveClassMethod:(SEL)sel
使用他们的前提是,相关方法的实现代码已经写好,只等着运行的时候动态插入在类里面。
这个常常用来实现@dynamic
属性。
后备接收者
当前接收者还有第二次机会处理,能不能把消息转发给其他接收者
- (id)forwardingTargetForSelector:(SEL)aSelector
找得到就返回对象,找不到就返回nil
。
完整的消息转发
- (void)forwardInvocation:(NSInvocation *)anInvocation
先创建NSInvocation
对象,把尚未处理的那条消息有关的全部细节都封在其中。此对象包含selector
,target
以及参数。
继承体系中的每个类都有机会处理此调用请求,直到NSObject
。如果还没有找到,那么该方法还会继续调用doesNotRecognizeSelector:
抛出异常,此异常表示最终未能处理。
这个机制属于底层机制,可以动态注入方法,甚至之前的可以动态注入属性,云后端服务商可以说基本就靠这个套路,通过KVC的样子往类里面添加属性。
用方法调配技术调试黑盒方法
黑科技。
IMP指针,改方法实现,替换系统方法,可以多添加日志打印。
类对象
OC 是一门极其动态的语言。
每个 OC 对象实例都是指向某块内存数据的指针。
typedef struct objc_object { Class isa;} *id;
每个对象结构体的首个成员是Class
类的变量。该变量定义了对象所属的类,通常称为is a
指针。
typedef struct objc_class *Class;struct objc_class { Class isa; Class super_class; const char *name; long version; long info; long instance_size; struct objc_ivar_list *ivars; struct objc_method_list **methodLists; struct objc_cache *cache; struct objc_protocol_list *protocols;};
此结构体存放类的元数据
,例如类的实例实现了几个方法,具备多少个实例变量等信息。
首个变量是isa
指针,说明Class
本身也是 OC 对象。
super_class
定义了本类的超类。类对象所属的类型是另一个类,叫做超类
。
每个类仅有一个类对象
,而每个类对象
仅有一个与之相关的元类
。
class
方法所返回的类表示发起代理的对象,而非接受代理的对象。
接口与 API 设计
(未完待续)
总结
纯属个人笔记,特别是底层机制很有作用,如今iOS
开发不再仅仅是把一个内容展现出来,里面还有涉及到各种安全性能,了解根本才是持续发展之道。