Objective-C對象之類對象和元類對象


作者: wangzz
轉載請注明出處
如果覺得文章對你有所幫助,請通過留言或關注微信公眾帳號wangzzstrive來支持我,謝謝!
 

作為C語言的超集,面向對象成為Objective-C與C語言的最大區別,因此,對象是Objective-C中最重要的部分之一。目前面向對象的語言有很多,Objective-C中的對象又和其他語言中的對象有什么區別呢?下面來簡單介紹Objective-C中對象的實現。
1、Objective-C中的類

誰都知道,所有的對象都是由其對應的類實例化而來,殊不知類本身也是一種對象,先不要對這句話感到驚訝。
首先我們來關注Objective-C中的類。在Objective-C中,我們用到的幾乎所有類都是NSObject類的子類,NSObject類定義格式如下(忽略其方法聲明):
@interface NSObject <NSObject> {
Class isa;
}
這個Class為何物?在objc.h中我們發現其僅僅是一個結構(struct)指針的typedef定義:
typedef struct objc_class *Class;
同樣的,objc_class又是什么呢?在Objective-C2.0中,objc_class的定義如下:
struct objc_class {
Class isa;
}
寫到這里大家可能就暈了,怎么又有一個isa??這些isa到底是什么?之間有什么區別和聯系?接下來解答這一連串的疑問。
其實在Objective-C中任何的類定義都是對象。即在程序啟動的時候任何類定義都對應於一塊內存。在編譯的時候,編譯器會給每一個類生成一個且只生成一個”描述其定義的對象”,也就是蘋果公司說的類對象(class object),他是一個單例(singleton), 而我們在C++等語言中所謂的對象,叫做實例對象(instance object)。對於實例對象我們不難理解,但類對象(class object)是干什么吃的呢?我們知道Objective-C是門很動態的語言,因此程序里的所有實例對象(instace object)都是在運行時由Objective-C的運行時庫生成的,而這個類對象(class object)就是運行時庫用來創建實例對象(instance object)的依據。
再回到之前的問題,腫么這個實例對象(instance object)的isa指針指向的類對象(class object)里面還有一個isa呢?這個類對象(class objec)的isa指向的依然是一個objc-class,它就是“元類對象”(metaclass object),它和類對象(class object)的關系是這樣的:

2、類對象(class object)
①類對象的實質
我們知道了:類對象是由編譯器創建的,即在編譯時所謂的類,就是指類對象(官方文檔中是這樣說的: The class object is the compiled version of the class)。任何直接或間接繼承了NSObject的類,它的實例對象(instance objec)中都有一個isa指針,指向它的類對象(class object)。這個類對象(class object)中存儲了關於這個實例對象(instace object)所屬的類的定義的一切:包括變量,方法,遵守的協議等等。因此,類對象能訪問所有關於這個類的信息,利用這些信息可以產生一個新的實例,但是類對象不能訪問任何實例對象的內容。
當你調用一個 “類方法” 例如 [NSObject alloc],你事實上是發送了一個消息給他的類對象。

②類對象和實例對象的區別
當然有區別了,盡管類對象保留了一個類實例的原型,但它並不是實例本身。它沒有自己的實例變量,也不能執行那些類的實例的方法(只有實例對象才可以執行實例方法)。然而,類的定義能包含那些特意為類對象准備的方法–類方法( 而不是的實例方法)。類對象從父類那里繼承類方法,就像實例從父類那里繼承實例方法一樣。
③類對象與類名
在源代碼中,類對象由類名表示。
在下面的例子中,Retangle類 用從NSObject那里繼承來的方法來返回類的版本號:
int versionNumber = [Rectangle version];
只有在消息表達式中作為接收者,類名才代表類對象。其他地方,你需要要求一個實例或者類返回class id。 響應class消息:
id aClass = [anObject class];
id rectClass = [Rectangle class];
如同上面的例子顯示的那樣,類對象像其他對象一樣,也是id類型。

總之,類對象是一個功能完整的對象,所以也能被動態識別(dynamically typed),接收消息,從其他類繼承方法。特殊之處在於它們是由編譯器創建的,缺少它們自己的數據結構(實例變量),只是在運行時產生實例的代理。

3、元類對象(metaclass object)
①元類對象的實質
實際上,類對象是元類對象的一個實例!!元類描述了 一個類對象,就像類對象描述了普通對象一樣。不同的是元類的方法列表是類方法的集合,由類對象的選擇器來響應。當向一個類發送消息時,objc_msgSend會通過類對象的isa指針定位到元類,並檢查元類的方法列表(包括父類)來決定調用哪個方法。元類代替了類對象描述了類方法,就像類對象代替了實例對象描述了實例化方法。
很顯然,元類也是對象,也應該是其他類的實例,實際上元類是根元類(root class’s metaclass)的實例,而根元類是其自身的實例,即根元類的isa指針指向自身。
類的super_class指向其父類,而元類的super_class則指向父類的元類。元類的super class鏈與類的super class鏈平行,所以類方法的繼承與實例方法的繼承也是並行的。而根元類(root class’s metaclass)的super_class指向根類(root class),這樣,整個指針鏈就鏈接起來了!!

記住,當一個消息發送給任何一個對象, 方法的檢查 從對象的 isa 指針開始,然后是父類。實例方法在類中定義, 類方法 在元類和根類中定義。(根類的元類就是根類自己)。在一些計算機語言的原理中,一個類和元類層次結構可以更自由的組成,更深元類鏈和從單一的元類繼承的更多的實例化的類。Objective-C 的類方法 是使用元類的根本原因,在其他方面試圖在隱藏元類。例如 [NSObject class] 完全相等於 [NSObject self],所以,在形式上他還是返回的 NSObject->isa 指向的元類。 Objective-C語言是一組實用的折中方案。

還有些不明白? 下面這個圖標可能會有些幫助:



綜上所述,類對象(class object)中包含了類的實例變量,實例方法的定義,而元類對象(metaclass object)中包括了類的類方法(也就是C++中的靜態方法)的定義。類對象和元類對象中當然還會包含一些其它的東西,蘋果以后也可能添加其它的內容,但對於我們只需要記住:類對象存的是關於實例對象的信息(變量,實例方法等),而元類對象(metaclass object)中存儲的是關於類的信息(類的版本,名字,類方法等)。要注意的是,類對象(class object)和元類對象(metaclass object)的定義都是objc_class結構,其不同僅僅是在用途上,比如其中的方法列表在類對象(instance object)中保存的是實例方法(instance method),而在元類對象(metaclass object)中則保存的是類方法(class method)。關於元類對象可以參考蘋果官方文檔" The Objective-‐C Programming Language "

4、類對象和元類對象的相關方法

①object_getClass跟隨實例的isa指針,返回此實例所屬的類,對於實例對象(instance)返回的是類(class),對於類(class)則返回的是元類(metaclass),
②-class方法對於實例對象(instance)會返回類(class),但對於類(class)則不會返回元類(metaclass),而只會返回類本身,即[@"instance" class]返回的是__NSCFConstantString,而[NSString class]返回的是NSString。
③class_isMetaClass可判斷某類是否為元類.                                     

④使用objc_allocateClassPair可在運行時創建新的類與元類對,使用class_addMethod和class_addIvar可向類中增加方法和實例變量,最后使用objc_registerClassPair注冊后,就可以使用此類了。看到動態語言牛逼的地方了嗎,可以在需要時更改已經定義好的類!Objective-C的類別方法估計底層就是這么實現的,只是不知道為什么類別不能增加實例變量,有高手請留言。

 

 

Objective-C為我們提供了兩種初始化對象的方法:Objective-C2.0以后可用的new方法和兩段構造法。既然要比較這兩種初始化方法,就從它們本身的異同出發吧。

一、兩段構造法

這是Objective-C特有的對象創建方法,書寫形式如下:

NSString*s=[[NSString alloc] init];

所謂的兩段構造,就是指將alloc和init分開來寫,這和大多數其它語言(如C、C++、Java、JavaScript)都不一樣。先來看看alloc和init都干了什么吧:

1、alloc方法

當對象創建時,cocoa會從應用程序的虛擬地址空間上為該對象分配足夠的內存。cocoa會遍歷該對象所有的成員變量,通過成員變量的類型來計算所需占用的內存。
當我們通過alloc或allocWithZone方法創建對象時,cocoa會返回一個未”初使化“過的對象。在這個過程中,cocoa除了上面提到的申請了一塊足夠大的內存外,還做了以下3件事:
①將該新對象的引用計數(Retain Count)設置成1。
②將該新對象的isa成員變量指向它的類對象。isa成員變量指向分配內存的類對象(class object),這是在NSObject類中定義的,所以保證Cocoa的所有對象都帶有此成員變量。它與Objective-C的運行時是一體的,借助該變量可以實現Cocoa對象在運行時的自省(Introspection)功能。
③將該新對象的所有其它成員變量的值設置成零。(根據成員變量類型的不同,零有可能是指nil或0)
④返回指向該對象的一個指針。

2、init方法

大部分情況下,我們都不希望所有成員變量都是零,所以

①init方法會做真正的初使化工作,讓對象的成員變量的值符合我們程序邏輯中的初始化狀態。例如,NSMutableString可能就會額外再申請一塊字符數組,用於動態修改字符串。

②返回真正可以使用的指向該對象的指針

init還有一個需要注意的問題,某些情況下,init會造成alloc的原本空間不夠用,而進行第二次分配內存空間。所以下面的寫法是錯的:
NSString  *s=[NSString alloc];

[s init];// 這兒init返回的地址可能會變。s原本的指針地址可能是無效的地址。

為此,蘋果引入了一個編程規范,讓大家寫的時候將alloc 和init寫在一行。所以上面的代碼正確的寫法是
NSString  *s=[[NSString alloc] init];

二、new方法

可能是為了和其他語言保持一致,蘋果后來也推出了new方法來初始化對象。作為類方法的new,只是簡單地等價於 alloc + init,卻不能指定init的參數,所以實際使用中很少見到。

三、使用兩段構造法的原因

有人可能要問,Objective-C的對象創建方法和大多數其它語言(如C、C++、Java、JavaScript)都不一樣,是什么原因促使Objective-C做了這種設計?

1、歷史原因

這里面多多少少就有歷史的因素了。Objective-C是一門非常老的語言。如果你查閱文檔,你會發現它和C++出生在同一時代(兩種語言的發行年份都是1983年),都是作為C語言的面向對象的接班人被推出。當然,最終C++勝出。由於歷史久遠,Objective-C也無法有太多優秀的語言做參考,所以,有很多歷史遺留的設計。

2、設計原則

簡單看來,根據設計模式的Single Responsibility的設計原則,蘋果覺得alloc和init是做的2件不同的事情,把這兩件事情分開放在2個函數中,對於程序員更加清楚明了。更詳細查閱文檔后,我覺得這是由於歷史原因,讓蘋果覺得alloc方法過於復雜,在歷史上,alloc不僅僅是分配內存,還可以詳細的指定該內存所在的內存分區(用NSZone表示)。

同時由於分配和初始化階段是分開的,初始化方法的實現只需處理新實例的變量,並完全忽略有關分配的問題,簡化了初始化方法的過程。

四、NSZone簡介

早期蘋果是建議程序員使用 allocWithZone來管理內存分配的,每個NSZone表示一塊內存分區,+allocWithZone:(NSZone *)zone方法可以允許對象從指定分區分配內存。內存區是Cocoa的一個功能部件,它能使同時使用的對象或計算機的地址空間中相鄰的對象保持在內存中,以此提高程序的性能。要解釋對象在內存中的位置會如何影響性能,需要解釋應用程序需要比物理內存更大的內存時會發生什么情況。

每個Cocoa應用程序都有很大的可尋址內存,當應用程序動態的分配內存時,即使計算機的所有物理內存都已經被占用,操作系統仍然會提供內存。要滿足該分配要求,操作系統會使用頁面調度(paging)或者交換(swapping)操作將一些物理內存中的內容復制到硬盤,之前正在使用的物理內存就可以被提供出來使用了,而之前的那些數據應經被寫入硬盤。如果有需要先前復制到硬盤的那部分內存數據,操作系統會將另外一塊物理內存復制到硬盤,並將先前的舊內存再度調回內存。即時內存在硬盤間調度,操作系統仍然能為每個應用程序映射地址空間到物理內存。操作系統的這一功能即是虛擬內存(virtual memory)。

由於從物理內存額硬盤中相互調度是很消耗時間的,因此,使用虛擬內存會影響性能。過多的頁面調度會降低系統性能,這稱為抖動(thrashing)。如果一起使用的兩個或多個對象在內存中的位置很遠,抖動發生的可能性將會大大增加,因此對象實例的內存分配的位置也很重要。

分區用於確保分配給同時使用的對象的內存位於相鄰位置。當需要某個對象時,另外相鄰的對象也基本會用到,需要的所有對象同時調入內存的可能性就更大,當不需要時,又可以都同時調出內存,Cocoa中的NSZone類型是指定標識內存區的C結構的對象,+allocWithZone:(NSZone *)zone方法允許NSZone變量從指定分區分配內存。已達到減少抖動的目的。可見當年蘋果的設計師們的良苦用心!!!

只是,分區是一個十分底層的東西,而且,隨着硬件設備的發展,物理內存的不斷增大,以及操作系統內存分配函數復雜性的提高,使用分區的最初目的已經逐漸消失了。自從Mac OS X 10.5上引入了垃圾回收機制后,蘋果就不建議程序員使用allocWithZone了,事實上,cocoa框架也會忽略+allocWithZone:(NSZone *)zone指定的分區。蘋果在文檔中也提到,+allocWithZone:(NSZone *)zone僅僅是一個歷史遺留設計了。           


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM