第八章 類NSObject和運行時系統

根類的作用:

作為一門動態編程語言,Objective-C有很多動態的特性,因此Objective-C不僅需要編譯環境,同時還需要一個運行時系統來執行編譯好的代碼。運行時系統扮演的角色類似于Objective-C的操作系統,它負責完成對象生成,釋放時的內存管理,為發來的消息查找對應的處理方法等。

通常情況下,程序中無法直接使用運行時系統提供的功能。根類方法中提供了運行時系統的基本功能。根類相當于運行時系統的一個接口。

根類通過哪些方式提供了哪些功能對系統有很大的影響。因此根類不同的系統之間是無開發出通用的程序的。

類和實例:

NSObject只有一個實例變量,就是Class類型的變量isa。isa用于標識實例對象屬于哪個類對象。因為isa決定著實例變量和類的關系,非常重要,所以子類不可修改isa的值。另外,也不能通過直接訪問isa來查詢實例變量到底屬于哪個類,而要通過實例方法class來完成查詢。

下面對類和實例變量的相關方法進行說明:

- (Class)class//返回消息接受者所屬類的類對象

+ (Class)

class//返回類對象

//雖然可以使用類名作為消息的接受者來調用類方法,但當類對象是其他消息的參數,或者將類對象賦值給變量的時候,需要通過這個類方法來獲取類對象

- (

id)self//返回消息接收者自身。是一個無任何實際動作但很有用的方法

- (

BOOL)isMemberOfClass:(Class)aClass//判斷消息接受者是不是參數aClass類的對象

- (

BOOL)isKindOfClass:(Class)aClass//判斷消息接受者是否是參數aClass類或者aClass類的子類的實例。這個函數和isMemberOfClass:的區別在于當消息的接收者是aClass的子類的實例時也會返回YES。

- (Class)superclass

//返回消息接收者所在類餓父類的類對象

+ (Class)superclass

//返回消息接收類的父類的類對象

實例對象的生成和釋放:

+ (id)alloc//生成消息接收類的實例對象。通常和init或init開頭的方法連用,生成實例對象的同時需要對其進行初始化。子類不允許重寫alloc。

- (

void)dealloc//釋放實例對象。dealloc被作為release的結果調用。除了在子類中重寫dealloc的情況之外,程序中不允許直接調用dealloc。

- (

onewayvoid)release//將消息的引用計數減1。引用計數變為0,dealloc方法被調用,消息接收者被釋放

- (

id)retain//為消息接收者的引用計數加1,同時返回消息接收者。

- (

id)autorelease//把消息的接收者加入到自動釋放池中,同時返回消息接收者。

- (NSUInteger)retainCount

//返回消息接收者的引用計數,可在調試時使用這個方法。NSUInteger是無符號整數類型。

- (

void)finalize//垃圾收集器在釋放接收者對象之前會執行finalize方法

上面從dealloc到reatainCount都是手動引用計數管理內存時使用的方法,使用ARC時不可用,finalize僅供垃圾回收有效時使用。

初始化:

- (id) init//init可對alloc生成的實例對象進行初始化。子類中可以重寫init或者定義新的以init開頭的初始化函數。

+ (

void)initialize//被用于類的初始化,也就是對類中共同使用的變量進行初始化設定等。這個方法會在類收到第一個消息之前被自動執行,不允許手動調用。

+ (

id)new//new是alloc和init的組合。new方法返回的實例對象的所有者就是調用new方法的對象。但是把alloc和init組合定義為new并沒有什么優點。根據類的實現不同,new方法并不會每次都返回一個全新的實例對象。有時new方法會返回對象池中預先生成的對象,也有可能每次都返回同一個對象。

對象的比較:

- (BOOL)isEqual:(id)anObject//消息接收者如果和參數anObject相等就返回YES。

- (NSUInteger)hash

//在把對象放入容器等的時候,返回系統內部用的散列值。

對象內容的描述:

+ (NSString*)description//返回一個NSString類型的字符串,表示消息接收者所屬類的內容。通常都是這個類的類名。

- (NSString *)description

//返回一個NSString類型的字符串,表示消息接收者的實例對象的內容。通常是類名加id值。子類中也可以重新定義description的返回值。例如,NSString的實例會返回字符串的內容,NSArray的實例會對數組中的每一個元素調用description,然后將調用結果用句號進行分割,并一起返回。

消息發送機制:

選擇器和SEL類型:

程序中的方法名(選擇器)在編譯后會被一個內部標識符所替代,這個內部標識符所對應的數據類型就是SEL類型。

OC為了能夠在程序中操作編譯后的選擇器,定義了@selector()指令,通過使用@selector()指令,就可以直接引用編譯后的選擇器。選擇器對應的SEL類型的值和處理器相關。如果SEL類型的變量無效的話,可設其為NULL,或者也可以使用(SEL)0這種常見的表達方法。

用SEL類型的變量來發送消息的方法如下:

- (id)performSelector:(SEL)aSelector//向消息接收者發送aSelector代表的消息,返回這個消息執行的結果。

- (

id)performSelector:(SEL

)aSelector

withObject:(

id)anObject//向消息接收者發送aSelector代表的消息,消息的參數為anObject,返回這個消息執行的結果。

下面兩個消息表達式進行的處理是相同的:

[target description];

[target performSelector:

@selector(description)];

下面這個例子展示了如何根據條件動態決定執行哪個方法:

SELmethod = [cond1] ?@selector(activate:) :@selector(hide:);id

obj = [cond2] ? myDocument : defaultDocument;

[target performSelector:method withObject:obj];

這種調用方式很像C語言中的函數指針,利用函數指針也可以實現和上面同樣的功能。

函數指針是函數在內存中的地址,指針對應的函數是在編譯時決定的,不能夠執行指定之外的函數。SEL類型就相當于方法名,根據消息接收者的不同,來動態執行不同的方法。

消息搜索:

對象受到一個消息后會執行哪個方法是被動態決定的。

所有的實例變量都存在一個Class類型的isa變量,它就是類對象。當收到消息后,運行時系統會從類內開始檢查是否有和這個消息選擇器相同的方法,找到就執行。沒找到就去父類中找,一直到根類,如果還沒找到,就會提示執行時錯誤。

如果每次收到消息都需要查找相應的方法,會造成很大的開銷。因此,運行時系統內部會緩存一個散列表。當下次在收到同樣的消息時,直接利用上次緩存的信息即可,不需要從頭搜索。

NSObject定義了一個可以動態查詢一個對象是否能夠響應某個選擇器的方法:

- (BOOL)respondsToSelector:(SEL)aSelector//查詢消息接收者中是否有能夠響應aSelector的方法,包括從父類繼承來的方法。

- (

BOOL)instancesRespondToSelector:(SEL)aSelector//查詢消息接收者所屬的類中是否有能夠響應aSelector的方法,包括從父類繼承來的方法。

以函數的形式來調用方法:

類中定義的方法通常是以函數的形式實現的,但通常在編程的時候并不會直接操作方法所對應的函數。但如果想讓程序盡可能的快一點,或者需要按照C語言的慣例傳遞函數指針的時候,可以直接調用方法對應的函數,以節省發送消息的開銷。另外,執行動態加載方法的定義等時,也可以將方法作為函數調用。但是,如果以函數的形式來調用方法的話,將無法利用面向對象的動態綁定等功能。雖然消息發送同函數調用相比確實慢一點,但卻有面向對象的動態綁定,多態等優點。同這些優點相比,速度上略微的損失是不值得一提的。

通過下面的方法,可以獲得某個對象持有的方法的函數指針,這些方法都被定義在NSObject中:

- (IMP)methodForSelector:(SEL)aSelector//搜索和指定選擇器相對應的方法,并返回指向該方法實現的函數指針。實例對象和類對象都可以使用這個方法。對實例對象使用時,會返回實例方法對應的函數,對類對象使用時,會返回類對象對應的函數。

+ (

IMP)instanceMethodForSelector:(SEL)aSelector//搜索和指定選擇器相對應的實例方法,并返回指向該實例方法實現的函數指針。

IMP是“implementation”的縮寫,它是一個函數指針,指向了方法實現代碼的入口。IMP的定義為:

typedefid(*IMP)(id,SEL, ...);

這個被指向的函數包括id(self指針),調用的SEL(方法名),以及其他一些參數。例:

- (id)setBox:(id)obj1 title:(id)obj2;

foo是這個方法所屬類的一個實例變量。獲取指向setBox的函數指針,并通過該指針進行函數調用過程如下:

IMP

funcp;

funcp = [foo methodForSelector:

@selector

(setBox:title:)];

xyz = (*funcp)(foo,

@selector(setBox:title:), param1, param2);

對self進行賦值:

self是方法的一個隱含參數,它代表的是收到消息的對象自身。

- (id)initWitMax:(int)a/*推薦使用這種寫法*/

{

if((self= [superinit]) !=nil

) {

max = a;

}

returnself

;

}

在OPENSTEP時代,OC初始化方法一般采用下面這種寫法。但這里需要注意的是沒有用父類初始化方法的返回值對self進行賦值。子類的初始化方法和父類的初始化方法都是對同一個對象進行操作的,所以不需要顯示地對self進行賦值操作。需要注意的是這種寫法也有可能出錯,除了初始化失敗外,父類的初始化方法也有可能并沒有返回self而是返回了其他對象。如類簇構成的類在初始化方法中就沒有返回self。所以第一種方法是更為安全的做法。

- (

id)initWithMax:(int)a/*舊的寫法*/

{

[

super

init];

max = a;

returnself

;

}

另外,在用ARC的時候,如果初始化方法的返回值沒有被用到,編譯就會發生錯誤。

發送消息的速度:

消息送信所需要的時間是函數調用所需時間的2倍。另外,直接發送消息比用performSelector:發送消息速度更快。

類對象和根類:

類對象的類被叫做元類。實例對象所屬的類是class。類對象所屬的類是元類。

OC中很多的概念都來自Smalltalk,元類的概念就是其中之一。但現在的OC中已經不存在元類的概念了,程序中也不能操作元類。用于表示對象的id類型和表示類的Class類型實際上都是指向結構的指針。

類對象和實例對象都存在一個成員變量isa,它是一個objc_class類型的指針。類對象中保存的是實例方法,元類對象中保存的是類方法,通過這樣的定義能夠統一實現實例方法和類方法的調用機制。

任何一個類對象都是都是繼承了根類的元類對象的一個實例。也就是說,類對象可以執行根類對象的實例方法。

總結:

所有類的實例對象都可以執行根類的實例方法

如果在派生類在重新定義類實例方法,新定義的方法會被執行

所有類的類對象都可以執行根類的類方法

如果在派生類中重新定義了類方法,新定義的方法會被執行 。

所有類的類對象都可以執行根類的實例方法

即使在派生類中重新定義了實例方法,根類中的方法也會被執行。

如果在派生類中將實例方法作為類方法重新定義了的話,新定義的方法會被執行。

Target-action paradigm(目標-動作模式):

通過使用SEL類型的變量,能夠在運行時動態決定執行哪個方法。實際上,Application框架就利用了這種機制實現了GUI控件對象間的通信。

來看看下面這個例子:

@interfacemyCell :NSObject

{

SEL action;

id target;

...

}

- ?(void)setAction:(SEL)aSelector;

- ?(void)setTarget:(id)anObject;

- ?(void)performClick:(id)sender;

...

@end

@implementation

myCell

- (void)setAction:(SEL)aSelector

{

action = aSelector;

}

- ?(void)setTarget:(id)anObject

{

target = anObject;

}

- ?(void)performClick:(id)sender

{

(void)[targetperformClick:action withObject:sender];

}

...

@end

Application框架的目標-動作模式在發送消息時使用了下面這種形式定義的方法,即只有一個id類型的參數,沒有返回值。這種形式的方法叫做動作方法。

- (void) xxxx:(id)sender;

當用戶操作了某個GUI控件,action指定的消息就會被發送到事先設定好的target,消息定義如上所示,消息的參數通常都是GUI的控件id。這樣一來,消息的接收者target就會知道到底哪個控件發送了什么樣的消息。

setTarget:和setAction:來指定目標和動作,使用ARC時推薦使用弱引用。

UIKit是iPhone和iPad中用來建立和管理應用程序界面的框架。Application框架中的動作方法只有一種格式,UIKit框架中的動作方法則有以下三種格式:

- (void)xxxx;

- (void)xxxx:(id)sender;

- (void)xxxx:(id)sender forEvent:(UIEvent *)event;//表示與操作相關的事件信息

Xcode中的動作方法和Outlet的寫法:

綜合開發環境Xcode及其用于設計和測試GUI的工具Interface Builder中,為了連接對象,在類的接口部分中聲明了一些宏,通過這些宏變量能夠將代碼連接到nib。

動作方法的聲明如下,IBAction是一個宏,被定義為void:

- (IBAction) xxxx:(id)sender;

outlet的含義:

outlet可以被理解為一個插座,可以通過outlet從控件取出信息,或將新的信息賦給控件。outlet通常是一個類的實例變量,例如:一個指向NSButton類型的控件的實例變量聲明如下:

IBOutletNSButton *theButton;

這里的IBOutlet也是一個宏,被定義為空,另外還有下面這樣一種定義方式:

IBOutletCollection(NSButton)NSArray*buttons;

這樣聲明之后,就可以將Xcode上面的多個控件都連接到這個Outlet上。IBOutletCollection(NSButton)也是一個宏定義,編譯之后會被替換為空。在多個控件需要統一處理的時候,這種定義方法會很方便。

屬性聲明也可以定義為Outlet,如下所示:

@property(weak)IBOutletNSButton *okButton;

Xcode可以把這個屬性聲明看作是Outlet,并用其來連接控件。另外也可以為這個屬性聲明增加訪問方法。

使用ARC進行開發的情況下,要注意避免形成對象之間的引用循環。所以,除了主要的對象之間的連接使用強引用之外,其余的對象之間進行連接時都推薦使用弱引用。屬性聲明時,建議加上assign或者weak選項。

Objective-C和Cocoa環境:

cocoa環境和Mac OS X:

我們經常提到的cocoa環境通常是指AppKit和Foundation這兩個核心框架,但有時候也包含Core Foundation或Core Date等框架。

Cocoa是為了提供構建應用程序所必需的功能而設計的,所以才會利用下層的功能。

Cocoa Touch 和iOS:

iPhone和iPad的操作系統iOS使用Cocoa Touch作為GUI環境。下圖展示了iOS的架構以及和Cocoa Touch的關系。

有Foundation和UIKit框架組合而成的GUI環境稱為Cocoa Touch:

框架:

將開發軟件和執行軟件所必需的圖形庫,頭文件和設定用的各種信息全部匯總在一起就構成了框架。其中包括了應用程序執行時所必須的動態鏈接庫。

框架提供了程序運行的基本功能和GUI基礎。在這些基本功能之上,通過添加獨有的處理,就可以實現要實現的功能。

在Mac OSX中,將開發和執行軟件所必需的圖形庫、頭文件和設定用的各種信息全部匯總在一起就構成了框架。值得一提的是,其中包括了應用程序執行時所必須的動態鏈接庫。

框架是應用程序的骨架的意思。框架提供了程序運行的基本功能和GUI基礎。在這些基本功能

之上,通過添加獨有的處理,就可以實現要實現的功能。

Mac OSX中最重要的框架是Foundation框架、Application框架(也稱為Appkit框架或Application Kt框架)、Core Foundation框架和System框架。Foundation框架提供了包括NSObject在內的Objective-C的基本類庫。Core Foundation框架是一組C語言接口,它們為iOS應用程序提供基本的數據管理和服務功能(詳情請參照附錄B)。Application框架包含了與Cocoa的GUI的基礎——窗口環境相關的類。另外,System框架包含了與Cocoa最底層的Mach核心和Unix相關的類庫。因為系統框架通常都和程序執行相關,所以編譯程序的時候不需要指定-framewok選項。

還有一個Cocoa框架,實際上它是由Foundation框架、Application框架和CoreData框架組成的。像這樣,將多個框架嵌套打包的技術稱為umbrella framework。不過該技術僅僅是蘋果公司提供功能的一種方法,不建議普通開發者提供自己的umbrella framework。

如上圖,iOS中的Cocoa Touch由Foundation框架和UIKit框架構成。iOS 的 Foundation框架和Mac OS X的Foundation框架有很大一部分是共用的,例如字符串或數組等的基本類。UIKit是負責用戶接口的框架,其中定義了基本的GUI控件和用于處理觸摸屏幕之類的事件的類。另外,iOS可以通過利用Core Foundation框架中的數據結構提供面向其他框架或設備的功能。

框架的構成和頭文件:

框架的每個目錄通常包含以下要素:

類庫----框架同名文件

CodeResources----記錄了文件散列值的XML文件

Headers----包含了頭文件的目錄

Resources----包含了不同國家語言用的文件等各類資源等目錄

Versions----包含了框架的各種版本的目錄

#import<框架名/頭文件名>

全新的運行時系統

64位模型和整數類型:

數據類型發生了變化的話,主要受影響的是調用API時的參數和返回值的類型。

ILP32和LP64中的int類型都是32位,這種情況下,就算數據類型變更到了64位,因為當前的程序使用的整數類型還是32位,所以也不能利用到64位的優點。為了解決這個問題,Cocoa環境引入了NSInter類型,NSInteger在32位的數據模型下被定義為int,在64位數據模型下被定義位long。除此之外,Cocoa中還定義了無符號的NSUInteger類型。

健壯的實例變量:

如果框架中的類發生了變化,應用程序就一定得重新編譯才能夠在早期的運行時系統中繼續執行。這稱為脆弱的二進制接口。

現代運行時系統針對這個問題進行了改進,使實例變量發生變化的情況下,應用程序不重新編譯也能夠繼續執行。這稱為健壯的實例變量。但實例變量的改變僅限于變量順序的變化或增加了新的實例變量這種程度,刪除了實例變量或改變了實例變量的類型時還是需要重新編譯。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容