iOS-Runtime-實踐篇

前言

首先, 如果不太了解Runtime的原理的, 可以去我的上一篇文章里先了解了解iOS-Runtime-原理篇

其次, 所有runtime代碼都是基于C的函數(shù), 所以要用到runtime的函數(shù)必須導(dǎo)入

#import <objc/objc-runtime.h> // 模擬器
或者
#import <objc/runtime.h> // 真機
#import <objc/message.h> // 真機

然后, 以下所有代碼都可以在我的Github上面下載, 大家覺得有幫助的希望可以給個star.


目錄

  1. 動態(tài)添加一個類
  2. 打印一個類的所有ivar, property 和 method
  3. 給分類增加屬性
  4. 動態(tài)添加方法實現(xiàn)
  5. 更換方法調(diào)用者
  6. 更改特定方法的實現(xiàn)

1. 動態(tài)添加一個類

就像KVO一樣, 系統(tǒng)是在程序運行的時候根據(jù)你要監(jiān)聽的類, 動態(tài)添加一個新類繼承自該類, 然后重寫原類的setter方法并在里面通知observer的.

那么, 如何動態(tài)添加一個類呢? show code~

// 創(chuàng)建一個類(size_t extraBytes該參數(shù)通常指定為0, 該參數(shù)是分配給類和元類對象尾部的索引ivars的字節(jié)數(shù)隅很。)
Class clazz = objc_allocateClassPair([NSObject class], "GoodPerson", 0);
    
// 添加ivar
// @encode(aType) : 返回該類型的內(nèi)部表示字符串, 如@encode(int) -> i
class_addIvar(clazz, "_name", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *));
    
class_addIvar(clazz, "_age", sizeof(NSUInteger), log2(sizeof(NSUInteger)), @encode(NSUInteger));
    
// 注冊該類
objc_registerClassPair(clazz);
    
// 創(chuàng)建實例對象
id object = [[clazz alloc] init];
    
// 設(shè)置ivar
[object setValue:@"Tracy" forKey:@"name"];
    
Ivar ageIvar = class_getInstanceVariable(clazz, "_age");
object_setIvar(object, ageIvar, @18);
    
// 打印對象的類和內(nèi)存地址
NSLog(@"%@", object);
    
// 打印對象的屬性值
NSLog(@"name = %@, age = %@", [object valueForKey:@"name"], object_getIvar(object, ageIvar));
    
// 當(dāng)類或者它的子類的實例還存在稀颁,則不能調(diào)用objc_disposeClassPair方法
object = nil;
     
// 銷毀類
objc_disposeClassPair(clazz);

運行結(jié)果為
2016-09-04 17:04:08.328 Runtime-實踐篇[13699:1043458] <GoodPerson: 0x1002039b0>
2016-09-04 17:04:08.329 Runtime-實踐篇[13699:1043458] name = Tracy, age = 18

這樣, 我們就在程序運行時動態(tài)添加了一個繼承自NSObject的GoodPerson類, 并為該類添加了name和age成員變量. 這里我們需要注意的是, 添加成員變量的class_addIvar方法必須要在objc_allocateClassPairobjc_registerClassPair之間調(diào)用才行, 這里涉及到OC中類的成員變量的偏移量, 如果在類注冊之后再addIvar的話會破壞原來類成員變量正確的偏移量, 這樣的話會導(dǎo)致你訪問的那個成員變量并不是你想訪問的成員變量, 如圖 :

在類中新增另一個實例變量前后的數(shù)據(jù)布局圖

大家可以試試把class_addIvar方法放在objc_registerClassPair方法之后執(zhí)行, 看看會發(fā)生什么? (用KVC賦值和取值直接報錯, 用getIvar的話取值為null)

2. 打印一個類的所有ivar, property 和 method

這個還是比較簡單的, 應(yīng)該直接看代碼都能看懂

Person *p = [[Person alloc] init];
[p setValue:@"Kobe" forKey:@"name"];
[p setValue:@18 forKey:@"age"];
//    p.address = @"廣州大學(xué)城";
p.weight = 110.0f;
    
// 1.打印所有ivars
unsigned int ivarCount = 0;
// 用一個字典裝ivarName和value
NSMutableDictionary *ivarDict = [NSMutableDictionary dictionary];
Ivar *ivarList = class_copyIvarList([p class], &ivarCount);
for(int i = 0; i < ivarCount; i++){
    NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivarList[i])];
    id value = [p valueForKey:ivarName];    
    if (value) {
        ivarDict[ivarName] = value;
    } else {
        ivarDict[ivarName] = @"值為nil";
    }
}
// 打印ivar
for (NSString *ivarName in ivarDict.allKeys) {
    NSLog(@"ivarName:%@, ivarValue:%@",ivarName, ivarDict[ivarName]);
}
    
// 2.打印所有properties
unsigned int propertyCount = 0;
// 用一個字典裝propertyName和value
NSMutableDictionary *propertyDict = [NSMutableDictionary dictionary];
objc_property_t *propertyList = class_copyPropertyList([p class], &propertyCount);
for(int j = 0; j < propertyCount; j++){
    NSString *propertyName = [NSString stringWithUTF8String:property_getName(propertyList[j])];
    id value = [p valueForKey:propertyName];
        
    if (value) {
        propertyDict[propertyName] = value;
    } else {
        propertyDict[propertyName] = @"值為nil";
    }
}
// 打印property
for (NSString *propertyName in propertyDict.allKeys) {
    NSLog(@"propertyName:%@, propertyValue:%@",propertyName, propertyDict[propertyName]);
}
    
// 3.打印所有methods
unsigned int methodCount = 0;
// 用一個字典裝methodName和arguments
NSMutableDictionary *methodDict = [NSMutableDictionary dictionary];
Method *methodList = class_copyMethodList([p class], &methodCount);
for(int k = 0; k < methodCount; k++) {
    SEL methodSel = method_getName(methodList[k]);
    NSString *methodName = [NSString stringWithUTF8String:sel_getName(methodSel)];
        
unsigned int argumentNums = method_getNumberOfArguments(methodList[k]);
        
methodDict[methodName] = @(argumentNums - 2); // -2的原因是每個方法內(nèi)部都有self 和 selector 兩個參數(shù)
}
// 打印method
for (NSString *methodName in methodDict.allKeys) {
    NSLog(@"methodName:%@, argumentsCount:%@", methodName, methodDict[methodName]);
}

打印結(jié)果為 : 
2016-09-04 17:06:49.070 Runtime-實踐篇[13723:1044813] ivarName:_name, ivarValue:Kobe
2016-09-04 17:06:49.071 Runtime-實踐篇[13723:1044813] ivarName:_age, ivarValue:18
2016-09-04 17:06:49.071 Runtime-實踐篇[13723:1044813] ivarName:_weight, ivarValue:110
2016-09-04 17:06:49.072 Runtime-實踐篇[13723:1044813] ivarName:_address, ivarValue:值為nil
2016-09-04 17:06:49.072 Runtime-實踐篇[13723:1044813] propertyName:address, propertyValue:值為nil
2016-09-04 17:06:49.072 Runtime-實踐篇[13723:1044813] propertyName:weight, propertyValue:110
2016-09-04 17:06:49.073 Runtime-實踐篇[13723:1044813] methodName:setWeight:, argumentsCount:1
2016-09-04 17:06:49.073 Runtime-實踐篇[13723:1044813] methodName:weight, argumentsCount:0
2016-09-04 17:06:49.074 Runtime-實踐篇[13723:1044813] methodName:setAddress:, argumentsCount:1
2016-09-04 17:06:49.074 Runtime-實踐篇[13723:1044813] methodName:address, argumentsCount:0
2016-09-04 17:06:49.074 Runtime-實踐篇[13723:1044813] methodName:.cxx_destruct, argumentsCount:0

前面2節(jié)主要是熟悉runtime的函數(shù)調(diào)用, 畢竟有許多函數(shù)前綴objc, class, object等等. 其實這里面也有規(guī)律 :

  • objc_ : 高于類的操作, 例如添加類, 注冊類, 銷毀類還有許多高于一個類本身的操作一般都是objc開頭
  • class : 對類的內(nèi)部進行修改的, 例如添加ivar, 添加property, 添加method等等
  • object : 對某個對象進行修改, 例如設(shè)置ivar值, 獲取ivar值, 設(shè)置property值, 獲取property值, 調(diào)用某個method等等
  • ivar, property, method : 這三個方法大家可以手動去敲敲看一看

3. 給分類增加屬性

在分類只能對原類擴充方法, 并不能擴充屬性, 你可以創(chuàng)建一個分類, 然后在分類中敲幾個@property, 然后用第二節(jié)的方法打印下原類的property看看存不存在? 答案顯然是不存在這個屬性.

那么我們可以使用runtime中的一個叫關(guān)聯(lián)對象的辦法, 給分類添加一個property, 并且打印原類的property列表是真真切切存在的. 上代碼

// Person+RunningMan.h
@interface Person (RunningMan)

/** 速度(km/h) */
@property (nonatomic, assign) CGFloat speed;

@end

// Person+RunningMan.m
#import <objc/objc-runtime.h>

@implementation Person (RunningMan)

- (CGFloat)speed
{
    id value = objc_getAssociatedObject(self, _cmd);
    return [value doubleValue];
}

- (void)setSpeed:(CGFloat)speed {
    objc_setAssociatedObject(self, @selector(speed), @(speed), OBJC_ASSOCIATION_ASSIGN);
}

@end

好的, 我們看看加了這個分類之后再利用第二節(jié)的辦法打印下瞧瞧~

2016-09-04 17:26:00.403 Runtime-實踐篇[13795:1050331] ivarName:_name, ivarValue:Kobe
2016-09-04 17:26:00.404 Runtime-實踐篇[13795:1050331] ivarName:_age, ivarValue:18
2016-09-04 17:26:00.405 Runtime-實踐篇[13795:1050331] ivarName:_weight, ivarValue:110
2016-09-04 17:26:00.405 Runtime-實踐篇[13795:1050331] ivarName:_address, ivarValue:值為nil
2016-09-04 17:26:00.405 Runtime-實踐篇[13795:1050331] propertyName:speed, propertyValue:0
2016-09-04 17:26:00.405 Runtime-實踐篇[13795:1050331] propertyName:address, propertyValue:值為nil
2016-09-04 17:26:00.405 Runtime-實踐篇[13795:1050331] propertyName:weight, propertyValue:110
2016-09-04 17:26:00.405 Runtime-實踐篇[13795:1050331] methodName:speed, argumentsCount:0
2016-09-04 17:26:00.406 Runtime-實踐篇[13795:1050331] methodName:setWeight:, argumentsCount:1
2016-09-04 17:26:00.406 Runtime-實踐篇[13795:1050331] methodName:setSpeed:, argumentsCount:1
2016-09-04 17:26:00.406 Runtime-實踐篇[13795:1050331] methodName:weight, argumentsCount:0
2016-09-04 17:26:00.446 Runtime-實踐篇[13795:1050331] methodName:setAddress:, argumentsCount:1
2016-09-04 17:26:00.447 Runtime-實踐篇[13795:1050331] methodName:address, argumentsCount:0
2016-09-04 17:26:00.447 Runtime-實踐篇[13795:1050331] methodName:.cxx_destruct, argumentsCount:0

看到了嘛? speed這個屬性乖乖的在那兒呢.

其實關(guān)聯(lián)對象這個技術(shù)就是用哈希表實現(xiàn)的, 將一個類映射到一張哈希表上, 然后根據(jù)key找到關(guān)聯(lián)對象, 所以嚴(yán)格說, 關(guān)聯(lián)對象跟本類沒有任何聯(lián)系, 它不是儲存在類的內(nèi)部的. 它的底層原理就不多介紹了, 不屬于本文的范疇, 大家感興趣的可以到以下兩篇文章里面看看
Associated Objects
Objective-C Associated Objects 的實現(xiàn)原理

4. 動態(tài)添加方法實現(xiàn)

好了, 繞來繞去又回到了runtime強大的消息轉(zhuǎn)發(fā)身上了, 當(dāng)一個方法沒有實現(xiàn)的時候, OC會怎么做的呢? 還記得那四個步驟嗎, 不記得也沒關(guān)系, 我們看代碼!

/*
    Person類只有- (void)noIMPMethod方法的聲明, 
    沒有他的實現(xiàn), 一般來說程序運行, 調(diào)用noIMPMethod這個方法, 肯定要報錯的,
    我們可以在這個方法里動態(tài)添加該方法的實現(xiàn)
*/
    
// 用來實現(xiàn)noIMPMethod方法實現(xiàn)的函數(shù)
void otherFunction(id self, SEL cmd)
{
    NSLog(@"動態(tài)處理了noIMPMethod方法的實現(xiàn)");
}

// 第一步, 對象在收到無法解讀的消息后, 首先調(diào)用其所屬類的這個類方法
// 返回YES則結(jié)束消息轉(zhuǎn)發(fā), 返回NO則進入下一步
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    // 如果是noIMPMethod方法
    if([NSStringFromSelector(sel) isEqualToString:@"noIMPMethod"]){
    // 動態(tài)添加方法實現(xiàn)
    class_addMethod([self class], sel, (IMP)otherFunction, "v@:");
        return YES;
    } else {
        return [super resolveInstanceMethod:sel];
    }
}

程序運行結(jié)果 : 
2016-09-04 17:38:24.301 Runtime-實踐篇[13856:1054351] 動態(tài)處理了noIMPMethod方法的實現(xiàn)

代碼應(yīng)該也很明了, 當(dāng)判斷到無法解讀的SEL后, 可以給該SEL動態(tài)添加方法的實現(xiàn).

NOTE:
消息轉(zhuǎn)發(fā)的另外3個方法會在下文放上, 因為本例子用不上所以就不放上來了

5. 更換方法調(diào)用者

試想一下, 一個腿部殘疾的人, 他想跑, runtime知道他自己跑不了, 于是就讓他的狗替代他去跑了(person沒有run方法的聲明和實現(xiàn), dog有run方法的聲明和實現(xiàn))

// 第一步, 對象在收到無法解讀的消息后, 首先調(diào)用其所屬類的這個類方法
// 返回YES則結(jié)束消息轉(zhuǎn)發(fā), 返回NO則進入下一步
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    return NO;
}

// 第二步, 動態(tài)方法解析失敗, 則調(diào)用這個方法
// 返回的對象將處理該selector, 返回nil則進入下一步
- (id)forwardingTargetForSelector:(SEL)aSelector
{
    return nil;
}

// 第三步, 在這里返回方法的消息簽名
// 返回YES則進入下一步, 返回nil則結(jié)束消息轉(zhuǎn)發(fā)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    if ([NSStringFromSelector(aSelector) isEqualToString:@"run"]){
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    } else {
        return [super methodSignatureForSelector:aSelector];
    }
}

// 第四步, 最后一次處理該消息的機會
// 這里處理不了這個invocation就會結(jié)束消息轉(zhuǎn)發(fā)
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    // 在這我們修改調(diào)用該方法的對象
    Dog *dog = [[Dog alloc] init];
    // 讓dog去調(diào)用該方法
    [anInvocation invokeWithTarget:dog];
}

那么我們通過((void(*)(id, SEL))objc_msgSend)((id)p, @selector(run)); // 這里強轉(zhuǎn)是為了不讓編譯器報參數(shù)過多的錯誤方法調(diào)用person的run方法, 得到的輸出為 :

2016-09-04 17:49:42.634 Runtime-實踐篇[13939:1059419] 是狗在跑步

同樣, 其實可以在第二部就把這件事做了, 只需返回dog實例即可, 大家可以親手操作試試

6. 更改特定方法的實現(xiàn)

一條狗在吃著骨頭, 然后他的主人一把把一個球扔得遠遠的, 礙于主人的淫威之下, 狗就不得不停下來跑去撿球了(更改[dog eat]方法的實現(xiàn)為[dog run])

// 第一步, 對象在收到無法解讀的消息后, 首先調(diào)用其所屬類的這個類方法
// 返回YES則結(jié)束消息轉(zhuǎn)發(fā), 返回NO則進入下一步
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    return NO;
}
    
// 第二步, 動態(tài)方法解析失敗, 則調(diào)用這個方法
// 返回的對象將處理該selector, 返回nil則進入下一步
- (id)forwardingTargetForSelector:(SEL)aSelector
{
    return nil;
}

// 第三步, 在這里返回方法的消息簽名
// 返回YES則進入下一步, 返回nil則結(jié)束消息轉(zhuǎn)發(fā)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    if ([NSStringFromSelector(aSelector) isEqualToString:@"eat"]){
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    } else {
        return [super methodSignatureForSelector:aSelector];
    }
}

// 第四步, 最后一次處理該消息的機會
// 這里處理不了這個invocation就會結(jié)束消息轉(zhuǎn)發(fā)
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    // 在這我們修改選擇子為run
    [anInvocation setSelector:@selector(run)];
    // 讓dog去調(diào)用該方法
    [anInvocation invokeWithTarget:self];
}

((void(*)(id, SEL))objc_msgSend)((id)dog, @selector(eat));的輸出結(jié)果為 :

2016-09-04 17:56:53.238 Runtime-實踐篇[14037:1063170] 是狗在跑步

大部分demo還是比較簡單, 只要看了, 親手敲敲代碼都能掌握, 并不存在什么技術(shù)含量, 難的是在真實項目中出現(xiàn)這種需求的時候能夠在腦子里喚醒這部分知識, 并靈活運用其中. 共勉吧, 程序員大兄弟們!

另外, demo在這里Github, 不要忘了給star哦~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市寨躁,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌咒精,老刑警劉巖畦贸,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異扑浸,居然都是意外死亡,警方通過查閱死者的電腦和手機燕偶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進店門首装,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人杭跪,你說我怎么就攤上這事仙逻。” “怎么了涧尿?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵系奉,是天一觀的道長。 經(jīng)常有香客問我姑廉,道長缺亮,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任桥言,我火速辦了婚禮萌踱,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘号阿。我一直安慰自己并鸵,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布扔涧。 她就那樣靜靜地躺著园担,像睡著了一般。 火紅的嫁衣襯著肌膚如雪枯夜。 梳的紋絲不亂的頭發(fā)上弯汰,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天,我揣著相機與錄音湖雹,去河邊找鬼咏闪。 笑死,一個胖子當(dāng)著我的面吹牛摔吏,可吹牛的內(nèi)容都是我干的鸽嫂。 我是一名探鬼主播织鲸,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼溪胶!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起稳诚,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤哗脖,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后扳还,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體才避,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年氨距,在試婚紗的時候發(fā)現(xiàn)自己被綠了桑逝。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡俏让,死狀恐怖楞遏,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情首昔,我是刑警寧澤寡喝,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站勒奇,受9級特大地震影響预鬓,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜赊颠,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一格二、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧竣蹦,春花似錦顶猜、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至远寸,卻和暖如春抄淑,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背驰后。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工肆资, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人灶芝。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓郑原,卻偏偏與公主長得像唉韭,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子犯犁,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,925評論 2 344

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

  • 對于從事 iOS 開發(fā)人員來說属愤,所有的人都會答出【runtime 是運行時】什么情況下用runtime?大部分人能...
    夢夜繁星閱讀 3,697評論 7 64
  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,682評論 0 9
  • 這篇文章完全是基于南峰子老師博客的轉(zhuǎn)載 這篇文章完全是基于南峰子老師博客的轉(zhuǎn)載 這篇文章完全是基于南峰子老師博客的...
    西木閱讀 30,544評論 33 466
  • 本文轉(zhuǎn)載自:http://yulingtianxia.com/blog/2014/11/05/objective-...
    ant_flex閱讀 748評論 0 1
  • 測試
    xzyangg閱讀 121評論 2 0