Objective-C runtime常見用法
runtime是Objective-C上一個非常強大的屠龍刀,提供了很多奇幻的魔法,當然,如果過度濫用的話,維護上的代價也是顯而易見的。
我們這里只討論一下我們平常工作中常用的特性,當然,它有大量功能,只是我們并不一定用的到,類似objc_msgSend這種的我們也不作介紹。
Objective-C runtime已經開源了,有閱讀源碼習慣的程序員可以前往官網下載閱讀。
下面是下載地址: http://www.opensource.apple.com/tarballs/objc4/
添加、獲取屬性
以開源庫 SVPullToRefresh (SVPullToRefresh是一個提供上下拉刷新的庫)舉例。
在 UIScrollView+SVPullToRefresh 這個Category上, SVPullToRefresh 給UIScrollView動態添加了一個屬性,我們以 SVPullToRefreshView *pullToRefreshView 這個屬性舉例。
在 UIScrollView+SVPullToRefresh.h 上先申明了這個屬性
@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);
語法結構如下:
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
這個方法用于使用一個給定的Key和policy,將對象和值(Value)相關聯。
object:需要關聯的對象
key:用于關聯的Key
value:使用Key來關聯在對象上的值,如果設置為nil,則清除已綁定的值。
policy:關聯的政策,提供assign、retain、copy等政策。
Getter:
- (SVPullToRefreshView *)pullToRefreshView { return objc_getAssociatedObject(self, &UIScrollViewPullToRefreshView); }
和上面類似,這里使用的是runtime里面的這個方法:
id objc_getAssociatedObject(id object, const void *key)
這個方法用于獲取對象相關聯的值,與上面的方法相呼應。 objc_getAssociatedObject 這種方式只能獲取已知的屬性,如果一個類有很多屬性,但是我們可能并不知道它的具體名稱和類型,那怎么辦呢?
我們以 JsonModel 舉例, JsonModel 是一個 Json 轉 Model 的一個庫,一般用于將從服務端或者某處獲得的 Json 字符串映射到對象上,并完成填充工作。
我們以官方文檔上的例子舉例:
#import "JSONModel.h" @interface CountryModel : JSONModel @property (strong, nonatomic) NSString* country; @end
我們寫了一個類,名叫 CountryModel ,繼承于 JSONModel ,我們在 CountryModel 中聲明了名叫 country 的一個 NSString 類型的屬性。
之后,我們將獲取到的 Json 字符串進行填充的時候,它就自動填充好了。
#import "CountryModel.h" ... NSString* json = (fetch here JSON from Internet) ... NSError* err = nil; CountryModel* country = [[CountryModel alloc] initWithString:json error:&err];
如果不考慮一些其他東西的話,這要比我們手寫賦值要簡單,起碼可以省一些代碼。那,它是怎么實現的呢,我們看一下它的源代碼:
JsonModel.m
-(void)__inspectProperties { ... while (class != [JSONModel class]) { //JMLog(@"inspecting: %@", NSStringFromClass(class)); unsigned int propertyCount; objc_property_t *properties = class_copyPropertyList(class, ∝ertyCount); 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); ... }
這個是 JsonModel 中最主要的方法之一,比較長,我只摘取其中的一部分,忽略了很多其他特性,比如它使用protocol的方式去給屬性添加option等描述,或者從protocol上取類名,再給數組賦值。
JsonModel 會遍歷Model,通過 class_copyPropertyList 方法來獲取類上所有的屬性:
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
通過 property_getName 方法來獲取屬性名稱: const char *property getName(objc property_t property)
之后會依據此名稱,動態創建setter和getter方法,這里不提。
objc_getAssociatedObject 、 class_copyPropertyList 、 property_getName 和 objc_setAssociatedObject 方法讓我們在開發中,可以很方便的對對象進行屬性獲取和屬性添加的操作。
添加、獲取、替換方法
蘋果提供了Category等方式,使得我們可以很簡單的給已知類添加方法。但是在很多時候,我們并不能明確的知道我們要添加的方法叫什么,這個時候我們可以使用runtime提供的一些方法。
以開源庫 Aspects 為例,Aspects是一個攔截器,它可以在某個方法運行前、運行后進行攔截,來加載其他的一些操作,或者替換整個方法。
使用方式類似于下面這種:
[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];
如果現在有一個需求,要給所有VC的 viewWillAppear: 方法上加一個log的方法,我們可能會使用繼承的辦法,在基類上面重寫 viewWillAppear: 方法,并添加打印的辦法。除此之外,使用Aspects攔截 viewWillAppear: ,然后在執行之前或者執行之后添加log方法,也是一種辦法,這里并不探討孰優孰劣。
我們來看一下它的實現原理:
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類并添加或者替換方法的方法。 這里入參上面的selector即我們的@selector(viewWillAppear:) 。
Method targetMethod = class_getInstanceMethod(klass, selector); IMP targetMethodIMP = method_getImplementation(targetMethod);
class_getInstanceMethod 方法可以獲取到指定類的指定方法,返回的是一個Method,而通過 method_getImplementation 方法獲取到Method結構體的IMP指針。 (這個Method是一個結構體,這里不多說明,大家可以自行查看一下runtime的源碼中對Method結構體的定義,在 objc-private.h 中可以找到,或者你可以參考我的另一篇博客 Objective-C中為什么不支持泛型方法 。)
SEL aliasSelector = aspect_aliasForSelector(selector);
之后,使用 aspect_aliasForSelector 方法,給selector添加了一個前綴,新的方法名為 aspects__viewWillAppear: 。
__unused BOOL addedAlias = class_addMethod(klass, aliasSelector, method_getImplementation(targetMethod), typeEncoding);
class_addMethod 方法會給類動態添加方法,這里就是給 UIViewController 添加了一個 aspects__viewWillAppear 方法,而這個方法的實現,依然是原來 viewWillAppear: 方法的實現。
到這一步,Aspects只是給類添加了一個 aspects__viewWillAppear: 方法,并沒有hook進去。
class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
class_replaceMethod 方法,顧名思義,用來替換方法的。這個方法使用 aspects__viewWillAppear: 替換了 viewWillAppear: 方法,但是類里面并沒有實現 aspects__viewWillAppear: 方法,這個意思就是說,當執行 viewWillAppear: 方法的時候,就會去執行 aspects__viewWillAppear: ,而 aspects__viewWillAppear: 這個方法我們并沒有實現。那會不會掛呢。。。
Aspects這里使用了一個特殊的辦法來hook。
- (void)forwardInvocation:(NSInvocation *)anInvocation
NSObject中提供了 forwardInvocation: 方法,這個方法會在對象收到一個無法響應的selector之后,給 forwardInvocation: 方法發一條消息,或者說調用一下NSObject的這個方法。
我們回到最上面的一條方法:
Class klass = aspect_hookClass(self, error);
這個方法會調用這么幾個方法,其中有一個是:
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)); }
上面的這個方法會用 __aspects_forwardInvocation: 方法來替換 forwardInvocation: 方法。Aspects創建了AspectInfo這個類,里面包含類實例和一個 NSInvocation ,這個 NSInvocation 就來自于 forwardInvocation: 方法的傳參。Aspects再將AspectInfo實例關聯到對象上。
當程序執行 viewWillAppear: 時,就會執行替換過的 aspects__viewWillAppear: 方法,由于并沒有這個方法的實現,那么就會走 forwardInvocation: 方法,這個方法也已經被替換為 __aspects_forwardInvocation: 方法,那么最終走的就會是 __aspects_forwardInvocation: 這個方法。
OK,我們理清了。
我們繼續:
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)]; }
這里有個宏定義, aspect_invoke ,這個方法是這樣的:
#define aspect_invoke(aspects, info) \ for (AspectIdentifier *aspect in aspects) {\ [aspect invokeWithInfo:info];\ if (aspect.options & AspectOptionAutomaticRemoval) { \ aspectsToRemove = [aspectsToRemove?:@[] arrayByAddingObject:aspect]; \ } \ }
aspect_invoke 方法會遍歷 AspectsContainer 上綁定的 AspectIdentifier ,然后去執行 AspectIdentifier 上的 - (BOOL)invokeWithInfo:(id<AspectInfo>)info 方法,這個方法中會執行 [blockInvocation invokeWithTarget:self.block]; 方法,而這個self.block就是
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector withOptions:(AspectOptions)options usingBlock:(id)block error:(NSError **)error;
這個方法上的block。
所以 __ASPECTS_ARE_BEING_CALLED__ 方法在執行上,會先去執行 beforeAspects 數組中的 AspectIdentifier ,之后執行 insteadAspects 數組中的 AspectIdentifier ,最后到 afterAspects 。這就實現了在方法前和方法后的hook。
class_getInstanceMethod 、 class_addMethod 、 class_replaceMethod 方法,讓我們在開發中可以很方便的獲取、添加、替換對象的方法。當然,如果你想獲取對象所有的方法的話,你可以使用 Method class_getInstanceMethod(Class cls, SEL name) 這個方法。
交換方法
UITextView+Placeholder 是一個用來給 UITextView 添加Placeholder的category,在它的源碼里面有這么一個方法:
- (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]; }
我們在 swizzledDealloc 方法最后看到了這么一行:
[self swizzledDealloc];
這不死循環了嘛!其實不是。
在這個類的load方法上,已經將 dealloc 方法和 swizzledDealloc 方法進行交換了。我們看下面的代碼:
+ (void)load { [super load]; method_exchangeImplementations(class_getInstanceMethod(self.class, NSSelectorFromString(@"dealloc")), class_getInstanceMethod(self.class, @selector(swizzledDealloc))); }
我們在調用 dealloc 的時候,實際上會去走 swizzledDealloc 方法,而在 swizzledDealloc 方法中調用 swizzledDealloc 方法,會去走真正的 dealloc 方法。
這種方法可以實現一定的hook,通過這種方式我們依然可以在某個方法執行前、執行后添加代碼,或者直接替換方法。
獲取、添加類
這是一個很有意思的東西。
現在,某個實習生接到了這么一個任務:目前項目中都是使用UIAlertView,現在需要在iOS8上使用UIAlertController來代替UIAlertView,而在iOS8下,依然保持原樣。
項目現在比較大,那怎么辦呢,他想到了一個辦法,做了一個XXAlertView,所有人使用AlertView的時候,都使用XXAlertView, XXAlertView接口和UIAlertView接口比較一致,修改的量倒不是很大。但是如果以后某個實習生不小心忘記使用XXAlertView,而直接使用了UIAlertController,導致程序掛了怎么辦呢?那是否要使用另一個腳本或工具來保證這種事情不會發生呢?
當然,這是一種辦法,有沒有其他更簡單的辦法呢?有! (當然,我不是說上面的辦法不好,也不是說下面的辦法更好,這只是一種方式。)
我們可以對iOS8以下的系統添加一個類,名叫UIAlertController,API與系統UIAlertController保持一致。開發人員在以后的程序開發中,都寫UIAlertController,他不用時刻提醒自己,要使用XXAlertView,完全可以按照自己的習慣寫。哪怕他沒寫UIAlertController,而寫了UIAlertView也沒事,好歹可以正常工作的。
FDStackView 是一個ForkingDog組織開發和維護的一個開源項目。
UIStackView 是iOS9上新增加的一種很方便進行流式布局的工具,很好,很強,但是只在iOS9及其以上。所以,他們做了一個 FDStackView 。
只需要將代碼添加到工程中,不需要import。什么也不用做,就和平時寫 UIStackView 一樣。
在iOS9及其以上,自然會調用系統的 UIStackView ,而在iOS9以下,會使用 FDStackView 替換 UIStackView 。
怎么實現的呢,我們看源碼:
// ---------------------------------------------------- // 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 的代碼,有一些匯編,不好懂(其實一定意義上來說,這里面一部分已經不是runtime的特性了,不過也確實是在運行時去做的,我這里也就一并搬上來了)。大家有興趣可以看一下 yaqing 對這段匯編的解釋。
那么,上面代碼中的 objc_getClass 就是獲取類的一種方式,當然,還有 objc_lookUpClass 、 NSClassFromString 等方式。
添加類的話,可以使用 objc_addClass 方法。這與我們上面的舉例不一樣,主要是因為 FDStackView 類創建的類是需要去替換 UIStackView 類的,在iOS9以下, UIStackView 類即 FDStackView 類。
FDStackView 的這種做法很方便,但是,也有個問題,在低版本上,對 UIStackView 添加的category會失效,因為低版本上已經使用的是 FDStackView ,而不是UIStackView了,而category是添加在 UIStackView 上的,所以是不起作用的。 大家可以參考這篇文檔: http://hailoong.sinaapp.com/?p=125
其他
runtime還有其他大量功能,這些都是黑魔法,用好了,省時省力,用不好,說不定哪天就是災難。
來自: http://ifujun.com/objective-c-runtimechang-jian-yong-fa/