ps:積少成多纵寝,積善成德
引言
曾經(jīng)的我并不知道運(yùn)行時(shí)這東西拢锹,因?yàn)槟莻€(gè)時(shí)候開(kāi)發(fā)并不是經(jīng)常用到這東西,我相信很多初出茅廬的開(kāi)發(fā)者也和我一樣鳞滨,每天雖然看似很忙,但是干的都是碼農(nóng)的活蟆淀。但是我也覺(jué)得這個(gè)過(guò)程是每個(gè)人必須經(jīng)歷的拯啦,你也不用灰心喪氣。好了熔任,不扯多了...今天我們一起來(lái)探討一下運(yùn)行時(shí)褒链。
簡(jiǎn)介
不知道你是否記得在C語(yǔ)言中當(dāng)我們是怎么調(diào)用一個(gè)函數(shù)的?我們都說(shuō)oc是一門(mén)動(dòng)態(tài)的語(yǔ)言笋敞,所以它總是想辦法把一些決定工作從編譯連接推遲到運(yùn)行時(shí)。也就是說(shuō)只有編譯器是不夠的荠瘪,還需要一個(gè)運(yùn)行時(shí)系統(tǒng) (runtime system)來(lái)執(zhí)行編譯后的代碼夯巷。這就是 Objective-C Runtime 系統(tǒng)存在的意義,它是整個(gè)Objc運(yùn)行框架的一塊基石哀墓。
Runtime其實(shí)有兩個(gè)版本:“modern”和 “l(fā)egacy”趁餐。我們現(xiàn)在用的 Objective-C 2.0采用的是現(xiàn)行(Modern)版的Runtime系統(tǒng),只能運(yùn)行在 iOS 和 OS X 10.5 之后的64位程序中篮绰。而OSX較老的32位程序仍采用 Objective-C 1中的(早期)Legacy 版本的 Runtime系統(tǒng)后雷。這兩個(gè)版本最大的區(qū)別在于當(dāng)你更改一個(gè)類(lèi)的實(shí)例變量的布局時(shí),在早期版本中你需要重新編譯它的子類(lèi),而現(xiàn)行版就不需要臀突。
Runtime基本是用C和匯編寫(xiě)的勉抓,可見(jiàn)蘋(píng)果為了動(dòng)態(tài)系統(tǒng)的高效而作出的努力。你可以在這里下到蘋(píng)果維護(hù)的開(kāi)源代碼候学。蘋(píng)果和GNU各自維護(hù)一個(gè)開(kāi)源的runtime版本藕筋,這兩個(gè)版本之間都在努力的保持一致。
交互
Objc 從三種不同的層級(jí)上與 Runtime 系統(tǒng)進(jìn)行交互梳码,分別是通過(guò) Objective-C 源代碼隐圾,通過(guò) Foundation 框架的NSObject類(lèi)定義的方法,通過(guò)對(duì) runtime 函數(shù)的直接調(diào)用掰茶。
Objective-C源代碼
大部分情況下你就只管寫(xiě)你的Objc代碼就行暇藏,runtime 系統(tǒng)自動(dòng)在幕后辛勤勞作著。
NSObject的方法
Cocoa 中大多數(shù)類(lèi)都繼承于NSObject類(lèi)濒蒋,也就自然繼承了它的方法盐碱。最特殊的例外是NSProxy,它是個(gè)抽象超類(lèi)啊胶,它實(shí)現(xiàn)了一些消息轉(zhuǎn)發(fā)有關(guān)的方法甸各,可以通過(guò)繼承它來(lái)實(shí)現(xiàn)一個(gè)其他類(lèi)的替身類(lèi)或是虛擬出一個(gè)不存在的類(lèi),說(shuō)白了就是領(lǐng)導(dǎo)把自己展現(xiàn)給大家風(fēng)光無(wú)限焰坪,但是把活兒都交給幕后小弟去干趣倾。
有的NSObject中的方法起到了抽象接口的作用,比如description方法需要你重載它并為你定義的類(lèi)提供描述內(nèi)容某饰。NSObject還有些方法能在運(yùn)行時(shí)獲得類(lèi)的信息儒恋,并檢查一些特性,比如class返回對(duì)象的類(lèi)黔漂;isKindOfClass:和isMemberOfClass:則檢查對(duì)象是否在指定的類(lèi)繼承體系中诫尽;respondsToSelector:檢查對(duì)象能否響應(yīng)指定的消息;conformsToProtocol:檢查對(duì)象是否實(shí)現(xiàn)了指定協(xié)議類(lèi)的方法炬守;methodForSelector:則返回指定方法實(shí)現(xiàn)的地址牧嫉。
Runtime的函數(shù)
Runtime 系統(tǒng)是一個(gè)由一系列函數(shù)和數(shù)據(jù)結(jié)構(gòu)組成,具有公共接口的動(dòng)態(tài)共享庫(kù)减途。頭文件存放于/usr/include/objc目錄下酣藻。許多函數(shù)允許你用純C代碼來(lái)重復(fù)實(shí)現(xiàn) Objc 中同樣的功能。雖然有一些方法構(gòu)成了NSObject類(lèi)的基礎(chǔ)鳍置,但是你在寫(xiě) Objc 代碼時(shí)一般不會(huì)直接用到這些函數(shù)的辽剧,除非是寫(xiě)一些 Objc 與其他語(yǔ)言的橋接或是底層的debug工作。在Objective-C Runtime Reference中有對(duì) Runtime 函數(shù)的詳細(xì)文檔税产。
Runtime術(shù)語(yǔ)
還記得引言中的objc_msgSend:方法吧怕轿,它的真身是這樣的:
id objc_msgSend (idself, SEL op, ... );
下面將會(huì)逐漸展開(kāi)介紹一些術(shù)語(yǔ)偷崩,其實(shí)它們都對(duì)應(yīng)著數(shù)據(jù)結(jié)構(gòu)。
SEL
objc_msgSend函數(shù)第二個(gè)參數(shù)類(lèi)型為SEL撞羽,它是selector在Objc中的表示類(lèi)型(Swift中是Selector類(lèi))阐斜。selector是方法選擇器,可以理解為區(qū)分方法的 ID放吩,而這個(gè) ID 的數(shù)據(jù)結(jié)構(gòu)是SEL:
typedef struct objc_selector *SEL;
其實(shí)它就是個(gè)映射到方法的C字符串智听,你可以用 Objc 編譯器命令@selector()或者 Runtime 系統(tǒng)的sel_registerName函數(shù)來(lái)獲得一個(gè)SEL類(lèi)型的方法選擇器。
不同類(lèi)中相同名字的方法所對(duì)應(yīng)的方法選擇器是相同的渡紫,即使方法名字相同而變量類(lèi)型不同也會(huì)導(dǎo)致它們具有相同的方法選擇器到推,于是 Objc 中方法命名有時(shí)會(huì)帶上參數(shù)類(lèi)型(NSNumber一堆抽象工廠方法拿走不謝),Cocoa 中有好多長(zhǎng)長(zhǎng)的方法哦惕澎。
id
objc_msgSend第一個(gè)參數(shù)類(lèi)型為id莉测,大家對(duì)它都不陌生,它是一個(gè)指向類(lèi)實(shí)例的指針:
typedef struct objc_object *id;
那objc_object又是啥呢:
struct objc_object { Class isa; };
objc_object結(jié)構(gòu)體包含一個(gè)isa指針唧喉,根據(jù)isa指針就可以順藤摸瓜找到對(duì)象所屬的類(lèi)捣卤。
PS:isa指針不總是指向?qū)嵗龑?duì)象所屬的類(lèi),不能依靠它來(lái)確定類(lèi)型八孝,而是應(yīng)該用class方法來(lái)確定實(shí)例對(duì)象的類(lèi)董朝。因?yàn)镵VO的實(shí)現(xiàn)機(jī)理就是將被觀察對(duì)象的isa指針指向一個(gè)中間類(lèi)而不是真實(shí)的類(lèi),這是一種叫做isa-swizzling的技術(shù)干跛,詳見(jiàn)官方文檔
Class
之所以說(shuō)isa是指針是因?yàn)镃lass其實(shí)是一個(gè)指向objc_class結(jié)構(gòu)體的指針:
typedef struct objc_class *Class;
而objc_class就是我們摸到的那個(gè)瓜子姜,里面的東西多著呢:
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;
可以看到運(yùn)行時(shí)一個(gè)類(lèi)還關(guān)聯(lián)了它的超類(lèi)指針,類(lèi)名楼入,成員變量哥捕,方法,緩存嘉熊,還有附屬的協(xié)議遥赚。
PS:OBJC2_UNAVAILABLE之類(lèi)的宏定義是蘋(píng)果在? Objc? 中對(duì)系統(tǒng)運(yùn)行版本進(jìn)行約束的黑魔法,為的是兼容非Objective-C 2.0的遺留邏輯阐肤,但我們?nèi)阅軓闹蝎@得一些有價(jià)值的信息凫佛,有興趣的可以查看源代碼。
Objective-C 2.0 的頭文件雖然沒(méi)暴露出objc_class結(jié)構(gòu)體更詳細(xì)的設(shè)計(jì)孕惜,我們依然可以從Objective-C 1.0 的定義中小窺端倪:
在objc_class結(jié)構(gòu)體中:ivars是objc_ivar_list指針愧薛;methodLists是指向objc_method_list指針的指針。也就是說(shuō)可以動(dòng)態(tài)修改*methodLists的值來(lái)添加成員方法诊赊,這也是Category實(shí)現(xiàn)的原理厚满,同樣解釋了Category不能添加屬性的原因府瞄。關(guān)于二級(jí)指針碧磅,可以參考這篇文章碘箍。而最新版的 Runtime 源碼對(duì)這一塊的描述已經(jīng)有很大變化,可以參考下美團(tuán)技術(shù)團(tuán)隊(duì)的深入理解Objective-C:Category鲸郊。
PS:任性的話(huà)可以在Category中添加@dynamic的屬性丰榴,并利用運(yùn)行期動(dòng)態(tài)提供存取方法或干脆動(dòng)態(tài)轉(zhuǎn)發(fā);或者干脆使用關(guān)聯(lián)度對(duì)象(AssociatedObject)
其中objc_ivar_list和objc_method_list分別是成員變量列表和方法列表:
struct objc_ivar_list {
int ivar_count? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_ivar ivar_list[1]? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;
}? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;
struct objc_method_list {
struct objc_method_list *obsolete? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;
int method_count? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1]? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;
}
如果你C語(yǔ)言不是特別好秆撮,可以直接理解為objc_ivar_list結(jié)構(gòu)體存儲(chǔ)著objc_ivar數(shù)組列表四濒,而objc_ivar結(jié)構(gòu)體存儲(chǔ)了類(lèi)的單個(gè)成員變量的信息;同理objc_method_list結(jié)構(gòu)體存儲(chǔ)著objc_method數(shù)組列表职辨,而objc_method結(jié)構(gòu)體存儲(chǔ)了類(lèi)的某個(gè)方法的信息盗蟆。
最后要提到的還有一個(gè)objc_cache,顧名思義它是緩存舒裤,它在objc_class的作用很重要喳资,在后面會(huì)講到。
不知道你是否注意到了objc_class中也有一個(gè)isa對(duì)象腾供,這是因?yàn)橐粋€(gè) ObjC 類(lèi)本身同時(shí)也是一個(gè)對(duì)象仆邓,為了處理類(lèi)和對(duì)象的關(guān)系,runtime 庫(kù)創(chuàng)建了一種叫做元類(lèi) (Meta Class) 的東西伴鳖,類(lèi)對(duì)象所屬類(lèi)型就叫做元類(lèi)节值,它用來(lái)表述類(lèi)對(duì)象本身所具備的元數(shù)據(jù)。類(lèi)方法就定義于此處榜聂,因?yàn)檫@些方法可以理解成類(lèi)對(duì)象的實(shí)例方法搞疗。每個(gè)類(lèi)僅有一個(gè)類(lèi)對(duì)象,而每個(gè)類(lèi)對(duì)象僅有一個(gè)與之相關(guān)的元類(lèi)峻汉。當(dāng)你發(fā)出一個(gè)類(lèi)似[NSObject alloc]的消息時(shí)贴汪,你事實(shí)上是把這個(gè)消息發(fā)給了一個(gè)類(lèi)對(duì)象 (Class Object) ,這個(gè)類(lèi)對(duì)象必須是一個(gè)元類(lèi)的實(shí)例休吠,而這個(gè)元類(lèi)同時(shí)也是一個(gè)根元類(lèi) (root meta class) 的實(shí)例扳埂。所有的元類(lèi)最終都指向根元類(lèi)為其超類(lèi)。所有的元類(lèi)的方法列表都有能夠響應(yīng)消息的類(lèi)方法瘤礁。所以當(dāng)[NSObject alloc]這條消息發(fā)給類(lèi)對(duì)象的時(shí)候阳懂,objc_msgSend()會(huì)去它的元類(lèi)里面去查找能夠響應(yīng)消息的方法,如果找到了柜思,然后對(duì)這個(gè)類(lèi)對(duì)象執(zhí)行方法調(diào)用岩调。
上圖實(shí)線是super_class指針,虛線是isa指針赡盘。 有趣的是根元類(lèi)的超類(lèi)是NSObject号枕,而isa指向了自己,而NSObject的超類(lèi)為nil陨享,也就是它沒(méi)有超類(lèi)葱淳。
Method
Method是一種代表類(lèi)中的某個(gè)方法的類(lèi)型钝腺。
typedef struct objc_method *Method;
而objc_method在上面的方法列表中提到過(guò),它存儲(chǔ)了方法名赞厕,方法類(lèi)型和方法實(shí)現(xiàn):
struct objc_method {
SEL method_name? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;
char* method_types? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;
IMP method_imp? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;
}? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;
1.方法名類(lèi)型為SEL艳狐,前面提到過(guò)相同名字的方法即使在不同類(lèi)中定義,它們的方法選擇器也相同皿桑。
2.方法類(lèi)型method_types是個(gè)char指針毫目,其實(shí)存儲(chǔ)著方法的參數(shù)類(lèi)型和返回值類(lèi)型。
3.method_imp指向了方法的實(shí)現(xiàn)诲侮,本質(zhì)上是一個(gè)函數(shù)指針镀虐,后面會(huì)詳細(xì)講到。
Ivar
Ivar是一種代表類(lèi)中實(shí)例變量的類(lèi)型沟绪。
typedef struct objc_ivar *Ivar;
而objc_ivar在上面的成員變量列表中也提到過(guò):
struct objc_ivar {
char* ivar_name? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;
char* ivar_type? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;
int ivar_offset? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;
#endif
}? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;
可以根據(jù)實(shí)例查找其在類(lèi)中的名字粉私,也就是“反射”:
-(NSString*)nameWithInstance:(id)instance {
unsigned int numIvars =0;
NSString*key=nil;
Ivar * ivars = class_copyIvarList([selfclass], &numIvars);
for(int i =0; i < numIvars; i++) {
Ivar thisIvar = ivars[i];
constchar* type = ivar_getTypeEncoding(thisIvar);
NSString* stringType =? [NSStringstringWithCString:type encoding:NSUTF8StringEncoding];
if(![stringType hasPrefix:@"@"]) {
continue;
}
if((object_getIvar(self, thisIvar) == instance)) {//此處若 crash 不要慌!
key = [NSStringstringWithUTF8String:ivar_getName(thisIvar)];
break;
}
}
free(ivars);
returnkey;
}
class_copyIvarList函數(shù)獲取的不僅有實(shí)例變量近零,還有屬性诺核。但會(huì)在原本的屬性名前加上一個(gè)下劃線。
IMP
IMP在objc.h中的定義是:
typedef id(*IMP)(id, SEL, ...);
它就是一個(gè)函數(shù)指針久信,這是由編譯器生成的窖杀。當(dāng)你發(fā)起一個(gè) ObjC 消息之后,最終它會(huì)執(zhí)行的那段代碼裙士,就是由這個(gè)函數(shù)指針指定的入客。而IMP這個(gè)函數(shù)指針就指向了這個(gè)方法的實(shí)現(xiàn)。既然得到了執(zhí)行某個(gè)實(shí)例某個(gè)方法的入口腿椎,我們就可以繞開(kāi)消息傳遞階段桌硫,直接執(zhí)行方法,這在后面會(huì)提到啃炸。
你會(huì)發(fā)現(xiàn)IMP指向的方法與objc_msgSend函數(shù)類(lèi)型相同铆隘,參數(shù)都包含id和SEL類(lèi)型。每個(gè)方法名都對(duì)應(yīng)一個(gè)SEL類(lèi)型的方法選擇器南用,而每個(gè)實(shí)例對(duì)象中的SEL對(duì)應(yīng)的方法實(shí)現(xiàn)肯定是唯一的膀钠,通過(guò)一組id和SEL參數(shù)就能確定唯一的方法實(shí)現(xiàn)地址;反之亦然裹虫。
Cache
在runtime.h中Cache的定義如下:
typedef struct objc_cache *Cache;
還記得之前objc_class結(jié)構(gòu)體中有一個(gè)struct objc_cache *cache吧肿嘲,它到底是緩存啥的呢,先看看objc_cache的實(shí)現(xiàn)
struct objc_cache {
unsignedi nt mask/* total = mask + 1 */OBJC2_UNAVAILABLE;
unsigned int occupied? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;
Method buckets[1]? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;
};
Cache為方法調(diào)用的性能進(jìn)行優(yōu)化筑公,通俗地講雳窟,每當(dāng)實(shí)例對(duì)象接收到一個(gè)消息時(shí),它不會(huì)直接在isa指向的類(lèi)的方法列表中遍歷查找能夠響應(yīng)消息的方法匣屡,因?yàn)檫@樣效率太低了封救,而是優(yōu)先在Cache中查找际长。Runtime 系統(tǒng)會(huì)把被調(diào)用的方法存到Cache中(理論上講一個(gè)方法如果被調(diào)用,那么它有可能今后還會(huì)被調(diào)用)兴泥,下次查找的時(shí)候效率更高。這根計(jì)算機(jī)組成原理中學(xué)過(guò)的 CPU 繞過(guò)主存先訪問(wèn)Cache的道理挺像虾宇,而我猜蘋(píng)果為提高Cache命中率應(yīng)該也做了努力吧搓彻。
Property
@property標(biāo)記了類(lèi)中的屬性,這個(gè)不必多說(shuō)大家都很熟悉嘱朽,它是一個(gè)指向objc_property結(jié)構(gòu)體的指針:
typedef struct objc_property *Property;
typedef struct objc_property *objc_property_t;//這個(gè)更常用
可以通過(guò)class_copyPropertyList和protocol_copyPropertyList方法來(lái)獲取類(lèi)和協(xié)議中的屬性:
objc_property_t*class_copyPropertyList(Class cls,unsignedint*outCount)
objc_property_t*protocol_copyPropertyList(Protocol *proto,unsignedint*outCount)
返回類(lèi)型為指向指針的指針旭贬,哈哈,因?yàn)閷傩粤斜硎莻€(gè)數(shù)組搪泳,每個(gè)元素內(nèi)容都是一個(gè)objc_property_t指針稀轨,而這兩個(gè)函數(shù)返回的值是指向這個(gè)數(shù)組的指針。
舉個(gè)栗子岸军,先聲明一個(gè)類(lèi):
@interfaceLender :NSObject {
floatalone;
}
@propertyfloatalone;
@end
你可以用下面的代碼獲取屬性列表:
id LenderClass = objc_getClass("Lender");
unsigned int outCount;
objc_property_t *properties = class_copyPropertyList(LenderClass,&outCount);
你可以用property_getName函數(shù)來(lái)查找屬性名稱(chēng):
const char *property_getName(objc_property_t property)
你可以用class_getProperty和protocol_getProperty通過(guò)給出的名稱(chēng)來(lái)在類(lèi)和協(xié)議中獲取屬性的引用:
objc_property_t class_getProperty(Class cls, const char *name)
objc_property_t protocol_getProperty(Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty)
你可以用property_getAttributes函數(shù)來(lái)發(fā)掘?qū)傩缘拿Q(chēng)和@encode類(lèi)型字符串:
const char *property_getAttributes(objc_property_t property)
把上面的代碼放一起奋刽,你就能從一個(gè)類(lèi)中獲取它的屬性啦:
id LenderClass = objc_getClass("Lender");
unsigned int outCount, i;
objc_property_t*properties = class_copyPropertyList(LenderClass, &outCount);
for(i =0; i < outCount; i++){
objc_property_t property = properties[i];
fprintf(stdout,"%s %s\n", property_getName(property), property_getAttributes(property));
}
對(duì)比下class_copyIvarList函數(shù),使用class_copyPropertyList函數(shù)只能獲取類(lèi)的屬性艰赞,而不包含成員變量佣谐。但此時(shí)獲取的屬性名是不帶下劃線的。