作為一名iOS開發(fā)者,runtime是必須要了解滴!
先來給大家講講什么是runtime
runtime到底是個(gè)什么東西呢?
我們都知道Objective-C是 C 語言的擴(kuò)展,并加入了面向?qū)ο筇匦院偷南鬟f機(jī)制。而這個(gè)擴(kuò)展的核心是一個(gè)用 C 和 編譯語言 寫的 runtime 庫(kù)。
所以簡(jiǎn)單說runtime是一個(gè)用C和編譯語言寫的一個(gè)庫(kù)
而且我們都知道 Objective-C 是一門動(dòng)態(tài)語言,它會(huì)將一些工作放在代碼運(yùn)行時(shí)才處理而并非編譯時(shí)。也就是說,有很多類和成員變量在我們編譯的時(shí)是不知道的,而在運(yùn)行時(shí),我們所編寫的代碼會(huì)轉(zhuǎn)換成完整的確定的代碼運(yùn)行。這一切都是因?yàn)?code>runtime庫(kù)的存在
runtime消息傳遞
OC中任何方法的調(diào)用都用到了消息傳遞機(jī)制。
[receiver message];
// 底層運(yùn)行時(shí)會(huì)被編譯器轉(zhuǎn)化為:
objc_msgSend(receiver, selector)
// 如果其還有參數(shù)比如:
[receiver message:(id)arg...];
// 底層運(yùn)行時(shí)會(huì)被編譯器轉(zhuǎn)化為:
objc_msgSend(receiver, selector, arg1, arg2, ...)
如圖我們所看到的編譯器發(fā)送消息的時(shí)候會(huì)攜帶兩個(gè)隱式參數(shù),分別是調(diào)用者本身和方法編號(hào)。為什么要攜帶這兩個(gè)參數(shù)呢?我們看一下runtime的執(zhí)行流程就知道啦~!
舉例
[people run]
1、通過
people的isa指針找到他的class
2、在class的方法列表中找到run方法
3、如果class中沒有找到會(huì)到superclass中繼續(xù)找,直到找到為止
4、找到這個(gè)函數(shù)并實(shí)現(xiàn)
其實(shí)我們最關(guān)心的還是在平時(shí)的開發(fā)中我們要怎么去使用runtime呢?
1、獲取class中所有屬性、成員變量和方法列表
如獲取UITextField所有屬性列表,因?yàn)橛行傩晕覀冊(cè)赨ITextField的類里面是找不到的,我們想要用卻不知道屬性的名稱,那么我們就可以將屬性打印出來,然后根據(jù)需求去使用它們。
Ivar *ivars = class_copyIvarList([UITextField class], &count);
for (int i = 0; i < count; i++) {
Ivar ivar = ivars[i];
NSLog(@"---name=%s",ivar_getName(ivar));
}
//通過KVC修改 textField的placeholder的顏色
[textField setValue:[UIColor greenColor] forKeyPath:@"_placeholderLabel.textColor"];
2、交換方法(主要會(huì)被用來和系統(tǒng)方法進(jìn)行交換)
用法:
先給要替換的方法的類添加一個(gè)Category,然后在Category中的+(void)load方法中實(shí)現(xiàn)方法交換。(由于load方法在程序運(yùn)行時(shí)就會(huì)被加載到內(nèi)存中)
+ (void)load {
Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(cm_objectAtIndex:));
method_exchangeImplementations(fromMethod, toMethod);
}
//實(shí)現(xiàn)要交換的方法
- (id)cm_objectAtIndex:(NSUInteger)index {
}
OK,知道怎么用了,下面我們來說一說實(shí)際的使用場(chǎng)景:
2.1 比如說有一個(gè)項(xiàng)目,已經(jīng)開發(fā)了2年,忽然項(xiàng)目負(fù)責(zé)人添加一個(gè)功能,每次UIImage加載圖片,告訴我是否加載成功。
+ (void)load
{
Method imageNamedMethod = class_getClassMethod(self, @selector(imageNamed:));
Method jyc_imageNamedMethod = class_getClassMethod(self, @selector(jyc_imageNamed:));
method_exchangeImplementations(imageNamedMethod, jyc_imageNamedMethod);
}
// 1.加載圖片
// 2.判斷是否加載成功
+ (UIImage *)jyc_imageNamed:(NSString *)name
{
// 圖片
UIImage *image = [UIImage xmg_imageNamed:name];
if (image) {
NSLog(@"加載成功");
} else {
NSLog(@"加載失敗");
}
return image;
}
2.2
防崩潰處理:數(shù)組越界問題
在這里需要注意的一點(diǎn)是,數(shù)組的classname到底是什么呢?
經(jīng)過研究發(fā)現(xiàn),NSArray只有一個(gè)元素時(shí)其class為__NSSingleObjectArrayI,當(dāng)一個(gè)元素都沒有的時(shí)候其class為__NSArray0,其他情況的class才是__NSArrayI。不確定的小伙伴們可以輸出一下,如下:
NSLog(@"%@",NSStringFromClass([array class]));
因此判斷數(shù)組是否越界,我們需要交換的方法需要寫多個(gè)
+ (void)load {
Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(cm_objectAtIndex:));
method_exchangeImplementations(fromMethod, toMethod);
Method fromMethod1 = class_getInstanceMethod(objc_getClass("__NSSingleObjectArrayI"), @selector(objectAtIndex:));
Method toMethod1 = class_getInstanceMethod(objc_getClass("__NSSingleObjectArrayI"), @selector(cm1_objectAtIndex:));
method_exchangeImplementations(fromMethod1, toMethod1);
Method fromMethod2 = class_getInstanceMethod(objc_getClass("__NSArray0"), @selector(objectAtIndex:));
Method toMethod2 = class_getInstanceMethod(objc_getClass("__NSArray0"), @selector(cm2_objectAtIndex:));
method_exchangeImplementations(fromMethod2, toMethod2);
}
//實(shí)現(xiàn)交換的方法
- (id)cm_objectAtIndex:(NSUInteger)index {
// 判斷下標(biāo)是否越界,如果越界就進(jìn)入異常攔截
if (self.count-1 < index) {
@try {
return [self cm_objectAtIndex:index];
}
@catch (NSException *exception) {
// 在崩潰后會(huì)打印崩潰信息。如果是線上,可以在這里將崩潰信息發(fā)送到服務(wù)器
NSLog(@"---------- %s Crash Because Method %s ----------\n", class_getName(self.class), __func__);
NSLog(@"%@", [exception callStackSymbols]);
return nil;
}
@finally {}
} // 如果沒有問題,則正常進(jìn)行方法調(diào)用
else {
return [self cm_objectAtIndex:index];
}
}
2.3
攔截點(diǎn)擊事件
如,app多個(gè)地方需要先登錄才可以,那么我們可以攔截按鈕的點(diǎn)擊事件,沒有登錄的話我們就去登錄。
上代碼:
static const char *UIControl_isNeedLogin="UIControl_isNeedLogin";
#pragma mark - isNeedLogin
-(void)setIsNeedLogin:(BOOL)isNeedLogin{
objc_setAssociatedObject(self, UIControl_isNeedLogin, @(isNeedLogin), OBJC_ASSOCIATION_ASSIGN);
}
-(BOOL)isNeedLogin{
return [objc_getAssociatedObject(self, UIControl_isNeedLogin) boolValue];
}
+(void)load{
Method method1 = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
Method method2 = class_getInstanceMethod(self, @selector(jyc_sendAction:to:forEvent:));
method_exchangeImplementations(method1, method2);
}
-(void)jyc_sendAction:(SEL)action to:(id)target forEvent:(UIEvent*)event{
if (self.isNeedLogin) {
NSLog(@"哼!先登錄去");
return;
}
[self jyc_sendAction:action to:target forEvent:event];
}
-------------------------------------
//用到的時(shí)候添加一個(gè)屬性就可以了
UIButton* btn = [[UIButton alloc]initWithFrame:CGRectMake(100, 100, 100, 30)];
[btn setTitle:@"點(diǎn)我" forState:UIControlStateNormal];
btn.backgroundColor = UIColor.redColor;
btn.isNeedLogin = YES;
[btn addTarget:self action:@selector(click) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:btn];
當(dāng)然還有一些其他的作用,如按鈕三秒內(nèi)不能重復(fù)點(diǎn)擊、網(wǎng)絡(luò)加載數(shù)據(jù)時(shí)按鈕不能點(diǎn)擊等,我們可以根據(jù)項(xiàng)目的具體情況去應(yīng)用。