我們這里只討論一下我們平常工作中常用的特性,當(dāng)然,它有大量功能,只是我們并不一定用的到,類似objc_msgSend這種的我們也不作介紹。
Objective-C runtime已經(jīng)開源了,有閱讀源碼習(xí)慣的程序員可以前往官網(wǎng)下載閱讀。
下面是下載地址:
http://www.opensource.apple.com/tarballs/objc4/
添加、獲取屬性
以開源庫SVPullToRefresh(SVPullToRefresh是一個(gè)提供上下拉刷新的庫)舉例。
在UIScrollView+SVPullToRefresh
這個(gè)Category上,SVPullToRefresh
給UIScrollView動(dòng)態(tài)添加了一個(gè)屬性,我們以SVPullToRefreshView *pullToRefreshView
這個(gè)屬性舉例。
在UIScrollView+SVPullToRefresh.h
上先申明了這個(gè)屬性
@property (nonatomic, strong, readonly) SVPullToRefreshView *pullToRefreshView;
之后,在UIScrollView+SVPullToRefresh.m
中重寫了它的Setter和Getter方法,分別如下:
Setter:
- (void)setPullToRefreshView:(SVPullToRefreshView *)pullToRefreshView {
//[self willChangeValueForKey:@"SVPullToRefreshView"];
objc_setAssociatedObject(self, &UIScrollViewPullToRefreshView,
pullToRefreshView,
OBJC_ASSOCIATION_ASSIGN);
//[self didChangeValueForKey:@"SVPullToRefreshView"];
}
我們將注釋掉的兩行忽略,只看中間的一行:
objc_setAssociatedObject(self, &UIScrollViewPullToRefreshView,
pullToRefreshView,
OBJC_ASSOCIATION_ASSIGN);
語法結(jié)構(gòu)如下:
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
這個(gè)方法用于使用一個(gè)給定的Key和policy,將對(duì)象和值(Value)相關(guān)聯(lián)。
object:需要關(guān)聯(lián)的對(duì)象
key:用于關(guān)聯(lián)的Key
value:使用Key來關(guān)聯(lián)在對(duì)象上的值,如果設(shè)置為nil,則清除已綁定的值。
policy:關(guān)聯(lián)的策略,提供assign、retain、copy等策略。
Getter:
- (SVPullToRefreshView *)pullToRefreshView {
return objc_getAssociatedObject(self, &UIScrollViewPullToRefreshView);
}
和上面類似,這里使用的是runtime里面的這個(gè)方法:
id objc_getAssociatedObject(id object, const void *key)
這個(gè)方法用于獲取對(duì)象相關(guān)聯(lián)的值,與上面的方法相呼應(yīng)。
objc_getAssociatedObject
這種方式只能獲取已知的屬性,如果一個(gè)類有很多屬性,但是我們可能并不知道它的具體名稱和類型,那怎么辦呢?
我們以JsonModel舉例,JsonModel
是一個(gè)Json
轉(zhuǎn)Model
的一個(gè)庫,一般用于將從服務(wù)端或者某處獲得的Json
字符串映射到對(duì)象上,并完成填充工作。
我們以官方文檔上的例子舉例:
#import "JSONModel.h"
@interface CountryModel : JSONModel
@property (strong, nonatomic) NSString* country;
@end
我們寫了一個(gè)類,名叫CountryModel
,繼承于JSONModel
,我們?cè)?code>CountryModel中聲明了名叫country
的一個(gè)NSString
類型的屬性。
之后,我們將獲取到的Json
字符串進(jìn)行填充的時(shí)候,它就自動(dòng)填充好了。
#import "CountryModel.h"
...
NSString* json = (fetch here JSON from Internet) ...
NSError* err = nil;
CountryModel* country = [[CountryModel alloc] initWithString:json error:&err];
如果不考慮一些其他東西的話,這要比我們手寫賦值要簡單,起碼可以省一些代碼。那,它是怎么實(shí)現(xiàn)的呢,我們看一下它的源代碼:
JsonModel.m
-(void)__inspectProperties
{
...
while (class != [JSONModel class]) {
//JMLog(@"inspecting: %@", NSStringFromClass(class));
unsigned int propertyCount;
objc_property_t *properties = class_copyPropertyList(class, &propertyCount);
for (unsigned int i = 0; i < propertyCount; i++) {
JSONModelClassProperty* p = [[JSONModelClassProperty alloc] init];
//get property name
objc_property_t property = properties[i];
const char *propertyName = property_getName(property);
p.name = @(propertyName);
...
}
這個(gè)是JsonModel
中最主要的方法之一,比較長,我只摘取其中的一部分,忽略了很多其他特性,比如它使用protocol的方式去給屬性添加option等描述,或者從protocol上取類名,再給數(shù)組賦值。
JsonModel
會(huì)遍歷Model,通過class_copyPropertyList
方法來獲取類上所有的屬性:
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
通過property_getName
方法來獲取屬性名稱:
const char *propertygetName(objcproperty_t property)
之后會(huì)依據(jù)此名稱,進(jìn)行其他操作,這里不提。
objc_getAssociatedObject
、class_copyPropertyList
、property_getName
和objc_setAssociatedObject
方法讓我們?cè)陂_發(fā)中,可以很方便的對(duì)對(duì)象進(jìn)行屬性獲取和屬性添加的操作。
添加、獲取、替換方法
蘋果提供了Category等方式,使得我們可以很簡單的給已知類添加方法。但是在很多時(shí)候,我們需要依據(jù)某些條件,在運(yùn)行時(shí)給這些類添加方法,這個(gè)時(shí)候我們可以使用runtime提供的一些方法。
以開源庫Aspects為例,Aspects是一個(gè)攔截器,它可以在某個(gè)方法運(yùn)行前、運(yùn)行后進(jìn)行攔截,來加載其他的一些操作,或者替換整個(gè)方法。
使用方式類似于下面這種:
[UIViewController aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo, BOOL animated) {
NSLog(@"View Controller %@ will appear animated: %tu", aspectInfo.instance, animated);
} error:NULL];
如果現(xiàn)在有這么一個(gè)需求,要給所有VC的viewWillAppear:
方法上加一個(gè)log的方法,我們可能會(huì)使用繼承的辦法,在基類上面重寫viewWillAppear:
方法,并添加打印的辦法,之后其他VC需要繼承我們這個(gè)基類。除此之外,使用Aspects攔截viewWillAppear:
,然后在執(zhí)行之前或者執(zhí)行之后添加log方法,也是一種辦法,這里并不探討孰優(yōu)孰劣。
我們來看一下它的實(shí)現(xiàn)原理:
static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
NSCParameterAssert(selector);
Class klass = aspect_hookClass(self, error);
Method targetMethod = class_getInstanceMethod(klass, selector);
IMP targetMethodIMP = method_getImplementation(targetMethod);
if (!aspect_isMsgForwardIMP(targetMethodIMP)) {
// Make a method alias for the existing method implementation, it not already copied.
const char *typeEncoding = method_getTypeEncoding(targetMethod);
SEL aliasSelector = aspect_aliasForSelector(selector);
if (![klass instancesRespondToSelector:aliasSelector]) {
__unused BOOL addedAlias = class_addMethod(klass, aliasSelector, method_getImplementation(targetMethod), typeEncoding);
NSCAssert(addedAlias, @"Original implementation for %@ is already copied to %@ on %@", NSStringFromSelector(selector), NSStringFromSelector(aliasSelector), klass);
}
// We use forwardInvocation to hook in.
class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector));
}
}
上面的代碼是用來hook類并添加或者替換方法的。
這里入?yún)⑸厦娴膕elector即我們的@selector(viewWillAppear:) 。
Method targetMethod = class_getInstanceMethod(klass, selector);
IMP targetMethodIMP = method_getImplementation(targetMethod);
class_getInstanceMethod
方法可以獲取到指定類的指定方法,返回的是一個(gè)Method,而通過method_getImplementation
方法獲取到Method結(jié)構(gòu)體的IMP指針。
(這個(gè)Method是一個(gè)結(jié)構(gòu)體,這里不多說明,大家可以自行查看一下runtime的源碼中對(duì)Method結(jié)構(gòu)體的定義,在objc-private.h
中可以找到,或者你可以參考我的另一篇博客Objective-C中為什么不支持泛型方法。)
SEL aliasSelector = aspect_aliasForSelector(selector);
之后,使用aspect_aliasForSelector
方法,給selector添加了一個(gè)前綴,新的方法名為aspects__viewWillAppear:
。
__unused BOOL addedAlias = class_addMethod(klass, aliasSelector, method_getImplementation(targetMethod), typeEncoding);
class_addMethod
方法會(huì)給類動(dòng)態(tài)添加方法,這里就是給UIViewController
添加了一個(gè)aspects__viewWillAppear
方法,而這個(gè)方法的實(shí)現(xiàn),依然是原來viewWillAppear:
方法的實(shí)現(xiàn)。
到這一步,Aspects只是給類添加了一個(gè)aspects__viewWillAppear:
方法,并沒有hook進(jìn)去。
class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
class_replaceMethod
方法,顧名思義,用來替換方法的。這個(gè)方法使用aspects__viewWillAppear:
替換了viewWillAppear:
方法,但是類里面并沒有實(shí)現(xiàn)aspects__viewWillAppear:
方法,這個(gè)意思就是說,當(dāng)執(zhí)行viewWillAppear:
方法的時(shí)候,就會(huì)去執(zhí)行aspects__viewWillAppear:
,而aspects__viewWillAppear:
這個(gè)方法我們并沒有實(shí)現(xiàn)。那會(huì)不會(huì)掛呢。。。
Aspects這里使用了一個(gè)特殊的辦法來hook。
- (void)forwardInvocation:(NSInvocation *)anInvocation
NSObject中提供了forwardInvocation:
方法,這個(gè)方法會(huì)在對(duì)象收到一個(gè)無法響應(yīng)的selector之后,給forwardInvocation:
方法發(fā)一條消息,或者說調(diào)用一下NSObject的這個(gè)方法。
我們回到最上面的一條方法:
Class klass = aspect_hookClass(self, error);
這個(gè)方法會(huì)調(diào)用這么幾個(gè)方法,其中有一個(gè)是:
static NSString *const AspectsForwardInvocationSelectorName = @"__aspects_forwardInvocation:";
static void aspect_swizzleForwardInvocation(Class klass) {
NSCParameterAssert(klass);
// If there is no method, replace will act like class_addMethod.
IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");
if (originalImplementation) {
class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@");
}
AspectLog(@"Aspects: %@ is now aspect aware.", NSStringFromClass(klass));
}
上面的這個(gè)方法會(huì)用__aspects_forwardInvocation:
方法來替換forwardInvocation:
方法。Aspects創(chuàng)建了AspectInfo這個(gè)類,里面包含類實(shí)例和一個(gè)NSInvocation
,這個(gè)NSInvocation
就來自于forwardInvocation:
方法的傳參。Aspects再將AspectInfo實(shí)例關(guān)聯(lián)到對(duì)象上。
當(dāng)程序執(zhí)行viewWillAppear:
時(shí),就會(huì)執(zhí)行替換過的aspects__viewWillAppear:
方法,由于并沒有這個(gè)方法的實(shí)現(xiàn),那么就會(huì)走forwardInvocation:
方法,這個(gè)方法也已經(jīng)被替換為__aspects_forwardInvocation:
方法,那么最終走的就會(huì)是__aspects_forwardInvocation:
這個(gè)方法。
OK,我們理清了。
我們繼續(xù):
static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) {
NSCParameterAssert(self);
NSCParameterAssert(invocation);
SEL originalSelector = invocation.selector;
SEL aliasSelector = aspect_aliasForSelector(invocation.selector);
invocation.selector = aliasSelector;
AspectsContainer *objectContainer = objc_getAssociatedObject(self, aliasSelector);
AspectsContainer *classContainer = aspect_getContainerForClass(object_getClass(self), aliasSelector);
AspectInfo *info = [[AspectInfo alloc] initWithInstance:self invocation:invocation];
NSArray *aspectsToRemove = nil;
// Before hooks.
aspect_invoke(classContainer.beforeAspects, info);
aspect_invoke(objectContainer.beforeAspects, info);
// Instead hooks.
BOOL respondsToAlias = YES;
if (objectContainer.insteadAspects.count || classContainer.insteadAspects.count) {
aspect_invoke(classContainer.insteadAspects, info);
aspect_invoke(objectContainer.insteadAspects, info);
}else {
Class klass = object_getClass(invocation.target);
do {
if ((respondsToAlias = [klass instancesRespondToSelector:aliasSelector])) {
[invocation invoke];
break;
}
}while (!respondsToAlias && (klass = class_getSuperclass(klass)));
}
// After hooks.
aspect_invoke(classContainer.afterAspects, info);
aspect_invoke(objectContainer.afterAspects, info);
// If no hooks are installed, call original implementation (usually to throw an exception)
if (!respondsToAlias) {
invocation.selector = originalSelector;
SEL originalForwardInvocationSEL = NSSelectorFromString(AspectsForwardInvocationSelectorName);
if ([self respondsToSelector:originalForwardInvocationSEL]) {
((void( *)(id, SEL, NSInvocation *))objc_msgSend)(self, originalForwardInvocationSEL, invocation);
}else {
[self doesNotRecognizeSelector:invocation.selector];
}
}
// Remove any hooks that are queued for deregistration.
[aspectsToRemove makeObjectsPerformSelector:@selector(remove)];
}
這里有個(gè)宏定義,aspect_invoke
,這個(gè)方法是這樣的:
#define aspect_invoke(aspects, info) for (AspectIdentifier *aspect in aspects) { [aspect invokeWithInfo:info]; if (aspect.options & AspectOptionAutomaticRemoval) { aspectsToRemove = [aspectsToRemove?:@[] arrayByAddingObject:aspect]; } }
aspect_invoke
方法會(huì)遍歷AspectsContainer
上綁定的AspectIdentifier
,然后去執(zhí)行AspectIdentifier
上的- (BOOL)invokeWithInfo:(id<AspectInfo>)info
方法,這個(gè)方法中會(huì)執(zhí)行[blockInvocation invokeWithTarget:self.block];
方法,而這個(gè)self.block就是
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
這個(gè)方法上的block。
所以__ASPECTS_ARE_BEING_CALLED__
方法在執(zhí)行上,會(huì)先去執(zhí)行beforeAspects
數(shù)組中的AspectIdentifier
,之后執(zhí)行insteadAspects
數(shù)組中的AspectIdentifier
,最后到afterAspects
。這就實(shí)現(xiàn)了在方法前和方法后的hook。
class_getInstanceMethod
、class_addMethod
、class_replaceMethod
方法,讓我們?cè)陂_發(fā)中可以很方便的獲取、添加、替換對(duì)象的方法。當(dāng)然,如果你想獲取對(duì)象所有的方法的話,你可以使用Method class_getInstanceMethod(Class cls, SEL name)
這個(gè)方法。
交換方法
UITextView+Placeholder是一個(gè)用來給UITextView
添加Placeholder的category,在它的源碼里面有這么一個(gè)方法:
- (void)swizzledDealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
UILabel *label = objc_getAssociatedObject(self, @selector(placeholderLabel));
if (label) {
for (NSString *key in self.class.observingKeys) {
@try {
[self removeObserver:self forKeyPath:key];
}
@catch (NSException *exception) {
// Do nothing
}
}
}
[self swizzledDealloc];
}
我們?cè)?code>swizzledDealloc方法最后看到了這么一行:
[self swizzledDealloc];
這不死循環(huán)了嘛!其實(shí)不是。
在這個(gè)類的load方法上,已經(jīng)將dealloc
方法和swizzledDealloc
方法進(jìn)行交換了。我們看下面的代碼:
+ (void)load {
[super load]; method_exchangeImplementations(class_getInstanceMethod(self.class, NSSelectorFromString(@"dealloc")),
class_getInstanceMethod(self.class, @selector(swizzledDealloc)));
}
我們?cè)谡{(diào)用dealloc
的時(shí)候,實(shí)際上會(huì)去走swizzledDealloc
方法,而在swizzledDealloc
方法中調(diào)用swizzledDealloc
方法,會(huì)去走真正的dealloc
方法。
這種方法可以實(shí)現(xiàn)一定的hook,通過這種方式我們依然可以在某個(gè)方法執(zhí)行前、執(zhí)行后添加代碼,或者直接替換方法。
獲取、添加類
這是一個(gè)很有意思的東西。
現(xiàn)在,某個(gè)實(shí)習(xí)生接到了這么一個(gè)任務(wù):目前項(xiàng)目中都是使用UIAlertView,現(xiàn)在需要在iOS8上使用UIAlertController來代替UIAlertView,而在iOS8下,依然保持原樣。
項(xiàng)目現(xiàn)在比較大,那怎么辦呢,他想到了一個(gè)辦法,做了一個(gè)XXAlertView,所有人使用AlertView的時(shí)候,都使用XXAlertView, XXAlertView接口和UIAlertView接口比較一致,修改的量倒不是很大。但是如果以后某個(gè)實(shí)習(xí)生不小心忘記使用XXAlertView,而直接使用了UIAlertController,導(dǎo)致程序掛了怎么辦呢?那是否要使用另一個(gè)腳本或工具來保證這種事情不會(huì)發(fā)生呢?
當(dāng)然,這是一種辦法,有沒有其他更簡單的辦法呢?有!
(當(dāng)然,我不是說上面的辦法不好,也不是說下面的辦法更好,這只是一種方式。)
我們可以對(duì)iOS8以下的系統(tǒng)添加一個(gè)類,名叫UIAlertController,API與系統(tǒng)UIAlertController保持一致。開發(fā)人員在以后的程序開發(fā)中,都寫UIAlertController,他不用時(shí)刻提醒自己,要使用XXAlertView,完全可以按照自己的習(xí)慣寫。哪怕他沒寫UIAlertController,而寫了UIAlertView也沒事,好歹可以正常工作的。
FDStackView是一個(gè)ForkingDog組織開發(fā)和維護(hù)的一個(gè)開源項(xiàng)目。
UIStackView
是iOS9上新增加的一種很方便進(jìn)行流式布局的工具,很好,很強(qiáng),但是只在iOS9及其以上。所以,他們做了一個(gè)FDStackView
。
只需要將代碼添加到工程中,不需要import。什么也不用做,就和平時(shí)寫UIStackView
一樣。
在iOS9及其以上,自然會(huì)調(diào)用系統(tǒng)的UIStackView
,而在iOS9以下,會(huì)使用FDStackView
替換UIStackView
。
怎么實(shí)現(xiàn)的呢,我們看源碼:
// ----------------------------------------------------
// Runtime injection start.
// Assemble codes below are based on:
// https://github.com/0xced/NSUUID/blob/master/NSUUID.m
// ----------------------------------------------------
#pragma mark - Runtime Injection
__asm(
".section __DATA,__objc_classrefs,regular,no_dead_strip\n"
#if TARGET_RT_64_BIT
".align 3\n"
"L_OBJC_CLASS_UIStackView:\n"
".quad _OBJC_CLASS_$_UIStackView\n"
#else
".align 2\n"
"_OBJC_CLASS_UIStackView:\n"
".long _OBJC_CLASS_$_UIStackView\n"
#endif
".weak_reference _OBJC_CLASS_$_UIStackView\n"
);
// Constructors are called after all classes have been loaded.
__attribute__((constructor)) static void FDStackViewPatchEntry(void) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
@autoreleasepool {
// >= iOS9.
if (objc_getClass("UIStackView")) {
return;
}
Class *stackViewClassLocation = NULL;
#if TARGET_CPU_ARM
__asm("movw %0, :lower16:(_OBJC_CLASS_UIStackView-(LPC0+4))\n"
"movt %0, :upper16:(_OBJC_CLASS_UIStackView-(LPC0+4))\n"
"LPC0: add %0, pc" : "=r"(stackViewClassLocation));
#elif TARGET_CPU_ARM64
__asm("adrp %0, L_OBJC_CLASS_UIStackView@PAGE\n"
"add %0, %0, L_OBJC_CLASS_UIStackView@PAGEOFF" : "=r"(stackViewClassLocation));
#elif TARGET_CPU_X86_64
__asm("leaq L_OBJC_CLASS_UIStackView(%%rip), %0" : "=r"(stackViewClassLocation));
#elif TARGET_CPU_X86
void *pc = NULL;
__asm("calll L0\n"
"L0: popl %0\n"
"leal _OBJC_CLASS_UIStackView-L0(%0), %1" : "=r"(pc), "=r"(stackViewClassLocation));
#else
#error Unsupported CPU
#endif
if (stackViewClassLocation && !*stackViewClassLocation) {
Class class = objc_allocateClassPair(FDStackView.class, "UIStackView", 0);
if (class) {
objc_registerClassPair(class);
*stackViewClassLocation = class;
}
}
}
});
}
FDStackView
使用了一段來自NSUUID
的代碼,有一些匯編,不好懂(其實(shí)一定意義上來說,這里面一部分已經(jīng)不是runtime的特性了,不過也確實(shí)是在運(yùn)行時(shí)去做的,我這里也就一并搬上來了)。大家有興趣可以看一下yaqing對(duì)這段匯編的解釋。
那么,上面代碼中的objc_getClass
就是獲取類的一種方式,當(dāng)然,還有objc_lookUpClass
、NSClassFromString
等方式。
添加類的話,可以使用objc_addClass
方法。這與我們上面的舉例不一樣,主要是因?yàn)?code>FDStackView類創(chuàng)建的類是需要去替換UIStackView
類的,在iOS9以下,UIStackView
類即FDStackView
類。
FDStackView
的這種做法很方便,但是,也有個(gè)問題,在低版本上,對(duì)UIStackView
添加的category會(huì)失效,因?yàn)榈桶姹旧弦呀?jīng)使用的是FDStackView
,而不是UIStackView了,而category是添加在UIStackView
上的,所以是不起作用的。
大家可以參考這篇文檔:
http://hailoong.sinaapp.com/?p=125
其他
runtime還有其他大量功能,這些都是黑魔法,用好了,省時(shí)省力,用不好,說不定哪天就是災(zāi)難。