嘗試手寫一個更好用的performSelector-msgSend

遷移一批老文章到掘金html

這實際上是一個NSInvocation練習做業ios

嘗試手寫一個更好用的performSelector/msgSend程序員

引子

  • 工做中不免會遇到一些場景,開發的時候不想引入整個頭文件,可是又想調用一些方法
  • 動態建立,動態調用看起來比較酷
  • 這種使用場景確實不常見,導入了頭文件最省事,最直接,可是這種方式我以爲能搞出不少好玩的東西

一個羣裏聊天的時候聊到了一個場景,tableView內的cell有N種樣式,在cellForRow的時候,經過NSClassFromString從字符串建立對象,而後挨個對Cell的UI賦值,接下來問題就來了。數組

實在不想import如此繁多cell.h頭文件應該怎麼辦?xcode

  • 有一個辦法,全部cell都有個基類,基類統一全部UI賦值的接口,子類重載這些UI賦值,這樣建立出來的對象強轉成基類,調用基類的接口。這樣只須要import一個基類頭文件就夠了
    • 這樣要求子類的接口必須和基類徹底一致
    • 若是子類設計不少樣,賦值UI的元素更多,就會不太合理
  • 還有一個辦法performSelector,恩說實話,我以爲很很差用
  • 會有人說用運行時Objc_msgSend,恩,這個靠譜,聽起來也挺易用的
  • 老老實實引入各類頭文件,別搞什麼動態建立,動態調用的花樣了

聊聊performSelector

這裏不是說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

  • 參數類型:我湊,不須要參數的接口用起來最直觀,我也以爲還算好用,一旦須要參數,withObject:id是什麼鬼?我傳BOOL,傳NSInteger怎麼傳啊?我包裝成NSNumber對面能認識麼?
  • 參數個數:爲毛只能不帶參數,1個參數,2個參數呢?我想調用的東西含參數特別多咋辦啊?
  • 調用寫法:每一個參數還得用withObject來傳,寫出來一點都不酷

就像我說的,之前我幾乎只會去用performSelector調用無參數的函數,一旦有參數,我都不愛用performSelector異步

聊聊objc_msgSend

你們都知道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學習

  • 先從runtime method cache裏面查找selector,
  • 找不到再從 method list裏查找,
  • 找到selector,獲取具體實現的ImpC函數,
  • 調用Imp

因此在64位下,直接使用objc_msgSend同樣會引發崩潰,必須進行一次強轉

((void(*)(id, SEL,int))objc_msgSend)(self, @selector(doSomething:), 0);
複製代碼

因此之前32位的時候objc_msgSend是咱們最方便的作法,如今64位了,他已經不是那麼方便了,畢竟使用起來還須要人自行手寫這部分強轉工做

本着程序員偷懶大法,這部分能不能也省略了?變得更方便一些?

設計個人callSelector的接口

我但願我設計的接口是這樣的

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];
複製代碼
  • 它是一個NSObject的Category,只要你對強轉成聽從<vk_msgSend>的id對象,就能直接調用
  • 它像performSelector同樣輸入SEL作參數執行,可是傳參很是容易,基礎類型,struct都支持,不須要withObject,不須要轉成id,只須要像NSLog()同樣,按順序輸入可變參數就好。
  • 有一個error指針能夠用來返回錯誤信息,也能夠填nil不傳
  • 它支持類方法
  • 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,...;
複製代碼

實現這樣的callSelector

可變參數接口透傳的問題

既然接口設計的但願使用者怎麼簡單怎麼來,使用者用可變參數的方式一字羅列全部參數,無需轉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了,這裏我也踩了個大坑。容我細細道來

  • 從va_list裏面一個一個的取出參數須要明確知道,每個參數的類型,可是咱們想作的是一個通用型的方法,這塊就不能寫死,但是從哪知道參數類型呢? -- 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];
複製代碼
  • 遇到了一個va_arg()的坑

我在調試中,發現當我對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,一切正常。

實現調用:NSInvocation

咱們如今已經拿到了包裝好的參數數組NSArray,能夠開始調用函數了,使用NSInvocation

1 首先先要生成NSInvocation

Class cls = [target class];
NSMethodSignature *methodSignature = vk_getMethodSignature(cls, selector);
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
複製代碼

2 設置target和SEL

[invocation setTarget:target];
[invocation setSelector:selector];
複製代碼

3 循環壓入參數

具體過程和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的時候處理裏面的值,發現那個值野指針了。

  • 有人有更好的辦法不?我想不到了,也球建議

4 執行NSInvocation

[invocation invoke];
複製代碼

注意上文提到的invoke後處理一下 id* 的內存問題

4 取出返回值 具體能夠看下一篇 NSInvocation內存處理

如同壓入參數同樣,仍是經過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 其實也差很少,使用者拿到後都得轉一下。。。

相關文章
相關標籤/搜索