序言
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
這樣的存在诚啃,必然也會有相反的機(jī)制,在OC中我們可以使用@dynamic propertyName
的方式阻止編譯器為屬性完成變量捆綁和setter私沮、getter生成的工作始赎,然后交由我們在運(yùn)行時再去生成這些方法。這些將會在runtime的消息篇中講解。
-
問題五:@synthesize如何判斷屬性的類型造垛?
假如我們在上面自定義的綁定代碼中綁定的不是
_name
而是_ch
呢魔招?那么編譯器會報錯,這是由于類型檢測的結(jié)果五辽。但是編譯器在默認(rèn)生成屬性對應(yīng)的變量內(nèi)存的時候办斑,又是怎么判斷屬性的類型的?另外杆逗,屬性還擁有著copy
乡翅、strong
、weak
···更多的屬性類型髓迎,這關(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
争舞,其中#表示對象凛忿,后面跟著的分別表示id
、id
竞川、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)用:
應(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ù)還沒有那么多的時候,這么寫并不會出現(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