對象 --- 在Objective-C等面向對象語言編程時,“對象”(object)就是“基本構造單元”,開發者可以通過對象來存儲并傳遞數據。
消息 --- 在對象之間傳遞數據并執行任務的過程就叫做“消息傳遞”(Messaging)
運行期 --- 當應用程序運行起來以后,為其提供相關支持的代碼叫做“Objective-C運行期環境”(Objectice-C runtime),它提供了一些使得對象之間能夠傳遞消息的重要函數,并且包含創建類實例所用的全部邏輯。
六、理解“屬性”這一概念
屬性(property)是Objective-C的一項特性,用戶封裝對象中的數據,iOS開發中最常用最方便的變量聲明方式,允許我們用點語法來訪問對象的實例變量。實質上類似如下
屬性 = 成員變量 + set方法 + get方法。
屬性特質
@property(nonatomic,readwrite,copy,setter=<name>) NSString *firstName;
原子性
如果屬性具備noatomic特質,則不使用同步鎖,iOS由于性能原因,都是使用noatomic的,MacOS則多用atomic,使用同步鎖。讀寫權限
擁有readwrite特質的屬性擁有獲取方法getter和設置方法setter。
擁有readonly特質的屬性僅擁有獲取方法。可以在.h頭文件中對外公開為只讀屬性,然后再.m的class-continuation分類中將其重新定義為讀寫屬性。-
內存管理語義
屬性用于封裝數據,而數據則要有“具體的所有權語義。內存管理語義這個特質僅會影響設置方法。assign 設置方法只會執行針對純量類型(非OC對象)的簡單賦值操作,如CGFloat,NSInteger
strong 定義了一種擁有關系。為這種屬性設置新值時,設置方法會先保留新值,并釋放舊值,然后再將新值設置上去。對象引用計數+1,功能等價于MRC里面的retain
weak 非擁有關系,為這種屬性設置新值時,既不保留新值,也不釋放舊值。然而在屬性值所指對象遭到摧毀時,屬性值也會被清空為nil。weak是為打破循環引用而生的
unsafe_unretained 類似weak,但是區別于在當目標對象遭到摧毀時,屬性值不會自動清空,這是不安全的。不建議使用!
-
copy 擁有關系,實質上是設置為傳入對象的copy對象的指針。設置完后,與傳入的對象無關聯。
@property (nonatomic, copy) NSString *stringCopy -(void)setStringCopy:(NSString *)stringCopy{ [_stringCopy release]; _stringCopy = [stringCopy copy]; }
當屬性類型為NSString *時,經常用此特質來保護其封裝性
-
方法名
@property(nonatomic,getter=isOn)Bool on; //getter=<name> 指定獲取方法的方法名 //setter=<name>,這種用法一般不常用,沒必要
特別注意:
如果想在其他方法里設置屬性值,那么同樣要遵守屬性定義中所宣稱的語義。如下代碼:
@interface EOCPerson:NSManagedObject
@property(copy,readonly) NSString *firstName;
@property(copy,readonly) NSString *lastName;
-(id)initWithFirstName:(NSString *)firstName lastName:(NSString*)lastName;
@end
//.m文件中
-(id)initWithFirstName:(NSString *)firstName lastName:(NSString*)lastName{
if((self = [super init])){
_firstName = [firstName copy];
_lastName = [lastName copy];
}
}
在實現這個自定義初始化方法時,一定要遵循屬性定義中宣稱的”copy“語義,因為屬性定義就相當于類和待設置的屬性值之間達成的契約。
七、在對象內部盡量直接訪問實例變量
- "A:在對象內部讀取數據時,應該直接通過實例變量來讀。B:寫入數據時,則應該通過屬性來寫。" 這種方案的優點是:既能提高讀取操作的速度,又能控制對屬性的寫入操作,使得相關屬性的內存管理語義得以貫徹。
- 針對1中A讀取內部數據的例外情況是,在使用懶加載初始化技術配置某個數據的話,應該通過屬性來讀取數據
- 針對1中B,寫入數據的例外情況是,初始化方法和dealloc方法中,總是應該直接通過實例變量來讀寫數據
八、理解"對象等同性"這一概念
根據“等同性”(equality)來比較對象是一個非常有用的功能。按照==操作符比較出來的結果未必是我們想要的,應為該操作比較的是兩個指針本身,而不是其所指的對象。 我們應該使用NSObject協議中聲明的"isEqual":方法來判斷兩個對象的等同性。
-(BOOL)isEqual:(id)object{
if(self == object)
return YES;
if([self class] != [object class])
return NO;
EOCPerson *otherPerson = (EOCPerson *)object;
if(![_firstName isEqualToString:otherPerson.firstName])
return NO;
if(![_lastName isEqualToString:otherPerson.lastName])
return NO;
if(_age != otherPerson.age)
return NO;
return YES;
}
-(NSUInterger)hash {
NSUInteger firstNameHash = [_firstName hash];
NSUInteger lastNameHash = [_lastName hash];
NSUInteger ageHash = _age;
return firstNameHash ^ lastNameHash ^ ageHash;
}
- 若想檢測對象的等同性,請提供“isEqual:”與hash方法
- 相同的對象必須具有相同的哈希碼,但是兩個哈希碼相同的對象卻未必相同
- 不要盲目地逐個檢測每條屬性,而是應該依照具體需求來定制檢測方案。
- 編寫hash方法時,應該使用計算速度快而哈希碼碰撞幾率低的算法
九、以“類族模式”隱藏實現細節
創建如NSArray類族的子類時,需要遵守幾條規則:
- 子類應該繼承自類族中的抽象基類
- 子類應該定義自己的數據存儲方式
- 子類應當覆寫超類文檔中指明需要覆寫的方法
要點:
- 類族模式可以把實現細節隱藏在一套簡單的公共接口后面
- 系統框架中經常使用類族
- 從類族的公共抽象基類中繼承子類時要當心,若有開發文檔,則應首先閱讀
十、在既有類中使用關聯對象存放自定義數據
“關聯對象”(Associated Object)是一個黑科技,在某些類無法繼承出子類來存放額外信息時,可以用關聯對象的方法,來增加鍵值對,達到給既定類存放額外信息的目的。
關聯類型 | 等效的@property屬性 |
---|---|
OBJC_ASSOCIATION_ASSIGN | assign |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | nonatomic,retain |
OBJC_ASSOCIATION_COPY_NONATOMIC | nonatomic,copy |
OBJC_ASSOCIATION_RETAIN | retain |
OBJC_ASSOCIATION_COPY | copy |
有以下方法管理關聯對象:
void objc_setAssociatedObject(id object,void *key,id value,objc_AssociationPolicy)
//該方法以給定的鍵和策略為某對象設置關聯對象值
id objc_getAssociatedObject(id object, void *key)
//此方法根據給定的鍵從某對象中獲取相應的關聯對象值
void objc_removeAssociatedObjects(id object)
//此方法移除指定對象的全部關聯對象
特別注意: 設置關聯對象時用的鍵key是個不透明的指針。設置關聯對象值時,通常使用靜態全局變量做鍵。
staic void *EOCMyALertViewKey = "EOCMyAlertViewKey";
//關聯對象,綁定block
objc_setAssociatedObject(alertView,EOCMyAlertViewKey,block,OBJC_ASSOCIATION_COPY);
//取值,取出block
void(^block)(NSInteger) = objc_getAssociatedObject(alertView,EOCMyAlertViewKey);
- 可以通過“關聯對象”機制來把兩個對象連起來
- 定義關聯對象時可以指定內存管理語義,用以模仿定義屬性時所采用的“擁有關系”與“非擁有關系”
- 只有在其他做法不可行時才應選用 關聯對象,因為這種做法通常會引入難以查找的bug,如循環引用
十一、理解objc_msgSend的作用
id returnValue = [someObject messageName:parameter];
someObject叫做"接收者"(receiver),messageName叫做“選擇子”(selector),選擇子與參數合起來稱為“消息”(message)。 編譯器看到此消息后,將其轉換為一條標準的c語言函數調用,所調用的函數乃是消息傳遞機制中的核心函數,叫做objc_msgSend,其原型如下
void objc_msgSend(id self,SEL cmd, ...)
編譯器會把上面例子中的消息轉換為如下函數:
id returnValue = objc_msgSend(someObject,@selector(messageName:),parameter);
objc_msgSend函數會依據接收者與選擇子的類型來調用適當的方法,為了完成此操作,該方法需要在接收者所屬的類中搜尋其“方法列表”(list of methods),如果能找到與選擇子名稱相符的方法,就跳至其實現代碼。若是找不到,那就沿著繼承體系繼續向上查找,等找到合適的方法再跳轉,如果最終還是找不到相符的方法,那就執行消息轉發(message forwarding)操作。
objc_msgSend會將匹配結果緩存在“快速映射表”(fast map)里面,每個類都有這樣一塊緩存,若是稍后還向該類發送與選擇子相同的消息,那么執行就會很快了。
Objective-C運行環境中的另一些函數
- objc_msgSend_stret 如果待發送的消息要返回結構體,就交由該函數處理
- objc_msgSend_fpret 如果消息返回的是浮點數,則交由此函數處理
- objc_msgSendSuper 如果要給超類發消息,例如[super message:parameter],那么就交由次函數處理
如果某函數的最后一項操作是調用另外一個函數,那么就可以運用“尾調用技術”,編譯器會生成調轉至另一函數所需的指令碼,而且不會向調用堆棧中推入新的棧幁。
要點:
- 消息由接收者、選擇子以及參數構成。給某對象“發送消息",也就相當于在該對象上"調用方法"
- 發給某對象的全部消息都要由"動態消息派發系統"來處理,該系統會查出對應的方法,并執行其代碼
十二、理解消息轉發機制
當對象接收無法解讀的消息后,就會啟動"消息轉發"(message forwarding)機制,程序員可經由此過程告訴對象應該如何處理未知消息。
消息轉發分為兩大階段:
- 第一階段先征詢接收者,所屬的類,看其是否能 動態添加方法,以處理當前這個"未知的選擇子",這叫做"動態方法解析"(dynamic method resolution)
- 第二階段涉及"完整的消息轉發機制"(full forwarding mechanism),細分為兩步:首先,請接收者看看有沒有其他對象能處理這條消息,若有,則運行期系統會把消息轉個那個對象,消息轉發過程結束。其次,若沒有備援的接收者(replacement receiver),則啟動完整的消息轉發機制,運行期系統會把與消息有關的全部細節都封裝到NSInvocation對象中,再給接收者最后一次機會,令其設法解決當前未處理的這條消息
動態方法解析
對象在收到無法解讀的消息后,首先調用其所屬類的下列方法:
+(BOOL)resolveInstanceMethod:(SEL)selector
該方法的參數就是那個未知的選擇子,令其返回值為Boolean類型,<u>表示這個類是否能新增一個實例方法用于處理此選擇子</u>。在繼續往下執行轉發機制之前,本類有機會新增一個處理此選擇子的方法。假如未實現的方法不是實例方法而是類方法,那么運行期系統就會調用另一個方法,叫做"resolveClassMethod:" 。
<u>使用動態方法解析的前提是:相關方法的實現代碼已經寫好,只等著運行的時候動態插在類里面就可以了</u>。 此方案常用來實現@dynamic屬性,比方說,要訪問CoreData框架中NSManagedObjects對象的屬性時就可以這么做,因為實現這些屬性所需的存取方法在編譯期就能確定。
id autoDictionaryGetter(id self,SEL _cmd);
void autoDictionarySetter(id self,SEL _cmd,id value);
+(BOOL)resolveInstanceMethod:(SEL)selector{
NSString *selectorStirng = NSStringFormSelector(selector);
if(/* selector is from a @dynamic property */){
if([selectorString hasPrefix:@"set"]){
class_addMethod(self,selector,(IMP)autoDictionarySetter,@"v@:@");
}else{
class_addMethod(self,selector,(IMP)autoDictionaryGetter,@"@@:"
}
return YES;
}
return [super resolveInstanceMethod:selector];
}
備援接收者
當前接收者還有第二次機會能處理未知的選擇子,在這一步中,運行期系統會問它:能不能把這條消息轉給其他接收者來處理。
-(id)forwardingTargetForSelector:(SEL)selector
方法參數代表未知的選擇子,若當前接收者能找到備援對象,則將其返回,否則就返回nil。
值得注意的是,我們無法操作經由這一步所轉發的消息,若是想在發送給備援接收者之前修改消息內容,那就得通過完整的消息轉發機制來做了。
完整的消息轉發
如果轉發算法已經來到這一步的話,那么唯一能做的就是啟用完整的消息轉發機制了。
首先創建NSInvocation對象,把尚未處理的那條消息有關的全部細節都封于其中。此對象包含選擇子、目標(target)及參數。在觸發NSInvocation對象時,"消息派發系統"(message-dispatch system)將親自出馬,把消息指派給目標對象。
-(void)forwardInvocation:(NSInvocation *)invocation
消息轉發全流程
要點
- 若對象無法響應某個選擇子,則進入消息轉發流程
- 通過運行期的動態方法解析功能,我們可以在需要用到某個方法時再將其加入類中
- 對象可以把其無法解讀的某些選擇子轉交給其他對象來處理
- 經過上述兩步之后,如果還是沒有辦法處理選擇子,那就啟動完整的消息轉發機制
十三、用"方法調配技術"
類的方法列表會把選擇子的名稱映射到相關的方法實現之上,使得"動態消息派發系統"能夠據此找到應該調用的方法.這些方法均以函數指針的形式來表示,這種指針叫做IMP。其原型如下:
id (*IMP)(id,SEL,...)
可以通過以下c函數互換兩個方法實現
void method_exchangeImplementations(Method m1,Method m2)
//此函數的兩個參數表示待交換的兩個方法實現,而方法實現則可通過下列函數獲得
Method class_getInstanceMethod(Class class,SEL aSelector)
//此函數根據給定的選擇子從類中取出與之相關的方法
可以用自定義的類中方法交換某個目標類中方法,如下:
@implement 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);
通過此方案,開發者可以為那些"完全不知道其具體實現的"黑盒方法增加日志記錄功能,這非常有助于程序調試,然后此做法只在調試的時候有用。若是濫用,會令代碼變得不易讀懂且難于維護。
要點
- 在運行期,可以向類中新增或替換選擇子所對應的方法實現
- 使用另一份實現來代替原有的方法實現,這道工序叫做"方法調配",開發者常用此技術向原有實現中添加新功能
- 一般來說,只有調試程序的時候才需要在運行期修改方法實現,這種方法不宜濫用
十四、理解"類對象"的用意
類型信息查詢: "在運行期檢視對象類型"這一操作叫做類型信息查詢(introspection,內省),這個強大而有用的特性內置于Foundation框架的NSObject協議里,凡是由公共根類(common root class,即NSObject與NSProxy)繼承而來的對象都要遵從此協議。
Class對象也定義在運行期程序庫的頭文件中:
typedef struct objc_class *Class;
struct objc_class{
Class isa;
Class superClass;
const char *name;
long version;
long info;
long instance_size;
struct objc_ivar_list *ivars;
struct objc_method_list **methodLists;
struct objc_cache *cache;
struct objc_protocol_list *protocols;
}
其中super_class 指針確立了繼承關系,而isa指針描述了實例所屬的類。
"isMemberOfClass:"能夠判斷出對象是否為某個特定類的實例;而"isKindOfClass:"則能夠判斷出是否為某類或其派生類的實例
要點:
- 每個實例都有一個指向Class對象的指針isa,用以表明其類型,而這些Class對象則構成了類的繼承體系
- 如果對象類型無法在編譯期確定,那么就應該使用類型信息查詢方法來探知
- 盡量使用類型信息查詢方法來確定對象類型,而不要直接比較類對象,因為某些對象可能實現了消息轉發功能