Runtime介紹:
runtime顧名思義就是運行時,其實我們的App從你按下command+R開始一直到App運行起來經(jīng)歷了大致兩個階段,1:編譯時,2:運行時。還記得一道很經(jīng)典的面試題

這里給大家解釋下:首先, * testObject 是告訴編譯器,testObject是一個指向某個Objective-C對象的指針。因為不管指向的是什么類型的對象,一個指針?biāo)嫉膬?nèi)存空間都是固定的,所以這里聲明成任何類型的對象,最終生成的可執(zhí)行代碼都是沒有區(qū)別的。這里限定了NSString只不過是告訴編譯器,請把testObject當(dāng)做一個NSString來檢查,如果后面調(diào)用了非NSString的方法,會產(chǎn)生警告。接著,你創(chuàng)建了一個NSData對象,然后把這個對象所在的內(nèi)存地址保存在testObject里。那么運行時(從這段代碼執(zhí)行開始,到程序結(jié)束),testObject指向的內(nèi)存空間就是一個NSData對象。你可以把testObject當(dāng)做一個NSData對象來用。 所以編譯時是NSString,運行時是NSData。
runtime是什么:
在runtime中,所有的類在OC中都會被定義成一個結(jié)構(gòu)體,像這樣
類在runtime中的表示
struct objc_class {
Class isa;//指針,顧名思義,表示是一個什么, //實例的isa指向類對象,類對象的isa指向元類
#if !__OBJC2__
Class super_class; //指向父類
const char *name; //類名
long version; //類的版本信息,默認(rèn)初始化為 0。我們可以在運行期對其進(jìn)行修改(class_setVersion)或獲?。╟lass_getVersion)。
long info; /*供運行期使用的一些位標(biāo)識。有如下一些位掩碼:
CLS_CLASS (0x1L) 表示該類為普通 class ,其中包含實例方法和變量;
CLS_META (0x2L) 表示該類為 metaclass,其中包含類方法;
CLS_INITIALIZED (0x4L) 表示該類已經(jīng)被運行期初始化了,這個標(biāo)識位只被 objc_addClass 所設(shè)置;
CLS_POSING (0x8L) 表示該類被 pose 成其他的類;(poseclass 在ObjC 2.0中被廢棄了);
CLS_MAPPED (0x10L) 為ObjC運行期所使用
CLS_FLUSH_CACHE (0x20L) 為ObjC運行期所使用
CLS_GROW_CACHE (0x40L) 為ObjC運行期所使用
CLS_NEED_BIND (0x80L) 為ObjC運行期所使用
CLS_METHOD_ARRAY (0x100L) 該標(biāo)志位指示 methodlists 是指向一個 objc_method_list 還是一個包含 objc_method_list 指針的數(shù)組;*/
long instance_size //該類的實例變量大小(包括從父類繼承下來的實例變量);
struct objc_ivar_list *ivars //成員變量列表
struct objc_method_list **methodLists; //方法列表
struct objc_cache *cache;//緩存 一種優(yōu)化,調(diào)用過的方法存入緩存列表,下次調(diào)用先找緩存
struct objc_protocol_list *protocols //協(xié)議列表
#endif
} OBJC2_UNAVAILABLE;
相關(guān)的定義
/// 描述類中的一個方法
typedef struct objc_method *Method;
/// 實例變量
typedef struct objc_ivar *Ivar;
/// 類別Category
typedef struct objc_category *Category;
/// 類中聲明的屬性
typedef struct objc_property *objc_property_t;
ObjC 為每個類的定義生成兩個 objc_class ,一個即普通的 class,另一個即 metaclass。我們可以在運行期創(chuàng)建這兩個 objc_class 數(shù)據(jù)結(jié)構(gòu),然后使用 objc_addClass 動態(tài)地創(chuàng)建新的類定義。
runtime能干什么:
- :1:獲取一個類中的列表比如方法列表、屬性列表、協(xié)議列表、成員變量列表像如下這樣 其中獲取到的屬性、方法都是可以獲取public和private的。
unsigned int count;
Class clas = [WKWebViewController class]; //是我自己的類,之所以不用系統(tǒng)的類是因為系統(tǒng)的類方法屬性太多了
objc_property_t * propertyList = class_copyPropertyList(clas, &count);
for (int i = 0; i < count; i++) {
const char *propertyName = property_getName(propertyList[i]);
NSLog(@" %@ 屬性(包括私有) -------->>>>> %@",clas,[NSString stringWithUTF8String:propertyName]);
}
NSLog(@"-------------------------------------------------------------------------------------------------------------- ");
Method * methodList = class_copyMethodList(clas, &count);
for (int i = 0; i < count; i++) {
Method methodName = methodList[i];
NSLog(@" %@ 方法(包括私有) -------->>>>> %@",clas,NSStringFromSelector(method_getName(methodName)));
}
NSLog(@"-------------------------------------------------------------------------------------------------------------- ");
Ivar *ivarList = class_copyIvarList(clas, &count);
for (int i = 0; i<count; i++) {
Ivar myIvar = ivarList[i];
const char *ivarName = ivar_getName(myIvar);
NSLog(@"%@ 成員變量(包括私有) -------->>>>> %@",clas, [NSString stringWithUTF8String:ivarName]);
}
NSLog(@"-------------------------------------------------------------------------------------------------------------- ");
//獲取協(xié)議列表
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
for (int i = 0; i<count; i++) {
Protocol *myProtocal = protocolList[i];
const char *protocolName = protocol_getName(myProtocal);
NSLog(@"%@ 協(xié)議 -------->>>>> %@",clas, [NSString stringWithUTF8String:protocolName]);
}
輸出后的結(jié)果是

其中也包括了私有方法。
-
2:攔截方法調(diào)用
有的時候我們用一個類或者一個實例變量去調(diào)用一個方法,由于操作失誤或者是其他原因,導(dǎo)致這個所被調(diào)用的方法并不存在,報出這樣的錯誤,然后閃退!
image.png
這個時候如果我們想避免這些崩潰,我們就需要在運行時對其做一些手腳。iOS中方法調(diào)用的流程:其實調(diào)用方法就是發(fā)送消息,所有調(diào)用方法的代碼例如 [obj aaa] 在運行時runtime會將這段代碼轉(zhuǎn)換為objc_msgSend(obj, [@selector]);(本質(zhì)就是發(fā)送消息)然后obj會通過其中isa指針去該類的緩存中(cache)查找對應(yīng)函數(shù)的Method, 如果沒有找到,再去該類的方法列表(methodList)中查找,如果沒有找到再去該類的父類找,如果找到了,就先將方法添加到緩存中,以便下次查找,然后通過method中的指針定位到指定方法執(zhí)行。如果一直沒有找到,便會走完如下四個方法之后崩潰。
/**
如果調(diào)用的是不存在的實例方法則會在奔潰前進(jìn)入該方法,防止崩潰可以在此處做處理
*/
+(BOOL)resolveInstanceMethod:(SEL)sel {
return YES;
}
/**
如果調(diào)用的是不存在的類方法則會在奔潰前進(jìn)入該方法,防止崩潰可以在此處做處理
*/
+(BOOL)resolveClassMethod:(SEL)sel {
return YES;
}
/**
這個方法會把你所調(diào)用的不存在的方法重定向到一個聲明了該方法的類中,只需要你返回一個有該方法的
類就可以,如果你重定向的這個類仍然不具有該方法那么會繼續(xù)崩潰
*/
-(id)forwardingTargetForSelector:(SEL)aSelector {
}
/**
將你不存在的方法打包成NSInvocation對象,做完你自己的處理之后
調(diào)用invokeWithTarget讓某個target來處理該方法
*/
-(void)forwardInvocation:(NSInvocation *)anInvocation {
[anInvocation invokeWithTarget:self];
}
3:動態(tài)添加方法
因為我們調(diào)用了一個不存在的方法導(dǎo)致崩潰,那么我們在判斷出不存在后就動態(tài)添加上一個方法吧 這樣不就不會蹦了嗎?我們先寫一個方法用來給我們做出提示
- (void) errorMethod {
NSLog(@"no method!!!!!!!");
}
如果調(diào)用了沒有的方法,那么就把這個方法添加進(jìn)去,然后把被調(diào)用的方法的指針指向這個error1:,那么一旦調(diào)用了沒有的方法就會走這個。我們來看代碼
+(BOOL)resolveInstanceMethod:(SEL)sel {
Method errorMethod = class_getInstanceMethod([self class], @selector(errorMethod));
if ([NSStringFromSelector(sel) isEqualToString:@"testMethod"]) {
BOOL isAdd = class_addMethod([self class], sel, method_getImplementation(errorMethod), method_getTypeEncoding(errorMethod));
NSLog(@"tinajia = %d",isAdd);
}
//Do something
return YES;
}
主要用到
/**
添加方法
@param class] 在哪個類里添加
@param sel 添加的方法的名字
@param errorMethod 添加的方法的實現(xiàn)IMP指
@param types 方法的標(biāo)示符
@return 是否添加成功
*/
BOOL isAdd = class_addMethod([self class], sel, method_getImplementation(errorMethod), method_getTypeEncoding(errorMethod));
然后運行下:
WKWebViewController * vc= [[WKWebViewController alloc] init];
[vc performSelector:@selector(testMethod)];
我調(diào)用了并不存在的testMethod方法并沒有崩潰并且方法成功添加了

-
4:動態(tài)交換方法(也叫iOS黑魔法,慎用)
沒什么好例子,用一個網(wǎng)上說的例子(引用別人的東西,懶得復(fù)制了,就接了圖)
其實本質(zhì)即使SEL和IMP的交換,原理是這樣的:在iOS中每一個類中都有一個叫dispatch table的東西,里面存放在SEL 和他所對應(yīng)的IMP指針,之前也說過方法調(diào)用就是通過sel找IMP指針然后指針定位調(diào)用方法。方法交換就是對這個dispatch table進(jìn)行操作。讓A的SEL去對應(yīng)B的IMP,B的SEL對應(yīng)A的IMP,如圖這樣就達(dá)到方法交換的目的,下面看代碼:
+ (void)changeMethod {
// 如果是類方法 要使用 !
// 如果是系統(tǒng)的集合類的屬性要用元類 比如 __NSSetM = NSMutableSet
// Class class = NSClassFromString(@"__NSSetM");
// Class metaClass = objc_getMetaClass([NSStringFromClass(class) UTF8String]);
Class systemClass = NSClassFromString(__NSSetM);
SEL sel_System = NSSelectorFromString(addObject:);
SEL sel_Custom = @selector(swizzle_addObject:);
Method method_System = class_getInstanceMethod(systemClass, sel_System);
Method method_Custom = class_getInstanceMethod([self class], sel_Custom);
IMP imp_System = method_getImplementation(method_System);
IMP imp_Custom = method_getImplementation(method_Custom);
method_exchangeImplementations(method_System, method_Custom);
}
- (void)swizzle_addObject:(id) obj {
if (obj) {
[self swizzle_addObject:obj];
}
}
主要代碼 method_exchangeImplementations(method1, method2); 這兩個參數(shù)很簡單,就是兩個需要交換的方法。
最后我調(diào)用了m1但是實際上走了m2。
動態(tài)交換方法的原理以及交換過程中指針的變化
在通常的方法交換中我們通常有兩種情景,一種是我會針對被交換的類建一個category,然后hook的方法會寫在category中。另一種是自己創(chuàng)建一個Tool類里面放些常用的工具方法其中包含了方法交換。可能大家普遍選擇第一種方法,但是如果你需要hook的類非常多的(我實際項目中就遇到這樣的問題)那你就需要針對不同的類創(chuàng)建category,就會導(dǎo)致文件過多,且每一個文件中只有一個hook方法,這樣一來左側(cè)一堆文件,所以我用了第二種方法,但是在使用過程中出現(xiàn)一個問題,先看下我的代碼機構(gòu)

我要hook的是ViewController中的viewDidLoad方法,我建立了兩個類一個是ViewController的category,另一個是Tool類,為了一會區(qū)別演示不同類hook的不同(兩個類中hook的代碼完全一樣)
-
ViewController中將要被替換的系統(tǒng)方法
被替換的方法(系統(tǒng)方法) -
Category中將要用來替換的自定義方法
用來替換的方法(自定義方法) -
然后在ViewController中的load中做方法替換
進(jìn)行方法替換
運行一下的輸出結(jié)果想必大家已經(jīng)猜到了先執(zhí)行custom再執(zhí)行system,這是通常情況下大家的做法。
結(jié)果
下面再來看下如果我將替換方法寫在不同類中會怎樣,調(diào)用Tool中的交換方法
執(zhí)行Tool中的交換方法
然后直接看結(jié)果了,因為代碼都是一模一樣的我直接復(fù)制過去的
結(jié)果
發(fā)生了crash,原因是ViewController中沒有swizzel_viewDidLoad_custom這個方法,為什么不同類的交換會出現(xiàn)這種問題,我們用個圖來說明下
image.png
解決的辦法是我們在交換方法之前要先像其中添加方法,也就是說把customMethod添加到SystemClass中,但是注意要把customMethod的實現(xiàn)指向syetemMethod的實現(xiàn)。這樣一來就可以達(dá)到SystemClass調(diào)用customMethod卻執(zhí)行systemMethod的代碼的效果,實現(xiàn)以上要求我們需要在交換之前執(zhí)行這個方法。
class_addMethod(systemClass, sel_Custom, imp_System, method_getTypeEncoding(method_System))
其中第一個參數(shù)是需要往哪個類添加;第二個參數(shù)是要添加的方法的方法名;第三個參數(shù)是所添加的方法的方法實現(xiàn),第四個是方法的標(biāo)識符。經(jīng)過就該之后我們的代碼是這樣
.
.
之前的都一樣就省略
.
.
if (class_addMethod(systemClass, sel_Custom, imp_System, method_getTypeEncoding(method_System))) {
class_replaceMethod(systemClass, sel_System, imp_Custom, method_getTypeEncoding(method_System));
} else {
method_exchangeImplementations(method_System, method_Custom);
}
我們來看下執(zhí)行完add操作之后此時的方法和類的對應(yīng)關(guān)系(紅色的為add的修改)

因為SystemClass中本身不包含customMethod所以add一定是成功的,也就是說會進(jìn)入判斷執(zhí)行replace方法。
class_replaceMethod(systemClass, sel_System, imp_Custom, method_getTypeEncoding(method_System));
第一個參數(shù):需要修改的方法的所在的類;第二個參數(shù):需要替換其實現(xiàn)的方法名;第三個參數(shù):需要把哪個實現(xiàn)替換給他;第四個參數(shù):方法標(biāo)識符。此時看下我們做完replace之后的類與方法名以及他們實現(xiàn)的關(guān)系(紅色的為replace的修改)。

此時大家已經(jīng)看出來了,雖然沒有執(zhí)行exchange方法,但是我已經(jīng)達(dá)到了方法交換的目的。系統(tǒng)執(zhí)行systemMethod時候會走customMethod的實現(xiàn)但是因為在customMethod方法中我會遞歸執(zhí)行[self customMethod],所以又會走到systemMethod的實現(xiàn),因為之前進(jìn)行了方法添加,所以此時A類中有了customMethod方法,不會再發(fā)生之前的crash。達(dá)到一個不同類進(jìn)行Method Swizzling的目的。
綜上來看一個完整嚴(yán)謹(jǐn)?shù)腗ethodSwizzling應(yīng)該在交換前先add,并且add方法的參數(shù)不能錯
+ (void)changeMethod {
Class systemClass = NSClassFromString(@"你的類");
SEL sel_System = @selector(系統(tǒng)方法);
SEL sel_Custom = @selector(你自己的方法);
Method method_System = class_getInstanceMethod(systemClass, sel_System);
Method method_Custom = class_getInstanceMethod([self class], sel_Custom);
IMP imp_System = method_getImplementation(method_System);
IMP imp_Custom = method_getImplementation(method_Custom);
if (class_addMethod(systemClass, sel_Custom, imp_System, method_getTypeEncoding(method_System))) {
class_replaceMethod(systemClass, sel_System, imp_Custom, method_getTypeEncoding(method_System));
} else {
method_exchangeImplementations(method_System, method_Custom);
}
}









