runtime-屬性與變量

序言


runtime簡稱運行時,就是在程序運行時的一些機制,在iOS開發中runtime的特性使得oc這門語言具有獨特的魅力。
對于C、C++來說,在程序編譯運行時,類對象能調用哪些方法,能進行什么操作,都被決定好了。而runtime機制讓oc能在運行時動態的創建類、黑盒測試、擴展屬性等等,極大的提高了語言的靈活性。今天結合runtime的一些機制來談談oc的屬性和變量。(這是我關于runtime機制的開篇,若文中提及的某些知識點有什么不同的意見,歡迎在評論中與我一同探討)

property和ivar


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

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

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

從某個意義上來說,屬性是對成員變量的封裝,在其基礎上添加了setter和getter兩種方法使變量更符合面向對象的需求。(對于不明白為什么要存在setter和getter的開發者們可以看這篇文章getter和setter方法有什么用

屬性的內存結構與@synthesize


在我之前那篇KVO實現文章中,我稍微提到過類的內存結構,這里要更為深入的了解聲明屬性然后運行后內存結果發生的改變,這里我們會發現@synthesize具體做的事情。
現在我的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作為一門動態語言有自己的特性,但是從類結構的角度來說,和其他語言的差別并不會很大。按照類結構的角度來看,類中的成員變量的地址都是基于類對象自身地址進行偏移的,那么這幾個變量的地址應該是依次增加0x8(32位系統上則是0x4)。上面代碼的日志輸出如下:

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

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

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

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

  • 問題二:地址偏移的計算方式是什么?

    指針在64位系統占用8bit這個沒有任何問題,但是char類型只用到一bit,但是這里同樣偏移了8位,是否也是按照結構體的地址偏移計算的?
    這里要提到一個給類添加變量的函數class_addIvar(const char *, NSUInteger *, NSUInteger *),其中最后一個參數用來表示變量的內存地址對其方式。蘋果對這個參數解釋是:

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是變量以字節為單位的最小對齊方式,但是卻 沒有細說怎樣對齊。而在objc-runtime-new.mm中有地址偏移計算的代碼,我們可以通過這些代碼了解的更清楚:

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

簡單來說就是蘋果規定了某個變量它的偏移默認為1 << alignment,而在上下文中這個值為指針長度。因此,OC中類結構地址的偏移計算與結構體還是有不同的,只要是小于8bit長度的地址,統一歸為8bit偏移。

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

    前面我們說過了使用@property聲明的屬性在編譯階段會自動生成一個以下劃線開頭的ivar并且綁定setter和getter方法,所以我們可以在類文件中使用_property的方式訪問變量。那么根據上面的地址偏移的輸出,屬性生成的變量實際上是跟在成員變量的后面的,那么這是怎么實現的?
    在問題二中我提到了一個runtime的函數class_addIvar(),在Xcode中函數的描述如下:
    * @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)).

    在編譯器編譯代碼的期間,對類的操作包括了創建類內存、添加變量、屬性、方法列表……操作,在完成這些操作之后,還需要注冊類類型后才能夠使用。而class_addIvar()函數在注冊前使用,為類添加成員變量并且加入變量列表當中。根據這個函數,我們推測@synthesize在編譯期間通過了這個函數為屬性添加實例變量,并且存放起來。如果我們的猜測是正確的,那么我們可以在實例變量的列表中找到這些屬性對應的變量。
    對于這個問題,runtime同樣提供了方法給我們進行測試。Ivar * class_copyIvarList(Class, unsigned int *)返回類結構中的變量列表,我們可以通過下面的代碼獲取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類的實例變量列表輸出結果如下:
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確實調用了這個方法,其綁定屬性與變量內存的方式是通過class_addIvar()函數來實現的。

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

    這個問題可能有些匪夷所思,從上面的代碼跟問題結合來看,毫無疑問@synthesize為變量生成并且綁定了變量內存。
    我們在聲明屬性的時候,比如Person類中的abc屬性,那么編譯器會在編譯期間幫我們自動生成@synthesize abc = _abc;這句代碼,這意味著我們可以自己來寫出這句。那么假如我們把屬性和已存在的成員變量進行綁定呢?比如寫成@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的內存地址,那么編譯器就不會生成_abc變量,所以在類中找不到這個變量的存在。在創建Person的實例后控制臺輸出的地址信息沒有發生變化,依舊是相差0x8

  0x7ff92b45a438, 0x7ff92b45a440, 0x7ff92b45a448, 0x7ff92b45a458

為了檢測abc_name的關系,我在main函數中加入了這段代碼:

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

輸出的結果是abc_name的結果是一樣的。通過這個小??,我們不難發現@synthesize在為屬性添加變量內存的時候,會先搜索是否已經存在同名的實例變量,如果存在,將生成getter和setter方法來訪問這塊內存地址。否則生成新的成員變量地址,然后再綁定setter和getter。因此@synthesize在添加變量的工作中不僅僅是簡單的class_addIvar(),還有遍歷變量列表的過程。

@synthesize與@dynamic

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

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

    假如我們在上面自定義的綁定代碼中綁定的不是_name而是_ch呢?那么編譯器會報錯,這是由于類型檢測的結果。但是編譯器在默認生成屬性對應的變量內存的時候,又是怎么判斷屬性的類型的?另外,屬性還擁有著copystrongweak···更多的屬性類型,這關乎setter方法的實現,@synthesize又是怎么區分的?在Xcode中有個并不常用的關鍵字@encode,這個關鍵字使用后返回描述類型的編碼,我在main函數中添加了這么一段代碼以及控制臺的輸出結果:

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

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

從上面的輸出中我們看到了Person對應的編碼是#@@c,其中#表示對象,后面跟著的分別表示ididchar,結合類文件來看,這里分別表示_name_sex_ch。那么這也就可以看出@synthesize是怎么判斷出屬性綁定的變量類型了。而在class_addIvar()函數中接受一個const char *類型的參數用來表示實例變量的屬性類型、變量類型等,這時候@synthesize就能將獲取的類型編碼傳入然后生成對應的變量。

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

YYModel的類型編碼

應用


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

上面我總結出了很多頭頭是道的理論,但是如果不能使用并沒有什么卵用。在我們開發中,數據持久化是避不可免的業務實現,由于博主公司項目都不大,也沒有太多的數據需要存儲,因此正常來說博主都是直接使用NSCoding提供的數據歸檔進行的持久化。那么就經常出現這樣的代碼:

模型的數據歸檔

首先在模型數據還沒有那么多的時候,這么寫并不會出現什么問題。當模型的數據越來越多,直接這么寫就可能導致:

  1、數據過多導致歸檔操作中字符串可能對應不上,導致存取失敗
  2、工作量加大

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

  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);   

控制臺輸出屬性的相關信息:

--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來遍歷類屬性然后進行歸檔和反歸檔的過程中都有這么一段遍歷屬性的過程,那么可以定義一個LXDCodingHandler的block用來存儲遍歷中對objc_property_t相關屬性的處理并傳入這個遍歷中:

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

相關代碼我已經完成了封裝,實現了一行代碼對模型進行序列化操作。demo地址

下一篇:消息機制

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,333評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,491評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,263評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,946評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,708評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,409評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,939評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,774評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,641評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,872評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,650評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容

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