序言
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
這樣的存在,必然也會有相反的機制,在OC中我們可以使用@dynamic propertyName
的方式阻止編譯器為屬性完成變量捆綁和setter、getter生成的工作,然后交由我們在運行時再去生成這些方法。這些將會在runtime的消息篇中講解。
-
問題五:@synthesize如何判斷屬性的類型?
假如我們在上面自定義的綁定代碼中綁定的不是
_name
而是_ch
呢?那么編譯器會報錯,這是由于類型檢測的結果。但是編譯器在默認生成屬性對應的變量內存的時候,又是怎么判斷屬性的類型的?另外,屬性還擁有著copy
、strong
、weak
···更多的屬性類型,這關乎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
,其中#表示對象,后面跟著的分別表示id
、id
、char
,結合類文件來看,這里分別表示_name
、_sex
、_ch
。那么這也就可以看出@synthesize
是怎么判斷出屬性綁定的變量類型了。而在class_addIvar()
函數中接受一個const char *
類型的參數用來表示實例變量的屬性類型、變量類型等,這時候@synthesize
就能將獲取的類型編碼傳入然后生成對應的變量。
另外,對于屬性類型的判斷又是怎么樣的呢?同樣的,蘋果在runtime中提供給我們property_getAttributes()
來獲取一個對象的類型屬性,這些類型屬性也同樣采用了@encode
類似的一套類型編碼,這些類型編碼的標準表同樣可以在屬性類型編碼中找到。
如果你喜歡看各種開源框架的代碼,那么最近突起的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