Runtime是iOS中比較難以理解, 但又非常強大的技術(shù).
The Objective-C language defers as many decisions as it can from compile time and link time to runtime. Whenever possible, it does things dynamically. This means that the language requires not just a compiler, but also a runtime system to execute the compiled code. The runtime system acts as a kind of operating system for the Objective-C language; it’s what makes the language work.
所謂運行時, 就是盡可能地把決定從編譯器推遲到運行期, 就是盡可能地做到動態(tài). 只是在運行的時候才會去確定對象的類型和方法的. 因此利用Runtime機制可以在程序運行時動態(tài)地修改類和對象中的所有屬性和方法.
Objective-C中調(diào)用對象的方法時, 會向該對象發(fā)送一條消息, runtime根據(jù)該消息做出反應(yīng).
Runtime是一套比較底層的純C語言的API, Objective-C是運行在Runtime上的, 因此在Runtime中動態(tài)添加和實現(xiàn)一些非常強大的功能也就不足為奇了.
在Objective-C代碼中使用Runtime, 需要引入
#import <objc/runtime.h>
實例變量
OC中類和對象的實例變量, 實際上是一個指向objc_ivar結(jié)構(gòu)體的指針I(yè)var.
/// An opaque type that represents an instance variable.
typedef struct objc_ivar *Ivar;
通過runtime方法class_copyIvarList可獲取一個指向類和對象的所有實例變量的Ivar指針.
u_int count = 0;
Ivar *ivars = class_copyIvarList([UIView class], &count);
for (int i = 0; i < count; i++) {
const char *ivarName = ivar_getName(ivars[i]);
NSString *str = [NSString stringWithCString:ivarName encoding:NSUTF8StringEncoding];
NSLog(@"ivarName : %@", str);
}
獲取到實例變量的Ivar指針后, 即可通過ivar_getName方法獲取該實例變量的名稱, 由C語言的字符串表示.
部分打印結(jié)果如下:
ivarName : _constraintsExceptingSubviewAutoresizingConstraints
ivarName : _cachedTraitCollection
ivarName : _layer
ivarName : _layerRetained
ivarName : _gestureInfo
ivarName : _gestureRecognizers
ivarName : _subviewCache
ivarName : _templateLayoutView
ivarName : _charge
ivarName : _tag
ivarName : _viewDelegate
ivarName : _backgroundColorSystemColorName
ivarName : _countOfMotionEffectsInSubtree
ivarName : _countOfTraitChangeRespondersInDirectSubtree
ivarName : _cachedScreenScale
ivarName : _viewFlags
ivarName : _retainCount
可以看到, 其中包含了UIView中的很多我們經(jīng)常使用的實例變量.
對于屬性, 如layer, tag等, 會自動生成 _ 前綴的成員變量
屬性
OC中類和對象的屬性, 是一個指向objc_property結(jié)構(gòu)體的指針objc_property_t.
/// An opaque type that represents an Objective-C declared property.
typedef struct objc_property *objc_property_t;
通過runtime方法class_copyPropertyList可獲取一個指向類和對象的所有屬性的objc_property_t指針.
u_int count = 0;
objc_property_t *properties = class_copyPropertyList([UIView class], &count);
for (int i = 0; i < count; i++) {
const char *propertyName = property_getName(properties[i]);
NSString *str = [NSString stringWithCString:propertyName encoding:NSUTF8StringEncoding];
NSLog(@"propertyName : %@", str);
}
free(properties);
獲取到屬性的objc_property_t指針后, 即可通過property_getName方法獲取該屬性的名稱, 由C語言的字符串表示.
部分打印結(jié)果如下:
propertyName : hash
propertyName : superclass
propertyName : description
propertyName : debugDescription
propertyName : hash
propertyName : superclass
propertyName : description
propertyName : debugDescription
propertyName : userInteractionEnabled
propertyName : tag
propertyName : layer
propertyName : focused
propertyName : hash
propertyName : superclass
propertyName : description
propertyName : center
propertyName : bounds
propertyName : transform
方法
OC中類和對象的方法, 是一個指向objc_method結(jié)構(gòu)體的指針Method.
/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;
通過runtime方法class_copyMethodList可獲取一個指向類和對象的所有方法的Method指針.
u_int count = 0;
// 獲取所有方法
Method *methods = class_copyMethodList([UIView class], &count);
for (int i = 0; i < count; i++) {
Method method = methods[i];
// 方法類型是SEL選擇器類型
SEL methodName = method_getName(method);
NSString *str = [NSString stringWithCString:sel_getName(methodName) encoding:NSUTF8StringEncoding];
int arguments = method_getNumberOfArguments(method);
NSLog(@"methodName : %@, arguments Count: %d", str, arguments);
}
free(methods);
獲取到方法的Method指針后, 即可通過method_getName方法獲取該方法的名稱, 由C語言的字符串表示.
使用method_getNumberOfArguments可獲取該方法的參數(shù)個數(shù).
部分打印結(jié)果如下:
2016-04-12 13:32:19.925 DemoRuntime[2244:54083] methodName : captureImageIBAIncludingSubviews:, arguments Count: 3
2016-04-12 13:32:19.925 DemoRuntime[2244:54083] methodName : captureImageFromIOSurfaceIBA, arguments Count: 2
2016-04-12 13:32:19.925 DemoRuntime[2244:54083] methodName : hideSubviewsIBA, arguments Count: 2
2016-04-12 13:32:19.925 DemoRuntime[2244:54083] methodName : safeCaptureImageIBA, arguments Count: 2
2016-04-12 13:32:19.926 DemoRuntime[2244:54083] methodName : unhideSubviewsIBAWithContext:, arguments Count: 3
2016-04-12 13:32:19.926 DemoRuntime[2244:54083] methodName : snapshotViewHierarchyIBA, arguments Count: 2
2016-04-12 13:32:19.926 DemoRuntime[2244:54083] methodName : iba_viewControllerName, arguments Count: 2
2016-04-12 13:32:19.926 DemoRuntime[2244:54083] methodName : iba_viewControllerMemoryAddress, arguments Count: 2
2016-04-12 13:32:19.926 DemoRuntime[2244:54083] methodName : iba_alignmentRectForCurrentFrame, arguments Count: 2
2016-04-12 13:32:19.927 DemoRuntime[2244:54083] methodName : iba_participatingConstraints, arguments Count: 2
2016-04-12 13:32:19.927 DemoRuntime[2244:54083] methodName : setIba_contentHuggingPriorities:, arguments Count: 3
Method Swizzling
通過修改一個已存在類的方法, 來實現(xiàn)方法替換是比較常用的runtime技巧.
如在UIView的load方法中:
+ (void)load {
Method origin = class_getInstanceMethod([UIView class], @selector(touchesBegan:withEvent:));
Method custom = class_getInstanceMethod([UIView class], @selector(custom_touchesBegan:withEvent:));
method_exchangeImplementations(origin, custom);
}
- (void)custom_touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// TODO
}
這樣, 想要觸發(fā)UIView的touchesBegan:withEvent:方法時, 實際調(diào)用的卻是自定義的custom_touchesBegan:withEvent:方法.
另外, 關(guān)于runtime method swizzling的一個使用場景, 請參考博客:
iOS --- 使用runtime解決3D Touch導(dǎo)致UIImagePicker崩潰的問題
關(guān)聯(lián)對象
Objective-C中的Category無法向既有的類添加屬性, 因此可以使用runtime的關(guān)聯(lián)對象(associated objects)來實現(xiàn).
如將一個字符串關(guān)聯(lián)到一個數(shù)組:
static char overviewKey;
NSArray *array = [[NSArray alloc] initWithObjects:@"1", @"2", @"3", nil];
// 為了演示的目的,使用initWithFormat:來確保字符串可以被銷毀
NSString *overview = [[NSString alloc] initWithFormat:@"@", @"first three numbers"];
objc_setAssociatedObject(array, &overviewKey, overview, OBJC_ASSOCIATION_RETAIN);
這樣, 當(dāng)overview被手動釋放時, 卻不會被銷毀. 因為關(guān)聯(lián)策略指明了數(shù)組array要保有相關(guān)聯(lián)的對象.
而array也釋放時, overview才會被銷毀.
設(shè)置關(guān)聯(lián)對象, 指定被關(guān)聯(lián)對象array, 關(guān)聯(lián)關(guān)鍵字overviewKey, 關(guān)聯(lián)對象overview即可:
objc_setAssociatedObject(array, &overviewKey, overview, OBJC_ASSOCIATION_RETAIN);
獲取關(guān)聯(lián)對象, 需要傳遞被關(guān)聯(lián)對象array和關(guān)聯(lián)關(guān)鍵字overviewKey:
NSString *associatedObject = (NSString *)objc_getAssociatedObject(array, &overviewKey);
斷開關(guān)聯(lián), 只需要設(shè)置關(guān)聯(lián)對象為nil即可, 關(guān)聯(lián)策略就無所謂了.
objc_setAssociatedObject(array, &overviewKey, nil, OBJC_ASSOCIATION_ASSIGN);
使用objc_removeAssociatedObjects可斷開所有關(guān)聯(lián), 把對象恢復(fù)至原始狀態(tài).
Demo
Demo地址:
DemoRuntime