你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發綜合 >> [精通Objective-C]運行時系統

[精通Objective-C]運行時系統

編輯:IOS開發綜合

運行時系統概述

Objective-C擁有相當多的動態特性,這些特性在運行程序時發揮作用,而不是在編譯或鏈接代碼時發揮作用。Objective-C運行時系統實現了這些特性,而這些功能為Objective-C語言提供了非常多的強大功能和靈活性。開發人員使用它們能夠以實時方式促進程序的開發和更新,而無需重新編譯和重新部署軟件。

在運行時,Objective-C語言會執行其他語言在程序編譯或鏈接時會執行的許多常規操作,如確定類型和方法解析。這些操作還可以提供API,使編寫的程序能夠執行額外的運行時操作,如動態內省和以動態方式創建和加載代碼。

對象消息

在OPP術語中,消息傳遞是指一種在對象之間發送和接收消息的通信模式。在Objective-C中,消息傳遞用於調用類和對象的方法。下面以一個消息傳遞表達式為例:

[calculator sumAddend1:addend1 addend2:addend2];

calculator是消息的目的地(對象或類),而消息本身sumAddend1:addend1 addend2:addend2,由選擇器和相應的輸入參數構成。概括來說,Objective-C對象消息傳遞中具有下列關鍵元素:
消息:向對象/類發送的名稱(選擇器)和一系列參數。
方法:Objective-C中的類或實例方法,其聲明中含有名稱、輸入參數、返回值和方法簽名(即輸入參數和返回值的數據類型)。
方法綁定:接收向指定接收器發送的消息並尋找和執行適當方法的處理過程。Objective-C運行時系統在調用方法時,會以動態綁定方式處理信息。

選擇器

選擇器是一種文本字符串,用於指明調用對象或類中的哪個(些)方法。選擇器是一種分為多個段的文本字符串,每個段以冒號結尾並且後跟參數。下面是一些選擇器實例:

description
description:
sumAddend1:addend2:
sumAddend1::

在Objective-C中,消息的選擇器直接與一個或多個類/實例方法聲明對應。例如:

@interface Calculator : NSObject
// 與選擇器sumAddend1:addend2:對應
-(NSNumber *) sumAddend1:(NSNumber *)adder1 addend2:(NSNumber *)adder2;
// 與選擇器sumAddend1::對應
-(NSNumber *) sumAddend1:(NSNumber *)adder1 :(NSNumber *)adder2;
@end

選擇器類型(SEL)是一種特殊的Objective-C數據類型,是用於在編譯源代碼時替換選擇器值的唯一標識符。所有具有相同選擇器值的方法都擁有相同的SEL標識符。

下面是SEL類型變量的兩種使用方式:

編譯時創建選擇器:

[calculator performSelector:@selector(sumAddend1::) withObject:addend1 withObject:addend2];

運行時創建選擇器:

SEL selector = NSSelectorFromString(@"sumAddend1::");
[calculator performSelector:selector withObject:addend1 withObject:addend2];

方法簽名

方法簽名定義了方法輸入參數的數據類型和方法的返回值(如果存在)。編譯器會將[接收器 消息]形式的對象消息轉換為聲明中含有方法簽名的C函數調用語句。為了生成正確的對象消息傳遞代碼,編譯器需要獲得選擇器值和方法簽名。消息中可能含有輸入參數,而且因為接收器和相應的方法是在程序運行時確定的,所以編譯器無法知道使用怎樣的數據類型才能與要調用的方法對應起來。為了確定正確的方法簽名,編譯器會根據已解析的方法聲明進行猜測。如果它找不到方法簽名,或者它從方法聲明獲得的方法簽名與運行時實際執行的方法不匹配,就會出現方法簽名不匹配的情況,會導致各種各樣的警告或錯誤。

下面展示一個方法簽名不匹配的情況,該程序使用了3個類,3個類的接口分別如下所示:

@interface Calculator1 : NSObject
-(int) sumAddend1:(int)adder1 addend2:(int)adder2;
@end
@interface Calculator2 : NSObject
-(float) sumAddend1:(float)adder1 addend2:(float)adder2;
@end
@interface Calculator3 : NSObject
-(NSInteger) sumAddend1:(NSInteger)adder1 addend2:(NSInteger)adder2;
@end

當接收器類型為id時,發送消息[接收器 sumAddend1:25 addend2:10]時,根據程序的接口和運行時確定的接收器類型進行判斷(接收器類型可能為3個類中的任意一個),會出現方法簽名不匹配的情況。要避免出現這種情況,最好確保擁有不同特征的方法也擁有不同的名稱。

使用對象消息

下面是選擇器和SEL類型變量的具體使用示例:

首先創建一個類Calculator,類中有兩個方法,都是返回兩個輸入參數之和:

#import 

@interface Calculator : NSObject

-(NSNumber *) sumAddend1:(NSNumber *)adder1 addend2:(NSNumber *)adder2;
-(NSNumber *) sumAddend1:(NSNumber *)adder1 :(NSNumber *)adder2;
@end
#import "Calculator.h"

@implementation Calculator

-(NSNumber *) sumAddend1:(NSNumber *)adder1 addend2:(NSNumber *)adder2{

    // _cmd是一個類型為SEL的隱式參數,含有被發送消息中的選擇器
    NSLog(@"Invoking method on %@ object with selector %@", [self className], NSStringFromSelector(_cmd));
    return [NSNumber numberWithInteger:[adder1 integerValue] + [adder2 integerValue]];
}

-(NSNumber *) sumAddend1:(NSNumber *)adder1 :(NSNumber *)adder2{
    NSLog(@"Invoking method on %@ object with selector %@", [self className], NSStringFromSelector(_cmd));
    return [NSNumber numberWithInteger:[adder1 integerValue] + [adder2 integerValue]];
}

@end

下面是在main.m中進行測試:

#import 
#import "Calculator.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Calculator *calculator = [[Calculator alloc] init];
        NSNumber *addend1 = [NSNumber numberWithInteger:25];
        NSNumber *addend2 = [NSNumber numberWithInteger:10];

        // 直接使用選擇器
        NSLog(@"Sum of %@ + %@ = %@", addend1, addend2, [calculator sumAddend1:addend1 addend2:addend2]);

// performSelector方法如果找不到與該選擇器匹配的方法,那麼方法就會拋出異常導致內存洩漏。於是編譯器會發出警告。通過pragma指令可以消除該警告。
// 禁用指定的編譯器警告功能,使用push和pop可以保存和恢復編譯器當前的診斷設置
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        // 使用SEL類型變量創建選擇器,再用performSelector方法調用
        SEL selector = NSSelectorFromString(@"sumAddend1::");
        NSLog(@"Sum of %@ + %@ = %@", addend1, addend2, [calculator performSelector:selector withObject:addend1 withObject:addend2]);
#pragma clang diagnostic pop
    }
    return 0;
}

運行結果:

2016-07-06 12:48:48.651 Calculator[7049:69121] Invoking method on Calculator object with selector sumAddend1:addend2:
2016-07-06 12:48:48.652 Calculator[7049:69121] Sum of 25 + 10 = 35
2016-07-06 12:48:48.652 Calculator[7049:69121] Invoking method on Calculator object with selector sumAddend1::
2016-07-06 12:48:48.652 Calculator[7049:69121] Sum of 25 + 10 = 35
Program ended with exit code: 0

動態類型

運行時系統通過動態類型功能可以在運行時程序時決定對象的類型,因而可以使運行時因素能夠在程序中指定哪種對象。Objective-C通過id類型支持動態類型。id數據類型是一種Objective-C獨有的數據類型。其變量可以存儲任何數據類型的Objective-C對象,而不論該對象是哪種類的實例。以下是靜態類型和動態類型的使用:

// 聲明為靜態類型
Atom *atom1 = [[Atom alloc] init];
// 聲明為動態類型
id atom2 = [[Atom alloc] init];

由於Objective-C既支持靜態類型又支持動態類型,所以可在方法聲明中使用不同等級的類型信息:

// 輸入參數可以接收任何類的實例
-(NSInteger) computeValue1:(id)parameter;
// 輸入參數可以接收任何遵守Writer協議的對象
-(NSInteger) computeValue2:(id)parameter;
// 輸入參數可以接收任何類型為NSNumber的對象
-(NSInteger) computeValue3:(NSNumber *)parameter;
// 輸入參數可以接收任何類型為NSNumber且遵守Writer協議的對象
-(NSInteger) computeValue4:(NSNumber *)parameter;

動態綁定

動態綁定指在運行程序時(而不是在編譯時)將消息與方法對應起來的處理過程。因為許多接收器對象可能會實現相同的方法,調用方法的方式會動態變化。因此,動態綁定實現了OPP的多態性,可以在不影響既有代碼的情況下,將新對象和代碼連接或添加到系統中,從而降低對象之間的耦合度。同時通過消除用於處理多選情景的條件邏輯,動態綁定還能夠降低程序的復雜程度。以下面代碼段為例(Hydrogen類為Atom類的子類,而logInfo方法定義在Atom類中):

id atom = [[Hydrogen alloc] initWithNeutrons:1];
[atom logInfo];

執行這段代碼時,運行時系統會通過動態綁定確定變量atom的實際類型,然後使用消息選擇器將該消息與接收器的實例方法對應起來。在本例中,atom的類型被設置為Hydrogen *,因此運行時系統會搜索Hydrogen類的實例方法logInfo,如果沒有找到,就會在Hydrogen類的父類中尋找相應的實例方法。運行時系統會一直在類層次結果中尋找該實例方法,直到找到它為止。

動態綁定是Objective-C的一種繼承特性,它不需要任何API。使用動態綁定甚至可以將消息選擇器設置為在運行程序時確定的變量。

動態方法決議

使用動態方法決議能夠以動態方式實現方法。使用Objective-C中的@dynamic指令,可以告知編譯器與屬性關聯的方法會以動態方式實現。

NSObject類中含有resolveInstanceMethod:和resolveClassMethod:方法,它們能夠以動態方式分別為指定的實例和類方法選擇器提供實現代碼。

下面是以動態方式實現方法,來展示動態方法決議:

首先在之前創建的Calculator.m文件中導入運行時系統庫,並重寫resolveInstanceMethod:方法:

#import 
+(BOOL) resolveInstanceMethod:(SEL)sel{
    NSString *method = NSStringFromSelector(sel);
    if ([method hasPrefix:@"absoluteValue"]) {
        // 運行時系統API,動態方式將函數作為實例方法添加到類中
        // class_addMethod的4個參數分別添加方法的目標類、新方法的選擇器、函數的地址(數據類型為IMP)和描述方法參數的數據類型的字符串(字符串裡的內容分別為函數返回值類型和每個參數類型)
        class_addMethod([self class], sel, (IMP)absoluteValue, "@@:@");
        NSLog(@"Dynamically added instance method %@ to class %@", method, [self className]);
        return YES;
    }
    return [super resolveClassMethod:sel];
}

// 被添加為實例方法的函數
id absoluteValue(id self, SEL _cmd, id value){
    NSInteger intVal = [value integerValue];
    if (intVal < 0) {
        return [NSNumber numberWithInteger:(intVal * -1)];
    }
    return value;
}

最後在main.m中進行測試:

#import 
#import "Calculator.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Calculator *calculator = [[Calculator alloc] init];
        NSNumber *addend1 = [NSNumber numberWithInteger:-25];

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        SEL selector = NSSelectorFromString(@"absoluteValue:");
        NSLog(@"Invoking instance method %@ on object of class %@", NSStringFromSelector(selector), [calculator className]);
        NSLog(@"Absolute value of %@ = %@", addend1, [calculator performSelector:selector withObject:addend1]);
#pragma clang diagnostic pop
    }
    return 0;
}

運行結果:

2016-07-06 14:26:24.324 Calculator[12102:117902] Invoking instance method absoluteValue on object of class Calculator
2016-07-06 14:26:24.325 Calculator[12102:117902] Dynamically added instance method absoluteValue to class Calculator
2016-07-06 14:26:24.325 Calculator[12102:117902] Absolute value of -25 = 25

如運行結果所示,當使用選擇器absoluteValue:的消息通過performSelector方法被以動態方式調用時,Objective-C運行時系統會將新方法添加到Calculator類中。

動態加載

Objective-C程序通過動態加載功能可以根據需要加載可執行代碼和源代碼,而無需在啟動程序時就加載程序的所有組件。該方式不僅降低了對系統內存的需求,還提高了程序的可擴展性,因為它能夠使新軟件在不更改已存在程序的情況下,以動態方式將新增代碼添加到程序中。蘋果公司提供了以動態方式加載軟件的包bundle機制。

包是一種軟件交付機制。它由具有標准層次結構的目錄以及該目錄中的可執行代碼和源代碼構成。包可以含有可執行代碼、圖像、音頻文件、和其他類型的代碼與資源整合。它還含有一個運行時配置文件,即信息屬性列表info.plist。包可以分為3類:
1.應用程序包
2.框架包(如Foundation框架)
3.可選加載包(也稱為插件,用於動態加載的自定義包)

可以使用Foundation框架中的NSBundle類管理包。一個NSBundle對象就代表文件系統中的一個存儲位置,該位置存儲著可在程序中使用的代碼和數據資源。

下面是動態加載信息屬性列表和框架對象的示例:

// 動態加載信息屬性列表
NSBundle *bundle = [NSBundle mainBundle];
NSString *bundlePath = [bundle pathForResource:@"Info" ofType:@"plist"];

// 動態加載框架對象   
NSBundle *testBundle = [NSBundle bundleWithPath:@"/Test.bundle"];
id tester = [[[bundle classNamed:@"Tester"] alloc] init];

內省

Foundation框架中NSObject類的API含有非常多用於執行對象內省的方法,使用這些方法能夠以動態方式在程序運行時查詢與方法有關的信息和測試對象的繼承性、行為和一致性的信息。

下面是一些內省的語句:

// 檢測calculator是Calculator類的實例還是Calculator類子類的實例
BOOL isCalculator = [calculator isKindOfClass:[Calculator class]];
// 檢測calculator是否會對選擇器做出回應,即該對象是否實現或繼承了能夠對指定消息作出回應的方法
BOOL responds = [calculator respondsToSelector:@selector(sumAddend1::)];
// 檢測calculator是否遵守指定的協議
BOOL conforms = [calculator conformsToProtocol:@protocol(Writer)];
// 為選擇器提取方法簽名
NSMethodSignature *signature = [calculator methodSignatureForSelector:@selector(sumAddend1::)];

運行時系統的組成部分

運行時系統由兩個主要部分構成:編譯器和運行時系統庫。

編譯器

編譯器主要功能有兩個:

1.生成對象消息傳遞代碼
編譯器會將源代碼中所有消息傳遞表達式([接收器 消息]形式的),轉換為調用運行時系統庫函數objc_msgSend(…)的代碼,並為這些調用代碼提供源代碼所提供的參數。

2.生成類和對象的代碼
編譯器解析含有類定義和對象的代碼時,會生成相應的運行時數據結構:
Objective-C中的類與運行時系統庫中的Class數據結構對應。Class數據類型是指向帶objc_class表示符的不透明數據類型的指針:

typedef struct objc_class *Class;

Objective-C對象也擁有相應的運行時數據類型:

struct objc_object
{
   Class isa;
};

isa變量就是指向objc_class類型的指針。

Objective-C中id數據類型對應的運行時數據類型也是一種C語言結構,該結構被定義為指向objc_object的指針:

typedef struct objc_object
{
   Class isa;
}*id;

下面用一個例子來查看運行時系統的數據結構

#import 
#import 

//創建測試類
@interface TestClass1 : NSObject{
@public int myInt;
}
@end

@implementation TestClass1
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 為測試類創建兩個實例並顯示其數據
        TestClass1 *tc1A = [[TestClass1 alloc] init];
        tc1A->myInt = 0xa5a5a5a5;
        TestClass1 *tc1B = [[TestClass1 alloc] init];
        tc1B->myInt = 0xc3c3c3c3;
        long tc1Size = class_getInstanceSize([TestClass1 class]);
        NSData *obj1Data = [NSData dataWithBytes:(__bridge const void *)(tc1A) length:tc1Size];
        NSData *obj2Data = [NSData dataWithBytes:(__bridge const void *)(tc1B) length:tc1Size];
        NSLog(@"TestClass1 object tc1 contains %@", obj1Data);
        NSLog(@"TestClass1 object tc2 contains %@", obj2Data);
        NSLog(@"TestClass1 memory address = %p", [TestClass1 class]);

        // 獲取並顯示TestClass1類的數據
        id testClz = objc_getClass("TestClass1");
        long tcSize = class_getInstanceSize([testClz class]);
        NSData *tcData = [NSData dataWithBytes:(__bridge const void *)(testClz) length:tcSize];
        NSLog(@"TestClass1 class contains %@", tcData);
        NSLog(@"TestClass1 superclass memory address = %p", [TestClass1 superclass]);
    }
    return 0;
}

運行結果:

2016-07-06 15:55:11.030 Runspector[16745:165936] TestClass1 object tc1 contains <21120000 01801d00 a5a5a5a5 00000000>
2016-07-06 15:55:11.031 Runspector[16745:165936] TestClass1 object tc2 contains <21120000 01801d00 c3c3c3c3 00000000>
2016-07-06 15:55:11.031 Runspector[16745:165936] TestClass1 memory address = 0x100001220
2016-07-06 15:55:11.031 Runspector[16745:165936] TestClass1 class contains 
2016-07-06 15:55:11.031 Runspector[16745:165936] TestClass1 superclass memory address = 0x7fff773310f0

下面對這些數據進行分析。當編譯器解析對象時,就會生成objc_object類型的實例,該實例由一個isa指針和對象實例變量的值構成,就可以查明tc1對象含有兩項內容:一個isa指針(21120000 01801d00)和該對象實例變量的值(a5a5a5a5 00000000),類似地,tc2對象的isa指針(21120000 01801d00)和實例變量值(c3c3c3c3 00000000)。對象objc_object的數據結構中的第一項就是其isa指針,兩個對象的isa指針都是相同的,因為它們都是同一個類的實例,都指向該類的內存地址。

而之後的一行中顯示的TestClass1地址與前面的isa指針值卻不相同,這是因為,Mac計算機使用的是低字節序,它們會使用反轉的字節順序存儲數據(8位為一個字節,由2位16進制數表示)。將isa指針值翻轉後得到地址為0x1d80100001221,而後面輸出的TestClass1地址為0x100001220。在書上這兩個地址是完全一致的,但博主做了多次驗證發現類地址總是為isa地址去掉高位後減1,這可能是編譯環境或操作系統版本導致的(而後面的例子又沒有此問題)。

下一行輸出的是TestClass1中的內容,包含兩個指針,isa指針(f9110000 01801d00)和指向父類的指針(f0103377 ff7f0000),最後一行輸出的是TestClass1父類的地址0x7fff773310f0,與TestClass1中指向父類的指針按字節翻轉後完全一致。

運行時系統庫

下面是運行時系統庫API的簡單應用:

#import 
#import 
#import 

NSString *greeting(id self, SEL _cmd){
    return [NSString stringWithFormat:@"Hello, World!"];
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 以動態方式創建一個類
        Class dynaClass = objc_allocateClassPair([NSObject class], "DynaClass", 0);

        // 以動態方式添加一個方法,使用已有方法(NSObject類中的description方法,兩個方法的參數和返回值類型完全相同)獲取特征
        Method description = class_getInstanceMethod([NSObject class], @selector(description));
        const char *types = method_getTypeEncoding(description);
        class_addMethod(dynaClass, @selector(greeting), (IMP)greeting, types);

        // 注冊這個類
        objc_registerClassPair(dynaClass);

        // 使用該類創建一個實例並向其發送一條消息
        id dynaObj = [[dynaClass alloc] init];
        NSLog(@"%@",objc_msgSend(dynaObj,NSSelectorFromString(@"greeting")));
    }
    return 0;
}

注意:在目前版本的Xcode中,需要將 Apple LLVM X.X(版本號) - Preprocessing 中的 Enable Strict Checking of objc_msgSend Calls 設置為No,不然編譯會失敗。

運行時系統庫含有可用於訪問下列信息的函數

信息 函數 對象的類定義 objc_getClass 對象的元類定義 objc_getMetaClass 類的父類 class_getSuperClass 類的名稱 class_getName 類的版本信息 class_getVersion 以字節為單位的類尺寸 class_getInstanceSize 類的實例變量列表 class_copyIvarList 類的方法列表 class_copyMethodList 類的協議列表 class_copyProtocalList 類的屬性列表 class_copyProperyList

運行時系統中的方法數據類型

struct objc_method
{
    // 描述方法的名稱
    SEL method_name;
    // 描述方法參數的數據類型
    char * method_types;
    // 方法的地址
    IMP method_imp;
};
typedef objc_method Method;

通過虛函數表查找方法的流程:

Created with Rapha?l 2.1.0開始查找通過對象的isa指針,獲取該對象所屬的類通過搜索類方法緩存,查找方法的IMP指針是否找到了方法?跳轉到存儲方法代碼的地址並執行方法通過對象的isa指針,獲取該對象所屬的類是否找到了方法?跳轉到存儲方法代碼的地址並執行方法依次嘗試使用動態方法決議,快速轉發,標准轉發的方式嘗試找到可以處理消息的方法是否解決運行時系統發送一條doseNotRecongniseSelector:消息yesnoyesnoyesno

元類

實際上Objective-C中的類也是對象,因此它們也能接受消息。運行時系統是通過元類的實現這個功能的,元類是一種特殊的類對象,運行時系統使用其中含有的信息能夠找到並調用類方法。每個類都擁有一個獨一無二的元類。運行時系統API提供可訪問元類的函數,以下是對元類的操作示例:

        id metaClass = objc_getMetaClass("TestClass1");
        long mclzSize = class_getInstanceSize([metaClass class]);
        NSData *mclzData = [NSData dataWithBytes:(__bridge const void *)(metaClass) length:mclzSize];
        NSLog(@"TestClass1 class contains %@", mclzData);
        class_isMetaClass(metaClass) ? NSLog(@"Class %s is a metaclass",class_getName(metaClass)) : NSLog(@"Class %s is  not a metaclass",class_getName(metaClass));

運行結果:

2016-07-06 17:09:13.177 Runspector[20513:203884] TestClass1 class contains <19113377 ffff1d00 18113377 ff7f0000 00592000 01000000 07000000 01000000 d0006000 01000000>
2016-07-06 17:09:13.177 Runspector[20513:203884] Class TestClass1 is a metaclass

元類中含有isa指針,父指針和附加信息,TestClass1的父類是NSObject。因為這個類中沒有自定義的類方法,所以它的isa指針和父指針都指向NSObject類。這裡兩個指針的值也跟之前有同樣的問題(19113377 ffff1d00和18113377 ff7f0000按字節翻轉後去掉高位後相差1,原書中運行結果是完全相同的。)

與運行時系統交互

Objective-C程序通過與運行時系統交互實現動態特性,這些交互操作分為3個等級:Objective-C源代碼,Foundation框架中的NSObject類的方法,運行時系統庫API。

之前介紹了編譯器和運行時系統庫的作用,接下來展示NSObject類的運行時特性:

#import 

// 創建一個測試類
@interface Greeter : NSObject
@property(readwrite, strong) NSString *salutation;
-(NSString *)greeting:(NSString *) recipient;
@end
@implementation Greeter
-(NSString *)greeting:(NSString *)recipient{
    return [NSString stringWithFormat:@"%@,%@",[self salutation],recipient];
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Greeter *greeter = [[Greeter alloc] init];
        [greeter setSalutation:@"Hello"];

        // 使用NSObject類的對象內省,逐步測試對象是否能響應方法和遵循協議
        if ([greeter respondsToSelector:@selector(greeting:)] && [greeter conformsToProtocol:@protocol(NSObject)]) {
             // 使用運行時方法performSelector發送消息
            id result = [greeter performSelector:@selector(greeting:) withObject:@"Monster!"];
            NSLog(@"%@",result);
        }
    }
    return 0;
}
  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved