玩轉iOS開發:iOS開發中的裝逼技術 – RunTime(二)

CainLuo發表於2019-03-04

文章分享至我的個人技術部落格:https://cainluo.github.io/15034036545472.html


在前一章裡, 我們把RunTime的一些基礎概念和一些小東西給弄明白了, 正式踏入裝逼隊伍行列.

如果沒有加入到裝逼隊伍行列裡的小夥伴, 可以去看看玩轉iOS開發:iOS開發中的裝逼技術 – RunTime(一).

轉載宣告:如需要轉載該文章, 請聯絡作者, 並且註明出處, 以及不能擅自修改本文.


objc_msgSend的使用

在前面一篇文章裡, 我們用ClangRunTimeModel.m檔案重寫了, 得到了RunTimeModel.cpp, 裡面大多數都是C程式碼實現的.

那我們也可以仿著objc_msgSend來寫寫看, 工程仍然是之前的那個, 這裡我們新增了一個用來測試的類:

#import "TestModel.h"

@implementation TestModel

- (void)country {
    NSLog(@"中國");
}

- (void)getProvince:(NSString *)provinceName {
    NSLog(@"%@", provinceName);
}

- (void)getCity:(NSString *)cityName
        station:(NSString *)stationName {
    
    NSLog(@"%@, %@", cityName, stationName);
}

- (NSString *)getWeather {
    
    return @"晴天";
}

@end
複製程式碼

呼叫:

- (void)test {
    
    TestModel *objct = [[TestModel alloc] init];
    
    ((void (*) (id, SEL)) objc_msgSend) (objct, sel_registerName("country"));
    
    ((void (*) (id, SEL, NSString *)) objc_msgSend) (objct, sel_registerName("getProvince:"), @"廣東省");
    
    ((void (*) (id, SEL, NSString *, NSString *)) objc_msgSend) (objct, sel_registerName("getCity:station:"), @"深圳市", @"世界之窗");
    
    NSString *weather = ((NSString* (*) (id, SEL)) objc_msgSend) (objct, sel_registerName("getWeather"));
    
    NSLog(@"%@", weather);
}
複製程式碼

列印的結果:

2017-08-22 20:52:00.497 1.RunTime[34290:2794192] 中國
2017-08-22 20:52:00.497 1.RunTime[34290:2794192] 廣東省
2017-08-22 20:52:00.497 1.RunTime[34290:2794192] 深圳市, 世界之窗
2017-08-22 20:52:00.498 1.RunTime[34290:2794192] 晴天
複製程式碼

這裡看清楚咯, 我只是在TestModel.m檔案裡宣告瞭方法, 但是通過objc_msgSend, 依然可以呼叫.

再看看程式碼, 我們還會發現, 這裡的objc_msgSend做了一個強轉的操作, 如果我們把那個強轉幹掉的話, Xcode就會報錯:

Too many arguments to function call, expected 0, have 4.
複製程式碼

這個錯誤是根據你的方法引數大小來決定的.


objc_msgSendSuper

其實除開我們剛剛看到的objc_msgSend之外, 還有很多個, 比如:

  • objc_msgSend: 傳送具有簡單返回值的訊息到類的例項.
  • objc_msgSend_fpret: 傳送帶有浮點返回值的訊息到類的例項
  • objc_msgSend_stret: 將具有資料結構返回值的訊息傳送到類的例項
  • objc_msgSendSuper: 傳送一個簡單返回值的訊息到類的例項的超類
  • objc_msgSendSuper_stret: 將具有資料結構返回值的訊息傳送到類的例項的超類

這裡我們就重點說說objc_msgSendSuper, 它是在#import<objc/message.h>檔案中, 被定義成:

OBJC_EXPORT id objc_msgSendSuper(struct objc_super *super, SEL op, ...)
複製程式碼

平常我們在呼叫Super方法的時候, Runtime都會去呼叫objc_msgSendSuper, 比如:

[super methodName];
複製程式碼

我們可以在剛剛的TestModel裡重寫init方法, 並且列印一下:

- (instancetype)init {
    
    self = [super init];
    
    if (self) {
        
        NSLog(@"%@", [self class]);
        NSLog(@"%@", [super class]);
    }
    
    return self;
}
複製程式碼

寫完之後, 我們可以用Clang來重構一下:

1

PS: 記得你在哪個資料夾裡Clang重寫, 那麼新生成的檔案就在哪裡.

然後就在TestModel.cpp檔案裡找到:

NSLog((NSString *)&__NSConstantStringImpl__var_folders_86_ycmkjs0s48l_knc_xnscdqq00000gn_T_TestModel_ece3b7_mi_0, ((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class")));

NSLog((NSString *)&__NSConstantStringImpl__var_folders_86_ycmkjs0s48l_knc_xnscdqq00000gn_T_TestModel_ece3b7_mi_1, ((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("TestModel"))}, sel_registerName("class")));
複製程式碼
2

那麼當我們呼叫[super methodName]的時候, Runtime就會轉成objc_msgSendSuper, 它的過程是:

  • 先構造objc_super結構體
    • 第一個成員變數是self.
    • 第二個是(id)class_getSuperclass(objc_getClass(“TestModel”)).
  • 然後就是去超類裡找到- (Class)class方法, 如果沒有找到, 就會繼續往上一層去找, 一直找到NSObject, 找到了之後, 內部就會使用objc_msgSend(objc_super->receiver, @selector(class))去呼叫, 這裡就會和[self class]呼叫一樣, 所以輸出來的結果都是為TestModel.

物件關聯

物件關聯, 可以允許開發者對已存在的類的Category的類新增屬性:

OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0);
複製程式碼
  • object: 是源物件
  • key: 是關聯的鍵,
  • value: 被關聯的物件
  • policy: 是一個列舉

policy列舉:

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
                                            *   The association is made atomically. */
    OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
                                            *   The association is made atomically. */
};
複製程式碼

如果我們要獲取一個屬性的話, 那就可以使用下面這個方法, 也是用剛剛關聯的Key:

OBJC_EXPORT id objc_getAssociatedObject(id object, const void *key) OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0);
複製程式碼

如果要刪除一個被關聯的物件, 只要設定一下objc_setAssociatedObject並且把物件設定為nil就好了:

objc_setAssociatedObject(self, AttributeKey, nil, OBJC_ASSOCIATION_COPY_NONATOMIC);
複製程式碼

如果我們使用objc_removeAssociatedObjects的話, 就會把所有關聯的物件給全部移除:

OBJC_EXPORT void objc_removeAssociatedObjects(id object) OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0);
複製程式碼

我們直接來看程式碼吧:

#import "TestModel.h"

@interface TestModel (String)

@property (nonatomic, copy) NSString *testString;

@end
複製程式碼
#import "TestModel+String.h"
#import <objc/runtime.h>

static void *TestStringKey = &TestStringKey;

@implementation TestModel (String)

- (void)setTestString:(NSString *)testString {
    
    objc_setAssociatedObject(self, TestStringKey, testString, OBJC_ASSOCIATION_COPY);
}

- (NSString *)testString {
    
    return objc_getAssociatedObject(self, TestStringKey);
}

@end
複製程式碼

然後回到Controller裡引入標頭檔案, 在呼叫:

- (void)test {
    
    TestModel *objct = [[TestModel alloc] init];
    
    ((void (*) (id, SEL)) objc_msgSend) (objct, sel_registerName("country"));
    
    ((void (*) (id, SEL, NSString *)) objc_msgSend) (objct, sel_registerName("getProvince:"), @"廣東省");
    
    ((void (*) (id, SEL, NSString *, NSString *)) objc_msgSend) (objct, sel_registerName("getCity:station:"), @"深圳市", @"世界之窗");
    
    NSString *weather = ((NSString* (*) (id, SEL)) objc_msgSend) (objct, sel_registerName("getWeather"));
    
    NSLog(@"%@", weather);
    
    objct.testString = @"小明";
    
    NSLog(@"Category: %@", objct.testString);
}
複製程式碼
2017-08-23 00:09:38.236 1.RunTime[35345:2926512] TestModel
2017-08-23 00:09:38.236 1.RunTime[35345:2926512] TestModel
2017-08-23 00:09:38.236 1.RunTime[35345:2926512] 中國
2017-08-23 00:09:38.237 1.RunTime[35345:2926512] 廣東省
2017-08-23 00:09:38.237 1.RunTime[35345:2926512] 深圳市, 世界之窗
2017-08-23 00:09:38.237 1.RunTime[35345:2926512] 晴天
2017-08-23 00:09:38.237 1.RunTime[35345:2926512] Category: 小明
複製程式碼

工程地址

專案地址: https://github.com/CainRun/iOS-Project-Example/tree/master/RunTime/Two

注意: TestModel.cpp在目錄中, 我並沒有放到工程裡.


最後

碼字很費腦, 看官賞點飯錢可好
微信
支付寶

相關文章