1. 程式人生 > >runtime從入門到精通(六)—— runtime在實際開發中的應用

runtime從入門到精通(六)—— runtime在實際開發中的應用

上一篇文章,我們學習了runtime的訊息傳送和訊息轉發機制(檢視連結: runtime從入門到精通(五)—— 訊息傳送和訊息轉發 ),倒到此為止,有關runtime的理論知識介紹就先告於段落,小夥伴們,真正的乾貨來了,runtime在實際的開發中到底有何牛X的作用?我們該怎麼使用這麼牛X的工具呢?

想使用runtime,首先在寫執行時程式碼之前,要先加上標頭檔案:

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

一、動態新增一個類

(“KVO”的實現是利用了runtime能夠動態新增類)

原來當你對一個物件進行觀察時, 系統會自動新建一個類繼承自原類, 然後重寫被觀察屬性的setter方法. 然後重寫的setter方法會負責在呼叫原setter方法前後通知觀察者. 然後把原物件的isa指標指向這個新類, 我們知道, 物件是通過isa指標去查詢自己是屬於哪個類, 並去所在類的方法列表中查詢方法的, 所以這個時候這個物件就自然地變成了新類的例項物件.

就像KVO一樣, 系統是在程式執行的時候根據你要監聽的類, 動態新增一個新類繼承自該類, 然後重寫原類的setter方法並在裡面通知observer的.

那麼, 如何動態新增一個類呢? 直接上程式碼:

// 建立一個類(size_t extraBytes該引數通常指定為0, 該引數是分配給類和元類物件尾部的索引ivars的位元組數。)
Class clazz = objc_allocateClassPair([NSObject class], "GoodPerson", 0);

// 新增ivar
// @encode(aType) : 返回該型別的C字串
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); // 建立例項物件 id object = [[clazz alloc] init]; // 設定ivar [object setValue:@"Tracy" forKey:@"name"]; Ivar ageIvar = class_getInstanceVariable(clazz, "_age"); object_setIvar(object, ageIvar, @18); // 列印物件的類和記憶體地址 NSLog(@"%@", object); // 列印物件的屬性值 NSLog(@"name = %@, age = %@", [object valueForKey:@"name"], object_getIvar(object, ageIvar)); // 當類或者它的子類的例項還存在,則不能呼叫objc_disposeClassPair方法 object = nil; // 銷燬類 objc_disposeClassPair(clazz);

執行結果為:

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

這樣, 我們就在程式執行時動態添加了一個繼承自NSObject的GoodPerson類, 併為該類添加了name和age成員變數.

二、通過runtime獲取一個類的所有屬性,我們可以做些什麼?

1. 列印一個類的所有ivar, property 和 method(簡單直接的使用)

Person *p = [[Person alloc] init];
[p setValue:@"Kobe" forKey:@"name"];
[p setValue:@18 forKey:@"age"];
//    p.address = @"廣州大學城";
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的原因是每個方法內部都有self 和 selector 兩個引數
}
// 列印method
for (NSString *methodName in methodDict.allKeys) {
    NSLog(@"methodName:%@, argumentsCount:%@", methodName, methodDict[methodName]);
}

列印結果為 :

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

2. 動態變數控制

在程式中,XiaoMing的age是10,後來被runtime變成了20,來看看runtime是怎麼做到的:

-(void)changeAge{
     unsigned int count = 0;
     //動態獲取XiaoMing類中的所有屬性[當然包括私有] 
     Ivar *ivar = class_copyIvarList([self.xiaoMing class], &count);
     //遍歷屬性找到對應age欄位 
     for (int i = 0; i<count; i++) {
         Ivar var = ivar[i];
         const char *varName = ivar_getName(var);
         NSString *name = [NSString stringWithUTF8String:varName];
         if ([name isEqualToString:@"_age"]) {
             //修改對應的欄位值成20
             object_setIvar(self.xiaoMing, var, @"20");
             break;
         }
     }
     NSLog(@"XiaoMing's age is %@",self.xiaoMing.age);
 }

3. 在NSObject的分類中增加方法來避免使用KVC賦值的時候出現崩潰

在有些時候我們需要通過KVC去修改某個類的私有變數,但是又不知道該屬性是否存在,如果類中不存在該屬性,那麼通過KVC賦值就會crash,這時也可以通過執行時進行判斷。同樣我們在NSObject的分類中增加如下方法。

/**
 *  判斷類中是否有該屬性
 *
 *  @param property 屬性名稱
 *
 *  @return 判斷結果
 */
-(BOOL)hasProperty:(NSString *)property {
    BOOL flag = NO;
    u_int count = 0;
    Ivar *ivars = class_copyIvarList([self class], &count);
    for (int i = 0; i < count; i++) {
        const char *propertyName = ivar_getName(ivars[i]);
        NSString *propertyString = [NSString stringWithUTF8String:propertyName];
        if ([propertyString isEqualToString:property]){
            flag = YES;
        }
    }
}

4. 自動的歸檔和解檔

5. 字典轉模型

三、利用runtime的動態交換方法實現,我們可以做什麼?

1. 方法簡單的交換

建立一個Person類,類中實現以下兩個類方法,並在.h 檔案中宣告

+ (void)run {
    NSLog(@"跑");
}

+ (void)study {
    NSLog(@"學習");
}

控制器中呼叫,則先列印跑,後列印學習

[Person run];
[Person study];

下面通過runtime 實現方法交換,類方法用class_getClassMethod ,物件方法用class_getInstanceMethod

// 獲取兩個類的類方法
Method m1 = class_getClassMethod([Person class], @selector(run));
Method m2 = class_getClassMethod([Person class], @selector(study));
// 開始交換方法實現
method_exchangeImplementations(m1, m2);
// 交換後,先列印學習,再列印跑!
[Person run];
[Person study];

2. 攔截系統方法(Swizzle 黑魔法),也可以說成對系統的方法進行替換

由於某種原因,我們要改變這個方法的實現,但是又不能去動它的原始碼(系統的方法或者一些開源庫出現問題的時候),這個時候runtime就派上用場了。

需求:比如iOS6 升級 iOS7 後需要版本適配,根據不同系統使用不同樣式圖片(擬物化和扁平化),如何通過不去手動一個個修改每個UIImage的imageNamed:方法就可以實現為該方法中加入版本判斷語句?

步驟:
1、為UIImage建一個分類(UIImage+Category)

2、在分類中實現一個自定義方法,方法中寫要在系統方法中加入的語句,比如版本判斷

+ (UIImage *)xh_imageNamed:(NSString *)name {
    double version = [[UIDevice currentDevice].systemVersion doubleValue];
    if (version >= 7.0) {
        // 如果系統版本是7.0以上,使用另外一套檔名結尾是‘_os7’的扁平化圖片
        name = [name stringByAppendingString:@"_os7"];
    }
    return [UIImage xh_imageNamed:name];
}

3、分類中重寫UIImage的load方法,實現方法的交換(只要能讓其執行一次方法交換語句,load再合適不過了)

+ (void)load {
    // 獲取兩個類的類方法
    Method m1 = class_getClassMethod([UIImage class], @selector(imageNamed:));
    Method m2 = class_getClassMethod([UIImage class], @selector(xh_imageNamed:));
    // 開始交換方法實現
    method_exchangeImplementations(m1, m2);
}

注意:自定義方法中最後一定要再呼叫一下系統的方法,讓其有載入圖片的功能,但是由於方法交換,系統的方法名已經變成了我們自定義的方法名(有點繞,就是用我們的名字能呼叫系統的方法,用系統的名字能呼叫我們的方法),這就實現了系統方法的攔截!

利用以上思路,我們還可以給 NSObject 新增分類,統計建立了多少個物件,給控制器新增分類,統計有建立了多少個控制器,特別是公司需求總變的時候,在一些原有控制元件或模組上新增一個功能,建議使用該方法!

交換原理:

  • 交換之前:

  • 交換之後:

3. 執行時實現多繼承的效果

既然方法我們可以攔截,可以交換,那麼實現多繼承的效果就留給讀者自己思考了(避免篇幅太長,後續在部落格中再來探討這個問題)

四、動態新增方法

開發使用場景:如果一個類方法非常多,載入類到記憶體的時候也比較耗費資源,需要給每個方法生成對映表,可以使用動態給某個類,新增方法解決。

經典面試題:有沒有使用performSelector,其實主要想問你有沒有動態新增過方法。

簡單使用:

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    Person *p = [[Person alloc] init];

    // 預設person,沒有實現eat方法,可以通過performSelector呼叫,但是會報錯。
    // 動態新增方法就不會報錯
    [p performSelector:@selector(eat)];

}


@end


@implementation Person
// void(*)()
// 預設方法都有兩個隱式引數,
void eat(id self,SEL sel)
{
    NSLog(@"%@ %@",self,NSStringFromSelector(sel));
}

// 當一個物件呼叫未實現的方法,會呼叫這個方法處理,並且會把對應的方法列表傳過來.
// 剛好可以用來判斷,未實現的方法是不是我們想要動態新增的方法
+ (BOOL)resolveInstanceMethod:(SEL)sel
{

    if (sel == @selector(eat)) {
        // 動態新增eat方法

        // 第一個引數:給哪個類新增方法
        // 第二個引數:新增方法的方法編號
        // 第三個引數:新增方法的函式實現(函式地址)
        // 第四個引數:函式的型別,(返回值+引數型別) v:void @:物件->self :表示SEL->_cmd
        class_addMethod(self, @selector(eat), eat, "[email protected]:");

    }

    return [super resolveInstanceMethod:sel];
}
@end

五、利用執行時set和get這兩個API,可以讓類別可以新增屬性

步驟:

1、建立一個類別,比如給任何一個物件都新增一個name屬性,就是NSObject新增分類(NSObject+Category)

2、先在.h 中@property 宣告出get 和 set 方法,方便點語法呼叫

@property(nonatomic,copy)NSString *name;

3、在.m 中重寫set 和 get 方法,內部利用runtime 給屬性賦值和取值

char nameKey;

- (void)setName:(NSString *)name {
    // 將某個值跟某個物件關聯起來,將某個值儲存到某個物件中
    objc_setAssociatedObject(self, &nameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)name {
    return objc_getAssociatedObject(self, &nameKey);
}

六、萬能介面跳轉(使用了runtime的N多個方法)

由於文章篇幅長度原因,把這塊內容提取出來作為單獨一篇文章,跳轉連結 ——>runtime從入門到精通(九)—— 萬能介面跳轉

七、外掛開發

外掛入門

XCode 有個很坑爹的地方,就是它並不官方支援外掛開發,官方沒有文件,XCode 也沒有開源,但由於 XCode 是 Objective-C 寫的,OC 動態性太強大,導致在這麼封閉的情況下民間還是可以做出各種外掛,其核心開發方式就是:

dump 出 Xcode 所有標頭檔案,知道 Xcode 裡有哪些類和介面。

通過標頭檔案方法名猜測方法的作用,swizzle 這些方法,插入自己的程式碼實現外掛邏輯。

通過 NSNotificationCenter 監聽各種事件的發生。

更詳細的開發教程網上有不少文章,有興趣的自行搜尋吧。

八、Jspath 熱更新 也是使用執行時,jspatch 基本上算是黑科技,線上修復版本bug,微信都使用了這個技術,詳情百度“JSPatch”