遷移老文章到掘金javascript
這是上一篇博客提到的代碼的深刻剖析java
note:這個是JSPatch附屬新增的小功能點,想要詳細瞭解JsPatch總體部分的工做及原理戳這個wiki JSPatch實現原理詳解git
工做中遇到了一個case,有一部分代碼被重構了,一個函數被完全的廢棄而且.m文件中的具體函數實現已經被總體註釋掉了,可是.h文件這個函數還存在.github
因爲被重構的那部分在客戶端不少處代碼都有調用,沒有及時的替換成最新的函數,致使形成了線上crash,unrecognized selector
.設計模式
我最開始想用JsPatch發出一個hotfix,既然是unrecognized selector
,具體的函數實現不存在,那麼我用JSPatch動態補上這個函數實現,就能夠封住crash了.數組
結果操做後發現,沒法實現,緣由是.h文件中這個selector裏面有一個非id類型的參數.app
在JsPatch的Wiki中defineClass 有一句說明ide
能夠給一個類隨意添加 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
type encode
,因此沒法正確的添加任意參數類型的方法,因而統一設定爲id類型type encode
,則咱們的目的就能夠達成咱們能夠考慮修改defineClass的input,專門在新增方法處開新的接口傳入參數,從而使得一切信息都能到手,正常生成正確的新方法。
可是眼下還有2個問題
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 < numberOfArg; i ++) {
[typeDescStr appendString:@"@"];
}
overrideMethod(currCls, selectorName, jsMethod, !isInstance, [typeDescStr cStringUsingEncoding:NSUTF8StringEncoding]);
}
}
複製代碼
源碼中先判斷是否該方法已經存在,存在的狀況下進行覆蓋,若是不存在,先判斷defineClass中是否指定了Protocol,指定了的話從Protocol中尋找匹配的Method進行覆蓋和新增,若是在指定Protocol中也找不到,才進行強制id參數類型的方法新增。
因此我選一個比較好的角度,既不破壞本來defineClass的設計邏輯,又能將新的參數傳入其中。
那就是設計一個全新的接口defineProtocol,在這個全新的接口裏面輸入足夠多的參數信息,進而經過運行時建立全新的Protocol,建立完成的新Protocol就天然能夠藉助defineClass裏面的功能,引入正確的新增方法
一開始我是想直接讓使用者輸入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其餘接口徹底一致,
看下對比是否是和defineClass如出一轍?^_^
context[@"_OC_defineProtocol"] = ^(NSString *protocolDeclaration, JSValue *instProtocol, JSValue *clsProtocol) {
return defineProtocol(protocolDeclaration, instProtocol,clsProtocol);
};
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返回的字典裏識別這兩個參數
如接口設計裏面的樣例testProtocol,是被當作字典中的key,能夠直接取出來的,由於咱們設計defineProtocol中Js新方法的命名和defineClass一致,都是參數用_
代替,本來的_下劃線用__
代替,因此解析key這個字符串的步驟和defineClass也一致
NOTES:源碼中須要用paramsType的個數來判斷函數名結尾是否存在參數,因此在typeEncode可選參數使用的狀況下,paramsType能夠隨意輸入任意的字符串,可是必須保證數量匹配
如接口設計裏面的樣例,參數會輸入"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] > 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類型,不管是系統類型NSArray仍是用戶本身寫的CustomObject,在使用咱們的defineProtocol的時候用戶須要本身記得全部的NSObject都要輸入id
,仔細想一想這也挺不方便的對吧?
因此我額外作了一個處理,當從typeEncodeDic表裏面找不到對應的key的時候,就會NSClassFromString
來判斷是不是一個Oc對象,若是是自動轉換爲id的類型編碼@
NSString* argencode = [typeEncodeDic objectForKey:argstr];
if (argencode.length <= 0) {
Class cls = NSClassFromString(argstr);
if ([(id)cls isKindOfClass:[NSObject class]]) {
argencode = @"@";
}
}
複製代碼
這樣不管用戶輸入類名
仍是id
,我這邊的處理都是徹底同樣,等效的
paramsType:"id"
paramsType:"CustomObject"
複製代碼
SEL的類型編碼命名方式是這樣的
- (void) setSomething:(id) anObject
複製代碼
這個函數他的類型編碼是
v@:@
複製代碼
v
表明返回值是void即void的類型編碼@
表明self(實際上是第一個參數 Self和SEL是任何oc函數的隱藏參數),這個基本是固定的:
表明SEL(實際上是第二個參數 Self和SEL是任何oc函數的隱藏參數),這個基本是固定的@
表明Oc函數第一個參數的類型即id的類型編碼經過這些規律,咱們能夠手寫SEL的類型編碼了,每一種參數類型能夠查詢蘋果的定義
代碼中可選參數typeEncode優先級最高,若是用戶手寫了可選參數,則不會執行代碼自動生成,直接使用用戶輸入的typeEncode,生成Protocol。
if (typeEncode) {
addMethodToProtocol(protocol, selectorName, typeEncode, isInstance);
}else
{
//type encode string automatic create
}
複製代碼
咱們能夠手寫typeEncode,其實也能夠藉助oc代碼生成typeEncode
咱們先在代碼中實現- (void) setSomething:(id) anObject
這個方法,而後使用下面的代碼,就能經過系統取出SEL的typeEncode
Class cls = self.class;
SEL selstr = NSSelectorFromString(@"setSomething:");
Method method = class_getInstanceMethod(cls, selstr);
const char* type = method_getTypeEncoding(method);
複製代碼
通過系統的讀取,驚訝的發現,系統算出來的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聊,以爲這多是一個字節補齊的過程,並不影響使用