runtime-屬性與變量

序言


runtime簡稱運(yùn)行時匿级,就是在程序運(yùn)行時的一些機(jī)制蟋滴,在iOS開發(fā)中runtime的特性使得oc這門語言具有獨(dú)特的魅力。
對于C痘绎、C++來說津函,在程序編譯運(yùn)行時,類對象能調(diào)用哪些方法孤页,能進(jìn)行什么操作尔苦,都被決定好了。而runtime機(jī)制讓oc能在運(yùn)行時動態(tài)的創(chuàng)建類、黑盒測試允坚、擴(kuò)展屬性等等魂那,極大的提高了語言的靈活性。今天結(jié)合runtime的一些機(jī)制來談?wù)刼c的屬性和變量稠项。(這是我關(guān)于runtime機(jī)制的開篇涯雅,若文中提及的某些知識點(diǎn)有什么不同的意見,歡迎在評論中與我一同探討)

property和ivar


首先要確定的是展运,屬性(property)和成員變量(ivar)它們是不同的東西活逆,在oc中它們的區(qū)別如下:

  • 成員變量
    成員變量通常在類聲明@interface或者類實(shí)現(xiàn)@implementation后面的大括號中聲明的變量,默認(rèn)修飾為@protected保護(hù)類型(文件外不能訪問)拗胜,除此之外還有@public公共類型蔗候、@private私有類型和@package包內(nèi)訪問類型

    @interface Person
    {
    @public       ///< 所有可視范圍都能訪問
        NSString * name;
        NSString * sex;
    @private      ///<  只有本類能夠訪問
        NSString * personalWealth;
    @protected    ///< 本類和子類都能訪問 
        NSString * housesNumber;
    @package      ///<  框架內(nèi)視為public,框架外為private
        NSString * familyWealth;
    }
    
  • 屬性
    相比起變量埂软,在編譯期間琴庵,編譯器做了很多工作,包括這些:
    1仰美、使用@synthesize生成屬性對應(yīng)的ivar,通常ivar命名為下劃線+屬性名
    2儿礼、生成setter方法來設(shè)置ivar
    3咖杂、生成getter方法來獲取ivar

從某個意義上來說,屬性是對成員變量的封裝蚊夫,在其基礎(chǔ)上添加了setter和getter兩種方法使變量更符合面向?qū)ο蟮男枨笏咦帧#▽τ诓幻靼诪槭裁匆嬖趕etter和getter的開發(fā)者們可以看這篇文章getter和setter方法有什么用

屬性的內(nèi)存結(jié)構(gòu)與@synthesize


在我之前那篇KVO實(shí)現(xiàn)文章中,我稍微提到過類的內(nèi)存結(jié)構(gòu)知纷,這里要更為深入的了解聲明屬性然后運(yùn)行后內(nèi)存結(jié)果發(fā)生的改變壤圃,這里我們會發(fā)現(xiàn)@synthesize具體做的事情。
現(xiàn)在我的Person類的代碼如下:

@interface Person: NSObject {
    NSString * _name;
    NSString * _sex;
    char _ch;
}

@property(nonatomic, copy) NSString * abc;
@property(nonatomic, copy) NSString * efg;

@end    


@implementation Person

- (instancetype)init {
    if (self = [super init]) {
        NSLog(@"%p, %p, %p, %p, %p, %p, %p", self, &_name, &_sex, &_ch, _abc, _efg);
    }
    return self;
}
 
@end
  • 問題一:成員變量的地址偏移

    雖然OC作為一門動態(tài)語言有自己的特性琅轧,但是從類結(jié)構(gòu)的角度來說伍绳,和其他語言的差別并不會很大。按照類結(jié)構(gòu)的角度來看乍桂,類中的成員變量的地址都是基于類對象自身地址進(jìn)行偏移的冲杀,那么這幾個變量的地址應(yīng)該是依次增加0x8(32位系統(tǒng)上則是0x4)。上面代碼的日志輸出如下:

    0x7fb649c0c9b0, 0x7fb649c0c9c0, 0x7fb649c0c9c8, 0x7fb649c0c9d0, 0x7fb649c0c9d8, 0x7fb649c0c9e0
    

可以看到后面三個地址確實(shí)相差為0x8睹酌,但是在類對象和第一個成員變量之間相差的地址是0x10权谁。這是為什么呢?
蘋果開源文件的相關(guān)代碼中憋沿,我們可以找到Class類型的定義

  typedef struct objc_class *Class;
  struct objc_class {
      Class isa;
      ······
  }

Class表示OC中的類結(jié)構(gòu)旺芽,從這段代碼中我們可以看到它是結(jié)構(gòu)體objc_class的指針類型,在這個結(jié)構(gòu)體中有一個isa指針變量。而這個多出的指針變量也不難解釋了為什么上面的輸出中出現(xiàn)0x10的偏移——兩個地址之間相差了一個isa采章。更為詳細(xì)的內(nèi)容运嗜,將會在之后其他的runtime文章中具體講述。

  • 問題二:地址偏移的計(jì)算方式是什么共缕?

    指針在64位系統(tǒng)占用8bit這個沒有任何問題洗出,但是char類型只用到一bit,但是這里同樣偏移了8位图谷,是否也是按照結(jié)構(gòu)體的地址偏移計(jì)算的翩活?
    這里要提到一個給類添加變量的函數(shù)class_addIvar(const char *, NSUInteger *, NSUInteger *),其中最后一個參數(shù)用來表示變量的內(nèi)存地址對其方式便贵。蘋果對這個參數(shù)解釋是:

The instance variable's minimum alignment in bytes is 1<<align. The minimum alignment of an instance variable depends on the ivar's type and the machine architecture. For variables of any pointer type, pass log2(sizeof(pointer_type)).

這里說了alignment是變量以字節(jié)為單位的最小對齊方式菠镇,但是卻 沒有細(xì)說怎樣對齊。而在objc-runtime-new.mm中有地址偏移計(jì)算的代碼承璃,我們可以通過這些代碼了解的更清楚:

  uint32_t offset = cls->unalignedInstanceSize();
  uint32_t alignMask = (1<<alignment)-1;
  offset = (offset + alignMask) & ~alignMask;

簡單來說就是蘋果規(guī)定了某個變量它的偏移默認(rèn)為1 << alignment利耍,而在上下文中這個值為指針長度。因此盔粹,OC中類結(jié)構(gòu)地址的偏移計(jì)算與結(jié)構(gòu)體還是有不同的隘梨,只要是小于8bit長度的地址,統(tǒng)一歸為8bit偏移舷嗡。

  • 問題三:屬性的變量是怎么存放的轴猎?

    前面我們說過了使用@property聲明的屬性在編譯階段會自動生成一個以下劃線開頭的ivar并且綁定setter和getter方法,所以我們可以在類文件中使用_property的方式訪問變量进萄。那么根據(jù)上面的地址偏移的輸出捻脖,屬性生成的變量實(shí)際上是跟在成員變量的后面的,那么這是怎么實(shí)現(xiàn)的中鼠?
    在問題二中我提到了一個runtime的函數(shù)class_addIvar()可婶,在Xcode中函數(shù)的描述如下:
    * @note This function may only be called after objc_allocateClassPair and before objc_registerClassPair.
    * Adding an instance variable to an existing class is not supported.
    * @note The class must not be a metaclass. Adding an instance variable to a metaclass is not supported.
    * @note The instance variable's minimum alignment in bytes is 1<<align. The minimum alignment of an instance
    * variable depends on the ivar's type and the machine architecture.
    * For variables of any pointer type, pass log2(sizeof(pointer_type)).

    在編譯器編譯代碼的期間,對類的操作包括了創(chuàng)建類內(nèi)存援雇、添加變量矛渴、屬性、方法列表……操作惫搏,在完成這些操作之后曙旭,還需要注冊類類型后才能夠使用。而class_addIvar()函數(shù)在注冊前使用晶府,為類添加成員變量并且加入變量列表當(dāng)中桂躏。根據(jù)這個函數(shù),我們推測@synthesize在編譯期間通過了這個函數(shù)為屬性添加實(shí)例變量川陆,并且存放起來剂习。如果我們的猜測是正確的,那么我們可以在實(shí)例變量的列表中找到這些屬性對應(yīng)的變量。
    對于這個問題鳞绕,runtime同樣提供了方法給我們進(jìn)行測試失仁。Ivar * class_copyIvarList(Class, unsigned int *)返回類結(jié)構(gòu)中的變量列表,我們可以通過下面的代碼獲取Person所有的變量并且輸出變量名:

    unsigned int ivarCount;
    Ivar * ivars = class_copyIvarList([Person class], &ivarCount);
    
    for (int idx = 0; idx < ivarCount; idx++) {
        Ivar ivar = ivars[idx];
        NSLog(@"%s", ivar_getName(ivar));
    }
    free(ivars);
    

上面Person類的實(shí)例變量列表輸出結(jié)果如下:
2016-01-07 21:59:49.580 LXDCodingDemo[3036:255608] _omg
2016-01-07 21:59:49.581 LXDCodingDemo[3036:255608] _name
2016-01-07 21:59:49.581 LXDCodingDemo[3036:255608] _ch
2016-01-07 21:59:49.581 LXDCodingDemo[3036:255608] sct
2016-01-07 21:59:49.581 LXDCodingDemo[3036:255608] _sex
2016-01-07 21:59:49.581 LXDCodingDemo[3036:255608] _copying
2016-01-07 21:59:49.581 LXDCodingDemo[3036:255608] _egf
2016-01-07 21:59:49.581 LXDCodingDemo[3036:255608] _hij
我們可以看到@synthesize確實(shí)調(diào)用了這個方法们何,其綁定屬性與變量內(nèi)存的方式是通過class_addIvar()函數(shù)來實(shí)現(xiàn)的萄焦。

  • 問題四:@synthesize到底做了什么?

    這個問題可能有些匪夷所思冤竹,從上面的代碼跟問題結(jié)合來看拂封,毫無疑問@synthesize為變量生成并且綁定了變量內(nèi)存。
    我們在聲明屬性的時候鹦蠕,比如Person類中的abc屬性冒签,那么編譯器會在編譯期間幫我們自動生成@synthesize abc = _abc;這句代碼,這意味著我們可以自己來寫出這句钟病。那么假如我們把屬性和已存在的成員變量進(jìn)行綁定呢萧恕?比如寫成@synthesize abc = _name,那么修改之后再次輸出地址會變成怎樣肠阱?

    @implementation Person
    @synthesize abc = _name;      ///< 自定義綁定屬性
    
    - (instancetype)init {
        if (self = [super init]) {
            NSLog(@"%p, %p, %p, %p", &_name, &_sex, &_ch, &_efg);
        }
        return self;
    }
    
    @end
    

原先的代碼在添加了自定義綁定的這句代碼后會報錯票唆,由于我們給abc屬性綁定了_name的內(nèi)存地址,那么編譯器就不會生成_abc變量屹徘,所以在類中找不到這個變量的存在惰说。在創(chuàng)建Person的實(shí)例后控制臺輸出的地址信息沒有發(fā)生變化,依舊是相差0x8

  0x7ff92b45a438, 0x7ff92b45a440, 0x7ff92b45a448, 0x7ff92b45a458

為了檢測abc_name的關(guān)系缘回,我在main函數(shù)中加入了這段代碼:

  Person * p = [Person new];
  p.abc = @"123";
  NSLog(@"%@, %@", p.abc, p->_name);

輸出的結(jié)果是abc_name的結(jié)果是一樣的。通過這個小??典挑,我們不難發(fā)現(xiàn)@synthesize在為屬性添加變量內(nèi)存的時候酥宴,會先搜索是否已經(jīng)存在同名的實(shí)例變量,如果存在您觉,將生成getter和setter方法來訪問這塊內(nèi)存地址拙寡。否則生成新的成員變量地址,然后再綁定setter和getter琳水。因此@synthesize在添加變量的工作中不僅僅是簡單的class_addIvar()肆糕,還有遍歷變量列表的過程。

@synthesize與@dynamic

跟黑白對立一樣在孝,有了@synthesize這樣的存在诚啃,必然也會有相反的機(jī)制,在OC中我們可以使用@dynamic propertyName的方式阻止編譯器為屬性完成變量捆綁和setter私沮、getter生成的工作始赎,然后交由我們在運(yùn)行時再去生成這些方法。這些將會在runtime的消息篇中講解。

  • 問題五:@synthesize如何判斷屬性的類型造垛?

    假如我們在上面自定義的綁定代碼中綁定的不是_name而是_ch呢魔招?那么編譯器會報錯,這是由于類型檢測的結(jié)果五辽。但是編譯器在默認(rèn)生成屬性對應(yīng)的變量內(nèi)存的時候办斑,又是怎么判斷屬性的類型的?另外杆逗,屬性還擁有著copy乡翅、strongweak···更多的屬性類型髓迎,這關(guān)乎setter方法的實(shí)現(xiàn)峦朗,@synthesize又是怎么區(qū)分的?在Xcode中有個并不常用的關(guān)鍵字@encode排龄,這個關(guān)鍵字使用后返回描述類型的編碼波势,我在main函數(shù)中添加了這么一段代碼以及控制臺的輸出結(jié)果:

    NSLog(@"%s, %s, %s", @encode(Person), @encode(CGRect), @encode(NSInteger));
    
    ///輸出
    {Person=#@@c}, {CGRect={CGPoint=dd}{CGSize=dd}}, q
    

看起來有些混亂,在蘋果官方文檔中提到了編譯器用C字符來表示所有的OC類型橄维,而使用@encode(type)可以獲取這個類型的編碼尺铣,這些編碼的對應(yīng)關(guān)系在類型編碼中可以看到。

從上面的輸出中我們看到了Person對應(yīng)的編碼是#@@c争舞,其中#表示對象凛忿,后面跟著的分別表示idid竞川、char店溢,結(jié)合類文件來看,這里分別表示_name委乌、_sex床牧、_ch。那么這也就可以看出@synthesize是怎么判斷出屬性綁定的變量類型了遭贸。而在class_addIvar()函數(shù)中接受一個const char *類型的參數(shù)用來表示實(shí)例變量的屬性類型戈咳、變量類型等,這時候@synthesize就能將獲取的類型編碼傳入然后生成對應(yīng)的變量壕吹。

另外著蛙,對于屬性類型的判斷又是怎么樣的呢?同樣的耳贬,蘋果在runtime中提供給我們property_getAttributes()來獲取一個對象的類型屬性踏堡,這些類型屬性也同樣采用了@encode類似的一套類型編碼,這些類型編碼的標(biāo)準(zhǔn)表同樣可以在屬性類型編碼中找到咒劲。
如果你喜歡看各種開源框架的代碼暂吉,那么最近突起的YYModel中你可以看到作者對于類型編碼的大量應(yīng)用:

YYModel的類型編碼

應(yīng)用


不能實(shí)踐的理論都是廢話 —— 沃德天·毫率

上面我總結(jié)出了很多頭頭是道的理論胖秒,但是如果不能使用并沒有什么卵用。在我們開發(fā)中慕的,數(shù)據(jù)持久化是避不可免的業(yè)務(wù)實(shí)現(xiàn)阎肝,由于博主公司項(xiàng)目都不大,也沒有太多的數(shù)據(jù)需要存儲肮街,因此正常來說博主都是直接使用NSCoding提供的數(shù)據(jù)歸檔進(jìn)行的持久化风题。那么就經(jīng)常出現(xiàn)這樣的代碼:

模型的數(shù)據(jù)歸檔

首先在模型數(shù)據(jù)還沒有那么多的時候,這么寫并不會出現(xiàn)什么問題嫉父。當(dāng)模型的數(shù)據(jù)越來越多沛硅,直接這么寫就可能導(dǎo)致:

  1、數(shù)據(jù)過多導(dǎo)致歸檔操作中字符串可能對應(yīng)不上绕辖,導(dǎo)致存取失敗
  2摇肌、工作量加大

上面我們說到過runtime中存在class_copyIvarList()函數(shù)來獲取一個類的所有實(shí)例變量,對于屬性同樣存在著class_copyPropertyList()函數(shù)仪际。因此围小,我們可以通過這個函數(shù)來遍歷獲取屬性以及屬性名稱,然后實(shí)現(xiàn)類似單例宏定義的一鍵歸檔宏定義树碱。核心代碼如下:

  unsigned int propertyCount; 
  objc_property_t * properties = class_copyPropertyList([self class], &propertyCount);    

  for (int idx = 0; idx < propertyCount; idx++) {
      objc_property_t property = properties[idx];
      NSLog(@"\n--name: %s\n--attributes: %s", property_getName(property), property_getAttributes(property));  }   
  }   
  free(properties);   

控制臺輸出屬性的相關(guān)信息:

--name: abc
--attributes: T@"NSString",C,N,V_abc

--name: efg
--attributes: T@"NSString",C,N,V_efg

--name: hij
--attributes: T@"NSString",C,N,V_hij

通過runtime來遍歷類屬性然后進(jìn)行歸檔和反歸檔的過程中都有這么一段遍歷屬性的過程肯适,那么可以定義一個LXDCodingHandler的block用來存儲遍歷中對objc_property_t相關(guān)屬性的處理并傳入這個遍歷中:

typedef void(^LXDCodingHandler)(objc_property_t property, NSString * propertyName);

相關(guān)代碼我已經(jīng)完成了封裝,實(shí)現(xiàn)了一行代碼對模型進(jìn)行序列化操作成榜。demo地址

下一篇:消息機(jī)制

轉(zhuǎn)載請注明作者和地址:http://sindrilin.com/runtime/2016/01/08/屬性與變量.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末框舔,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子赎婚,更是在濱河造成了極大的恐慌刘绣,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件挣输,死亡現(xiàn)場離奇詭異纬凤,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)歧焦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來肚医,“玉大人绢馍,你說我怎么就攤上這事〕μ祝” “怎么了舰涌?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長你稚。 經(jīng)常有香客問我瓷耙,道長朱躺,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任搁痛,我火速辦了婚禮长搀,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘鸡典。我一直安慰自己源请,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布彻况。 她就那樣靜靜地躺著谁尸,像睡著了一般。 火紅的嫁衣襯著肌膚如雪纽甘。 梳的紋絲不亂的頭發(fā)上良蛮,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天,我揣著相機(jī)與錄音悍赢,去河邊找鬼决瞳。 笑死,一個胖子當(dāng)著我的面吹牛泽裳,可吹牛的內(nèi)容都是我干的瞒斩。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼涮总,長吁一口氣:“原來是場噩夢啊……” “哼胸囱!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起瀑梗,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤烹笔,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后抛丽,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體谤职,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年亿鲜,在試婚紗的時候發(fā)現(xiàn)自己被綠了允蜈。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡蒿柳,死狀恐怖饶套,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情垒探,我是刑警寧澤妓蛮,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站圾叼,受9級特大地震影響蛤克,放射性物質(zhì)發(fā)生泄漏捺癞。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一构挤、第九天 我趴在偏房一處隱蔽的房頂上張望髓介。 院中可真熱鬧,春花似錦儿倒、人聲如沸版保。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽彻犁。三九已至,卻和暖如春凰慈,著一層夾襖步出監(jiān)牢的瞬間汞幢,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工微谓, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留森篷,地道東北人。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓豺型,卻偏偏與公主長得像仲智,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子姻氨,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容

  • 寫下這些文字的時候屁股依然生疼钓辆。黎明即起,出門插秧肴焊。昨夜剛下過一場大雨前联,淹沒了一切聲音,睡眠變得綿長而酣暢娶眷。早上醒...
    爾時云起閱讀 300評論 0 2
  • 我是一個孤獨(dú)的影子似嗤。 我漂泊在寂靜的夜晚,欲哭無淚届宠。 我是一個孤獨(dú)的影子烁落。 我飛翔在寂寞的夜晚,只為尋...
    渲染凌軒閱讀 327評論 0 1