歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux編程 >> Linux編程 >> iOS開發總結

iOS開發總結

日期:2017/3/1 10:29:50   编辑:Linux編程

版本和平台

Runtime System對於Objective-C來說就好比是它的操作系統/運行平台,它使得Objective-C代碼能跑得起來。

相對於C/C++來說,Objective-C盡可能地把一些動作推遲到運行時來執行,即盡可能動態地做事情。因此,它不僅需要一個編譯器,還需要一個運行時環境來執行編譯後的代碼。

這裡會討論到NSObject類,Objective-C程序如何與Runtime System交互,運行時動態地加載新類,發消息給其它對象,以及運行時如何獲取對象信息。

Runtime System分為Legacy和Modern兩個版本,一般來說,我們現在用的都是Modern版本。

Modern版本的Runtime System有一個顯著的特征就是“non-fragile”,即父類的成員變量的布局發生改變時,子類不需要重新編譯。此外,還支持為聲明的屬性進行合成操作(即@property和@synthesis)。

與Runtime System交互

Objective-C程序和Runtime System在三個不同層次進行交互:通過Objective-C源碼;通過NSObject定義的函數;以及通過直接調用runtime functions。

通常來講,Runtime System都是在幕後工作,我們需要做的就是編寫Objective-C代碼,然後編譯。編譯器會為我們創建相應的數據結構和函數調用來實現語言的動態特性。這些數據結構保存著類、Category定義和Protocol聲明中所能找到的信息,包括成員變量模板、selectors,以及其它從源碼中提取到的信息。最主要的Runtime函數是用來發送消息的,它由源碼中的消息表達式激發。

Cocoa中大部分對象都是NSObject的子類(NSProxy是一個例外),繼承了NSObject的方法。因此在這個繼承體系中,子類可以根據需求重新實現NSObject定義的一些函數,實現多態和動態性,比如description方法。

一些NSObject定義的方法只是簡單地詢問Runtime System獲得信息,使得對象可以進行自省(introspection),比如一些類方法:用來確定類類型的isKindOfClass:,確定對象在繼承體系中的位置的isMemberOfClass:,判斷一個對象是否能接收某個特定消息的respondsToSelector:,判斷一個對象是否遵循某個協議的conformsToProtocol:,以及提供方法實現地址的methodForSelector:。這些方法讓一個對象可以進行自省(introspect about itself)。

Runtime System是一個動態共享庫,位於/usr/include/objc,擁有一套公共的接口,由一系列函數和數據結構組成。開發人員可以使用純C調用一些函數來做編譯器做的事情,或者擴展Runtime System,為開發環境制作一些工具等等。盡管一般情況下,編寫Objective-C並不需要了解這些內容,但有時候會很有用。所有的函數都在Objective-C Runtime Reference有文檔化信息。

發送消息是Objective-C程序中最經常出現的表達式,而該表達式最終會被轉換成objc_msgSend函數調用。

比如一個消息表達式[receiver message]會被轉換成objc_msgSend(receiver, selector),如果有參數則為objc_msgSend(receiver, selector, arg1, arg2, …)。

消息只有到運行時才會和函數實現綁定起來:首先objc_msgSend在receiver中查找selector對應的函數實現;然後調用函數過程,將receiving object(即this指針)和參數傳遞過去;最後,返回函數的返回值。

發送消息的關鍵是編譯器為類和對象創建的結構,包含兩個主要元素,一個是指向superclass的指針,另一個是類的dispatch table,該dispatch table中的表項將selector和對應的函數入口地址關聯起來。

當一個對象被創建時,內存布局中的第一個元素是指向類結構的指針,isa。通過isa指針,一個對象可以訪問它的類結構,進而訪問繼承的類結構。示例圖可參見:ObjCRuntimeGuide第14頁。

當向一個對象發送消息時,objc_msgSend先通過isa指針在類的dispatch table中查找對應selector的函數入口地址,如果沒有找到,則沿著class hierarchy(繼承體系)尋找,直到NSObject類。這就是在運行時選擇函數實現,用OOP的行話來說,就是動態綁定。

為了加速發送消息的速度,Runtime System為每個類創建了一個cache,用來緩存selector和對應函數入口地址的映射。

當objc_msgSend找到對應的函數實現時,它除了傳遞函數參數,還傳遞了兩個隱藏參數:receiving object和selector。之所以稱之為隱藏參數,是因為這兩個參數在源代碼中沒有顯示聲明,但還是可以通過self和_cmd來訪問。

當一個消息要被發送給某個對象很多次的時候,可以直接使用methodForSelector:來進行優化,比如下述代碼:

  1. //////////////////////////////////////////////////////////////
  2. void (*setter)(id, SEL, BOOL);
  3. int i;
  4. setter = (void (*)(id, SEL, BOOL))[target
  5. methodForSelector:@selector(setFilled:)];
  6. for ( i = 0; i < 1000, i++ )
  7. setter(targetList[i], @selector(setFilled:), YES);
  8. //////////////////////////////////////////////////////////////

其中,methodForSelector:是由Cocoa Runtime System提供的,而不是Objective-C本身的語言特性。這裡需要注意轉換過程中函數類型的正確性,包括返回值和參數,而且這裡的前兩個參數需要顯示聲明為id和SEL。

方法的動態決議

有時候我們想要為一個方法動態地提供實現,比如Objective-C的@dynamic指示符,它告訴編譯器與屬性對應的方法是動態提供的。我們可以利用resolveInstanceMethod:和resolveClassMethod:分別為對象方法和類方法提供動態實現。

一個Objective-C方法本質上是一個擁有至少兩個參數(self和_cmd)的C函數,我們可以利用class_addMethod向一個類添加一個方法(雖然文檔沒寫,但個人認為就是向dispatch table添加一個鍵值對)。

比如對於下面的函數:

  1. //////////////////////////////////////////////////////////////
  2. void dynamicMethodIMP(id self, SEL _cmd) {
  3. // implementation ….
  4. }
  5. //////////////////////////////////////////////////////////////

我們可以利用resolveInstanceMethod:將它添加成一個方法(比如叫resolveThisMethodDynamically):

  1. //////////////////////////////////////////////////////////////
  2. @implementation MyClass
  3. + (BOOL)resolveInstanceMethod:(SEL)aSEL
  4. {
  5. if (aSEL == @selector(resolveThisMethodDynamically)) {
  6. class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
  7. return YES;
  8. }
  9. return [super resolveInstanceMethod:aSEL];
  10. }
  11. @end
  12. //////////////////////////////////////////////////////////////

動態決議和發送消息並不沖突,在消息機制起作用之前,一個類是有機會動態決議一個方法的。當respondsToSelector:或者instancesRespondToSelector:被激活時,dynamic method resolver會優先有個機會為這個selector提供一份實現。如果實現了resolveInstanceMethod:,對於不想動態決議而想讓其遵循消息轉發機制的selectors,返回NO即可。

Objective-C程序可以在運行時鏈接新的類和category。動態加載可以用來做很多不同的事情,比如System Preferences裡頭各種模塊就是動態加載的。盡管有運行時函數可以動態加載Objective-C模塊(objc/objc-load.h中的objc_loadModules),但Cocoa的NSBundle類提供了更方便的動態加載接口。

消息轉發

向一個對象發送它不處理的消息是一個錯誤,不過在報錯之前,Runtime System給了接收對象第二次的機會來處理消息。在這種情況下,Runtime System會向對象發一個消息,forwardInvocation:,這個消息只攜帶一個NSInvocation對象作為參數——這個NSInvocation對象包裝了原始消息和相應參數。

通過實現forwardInvocation:方法(繼承於NSObject),可以給不響應的消��一個默認處理方式。正如方法名一樣,通常的處理方式就是轉發該消息給另一個對象:

  1. //////////////////////////////////////////////////////////////
  2. - (void)forwardInvocation:(NSInvocation *)anInvocation
  3. {
  4. if ([someOtherObject respondsToSelector:[anInvocation selector]])
  5. [anInvocation invokeWithTarget:someOtherObject];
  6. else
  7. [super forwardInvocation:anInvocation];
  8. }
  9. //////////////////////////////////////////////////////////////

對於不識別的消息(在dispatch table中找不到),forwardInvocation:就像一個中轉站,想繼續投遞或者停止不處理,都由開發人員決定。

類型編碼

為了支持Runtime System,編譯器將返回值類型、參數類型進行編碼,相應的編譯器指示符是@encode。

比如,void編碼為v,char編碼為c,對象編碼為@,類編碼為#,選擇符編碼為:,而符合類型則由基本類型組成,比如

  1. typedef struct example {
  2. id anObject;
  3. char *aString;
  4. int anInt;
  5. } Example;

編碼為{example=@*i}。

屬性聲明

當編譯器遇到屬性聲明時,它會生成一些可描述的元數據(metadata),將其與相應的類、category和協議關聯起來。存在一些函數可以通過名稱在類或者協議中查找這些metadata,通過這些函數,我們可以獲得編碼後的屬性類型(字符串),復制屬性的attribute列表(C字符串數組)。因此,每個類和協議的屬性列表我們都可以獲得。

與類型編碼類似,屬性類型也有相應的編碼方案,比如readonly編碼為R,copy編碼為C,retain編碼為&等。

通過property_getAttributes函數可以後去編碼後的字符串,該字符串以T開頭,緊接@encode type和逗號,接著以V和變量名結尾。比如:

@property char charDefault;

描述為:Tc,VcharDefault

而@property(retain)ididRetain;

描述為:T@,&,VidRetain

Property結構體定義了一個指向屬性描述符的不透明句柄:typedef struct objc_property *Property;。

通過class_copyPropertyList和protocol_copyPropertyList函數可以獲取相應的屬性數組:

  1. objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
  2. objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)

通過property_getName函數可以獲取屬性名稱。

通過class_getProperty和protocol_getProperty可以相應地根據給定名稱獲取到屬性引用:

  1. objc_property_t class_getProperty(Class cls, const char *name)
  2. objc_property_t protocol_getProperty(Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty)

通過property_getAttributes函數可以獲取屬性的@encode type string:

const char *property_getAttributes(objc_property_t property)

以上函數組合成一段示例代碼:

  1. @interface Lender : NSObject {
  2. float alone;
  3. }
  4. @property float alone;
  5. @end
  6. id LenderClass = objc_getClass("Lender");
  7. unsigned int outCount, i;
  8. objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);
  9. for (i = 0; i < outCount; i++) {
  10. objc_property_t property = properties[i];
  11. fprintf(stdout, "%s %s\n", property_getName(property), property_getAttributes(property));
  12. }

END。

Copyright © Linux教程網 All Rights Reserved