胡 坤, 特日根
(長光衛(wèi)星技術(shù)有限公司 a. 數(shù)據(jù)中心; b. 吉林省衛(wèi)星遙感應(yīng)用技術(shù)重點(diǎn)實(shí)驗(yàn)室, 長春 130000)
iOS的編程語言objective-C是對(duì)C語言的擴(kuò)展, 加入了面向?qū)ο蠛拖鬟f機(jī)制, 其函數(shù)的調(diào)用過程是動(dòng)態(tài)的[1], 在編譯時(shí)并不能決定真正調(diào)用的函數(shù), 只有在真正運(yùn)行時(shí)才會(huì)根據(jù)函數(shù)的名稱找到對(duì)應(yīng)的函數(shù)實(shí)體進(jìn)行調(diào)用。而這個(gè)消息傳遞機(jī)制的核心是由C語言和匯編語言編寫的Runtime庫完成的[2], 它是objective-C語言面向?qū)ο蠛蛣?dòng)態(tài)機(jī)制的核心[3]。
Runtime的核心是消息傳遞, 高級(jí)編程語言想要成為可執(zhí)行文件需要先編譯為匯編語言再匯編為機(jī)器語言。機(jī)器語言也是計(jì)算機(jī)能識(shí)別的唯一語言, 但objective-C并不能直接編譯為匯編語言, 而是要先轉(zhuǎn)寫為純C語言再進(jìn)行編譯和匯編操作, 從objective-C到C語言的過渡就是由Runtime實(shí)現(xiàn)的[4]。然而C語言是面向過程的編程語言, 而使用objective-C進(jìn)行面向?qū)ο箝_發(fā)時(shí), 就需要將面向?qū)ο蟮念愞D(zhuǎn)變?yōu)槊嫦蜻^程的結(jié)構(gòu)體實(shí)現(xiàn)[5]。
筆者針對(duì)實(shí)際編程開發(fā)中無法修改系統(tǒng)方法的問題, 首先研究Runtime的主要API(Application Programming Interface)接口以及消息傳遞的本質(zhì), 然后使用恰當(dāng)?shù)慕涌诤瘮?shù)對(duì)系統(tǒng)方法動(dòng)態(tài)的修改和替換, 從而提高了開發(fā)效率, 降低程序運(yùn)行故障率。
在objective-C語言中, 實(shí)例對(duì)象在調(diào)用方法時(shí), 編譯器在編譯階段不知道調(diào)用方法的具體位置, 只有在運(yùn)行時(shí), 才會(huì)向?qū)嵗龑?duì)象發(fā)送消息, 并通過遍歷實(shí)例對(duì)象進(jìn)行方法選擇。這種機(jī)制稱為消息機(jī)制。Runtime的特性主要是消息傳遞, 若消息在對(duì)象中找不到, 則進(jìn)行消息轉(zhuǎn)發(fā)。
對(duì)一個(gè)對(duì)象的普通方法[object func], 編譯器會(huì)轉(zhuǎn)成消息并發(fā)送objc_msgSend(object,func), 其中objc_msgSend方法定義如下。
定義1 OBJC_EXPORT id objc_msgSend(id self,SEL op,...)
Runtime消息的傳遞過程為:
1) 系統(tǒng)首先找到消息的接收對(duì)象, 然后通過對(duì)象的isa指針找到它的類;
2) 在它的類中遍歷method_list, 尋找func方法;
3) 若沒有func方法則查找父類的method_list;
4) 找到對(duì)應(yīng)的method, 執(zhí)行其IMP;
5) 轉(zhuǎn)發(fā)IMP的return值。
以下6個(gè)小節(jié)為消息傳遞中的概念。
1.1.1類對(duì)象(objc_class)
Objective-C類是由Class類型表示的, 它實(shí)際上是一個(gè)指向objc_class結(jié)構(gòu)體的指針。其中objc_class方法定義如下。
定義2 typedef struct objc_class*Class;
struct objc_class {
Class _Nonnull isa;
#if!__OBJC2__
Class_Nullable super_class;
const char*_Nonnull name;
long version;
long info;
long instance_size;
struct objc_ivar_list*_Nullable ivars;
struct objc_method_list*_Nullable*_Nullable methodLists;
struct objc_cache*_Nonnull cache;
struct objc_protocol_list*_Nullable protocols;
#endif
}
該結(jié)構(gòu)體的第1個(gè)成員變量也是isa指針, 這說明了Class本身其實(shí)也是一個(gè)對(duì)象, 因此稱之為類對(duì)象。類對(duì)象在編譯期產(chǎn)生用于創(chuàng)建實(shí)例的對(duì)象, 在全局中只有一個(gè), 這個(gè)唯一的實(shí)例, 稱之為單例。
1.1.2 實(shí)例(objc_object)
類對(duì)象中存儲(chǔ)了諸多信息, 這些信息描述如何創(chuàng)建一個(gè)實(shí)例。類對(duì)象和類方法應(yīng)該從isa指針指向的結(jié)構(gòu)體創(chuàng)建, 類對(duì)象的isa指針的指向?qū)ο蠓Q之為元類(metaclass), 元類中保存了創(chuàng)建類對(duì)象以及類方法的所有信息。其中實(shí)例objc_object的方法定義如下。
定義3 struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
ypedef struct objc_object *id;
1.1.3方法(objc_method)
方法(Method)和人們平時(shí)理解的函數(shù)是一致的, 即表示能獨(dú)立完成一個(gè)功能的一段代碼。方法(Method)定義如下。
定義4 typedef struct objc_method *Method;
struct objc_method {
SEL method_name
char *method_types
IMP method_imp
}
在這個(gè)結(jié)構(gòu)體中, SEL和IMP都是Method的屬性。
1.1.4 SEL(objc_selector)
方法選擇器SEL定義如下。
定義5 typedef struct objc_selector *SEL;
定義1中提及的消息發(fā)送函數(shù)objc_msgSend的第2個(gè)參數(shù)類型為SEL, 它是selector在Objective-C中的表示類型。selector是方法選擇器, 可以理解為區(qū)分方法的ID, 而這個(gè)ID的數(shù)據(jù)結(jié)構(gòu)是SEL, SEL定義為: @property SEL selector??梢钥吹絪elector是SEL的一個(gè)實(shí)例。
1.1.5 IMP函數(shù)實(shí)現(xiàn)
函數(shù)實(shí)現(xiàn)IMP定義如下。
定義6 typedef id (*IMP)(id,SEL,...);
#endif
IMP就是指向程序內(nèi)存地址的指針。在iOS的Runtime中, Method通過selector和IMP兩個(gè)屬性, 實(shí)現(xiàn)了快速查詢方法及實(shí)現(xiàn), 相對(duì)提高了性能, 又保持了靈活性。
1.1.6 Category(objc_category)
Category是表示一個(gè)指向分類結(jié)構(gòu)體的指針, 其定義如下。
定義7 struct category_t {
const char*name;
classref_t cls;
struct method_list_t*instanceMethods;
struct method_list_t*classMethods;
struct protocol_list_t*protocols;
struct property_list_t*instanceProperties;
};
從上面的category_t結(jié)構(gòu)體中可以看出, 分類中可以添加實(shí)例方法、 類方法, 甚至可以實(shí)現(xiàn)協(xié)議, 添加屬性, 但不可以添加成員變量。
當(dāng)有消息發(fā)送時(shí), 系統(tǒng)會(huì)在相關(guān)類對(duì)象的方法列表中搜索所需方法, 若找不到則會(huì)沿著繼承樹向上搜索, 若搜索到繼承樹根部(通常為NSObject)時(shí)仍未找到, 則此消息轉(zhuǎn)發(fā)失敗, 并通過執(zhí)行“doesNotRecognizeSelector:” 方法向系統(tǒng)報(bào)錯(cuò), 其錯(cuò)誤提示為“unrecognized selector”。
1) 動(dòng)態(tài)方法解析(resolveInstanceMethod)。首先, objective-C運(yùn)行時(shí)會(huì)調(diào)用“+resolveInstanceMethod:”, 使之有機(jī)會(huì)提供一個(gè)函數(shù)實(shí)現(xiàn)。若添加了函數(shù)并返回YES, 系統(tǒng)就會(huì)重新啟動(dòng)一次消息發(fā)送過程。
2) 備用接收者(備用receiver)。若方法“+resolveInstanceMethod:”返回為NO, 而且目標(biāo)對(duì)象實(shí)現(xiàn)了“-forwardingTargetForSelector:”方法, 此時(shí)Runtime就會(huì)調(diào)用這個(gè)方法, 并把這個(gè)消息轉(zhuǎn)發(fā)給備用接收者receiver。
3) 完整消息轉(zhuǎn)發(fā)。若在上一步還不能處理未知消息, 則唯一能做的就是啟用完整的消息轉(zhuǎn)發(fā)機(jī)制。首先它會(huì)發(fā)送“-methodSignatureForSelector:”消息獲得函數(shù)的參數(shù)和返回值類型。若“-methodSignatureForSelector:”返回“nil” , Runtime則會(huì)發(fā)出“-doesNotRecognizeSelector:”消息, 此時(shí)程序執(zhí)行完畢。若返回了一個(gè)函數(shù)簽名(函數(shù)簽名就是函數(shù)的聲明信息, 包括參數(shù)、 返回值、 調(diào)用約定), Runtime就會(huì)創(chuàng)建一個(gè)NSInvocation對(duì)象并發(fā)送“-forwardInvocation:”消息給目標(biāo)對(duì)象。
圖1 消息轉(zhuǎn)發(fā)流程Fig.1 Message forwarding process
完整轉(zhuǎn)發(fā)的代碼實(shí)現(xiàn)以及運(yùn)行打印結(jié)果如圖2所示。
圖2 消息轉(zhuǎn)發(fā)代碼及打印Fig.2 Message forwarding code and printing
KVO(Key-Value Observing)是蘋果提供的一套事件通知機(jī)制。允許對(duì)象監(jiān)聽另一個(gè)對(duì)象特定屬性的改變, 并在改變時(shí)接收到事件。在經(jīng)典的MVC設(shè)計(jì)模式中, 經(jīng)常用于在Model和Controller之間進(jìn)行通訊。
KVO實(shí)現(xiàn)依賴于Objective-C 的Runtime機(jī)制, 當(dāng)觀察某對(duì)象A時(shí), Runtime為對(duì)象A動(dòng)態(tài)創(chuàng)建一個(gè)子類, 并為這個(gè)新的子類重寫了被觀察屬性keyPath的setter方法。setter 方法隨后負(fù)責(zé)通知觀察對(duì)象屬性的改變狀況。
Apple使用了isa-swizzling技術(shù)實(shí)現(xiàn)KVO 。當(dāng)觀察對(duì)象A時(shí), Runtime創(chuàng)建一個(gè)名為“NSKVONotifying_A”的新類, 該類繼承自對(duì)象A, 且Runtime為NSKVONotifying_A重寫觀察屬性的setter方法, NSKVONotifying_A的setter方法會(huì)負(fù)責(zé)在調(diào)用對(duì)象A的setter方法前后, 觀察對(duì)象A屬性值的更改情況[6-7]。
在筆者參與的某項(xiàng)目中大量使用KVO監(jiān)聽變量屬性的改變, 其中監(jiān)聽鍵盤改變的代碼如下。
//注冊(cè)鍵盤出現(xiàn)通知
[[NSNotificationCenter defaultCenter] addObserver:selfselector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
//注冊(cè)鍵盤消失通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:)
name:UIKeyboardWillHideNotification object:nil];
由于分類無法添加成員屬性, 但可通過Runtime的關(guān)聯(lián)對(duì)象進(jìn)行實(shí)現(xiàn)。Runtime的關(guān)聯(lián)對(duì)象提供了下面幾個(gè)接口。
1) 關(guān)聯(lián)對(duì)象接口:
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
2)獲取關(guān)聯(lián)對(duì)象接口:
id objc_getAssociatedObject(id object, const void *key)
3) 移除關(guān)聯(lián)對(duì)象接口:
void objc_removeAssociatedObjects(id object)
下例使用Runtime為UIView的分類動(dòng)態(tài)添加“defaultColor”的屬性。
圖3 分類添加屬性代碼及打印Fig.3 Add attribute code and print for classification
該替換方案是通過“class_getInstanceMethod”函數(shù)獲取實(shí)例方法(實(shí)例方法是類的實(shí)例才能調(diào)用的方法)的實(shí)現(xiàn), 通過“class_getClassMethod”獲取類方法(類方法只能類本身進(jìn)行調(diào)用)實(shí)現(xiàn), 并最終通過“method_exchangeImplementations”方法對(duì)兩個(gè)函數(shù)的實(shí)現(xiàn)進(jìn)行調(diào)換[8]。
2.3.1 適配舊項(xiàng)目的高系統(tǒng)版本圖片問題
在項(xiàng)目開發(fā)中, 針對(duì)不同的iOS版本, 顯示的圖片常常不同。若項(xiàng)目中圖片的獲取方法為[UIImage imageNamed:@“test.png”], 而區(qū)分不用版本所需圖片的方法是在圖片的名稱后面加上后綴“_系統(tǒng)版本號(hào)”, 則項(xiàng)目中有n處圖片需要做版本適配, 同時(shí)再有m個(gè)版本需要做適配, 則需要新增n×m個(gè)圖片選擇的代碼[9-10]。通過Runtime的Method Swizzling方式將自己的方法和系統(tǒng)的“[UIImage imageNamed:]”方法進(jìn)行替換, 進(jìn)而減少代碼量, 降低代碼維護(hù)成本[11]。其代碼如下。
#import〈objc/runtime.h〉
+(void)load {
Class class=[self class];
//獲取想要替換方法實(shí)現(xiàn)
Method originalMethod=
class_getInstanceMethod(class,imageNamed:);
//獲取自定義方法hkImageNamed實(shí)現(xiàn)
Method swizzledMethod=
class_getInstanceMethod(class,hkImageNamed:);
//通過method_exchangeImplementations函數(shù)進(jìn)行IMP替換
method_exchangeImplementations(originalMethod,
swizzledMethod);
}
2.3.2 NSMutableArray中插入空串報(bào)錯(cuò)問題
在NSMutableArray數(shù)組中, 經(jīng)常有插入空串報(bào)錯(cuò)情況, 編譯器報(bào)錯(cuò)為“[__NSSetM addObject:] object cannot be nil”。這個(gè)錯(cuò)誤是因?yàn)檎{(diào)用了NSMutableArray的“[arrayM addObject:nil]”方法造成的[12]。若依次在報(bào)錯(cuò)位置加上if-else語句進(jìn)行判斷, 將會(huì)增加代碼維護(hù)成本。針對(duì)該問題, 同樣可以用Runtime的Method Swizzling進(jìn)行方法替換, 代碼如下。
#import 〈objc/runtime.h〉
+(void)load {
Class class=[self class];
//獲取想要替換方法實(shí)現(xiàn)
Method originalMethod=
class_getInstanceMethod(class,addObject:);
//獲取自定義方法hkAddObject:實(shí)現(xiàn)
Method swizzledMethod=
class_getInstanceMethod(class,hkAddObject:);
//通過method_exchangeImplementations函數(shù)進(jìn)行IMP替換
method_exchangeImplementations(originalMethod, swizzledMethod);
}
2.3.3 數(shù)組越界問題
同樣, 數(shù)組越界問題也是項(xiàng)目中常見問題[13]。對(duì)數(shù)組[self.arrays objectAtIndex:int]和self.arrays[int], 若int的值大于arrays元素?cái)?shù)量self.arrays.count時(shí), 則會(huì)報(bào)iOS “reason: ***-[__NSArrayM objectAtIndex:]: index int beyond bounds [0..self.arrays.count-1]”錯(cuò)誤[14-15]。針對(duì)這樣問題, 同樣可以用Runtime的Method Swizzling進(jìn)行方法替換, 代碼如下。
#import 〈objc/runtime.h〉
+(void)load {
Class class=[self class];
//獲取想要替換方法實(shí)現(xiàn)
Method originalMethod=
class_getInstanceMethod(class,objectAtIndex:);
//獲取自定義方法hkObjectAtIndex:實(shí)現(xiàn)
Method swizzledMethod=
class_getInstanceMethod(class,hkObjectAtIndex:);
//通過method_exchangeImplementations函數(shù)進(jìn)行IMP替換
method_exchangeImplementations(originalMethod, swizzledMethod);
}
筆者首先對(duì)objective-C語言的運(yùn)行時(shí)機(jī)制Runtime進(jìn)行了系統(tǒng)剖析, 闡述了Runtime是一套基于C語言的底層API庫的集合, objective-C是消息機(jī)制, 其方法最終都轉(zhuǎn)化成消息的轉(zhuǎn)發(fā)過程, 并通過消息轉(zhuǎn)發(fā)流程圖, 完整地闡述了消息轉(zhuǎn)發(fā)全過程。
然后, 通過對(duì)KVO機(jī)制分析, 給分類(category)增加屬性方法, 驗(yàn)證了Runtime在代碼運(yùn)行過程中起到的作用。并針對(duì)項(xiàng)目中常見問題, 提出了基于Runtime方式的解決方案, 使代碼更加精簡, 降低了代碼編碼和維護(hù)成本, 并證明了該方法可解決系統(tǒng)自帶功能不足的問題。