你好,歡迎來到IOS教程網

 Ios教程網 >> IOS訊息 >> 關於IOS >> JSPatch defineProtocol部分實現詳解

JSPatch defineProtocol部分實現詳解

編輯:關於IOS

JSPatch defineProtocol部分實現詳解

本文為投稿文章,作者:唯敬


這個是給JSPatch新增的小功能點,想要詳細了解JSPatch整體部分的工作及原理戳這個wiki JSPatch實現原理詳解。

 

感謝:JSPatch原作者bang哥的指導,還有DevSnow幫了我好多大忙。

出發點

一個不小心引發的bad case

工作中遇到了一個case,有一部分代碼被重構了,一個函數被徹底的廢棄並且.m文件中的具體函數實現已經被整體注釋掉了,但是.h文件這個函數還存在.

由於被重構的那部分在客戶端很多處代碼都有調用,沒有及時的替換成最新的函數,導致造成了線上crash,unrecognized selector.

我最開始想用JSPatch發出一個hotfix,既然是unrecognized selector,具體的函數實現不存在,那麼我用JsPatch動態補上這個函數實現,就可以封住crash了.

結果操作後發現,無法實現,原因是.h文件中這個selector裡面有一個非id類型的參數.

JsPatch只能新增參數類型為id的方法

在JsPatch的Wiki中defineClass 有一句說明

可以給一個類隨意添加 OC 未定義的方法,但所有的參數類型都是 id:

為什麼會這樣,探究其源碼可以發現

if (!overrided) {      NSMutableString *typeDescStr = [@"@@:" mutableCopy];      for (int i = 0; i < numberOfArg; i ++) {          [typeDescStr appendString:@"@"];                }      overrideMethod(currCls, selectorName, jsMethod, !isInstance, [typeDescStr cStringUsingEncoding:NSUTF8StringEncoding]); }

當使用defineClass對新方法命名的時候,defineClass能通過_自動識別參數的位置和個數,但是並沒有能識別參數的類型。

而在通過這段代碼創建新方法的時候,需要輸入方法的type encode,由於defineClass只有參數的個數和位置信息,並未獲得參數的類型,因此JsPatch默認要求新方法所有輸入的參數都是id類型,返回的參數也必須是id類型,通過@@:+參數數量個@來生成,只允許id類型的參數及返回的新方法

關於type encode後面會詳細解釋

當我在嘗試通過JsPatch修復我的case的時候,由於我希望新增的方法是一個含有非id類型參數的方法,而JsPatch最終添加的新方法的參數都是id,所以程序運行的時候依然會crash,因為他還是找不到那個他想要的方法,依然是unrecognized selector

修改思路

知道原因,尋找思路

  • defineClass為覆蓋修改方法而設計,對於新增方法,傳入的信息不足,不能生成正確的type encode,所以無法正確的添加任意參數類型的方法,於是統一設定為id類型。
  • 如果由使用者傳入足夠的信息,借而生成正確的type encode,則我們的目的就可以達成。

我們可以考慮修改defineClass的input,專門在新增方法處開新的接口傳入參數,從而使得一切信息都能到手,正常生成正確的新方法。

但是眼下還有3個問題

  • defineClass在設計上,新增方法和覆蓋修改方法走的是同一個輸入口,單獨為新增方法而重新調整輸入接口,會使代碼邏輯和設計模式變化比較大。
  • 在用戶已經養成的JsPatch編寫習慣上,新增和覆蓋二者本是統一的,為新增方法而大改defineClass的輸入模式,勢必會讓已經習慣使用的用戶有很大不便。
  • 尋找一個合適的方案,能不大范圍影響現在的設計模式,又能完成我的想法。

defineClass的Protocol

JsPatch的defineClass 中提到的Protocol的作用

可以在定義時讓一個類實現某些 Protocol 接口,寫法跟 OC 一樣。

defineClass("JPViewController: UIViewController<uiscrollviewdelegate, uitextviewdelegate="">", {})

這樣做的作用是,當添加 Protocol 裡定義的方法,而類裡沒有實現的方法時,參數類型不再全是 id,而是自動轉為 Protocol 裡定義的類型。

看到原作者bang的說明我們就可以明白,defineClass中的Protocol的作用本是借助已經存在的Protocol的定義,從已經存在的Protocol中就可以抽取出描述selector的type encode,進而生成含有非id參數的方法描述,從而能新增出正確的方法。

我們還可以看下源碼,就一清二楚

if (class_respondsToSelector(currCls, NSSelectorFromString(selectorName))) {                 overrideMethod(currCls, selectorName, jsMethod, !isInstance, NULL);
 } 
else
 {     
BOOL overrided = NO;   
  for (NSString *protocolName in protocols) {  
       char *types = methodTypesInProtocol(protocolName, selectorName, isInstance, YES);  
       if (!types) types = methodTypesInProtocol(protocolName, selectorName, isInstance, NO);       
  if (types) { 
           
 overrideMethod(currCls, selectorName, jsMethod, !isInstance, types);          
   free(types);         
    overrided = YES;        
     break;       
  }    
 }  
   if (!overrided) {     
    NSMutableString *typeDescStr = [@"@@:" mutableCopy];        
 for (int i = 0; i &lt; numberOfArg; i ++) {      
       [typeDescStr appendString:@"@"];  
       }    
     overrideMethod(currCls, selectorName, jsMethod, !isInstance, [typeDescStr cStringUsingEncoding:NSUTF8StringEncoding]);   
  }
 }

源碼中先判斷是否該方法已經存在,存在的情況下進行覆蓋,如果不存在,先判斷defineClass中是否指定了Protocol,指定了的話從Protocol中尋找匹配的Method進行覆蓋和新增,如果在指定Protocol中也找不到,才進行強制id參數類型的方法新增。

所以我選一個比較好的角度,既不破壞原本defineClass的設計邏輯,又能將新的參數傳入其中。

那就是設計一個全新的接口defineProtocol,在這個全新的接口裡面輸入足夠多的參數信息,進而通過運行時創建全新的Protocol,創建完成的新Protocol就自然可以借助defineClass裡面的功能,引入正確的新增方法

具體實現

JS接口設計

一開始我是想直接讓使用者輸入type encode這樣也省了我的事,後來和原作者交流覺得,盡可能的節省使用者的學習成本,畢竟type encode不知道的人還真不太能很快搞明白這一大堆: # @ v b i的亂七八糟字符到底該怎麼寫,如果輸入接口這樣,就會比較直觀

defineProtocol('lalalala',{ 
  testProtocol: {   
  paramsType:"int, id",     returnType:"BOOL" 
  },   ... }, 
{   ... });

使用者直接輸入int,float,id,void等,由代碼自動識別生成最終的type encode,而且因為自動識別需代碼進行逐一的支持和轉換,有些特殊的參數類型,代碼轉換並不能完全覆蓋,於是還添加了一個可選的參數typeEncode,一旦自動轉換無法支持的參數類型,就可以通過可選參數,需要使用者自己想辦法手寫type encode了,主要無法支持的參數是用戶自定義的struct

代碼實現

Js接口這部分實現就不詳細描述了,和JsPatch其他接口完全一致,

context[@"_OC_defineProtocol"] = ^(NSString *protocolDeclaration, JSValue *instProtocol, JSValue *clsProtocol) { 
    return defineProtocol(protocolDeclaration, instProtocol,clsProtocol); 
};

是不是和defineClass一模一樣?^_^

context[@"_OC_defineClass"] = ^(NSString *classDeclaration, JSValue *instanceMethods, JSValue *classMethods) {  
   return defineClass(classDeclaration, instanceMethods, classMethods); 
};

通過運行時objc_allocateProtocol創建新Protocol,通過protocol_addMethodDescription來為新Protocol增加方法,通過objc_registerProtocol來注冊新Protocol,這是基本的runtime代碼,不多描述了,源碼裡都可以看到。

唯一需要注意的是新protocol一經注冊生效objc_registerProtocol,就不可在更改了,所以defineProtocol不能修改已經存在的Protocol。

protocol_addMethodDescription需要輸入seletorName和type encode,接下來重點說下如何在js返回的字典裡識別這兩個參數。

識別selector

如接口設計裡面的樣例testProtocol,是被當做字典中的key,可以直接取出來的,因為我們設計defineProtocol中Js新方法的命名和defineClass一致,都是參數用_代替,原本的下劃線用`_`代替,所以解析key這個字符串的步驟和defineClass也一致。

NOTES:源碼中需要用paramsType的個數來判斷函數名結尾是否存在參數,所以在typeEncode可選參數使用的情況下,paramsType可以隨意輸入任意的字符串,但是必須保證數量匹配。

識別type encode

如接口設計裡面的樣例,參數會輸入"int, id"這樣的字符串,返回值會輸入"void"這樣的字符串,前者再通過,號拆分成字符串數組,就接下來就可以通過代碼獲取了,我打算構建一個有限字符串映射表typeEncodeDic,以type字符串為key,映射int到i這樣。

typeEncodeDic這個表已經構建好了,這樣從js傳來的type字符串當做key,直接從這個表裡就能get到編碼。

人肉去寫這個表太low了,怎麼也得用酷炫一點的方式支持一下,看到原作者bang,在JsPatch裡面風騷的宏的用法,我也照貓畫虎了一個

NSMutableDictionary* typeEncodeDic = [[NSMutableDictionary alloc]init]; #define JP_DEFINE_TYPE_ENCODE_CASE(_type) / if ([@#_type length] &gt; 0) {/     char* encode = @encode(_type);/     NSString * encodestr = [NSString stringWithUTF8String:encode];/     [typeEncodeDic setObject:encodestr forKey:@#_type];/ } JP_DEFINE_TYPE_ENCODE_CASE(id);

JP_DEFINE_TYPE_ENCODE_CASE這個宏就自動的將輸入參數_type通過語法糖@encode()寫入字典,這裡面還有一處很nb的地方

宏裡面用參數生成靜態字符串

這是一個很trick的地方,原本我的宏是這麼設計的JP_DEFINE_TYPE_ENCODE_CASE(@"id",id)為什麼這麼設計?因為我搞不定怎麼在宏裡將id轉成@“id”,試了很多種方法都不行╮(╯_╰)╭

後來原作者bang交流,他給了解決辦法,@#_type他在JsPatch裡已經用到了,說他當初也遇到一樣的困擾,然後查到的。

所以最終這個宏被設計成了這樣。

       JP_DEFINE_TYPE_ENCODE_CASE(id);      
  JP_DEFINE_TYPE_ENCODE_CASE(BOOL);     
   JP_DEFINE_TYPE_ENCODE_CASE(int);  
      JP_DEFINE_TYPE_ENCODE_CASE(void);  
      JP_DEFINE_TYPE_ENCODE_CASE(char);   
     JP_DEFINE_TYPE_ENCODE_CASE(short);    
    JP_DEFINE_TYPE_ENCODE_CASE(unsigned short);   
     JP_DEFINE_TYPE_ENCODE_CASE(unsigned int); 
       JP_DEFINE_TYPE_ENCODE_CASE(long);     
   JP_DEFINE_TYPE_ENCODE_CASE(unsigned long);  
      JP_DEFINE_TYPE_ENCODE_CASE(long long); 
       JP_DEFINE_TYPE_ENCODE_CASE(float);    
    JP_DEFINE_TYPE_ENCODE_CASE(double);    
    JP_DEFINE_TYPE_ENCODE_CASE(CGFloat);    
    JP_DEFINE_TYPE_ENCODE_CASE(CGSize);     
   JP_DEFINE_TYPE_ENCODE_CASE(CGRect);    
    JP_DEFINE_TYPE_ENCODE_CASE(CGPoint);  
      JP_DEFINE_TYPE_ENCODE_CASE(CGVector);   
     JP_DEFINE_TYPE_ENCODE_CASE(UIEdgeInsets); 
       JP_DEFINE_TYPE_ENCODE_CASE(NSInteger);  
      JP_DEFINE_TYPE_ENCODE_CASE(Class);    
    JP_DEFINE_TYPE_ENCODE_CASE(SEL);

從這可以看出來,想要擴展支持更多的參數類型?沒問題,在這裡添加就好了(不想修改源碼,動態添加就走之前說的可選參數typeEncode)

處理id類型參數

看到上面我們知道,如果我的新函數中存在id類型,無論是系統類型NSArray還是用戶自己寫的CustomObject,在使用我們的defineProtocol的時候用戶需要自己記得所有的NSObject都要輸入id,仔細想想這也挺不方便的對吧?

所以我額外做了一個處理,當從typeEncodeDic表裡面找不到對應的key的時候,就會NSClassFromString來判斷是否是一個Oc對象,如果是自動轉換為id的類型編碼@

NSString* argencode = [typeEncodeDic objectForKey:argstr];
 if (argencode.length &lt;= 0) {  
   Class cls = NSClassFromString(argstr);   
  if ([(id)cls isKindOfClass:[NSObject class]])
 {         
argencode = @"@";   
  } 
}

這樣無論用戶輸入類名還是id,我這邊的處理都是完全一樣,等效的

paramsType:"id" paramsType:"CustomObject"

生成SEL的類型編碼

SEL的類型編碼命名方式是這樣的

- (void) setSomething:(id) anObject

這個函數他的類型編碼是

if (class_respondsToSelector(currCls, NSSelectorFromString(selectorName))) {  
               overrideMethod(currCls, selectorName, jsMethod, !isInstance, NULL); 
}
else {  
   BOOL overrided = NO;   
  for (NSString *protocolName in protocols) {  
       char *types = methodTypesInProtocol(protocolName, selectorName, isInstance, YES);   
      if (!types) types = methodTypesInProtocol(protocolName, selectorName, isInstance, NO);
         if (types) {    

         overrideMethod(currCls, selectorName, jsMethod, !isInstance, types);     
        free(types);    
         overrided = YES;   
          break;      
   }    
 }    
 if (!overrided) {   
      NSMutableString *typeDescStr = [@"@@:" mutableCopy];  
       for (int i = 0; i &lt; numberOfArg; i ++) {   
    
      [typeDescStr appendString:@"@"];        
 }      
   overrideMethod(currCls, selectorName, jsMethod, !isInstance, [typeDescStr cStringUsingEncoding:NSUTF8StringEncoding]);   
  }
 }

0

  • 第一個v代表返回值是void即void的類型編碼
  • 第二個@代表self(其實是第一個參數 Self和SEL是任何oc函數的隱藏參數),這個基本是固定的
  • 第三個:代表SEL(其實是第二個參數 Self和SEL是任何oc函數的隱藏參數),這個基本是固定的
  • 第四個@代表Oc函數第一個參數的類型即id的類型編碼

通過這些規律,我們可以手寫SEL的類型編碼了,每一種參數類型可以查詢蘋果的定義

代碼中可選參數typeEncode優先級最高,如果用戶手寫了可選參數,則不會執行代碼自動生成,直接使用用戶輸入的typeEncode,生成Protocol。

if (class_respondsToSelector(currCls, NSSelectorFromString(selectorName))) {                 overrideMethod(currCls, selectorName, jsMethod, !isInstance, NULL); 
} else
 {   
  BOOL overrided = NO;  
   for (NSString *protocolName in protocols) {     
    char *types = methodTypesInProtocol(protocolName, selectorName, isInstance, YES);         if (!types) types = methodTypesInProtocol(protocolName, selectorName, isInstance, NO);         if (types) {      
       overrideMethod(currCls, selectorName, jsMethod, !isInstance, types);   
          free(types);      
       overrided = YES;       
      break;        
 }   
  }  
   if (!overrided) {    
     NSMutableString *typeDescStr = [@"@@:" mutableCopy];   
      for (int i = 0; i &lt; numberOfArg; i ++) {    
         [typeDescStr appendString:@"@"];   
      }      
   overrideMethod(currCls, selectorName, jsMethod, !isInstance, [typeDescStr cStringUsingEncoding:NSUTF8StringEncoding]);   
  }
 }

1

詳探TypeEncode

我們可以手寫typeEncode,其實也可以借助oc代碼生成typeEncode

我們先在代碼中實現- (void) setSomething:(id) anObject這個方法,然後使用下面的代碼,就能通過系統取出SEL的typeEncode

if (class_respondsToSelector(currCls, NSSelectorFromString(selectorName))) {                 overrideMethod(currCls, selectorName, jsMethod, !isInstance, NULL);
 } else { 
    BOOL overrided = NO;   
  for (NSString *protocolName in protocols) {   
      char *types = methodTypesInProtocol(protocolName, selectorName, isInstance, YES);         if (!types) types = methodTypesInProtocol(protocolName, selectorName, isInstance, NO);         if (types) {      
       overrideMethod(currCls, selectorName, jsMethod, !isInstance, types);    
         free(types);      
       overrided = YES;         
    break;       
 }   
  }   
  if (!overrided) {  
       NSMutableString *typeDescStr = [@"@@:" mutableCopy];   
      for (int i = 0; i &lt; numberOfArg; i ++) {  
           [typeDescStr appendString:@"@"];        
 }         
overrideMethod(currCls, selectorName, jsMethod, !isInstance, [typeDescStr cStringUsingEncoding:NSUTF8StringEncoding]);  
   } 
}

2

經過系統的讀取,驚訝的發現,系統算出來的type居然是v12@0:4@8,這他喵的一堆數字是什麼鬼!,剛才不是說v@:@嘛????????!!!!!!

經過我反復地測試,發現無論是輸入v12@0:4@8還是v@:@,Protocol都能正常的生成,一點區別也沒有,完全不影響使用,但是他喵的為什麼系統就會多出來這麼多數字?

棧溢出的一個回答似乎能解釋 StackOverFlow-What are the digits in an ObjC method type encoding string?

和gitHub上的@DevSonw聊,覺得這可能是一個字節補齊的過程,並不影響使用。

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved