文章分享至我的個人技術部落格:https://cainluo.github.io/15034036545472.html
在前一章裡, 我們把RunTime
的一些基礎概念和一些小東西給弄明白了, 正式踏入裝逼隊伍行列.
如果沒有加入到裝逼隊伍行列裡的小夥伴, 可以去看看玩轉iOS開發:iOS開發中的裝逼技術 – RunTime(一).
轉載宣告:如需要轉載該文章, 請聯絡作者, 並且註明出處, 以及不能擅自修改本文.
objc_msgSend的使用
在前面一篇文章裡, 我們用Clang
把RunTimeModel.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
來重構一下:
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")));
複製程式碼
那麼當我們呼叫[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
在目錄中, 我並沒有放到工程裡.