屬性
屬性與成員變量之間的關系
- 屬性對成員變量擴充了存儲方法
- 屬性默認會生成帶下劃線的成員變量
- 聲明了成員變量不會生成屬性
成員變量地址可以根據實例的內存地址偏移尋址。而屬性的讀寫都需要函數調用,相對更慢。self對應類的成員變量的首地址
類別
利用OC的動態運行時為現有類添加方法,您可以在先有類中添加屬性,但是不能添加成員變量,但是我們的編譯器會把屬性自動生成存儲方法和帶下劃線的成員變量,所以我們的聲明的屬性必須是@dynamic類型的,及需要重寫存儲方法。
#import "Person.h"
@interface Person (Ex)
@property(nonatomic, strong) NSString *name;
- (void)setName:(NSString *)name; //***
- (NSString *)name;//***
- (void)method_one;
@end
分類方法實現中可以訪問原來類中聲明的成員變量。
類別的優缺點
- 不破壞原有的類的基礎之上擴充方法。
- 實現代碼之間的解偶
缺點
- 無法在類別中添加新的實例變量,類別中沒有空間容納實例變量
- 命名沖突
類別無法添加實例變量的原因,從Runtime角度,我們發現的確沒有空間容納實力變量,為什么可以添加屬性,原因是屬性的本質是實例變量+ getter方法 + setter方法;所以我們重寫了set/get方法
同名方法調用的優先級為 分類 > 本類 > 父類
/*
* Category Template //類別模版
*/
typedef struct objc_category *Category;
struct objc_category {
char *category_name;
char *class_name;
struct objc_method_list *instance_methods;
struct objc_method_list *class_methods;
struct objc_protocol_list *protocols;
};
擴展
沒有名字的類別
類別中創建的方法和屬性都是私有的,只有這個類對象可以使用
優點
- 可以添加實例變量
- 可以更改讀寫權限(但是更改的權限的存儲方法只能是私有的)
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (nonatomic,readonly) NSString *name; //只有getter方法,是公有的
@end
#import "Person.h"
@interface Person ()
@property (nonatomic, readwrite) NSString *name; //更改name的權限,但是setter方法是私有的不能被外部訪問
@end
@implementation Person
@end
類別和擴展的區別
就category和extension的區別來看,我們可以推導出一個明顯的事實,extension可以添加實例變量,而category是無法添加實例變量的(因為在運行期,對象的內存布局已經確定,如果添加實例變量就會破壞類的內部布局,這對編譯型語言來說是災難性的)extension在編譯期決議,它就是類的一部分,但是category則完全不一樣,它是在運行期決議的。extension在編譯期和頭文件里的@interface以及實現文件里的@implement一起形成一個完整的類,它、extension伴隨類的產生而產生,亦隨之一起消亡。
- extension一般用來隱藏類的私有信息,你必須有一個類的源碼才能為一個類添加extension,所以你無法為系統的類比如NSString添加extension,除非創建子類再添加extension。而category不需要有類的源碼,我們可以給系統提供的類添加category。
- extension可以添加實例變量,而category不可以。
- extension和category都可以添加屬性,但是category的屬性不能生成成員變量和getter、setter方法的實現。
Extension
在編譯器決議,是類的一部分,在編譯器和頭文件的@interface和實現文件里的@implement一起形成了一個完整的類。
伴隨著類的產生而產生,也隨著類的消失而消失。
Extension一般用來隱藏類的私有消息,你必須有一個類的源碼才能添加一個類的Extension,所以對于系統一些類,如NSString,就無法添加類擴展
Category
是運行期決議的
類擴展可以添加實例變量,分類不能添加實例變量
原因:因為在運行期,對象的內存布局已經確定,如果添加實例變量會破壞類的內部布局,這對編譯性語言是災難性的。
非正式協議
創建一個NSObject的類別稱為"非正式協議"
正式協議
正式協議中可以方法,同時協議也是可以繼承的
@protocal MySuperDuberProtocol <MyParentProtocol>
@optional //可選
@required //必須要實現
@end
委托
委托就是某個對象指定另一個對象處理某些特定事物的設計模式
代理主要由三部分組成:
- 協議:用來指定代理雙方可以做什么,必須做什么。
- 代理:根據指定的協議,完成委托方需要實現的功能。
- 委托:根據指定的協議,指定代理去完成什么功能。
這里用一張圖來闡述一下三方之間的關系:
代理使用原理
在iOS中代理的本質就是代理對象內存的傳遞和操作,我們在委托類設置代理對象后,實際上只是用一個id類型的指針將代理對象進行了一個弱引用。委托方讓代理方執行操作,實際上是在委托類中向這個id類型指針指向的對象發送消息,而這個id類型指針指向的對象,就是代理對象。
代理內存管理
為什么我們設置代理屬性都使用weak呢?
我們定義的指針默認都是__strong類型的,而屬性本質上也是一個成員變量和set、get方法構成的,strong類型的指針會造成強引用,必定會影響一個對象的生命周期,這也就會形成循環引用。
Block
原理:
//最基礎的結構體實現
void (^Blk)(void) = ^(void) {
};
Blk();
clang -rewrite-objc main.m后得到的結果
/*
// __block_impl 是 block 實現的結構體
struct __block_impl
{
void *isa; //說明block是一個對象來實現的
int Flags; //按位承載 block 的附加信息;
int Reserved; //保留變量
void *FuncPtr; //函數指針,指向Block需要執行的函數
};
*/
// __main_block_impl_0 是 block 實現的結構體,也是 block 實現的入口
struct __main_block_impl_0 {
struct __block_impl impl; //實現的結構體變量及__block_impl
struct __main_block_desc_0* Desc; //描述結構體變量
//結構體的構造函數,初始化結構體變量impl、Desc
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
//__main_block_func_0最終需要執行的函數代碼塊
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
}
// __main_block_desc_0 是 block 的描述信息結構體
static struct __main_block_desc_0 {
size_t reserved; //結構體信息保留字段
size_t Block_size; //結構體大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)}; //定義一個結構體變量,初始化結構體,計算結構體大小
int main(int argc, const char * argv[]) {
void (*Blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)Blk)->FuncPtr)((__block_impl *)Blk);
}
isa 指向實例對象,表明 block 本身也是一個 Objective-C 對象。block 的三種類型:_NSConcreteStackBlock、_NSConcreteGlobalBlock、_NSConcreteMallocBlock,即當代碼執行時,isa 有三種值
- impl.isa = &_NSConcreteStackBlock;
- impl.isa = &_NSConcreteMallocBlock;
- impl.isa = &_NSConcreteGlobalBlock;
- NSConcreteGlobalBlock 全局的靜態 block,不會訪問任何外部變量。
- _NSConcreteStackBlock 保存在棧中的 block,當函數返回時會被銷毀。
- _NSConcreteMallocBlock 保存在堆中的 block,當引用計數為 0 時會被銷毀。
block 實現的執行流程
main() >> 調用__main_block_impl_0構造函數初始化結構體__main__block_impl_0(__main_block_func_0, __main_block_desc_0_DATA) >> 得到的__main_block_impl_0類型變量賦值給Blk >> 執行Blk->FuncPtr()函數 >> END
帶參數的Block
int intValue = 1;
void (^Blk)(void) = ^(void) {
NSLog(@"%d",intValue);
};
Blk();
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int intValue;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _intValue, int flags=0) : intValue(_intValue) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int intValue = __cself->intValue; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_r__t4y1n4fj5xlgntt308jvn7c80000gn_T_main_be769b_mi_0,intValue);
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, const char * argv[]) {
int intValue = 1;
void (*Blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, intValue));
((void (*)(__block_impl *))((__block_impl *)Blk)->FuncPtr)((__block_impl *)Blk);
}
原來 block 通過參數值傳遞獲取到 intValue 變量,通過函數
__main_block_impl_0 (void *fp, struct __main_block_desc_0 *desc, int _intValue, int flags=0) : intValue(_intValue)
保存到 __main_block_impl_0 結構體的同名變量 intValue,通過代碼 int intValue = __cself->intValue; 取出 intValue,打印出來。
構造函數 __main_block_impl_0 冒號后的表達式 intValue(_intValue) 的意思是,用 _intValue 初始化結構體成員變量 intValue。
有四種情況下應該使用初始化表達式來初始化成員:
- 初始化const成員
- 初始化引用成員
- 當調用基類的構造函數,而它擁有一組參數時
- 當調用成員類的構造函數,而它擁有一組參數時
KVC、KVO
KVC/KVO是觀察者模式的一種實現,在Cocoa中是以被萬物之源NSObject類實現的NSKeyValueCoding/NSKeyValueObserving非正式協議的形式被定義為基礎框架的一部分。從協議的角度來說,KVC/KVO本質上是定義了一套讓我們去遵守和實現的方法.當然,KVC/KVO實現的根本是Objective-C的動態性和runtime
KVC定義了一種按名稱(字符串)訪問對象的機制,而不是訪問器
KVC的實現細節
-(void)setValue:(id)value forKey:(NSString *)key;
- 首先搜索set方法,有就直接賦值
- 如果上面的 setter 方法沒有找到,再檢查類方法+ (BOOL)accessInstanceVariablesDirectly
- 返回 NO,則執行setValue:forUNdefinedKey:
- 返回 YES,則按<key>,<isKey>,<key>,<isKey>的順序搜索成員
- 還沒有找到的話,就調用setValue:forUndefinedKey:
// 允許直接訪問實例變量,默認返回YES。如果某個類重寫了這個方法,且返回NO,則KVC不可以訪問該類。
+ (BOOL)accessInstanceVariablesDirectly
// 如果Key不存在,且沒有KVC無法搜索到任何和Key有關的字段或者屬性,則會調用這個方法,默認是拋出異常
- (id)valueForUndefinedKey:(NSString *)key;
// 如果Key不存在,且沒有KVC無法搜索到任何和Key有關的字段或者屬性,則會調用這個方法,默認是拋出異常
- (id)valueForUndefinedKey:(NSString *)key;
-(id)valueForKey:(NSString *)key;
首先查找 getter 方法,找到直接調用。如果是 bool、int、float 等基本數據類型,會做 NSNumber 的轉換。
-
如果沒查到,再檢查類方法+ (BOOL)accessInstanceVariablesDirectly
- 返回 NO,則執行valueForUndefinedKey:
- 返回 YES,則按<key>,<isKey>,<key>,<isKey>的順序搜索成員
還沒有找到的話,調用valueForUndefinedKey:
KVC 與點語法比較
用 KVC 訪問屬性和用點語法訪問屬性的區別:
用點語法編譯器會做預編譯檢查,訪問不存在的屬性編譯器會報錯,但是用 KVC 方式編譯器無法做檢查,如果有錯誤只能運行的時候才能發現(crash)。
相比點語法用 KVC 方式 KVC 的效率會稍低一點,但是靈活,可以在程序運行時決定訪問哪些屬性。
用 KVC 可以訪問對象的私有成員變量。
KVO 實現原理
當某個類的對象第一次被觀察時,系統就會在運行期動態地創建該類的一個派生類,在這個派生類中重寫基類中任何被觀察屬性的 setter 方法。 派生類在被重寫的 setter 方法實現真正的通知機制,就如前面手動實現鍵值觀察那樣。這么做是基于設置屬性會調用 setter 方法,而通過重寫就獲得了 KVO 需要的通知機制。當然前提是要通過遵循 KVO 的屬性設置方式來變更屬性值,如果僅是直接修改屬性對應的成員變量,是無法實現 KVO 的。 同時派生類還重寫了 class 方法以“欺騙”外部調用者它就是起初的那個類。然后系統將這個對象的 isa 指針指向這個新誕生的派生類,因此這個對象就成為該派生類的對象了,因而在該對象上對 setter 的調用就會調用重寫的 setter,從而激活鍵值通知機制。此外,派生類還重寫了 dealloc 方法來釋放資源。
派生類 NSKVONotifying_Person 剖析:
在這個過程,被觀察對象的 isa 指針從指向原來的 Person 類,被 KVO 機制修改為指向系統新創建的子類 NSKVONotifying_Person 類,來實現當前類屬性值改變的監聽。
所以當我們從應用層面上看來,完全沒有意識到有新的類出現,這是系統“隱瞞”了對 KVO 的底層實現過程,讓我們誤以為還是原來的類。但是此時如果我們創建一個新的名為 NSKVONotifying_Person 的類(),就會發現系統運行到注冊 KVO 的那段代碼時程序就崩潰,因為系統在注冊監聽的時候動態創建了名為 NSKVONotifying_Person 的中間類,并指向這個中間類了。
因而在該對象上對 setter 的調用就會調用已重寫的 setter,從而激活鍵值通知機制。這也是 KVO 回調機制,為什么都俗稱 KVO 技術為黑魔法的原因之一吧:內部神秘、外觀簡潔。
子類 setter 方法剖析:
KVO 在調用存取方法之前總是調用 willChangeValueForKey:,通知系統該 keyPath 的屬性值即將變更。 當改變發生后,didChangeValueForKey: 被調用,通知系統該 keyPath 的屬性值已經變更。 之后,observeValueForKey:ofObject:change:context: 也會被調用。
重寫觀察屬性的 setter 方法這種方式是在運行時而不是編譯時實現的。 KVO 為子類的觀察者屬性重寫調用存取方法的工作原理在代碼中相當于:
- (void)setName:(NSString *)newName
{
[self willChangeValueForKey:@"name"]; // KVO在調用存取方法之前總調用
[super setValue:newName forKey:@"name"]; // 調用父類的存取方法
[self didChangeValueForKey:@"name"]; // KVO在調用存取方法之后總調用
}
總結: KVO 的本質就是監聽對象的屬性進行賦值的時候有沒有調用 setter 方法
系統會動態創建一個繼承于 Person 的 NSKVONotifying_Person
person 的 isa 指針指向的類 Person 變成 NSKVONotifying_Person,所以接下來的 person.age = newAge 的時候,他調用的不是 Person 的 setter 方法,而是 NSKVONotifying_Person(子類)的 setter 方法
重寫NSKVONotifying_Person的setter方法:[super setName:newName]
#import "ViewController.h"
#import "Student.h"
@interface ViewController ()
{
Student *_student;
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
_student = [[Student alloc] init];
_student.stuName = @"oldName_hu";
// 1.給student對象的添加觀察者,觀察其stuName屬性
[_student addObserver:self forKeyPath:@"stuName" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
// 此時,stuName發生了變化
_student.stuName = @"newName_wang";
}
// stuName發生變化后,觀察者(self)立馬得到通知。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
// 最好判斷目標對象object和屬性路徑keyPath
if(object == _student && [keyPath isEqualToString:@"stuName"])
{
NSLog(@"----old:%@----new:%@",change[@"old"],change[@"new"]);
}else
{
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
- (void)dealloc
{
// 移除觀察者
[_student removeObserver:self forKeyPath:@"stuName"];
}
@end
@interface Target : NSObject
{
int age;
}
// for manual KVO - age
- (int) age;
- (void) setAge:(int)theAge;
@end
@implementation Target
- (id) init
{
self = [super init];
if (nil != self)
{
age = 10;
}
return self;
}
// for manual KVO - age
- (int) age
{
return age;
}
- (void) setAge:(int)theAge
{
[self willChangeValueForKey:@"age"];
age = theAge;
[self didChangeValueForKey:@"age"];
}
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"age"]) {
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}
@end
///
/**
* 添加觀察者
*
* @param observer 觀察者
* @param keyPath 被觀察的屬性名稱
* @param options 觀察屬性的新值、舊值等的一些配置(枚舉值,可以根據需要設置,例如這里可以使用兩項)
* @param context 上下文,可以為nil。
*/
[person addObserver: self
forKeyPath: @"age"
options: NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context: nil];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
Person *per = object;
NSLog(@"keyPath: %@ object: %ld",keyPath, (long)per.age);
}