本文參考pingpong的iOS runtime 的使用場(chǎng)景--實(shí)戰(zhàn)篇非常感謝該作者
1.背景知識(shí)
1.1 OC的方法調(diào)用流程
下面以實(shí)例對(duì)象調(diào)用方法[blackDog walk]為例描述方法調(diào)用的流程:
1、編譯器會(huì)把`[blackDog walk]`轉(zhuǎn)化為`objc_msgSend(blackDog,SEL)`,SEL為@selector(walk)。
2、Runtime會(huì)在blackDog對(duì)象所對(duì)應(yīng)的Dog類的方法緩存列表里查找方法的SEL
3、如果沒有找到,則在Dog類的方法分發(fā)表查找方法的SEL。(類由對(duì)象isa指針指向,方法分發(fā)表即methodList)
4、如果沒有找到,則在其父類(設(shè)Dog類的父類為Animal類)的方法分發(fā)表里查找方法的SEL(父類由類的superClass指向)
5、如果沒有找到,則沿繼承體系繼續(xù)下去,最終到達(dá)NSObject類。
6、如果在234的其中一步中找到,則定位了方法實(shí)現(xiàn)的入口,執(zhí)行具體實(shí)現(xiàn)
7、如果最后還是沒有找到,會(huì)面臨兩種情況:``(1) 如果是使用`[blackDog walk]`的方式調(diào)用方法````(2) 使用`[blackDog performSelector:@selector(walk)]`的方式調(diào)用方法
1.2 消息轉(zhuǎn)發(fā)流程
1、動(dòng)態(tài)方法解析
接收到未知消息時(shí)(假設(shè)blackDog的walk方法尚未實(shí)現(xiàn)),runtime會(huì)調(diào)用+resolveInstanceMethod:(實(shí)例方法)或者+resolveClassMethod:(類方法)
2、備用接收者
如果以上方法沒有做處理,runtime會(huì)調(diào)用- (id)forwardingTargetForSelector:(SEL)aSelector方法。
如果該方法返回了一個(gè)非nil(也不能是self)的對(duì)象,而且該對(duì)象實(shí)現(xiàn)了這個(gè)方法,那么這個(gè)對(duì)象就成了消息的接收者,消息就被分發(fā)到該對(duì)象。
適用情況:通常在對(duì)象內(nèi)部使用,讓內(nèi)部的另外一個(gè)對(duì)象處理消息,在外面看起來就像是該對(duì)象處理了消息。
比如:blackDog讓女朋友whiteDog來接收這個(gè)消息
3、完整消息轉(zhuǎn)發(fā)
在- (void)forwardInvocation:(NSInvocation *)anInvocation方法中選擇轉(zhuǎn)發(fā)消息的對(duì)象,其中anInvocation對(duì)象封裝了未知消息的所有細(xì)節(jié),并保留調(diào)用結(jié)果發(fā)送到原始調(diào)用者。
比如:blackDog將消息完整轉(zhuǎn)發(fā)給主人dogOwner來處理
2.成員變量和屬性
2.1 json->model
- 原理描述:用runtime提供的函數(shù)遍歷Model自身所有屬性,如果屬性在json中有對(duì)應(yīng)的值,則將其賦值。
- 核心方法:在NSObject的分類中添加方法
- (instancetype)initWithDict:(NSDictionary *)dict {
if (self = [self init]) {
//(1)獲取類的屬性及屬性對(duì)應(yīng)的類型
NSMutableArray * keys = [NSMutableArray array];
NSMutableArray * attributes = [NSMutableArray array];
/*
* 例子
* name = value3 attribute = T@"NSString",C,N,V_value3
* name = value4 attribute = T^i,N,V_value4
*/
unsigned int outCount;
objc_property_t * properties = class_copyPropertyList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
objc_property_t property = properties[i];
//通過property_getName函數(shù)獲得屬性的名字
NSString * propertyName = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
[keys addObject:propertyName];
//通過property_getAttributes函數(shù)可以獲得屬性的名字和@encode編碼
NSString * propertyAttribute = [NSString stringWithCString:property_getAttributes(property) encoding:NSUTF8StringEncoding];
[attributes addObject:propertyAttribute];
}
//立即釋放properties指向的內(nèi)存
free(properties);
//(2)根據(jù)類型給屬性賦值
for (NSString * key in keys) {
if ([dict valueForKey:key] == nil) continue;
[self setValue:[dict valueForKey:key] forKey:key];
}
}
return self;
}
2.2 一鍵序列化
- 原理描述:用runtime提供的函數(shù)遍歷Model自身所有屬性,并對(duì)屬性進(jìn)行encode和decode操作。
- 核心方法:在Model的基類中重寫方法:
- (id)initWithCoder:(NSCoder *)aDecoder {
if (self = [super init]) {
unsigned int outCount;
Ivar * ivars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
Ivar ivar = ivars[i];
NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
[self setValue:[aDecoder decodeObjectForKey:key] forKey:key];
}
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
unsigned int outCount;
Ivar * ivars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
Ivar ivar = ivars[i];
NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
[aCoder encodeObject:[self valueForKey:key] forKey:key];
}
}
2.3 訪問私有變量
我們知道,OC中沒有真正意義上的私有變量和方法,要讓成員變量私有,要放在m文件中聲明,不對(duì)外暴露。如果我們知道這個(gè)成員變量的名稱,可以通過runtime獲取成員變量,再通過getIvar來獲取它的值。方法:
Ivar ivar = class_getInstanceVariable([Model class], "_str1");
NSString * str1 = object_getIvar(model, ivar);
3. 關(guān)聯(lián)對(duì)象
如何給NSArray添加一個(gè)屬性(不能使用繼承)
iOS runtime實(shí)戰(zhàn)應(yīng)用:關(guān)聯(lián)對(duì)象
- 問題
OC的分類允許給分類添加屬性,但不會(huì)自動(dòng)生成getter、setter方法
所以常規(guī)的僅僅添加之后,調(diào)用的話會(huì)crash - runtime如何關(guān)聯(lián)對(duì)象
//關(guān)聯(lián)對(duì)象
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
//獲取關(guān)聯(lián)的對(duì)象
id objc_getAssociatedObject(id object, const void *key)
//移除關(guān)聯(lián)的對(duì)象
void objc_removeAssociatedObjects(id object)
- 應(yīng)用,如何關(guān)聯(lián):
- (void)setCustomTabbar:(UIView *)customTabbar {
//這里使用方法的指針地址作為唯一的key
objc_setAssociatedObject(self, @selector(customTabbar), customTabbar, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (UIView *)customTabbar {
return objc_getAssociatedObject(self, @selector(customTabbar));
}
4.Method Swizzling
- Method Swizzling原理
每個(gè)類都維護(hù)一個(gè)方法(Method)列表,Method則包含SEL和其對(duì)應(yīng)IMP的信息,方法交換做的事情就是把SEL和IMP的對(duì)應(yīng)關(guān)系斷開,并和新的IMP生成對(duì)應(yīng)關(guān)系
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class selfClass = object_getClass([self class]);
SEL oriSEL = @selector(imageNamed:);
Method oriMethod = class_getInstanceMethod(selfClass, oriSEL);
SEL cusSEL = @selector(myImageNamed:);
Method cusMethod = class_getInstanceMethod(selfClass, cusSEL);
BOOL addSucc = class_addMethod(selfClass, oriSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));
if (addSucc) {
class_replaceMethod(selfClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
}else {
method_exchangeImplementations(oriMethod, cusMethod);
}
});
}
- 自己使用過例子:
使用場(chǎng)景,在iOS7中如果viewdidappear還沒有完成,就立刻執(zhí)行push或者pop操作會(huì)crash。 - 解決方案
就是利用method swizzing, 將系統(tǒng)的viewdidappear替換為自己重寫的sofaViewDidAppear
@implementation UIViewController (APSafeTransition)
+ (void)load
{
Method m1;
Method m2;
m1 = class_getInstanceMethod(self, @selector(sofaViewDidAppear:));
m2 = class_getInstanceMethod(self, @selector(viewDidAppear:));
method_exchangeImplementations(m1, m2);
}
5.查看私有成員變量
// 查看私有成員變量
- (void)checkPrivityVariable {
unsigned int count;
Ivar *ivars = class_copyIvarList([UITextField class], &count);
for (int i = 0; i < count; i++) {
// 取出i位置的成員變量
Ivar ivar = ivars[i];
NSLog(@"%s %s", ivar_getName(ivar), ivar_getTypeEncoding(ivar));
}
free(ivars);
}
運(yùn)行結(jié)果
5.1 設(shè)置UITextField占位文字的顏色
// 設(shè)置TextField占位符文字顏色
- (void)setTextFieldPlaceholderTextColor {
UITextField *textF = [[UITextField alloc] initWithFrame:CGRectMake(0, 0, 200, 25)];
textF.layer.borderWidth = 1;
textF.layer.borderColor = [UIColor blackColor].CGColor;
textF.center = CGPointMake(self.view.bounds.size.width * 0.5, 150);
textF.placeholder = @"請(qǐng)輸入用戶名";
[self.view addSubview:textF];
// 法一
// [textF setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor"];
// 法二
// UILabel *placeholderLbe = [textF valueForKeyPath:@"_placeholderLabel"];
// placeholderLbe.textColor = [UIColor redColor];
// 法三
NSMutableDictionary *attrs = [NSMutableDictionary dictionary];
attrs[NSForegroundColorAttributeName] = [UIColor redColor];
textF.attributedPlaceholder = [[NSMutableAttributedString alloc] initWithString:@"請(qǐng)輸入用戶名" attributes:attrs];
}
運(yùn)行結(jié)果
6 字典轉(zhuǎn)模型
// 建立NSObject的一個(gè)分類
#import <Foundation/Foundation.h>
@interface NSObject (Json)
+ (instancetype)cs_objectWithJson:(NSDictionary *)json;
@end
#import "NSObject+Json.h"
#import <objc/runtime.h>
@implementation NSObject (Json)
+ (instancetype)cs_objectWithJson:(NSDictionary *)json {
id obj = [[self alloc] init];
unsigned int count;
Ivar *ivars = class_copyIvarList(self, &count);
for (int i = 0; i < count; i++) {
// 取出位置的成員變量
Ivar ivar = ivars[i];
NSMutableString *name = [NSMutableString stringWithUTF8String:ivar_getName(ivar)];
[name deleteCharactersInRange:NSMakeRange(0, 1)];
// 設(shè)值
id value = json[name];
if ([name isEqualToString:@"ID"]) {
value = json[@"id"];
}
[obj setValue:value forKey:name];
}
free(ivars);
return obj;
}
@end
字典轉(zhuǎn)模型調(diào)用
#import <Foundation/Foundation.h>
@interface CSPersion : NSObject
@property (assign, nonatomic) int ID;
@property (assign, nonatomic) int weight;
@property (assign, nonatomic) int age;
@property (copy, nonatomic) NSString *name;
@end
// 字典轉(zhuǎn)模型
- (void)jsonToModel {
// 字典轉(zhuǎn)模型
NSDictionary *json = @{
@"id" : @20,
@"age" : @20,
@"weight" : @60,
@"name" : @"Jack",
@"no" : @30
};
CSPersion *persion = [CSPersion cs_objectWithJson:json];
NSLog(@"id = %d, age = %d, weight = %d, name = %@",persion.ID,persion.age,persion.weight,persion.name);
}
運(yùn)行結(jié)果
7 替換方法實(shí)現(xiàn)
CSCar.h和CSCar.m文件如下
#import <Foundation/Foundation.h>
@interface CSCar : NSObject
- (void)run;
- (void)test;
@end
#import "CSCar.h"
@implementation CSCar
- (void)run {
NSLog(@"%s", __func__);
}
- (void)test {
NSLog(@"%s", __func__);
}
@end
6.3.1 法一
// 該方法要寫在@@implementation實(shí)現(xiàn)外面
void myRun() {
NSLog(@"---myRun---");
}
// 替換方法的實(shí)現(xiàn)
- (void)replaceMethod {
// 方法一
CSCar *car = [[CSCar alloc] init];
class_replaceMethod([CSCar class], @selector(run), (IMP)myRun, "V");
[car run];
}
運(yùn)行結(jié)果
6.3.2 法二
// 替換方法的實(shí)現(xiàn)
- (void)replaceMethod {
CSCar *car = [[CSCar alloc] init];
// 方法二
class_replaceMethod([CSCar class], @selector(run), imp_implementationWithBlock(^{
NSLog(@"123123");
}), "v");
[car run];
}
運(yùn)行結(jié)果
6.3.3 法三
// 替換方法的實(shí)現(xiàn)
- (void)replaceMethod {
CSCar *car = [[CSCar alloc] init];
// 方法三
Method runMethod = class_getInstanceMethod([CSCar class], @selector(run));
Method testMethod = class_getInstanceMethod([CSCar class], @selector(test));
method_exchangeImplementations(runMethod, testMethod);
[car run];
}
運(yùn)行結(jié)果
7.讓你快速上手一個(gè)項(xiàng)目
對(duì)于一個(gè)大項(xiàng)目而言,最煩惱的就是在眾多界面難以找到對(duì)應(yīng)的viewController,要改個(gè)東西都要花好長(zhǎng)的時(shí)間去找對(duì)應(yīng)的類。
特別是當(dāng)你接手一個(gè)大項(xiàng)目的時(shí)候,對(duì)整體的業(yè)務(wù)邏輯不熟悉,整體的架構(gòu)體系不熟悉,讓你修復(fù)某個(gè)頁(yè)面的BUG,估計(jì)你找這個(gè)頁(yè)面所對(duì)應(yīng)的viewController都要找好久。
思考
能否有一種方式可以快速讓你上手一個(gè)大項(xiàng)目?快速找到某個(gè)頁(yè)面所對(duì)應(yīng)的viewController ?思路
在每一個(gè)頁(yè)面出現(xiàn)的時(shí)候,都打印出哪個(gè)類即將出現(xiàn),如下圖所示
解決方案
- 方案1
整個(gè)項(xiàng)目中建立一個(gè)基類的viewController,然后將項(xiàng)目中所有的viewController都繼承于基類的viewController,然后重寫基類中的viewWillAppear方法。
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
NSString *className = NSStringFromClass([self class]);
NSLog(@"%@ will appear", className);
}
- 方案2
#import "UIViewController+Swizzling.h"
#import @implementation UIViewController (Swizzling)
+ (void)load {
//我們只有在開發(fā)的時(shí)候才需要查看哪個(gè)viewController將出現(xiàn)
//所以在release模式下就沒必要進(jìn)行方法的交換
#ifdef DEBUG
//原本的viewWillAppear方法
Method viewWillAppear = class_getInstanceMethod(self, @selector(viewWillAppear:));
//需要替換成 能夠輸出日志的viewWillAppear
Method logViewWillAppear = class_getInstanceMethod(self, @selector(logViewWillAppear:));
//兩方法進(jìn)行交換
method_exchangeImplementations(viewWillAppear, logViewWillAppear);
#endif
}
- (void)logViewWillAppear:(BOOL)animated {
NSString *className = NSStringFromClass([self class]);
//在這里,你可以進(jìn)行過濾操作,指定哪些viewController需要打印,哪些不需要打印
if ([className hasPrefix:@"UI"] == NO) {
NSLog(@"%@ will appear",className);
}
//下面方法的調(diào)用,其實(shí)是調(diào)用viewWillAppear
[self logViewWillAppear:animated];
}
@end
優(yōu)缺點(diǎn)分析
方案1 適用于一個(gè)新項(xiàng)目,從零開始搭建的項(xiàng)目,建立一個(gè)基類controller,這種編程思想非常可取。但對(duì)于一個(gè)已經(jīng)成型的項(xiàng)目,則方案一行不通,你總不能建議一個(gè)基類,讓后將所有的controller繼承的類都改成基類吧?這工程量太大,太麻煩。
方案2 不論是從零開始搭建的項(xiàng)目,還是已經(jīng)成型的項(xiàng)目,方案2都適用。
更多有關(guān)runtime文章請(qǐng)看下面
iOS-runtime-API詳解+使用
iOS - runtime -詳解
iOS Runtime原理及使用
iOS - runtime如何通過selector找到對(duì)應(yīng)的 IMP地址(分別考慮類方法和實(shí)例方法)
iOS - Runtime之面試題詳解一
iOS-runtime之面試題詳解二