Runtime的入門與應用之六-實際運用

runtime在實際的開發中到底有何牛X的作用?我們該怎麼使用這麼牛X的工具呢?

想使用runtime,首先在寫運行時代碼之前,要先加上頭文件:

#import <objc/objc-runtime.h> // 模擬器
或者
#import <objc/runtime.h> // 真機
#import <objc/message.h> // 真機
  • 1
  • 2
  • 3
  • 4

一、動態添加一個類

(“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);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

運行結果爲:

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
  • 1
  • 2
  • 3

這樣, 我們就在程序運行時動態添加了一個繼承自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]);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63

打印結果爲 :

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

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);
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

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;
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

4. 自動的歸檔和解檔

由於文章篇幅長度原因,把這塊內容提取出來作爲單獨一篇文章,跳轉鏈接 ——> runtime從入門到精通(七)—— 自動歸檔和解檔

5. 字典轉模型

由於文章篇幅長度原因,把這塊內容提取出來作爲單獨一篇文章,跳轉鏈接 ——> runtime從入門到精通(八)—— 使用runtime實現字典轉模型

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

1. 方法簡單的交換

創建一個Person類,類中實現以下兩個類方法,並在.h 文件中聲明

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

+ (void)study {
    NSLog(@"學習");
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

控制器中調用,則先打印跑,後打印學習

[Person run];
[Person study];
  • 1
  • 2

下面通過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];
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

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];
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

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

利用以上思路,我們還可以給 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, "v@:");

    }

    return [super resolveInstanceMethod:sel];
}
@end
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45

五、利用運行時set和get這兩個API,可以讓類別可以添加屬性

步驟:

1、創建一個類別,比如給任何一個對象都添加一個name屬性,就是NSObject添加分類(NSObject+Category)

2、先在.h 中@property 聲明出get 和 set 方法,方便點語法調用

@property(nonatomic,copy)NSString *name;
  • 1
  • 2

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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

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

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

七、插件開發

插件入門

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

dump 出 Xcode 所有頭文件,知道 Xcode 裏有哪些類和接口。

通過頭文件方法名猜測方法的作用,swizzle 這些方法,插入自己的代碼實現插件邏輯。

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

更詳細的開發教程網上有不少文章,有興趣的自行搜索吧。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

八、Jspath 熱更新 也是使用運行時,jspatch 基本上算是黑科技,在線修復版本bug,微信都使用了這個技術,詳情百度“JSPatch”(現在在被蘋果封殺)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章