標籤:ios oc runtime
Objective-C語言是一門動態語言,它將很多靜態語言在編譯和連結時做的事放到了運行時來處理。同時OC也是一門簡單的語言,很大一部分是C的內容,只是在語言層面上加了關鍵字和文法,真正讓OC強大的是它的運行時,它很小卻很強大,其中核心是訊息分發。這種動態語言的優勢在於:我們寫代碼時更加靈活,如我們可以把訊息轉寄給我們想要的對象,或者隨意交換一個方法的實現。
這種特性意味著OC不僅需要一個編譯器,還需要一個運行時系統來執行編譯的代碼。對於OC來說,這個運行時系統就像一個作業系統一樣。這個運行時系統即Runtime,是用C和彙編寫的,這個庫使得C語言有了物件導向的能力。其中最主要的是訊息機制。對於C語言,函數的調用在編譯的會決定調用哪個函數,編譯完成之後順序執行,無任何二義性。OC的函數調用稱為訊息發送,屬於動態調用過程。在編譯的時候並不能決定真正的調用哪個函數(事實證明,在編譯階段,OC可以調用任何函數,即使這個函數並未實現,只要聲明過就不會報錯。而C語言在編譯階段就會報錯。)只有在真正啟動並執行時候才會根據函數的名稱找到對應的函數來調用。
用一句話說:我們編寫的OC代碼,程式運行過程中,其實都是轉化為runtime的C語言代碼,runtime算是OC幕後工作者。runtime屬於OC的底層,可以進行非常底層的操作(用OC是無法實現的。)
Runtime庫主要做下面幾件事:
1.封裝:在這個庫中,對象可以用C語言中的結構體表示,而方法可以用C函數來實現,另外再加上一些額外的特性。這些結構體和函數被Runtime函數封裝後,我們就可以在程式運行時建立、檢查、修改類、對象和他們的方法了。
2.找出方法的最終執行代碼:當程式執行[object doSomething]時,會向訊息接收者(object)發送一條訊息(doSomething),Runtime會根據訊息接收者是否能響應該訊息而做出不同的反應。
OC的Runtime目前有兩個版本:Modern Runtime和Legacy runtime. Modern Runtime主要用於64位應用。Legacy runtime主要用於32位應用。目前一般都是64位了。
關於執行效率問題,“靜態語言執行效率要比動態語言高”應該是沒問題的。因為一部分的CPU計算損耗在Runtime中。
那OC是怎麼實現動態調用的呢?假如在OC中寫了如下代碼:
[obj makeText];
其中obj是一個對象,makeText是一個函數名稱。執行一個方法,有些語言,編譯器會執行一些額外的最佳化和錯誤檢查,因為調用關係很直接也很明顯。但是對於訊息分發來說,就不那麼明顯了,在發訊息之前不必知道某個對象是否能夠處理訊息。你把訊息發給他,他可能會處理,也可能轉給其他的Object來處理。一個訊息不必對應一個方法,一個對象可能實現一個方法來處理多條訊息。
在編譯時間RunTime會將上述代碼轉化為:
objc_msgSend(obj,@selector(makeText));
所以其實
objc_msgSend(obj,@selector(makeText));和<pre name="code" class="plain">[obj makeText];
是等價的。
再比如:
[obj setTT:@"111" isOK:YES];
和
objc_msgSend(obj,@selector(setTT:isOK:),@"111",YES)
也是等價的。注意有參數的OC函數名的表達方式。
首先我們來看看obj這個對象,iOS中的obj都繼承與NSObject。
@interface NSObject{ Class isa OBJC_ISA_AVAILABILITY}
在NSObject中存在一個Class的isa指標。然後我們看看Class是什麼東西:這是在objc.h中定義的。
typedef struct objc_class *Class;struct objc_class{ Class isa; //指向metaclass Class super_class ;//指向父類 const char *name;//類名 long version; //類的版本資訊,初始化預設為0,可以通過runtime函數class_setVersion和class_getVersion進行修改讀取; long info; //一些標識資訊,如CLS_CLASS(0x1L)表示該類為普通的class,其中包含對象方法和成員變數;CLS_META(0x2L)表示該類為metaclass,其中包含類方法; long instance_size; 該類的執行個體變數大小(包括從父類繼承下來的執行個體變數); struct objc_ivar_list *ivars; //用於儲存每個成員變數的地址; struct objc_method_list **methodLists; //與info的一些標誌位有關,如CLS_CLASS(0x1L),則儲存物件方法; struct objc_cache *cache; //指向最近使用的方法的指標,用於提升效率; struct objc_protocol_list *protocols; //儲存該類遵守的協議;}
對於一個Class類中,存在很多東西,我們來解釋一下重要的東西:
Class isa :指向metaclass ,也就是靜態Class。一般一個Obj對象中的isa會指向普通的Class,這個Class中儲存普通成員變數和對象方法(-開頭的方法),普通Class中的isa指標指向靜態Class,靜態Class中儲存static類型成員變數和類方法(+開頭的方法)。
Class super_class:指向父類,如果這個類是根類,則為NULL。
我們通過下面這個圖來瞭解類和對象的繼承關係:
。
注意:所有metaclass中isa指標都指向根metaclass(也就是Root Class Meta),而根metaclass則指向自身。Root metaclass是通過繼承Root Class產生的。與root class結構成員一致,也就是前面提到的結構。不同的是Root metaclass的isa指標指向自身。
然後再來看看方法:
@selector(makeText):這是一個SEL方法選取器。SEL的主要作用就是通過方法名字(makeText)尋找到對應方法的函數指標,然後調用其函數。SEL其本身是一個int類型的一個地址,地址中存放著方法的名字。對於一個類中,每一個方法對應著一個SEL。所以iOS類中不能存在2個名稱相同的方法,即使參數類型不同,因為SEL是根據方法名字產生的,相同的方法名稱只能對應一個SEL。同時,我們也應該知道,OC是沒有方法重載的。
我們再來看看具體的訊息發送之後是怎麼樣來動態尋找對應的方法的。
首先,編譯器將代碼[obj makeText];轉化為objc_msgSend(obj,@selector(makeText));在objc_msgSend函數中,首先通過obj的isa指標找到對應的class。在Class中先去cache中通過SEL尋找對應函數method,若cache中未找到,再去methodList中尋找,若methodList中未找到,則取superClass中尋找。若能找到,則將method加入到cache中,以方便下次尋找,並通過method中的函數指標跳轉到對應的函數中去執行。
class的方法列表其實是一個字典,key為selector,value為IMP函數指標。一個IMP是指向方法在記憶體中的實現。很重要的一點,selector和IMP之間的關係是在運行時才決定的,而不是編譯時間。
對於物件導向而言,萬物皆對象,在OC中,類也是對象。The class is Object,也可以處理訊息。所以你現在知道為什麼會有類方法和執行個體方法了。
Method Swizzling
我們上面講過,方法由兩個部分組成。selector相當於一個方法的id,IMP是方法的實現。這樣分開的一個遍曆就是selector和IMP之間的對應關係可以被改變。比如一個IMP可以有多個selectors指向它。
而Method Swizzling可以交換兩個方法的實現。在OC中,兩種擴充class的途徑。首先是subclass.你可以重寫某個方法,調用父類的實現,這也意味著你必須使用這個subclass的執行個體。但是我們如果使用Category分類,重寫某個方法之後,就不能再調用原來的方法了。
Method Swizzling可以搞定這個問題,你可以重寫某個方法而不用繼承,同時還可以調用原先的實現。通常的做法是在Category中添加一個方法,可以通過method_exchangeImplementations這個運行時方法來交換實現。
著作權聲明:本文為博主原創文章,未經博主允許不得轉載。
Objective-C Runtime機制詳解