Effective Objective-C 2.0 笔记 (一)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Q52077987/article/details/82893614

第1条 了解Objective-C语言起源

这一章先是澄清了OC的消息机制和函数调用机制的区别。C++的函数调用机制在涉及到多态的时候也是动态绑定的,而OC只是普通的函数调用也是动态绑定的,也就是运行时查找应该执行的函数指针。

接着介绍了OC中的对象都是保存在堆上的,非对象类型都是保存在栈上的。

第2条 在类的头文件中尽量少引入其他头问题

介绍了通过@class关键字声明类,可以将类的定义文件引入推迟到.m文件中,这样头文件的互相依赖就会减少,加快编译速度。另外在使用protocol的时候,如果类和协议互相引用,使用@class是优雅的解决方法。

代码示例:

@class SGVDownloadButtonView;
@protocol SGVDownloadButtonViewDelegate<NSObject>
@optional
-(void)willPerformDownloadForView:(SGVDownloadButtonView *)view;
-(void)didPerformDownloadForView:(SGVDownloadButtonView *)view;
@end

@interface SGVDownloadButtonView : UIView
@property (nonatomic, weak)id<SGVDownloadButtonViewDelegate> delegate;
@property (nonatomic) SGVVideoListModel *model;

@end

第3条 多用字面语法,少用与之等价的方法

介绍了字符串,数组,字典,数组对象等字面变量的语法,推荐使用的原因主要是简洁。

代码示例:

NSString *str = @"hello world";
NSDictionary *dict = @{@"key": @"value"};
NSArray *array = @[@1, @3, @5];

第4条 多用类型常量,少用#define预处理指令

因为预处理指令仅仅是简单的替换,不能让编译器检查语法。因此包括C和C++语言中都一样不推荐使用#define。文中建议使用static const方式声明常量,如果是全局的使用extern const。前者表示这个常量的定义仅仅在本编译单元有效,后者表示这个常量是全局的,其定义在全局符号表中,在链接阶段会找到的。

实际实现中,static const的常量编译器根本不会创建符号,而是和预处理指令一样,直接替换。

const NSString * 和NSString * const

const NSString *表示字符串的内容是不能改的,实际上这么写没有意义,NSString本身就是不可变对象。NSString * const 表示变量只能指向这个字符串,不能重新给变量赋值。

    NSString *const str1 = @"hello";
    const NSString *str2 = @"world";
    str1 = str2; //编译不过
    str2 = str1;

因此使用全局变量声明的时候应该写extern NSString * const str = @“XXX”;

第5条 用枚举表示状态、选项、状态码

前边介绍了C++11的enum新特性。但是在OC中应该使用NS_ENUM和NS_OPTION来定义枚举,前者的枚举值是累加的,后者的枚举值是位移的,可以用于位或。

代码示例:

typedef NS_ENUM(NSUInteger, EOCConnectionState){
	EOCConnectionStateDisconnected, //=0
    EOCConnectionStateConnecting,
    EOCConnectionStateConnected,
};

typedef NS_OPTION(NSUInteger, EOCPermittedDirection){
    EOCPermittedDirectionUp = 1 << 0,
    EOCPermittedDirectionDown = 1 << 1,
    EOCPermittedDirectionLeft = 1 << 2,
    EOCPermittedDirectionRight = 1 << 3,
}

另外作者建议在switch语句中使用枚举值时,不要使用default,这样如果枚举多了一项,会得到编译器的警告。感觉这个看团队编码规范,因为有时候仅仅需要处理少数状态,其他的状态大可使用default处理,而新增一种状态的时候,往往是要增加逻辑的,编程者一般也会考虑到。

第6条 理解“属性”这一概念

当我们写下@property的时候,编译器会合成getter和setter方法,还有一个实例变量。对于类内部的时候,也可以不使用属性,直接定义实例变量。

@interface EOCPerson : NSObject{
    NSString *_firstName;
    NSString *_lastName;
    NSString *someInternalImp;
}
@end

但是这样定义的变量是"private"的,也就是只能类内部使用。OC这么做是为了ABI(应用程序二进制接口)兼容性。如果将示例变量暴露给外边,如果类实例变量改变了,就会破坏ABI的兼容性(即使外部使用的那个实例变量没发生变化,也会有问题,因为编译器靠偏移量找实例变量)。

而使用属性,就等于通过函数访问实例变量,改变了实例变量,这个类本身肯定是要重新编译的,这样通过函数访问实例变量的过程也会得到更新,这样对外的接口就是稳定的(使用这个类的部分不用重新编译)。

属性的标识符(特质)

atomic,nonatomic

因为属性的访问会转化成函数的调用(严肃的说是消息的发送),因此atomic能保证属性访问或者设置的原子性,atomic是属性的默认设置,如果在编程的时候能保证属性访问不是多线程的,应该加上nonatomic,因为在iOS上atomic的开销还是比较大的。

readwrite,readonly

属性默认设置是readwrite的,如果希望属性是只读的,使用readonly,编译器将只合成getter方法。一个使用的技巧是属性在接口中只读,但是在内部通过扩展重新指定为readwrite。(直接访问示例变量也可以达到目的,见下节)

assign,strong,weak,unsafe_unretained,copy

assign标量类型的属性设置,strong,对象类型属性设置,weak,表示不增加引用计数,并且当对象销毁的时候能自动置为nil,unsafe_unretained和weak作用一样,但是不能自动置为nil,只有当weak不能用的时候才用这个(有些foundation的类不支持weak);copy用于有可变子类的对象,如NSArray,NSDictionary,NSString等。

还有一个知识点就是@dynamic,告诉编译器不要合成实例变量,只合成getter,setter方法。这在Core Data中常见,因为数据是动态产生的。

第7条 在对象内部尽量直接访问实例变量

第一,在初始化的时候,应该总是直接访问实例变量,因为子类可能会覆盖setter方法。 但是如果初始化的是基类的不可见变量,只能通过属性的方式。

第二,懒惰初始化。如果有这样的设计,应该总是使用属性而不是直接访问实例变量,只有一种情况例外:就是想释放资源的时候。(更合理的方式是,使用了懒惰初始化同时提供一个释放资源的方法)

第三,如果想触发KVO应该使用属性方式。

可以看出这个话题并没有严格统一的答案,应该在编码时根据语义来决定。

第8条 理解“对象等同性”这一概念

什么叫对象等同?

使用==判断两个对象是否等同,表示比较的是两个对象的指针是否指向同一个地址。一般情况下,地址相等就意味着两个对象等同。

C语言中会根据强制类型转换来解释一个地址之后的内存,因此,也可能发生地址相同,但是两个对象意义不同的情况。不过C语言中没有对象这一概念,只有结构体和Union。

更多情况下,对象是否等同是和语义相关的。比如

NSString *str1 = @"hello";
NSString *str2 = [NSString stringWithFormat:@"%@%@",@"hell", @"o"];

str1和str2应该是等同的,虽然他们的地址不同。

比较对象是否等同应该使用isEqual来决定,isEqual是NSObject协议中的一个方法。NSObject中默认的实现就是比较地址。

如果想要实现自己语义的等同,需要实现NSObject协议中以下两个方法:

-(BOOL)isEqual:(id)object;
-(NSUInteger)hash;

等同的语义是:hash函数返回同一个值,且isEqual返回YES;

理由是这样的:如果把一个对象放入一个NSMutableSet,容器是先判断hash值的,如果已经存在该hash值的对象(这些对象一般会保存到一个数组中),会遍历这些hash值相同的对象判断是否相等,如果相等,就认为对象重复,不会添加到Set中。因此isEqual满足的前提应该是hash值相等,如果不这么做,使用HashTable相关的集合类型会发生错误。同样,上边的例子也说明了hash值相等的对象isEqual可以是NO。

一个面试问题,一个类的hash函数,这样写会有什么后果?

-(NSUInteger)hash{
    return 1337
}

答:会极大的影响在Hash Table类型的容器中的效率,这包括NSMutableSet,NSDictionary的key等。因为所有的对象都返回一个hash值,那么所有的对象按照数组方式存放,Hash Table的优势丧失殆尽。

一个常用的设计模式:

-(BOOL)isEqualToPerson:(EOCPerson *)otherPerson{
	if(self == object) return YES;
    //逐个比较属性值,有一个不同就返回NO
    return YES;
}

-(BOOL)isEqual:(id)object{
    if([self class] == [object class]){
		return [self isEqualToPerson:(EOCPersion *)object];
    }else{
        return [super isEqual:object];
    }
}

实现自己类别的isEqualToXXX(就像NSString的isEqualToString:一样),然后在实现isEqual:的时候判断如果传入的对象是自己的类别,就使用自己的比较方式,如果不是就直接调用基类的比较方式。

最后,提示了如果将可变对象放入可变容器,尤其是Set类型的容器可能会产生的Bug。可能会造成Set容器中有两个相同的元素。

第9条 以"类族模式"隐藏细节

就是一个简单工厂和抽象基类的结合,不过OC中没有抽象类的概念。举个UIKit中的例子:

+(UIButton *)buttonWithType:(UIButtonType)type;

实际上返回的是一个UIButton的子类,然而一般情况下使用者不关系子类的具体实现,只要用基类的接口就可以完成工作了。

使用这种模式需要针对每种子类定义一套类型常量,或者字符串常量,也就是上边参数的type。

如果不使用运行时,基类的实现中(.m文件)中将需要包含子类的头文件,形成不符合设计模式的依赖。这个要根据实际情况看看是否可以接受。一个直接后果就是不能不修改基类源代码扩展。

第10条 在既有的类中使用关联对象存放自定义数据

关联对象的技术是通过runtime的API在不侵入继承体系的情况下给类添加成员数据。但是滥用也容易破坏代码结构,写不好还会引起内存管理的问题。

static void *EOCMyAlertViewKey = "EOCMyAlertViewKey";
// ...
// 设置
objc_setAssociatedObject(alert, EOCMyAlertViewKey, block, OBJC_ASSOCIATION_COPY);
// ...
//使用
void (^block) (NSInteger) = objc_getAssociatedObject(alertView, EOCMyAlertViewKey);
block(buttonIndex);

关联对象使用指针的地址作为key,因此要静态定义一个指针,即使不给指针赋值也可以,因为并不用到指针指向的内容。

这是关联对象的时候,使用内存管理语义的,和@property的语义一致。

关联对象常常和Category同时使用,用于给没有源码的类或者不想侵入的类添加属性。

作者建议,只有在没有其他更好的办法的时候才使用关联对象技术,因为容易引入难以查找的bug。

第11条 理解objc_msgSend的作用

对于Objective-C的消息机制,另外成文,参考官方文档我的博客

第12条 理解消息转发机制

同11

第13条 用“方法调配技术”调试“黑盒方法”

又叫method swizzling,就是在运行时将方法替换掉,实现一些“黑魔法”效果。

@interface NSString(EOCMyAdditions)
-(NSString *)eoc_myLowercaseString;
@end

@implementation NSString(EOCMyAdditions)
-(NSString *)eoc_myLowercaseString{
    NSString *lowercase = [self eoc_myLowercaseString];
    NSLog(@"%@ == > %@", self, lowercase);
    return lowercase;
}
@end
    

//交换
Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSString class], @selector(eoc_myLowercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);

上边一段代码可以在将NSString转换成小写的时候打印日志。这里并没有任何关于NSString的lowercaseString函数的知识,就可以给它添加功能。

通常方法的交换在一个类的+load方法中,确保在使用前已经调换好,并且要使用dispatch_once保证线程安全。

method swizzling可以用于统一的打点统计,输出DEBUG信息等。有一种设计模式叫做面向切片编程,就是和主业务垂直的辅助业务,可以通过在程序架构中某一层注入的方式来解除耦合。method swizzling是面向切片编程的武器之一。

第14条 理解“类对象”的用意

在这里插入图片描述

上图是一个书中的例子,一个SomeClass的类继承自NSObject,其类对象和元类对象结构如图。

一个类的信息(如类的实例方法都有啥,实例变量都有啥等)保存在“类对象”的实例中,每一个类都有一个类对象,并且是单例的。类对象是Class类型,也是一个OC类,它也有信息,保存Class类的信息的对象叫做元类对象,元类对象的数据已经很固定,就没有自己的类对象了。最终继承体系闭合在NSObject的元类对象上。

isKindOfClass判断一个实例是否在一个继承体系上,isMemberOfClass精确的判断了这个对象是否是某个类的实例。

以下两种写法有什么区别?

[obj isKindOfClass: XXXClass];
[obj class] == [XXXClass class];

1)如果obj是XXXClass的子类实例,第一个表达式返回YES,第二个返回NO。

2)如果obj是一个Proxy,继承自NSProxy的,也就是通过消息转发方式代理了其他对象,有可能两个表达式返回的值不同。具体情况看isKindOfClass的转发情况。

猜你喜欢

转载自blog.csdn.net/Q52077987/article/details/82893614