iOS runtime的使用場(chǎng)景-實(shí)戰(zhàn)篇

173709-2be9edadc94d5cbb.png
本文參考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來處理
173709-7bcc4302c515f1e0.png
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é)果

image.png
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é)果

image.png
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é)果

image.png
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é)果

image.png
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é)果

image.png
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é)果

image.png
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都適用。


項(xiàng)目連接地址


更多有關(guān)runtime文章請(qǐng)看下面
iOS-runtime-API詳解+使用
iOS - runtime -詳解
iOS Runtime原理及使用
iOS - runtime如何通過selector找到對(duì)應(yīng)的 IMP地址(分別考慮類方法和實(shí)例方法)
iOS - Runtime之面試題詳解一
iOS-runtime之面試題詳解二

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,321評(píng)論 6 543
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,559評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,442評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,835評(píng)論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,581評(píng)論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,922評(píng)論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,931評(píng)論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,096評(píng)論 0 290
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,639評(píng)論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,374評(píng)論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,591評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,104評(píng)論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,789評(píng)論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,196評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,524評(píng)論 1 295
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,322評(píng)論 3 400
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,554評(píng)論 2 379

推薦閱讀更多精彩內(nèi)容

  • runtime的文檔網(wǎng)上多的一塌糊涂,細(xì)節(jié)原理就不講了這里只是簡(jiǎn)單整理下,平時(shí)可能會(huì)用到的一些場(chǎng)景 申明:大部分代...
    pingpong_龘閱讀 7,728評(píng)論 1 26
  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,755評(píng)論 0 9
  • 對(duì)于從事 iOS 開發(fā)人員來說,所有的人都會(huì)答出【runtime 是運(yùn)行時(shí)】什么情況下用runtime?大部分人能...
    夢(mèng)夜繁星閱讀 3,732評(píng)論 7 64
  • 從來都抱有期待,期待明天如我所想,期待我愛你你也愛我,期待父母健康,期待所有的人正直可愛明辨是非,每一次期待落空,...
    周末保持距離閱讀 229評(píng)論 0 0
  • 《查理九世》《哈利波特》《大腦門一旦》
    愛蓮說Alice閱讀 236評(píng)論 0 1