遷移一批老文章到掘金html
這實際上是一個NSInvocation練習做業ios
嘗試手寫一個更好用的performSelector/msgSend程序員
一個羣裏聊天的時候聊到了一個場景,tableView內的cell有N種樣式,在cellForRow
的時候,經過NSClassFromString
從字符串建立對象,而後挨個對Cell的UI賦值,接下來問題就來了。數組
實在不想import如此繁多cell.h
頭文件應該怎麼辦?xcode
這裏不是說performSelector
中關於異步調用的那一部分,而是單說同步的:安全
- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
複製代碼
這個是NSObject
系統開放的performSelector
同步接口,這個好用麼?我之前以爲很很差用app
就像我說的,之前我幾乎只會去用performSelector調用無參數的函數,一旦有參數,我都不愛用performSelector異步
你們都知道OC的消息機制,函數調用其實都是發送消息,這個太多的地方有講了,我就很少說了。ide
一個咱們想要調用的函數函數
- (int) doSomething:(int) x { ... }
複製代碼
在32位的時代,想要實現我要的效果,能夠直接使用objc_msgSend
objc_msgSend(self,@selector(doSomething:), 0);
複製代碼
可是一旦在64位設備上執行,就會產生崩潰,緣由參見蘋果Converting Your App to a 64-Bit Binary,中Take Care with Functions and Function Pointers,這一部分。
簡單的說,64位下runtime調用和32位變化十分大,尤爲是讀取函數參數列表,進行傳參這部分,因此蘋果列出了一句話
Always Define Function Prototypes
Function Pointers Must Use the Correct Prototype
直接的調用C函數指針的時候必須先進行嚴格的類型匹配強轉,不能直接使用Imp
這個通用型的指針。
而objc_msgSend的內部實現也是一個這樣的過程,objc_msgSend學習
Imp
C函數,Imp
因此在64位下,直接使用objc_msgSend同樣會引發崩潰,必須進行一次強轉
((void(*)(id, SEL,int))objc_msgSend)(self, @selector(doSomething:), 0);
複製代碼
因此之前32位的時候objc_msgSend是咱們最方便的作法,如今64位了,他已經不是那麼方便了,畢竟使用起來還須要人自行手寫這部分強轉工做
我但願我設計的接口是這樣的
Class cls = NSClassFromString(@"testClassA");
id<vk_msgSend> abc = [[cls alloc]init];
NSError *err;
NSString *return1 = [abc vk_callSelector:@selector(testfunction:withB:) error:&err,4,3.5f];
複製代碼
<vk_msgSend>
的id對象,就能直接調用SEL
作參數執行,可是傳參很是容易,基礎類型,struct都支持,不須要withObject
,不須要轉成id
,只須要像NSLog()同樣,按順序輸入可變參數就好。SEL
參數還能夠改傳字符串因此他的定義是這樣的
+ (id)vk_callSelector:(SEL)selector error:(NSError *__autoreleasing *)error,...;
+ (id)vk_callSelectorName:(NSString *)selName error:(NSError *__autoreleasing *)error,...;
- (id)vk_callSelector:(SEL)selector error:(NSError *__autoreleasing *)error,...;
- (id)vk_callSelectorName:(NSString *)selName error:(NSError *__autoreleasing *)error,...;
複製代碼
既然接口設計的但願使用者怎麼簡單怎麼來,使用者用可變參數的方式一字羅列全部參數,無需轉id之類的。那咱們也得按照可變參數去處理。
這裏我遇到了一個問題,我一共設計4個接口,這4個接口其實大同小異,核心邏輯是同樣的,因此我確定是用一個公共的方法進行處理,可是,可變參函數怎麼透傳呢?
- (id)vk_callSelectorName:(NSString*)selName error:(NSError*__autoreleasing*)error,...{
SEL selector = NSSelectorFromString(selName);
[self vk_callSelector:selector error:error,...];
}
複製代碼
我但願這樣就能搞定,把...原封不動的塞到下面那個函數,但是xcode不認吶親╮(╯_╰)╭
後來公司討論組裏有位大神給出了建議,直接把va_list
當作公共函數的參數,進行透傳
設計公共方法的接口聲明爲,第一個參數就是va_list
static NSArray *vk_targetBoxingArguments(va_list argList, Class cls, SEL selector, NSError *__autoreleasing *error)
複製代碼
而後在調用的時候
va_list argList;
va_start(argList, error);
SEL selector = NSSelectorFromString(selName);
NSArray *boxingAruments = vk_targetBoxingArguments(argList, [self class], selector, error);
va_end(argList);
複製代碼
用va_start
獲取va_list
而後就能夠一層層的透傳給公共方法進行處理了
雖然輸入接口能夠支持任意的類型,基礎類型,struct,id,可是我內部實現的時候,仍是把它們統一轉換成了id,方便後續傳遞處理,這個步驟就是包裝一下全部傳進來的參數,也就是上面提到的vk_targetBoxingArguments
這個包裝的過程涉及到va_list
的取值過程va_arg
了,這裏我也踩了個大坑。容我細細道來
NSMethodSignature
NSMethodSignature
我理解他其實就是SEL的typeEncode的對象封裝,分別記錄了這個SEL的返回值類型和各個參數類型
咱們有調用對象,就能獲取到對象的Class,咱們有SEL,就能獲取到NSMethodSignature
methodSignature = [cls instanceMethodSignatureForSelector:selector];
複製代碼
NSMethodSignature
咱們就能按着循環去獲取每一個參數類型,從而讀取va_list
了。for (int i = 2; i < [methodSignature numberOfArguments]; i++) {
const char *argumentType = [methodSignature getArgumentTypeAtIndex:i];
switch (argumentType[0] == 'r' ? argumentType[1] : argumentType[0]) {
//抽取參數
}
複製代碼
NSMethodSignature
中前兩個分別表明返回值和reciever,咱們在抽取參數,因此直接從[2]下標開始取值,剩下的就是一個根據typeEcode,從va_list
取值,而後包裝成id,塞入數組的過程了,具體到每一種類型的case,能夠參見源碼。
1)取基礎類型int,va_arg(argList, int)取值,包裝成NSNumber(只舉一個例子,其餘見源碼)
int value = va_arg(argList, int);
[argumentsBoxingArray addObject:@(value)];
break;
複製代碼
2)取CGSize,va_arg(argList, CGSize)取值,包裝成NSValue(只舉一個例子,其餘見源碼)
CGSize val = va_arg(argList, CGSize);
NSValue* value = [NSValue valueWithCGSize:val];
[argumentsBoxingArray addObject:value];
break;
複製代碼
3)取id,va_arg(argList, id),不包裝,直接塞進去啦
這裏要注意,若是傳入的參數爲nil,須要特殊處理一下,nil沒法放入數組,因此我建立了一個vk_nilObject對象,來代表這個位置傳進來nil了
id value = va_arg(argList, id);
if (value) {
[argumentsBoxingArray addObject:value];
}else{
[argumentsBoxingArray addObject:[vk_nilObject new]];
}
複製代碼
4)取SEL,va_arg(argList,SEL),處理成string
由於SEL自己的意義就是一個函數的名字相似string同樣的鍵值,是用來查找函數用的,因此當成字符串處理啦
SEL value = va_arg(argList, SEL);
NSString *selValueName = NSStringFromSelector(value);
[argumentsBoxingArray addObject:selValueName];
複製代碼
5)取block,其實block就是id,因此和id的處理如出一轍
//同id
複製代碼
6)取id*,va_arg(argList, void**)
這裏須要注意一下,由於我取出來的是一個pointer,是不能直接放入數組裏的,因此我建立了一個vk_pointer對象,持有一個void*屬性,而後就能夠塞進數組了
void *value = va_arg(argList, void**);
vk_pointer *pointerObj = [[vk_pointer alloc]init];
pointerObj.pointer = value;
[argumentsBoxingArray addObject:pointerObj];
複製代碼
我在調試中,發現當我對typeEncode的f
取參數的時候
va_arg(argList, float)
複製代碼
xcode報了個warning
/Users/Awhisper/Desktop/GitHub/vk_msgSend/vk_msgSend/NSObject+vk_msgSend.m:280:49: Second argument to 'va_arg' is of promotable type 'float'; this va_arg has undefined behavior because arguments will be promoted to 'double'
複製代碼
一開始我看到warning沒管,就繼續編碼去了,結果運行的時候,參數裏含有float,發現了大問題
正如warning所說,此處編譯器是按着double實現的,可是我用va_arg()取的時候按着float取,就直接致使我取出來的float值不對,是0,(一個比較小的double值取了前面幾位天然都是0)
而float後面那個參數,id用va_arg(argList, id)取的時候直接崩潰,(指針已經亂了,從double的中間開始,按着id的長度取id,直接崩潰)
老老實實的修掉warning,改爲用va_arg(argList, double)
處理f
,一切正常。
咱們如今已經拿到了包裝好的參數數組NSArray,能夠開始調用函數了,使用NSInvocation
Class cls = [target class];
NSMethodSignature *methodSignature = vk_getMethodSignature(cls, selector);
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
複製代碼
[invocation setTarget:target];
[invocation setSelector:selector];
複製代碼
具體過程和Boxing同樣,遍歷methodSignature,按着typeEncode來從數組中取出id類型的參數,還原參數,壓入invocation。
遍歷的時候確定是根據每一個參數的typeEncode,去switch處理不一樣類型
for (int i = 2; i< [methodSignature numberOfArguments]; i++) {
const char *argumentType = [methodSignature getArgumentTypeAtIndex:i];
id valObj = argsArr[i-2];
switch (argumentType[0]=='r'?argumentType[1]:argumentType[0]) {
//switch case
}
}
複製代碼
這裏我會詳細分類別舉例如何壓入各類不一樣類型的參數,從[2]下表開始的緣由和前邊一致
[invocation setArgument:&value atIndex:i];`的做用就是壓入參數
複製代碼
1)int等基礎類型參數,對應上文的參數包裝(只舉一個例子,其餘見源碼)
int value = [valObj intValue];
[invocation setArgument:&value atIndex:i];
break;
複製代碼
2)CGSize基礎結構體參數,對應上文參數包裝(只舉一個例子,其餘見源碼)
CGSize value = [val CGSizeValue];
[invocation setArgument:&value atIndex:i];
複製代碼
3)id參數,對應上文參數包裝
上文提到若是傳入的id爲nil,被上文包裝成了vk_nilObject對象扔進數組的,因此這裏要針對這個處理一下
不是vk_nilObject的照常處理
是vk_nilObject,證實這個位置的參數傳入方爲空,因此我準備了一個空指針
static vk_nilObject *vknilPointer = nil;
複製代碼
把這個空指針傳進去
if ([valObj isKindOfClass:[vk_nilObject class]]) {
[invocation setArgument:&vknilPointer atIndex:i];
}else{
[invocation setArgument:&valObj atIndex:i];
}
複製代碼
4)SEL參數,對應上文包裝
上文提到,SEL被直接轉成了string,因此咱們這裏要還原成SEL,而後直接壓入參數
NSString *selName = valObj;
SEL selValue = NSSelectorFromString(selName);
[invocation setArgument:&selValue atIndex:i];
複製代碼
5)block參數,對應上文包裝
上文提到block和id是一回事
//同id
複製代碼
6)id*的處理,對應上文包裝,這裏極其噁心,我會專門寫一篇詳細說一下,這裏只寫個大概吧 上文已經把void*包裝成了 vk_pointer,因此咱們取出vk* 而後壓入參數
vk_pointer *value = valObj;
void* pointer = value.pointer;
[invocation setArgument:&pointer atIndex:i];
複製代碼
#你覺得這樣就能夠了麼?你太天真了
若是斷點調試,整個call_selector的過程徹底走完都不會有事,可是一旦放開斷點,完全走完就崩潰。 爲啥呢?由於在使用invocation的時候 invoke的過程當中,若是對象在invoke內被建立初始化了,invoke結束後,在下一個autorelease的時間點就會產生zombie的crash,send release to a dealloc object
爲何會這樣,簡單的說下個人理解不細說吧,invoke和直接函數調用不太同樣,若是發生了alloc對象,那麼這個對象系統會額外多一次autorelease,因此,不會馬上崩潰,但當autoreleasepool釋放的時候,就會發生過分release。
給幾個LINK有興趣你們能夠深刻探討一下 棧溢出1,棧溢出2
看一下個人解決辦法
vk_pointer *value = valObj;
void* pointer = value.pointer;
id obj = *((__unsafe_unretained id *)pointer);
if (!obj) {
if (argumentType[1] == '@') {
if (!_vkNilPointerTempMemoryPool) {
_vkNilPointerTempMemoryPool = [[NSMutableDictionary alloc] init];
}
if (!_markArray) {
_markArray = [[NSMutableArray alloc] init];
}
memset(pointer, 0, sizeof(id));
[_markArray addObject:valObj];
}
}
[invocation setArgument:&pointer atIndex:i];
複製代碼
我會先判斷一下 void*指向的對象是否存在,若是傳入的是一個已經alloc init 好了的 mutableArray之類的對象,我會直接壓入參數,由於invoke過程內,只是往mutableArray裏面執行操做,並無在void*指針處從新new的操做的話,是安全的不會崩潰的。
若是void*指向的對象不存在,至關於我傳入了一個 NSError*,等着由invoke內部去建立,這樣外面能夠捕獲,這種使用場景,就會致使crash,是由於過分release,那個人思路就是先把他持有一下。。。由於多了個release,那我再arc下不能強制retain,那我就add到一個字典裏,讓他被arc retain一下。
if ([_markArray count] > 0) {
for (vk_pointer *pointerObj in _markArray) {
void *pointer = pointerObj.pointer;
id obj = *((__unsafe_unretained id *)pointer);
if (obj) {
@synchronized(_vkNilPointerTempMemoryPool) {
[_vkNilPointerTempMemoryPool setObject:obj forKey:[NSNumber numberWithInteger:[(NSObject*)obj hash]]];
}
}
}
}
複製代碼
這段代碼放在[invocation invoke]
以後,由於只有執行以後咱們才知道void*指向的位置是否建立了新對象,判斷obj
是否存在,若是存在則向一個全局的static字典_vkNilPointerTempMemoryPool
寫入這個對象。
有人說?我爲何不是用棧溢出的答案?,棧溢出的答案倒是是保證不crash了,可是傳入的參數已經不是void** 而是一個 void***了,這樣會致使被調用的函數雖然建立了NSError,可是執行完畢後,並無賦值給有的指針,會致使外面看NSErro仍是空(這麼表述可能不對,這幾天啃指針,這塊已經把我弄得有點亂了,可是你們在函數外取個地址&error看一下,而後在函數內看傳入的error地址,就會發現已經不對了)
有人說,你這樣不是內存泄露了麼?一個對象在用過之後就永久被添加進了一個static字典裏,我只能說是的
,可是狀況不是那麼絕對,crash的緣由是系統的一次額外的release,而且還發生在代碼操做者沒法掌控的autoreleasepool的drain
時機,也就是說,在drain
前,這個字典裏的這個值是正常的(若是沒有字典,此時並沒崩潰),在drain
後,這個字典裏的值由於一次額外release了,此時這個字典內這個key還存在,可是他指向的對象已經野指針了(若是沒有字典,此時就崩潰了,由於對一個dealloc對象 release),我試過在幾秒以後確定保證drain
結束了,對字典執行removeAll
,仍是會崩潰!由於removeall的時候處理裏面的值,發現那個值野指針了。
有人有更好的辦法不?我想不到了,也球建議
[invocation invoke];
複製代碼
注意上文提到的invoke後處理一下 id* 的內存問題
如同壓入參數同樣,仍是經過typeEncode來判斷返回類型
const char *returnType = [methodSignature methodReturnType];
複製代碼
從invocation按類型取出返回值,返回
1)int 等基礎類型,注意我包裝成了NSNumber* 返回的,後文有講(只舉一個例子,其餘見源碼)
int returnValue;
[invocation getReturnValue:&returnValue];
return @(returnValue);
break;
複製代碼
2)CGSize等基礎類型,注意我包裝成了NSValue* 返回的,後文有講(只舉一個例子,其餘見源碼)
CGSize result;
[invocation getReturnValue:&result];
NSValue * returnValue = [NSValue valueWithBytes:&(result) objCType:@encode(CGSize)];\
return returnValue;
複製代碼
3)id類型,這裏面也有個坑 我是這麼作的
void *result;
[invocation getReturnValue:&result];
if (result == NULL) {
return nil;
}
id returnValue;
returnValue = (__bridge id)result;
return returnValue;
複製代碼
爲何這麼作,是由於getReturnValue只是拷貝返回值到指定的地址,你如今返回的是一個id,是一個指針,那麼實際對象會在函數runloop結束後自動釋放的,緣由很相似以前的id*參數問題,可是這裏是返回值。
注意個人返回值被強迫指定成了id,也就是說,若是原函數返回的是NSInteger,我會返回一個NSNumber。
爲何會這樣?我搞不定如何在聲明函數的時候,用一個兼容基礎和id,全部類型的符號來定義函數。。
參數之因此能夠兼容id與基礎類型,是由於我用可變參數...繞過去了。。
可是返回值我就搞不定了,有人說用void *
但個人初衷是但願使用者直接拿到最終的值,目前的困難不是如何把值傳出去。而是傳出去一個使用者不須要手動轉換的最終結果。
用void *
這麼看和用id 其實也差很少,使用者拿到後都得轉一下。。。