6、理解“屬性”這一概念
Objective-C面向對象語言編程。
對象就是“基本構造單元”,開發者用對象存儲并傳遞數據。
對象之間傳遞數據并且執行任務的過程就叫做“消息傳遞”
程序運行起來。相關支持的代碼就叫做“Objective-C runtime”。“屬性”是Objective-C的一項特性,用于封裝對象中的數據。
Objective-C對象會把數據保存為各種實例變量。
實例變量通過“存取方法”(accessmethod)訪問。
getter 用于讀取變量值
setter用于寫入變量值@property
對象接口的定義中,可以使用屬性。 -> 標準的寫法
編譯器會自動寫出一套存取方法,用以訪問給定類型中具有給定名稱的變量。
@property (nonatomic ,copy) NSString *nameString;
等同于下面的這種寫法
- (void)setNameString:(NSString *)nameString;
- (NSString *)nameString;
-
屬性優勢:
- 編譯器自動編寫與訪問這些屬性所需的方法 =“自動合成”(autosynthesis)
這個過程是由編譯器在編譯時期執行的,所以編輯器看不到這些“合成方法”的源代碼。 - 除了生成方法代碼之外,編譯器還會自動向類中添加適當類型的實例變量,并且添加下劃線作為實例變量的名字。
比如上面的名稱就為_nameString。
- 編譯器自動編寫與訪問這些屬性所需的方法 =“自動合成”(autosynthesis)
-
自己實現存取方法
- 使用@dynamic關鍵字 。告訴編譯器不讓自動創建實例變量、存取方法!
-
屬性特質
- 原子性
默認情況下。編譯器合成的方法通過通過鎖定機制確實其原子性(atomicity)。
如果屬性具備nonatomic特質,則不使用同步鎖。 - 讀/寫權限
readwrite(讀寫):具有setter和getter方法!
readonly(只讀):僅有getter方法。 - 內存管理
assign:針對CGFloat或者NSInteger等“純量類型”(基礎數據類型 和C數據類型)簡單賦值,不更改索引計算。
strong:為屬性設置新值時,保留新值,釋放舊值,再將新值設置上去。
weak:既不保留新值,也不釋放舊值,同assign。
copy:與strong類似,但是并不保留新值。而是將其“拷貝”。
經典案列:NSString 確保對象中的字符串值不會無意間改動。
- 原子性
7、在對象內部盡量直接訪問實例變量
- 使_XXX直接訪問實例變量。
- 速度快。不經過Objective-C的“方法派發”(method dispatch),編譯器直接訪問保存對象實例變量的那塊內存
- 直接訪問實例變量,不調取“設置方法”(setter)這就繞過了內存管理語義。
- 直接訪問實例變量,也不會觸發“鍵值觀測”(Key-Value Observing, KVO)
這樣做是否有問題還是取決于具體的對象行為。
- 使用self.XXX來訪問
- 有助于排查與之相關的錯誤,因為可以給“setter”和“getter”設置斷點
- 懶加載也是需要通過“獲取方法”來訪問屬性。否則。實例變量永遠不會初始化。
- 建議:
除特殊情況。
在讀取實例變量的時候采用直接訪問的形式,
在設置實例變量的時候通過屬性來做。
8、理解“對象等同性”這一概念
等同性(equality)
一般情況下相比較我們都是用的 “==” 但是比較出來的結果未必是我們想要的。
因為“==”比較是指針本身,而不是其對象。-
NSObject中的“isEqual”
- 判斷兩個對象的等同性。
- NSObject協議中2個判斷等同性的關鍵方法
- (BOOL)isEqual:(id)object; @property (readonly) NSUInteger hash;
定義:如果“isEqual:”方法判斷兩個對象相等,那么其hash方法也必須返回同一個值。
如果兩個對象的hash方法返回同一個值,“isEqual”未必會認為兩者相等。
NSString實現了一個獨有的等同性判斷方法:isEqualToString
該方法比“isEqual”快,因為該方法快遞對象規定為NSString。而“isEqual”還要執行額外步驟,因為“isEqual”不知道受測對象類型。NSArray與NSDictionary也有類似的特殊的等同性方法。
“isEqualToArray”與“isEqualToDictionary”
如果檢測到受測對象不是數組或者字典就會拋出異常。-
自己實現等同性方法原理:
- 首先,判斷兩個指針是否相等。相等則說明指向同一對象!
- 其次,比較兩個對象所屬的類(考慮到父類與子類的判斷)
- 然后,檢測每個屬性是否相等(不要盲目逐步檢查每條屬性,而是根據需求來定制)
- 最后,實現hash方法:
等同性約定:若兩個對象相等,則哈希碼相等,但是兩個哈希碼相同的對象卻未必相等。(應使用計算速度快而且哈希碼碰撞幾率低的算法,否則會影響性能)
-
小技巧:
- 如果要重寫“isEqual”方法。
如果受測參數與接收消息對象屬于同一個類,就調用自己寫的判定方法。否則就交給超類來判斷。 - 等同性的判斷深度。
如果判斷兩個對象的所有屬性是否相等,這樣的叫“深度等同性判定”。
不過更多時候是根據其中部分數據即可判斷二者是否等同。
- 如果要重寫“isEqual”方法。
9、以“類族模式”隱藏實現細節
- 類族:把實現細節隱藏在一套簡單的公共接口后面。
- 例子:
- 系統框架中有很多類族。
比如UIButton創建的時候的類方法
UIButton *button = [UIButton buttonWithType:<#(UIButtonType)#>]
這樣該方法返回的對象,決定傳入的按鈕類型。 - NSArray與可變類型NSMutableArray。
有兩個抽象基類,一個不可變數組,一個可變數組。 - 動手創建一個類族:
- 需求:一個公司分為2種人:1、管理者。2、工人。分別干不同的事!
- 創建一個基于NSObject的類
#import <Foundation/Foundation.h>
typedef NS_ENUM(NSUInteger, SCUserType) {
SCUserTypeManager,
SCUserTypeWorker,
};
@interface SCUser : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
//創建
+ (SCUser *)scUserWithType:(SCUserType)type;
//做事情
- (void)doWork;
@end
- .m的實現:
#import "SCUser.h"
#import "SCUserWorker.h"
#import "SCUserManager.h"
@implementation SCUser
+ (SCUser *)scUserWithType:(SCUserType)type {
switch (type) {
case SCUserTypeManager:
return [SCUserManager new];
break;
case SCUserTypeWorker:
return [SCUserWorker new];
break;
}
}
- (void)doWork {
}
@end
每個“實體子類” 都是從基類繼承來的。比如:
#import "SCUser.h"
@interface SCUserManager : SCUser
@end
//.m的實現
#import "SCUserManager.h"
@implementation SCUserManager
- (void)doWork {
NSLog(@"管理者巡邏");
}
@end
10、在既有類中使用關聯對象存放自定義數據
-
關聯對象(Associated Object)
為了解決某些情況(無法從對象所屬的類中繼承一個子類,然后用子類對象存放相關信息)
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
此方法可以給定的鍵和策略為某對象設置關聯對象值objc_getAssociatedObject(id object, const void *key)
此方法根據給定的鍵從某個對象中獲取相關對象值
objc_removeAssociatedObjects(id object)此方法移除指定對象的全部關聯對象。
項目中遇到的問題(runtime)
當時是為了解決NSURL中有個URLWithString的方法會默認把帶中文的鏈接給轉碼。因為程序中很多地方都用到了這個方法,很明顯,一個一個的修改是很費力氣的。用到了runtime中的一個交換方法:
自己實現一個函數,然后與系統的函數交換一下。完美解決問題。
主要代碼:
#import <objc/runtime.h>
@implementation NSURL (Unicode)
+ (void)load {
/*
self:UIImage
誰的事情,誰開頭 1.發送消息(對象:objc) 2.注冊方法(方法編號:sel) 3.交互方法(方法:method) 4.獲取方法(類:class)
Method:方法名
獲取方法,方法保存到類
Class:獲取哪個類方法
SEL:獲取哪個方法
imageName
*/
// 獲取imageName:方法的地址
Method URLWithStringMethod = class_getClassMethod(self, @selector(URLWithString:));
// 獲取wg_imageWithName:方法的地址
Method sc_URLWithStringMethod = class_getClassMethod(self, @selector(sc_URLWithString:));
// 交換方法地址,相當于交換實現方式2
method_exchangeImplementations(URLWithStringMethod, sc_URLWithStringMethod);
}
+ (NSURL *)sc_URLWithString:(NSString *)URLString {
NSString *newURLString = [self IsChinese:URLString];
return [NSURL sc_URLWithString:newURLString];
}
11、理解 objc_msgSend 的作用
-
Objective-C中給對象發消息
[object message:parameter];
原理:
objc_msgSend(id self, SEL cmd,...)
核心函數,這個是“參數個數可變的函數”,能接收兩個或者兩個以上的參數。
第一個參數代表接收者。第二個參數代表方法的名字。
上面的OC代碼換成函數就為:
objc_msgSend(object, @selector(message:),parameter);
objc_msgSend函數根據接收者和方法名來調用適當的方法。
過程:
1.先到所屬的類尋找“方法列表”,找到就跳轉
2.找不到,就會沿著繼承體系繼續向上查找,找到再跳轉。
3.實在找不到就執行“消息轉發”
看起來調用一個方法需要很多步驟。但是objc_msgSend會將匹配結果緩存在“快速映射表”里。這樣子執行就快了。 其他的方法:
// 待發送消息返回結構體
objc_msgSend_stret
//消息返回的是浮點數
objc_msgSend_fpret
//給超類發消息 例如[super XXX];
objc_msgSendSuper
大家碼代碼時期能更多的了解一些底層的工作原理。在調試的時候會幫助你很多。
12、理解消息轉發機制
- 消息轉發:
因為Objective-C中,在編譯期向類發送無法解讀的消息并不會報錯,因為在運行期還可以繼續向類添加方法。因此,編譯器在編譯時無法確定類中到底會不會有某個方法實現。
當對象接受到無法解讀的消息后,就會啟動“消息轉發”機制,程序員可以由此告訴對象如何處理未知的消息。
大家在開發期間肯定見過這樣的錯誤:
unrecognized selector sent to instance 0x610000026560
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[SCUserWorker sss]: unrecognized selector sent to instance 0x610000026560'
錯誤原因:是因為接收者無法理解sss的這個方法名,因此致使程序崩潰。
- 對象在接收到無法解讀的消息后,會依次調用下列方法。
+ (BOOL)resolveInstanceMethod:(SEL)sel
在這個方法中,參數就是未知的方法名稱。在這里你可以解決問題。
- (id)forwardingTargetForSelector:(SEL)aSelector
這個是備援接收者,也就是給接收者第二次處理的機會,如果可以找到備援對象則將其返回,若找不到就返回nil。 - 完整的消息轉發
- (void)forwardInvocation:(NSInvocation *)anInvocation
啟動完整的消息轉發機制,首先要創建 NSInvocation 對象,把尚未處理的消息相關的細節全部封與其中(方法名,目標以及參數)。
使用:只需改變調用目標,使消息在新目標上得以調用就好了,和第二種方法“備援接收者”等效。
-
案列:使用class_addMethod動態添加方法
假設我故意一個類只在.h聲明了方法 沒有在.m中實現該方法
結果就會報上面的錯,接收者無法解讀消息。讓你用runtime動態添加方法你會怎么辦呢?- 考慮
原因是因為沒有實現該方法,所以無法解讀,那么我們要為其添加方法。
那么這個方法添加到哪呢?該如何添加? - 動手
首先找到沒有實現方法的那個類,在其.m添加
+ (BOOL)resolveInstanceMethod:(SEL)sel
這個方法。上述講到過,無法解讀消息時會第一時間調用這個方法,我們可以在這來解決問題。
接下來要用到runtime中的 class_addMethod 方法
class_addMethod(__unsafe_unretained Class cls, SEL name, IMP imp, const char *types)
1.Class cls : 參數表示添加新方法的類
2.SEL name : 表示方法名稱
3.IMP imp : 表示由編譯器生成的、指向實現方法的指針。也就是說,這個指針指向的方法就是我們要添加的方法。
4.const char **types :最后一個參數 *types 表示我們要添加的方法的返回值和參數。
主要代碼在下面:
eat:只聲明沒有實現的方法。
SCUser:創建的類。
- 考慮
C語言函數的實現
記得導入
#import <objc/runtime.h>
void sayHello (id self,SEL _cmd) {
NSLog(@"Hello");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(eat)) {
class_addMethod([SCUser class], sel, (IMP)sayHello, "v@:@");
return YES;
}
return [super resolveInstanceMethod:sel];
};
- OC形式的實現
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(eat)) {
class_addMethod([SCUser class], sel, class_getMethodImplementation(self, @selector(sayHello)), "v@:@");
return YES;
}
return [super resolveInstanceMethod:sel];
};
- (void)sayHello {
NSLog(@"hahaha");
}
13、用“方法調配技術”調試“黑盒方法”
- 在第10條中有介紹過交換方法的案列。
- 交換方法:
- 在運行期,向類中新增或者替換方法。
- 使用另一個方法替換一個方法可以向其添加新功能。
- 只有調試程序中才會用的運行期修改方法,不宜濫用。
14、理解“類對象”的用意
- 看下面的代碼
NSString *nameStirng = @"小伙子";
可以理解:nameString 為存放內存地址的變量。而NSString自身的數據就存在地址中。所有的Objective-C對象都是如此。 - 還可以這樣寫:
id nicknameStirng = @"牛";
對于通用的對象類型id,因為其自身已經是指針了。所以可以這樣寫。 - 比較
兩者語法意義相同,
唯一區別:如果聲明的時候指定了具體類型,那么在該類實例上調用沒有的方法時,編譯器會發出警告信息。 - id的定義
typedef struct object {
Class isa;
} *id;
說明每個對象結構體的首個成員是Class類的變量。通常稱為“isa”指針。
- metaclass
metaclass就是isa指向的一個結構體。 - 舉例
用一個例子說明:
大學期間,小明的輔導員要調查小明家里有沒有黨員。
首先,輔導員通過身份證號找到小明的檔案,發現小明不是黨員,從小明的檔案中發現小明父母的身份證號,通過小明父母的身份證號找到小明父母的檔案,發現小明父母都是黨員。
身份證號 = isa ,檔案 = metaclass。
(這是作者自己的粗淺理解,如果不對,歡迎指出) - 檢測繼承體系
- isKindOfClass
isKindOfClass來確定一個對象是否是一個類的成員,或者是派生自該類的成員。 - isMemberOfClass
isMemberOfClass只能確定一個對象是否是當前類的成員。
- isKindOfClass
接下來也將會繼續整理。如果覺得有用請點個喜歡!
您的支持將是我繼續寫作的動力!謝謝。
觀“編寫高質量iOS與OC X代碼的52個有效方法”有感(一)· 熟悉Objective-C
觀“編寫高質量iOS與OC X代碼的52個有效方法”有感(二)· 對象、消息、運行時
觀“編寫高質量iOS與OC X代碼的52個有效方法”有感(三)· 接口與API設計
觀“編寫高質量iOS與OC X代碼的52個有效方法”有感(四)· 協議與分類