iOS-RunTime

原帖:http://www.cnblogs.com/Mike-zh/p/4557014.htmlhtml

1.Runtime簡介

    由於Objc是一門動態語言,因此它老是想辦法把一些決定工做從編譯鏈接推遲到運行時。也就是說只有編譯器是不夠的,還須要一個運行時系統 (runtime system) 來執行編譯後的代碼。這就是 Objective-C Runtime 系統存在的意義,它是整個Objc運行框架的一塊基石。
    Runtime其實有兩個版本:「modern」和 「legacy」。咱們如今用的 Objective-C 2.0 採用的是現行(Modern)版的Runtime系統,只能運行在 iOS 和 OS X 10.5 以後的64位程序中。而OS X較老的32位程序仍採用 Objective-C 1中的(早期)Legacy 版本的 Runtime 系統。這兩個版本最大的區別在於當你更改一個類的實例變量的佈局時,在早期版本中你須要從新編譯它的子類,而現行版就不須要。
    Runtime基本是用C和彙編寫的,可見蘋果爲了動態系統的高效而做出的努力。你能夠在這裏下到蘋果維護的開源代碼。蘋果和GNU各自維護一個開源的runtime版本,這兩個版本之間都在努力的保持一致。ios

2.Runtime相關的頭文件   

ios的sdk中 usr/include/objc文件夾下面有這樣幾個文件
List.h
NSObjCRuntime.h
NSObject.h
Object.h
Protocol.h
a.txt
hashtable.h
hashtable2.h
message.h
module.map
objc-api.h
objc-auto.h
objc-class.h
objc-exception.h
objc-load.h
objc-runtime.h
objc-sync.h
objc.h
runtime.h
    都是和運行時相關的頭文件,其中主要使用的函數定義在message.h和runtime.h這兩個文件中。 在message.h中主要包含了一些向對象發送消息的函數,這是OC對象方法調用的底層實現。 runtime.h是運行時最重要的文件,其中包含了對運行時進行操做的方法。 主要包括:api

  • 1. 操做對象的類型的定義
/// An opaque type that represents a method in a class definition. 一個類型,表明着類定義中的一個方法
typedef struct objc_method *Method;

/// An opaque type that represents an instance variable.表明實例(對象)的變量
typedef struct objc_ivar *Ivar;

/// An opaque type that represents a category.表明一個分類
typedef struct objc_category *Category;

/// An opaque type that represents an Objective-C declared property.表明OC聲明的屬性
typedef struct objc_property *objc_property_t;

// Class表明一個類,它在objc.h中這樣定義的 typedef struct objc_class *Class;
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE; 

    這些類型的定義,對一個類進行了徹底的分解,將類定義或者對象的每個部分都抽象爲一個類型type,對操做一個類屬性和方法很是方便。OBJC2_UNAVAILABLE標記的屬性是Ojective-C 2.0不支持的,但實際上能夠用響應的函數獲取這些屬性,例如:若是想要獲取Class的name屬性,能夠按以下方法獲取:數組

Class classPerson = Person.class;
// printf("%s\n", classPerson->name); //用這種方法已經不能獲取name了 由於OBJC2_UNAVAILABLE
const char *cname = class_getName(classPerson);
printf("%s", cname); // 輸出:Person
  • 2.函數的定義

    對對象進行操做的方法通常以object_開頭
    對類進行操做的方法通常以class_開頭
    對類或對象的方法進行操做的方法通常以method_開頭
    對成員變量進行操做的方法通常以ivar_開頭
    對屬性進行操做的方法通常以property_開頭開頭
    對協議進行操做的方法通常以protocol_開頭
    根據以上的函數的前綴 能夠大體瞭解到層級關係。對於以objc_開頭的方法,則是runtime最終的管家,能夠獲取內存中類的加載信息,類的列表,關聯對象和關聯屬性等操做。
    例如:使用runtime對當前的應用中加載的類進行打印,別被嚇一跳。框架

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    unsigned int count = 0;
    Class *classes = objc_copyClassList(&count);
    for (int i = 0; i < count; i++) {
        const char *cname = class_getName(classes[i]);
        printf("%s\n", cname);
    }
}

 

3.技術點和應用場景

    在如下的代碼中,都用到了Person類,Person類知識簡單的定義了一個成員變量和兩個屬性函數

@interface Person : NSObject
{
    @private
    float _height;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@end
  • 3_1.獲取屬性\成員變量列表

    對於獲取成員變量的列表可使用class_copyIvarList函數,若是想要獲取屬性列表可使用class_copyPropertyList函數,使用的示例以下:佈局

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {

Class classPerson = NSClassFromString(@"Person"); // 與下面一句效果同樣,能夠不用導入頭文件
// Class clazz = Person.class;
unsigned int count = 0;
Ivar *ivarList = class_copyIvarList(classPerson, &count); // 獲取成員變量數組
for (int i = 0; i < count; i++) {
const char *cname = ivar_getName(ivarList[i]); // 獲取成員變量的名字
NSString *name = [NSString stringWithUTF8String:cname];
NSLog(@"%@", name);
}
NSLog(@"-------------------分割線------------------");
objc_property_t *propertyList = class_copyPropertyList(classPerson, &count); // 獲取屬性數組
for (int i = 0; i < count; i++) {
const char *cname = property_getName(propertyList[i]);
NSString *name = [NSString stringWithUTF8String:cname];
NSLog(@"%@", name);
}

}

以上代碼的輸出爲:測試

2015-06-05 22:28:16.194 runtime終極[4192:195757] _height
2015-06-05 22:28:16.195 runtime終極[4192:195757] _age
2015-06-05 22:28:16.195 runtime終極[4192:195757] _name
2015-06-05 22:28:16.195 runtime終極[4192:195757] -------------------分割線------------------
2015-06-05 22:28:16.195 runtime終極[4192:195757] name
2015-06-05 22:28:16.195 runtime終極[4192:195757] age

    爲何會有上面的輸出結果,由於@property會作三份工做:
    1.生成一個帶下劃線的成員變量
    2.生成這個成員變量的get方法
    3.生成這個成員變量的set方法
    所以會輸出三個成員變量_height、_age和_name。須要注意的是屬性名是不帶下劃線的,和定義時的名字同樣。所以能夠說:ivarList能夠獲取到@property關鍵字定義的屬性 ,而propertyList不能夠獲取到成員變量。也就是:使用ivarList是能夠將全部的成員變量和屬性都獲取的。
    當屬性是readonly的並且重寫了getter時,這種狀況仍是會碰見的,好比一個屬性是計算型屬性,須要依賴其餘屬性的值計算而來。此時生成的帶下劃線的成員變量就不在了, 經過ivarList不能獲取該屬性了。所以當有這種值的時候,不管使用ivarList仍是使用propertyList都沒法獲取所有的屬性或變量。
    在進行下一個話題以前:先須要弄清楚另外一個問題:對於一個readonly的屬性,究竟是didSet+set好,仍是重寫getter好?
    大部分的readonly的屬性是計算型的,依舊是依賴於其餘屬性,所以可使用didSet+set,也就是在其餘屬性的set方法內,將本屬性set。 可是didSet+set有時候徹底沒有必要,不符合懶加載的規則,浪費了計算能力,用重寫getter的方法好一些。 所以重寫getter老是會好一點。
    迴歸正題:在KVC時,想要獲取所有的成員變量和屬性, 怎麼辦呢?
    首先要了解setValue: forKeyPath:方法的底層實現:以name屬性爲例
1.首先先去類的方法列表去尋找有木有setName:,若是有,就直接調用[person setName:value]
2.找找有沒有帶下劃線的成員變量_name,若是有 _name = value;
3.找有沒有成員變量name,若是有 name = value;
4.若是都沒有找到,就直接報錯。
    所以對於readonly的又重寫了getter的屬性而言:若是對propertyList的屬性一次使用kvc,就會報錯,所以爲保證代碼正常,不能使用propertyList的屬性進行kvc;
    另外:這種屬性原本就是計算型的了,爲何還有爲它賦值呢,所以對它進行kvc也不合情理。
    當使用ivaList時,直接就沒法獲取到這種屬性,所以是kvc的最佳方案。再者,使用propertyList沒法獲取成員變量(_height),沒法對成員變量進行賦值。而使用ivaList是能夠將該賦值的成員變量都獲取的。
    <span以上就是使用ivar仍是使用property進行kvc的論證。
    話題外: 不少類 有些成員變量 既沒有暴露給外部調用的getter又沒有setter,只是用@private聲明瞭一下:爲何??
    猜想是:是方法調用時使用的中間變量,由於是跟隨對象產生,不適合使用靜態static,又由於外部不會使用,因此不必給外部提供接口,可是可能有好幾個方法都須要這個量,不適合作局部變量,因此就這樣定義了。
    對於這種狀況,要想不對這種成員變量賦值,在KVC時又能夠這樣改進一下,經過ivarList獲取,去掉propertyList中沒有的成員變量,這樣就過濾掉了上面的那種成員變量了。this

  •     3_1_1.應用1:KVC字典轉模型

    獲取屬性\成員列表一個重要的應用就是,一次取出模型中的屬性\成員變量,根據它的名字獲取字典中的key而後取出字典中這個key對應的value,使用setValue:      forKeyPath:方法設置值。爲何要這樣,而再也不使用方法setValuesForKeysWithDictionary:。由於在setValuesForKeysWithDictionary:方法內部會執行這樣一個過程
遍歷字典裏面的全部key,一個一個取出來,遍歷每一個key按照如下過程
1.取出key,
2.取出key的value,即dict[key],直接給模型的屬性\成員變量賦值
3.怎麼給模型的屬性賦值,使用方法setValue:value forKeyPath:key進行賦值,這個方法的執行過程在前面已經提到。
    所以,開發中常常遇到的字典中的key比模型中多時,會出現的 this class is not key-value compliant for ‘xxx’這個bug就很好解釋了,一般是由於字典中的key,比模型中的屬性\成員變量多。那麼當模型中的屬性比字典中多時,使用setValuesForKeysWithDictionary:會不不會有bug呢?經測試:當多出來的屬性是對象數據類型時,爲null,當屬性是基本數據類型時,會有一個系統默認值(如int爲0)。
    所以使用逐一爲屬性賦值的方法進行KVC:編碼

Class clazz = Person.class;
unsigned int count = 0;

Person *person = [[Person alloc]init];
NSDictionary *dict = @{@"name":@"zhangsan",@"age":@19, @"height": @1.75};

Ivar *ivars = class_copyIvarList(clazz, &count);
// NSLog(@"%tu", count); // 3
for (int i = 0; i < count; i++) {
const char *cname = ivar_getName(ivars[i]);
NSString *name = [NSString stringWithUTF8String:cname];
NSString *key = [name substringFromIndex:1]; // 去掉'_'
[person setValue:dict[key] forKey:key];
}
NSLog(@"%@", person); // 已經重寫了description方法 輸出是:
<Person, 0x7ff15b80f230>{ name = zhangsan, height = 1.750000, age = 19}

    使用這種方式進行kvc,即便字典中的key多的時候也不會有bug,可是新的問題出現了,若是模型中的屬性比字典中的key多便會出現bug並且:若是多的是對象類型不會有bug,該屬性的值爲null,若是是基本數據類型就會出錯could not set nil as the value for the key ‘xxx’。例如,將上面的字典改成:
NSDictionary *dict = @{@"age":@19, @"height": @1.75}; // 去掉了name NSString類型 修改以後輸出爲:
<Person, 0x7f996263fbd0>{ name = (null), height = 1.750000, age = 19} 若是將字典改成:
NSDictionary *dict = @{@"name":@"zhangsan",@"age":@19}; // 去掉了height float類型 程序直接崩潰。
    如何解決上面的bug:能夠在setValue:value forKeyPath:key方法調用以前進行以下處理:取出屬性對應的類型,若是類型是基本數據類型,value替換爲默認值(如int對應默認值爲0)。
runtime提供的ivar_getTypeEncoding函數能夠獲取到屬性的類型,返回值表明的含義以下:

height是float類型對應的TypeCode是"f"所以能夠進行過濾一下,代碼改動以下:

Class clazz = Person.class;
unsigned int count = 0;

Person *person = [[Person alloc]init];
NSDictionary *dict = @{@"name":@"zhangsan",@"age":@19, @"height": @1.75};

Ivar *ivars = class_copyIvarList(clazz, &count);
for (int i = 0; i < count; i++) {
const char *cname = ivar_getName(ivars[i]);
NSString *name = [NSString stringWithUTF8String:cname];
NSString *key = [name substringFromIndex:1];

const char *coding = ivar_getTypeEncoding(ivars[i]); // 獲取類型
NSString *strCode = [NSString stringWithUTF8String:coding];
id value = dict[key];
if ([strCode isEqualToString:@"f"]) {// 判斷類型是不是float
value = @(0.0);
}

[person setValue:value forKey:key];
}
NSLog(@"%@", person); 這樣就能夠正常執行了,輸出爲:
<Person, 0x7fc75d004a00>{ name = zhangsan, height = 0.000000, age = 19}
  • 3_1_2.應用2:NSCoding歸檔和解檔

    獲取屬性\成員列表另一個重要的應用就是進行歸檔和解檔,其原理和上面的kvc基本上同樣,這裏只是展現一些代碼:

- (void)encodeWithCoder:(NSCoder *)aCoder {
unsigned int count = 0;
Ivar *ivars = class_copyIvarList(self.class, &count);
for (int i = 0; i < count; i++) {
const char *cname = ivar_getName(ivars[i]);
NSString *name = [NSString stringWithUTF8String:cname];
NSString *key = [name substringFromIndex:1];

id value = [self valueForKey:key]; // 取出key對應的value
[aCoder encodeObject:value forKey:key]; // 編碼
}
}
- (id)initWithCoder:(NSCoder *)aDecoder {
if (self = [super init]) {
unsigned int count = 0;
Ivar *ivars = class_copyIvarList(self.class, &count);
for (int i = 0; i < count; i++) {
const char *cname = ivar_getName(ivars[i]);
NSString *name = [NSString stringWithUTF8String:cname];
NSString *key = [name substringFromIndex:1];

id value = [aDecoder decodeObjectForKey:key]; // 解碼
[self setValue:value forKey:key]; // 設置key對應的value
}
}
return self; 
}
  • 3_2.交換方法實現

    交換方法實現的需求場景:本身建立了一個功能性的方法,在項目中屢次被引用,當項目的需求發生改變時,要使用另外一種功能代替這個功能,要求是不改變舊的項目(也就是不改變原來方法的實現)。
    能夠在類的分類中,再寫一個新的方法(是符合新的需求的),而後交換兩個方法的實現。這樣,在不改變項目的代碼,而只是增長了新的代碼 的狀況下,就完成了項目的改進。
    交換兩個方法的實現通常寫在類的load方法裏面,由於load方法會在程序運行前加載一次,而initialize方法會在類或者子類在 第一次使用的時候調用,當有分類的時候會調用屢次。

// 程序一運行的時候調用
+ (void)load
{
// 若是是類方法,使用的是class_getClassMethod,若是是對象方法使用的是class_getInstanceMethod
Method methodOne = class_getInstanceMethod(self, @selector(methodOne:));
Method methodTwo = class_getInstanceMethod(self, @selector(methodTwo:));
// 交換兩個方法的實現
method_exchangeImplementations(methodOne, methodTwo);
} 


注意的是
1.能夠交換的兩個方法的參數必須是匹配的,參數的類型一致。
2.若是在方法one的內部想要調用方法two,此時在方法one的內部應該用one調用,而其實是在調用two,不然會形成死循環。
例如:

// 交換前
- (NSString *) methodOne:(NSString *)str{
    NSLog(@"%@", [self methodTwo:str]);
    return "suc";
}
// 交換後 在方法的實現中,要注意將調用two的地方,換成本身的名字
- (NSString *) methodOne:(NSString *)str{
    NSLog(@"%@", [self methodOne:str]);
    return "suc";
}

    任何一個方法都有兩個重要的屬性:SEL是方法的編號 ,IMP是方法的實現,方法的調用過程實際上去根據SEL去尋找IMP。
    在這個例子中,假設在交換以前SEL爲methodOne:的方法指向着IMP1,SEL爲methodTwo的方法指向着IMP2。
    交換實現其實是在底層是交換了方法編號的指向,也就是讓methodOne:指向了IMP2,methodTwo指向了IMP1。
應用場景

  • 3_3.類\對象的關聯對象

    關聯對象不是爲類\對象添加屬性或者成員變量(由於在設置關聯後也沒法經過ivarList或者propertyList取得),而是爲類添加一個相關的對象,一般用於存儲類信息,例如存儲類的屬性列表數組,爲未來字典轉模型的方便。 例如,將屬性的名稱存到數組中設置關聯

const char* propertiesKey = "propertiesKey";
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([Person class], &count);

NSMutableArray *arrayM = [NSMutableArray arrayWithCapacity:count];

for (unsigned int i = 0; i < count; ++i) {
    Ivar pty = ivars[i];
    const char *cname = ivar_getName(ivars[i]);
    NSString *name = [NSString stringWithUTF8String:cname];
    NSString *key = [name substringFromIndex:1]; // 去掉_
    [arrayM addObject:key];
}
free(ivars);
objc_setAssociatedObject(self, propertiesKey, arrayM, OBJC_ASSOCIATION_COPY_NONATOMIC);
NSLog(@"%@", arrayM); 輸出是
(
    age,
    height,
    name
)objc_setAssociatedObject
方法的參數解釋: 第一個參數id object, 當前對象 第二個參數const void *key, 關聯的key,是c字符串 第三個參數id value, 被關聯的對象 第四個參數objc_AssociationPolicy policy關聯引用的規則,取值有如下幾種:
enum {
    OBJC_ASSOCIATION_ASSIGN = 0,
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
    OBJC_ASSOCIATION_RETAIN = 01401,
    OBJC_ASSOCIATION_COPY = 01403
}; 
若是想要獲取已經關聯的對象,經過key取得便可
NSArray *pList = objc_getAssociatedObject(Person, propertiesKey); 能夠將以上兩種操做封裝起來,爲Person類增長一個properties類方法,封裝上面的操做,用於方便獲取類的屬性列表。
const char* propertiesKey = "propertiesKey";

@implementation Person
+ (NSArray *)properties {

    NSArray *pList = objc_getAssociatedObject(self, propertiesKey);
    if (pList != nil) {
        return pList;
    }

    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList([self class], &count);

    NSMutableArray *arrayM = [NSMutableArray arrayWithCapacity:count];

    for (unsigned int i = 0; i < count; ++i) {
        Ivar pty = ivars[i];
        const char *cname = ivar_getName(ivars[i]);
        NSString *name = [NSString stringWithUTF8String:cname];
        NSString *key = [name substringFromIndex:1];
        [arrayM addObject:key];
    }
    free(ivars);
    objc_setAssociatedObject(self, propertiesKey, arrayM,                          OBJC_ASSOCIATION_COPY_NONATOMIC);
    return arrayM.copy;
}
@end            
  • 3_4.動態添加方法,攔截未實現的方法

    每一個類都有都有一下兩個類方法(來自NSObject)
+ (BOOL)resolveClassMethod:(SEL)sel
+ (BOOL)resolveInstanceMethod:(SEL)sel
    以上兩個一個使用於類方法,一個適用於對象方法。在代碼中調用沒有實現的方法時,也就是sel標識的方法沒有實現 都會現調用這兩個方法中的一個(若是是類方法就調用第一個,若是是對象方法就調用第二個)攔截。 一般的作法是在resolve的內部指定sel對應的IMP,從而完成方法的動態建立和調用兩個過程,也能夠不指定IMP打印錯誤信息後直接返回。
    假如在Person類中沒有-sayHi這個方法,若是對象p使用[p performSelector:@selector(sayHi) withObject:nil];那麼就會必須通過Person類的resolveInstanceMethod:(SEL)sel方法,在這裏爲-sayHi指定實現。

void abc(id self, SEL _cmd){
    NSLog(@"%@說了hello", [self name]);
}

@implementation Person

//動態添加方法:在resolve中添加相應的方法,注意是類方法仍是對象方法。
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if ([NSStringFromSelector(sel) isEqualToString:@"sayHi"]) {
        class_addMethod(self, sel, abc, "v@:"); // 爲sel指定實現爲abc
    }
    return YES;
}
@end    

對實現(abc)的前兩個參數的說明
  每一個方法的內部都默認包含兩個參數,被稱爲隱式參數
  id類型self(表明類或對象)和SEL類型的_cmd(方法編號)
  class_addMethod函數參數的含義:
  第一個參數Class cls, 類型
  第二個參數SEL name, 被解析的方法
  第三個參數 IMP imp, 指定的實現
  第四個參數const char *types,方法的類型,具體參照類型的codeType那張圖,可是要注意一點:Since the function must take at least two arguments—self and _cmd, the second and third characters must be 「@:」 (the first character is the return type).譯爲:由於函數必須至少有兩個參數self和_cmd,第二個和第三個字符必須是「@:」。若是想要再增長參數,就能夠從實現的第三個參數算起,看下面的例子就明白。
返回值:YES if the method was found and added to the receiver, otherwise NO.
爲-sayHi方法的實現增長參數
  調用時:

Person *p = [[Person alloc] init];

p.name = @"zhangsan";
p.age = 10;
[p performSelector:@selector(sayHi:) withObject:@"world"]; // 增長了一個參數,多了冒號 對Person類中的代碼作了修改
void abc(id self, SEL _cmd, NSString *content){ // 增長了一個參數content
    NSLog(@"%@說了hello%@", [self name], content);
}

@implementation Person

//動態添加方法:在resolve中添加相應的方法,注意是類方法仍是對象方法。
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if ([NSStringFromSelector(sel) isEqualToString:@"sayHi:"]) {
        class_addMethod(self, sel, abc, "v@:@"); // 增長了一個對象類型參數 增長了@
    }
    return YES;
}
@end 
輸出爲: zhangsan說了helloworld
  • 3_5.動態建立一個類

動態建立一個類,爲這個類添加成員變量和方法,並建立這個類型的對象:

#import "ViewController.h"
#import <objc/runtime.h>
#import <objc/message.h>
#import "Person.h"

static void printSchool(id self, SEL _cmd) {
    NSLog(@"個人學校是%@", [self valueForKey:@"schoolName"]);
}

@implementation ViewController

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    Class classStudent = objc_allocateClassPair(Person.class, "Student", 0);

    // 添加一個NSString的變量,第四個參數是對其方式,第五個參數是參數類型
    if (class_addIvar(classStudent, "schoolName", sizeof(NSString *), 0, "@")) {
        NSLog(@"添加成員變量schoolName成功");
    }

    // 爲Student類添加方法 "v@:"這種寫法見參數類型鏈接
    if (class_addMethod(classStudent, @selector(printSchool), (IMP)printSchool,     "v@:")) {
        NSLog(@"添加方法printSchool:成功");
    }

    // 註冊這個類到runtime系統中就可使用他了
    objc_registerClassPair(classStudent); // 返回void



    // 使用建立的類
    id student = [[classStudent alloc] init];
    NSString *schoolName = @"清華大學";
    // 給剛剛添加的變量賦值
    // object_setInstanceVariable(student, "schoolName", (void *)&str);在ARC下不容許使用
    [student setValue:schoolName forKey:@"schoolName"];
    // 調用printSchool方法,也就是給student這個接受者發送printSchool:這個消息
    // objc_msgSend(student, "printSchool"); // 我嘗試用這種方法調用可是沒有成功
    [student performSelector:@selector(printSchool) withObject:nil]; // 動態調用未顯式在類中聲明的方法

}
@end 輸出的結果是:
添加成員變量schoolName成功
添加方法printSchool成功
個人學校是清華大學
        
相關文章
相關標籤/搜索