这本书总共7章,准备分3次记录
前2章说的是熟悉OC,对象、消息、运行时
第一章:了解Objective
1、 Objectve-C是运行时,是使用面向对象动态绑定的消息结构,每次调用其实都是发送消息
2、 在类的头文件尽量少引入其他头文件
随着项目的迭代,可能会造成"循环引用",这样子会使编译的时间大大增加。怎么办:多使用向前声明(forward declaring),包括协议也可以这样子写。
例子:创建2个继承NSObject的Person类和Dog类,如果只希望在Person类中引入Dog类而不是其他的,那么可以使用向前声明-即在编译到Dog类的时候才会发送消息给Dog类。而不是直接#import "Dog.h"
#import <Foundation/Foundation.h>
//#import "Dog.h" //Person 的实现文件则需要引入Dog.h文件,需要知道Dog所有接口细节。
@class Dog; //简单的引入Dog.h
@interface Person : NSObject
@property(copy,nonatomic) NSString *name;
@property(strong,nonatomic) Dog *dog;
@end
在.m里如果需要是用Dog类的话另外在import "Dog.h"
- 3、多用字面量语法,少用与之等价的方法
NSString *something = @"字面量语法";
NSString *something = [[NSString alloc] initWithFormat:@"非字面量语法"];
NSArray *animalss = @[@"字",@"面",@"量"];
NSString *zi = animalss[0];
NSArray *animalB = [NSArray arrayWithObjects:@"非",@"字",@"面",@“量”, nil];
NSString *fei = [animals objectAtIndex:0];
注意:但是如果使用字面量语法的时候不能让元素为nil,而animalB如果碰到了第2个元素为nil,那么就不会再获取后面的元素。所以字面量语法能够保证每次的元素都是更为安全的。字典同理。但是字面量语法所创建的对象必须属于Foundation框架才行。 - 4、多用类型常量,少用#define预处理指令
define预处理指令#define ANIMATION_DURATION 0.3
类型常量:static const NSTimeInterval kAnimationDuration = 1;
理由:类型常量定义的对象含有对象类型,预处理指令命名不够清晰,并且可能会替代掉相同类型名。而且他们都不应该被定义成公共变量,因为当然import
的时候会连同公共变量一起导入,随着版本的迭代会造成命名混乱,指代不清晰。注意:在.m里用static const NSTimeInterval ClassNameAnimationDuration = 1
,在.h里用extern constNSTimeInterval ClassNameAnimationDuration
- 5、用枚举表示状态、选项、状态码
注意,用枚举+switch的时候处理判断的时候。switch不要实现default分支了,因为编译器会提示开发者:switch语句并未处理所有枚举
typedef NS_ENUM(NSUInteger,ClassNameConnectionState) {
ClassNameConnectionStateDisconnected,//0
ClassNameConnectionStateConnecting,//1
ClassNameConnectionStateConnected,//2
};
第二章:对象、消息、运行时
在OC中,开发者可以通过对象来储存并传递数据,在对象之间传递数据并执行任务的过程叫做"消息传递"(Messaging)。程序运行起来后,为其提供支持的是OC runtime
- 5、属性
编译器根据名称自动创建出存(setter)取(getter)方法。这里的copy,assign,retain,strong,weak就放在后面的内存管理一起记录
@interface Person : NSObject
@property (nonatomic,readwrite,copy)NSString *name;
//用property创建后编译器会自动生成非同步锁,可读可写的copy特质的如下2个方法
- (NSString *)name; //readonly
- (void)setName:(NSString *)name;//writeonly
@end
我们在访问的时候可以使用"点语法"
Person *person = [Person new];
person.name = @"nike"; //Same as;
[person setName:@"nike"];
- 6、理解“属性"这一概念
属性用于封装对象中的数据,OC对象通常会把其所需要的数据保存为各种实例变量。而实例变量一般通过存取方法来访问。getter用于读取变量,setter用于写入变量。
用属性的好处就是不论在编译期还是运行期,系统能够正确的使实例变量的偏移量正确的指向其实例变量。甚至可以在运行期增加实例变量。
例:
@interface EOCPerson : NSObject
@property NSString *firstName;//same as getter/setter
@property NSString *lastName;
@end
@interface EOCPerson : NSObject
- (NSString*)firstName;
- (void)setFirstName:(NSString*)firstName;
- (NSString*)lastName;
- (void)setLastName:(NSString*)lastName;
@end
访问属性,可以使用点语法。编译器会把点语法转换为对存取方法的调用:
aPerson.firstName = @"Bob"; // Same as:
[aPerson setFirstName:@"Bob"];
NSString *lastName = aPerson.lastName; // Same as:
NSString *lastName = [aPerson lastName];
如果我们不希望编译器自动生成存取方法的话,也不要创建存取方法。需要设置@dynamic 字段:
@interface EOCPerson : NSManagedObject
@property NSString *firstName;
@property NSString *lastName;
@end
@implementation EOCPerson
@dynamic firstName, lastName; //_firstName变成了firstName
@end
内存管理
assign:纯量类型(scalar type)的简单赋值操作
strong:拥有关系保留新值,释放旧值,再设置新值
weak:非拥有关系(nonowning relationship),不保留新值不释放旧值,属性所指的对象遭到摧毁时,属性也会清空
unsafe_unretained :类似assign,适用于对象类型,非拥有关系,属性所指的对象遭到摧毁时,属性不会清空。
copy:不保留新值,而是将其拷贝,NSString * 用此保护其封装性。因为NSString可能指向一个NSMutableString,所以为了保证对象中的字符串值不会无意间变动,需要设置Copy
- 7、在对象内部尽量直接访问实例变量
关于实例变量的访问,可以直接访问,(_InstanceName)也可以通过属性的方式(点语法)来访问。书中作者建议在读取实例变量时采用直接访问的形式,而在设置实例变量的时候通过属性来做。
- 直接访问属性的特点:
绕过set,get语义,速度快;
不能触发KVO,通知 - 通过属性访问属性的特点:
不会绕过属性定义的内存管理语义(不能设置strong或者copy等类型)
有助于打断点排查错误
可以触发KVO
因此,有个关于折中的方案:
设置属性:通过属性
读取属性:直接访问
不过有两个特例:
- 1、初始化方法和dealloc方法中,需要直接访问实例变量来进行设置属性操作。因为如果在这里没有绕过set方法,就有可能触发其他不必要的操作。
- 2、 惰性初始化(lazy initialization)的属性,必须通过属性来读取数据。因为惰性初始化是通过重写get方法来初始化实例变量的,如果不通过属性来读取该实例变量,那么这个实例变量就永远不会被初始化。
- 8、理解“对象等同性”概念
按照 == 操作符比较出来的是指针,而非指针所指的对象,应该使用isEqual方法来判断等同性。
NSString *foo = @"Badger 123";
NSString *bar = [NSString stringWithFormat:@"Badger %i",123];
//比较指针,而非指针所指的对象foo与bar,foo与bar指的不同的对象
BOOL equalA = (foo == bar); // equalA = NO
//比较类型以及内容
BOOL equalB = [foo isEqual:bar]; // equalA = YES
//仅能判断string类型,调用方法比isEqual快
BOOL equalC = [foo isEqualToString:bar]; // equalA = YES
指针相同,那么所指的内容一定相同。内容相同,所指的指针不一定相同
判断的时候优先判断指针(==),再判断类型Class,最后判断内容
如果我们有以下这个类
@interface Person : NSObject
@property (copy,nonatomic) NSString *firstName;
@property (copy,nonatomic) NSString *lastName;
@property (assign,nonatomic) NSUInteger age;
@end
我们要判断2个类是不是相同的的流程
- (BOOL)isEqual:(id)object{
//判断指针是否相同,如果指针相同,那么肯定是同一个类
if (self == object)return YES;
//判断2个类是否相同,如果不同,那么不是同一个类
if ([self Class] != [object class]) return NO;
//判断类型里的所有内容是否相同,如果有一个不同,那么他们就不是一个类
Person *otherPerson = (Person *)object;
if (![_firstName isEqualToString:otherPerson.firstName]) {
return NO;
}
if (![_lastName isEqualToString:otherPerson.lastName]) {
return NO;
}
if (_age != otherPerson.age) {
return NO;
}
return YES;
}
hash方法只在对象被添加至NSSet和设置为NSDictionary的key时会调用,因为用的少就不做介绍了
- 9 、以"类族模式"隐藏实现细节
API典型例子。你可以通过传入不同的Type直接创建不同的Btn,但是不管是什么类型的对象,他们都继承同一个基类:UIButton
+ (instancetype)buttonWithType:(UIButtonType)buttonType;
比如你可以这么写:
- (void)viewDidLoad {
[super viewDidLoad];
UIButton *btn = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
btn.frame = CGRectMake(50, 50, 100, 100);
[self.view addSubview:btn];
EOCEmployee *employ = [EOCEmployee employeeWithType:EOCEmployeeTypeDesigner];
employ.name = @"dddddddddddddd";
employ.salary = 111;
[employ doADaysWork];
}
在你的父类.h里,通过不同的type创建不同的EOCEmployee
#import <Foundation/Foundation.h>
//这里必须要在父类里导入子类才能创建子类 这个太蠢了 所以用向前声明
@class EOCEmployeeDeveloper;
@class EOCEmployeeDesigner;
@class EOCEmployeeFinance;
typedef NS_ENUM(NSUInteger,EOCEmployeeType){
EOCEmployeeTypeDeveloper,
EOCEmployeeTypeDesigner,
EOCEmployeeTypeFinance,
};
@interface EOCEmployee : NSObject
@property (nonatomic,copy) NSString *name;
@property (nonatomic,assign) NSUInteger salary;
+ (EOCEmployee *)employeeWithType:(EOCEmployeeType)type;
- (void)doADaysWork;
@end
在父类的.m里实现
#import "EOCEmployee.h"
#import "EOCEmployeeDeveloper.h"
#import "EOCEmployeeDesigner.h"
#import "EOCEmployeeFinance.h"
@implementation EOCEmployee
+(EOCEmployee *)employeeWithType:(EOCEmployeeType)type{
switch (type) {
case EOCEmployeeTypeDeveloper:
NSLog(@"EOCEmployeeTypeDeveloper");
return [EOCEmployeeDeveloper new];
break;
case EOCEmployeeTypeDesigner:
NSLog(@"EOCEmployeeTypeDesigner");
return [EOCEmployeeDesigner new];
break;
case EOCEmployeeTypeFinance:
NSLog(@"EOCEmployeeTypeFinance");
return [EOCEmployeeFinance new];
break;
// default:
// break;
}
}
- (void)doADaysWork{
NSLog(@"EOCEmployee---doADaysWork");
}
@end
创建EOCEmployeeDeveloper继承自父类EOCEmployee
#import "EOCEmployee.h"
@interface EOCEmployeeDeveloper : EOCEmployee
@end
#import "EOCEmployeeDeveloper.h"
@implementation EOCEmployeeDeveloper
- (instancetype)init
{
self = [super init];
if (self) {
NSLog(@"init");
}
return self;
}
- (void)doADaysWork{
NSLog(@"EOCEmployeeDeveloper----doADaysWork");
}
@end
- 10、在既有类中使用关联对象存放自定义数据
感觉没什么卵用啊 - 11、理解objc_msgSend的作用
OC的调用方法称传递消息,消息与名称或者选择子,可以接受参数,可有返回值。
然而对象收到 消息后,究竟该调用哪个方法则完全于运行期决定,甚至可以在程序运行时改变,这些特性使得OC成为一门真正的动态语言。
在OC中,给对象发送消息的语法是:
id returnValue = [someObject messageName:parameter];
这里,someObject叫做“接收者(receiver)”,messageName:叫做"选择子(selector)",选择子和参数合起来称为“消息”。编译器看到此消息后,将其转换为一条标准的C语言函数调用,所调用的函数乃是消息传递机制中的核心函数叫做objc_msgSend,它的原型如下:
void objc_msgSend(id self, SEL cmd, ...)
第一个参数代表接收者,第二个参数代表选择子,后续参数就是消息中的那些参数,数量是可变的,所以这个函数就是参数个数可变的函数。
因此,上述以OC形式展现出来的函数就会转化成如下函数:
id returnValue = objc_msgSend(someObject,@selector(messageName:),parameter);
这个函数会在接收者所属的类中搜寻其“方法列表”,如果能找到与选择子名称相符的方法,就去实现代码,如果找不到就沿着继承体系继续向上查找。如果找到了就执行,如果最终还是找不到,就执行消息转发操作。
注意:如果匹配成功的话,这种匹配的结果会缓存在“快速映射表”里面。每个类都有这样一块缓存。所以如果将来再次向该类发送形同的消息,执行速度就会更快了。
- 12、理解消息转发机制
如果类和其所有的父类无法立即响应某个选择子,那么就会启动消息转发流程(message forwarding)。是一种OC设计的防止找不到选择器而导致程序崩溃的debug方式
如果
//这个方法是给Person类发送run的消息
[Person run]
但是报了如下错误 : unrecognized selector sent to instance
这是因为,当run这个方法只有定义没有实现会怎么样呢
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Person run]: unrecognized selector sent to instance
接下来重现[Person run]
方法的调用过程
首先,该方法在调用时,系统会查看这个对象能否接收这个消息(查看这个类有没有这个方法,或者有没有实现这个方法。),如果不能并且只在不能的情况下,就会调用下面这几个方法,给你“补救”的机会,你可以先理解为几套防止程序crash的备选方案,我们就是利用这几个方案进行消息转发,注意一点,前一套方案实现后一套方法就不会执行。如果这几套方案你都没有做处理,那么程序就会报错crash。
方案一:动态方法解析
+(BOOL)resolveInstanceMethod:(SEL)sel 或者
+(BOOL)resolveClassMethod:(SEL)sel
方案二:消息转发重定向
-(id)forwardingTargetForSelector:(SEL)aSelector
方案三:
//生成方法签名
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
// 拿到签名转发消息
-(void)forwardInvocation:(NSInvocation *)anInvocation;
example:
VC
- (void)viewDidLoad {
[super viewDidLoad];
Person *p = [[Person alloc] init];
[p run:@"2"];
Car *c = [[Car alloc] init];
[c run:@"33"];
[c study:@"44"];
}
Person.h
@interface Person : NSObject
- (void)run:(NSString *)time;
@end
Person.m
#import "Person.h"
#import <objc/runtime.h>
#import "Car.h"
@implementation Person
//正常情况下走run:
//- (void)run:(NSString *)time{
// NSLog(@"%s",__func__);
// log出的是Person类里的run:2
//}
//方案1 如果没有实现run方法,那么就会走到该方法,这里的操作是动态添加run方法,保护程序不崩溃
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"sel = %@",NSStringFromSelector(sel));
//1.判断没有实现方法, 那么我们就是动态添加一个方法
if (sel == @selector(run:)) {
class_addMethod(self, sel, (IMP)newRun, "v@:@:");
return YES;
}
//不做任何操作直接调用父类
return [super resolveInstanceMethod:sel];
}
void newRun(id self,SEL sel,NSString *str) {
NSLog(@"%@",self);
NSLog(@"%s",sel_getName(sel));
NSLog(@"---runok---%@",str);
}
//方案2 这个方法返回你需要转发消息的对象。这里将run:转发给Car里的run:
- (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"%s",__func__);
//将消息转发给Car类里的run:选择器
return [[Car alloc] init];
不做任何操作直接调用父类
//return [super forwardingTargetForSelector:aSelector];
}
/*
关于生成签名的类型"v@:"解释一下。每一个方法会默认隐藏两个参数,self、_cmd,self代表方法调用者,_cmd代表这个方法的SEL,签名类型就是用来描述这个方法的返回值、参数的,v代表返回值为void,@表示self,:表示_cmd。
开头我们要找的错误unrecognized selector sent to instance原因,原来就是因为methodSignatureForSelector这个方法中,由于没有找到run对应的实现方法,所以返回了一个空的方法签名,最终导致程序报错崩溃。
所以我们需要做的是自己新建方法签名,再在forwardInvocation中用你要转发的那个对象调用这个对应的签名,这样也实现了消息转发。
*/
//方案3
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector OBJC_SWIFT_UNAVAILABLE(""){
//转化字符
NSString *sel = NSStringFromSelector(aSelector);
//判断, 手动生成签名
if([sel isEqualToString:@"run:"]){
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation OBJC_SWIFT_UNAVAILABLE(""){
NSLog(@"---%@---",anInvocation);
//取到消息
SEL seletor = [anInvocation selector];
//转发
Car *c = [[Car alloc] init];
if([c respondsToSelector:seletor]){
//唤醒对象,进行转发
[anInvocation invokeWithTarget:c];
}else{
return [super forwardInvocation:anInvocation];
}
}
@end
Car.h
@interface Car : NSObject
- (void)study:(NSString *)time;;
- (void)run:(NSString *)time;;
@end
Car.m
#import "Car.h"
@implementation Car
//- (instancetype)init{
// self = [super init];
// if (self) {
// NSLog(@"%s",__func__);
// }
// return self;
//}
//会从[P run:@"2"];----->调用至此处
- (void)run:(NSString *)time{
NSLog(@"%s",__func__);
NSLog(@"%@",time);
}
//消息不会转发到这 正常调用会
- (void)study:(NSString *)time{
NSLog(@"%s",__func__);
NSLog(@"%@",time);
}
@end
- 13、用"方法调配技术(method swizzling)"调试"黑盒方法" ,可以交换系统方法(runtime:method_exchangeImplemetations(originalMethod,swappendMethod))
即在OC的运行期改变其选择器的方法而不通过继承或者重写
+ (void)load {
//方法交换应该被保证,在程序中只会执行一次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//获得viewController的生命周期方法的selector
//在一个子视图将要被添加到另一个视图的时候会调用此方法
SEL systemSel = @selector(willMoveToSuperview:);
//自己实现的将要被交换的方法的selector
SEL swizzSel = @selector(myWillMoveToSuperview:);
//两个方法的Method 也可以是类方法
Method systemMethod = class_getInstanceMethod([self class], systemSel);
Method swizzMethod = class_getInstanceMethod([self class], swizzSel);
//首先动态添加方法,实现是被交换的方法,返回值表示添加成功还是失败
BOOL isAdd = class_addMethod(self, systemSel, method_getImplementation(swizzMethod), method_getTypeEncoding(swizzMethod));
if (isAdd) {
//如果成功,说明类中不存在这个方法的实现
//替换类中已有方法的实现,如果该方法不存在添加该方法
//将被交换方法的实现替换到这个并不存在的实现
class_replaceMethod(self, swizzSel, method_getImplementation(systemMethod), method_getTypeEncoding(systemMethod));
} else {
//否则,交换两个方法的实现
method_exchangeImplementations(systemMethod, swizzMethod);
}
});
}
- (void)myWillMoveToSuperview:(UIView *)newSuperview {
//调用myWillMoveToSuperview等于调用系统方法
[self myWillMoveToSuperview:newSuperview];
if (self) {
if (self.tag == 10086) {
self.textColor = [UIColor redColor];
}
}
}
14、理解"类对象"的用意
isMenberOfClass 判断出对象是否为某个特定类的实例
isKindOfClass 判断出对象是否为某类或其派生类的实例