IOS-閱讀Effective Objective-C 20的記錄(一)

這本書總共7章,準(zhǔn)備分3次記錄
前2章說的是熟悉OC,對象、消息、運行時

第一章:了解Objective

  • 1、 Objectve-C是運行時,是使用面向?qū)ο髣討B(tài)綁定的消息結(jié)構(gòu),每次調(diào)用其實都是發(fā)送消息

  • 2、 在類的頭文件盡量少引入其他頭文件
    隨著項目的迭代,可能會造成"循環(huán)引用",這樣子會使編譯的時間大大增加。怎么辦:多使用向前聲明(forward declaring),包括協(xié)議也可以這樣子寫。
    例子:創(chuàng)建2個繼承NSObject的Person類和Dog類,如果只希望在Person類中引入Dog類而不是其他的,那么可以使用向前聲明-即在編譯到Dog類的時候才會發(fā)送消息給Dog類。而不是直接#import "Dog.h"

#import <Foundation/Foundation.h>
//#import "Dog.h" //Person 的實現(xiàn)文件則需要引入Dog.h文件,需要知道Dog所有接口細節(jié)。

@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,那么就不會再獲取后面的元素。所以字面量語法能夠保證每次的元素都是更為安全的。字典同理。但是字面量語法所創(chuàng)建的對象必須屬于Foundation框架才行。
  • 4、多用類型常量,少用#define預(yù)處理指令
    define預(yù)處理指令#define ANIMATION_DURATION 0.3
    類型常量:static const NSTimeInterval kAnimationDuration = 1;
    理由:類型常量定義的對象含有對象類型,預(yù)處理指令命名不夠清晰,并且可能會替代掉相同類型名。而且他們都不應(yīng)該被定義成公共變量,因為當(dāng)然import的時候會連同公共變量一起導(dǎo)入,隨著版本的迭代會造成命名混亂,指代不清晰。注意:在.m里用static const NSTimeInterval ClassNameAnimationDuration = 1,在.h里用extern constNSTimeInterval ClassNameAnimationDuration
  • 5、用枚舉表示狀態(tài)、選項、狀態(tài)碼
    注意,用枚舉+switch的時候處理判斷的時候。switch不要實現(xiàn)default分支了,因為編譯器會提示開發(fā)者:switch語句并未處理所有枚舉
typedef NS_ENUM(NSUInteger,ClassNameConnectionState) {
    ClassNameConnectionStateDisconnected,//0
    ClassNameConnectionStateConnecting,//1
    ClassNameConnectionStateConnected,//2
};

第二章:對象、消息、運行時

在OC中,開發(fā)者可以通過對象來儲存并傳遞數(shù)據(jù),在對象之間傳遞數(shù)據(jù)并執(zhí)行任務(wù)的過程叫做"消息傳遞"(Messaging)。程序運行起來后,為其提供支持的是OC runtime

  • 5、屬性

編譯器根據(jù)名稱自動創(chuàng)建出存(setter)取(getter)方法。這里的copy,assign,retain,strong,weak就放在后面的內(nèi)存管理一起記錄

@interface Person : NSObject
@property (nonatomic,readwrite,copy)NSString *name;
//用property創(chuàng)建后編譯器會自動生成非同步鎖,可讀可寫的copy特質(zhì)的如下2個方法
- (NSString *)name; //readonly
- (void)setName:(NSString *)name;//writeonly
@end

我們在訪問的時候可以使用"點語法"

Person *person = [Person new];
person.name = @"nike"; //Same as;
[person setName:@"nike"];
  • 6、理解“屬性"這一概念

屬性用于封裝對象中的數(shù)據(jù),OC對象通常會把其所需要的數(shù)據(jù)保存為各種實例變量。而實例變量一般通過存取方法來訪問。getter用于讀取變量,setter用于寫入變量。
用屬性的好處就是不論在編譯期還是運行期,系統(tǒng)能夠正確的使實例變量的偏移量正確的指向其實例變量。甚至可以在運行期增加實例變量。
例:

@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

訪問屬性,可以使用點語法。編譯器會把點語法轉(zhuǎn)換為對存取方法的調(diào)用:

aPerson.firstName = @"Bob"; // Same as:
[aPerson setFirstName:@"Bob"];

NSString *lastName = aPerson.lastName; // Same as:
NSString *lastName = [aPerson lastName];

如果我們不希望編譯器自動生成存取方法的話,也不要創(chuàng)建存取方法。需要設(shè)置@dynamic 字段:

@interface EOCPerson : NSManagedObject

@property NSString *firstName;
@property NSString *lastName;

@end

@implementation EOCPerson
@dynamic firstName, lastName; //_firstName變成了firstName
@end

內(nèi)存管理
assign:純量類型(scalar type)的簡單賦值操作
strong:擁有關(guān)系保留新值,釋放舊值,再設(shè)置新值
weak:非擁有關(guān)系(nonowning relationship),不保留新值不釋放舊值,屬性所指的對象遭到摧毀時,屬性也會清空
unsafe_unretained :類似assign,適用于對象類型,非擁有關(guān)系,屬性所指的對象遭到摧毀時,屬性不會清空。
copy:不保留新值,而是將其拷貝,NSString * 用此保護其封裝性。因為NSString可能指向一個NSMutableString,所以為了保證對象中的字符串值不會無意間變動,需要設(shè)置Copy

  • 7、在對象內(nèi)部盡量直接訪問實例變量

關(guān)于實例變量的訪問,可以直接訪問,(_InstanceName)也可以通過屬性的方式(點語法)來訪問。書中作者建議在讀取實例變量時采用直接訪問的形式,而在設(shè)置實例變量的時候通過屬性來做。

  • 直接訪問屬性的特點:
    繞過set,get語義,速度快;
    不能觸發(fā)KVO,通知
  • 通過屬性訪問屬性的特點:
    不會繞過屬性定義的內(nèi)存管理語義(不能設(shè)置strong或者copy等類型)
    有助于打斷點排查錯誤
    可以觸發(fā)KVO

因此,有個關(guān)于折中的方案:

設(shè)置屬性:通過屬性
讀取屬性:直接訪問

不過有兩個特例:

  • 1、初始化方法和dealloc方法中,需要直接訪問實例變量來進行設(shè)置屬性操作。因為如果在這里沒有繞過set方法,就有可能觸發(fā)其他不必要的操作。
  • 2、 惰性初始化(lazy initialization)的屬性,必須通過屬性來讀取數(shù)據(jù)。因為惰性初始化是通過重寫get方法來初始化實例變量的,如果不通過屬性來讀取該實例變量,那么這個實例變量就永遠不會被初始化。
  • 8、理解“對象等同性”概念

按照 == 操作符比較出來的是指針,而非指針?biāo)傅膶ο螅瑧?yīng)該使用isEqual方法來判斷等同性。

NSString *foo = @"Badger 123";
NSString *bar = [NSString stringWithFormat:@"Badger %i",123];
//比較指針,而非指針?biāo)傅膶ο骹oo與bar,foo與bar指的不同的對象
BOOL equalA = (foo == bar);  // equalA = NO
//比較類型以及內(nèi)容
BOOL equalB = [foo isEqual:bar];  // equalA = YES
//僅能判斷string類型,調(diào)用方法比isEqual快 
BOOL equalC = [foo isEqualToString:bar];  // equalA = YES

指針相同,那么所指的內(nèi)容一定相同。內(nèi)容相同,所指的指針不一定相同
判斷的時候優(yōu)先判斷指針(==),再判斷類型Class,最后判斷內(nèi)容
如果我們有以下這個類

@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;
    
//判斷類型里的所有內(nèi)容是否相同,如果有一個不同,那么他們就不是一個類
    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和設(shè)置為NSDictionary的key時會調(diào)用,因為用的少就不做介紹了

  • 9 、以"類族模式"隱藏實現(xiàn)細節(jié)
    API典型例子。你可以通過傳入不同的Type直接創(chuàng)建不同的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創(chuàng)建不同的EOCEmployee

#import <Foundation/Foundation.h>

//這里必須要在父類里導(dǎo)入子類才能創(chuàng)建子類 這個太蠢了 所以用向前聲明
@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里實現(xiàn)

#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

創(chuàng)建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、在既有類中使用關(guān)聯(lián)對象存放自定義數(shù)據(jù)
    感覺沒什么卵用啊
  • 11、理解objc_msgSend的作用

OC的調(diào)用方法稱傳遞消息,消息與名稱或者選擇子,可以接受參數(shù),可有返回值。
然而對象收到 消息后,究竟該調(diào)用哪個方法則完全于運行期決定,甚至可以在程序運行時改變,這些特性使得OC成為一門真正的動態(tài)語言。

在OC中,給對象發(fā)送消息的語法是:
id returnValue = [someObject messageName:parameter];

這里,someObject叫做“接收者(receiver)”,messageName:叫做"選擇子(selector)",選擇子和參數(shù)合起來稱為“消息”。編譯器看到此消息后,將其轉(zhuǎn)換為一條標(biāo)準(zhǔn)的C語言函數(shù)調(diào)用,所調(diào)用的函數(shù)乃是消息傳遞機制中的核心函數(shù)叫做objc_msgSend,它的原型如下:

void objc_msgSend(id self, SEL cmd, ...)

第一個參數(shù)代表接收者,第二個參數(shù)代表選擇子,后續(xù)參數(shù)就是消息中的那些參數(shù),數(shù)量是可變的,所以這個函數(shù)就是參數(shù)個數(shù)可變的函數(shù)。

因此,上述以O(shè)C形式展現(xiàn)出來的函數(shù)就會轉(zhuǎn)化成如下函數(shù):

id returnValue = objc_msgSend(someObject,@selector(messageName:),parameter);

這個函數(shù)會在接收者所屬的類中搜尋其“方法列表”,如果能找到與選擇子名稱相符的方法,就去實現(xiàn)代碼,如果找不到就沿著繼承體系繼續(xù)向上查找。如果找到了就執(zhí)行,如果最終還是找不到,就執(zhí)行消息轉(zhuǎn)發(fā)操作。

注意:如果匹配成功的話,這種匹配的結(jié)果會緩存在“快速映射表”里面。每個類都有這樣一塊緩存。所以如果將來再次向該類發(fā)送形同的消息,執(zhí)行速度就會更快了。

  • 12、理解消息轉(zhuǎn)發(fā)機制

如果類和其所有的父類無法立即響應(yīng)某個選擇子,那么就會啟動消息轉(zhuǎn)發(fā)流程(message forwarding)。是一種OC設(shè)計的防止找不到選擇器而導(dǎo)致程序崩潰的debug方式

如果

//這個方法是給Person類發(fā)送run的消息
[Person run] 

但是報了如下錯誤 : unrecognized selector sent to instance
這是因為,當(dāng)run這個方法只有定義沒有實現(xiàn)會怎么樣呢

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Person run]: unrecognized selector sent to instance

接下來重現(xiàn)[Person run]方法的調(diào)用過程

首先,該方法在調(diào)用時,系統(tǒng)會查看這個對象能否接收這個消息(查看這個類有沒有這個方法,或者有沒有實現(xiàn)這個方法。),如果不能并且只在不能的情況下,就會調(diào)用下面這幾個方法,給你“補救”的機會,你可以先理解為幾套防止程序crash的備選方案,我們就是利用這幾個方案進行消息轉(zhuǎn)發(fā),注意一點,前一套方案實現(xiàn)后一套方法就不會執(zhí)行。如果這幾套方案你都沒有做處理,那么程序就會報錯crash。

方案一:動態(tài)方法解析
 +(BOOL)resolveInstanceMethod:(SEL)sel 或者
 +(BOOL)resolveClassMethod:(SEL)sel
方案二:消息轉(zhuǎn)發(fā)重定向
-(id)forwardingTargetForSelector:(SEL)aSelector
方案三:
//生成方法簽名
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
// 拿到簽名轉(zhuǎn)發(fā)消息
-(void)forwardInvocation:(NSInvocation *)anInvocation;
消息轉(zhuǎn)發(fā)邏輯圖

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 如果沒有實現(xiàn)run方法,那么就會走到該方法,這里的操作是動態(tài)添加run方法,保護程序不崩潰
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    NSLog(@"sel = %@",NSStringFromSelector(sel));
    //1.判斷沒有實現(xiàn)方法, 那么我們就是動態(tài)添加一個方法
    if (sel == @selector(run:)) {
        class_addMethod(self, sel, (IMP)newRun, "v@:@:");
        return YES;
    }
    //不做任何操作直接調(diào)用父類
    return [super resolveInstanceMethod:sel];
}
void newRun(id self,SEL sel,NSString *str) {
    NSLog(@"%@",self);
    NSLog(@"%s",sel_getName(sel));
    NSLog(@"---runok---%@",str);
}

//方案2  這個方法返回你需要轉(zhuǎn)發(fā)消息的對象。這里將run:轉(zhuǎn)發(fā)給Car里的run:
- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s",__func__);
    //將消息轉(zhuǎn)發(fā)給Car類里的run:選擇器
    return [[Car alloc] init];
    不做任何操作直接調(diào)用父類
    //return [super forwardingTargetForSelector:aSelector];
}
/*
 關(guān)于生成簽名的類型"v@:"解釋一下。每一個方法會默認(rèn)隱藏兩個參數(shù),self、_cmd,self代表方法調(diào)用者,_cmd代表這個方法的SEL,簽名類型就是用來描述這個方法的返回值、參數(shù)的,v代表返回值為void,@表示self,:表示_cmd。
 開頭我們要找的錯誤unrecognized selector sent to instance原因,原來就是因為methodSignatureForSelector這個方法中,由于沒有找到run對應(yīng)的實現(xiàn)方法,所以返回了一個空的方法簽名,最終導(dǎo)致程序報錯崩潰。
 所以我們需要做的是自己新建方法簽名,再在forwardInvocation中用你要轉(zhuǎn)發(fā)的那個對象調(diào)用這個對應(yīng)的簽名,這樣也實現(xiàn)了消息轉(zhuǎn)發(fā)。
 */
//方案3
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector OBJC_SWIFT_UNAVAILABLE(""){
    //轉(zhuǎn)化字符
    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];
    //轉(zhuǎn)發(fā)
    Car *c = [[Car alloc] init];
    if([c respondsToSelector:seletor]){
        //喚醒對象,進行轉(zhuǎn)發(fā)
        [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"];----->調(diào)用至此處
- (void)run:(NSString *)time{
    NSLog(@"%s",__func__);
    NSLog(@"%@",time);
}
//消息不會轉(zhuǎn)發(fā)到這 正常調(diào)用會
- (void)study:(NSString *)time{
    NSLog(@"%s",__func__);
    NSLog(@"%@",time);
}
@end

  • 13、用"方法調(diào)配技術(shù)(method swizzling)"調(diào)試"黑盒方法" ,可以交換系統(tǒng)方法(runtime:method_exchangeImplemetations(originalMethod,swappendMethod))
    即在OC的運行期改變其選擇器的方法而不通過繼承或者重寫
+ (void)load {
    //方法交換應(yīng)該被保證,在程序中只會執(zhí)行一次
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        //獲得viewController的生命周期方法的selector
        //在一個子視圖將要被添加到另一個視圖的時候會調(diào)用此方法
        SEL systemSel = @selector(willMoveToSuperview:);
        //自己實現(xiàn)的將要被交換的方法的selector
        SEL swizzSel = @selector(myWillMoveToSuperview:);
        //兩個方法的Method 也可以是類方法
        Method systemMethod = class_getInstanceMethod([self class], systemSel);
        Method swizzMethod = class_getInstanceMethod([self class], swizzSel);
        
        //首先動態(tài)添加方法,實現(xiàn)是被交換的方法,返回值表示添加成功還是失敗
        BOOL isAdd = class_addMethod(self, systemSel, method_getImplementation(swizzMethod), method_getTypeEncoding(swizzMethod));
        if (isAdd) {
            //如果成功,說明類中不存在這個方法的實現(xiàn)
            //替換類中已有方法的實現(xiàn),如果該方法不存在添加該方法
            //將被交換方法的實現(xiàn)替換到這個并不存在的實現(xiàn)
            class_replaceMethod(self, swizzSel, method_getImplementation(systemMethod), method_getTypeEncoding(systemMethod));
        } else {
            //否則,交換兩個方法的實現(xiàn)
            method_exchangeImplementations(systemMethod, swizzMethod);
        }
        
    });
}

- (void)myWillMoveToSuperview:(UIView *)newSuperview {
    //調(diào)用myWillMoveToSuperview等于調(diào)用系統(tǒng)方法
    [self myWillMoveToSuperview:newSuperview];

    if (self) {
        if (self.tag == 10086) {
            self.textColor = [UIColor redColor];
        }
    }
    
}

14、理解"類對象"的用意
isMenberOfClass 判斷出對象是否為某個特定類的實例
isKindOfClass 判斷出對象是否為某類或其派生類的實例

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容