我所理解的runtime是一個使用C編寫的庫,爲C添加了面向對象的特性,它是一個庫(Runtime Library中文:運行時庫).在這個庫中能夠用C函數來實現方法,對象也能夠用C語言的結構體來表示…全部oc的方法的背後都是經過runtime來運行的.程序員
首先新建一個類RuntimeModel
,並實現對象方法eat
,在RuntimeViewController中調用eat
方法,使用oc來語言來實現很簡單了xcode
RuntimeModel * model=[[RuntimeModel alloc]init];
[model eat];
複製代碼
接下來一點點用runtime實現上面的代碼,導入runtime的頭文件#import <objc/message.h>
,因爲xcode5.0開始蘋果不建議咱們使用底層的代碼,因此target->build setting->搜索msg->將YES改成NO
,這樣接下來咱們用runtime的時候纔會出現提示。 咱們使用objc_msgSend(<#id self#>, <#SEL op, ...#>)
這個方法,能夠看到須要兩個參數,第一個參數是id類型,表明誰要發送消息,第二個參數是要把消息發送給誰,咱們用runtime來實現[model eat];
這個方法。bash
objc_msgSend(model, sel_registerName("eat"));
複製代碼
而初始化對象一樣是調用了alloc init
這兩個方法。將導入的頭文件RuntimeModel
去掉,用純c語言的代碼實現上面的功能。網絡
objc_msgSend(objc_msgSend(objc_msgSend(objc_getClass("RuntimeModel"), sel_registerName("alloc")), sel_registerName("init")), sel_registerName("eat"));
複製代碼
運行,咱們能夠看到在eat方法中的nslog被調用了,雖然咱們實現了功能,可是怎麼才能知道咱們的oc語言在運行時確實是被轉換成了c語言的代碼呢? 新建工程,建立命令行工具。函數
person * p=[[person alloc]init];
關閉項目,打開終端,進入剛纔建的文件夾下,
ls
打開能夠看到剛纔咱們新建的類和main.m文件,接下來執行命令行
clang -rewrite-objc main.m
這時咱們能夠看到,在剛纔的工程中出現一個
main.cpp
的文件,打開而且拖到最下面。
作爲oc的程序員最悲慘的就是,運行--崩潰在main裏面,我尼瑪!!! 例如:工具
NSURL * url=[NSURL URLWithString:@"www.baidu.com.啦啦"];
複製代碼
當咱們沒有進行編碼的時候,這個url在編譯的時候是有效的,可是一旦運行起來,這個url就會變爲nil。由於檢測不到這是一個無效的url,會繼續發送網絡請求。 怎麼辦呢?在下面用if作一個判斷?但是要在每個url下面都作判斷。這時候最早想到的必定是重寫。 建立一個url的分類,重寫URLWithString:<#(nonnull NSString *)#>
可是,咱們看:ui
+(instancetype)URLWithString:(NSString *)URLString
{
// 首先建立一個URL
NSURL * url= ????????
if (url==nil) {
NSLog(@"有問題");
}
return url;
}
複製代碼
咱們該怎麼建立呢,死循環了是否是,因此runtime就起做用了。 在分類中自定義一個方法編碼
+(instancetype)TY_URLWithStr:(NSString *)Str;
{
NSURL * url =[NSURL URLWithString:Str];
if (url == nil) {
NSLog(@"是空!!!!");
}
return url;
}
複製代碼
可是咱們並非要每一次建立url都調用這個方法,由於程序裏有不少建立url的地方,咱們還繼續使用系統自帶的方法。在分類中實現load
方法,當程序加載這個類的時候最早調用這個方法。導入頭文件,開始進行方法交換。atom
+(void)load
{
// Method : 成員方法
//class_getClassMethod 拿到類方法
//class_getInstanceMethod 拿到對象方法
Method URLWithStr = class_getClassMethod([NSURL class], @selector(URLWithString:));
Method TYURLWithStr = class_getClassMethod([NSURL class], @selector(TY_URLWithStr:));
// 開始交換方法
method_exchangeImplementations(URLWithStr, TYURLWithStr);
}
複製代碼
記得將上面咱們自定義的方法中建立url的方法改變回去,否則再次死循環,改成url
+(instancetype)TY_URLWithStr:(NSString *)Str;
{
NSURL * url =[NSURL TY_URLWithStr:Str];
if (url == nil) {
NSLog(@"是空!!!!");
}
return url;
}
複製代碼
是否是有種一葉落而知天下秋的感受。
oc的序列化在這裏就很少說了,讓咱們來講一種常見的狀況,當須要歸檔的屬性過多時,咱們須要一條條的寫出來,十分繁瑣,有沒有可能簡化一些呢,若是單純的用for循環去作,那麼不一樣的類型該怎麼處理呢,這時候咱們的runtime又來了。首先建立一個Person類,多弄一些虛擬屬性。
// .h
@property(nonatomic,strong) NSString * name;
@property(nonatomic,strong) NSString * name1;
@property(nonatomic,assign) int age;
@property(nonatomic,assign) int age1;
@property(nonatomic,assign) double age2;
// .m
-(void)encodeWithCoder:(NSCoder *)Coder
{
}
-(instancetype)initWithCoder:(NSCoder *)aDecoder
{
self=[super init];
if (self) {
}
return self;
}
複製代碼
實現思路:
-(void)encodeWithCoder:(NSCoder *)Coder
{
for (int i = 0; i < 屬性數量; i++) {
[Coder encodeObject:屬性值 forKey:屬性名稱];
}
}
複製代碼
那麼 回到controller中,導入runtime頭文件,使用一個方法class_copyIvarList(<#__unsafe_unretained Class cls#>, <#unsigned int *outCount#>)
獲取屬性列表,第一個參數,傳入一個類[Person class]
,第二個參數,傳入一個指針,在上面定義unsigned int count = 0;
,而後傳入&count
。這個count就是獲取的屬性的數量。同時在c語言中是不分.h.m的,因此不管是在哪一個文件中定義的屬性,均可以取到。
unsigned int count = 0;
class_copyIvarList([Person class], &count);
複製代碼
而後咱們須要定義一個Ivar類型的指針,這個指針會指向每個屬性,下面這個圖說明一下,他並非同時指向每個屬性,而是一個一個分別指向來獲取屬性。
unsigned int count = 0;
Ivar * ivars = class_copyIvarList([Person class], &count);
Ivar ivar = ivars[0];
const char * name = ivar_getName(ivar);
NSLog(@"%s",name);
複製代碼
打印看到第一個屬性名,能夠改變ivars[第幾個],去獲取第幾個。並且即便角標越界,依然不會崩潰。 那麼咱們回到person類中,直接用剛纔的代碼實現咱們最開始提出的問題。
// 歸檔
-(void)encodeWithCoder:(NSCoder *)Coder
{
unsigned int count = 0;
Ivar * ivars = class_copyIvarList([Person class], &count);
for (int i = 0; i < count; i++) {
Ivar ivar= ivars[i];
const char * name = ivar_getName(ivar);
NSString * key = [[NSString alloc]initWithUTF8String:name];
// 使用KVC 拿出屬性的值
[Coder encodeObject:[self valueForKey:key] forKey:key];
}
}
// 解檔
-(instancetype)initWithCoder:(NSCoder *)Decoder
{
self=[super init];
if (self) {
unsigned int count = 0;
Ivar * ivars = class_copyIvarList([Person class], &count);
for (int i = 0; i < count; i++) {
Ivar ivar= ivars[i];
const char * name = ivar_getName(ivar);
NSString * key = [[NSString alloc]initWithUTF8String:name];
// 使用KVC 拿出屬性的值
id value = [Decoder decodeObjectForKey:key];
// 設置屬性
[self setValue:value forKey:key];
}
}
return self;
}
複製代碼
經過上面的講述,這段代碼就很容易理解了。咱們用的是kvc的賦值和取值,因此任何類型的歸檔解檔都是沒有問題的。
咱們先用oc實現一個簡單的KVO監聽。
// controller.m
self.c=[[Cat alloc]init];
self.d=[[Dog alloc]init];
// 註冊監聽
[self.d addObserver:self.c forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
// cat.m
// 監聽到了object的對象keyPath屬性變化爲樂change
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"監聽到了%@的對象%@屬性變化爲樂%@",object,keyPath,change);
}
複製代碼
這是KVO最簡單的應用,那麼接下來咱們看一下KVO底層究竟是怎麼實現的呢? 首先在KVO運行的時候會動態的添加一個類,繼承與被觀察者的類。名字叫作NSKVONotifying_Dog
這個類。類名可不是瞎編的哦。而後在.m文件中,調用父類的set方法:
-(void)setAge:(int)age
{
[super setAge:age];
// 在子類中調用這兩個方法
// 這個是 將要被改變的值是什麼
[self willChangeValueForKey:@"age"];
// 這個是 改變以後的新值是什麼
[self didChangeValueForKey:@"age"];
}
複製代碼
這樣就會監聽到改變而且傳值,可是爲何說KVO是這樣實現的呢? 將剛纔新建的NSKVONotifying_Dog
類刪掉,在controller中實現點擊改變值的方法
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.d.age=99;
}
複製代碼
在賦值的地方打斷點,若是程序運行到這裏,d的類型變爲剛纔那個方法的類型,那麼就說明KVO就是這樣實現的。
首先建立一個Person類,而後在controller中實例化person直接能夠這樣直接調用一個不存在的方法
Person * p=[[Person alloc]init];
[p performSelector:@selector(eat)];
複製代碼
這樣雖然編譯能夠過,可是運行起來就會崩潰 這時候我沒回到person.m中,來看兩個方法
// 當這個類被調用沒有實現的類方法 就會來到這裏
+(BOOL)resolveClassMethod:(SEL)sel
{
return [super resolveClassMethod:sel];
}
// 當這個類被調用沒有實現的對象方法 就會來到這裏
+(BOOL)resolveInstanceMethod:(SEL)sel
{
return [super resolveInstanceMethod:sel];
}
複製代碼
方法中的參數就是被調用的方法名,而後咱們須要實現一個名爲eat的函數
void eat(){
NSLog(@"lalal");
}
複製代碼
這時候,咱們將要用到一個方法class_addMethod(<#__unsafe_unretained Class cls#>, <#SEL name#>, <#IMP imp#>, <#const char *types#>)
第一個參數:類類型,第二個參數:方法編號,第三個參數:方法實現(函數指針),第四個參數:返回值類型。關於這第四個參數,這是c語音,咱們該怎麼寫返回類型呢?去查一下官方文檔關於第四個參數的描述。
+(BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(eat)) {
class_addMethod([Person class], sel, (IMP)eat, "v");
}
return [super resolveClassMethod:sel];
return [super resolveInstanceMethod:sel];
}
void eat(){
NSLog(@"lalal");
}
複製代碼
這樣就完成了一個動態添加方法,而後咱們接着看文檔,文檔中有一段代碼示例
咱們能夠看到,當動態添加方法是會傳入兩個參數,實際上每個函數被調用時都會傳入這兩個參數,叫作隱式參數。參數一:調用了哪一個類的,參數二:調用了哪一個方法,咱們用nslog打印一下這兩個參數,在這以前須要改一下上面的第四個參數爲"v@:"
,由於咱們返回值類型改變了。打印一下:
Person * p=[[Person alloc]init];
[p performSelector:@selector(eat:) withObject:@"6666"];
複製代碼
回去Person類,將判斷的方法名改成eat:
,並將eat函數增長一個參數:
+(BOOL)resolveInstanceMethod:(SEL)sel
{
// 方法名的判斷
if (sel == @selector(eat:)) {
class_addMethod([Person class], sel, (IMP)eat, "v@:");
}
return [super resolveClassMethod:sel];
return [super resolveInstanceMethod:sel];
}
void eat(id self, SEL _cmd ,id obj){
NSLog(@"%@ ",obj);
}
複製代碼
控制檯: