Objective-C基于C語言加入了面向對象特性和消息轉發,Objective-C 的消息轉發需要運行時系統來動態創建類和對象,從而進行消息發送和轉發.
objc_msgSend
C語言在編譯階段決定運行時調用的函數,會生成直接調用函數的指令,相當于靜態綁定.作為C語言的超集所有調用的函數直到運行期才能確定,對象收到消息后,具體調用哪個方法由運行期決定,甚至可以在運行的時候改變,所以Objective-C也被稱之為動態語言.
Objective-C正常調用實例對象方法:
[course fetchData];
以上代碼會被轉換成標準的C語言函數調用,等價于以下代碼:
Course *courseClass = objc_msgSend([Course class], sel_registerName("alloc"));
courseClass = objc_msgSend(courseClass, sel_registerName("init"));
SEL sel = sel_registerName("fetchData");
objc_msgSend(courseClass,sel);
頭文件導入:
#import <objc/message.h>
#import <objc/runtime.h>
編譯出錯,需要修改設置選項:
objc_msgSend定義如下:
/**
* Sends a message with a simple return value to an instance of a class.
*
* @param self A pointer to the instance of the class that is to receive the message.
* @param op The selector of the method that handles the message.
* @param ...
* A variable argument list containing the arguments to the method.
*
* @return The return value of the method.
*
* @note When it encounters a method call, the compiler generates a call to one of the
* functions \c objc_msgSend, \c objc_msgSend_stret, \c objc_msgSendSuper, or \c objc_msgSendSuper_stret.
* Messages sent to an object’s superclass (using the \c super keyword) are sent using \c objc_msgSendSuper;
* other messages are sent using \c objc_msgSend. Methods that have data structures as return values
* are sent using \c objc_msgSendSuper_stret and \c objc_msgSend_stret.
*/
OBJC_EXPORT id objc_msgSend(id self, SEL op, ...)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0);
/**
SEL是映射到方法的C字符串,可以通過Objc 編譯器命令@selector()或者 Runtime 系統的sel_registerName函數來獲得一個SEL類型的方法選擇器.
所以下面的代碼和的代碼執行結果一致:
Course *courseClass = objc_msgSend([Course class], @selector(alloc));
courseClass = objc_msgSend(courseClass, @selector(init));
SEL sel = @selector(fetchData);
objc_msgSend(courseClass,sel);
Objective-C類是由Class類型來表示的,它是一個指向objc_class結構體的指針,objc_class定義如下:
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
類對象也是一個實例,類對象中的isa指針指向的是元類(meta class),當我們進行類方法查詢的時候就會通過isa指針找到元類查詢.元類最終會指向根元類,形成查找閉環.
super_class指向父類,如果類對象本身無法實現,可以通過父類進行查找實現.
如果每次方法查詢都需要通過methodlist來查找實現,那么效率很低,所以查找過一次會通過cache保存其實現,提升方法查找效率.
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method buckets[1] OBJC2_UNAVAILABLE;
};
消息轉發
Objective-C中經常遇到找到實現方法的問題,解決這個問題有三種方案,動態方法解析,重定向接受者和完整的消息轉發.
動態方法解析,可以通過系統提供resolveClassMethod和resolveInstanceMethod來實現.
+ (BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
+ (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
Course類中定義一個calculatData方法,但是不實現.
@interface Course : NSObject
- (void)fetchData;
- (void)calculatData;
@end
調用:
Course *course = [Course new];
[course calculatData];
報錯信息:
[Course calculatData]: unrecognized selector sent to instance 0x608000013570
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Course calculatData]: unrecognized selector sent to instance 0x608000013570'
重寫resolveInstanceMethod方法:
+ (BOOL)resolveClassMethod:(SEL)sel {
return [super resolveClassMethod:sel];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(calculatData)) {
class_addMethod([self class], sel, (IMP)resolveMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
void resolveMethodIMP(id self, SEL _cmd) {
NSLog(@"resolveInstanceMethod---resolveMethodIMP");
}
重置接收者,當前接受者如果不能處理對象,可以交由其他對象處理,類似于web端的重定向,也可以通過這種組合的方式實現多繼承.
定義未實現的方法:
- (void)fetchMethod;
定義新的接收者:
@implementation Teacher
- (void)fetchMethod {
NSLog(@"Teacher---fetchMethod");
}
@end
forwardingTargetForSelector實現:
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(fetchMethod)) {
return [Teacher new];
}
return [super forwardingTargetForSelector:aSelector];
}
調用:
Course *course = [Course new];
[course calculatData];
[course fetchMethod];
結果:
Teacher---fetchMethod
最后一步保證是完成的消息轉發機制,需要重寫methodSignatureForSelector和forwardInvocation方法.
- (void)forwardInvocation:(NSInvocation *)anInvocation OBJC_SWIFT_UNAVAILABLE("");
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector OBJC_SWIFT_UNAVAILABLE("");
Course類中的方法簽名重寫和執行:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSLog(@"%@",NSStringFromSelector(_cmd));
NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
if (!signature) {
if ([Teacher instancesRespondToSelector:aSelector]) {
signature = [Teacher instanceMethodSignatureForSelector:aSelector];
}
}
return signature;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"%@",NSStringFromSelector(_cmd));
if ([Teacher instanceMethodForSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:[Teacher new]];
} else {
[super forwardInvocation:anInvocation];
}
}
Teacher類中重寫forwardMethod方法:
- (void)forwardMethod {
NSLog(@"Teacher---forwardMethod");
}
方法調用:
Course *course = [Course new];
[course forwardMethod];
屬性與方法
Runtime中最常見的方法就是屬性和方法的獲取,屬性獲取通過class_copyPropertyList實現.
/**
* Describes the properties declared by a class.
*
* @param cls The class you want to inspect.
* @param outCount On return, contains the length of the returned array.
* If \e outCount is \c NULL, the length is not returned.
*
* @return An array of pointers of type \c objc_property_t describing the properties
* declared by the class. Any properties declared by superclasses are not included.
* The array contains \c *outCount pointers followed by a \c NULL terminator. You must free the array with \c free().
*
* If \e cls declares no properties, or \e cls is \c Nil, returns \c NULL and \c *outCount is \c 0.
*/
OBJC_EXPORT objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
Course *course = [[Course alloc] init];
Class class = [course class];
unsigned int procount = 0;
objc_property_t * properties = class_copyPropertyList(class, &procount);
for (int i = 0; i < procount; i++) {
objc_property_t property = properties[i];
const char *propertyName = property_getName(property);
NSLog(@"屬性名稱: %s", propertyName);
}
free(properties);
class_copyIvarList獲取對象的變量:
/**
* Describes the instance variables declared by a class.
*
* @param cls The class to inspect.
* @param outCount On return, contains the length of the returned array.
* If outCount is NULL, the length is not returned.
*
* @return An array of pointers of type Ivar describing the instance variables declared by the class.
* Any instance variables declared by superclasses are not included. The array contains *outCount
* pointers followed by a NULL terminator. You must free the array with free().
*
* If the class declares no instance variables, or cls is Nil, NULL is returned and *outCount is 0.
*/
OBJC_EXPORT Ivar *class_copyIvarList(Class cls, unsigned int *outCount)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
unsigned int varcount = 0;
Ivar *varlist = class_copyIvarList(class, &varcount);
for (int i = 0; i < varcount; i++) {
Ivar var = varlist[i];
const char *varname = ivar_getName(var);
NSLog(@"變量名稱: %s", varname);
}
free(varlist);
class_copyMethodList獲取方法列表:
/**
* Describes the instance methods implemented by a class.
*
* @param cls The class you want to inspect.
* @param outCount On return, contains the length of the returned array.
* If outCount is NULL, the length is not returned.
*
* @return An array of pointers of type Method describing the instance methods
* implemented by the class—any instance methods implemented by superclasses are not included.
* The array contains *outCount pointers followed by a NULL terminator. You must free the array with free().
*
* If cls implements no instance methods, or cls is Nil, returns NULL and *outCount is 0.
*
* @note To get the class methods of a class, use \c class_copyMethodList(object_getClass(cls), &count).
* @note To get the implementations of methods that may be implemented by superclasses,
* use \c class_getInstanceMethod or \c class_getClassMethod.
*/
OBJC_EXPORT Method *class_copyMethodList(Class cls, unsigned int *outCount)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
unsigned int methodcount = 0;
Method *methods = class_copyMethodList(class, &methodcount);
for (int i = 0; i < methodcount; i++) {
Method method = methods[i];
SEL method_name = method_getName(method);
NSLog(@"方法名稱:%@",NSStringFromSelector(method_name));
}
free(methods);
方法交換
Objective-C中方法交換(Method Swizzling)發生在運行時,在運行時將兩個Method進行交換,可以通過方法交換的特性實現AOP(面向切面編程),可以在埋點統計中執行.
交換UIViewController的viewDidLoad方法,執行完新的方法之后,繼續執行交換之前的方法,交換代碼在load方法中執行.Method Swizzling本質上就是對IMP和SEL進行交換.
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSelector = @selector(viewDidLoad);
SEL swizzledSelector = @selector(fe_viewDidload);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL didAddMethod = class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
- (void)fe_viewDidload {
NSLog(@"fe_viewDidload執行");
[self fe_viewDidload];
}
原始方法:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
NSLog(@"viewDidload執行");
}
執行結果:
fe_viewDidload執行
viewDidload執行
isa-swizzling
iOS開發中常用的KVO(Key-Value Observing)鍵值觀察是典型的isa
swizzling,官方文檔介紹如下:
Automatic key-value observing is implemented using a technique called isa-swizzling.
The isa
pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.
When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.
You should never rely on the isa
pointer to determine class membership. Instead, you should use the class
method to determine the class of an object instance.
鍵值觀察的主要方法:
@interface NSObject(NSKeyValueObserverRegistration)
/* Register or deregister as an observer of the value at a key path relative to the receiver. The options determine what is included in observer notifications and when they're sent, as described above, and the context is passed in observer notifications as described above. You should use -removeObserver:forKeyPath:context: instead of -removeObserver:forKeyPath: whenever possible because it allows you to more precisely specify your intent. When the same observer is registered for the same key path multiple times, but with different context pointers each time, -removeObserver:forKeyPath: has to guess at the context pointer when deciding what exactly to remove, and it can guess wrong.
*/
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context NS_AVAILABLE(10_7, 5_0);
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
@end
KVO在調用addObserver方法之后,將isa指向到另外一個類,在新類里面重寫被觀察的對象的class,setter,dealloc,_isKVOA方法.
定義Teacher類:
@interface Teacher : NSObject
@property (copy, nonatomic) NSString *teacherName;
- (void)fetchMethod;
- (void)forwardMethod;
@end
Teacher *teacher = [Teacher new];
NSLog(@"Teacher->isa:%@",object_getClass(teacher));
NSLog(@"Teacher class:%@",[teacher class]);
NSLog(@"Teacher方法列表 = %@",ClassMethodNames(object_getClass(teacher)));
[teacher addObserver:self forKeyPath:@"courseName" options:NSKeyValueObservingOptionNew context:nil];
NSLog(@"Teacher->isa:%@",object_getClass(teacher));
NSLog(@"Teacher class:%@",[teacher class]);
NSLog(@"Teacher方法列表 = %@",ClassMethodNames(object_getClass(teacher)));
self.teacher = teacher;
static NSArray * ClassMethodNames(Class c)
{
NSMutableArray * array = [NSMutableArray array];
unsigned int methodCount = 0;
Method * methodList = class_copyMethodList(c, &methodCount);
unsigned int i;
for(i = 0; i < methodCount; i++) {
[array addObject: NSStringFromSelector(method_getName(methodList[i]))];
}
free(methodList);
return array;
}
執行結果isa指向了新的類NSKVONotifying_Teacher.
Teacher->isa:Teacher
Teacher class:Teacher
Teacher方法列表 = (
fetchMethod,
forwardMethod,
teacherName,
"setTeacherName:",
".cxx_destruct"
)
Teacher->isa:NSKVONotifying_Teacher
Teacher class:Teacher
Teacher方法列表 = (
class,
dealloc,
"_isKVOA"
)
重寫setter方法:
/* Given a key that identifies a property (attribute, to-one relationship, or ordered or unordered to-many relationship), send -observeValueForKeyPath:ofObject:change:context: notification messages of kind NSKeyValueChangeSetting to each observer registered for the key, including those that are registered with other objects using key paths that locate the keyed value in this object. Invocations of these methods must always be paired.
The change dictionaries in notifications resulting from use of these methods contain optional entries if requested at observer registration time:
- The NSKeyValueChangeOldKey entry, if present, contains the value returned by -valueForKey: at the instant that -willChangeValueForKey: is invoked (or an NSNull if -valueForKey: returns nil).
- The NSKeyValueChangeNewKey entry, if present, contains the value returned by -valueForKey: at the instant that -didChangeValueForKey: is invoked (or an NSNull if -valueForKey: returns nil).
*/
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
關聯對象
關聯對象在類別中比較頻繁,假設擴展UITextField,新加屬性placeHolderColor.
@property (strong,nonatomic) UIColor *placeHolderColor;
-(UIColor *)placeHolderColor {
return objc_getAssociatedObject(self, placeHolderColorKey);
}
-(void)setPlaceHolderColor:(UIColor *)placeHolderColor {
[self setValue:placeHolderColor forKeyPath:@"_placeholderLabel.textColor"];
objc_setAssociatedObject(self,placeHolderColorKey, placeHolderColor, OBJC_ASSOCIATION_RETAIN);
}
歸檔和解檔
Objective-C中實現自定義對象的歸檔和解檔需要實現NSCoding協議.
@protocol NSCoding
- (void)encodeWithCoder:(NSCoder *)aCoder;
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder; // NS_DESIGNATED_INITIALIZER
@end
以Teacher類為例:
// 解檔
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
NSLog(@"initWithCoder");
self = [super init];
if (self) {
self.teacherName = [aDecoder decodeObjectForKey:@"teacherName"];
self.age = [[aDecoder decodeObjectForKey:@"age"] integerValue];
}
return self;
}
// 歸檔
- (void)encodeWithCoder:(NSCoder *)aCoder {
NSLog(@"encodeWithCoder");
[aCoder encodeObject:self.teacherName forKey:@"teacherName"];
[aCoder encodeObject:@(self.age) forKey:@"age"];
}
歸檔和解檔保存和讀取:
- (IBAction)saveAction:(UIButton *)sender {
Teacher *teacher = [Teacher new];
teacher.teacherName = @"FlyElephant";
teacher.age = 27;
NSString *filePath = [NSHomeDirectory() stringByAppendingPathComponent:@"teacher.archiver"];
BOOL success = [NSKeyedArchiver archiveRootObject:teacher toFile:filePath];
if(success){
NSLog(@"歸檔保存成功");
}
}
- (IBAction)readAction:(UIButton *)sender {
NSString *filePath = [NSHomeDirectory() stringByAppendingPathComponent:@"teacher.archiver"];
Teacher *teacher = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
NSLog(@"Teacher:%@---age:%ld",teacher.teacherName,teacher.age);
}
歸檔保存成功
Teacher:FlyElephant---age:27
屬性如果過多會寫的很煩躁,通過runtime來解決:
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super init];
if (self) {
unsigned int varCount = 0;
Ivar *vars = class_copyIvarList([self class], &varCount);
for (int i = 0; i < varCount; i ++) {
Ivar var = vars[i];
const char *name = ivar_getName(var);
NSString *key = [NSString stringWithUTF8String:name];
id value = [aDecoder decodeObjectForKey:key];
[self setValue:value forKey:key];
}
}
return self;
}
// 歸檔
- (void)encodeWithCoder:(NSCoder *)aCoder{
unsigned int varCount = 0;
Ivar *vars = class_copyIvarList([self class], &varCount);
for (int i = 0; i < varCount; i ++) {
Ivar var = vars[i];
const char *name = ivar_getName(var);
NSString *key = [NSString stringWithUTF8String:name];
id value = [self valueForKey:key];
[aCoder encodeObject:value forKey:key];
}
}
同樣的Runtime可以通過屬性和變量的讀取,實現字典和模型之間的轉換,簡易版比較容易。
參考鏈接:
動態綁定、objc_msgSend、消息轉發機制
Objective-C Runtime 運行時之一:類與對象
Objective-C Runtime Programming Guide
KVO官方文檔
神經病院Objective-C Runtime出院第三天——如何正確使用Runtime
http://yulingtianxia.com/blog/2014/11/05/objective-c-runtime/