文章選自:新浪博客:http://blog.sina.com.cn/s/blog_9cd71e570102wn2j.html
感覺博客主人寫的內容,得到了很多啟發,也讓我對Runtime有了更多的認識
什么是Runtime
Runtime是運行時,主要實現基于消息機制;在C當中函數的調用順序在編譯階段就會被完全確定,不存在任何二義性;?而Objective_C在運行時才會真正的被確定,OC 方法的調用時基于方法選擇器(SEL,C的一個結構體,int型的指針)的,每一個SEL存儲一個唯一的方法名(注意:只是方法名,不包括參數列表),所以在OC一個類當中方法名是唯一的(不存在方法的重載);
?首先看一下OC中方法調用在Runtime中的表示:
我們寫的代碼在程序運行過程中都會被轉化成runtime的C代碼執行,例如[target doSomething];會被轉化成objc_msgSend(target, @selector(doSomething));。
再來看一下對象在Runtime中的表示:
OC中一切都被設計成了對象,我們都知道一個類被初始化成一個實例,這個實例是一個對象。實際上一個類本質上也是一個對象,在runtime中用結構體表示。
相關的定義:
/// 描述類中的一個方法
typedef struct objc_method *Method;
/// 實例變量
typedef struct objc_ivar *Ivar;
/// 類別Category
typedef struct objc_category *Category;
/// 類中聲明的屬性
typedef struct objc_property *objc_property_t;
類在runtime中的表示:
struct objc_class {
Class isa;//指針,顧名思義,表示是一個什么,
//實例的isa指向類對象,類對象的isa指向元類
if !OBJC2
Class super_class; //指向父類
const char *name; //類名
long version;
long info;
long instance_size
struct objc_ivar_list *ivars //成員變量列表
struct objc_method_list **methodLists; //方法列表
struct objc_cache *cache;//緩存
//一種優化,調用過的方法存入緩存列表,下次調用先找緩存
struct objc_protocol_list *protocols //協議列表
#endif
} OBJC2_UNAVAILABLE;
獲取列表
有時候會有這樣的需求,我們需要知道當前類中每個屬性的名字;
比如:字典轉模型,字典的Key和模型對象的屬性名字不匹配。我們可以通過setValueForkeyWithDitionary: 轉換匹配的KEY; setValue:forUndefinedKey: 忽略不匹配的KEY,對于不匹配的KEY,使用Runtime獲取模型屬性列表,單獨處理不匹配的Key,封裝自己的方法即可;
我們可以通過runtime的一系列方法獲取類的一些信息(包括屬性列表,方法列表,成員變量列表,和遵循的協議列表)。
unsigned int count;
//獲取屬性列表
objc_property_t *propertyList = class_copyPropertyList([self class], &count);
for (unsigned int i=0; i
const char *propertyName = property_getName(propertyList[i]);
NSLog(@"property---->%@", [NSString stringWithUTF8String:propertyName]);
}
//獲取方法列表
Method *methodList = class_copyMethodList([self class], &count);
for (unsigned int i; i
Method method = methodList[i];
NSLog(@"method---->%@", NSStringFromSelector(method_getName(method)));
}
//獲取成員變量列表
Ivar *ivarList = class_copyIvarList([self class], &count);
for (unsigned int i; i
Ivar myIvar = ivarList[i];
const char *ivarName = ivar_getName(myIvar);
NSLog(@"Ivar---->%@", [NSString stringWithUTF8String:ivarName]);
}
方法調用
讓我們看一下方法調用在運行時的過程
如果用實例對象調用實例方法,會到實例的isa指針指向的對象(也就是類對象)操作。如果調用的是類方法,就會到類對象的isa指針指向的對象(也就是元類對象)中操作。
首先,在相應操作的對象中的緩存方法列表(Cache)中找調用的方法,如果找到,轉向相應實現并執行。
如果沒找到,在相應操作的對象中的方法列表中找調用的方法,如果找到,轉向相應實現執行
如果沒找到,去父類指針所指向的對象中執行1,2.
如果找到,則將method加 入到Cache中,以方便下次查找,并通過method中的函數指針(保存在方法選擇器當中)跳轉到對應的函數中去執行。
以此類推,如果一直到根類還沒找到,轉向攔截調用。
如果沒有重寫攔截調用的方法,程序報錯(NSAssert)。
以上的過程給我帶來的啟發:
重寫父類的方法,并沒有覆蓋掉父類的方法,只是在當前類對象中找到了這個方法后就不會再去父類中找了。
如果想調用已經重寫過的方法的父類的實現,只需使用super這個編譯器標識符,它會在運行時跳過在當前的類對象中尋找方法的過程。
攔截調用
在方法調用中說到了,如果沒有找到方法就會轉向攔截調用。那么什么是攔截調用呢。攔截調用就是,在找不到調用的方法程序崩潰之前,你有機會通過重寫NSObject的四個方法來處理。
- (BOOL)resolveClassMethod:(SEL)sel;
- (BOOL)resolveInstanceMethod:(SEL)sel;
//后兩個方法需要轉發到其他的類處理
(id)forwardingTargetForSelector:(SEL)aSelector;
(void)forwardInvocation:(NSInvocation *)anInvocation;
第一個方法是當你調用一個不存在的類方法的時候,會調用這個方法,默認返回NO,你可以加上自己的處理然后返回YES。
第二個方法和第一個方法相似,只不過處理的是實例方法。
第三個方法是將你調用的不存在的方法重定向到一個其他聲明了這個方法的類,只需要你返回一個有這個方法的target。
第四個方法是將你調用的不存在的方法打包成NSInvocation
傳給你。做完你自己的處理后,調用invokeWithTarget:方法讓某個target觸發這個方法。
動態添加方法
重寫了攔截調用的方法并且返回了YES,我們要怎么處理呢?有一個辦法是根據傳進來的SEL類型的selector動態添加一個方法。
首先從外部隱式調用一個不存在的方法:
//隱式調用方法
[target performSelector:@selector(resolveAdd:) withObject:@"test"];
然后,在target對象內部重寫攔截調用的方法,動態添加方法。
void runAddMethod(id self, SEL _cmd, NSString *string){
NSLog(@"add C IMP ", string);
}
- (BOOL)resolveInstanceMethod:(SEL)sel{ //給本類動態添加一個法
if ([NSStringFromSelector(sel) isEqualToString:@"resolveAdd:"]) {
class_addMethod(self, sel, (IMP)runAddMethod, "v@:*");
} return YES;
}
其中class_addMethod的四個參數分別是:
Class cls
給哪個類添加方法,本例中是self
SEL name
添加的方法,本例中是重寫的攔截調用傳進來的selector。
IMP imp
方法的實現,C方法的方法實現可以直接獲得。如果是OC方法,可以用+ (IMP)instanceMethodForSelector:(SEL)aSelector;獲得方法的實現。
"v@:*"
方法的簽名,代表有一個參數的方法。
那么問題來了,如果我動態添加的方法名并不是缺失的方法名呢?手動改一改就好了,那么如多及時上百處調用了呢? 重寫缺失方法,調用我們的方法也可以; 介紹一個高大上的方法:方法交換(稍后講解);
關聯對象
現在你準備用一個系統的類,但是系統的類并不能滿足你的需求,你需要額外添加一個屬性。這種情況的一般解決辦法就是繼承。但是,只增加一個屬性,就去繼承一個類,總是覺得太麻煩類。這個時候,runtime的關聯屬性就發揮它的作用了。
//首先定義一個全局變量,用它的地址作為關聯對象的key
static char associatedObjectKey;
//設置關聯對象
objc_setAssociatedObject(target, &associatedObjectKey, @"添加的字符串屬性", OBJC_ASSOCIATION_RETAIN_NONATOMIC); //獲取關聯對象
NSString *string = objc_getAssociatedObject(target, &associatedObjectKey);
NSLog(@"AssociatedObject = %@", string);
objc_setAssociatedObject
的四個參數:
id object
給誰設置關聯對象。
const void *key
關聯對象唯一的key,獲取時會用到。
id value
關聯對象。
objc_AssociationPolicy
關聯策略,有以下幾種策略:
enum
{ OBJC_ASSOCIATION_ASSIGN = 0,
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
OBJC_ASSOCIATION_RETAIN = 01401,
OBJC_ASSOCIATION_COPY = 01403 };
如果你熟悉OC,看名字應該知道這幾種策略的意思了吧。
objc_getAssociatedObject
的兩個參數。
id object
獲取誰的關聯對象。
const void *key
根據這個唯一的key獲取關聯對象。
其實,你還可以把添加和獲取關聯對象的方法寫在你需要用到這個功能的類的類別中,方便使用。
//添加關聯對象
- (void)addAssociatedObject:(id)object{ objc_setAssociatedObject(self, @selector(getAssociatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC); }//獲取關聯對象- (id)getAssociatedObject{ return objc_getAssociatedObject(self, _cmd); }
注意:
這里面我們把getAssociatedObject方法的地址作為唯一的key,_cmd代表當前調用方法的地址。
方法交換
方法交換,顧名思義,就是將兩個方法的實現交換。例如,將A方法和B方法交換,調用A方法的時候,就會執行B方法中的代碼;
import "UIViewController+swizzling.h"
import
@implementation UIViewController (swizzling)
//load方法會在類第一次加載的時候被調用
//調用的時間比較靠前,適合在這個方法里做方法交換
-
(void)load{
//方法交換應該被保證,在程序中只會執行一次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{//獲得viewController的生命周期方法的selector SEL systemSel = @selector(viewWillAppear:); //自己實現的將要被交換的方法的selector SEL swizzSel = @selector(swiz_viewWillAppear:); //兩個方法的Method Method systemMethod = class_getInstanceMethod([self class], systemSel); Method swizzMethod = class_getInstanceMethod([self class], swizzSel); //首先動態添加方法,實現是被交換的方法,返回值表示添加成功還是失敗 BOOL isAdd = class_addMethod(self, systemSel, method_getImplementation(swizzMethod), method_getTypeEncoding(swizzMethod)); if (isAdd) { //如果成功,說明類中不存在這個方法的實現 //將被交換方法的實現替換到這個并不存在的實現 class_replaceMethod(self, swizzSel, method_getImplementation(systemMethod), method_getTypeEncoding(systemMethod)); }else{ //否則,交換兩個方法的實現 method_exchangeImplementations(systemMethod, swizzMethod); }
});
}
(void)swiz_viewWillAppear:(BOOL)animated{
//這時候調用自己,看起來像是死循環
//但是其實自己的實現已經被替換了
[self swiz_viewWillAppear:animated];
NSLog(@"swizzle");
}
@end
在一個自己定義的viewController中重寫viewWillAppear(void
)viewWillAppear:(BOOL)animated{ [super viewWillAppear:animated]; NSLog(@"viewWillAppear"); }
Run起來看看輸出吧!
我的理解:
特別注意::: 方法交換實際交換的是方法的實現部分;