《EffectiveObjective-c 2.0》第二章 对象

2017-07-26  本文已影响0人  神的旨意

第6条:理解"属性"这一概念

@interface WCPerson : NSObject {
    @public
    NSString *_firstName;
    NSString *_lastName;
    @private
    NSString *_someInternalData;
}
@end
@interface WCPerson : NSObject {
    @public
    NSDate *_dateOfBirth;
    NSString *_firstName;
    NSString *_lastName;
    @private
    NSString *_someInternalData;
}
@end

原来表示 _firstName 的偏移量现在却指向了 _dateOfBirth 了,那么 _firstName 之后的实例变量的偏移量硬编码都会读取到错误的值,对比一下,在加入 _dateOfBirth 这一实例变量之前于之后的内存布局情况,假设指针为4个字节。

WX20170726-142309.png
如果代码使用了编译期计算出来的偏移量,那么在修改类定义之后必须重新编译,否则会出错。
Objective-C的做法是,把实例变量当做一种存储偏移量所用的“特殊变量”,交由类对象“(class object)保管。偏移量会在运行期查找,如果类的定义变了,那么存储的偏移量也就变了,这样的话,无论何时访问实例变量,总能使用正确的偏移量。可以在运行期向类中新增实例变量,这就是"稳固的应用二进制接口"。
@interface WCPerson : NSObject
//声明变量为firstName的属性,特质为nonatomic和copy
@property (nonatomic,copy)NSString *firstName;
@end

在Objective-c的编译期间会变成如下代码,

//WCPerson.h 文件
@interface WCPerson : NSObject
//声明变量为firstName的属性,特质为nonatomic和copy
@property (nonatomic,copy)NSString *firstName;
 - (NSString *)firstName;
 - (void)setFirstName:(NSString *)firstName;
@end

 //WCPerson.m 文件
 #import "WCPerson.h"
 @interface WCPerson (){
     NSString *_firstName;//默认情况下,属性对应的实例变量
 }
 @end

 @implementation WCPerson
  //编译期间,编译器自动创建的getter方法
  - (NSString *)firstName{
     return _firstName;
 }
//编译期间,编译器自动创建的setter方法
 - (void)setFirstName:(NSString *)firstName{
     _firstName = [firstName copy];
 }
 @end

使用 @property 声明的属性,编译器在编译期做如下操作:

  1. 创建一个以 _ 为前缀的实例变量名。
  2. 创建标准的 getter,setter 方法。
    也可以使用 @synthesize 设置自定义的实例变量
@implementation WCPerson
@synthesize firstName = _myFirstName;
@end

也可以使用 @dynamic 告诉编译器:不创建属性所对应的实例变量,也不要创建存取方法。

@implementation WCPerson
 @dynamic firstName;
@end
//声明变量为firstName的属性,特质为nonatomic和copy
@property (nonatomic,copy)NSString *firstName;

 //firstName属性的特质为copy,那么在自定义setter方法时,应该具有copy特性
  - (void)setFirstName:(NSString *)firstName{
      _firstName = [firstName copy];//这里应该满足声明的copy特性
  }

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

在.m文件中,当给属性设置值时,使用self.name=@"123",这种方式,当读取值当时候,使用_name这种方式,原因如下:

WX20170726-165910.png
当然也不是绝对的,也要视情况而定。

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

    NSString *foo = @"123";
    NSString *bar = [NSString stringWithFormat:@"123"];
    BOOL equalA = (foo == bar);//. NO
    BOOL equalB = [foo isEqual:bar];// YES
    BOOL equalC = [foo isEqualToString:bar];//YES

字符串有自己独特的等同性判断方法 isEqualToString,且效率比 isEqual 高。

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

NSObject类对这两个方法等默认实现是:当且仅当其“指针值”完全相等时,这两个对象才相等。要想在自定义对象中正确覆写这些方法,就必须先理解其约定。如果 isEqual 方法判定两个对象相等,那么hash方法耶必须返回同一个值,但是,如果两个对象的hash方法返回同一个值,那么 isEqual 方法未必会认为两者相等。

hash方法与判等的关系?

hash方法主要是用于在Hash Table查询成员用的, 那么和我们要讨论的isEqual()有什么关系呢?
为了优化判等的效率, 基于hash的NSSet和NSDictionary在判断成员是否相等时, 会这样做
Step 1: 集成成员的hash值是否和目标hash值相等, 如果相同进入Step 2, 如果不等, 直接判断不相等
Step 2: hash值相同(即Step 1)的情况下, 再进行对象判等, 作为判等的结果
简单地说就是
hash值是对象判等的必要非充分条件

@interface WCPerson : NSObject
//声明变量为firstName的属性,特质为nonatomic和copy
@property (nonatomic,copy)NSString *firstName;
@property (nonatomic,copy)NSString *lastName;
@property (nonatomic,assign)NSUInteger age;
@end

@implementation WCPerson
- (BOOL)isEqual:(id)object{
    if (self == object) {//指针相同
        return YES;
    }
    if (![object isKindOfClass:[WCPerson class]]) {//同一类型
        return NO;
    }
    return [self isEqualToPerson:(WCPerson *)object];
}

- (BOOL)isEqualToPerson:(WCPerson *)person{//判断属性相同
    if (!person) {
        return NO;
    }
    BOOL haveEqualFirstNames = (!_firstName && !person.firstName) || [_firstName isEqualToString:person.firstName];
    BOOL haveEqualLastNames = (!_lastName && !person.lastName) || [_lastName isEqualToString:person.lastName];
    BOOL haveEqualAge = _age == person.age;
    return haveEqualFirstNames && haveEqualLastNames && haveEqualAge;
}
- (NSUInteger)hash{
    NSUInteger firstNameHash = [_firstName hash];
    NSUInteger lastNameHash = [_lastName hash];
//    NSLog(@"firstNameHash is %ld,lastNameHash is %ld",firstNameHash,lastNameHash);
    return firstNameHash ^ lastNameHash ^_age;//按位异或运算符
}
@end

按位异或运算符参考
isEqualhash 参考
总之:

WX20170728-145939.png

第9条:以“类族模式”隐藏实现细节

本条部分参考该作者内容

类族模式在UIKit(user interface framework)使用的范围已经远远超过我们的想象,比如,UIButton,NSArray,NSString等,这种模式最大的好处就是,可以隐藏抽象基类背后的复杂细节,使用者只需调用基类简单的方法就可以返回不同(或相同)的子类(或类)实例。

先看一下可以验证这个问题的方法:

- (BOOL)isKindOfClass:(Class)aClass;
- (BOOL)isMemberOfClass:(Class)aClass;

第一个函数的意思是:接收者是否是aClass类的实例或者从这个类继承的任何类的实例。如果是返回yes。
第二个函数的意思是:接收者是否是aClass的实例,如果是返回yes。

UIButton 测试

    //UIButton 的类族模式,返回的都是UIButton类
    UIButton *btnCust = [UIButton buttonWithType:UIButtonTypeCustom];
    UIButton *btnSys = [UIButton buttonWithType:UIButtonTypeSystem];
    UIButton *btnDark = [UIButton buttonWithType:UIButtonTypeInfoDark];
    NSLog(@"%@",[[btnCust class] debugDescription]);
    NSLog(@"%@",[[btnSys class] debugDescription]);
    NSLog(@"%@",[[btnDark class] debugDescription]);
    //UIButton返回的是基类实例对象,下面两个if语句都会输出
    if ([btnCust isKindOfClass:[UIButton class]]) {
        NSLog(@"isKindOfClass");
    }
    if ([btnCust isMemberOfClass:[UIButton class]]) {
        NSLog(@"isMemberOfClass");
    }

//输出如下:
2017-07-28 17:18:56.053 test[10739:166312] UIButton
2017-07-28 17:18:56.053 test[10739:166312] UIButton
2017-07-28 17:18:56.053 test[10739:166312] UIButton
2017-07-28 17:34:16.167 test[10819:172000] isKindOfClass
2017-07-28 17:34:16.167 test[10819:172000] isMemberOfClass

说明,虽然UIButton根据不同的类型(UIButtonType)创建,但返回的都是同一个类的实例对象。
其内部实现应该如下:

- (void)drawRect:(CGRect)rect{
    if (_type == TypeA) {//_type为UIButtonType,TypeA为客户传入的类型
        //draw TypeA button
    }else if (_type == TypeB){
        //draw TypeB button
    }
}

NSArray测试

    NSArray*array = [NSArray new];//返回__NSArray0
    NSLog(@"%@",[[array class] description]);
    NSLog(@"%@", [[[NSArray arrayWithObject:@"1"] class] description]);//返回__NSSingleObjectArrayI
    NSLog(@"%@", [[[NSArray arrayWithObjects:@"1", @"2", nil] class] description]);//返回__NSArrayI,在数组元素个数大于等于2时,返回同一个对象结果
    NSLog(@"%@", [[[NSArray arrayWithObjects:@"1", @"2", @"3", nil] class] description]);
    NSLog(@"%@", [[[NSArray arrayWithObjects:@"1", @"2", @"3", @"4", nil] class] description]);
    NSLog(@"%@", [[[NSArray arrayWithObjects:@"1", @"2", @"3", @"4", @"5", nil] class] description]);
    if ([array isKindOfClass:[NSArray class]]) {
        NSLog(@"isKindOfClass");
    }
    if ([array isMemberOfClass:[NSArray class]]) {
        NSLog(@"isMemberOfClass");
    }

输出结果如下:
2017-07-28 17:45:19.344 test[10870:176145] __NSArray0
2017-07-28 17:45:19.345 test[10870:176145] __NSSingleObjectArrayI
2017-07-28 17:45:19.345 test[10870:176145] __NSArrayI
2017-07-28 17:45:19.345 test[10870:176145] __NSArrayI
2017-07-28 17:45:19.345 test[10870:176145] __NSArrayI
2017-07-28 17:45:19.346 test[10870:176145] __NSArrayI
2017-07-28 17:45:19.346 test[10870:176145] isKindOfClass

NSArray的内部实现应该如下:

// .h 文件
typedef NS_ENUM(NSUInteger,LULEmployeeType) {
    LULEmployeeTypeDevlopers,
    LULEmployeeTypeProducters,
    LULEmployeeTypeTesters
};

@interface LULEmployee : NSObject
+(LULEmployee*)employeeWithType:  (LULEmployeeType)type;
-(void)doADayWork;
@end

//.m 文件

+(LULEmployee*)employeeWithType:  (LULEmployeeType)type{
    switch (type) {//类似Java中的工厂模式,Objective-C中也大量使用了该模式。面试题会被问到iOS使用到的经典工厂模式是什么对象
        case LULEmployeeTypeDevlopers:
            return [LULEmployeeTypeDevloper new];
            break;
        case LULEmployeeTypeProducters:
            return [LULEmployeeTypeProducter new];
            break; 
        case LULEmployeeTypeTesters:
            return [LULEmployeeTypeTester new];
            break;
    }
}

由上可以知道返回的每一个对象都是 NSArray 基类的子类的实例对象,而非 NSArray 的实例对象,所以如下代码永远不会执行

if ([array isMemberOfClass:[NSArray class]]) {
        NSLog(@"isMemberOfClass");
    }

集合元素都类似,包括,NSDictionary等。
所以对于这类(UIButton,NSArray, NSDictionary)对象判断其类型的时候,如下两种方式都不合适,

//UIButton 可以使用此方式
if ([array isMemberOfClass:[NSArray class]]) {
        NSLog(@"isMemberOfClass");
    }
if ([array class] == [NSArray class]]) {//永远不会执行
        NSLog(@"isMemberOfClass");
    }

这样做的意义:

UIButton类的使用者无需关心创建出来的按钮怒提属于哪个子类,也不用考虑按钮的绘制方式等实现细节,使用者只需明白如何创建按钮即可

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

//set
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
//get
objc_getAssociatedObject(id object, const void *key)
//remove 
void objc_removeAssociatedObjects(id object)

第11条:理解objc_msgSend的作用

  1. Objective-C 调用方法其内部其实就是消息派发,如:
id returnValue = [someObject messageName:parameter];
//底层调用方法如下
id returnValue = objc_msgSend(someObject,@selector(messageName:),parameter);
  1. “尾调用优化”,即某函数的最后一项操作是调用另外一个函数,那么就可以运用“尾调用优化”技术,编译器惠生产调转至另一个函数所需的指令码,而且不会向调用堆栈中推入新的“栈帧”。只有当莫函数的最后一个操作仅仅是调用其他函数而不会将其他返回值另作他用时,才执行“尾调用优化”。这优化对objc_msgSend非常关键,如果不这么做的话,那么每次调用OC方法之前,都需要为调用objc_msgSend函数准备“栈帧”。
    引用
    尾调用之所以与其他调用不同,就在于它的特殊的调用位置。
    我们知道,函数调用会在内存形成一个"调用记录",又称"调用帧"(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用记录上方,还会形成一个B的调用记录。等到B运行结束,将结果返回到A,B的调用记录才会消失。如果函数B内部还调用函数C,那就还有一个C的调用记录栈,以此类推。所有的调用记录,就形成一个"调用栈"(call stack)。
    参考

第12条:理解消息转发机制

  1. 当某个属性用 @dynamic 实现时,需要用消息转发机制。先逻辑图:
    WX20170801-154931.png
  2. 上代码
    WCAutoDictionary.h 文件
@interface WCAutoDictionary : NSObject
@property (nonatomic, strong) NSString *string;
@property (nonatomic, strong) NSNumber *number;
@property (nonatomic, strong) NSDate *date;
@property (nonatomic, strong) id opaqueObject;
@end

WCAutoDictionary.m 文件

#import "WCAutoDictionary.h"
#import <objc/runtime.h>

@interface WCAutoDictionary ()
@property (nonatomic, strong) NSMutableDictionary *backingStore;

@end
@implementation WCAutoDictionary
@dynamic string, number, date, opaqueObject;

- (instancetype)init{
    if (self = [super init]) {
        _backingStore = [NSMutableDictionary new];
    }
    return self;
}

//第二步 备援接收者
- (id)forwardingTargetForSelector:(SEL)aSelector{
    //可以"组合"来模拟出"多重继承"的某些特性,在一个对象内部,
    //可能还有一些列其他对象,
    //该对象可经由此方法将能够处理某选择子的相关内部对象返回,
    //这样的话,在外界看来,好像是该对象亲自处理了这些消息似的(此处不可修改消息内容)
   //可以交给其他类处理
    return nil;//转向第三步
}
//第三步
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    
}

//第一步
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    NSString *selectorString = NSStringFromSelector(sel);
    if ([selectorString hasPrefix:@"set"]) {
        class_addMethod(self, sel, (IMP)autoDictionarySetter, "v@:@");
    }else{
        class_addMethod(self, sel, (IMP)autoDictionaryGetter, "@@:");
    }
    return YES;
}

id autoDictionaryGetter(id self, SEL _cmd){
    //Get the backing store from the object
    WCAutoDictionary *typedSelf = (WCAutoDictionary *)self;
    NSMutableDictionary *backingStore = typedSelf.backingStore;
    
    NSString *key = NSStringFromSelector(_cmd);
    
    return [backingStore objectForKey:key];
}

void autoDictionarySetter(id self, SEL _cmd, id value){
    WCAutoDictionary *typeSelf = (WCAutoDictionary *)self;
    NSMutableDictionary *backingStore = typeSelf.backingStore;
    //setOpakeqObject:
    NSString *selectorString = NSStringFromSelector(_cmd);
    NSMutableString *key = [selectorString mutableCopy];
    
    [key deleteCharactersInRange:NSMakeRange(key.length - 1, 1)];
    
    [key deleteCharactersInRange:NSMakeRange(0, 3)];
    
    NSString *lowercaseFirstChar = [[key substringToIndex:1] lowercaseString];
    [key replaceCharactersInRange:NSMakeRange(0, 1) withString:lowercaseFirstChar];
    
    if (value) {
        [backingStore setObject:value forKey:key];
    }else{
        [backingStore removeObjectForKey:key];
    }
}

@end

调用方法

    WCAutoDictionary *dict = [WCAutoDictionary new];
    //dict.date赋值 触发+ (BOOL)resolveInstanceMethod:(SEL)sel方法
    dict.date = [NSDate dateWithTimeIntervalSince1970:123134123];
    //dict.date取值 触发+ (BOOL)resolveInstanceMethod:(SEL)sel方法
    NSLog(@"dict.date = %@",dict.date);

文章总结的很好,直接拿来用


WX20170801-160321.png

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

    Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
    Method swappedmethod = class_getInstanceMethod([NSString class], @selector(uppercaseString));
    method_exchangeImplementations(originalMethod, swappedmethod);

    NSString *string = @"ThIS iS my iOS";
    NSLog(@"lowercaseString = %@",[string lowercaseString]);
    NSLog(@"uppercaseString = %@",[string uppercaseString]);

输出: 
2017-08-01 17:21:17.837 test[3562:131744] lowercaseString = THIS IS MY IOS
2017-08-01 17:21:17.837 test[3562:131744] uppercaseString = this is my ios

使用 method_exchangeImplementations 可以调换实现的方法。
当然也可以有助于调试,当需要调试系统中的某个方法的时候,如下:
.h 文件

@interface NSString (WCMyAdditions)
- (NSString *)eoc_myLowerCaseString;
@end

.m 文件

@implementation NSString (WCMyAdditions)
//该方法调和后,是@selector(lowercaseString)的实现方法
- (NSString *)eoc_myLowerCaseString{
    NSString *lowercase = [self eoc_myLowerCaseString];//调和之后,该方法返回的是原来系统方法 lowercaseString的实现
    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 *string = @"ThIS iS my iOS";
    [string lowercaseString];
输出:
2017-08-01 17:24:23.549 test[3597:133219] ThIS iS my iOS => this is my ios

此方式方便于调试,不可滥用。

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

  1. 每个Objective-C对象实例都是指向某块内存数据的指针。OC 声明对象实例的时候都是如下做法
NSString *string = @"ThIS iS my iOS";
id temp = @"123";

而类的结构如下

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    const char *name                                         OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

即每个类都有isa指针和super_class父类。

NSString *string = @"ThIS iS my iOS";
//lldb 输出
(lldb) p string
(__NSCFConstantString *) $0 = 0x000000010f3b7430 @"ThIS iS my iOS" //string 对象真正属于__NSCFConstantString 类
(lldb) p string->isa //证明上一步
(Class) $1 = __NSCFConstantString
(lldb) p [string superclass]
(Class) $2 = __NSCFString
(lldb) p [[string superclass] superclass]
(Class) $3 = NSMutableString
(lldb) p [[[string superclass] superclass] superclass]
(Class) $4 = NSString
////到此处可以得知__NSCFConstantString是NSString的子类,所以我们经常使用到的NSString类其实是NSString的子类__NSCFConstantString,此处对应于前面讲的第九条,[类族模式]
(lldb) p [[[[string superclass] superclass] superclass] superclass]
(Class) $5 = NSObject
(lldb) p [[[[[string superclass] superclass] superclass] superclass] superclass]
(Class) $6 = nil  //NSObject的父类是nil
(lldb) 

下图是思维导图


WX20170802-161732.png
  1. 在类继承体系中查询类型信息,即使用
//isMemberOfClass判定步骤:1. 通过实例对象的isa指针得到实例所属的类
//2. 之后再用 == 判断实例所属的类和比较的类是否为同一类,返回判断结果
isMemberOfClass:  // 判断对象是否为某个特定类实例
//isKindOfClass判定步骤:
//1. 同isMemberOfClass步骤一
//2. 同isMemberOfClass步骤二,但此处如果返回结果是false,
//则在使用实例对象的super_class,找到实例对象的父类,在用 == 进行比较,如果还是false,
//则在找实例对象的super_class,继续比较,直到找到位置,如果使用实例对象的super_class查找到根类为NSObject还是没有比较成功的话,
//那么就返回false.
isKindOfClass:    //判断对象是否为某类或其派生类的实例
WX20170802-164143.png
上图很好的解释了,isa指针和super_class在代码中的使用。
为什么步直接使用 == 而代替 isMemberOfClass:isKindOfClass:(当然 isKindOfClass:是不能代替的) 呢?
书中的解释,看着很难理解:
WX20170802-170320.png
代码解释吧 参考
//YFStudent.h文件
@interface YFStudent : NSObject
//这里没有提供公开的方法和属性哦
@end

//YFStudent.m文件
@interface YFStudent ()
@property (nonatomic,copy)NSString *studentNum;
@end

@implementation YFStudent

- (void)study{
    NSLog(@"正在学习");
}
@end
//YFPerson.h 文件
@interface YFPerson : NSObject
//这里没有提供公开的方法和属性哦
@end
//YFPerson.m 文件
@interface YFPerson ()
@property (nonatomic,copy)NSString *name;
@end

@implementation YFPerson

- (void)eat{
    NSLog(@"%@正在吃饭",self.name);
}
@end
//YFProxy.h 文件   NSProxy和NSObject是同一级别的
@interface YFProxy : NSProxy
- (void)transformToObject:(NSObject *)object;
@end

//YFProxy.m 文件
@interface YFProxy ()
@property (nonatomic, strong) NSObject *object;
@end

@implementation YFProxy

- (void)transformToObject:(NSObject *)object{
    self.object = object;
}
//第一步,方法签名, sel为要执行的方法的@selector()选择器
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
    NSMethodSignature *methodSignature;
    if (self.object) {
        methodSignature = [self.object methodSignatureForSelector:sel];
    }else{
        methodSignature = [super methodSignatureForSelector:sel];
    }
    return methodSignature;
}

- (void)forwardInvocation:(NSInvocation *)invocation{
    if (self.object) {
        [invocation setTarget:self.object];//设置调用的目标
        [invocation invoke];//开始调用方法
    }
}
@end

调用

    YFPerson *person = [[YFPerson alloc] init];
    YFStudent *student = [[YFStudent alloc] init];
    YFProxy *proxy = [YFProxy alloc];
    [proxy transformToObject:person];
    if ([proxy isKindOfClass:[YFPerson class]]) {//此处如果用==的话,则会为false,lldb调试如下:
/**
(lldb) p proxy->isa
(Class) $0 = YFProxy
(lldb) p [proxy superclass]
(Class) $1 = NSProxy
*/
//如果使用isKindOfClass:这个的话,那么会走proxy的方法签名methodSignatureForSelector 
//跟调用[person isKindOfClass:[YFPerson class]] 就类似了,
//所以作者推崇使用isKindOfClass:做类型信息查询
        NSLog(@"true");
    }
    //尽管YFPerson没有提供公开的方法和属性但仍然可以调用该对象的name属性和eat方法
    [proxy performSelector:@selector(setName:) withObject:@"小明"];
    [proxy performSelector:@selector(eat)];
    
    [proxy transformToObject:student];
    [proxy performSelector:@selector(study)];

以上代码实现一下就明白了。

点击进入 第三章

上一篇下一篇

猜你喜欢

热点阅读