日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 前端技术 > javascript >内容正文

javascript

JSPatch defineProtocol 实现详解

發(fā)布時(shí)間:2025/3/8 javascript 24 豆豆
生活随笔 收集整理的這篇文章主要介紹了 JSPatch defineProtocol 实现详解 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

遷移老文章到掘金

這是上一篇博客提到的代碼的深入剖析

note:這個(gè)是JSPatch附屬新增的小功能點(diǎn),想要詳細(xì)了解JsPatch整體部分的工作及原理戳這個(gè)wiki JSPatch實(shí)現(xiàn)原理詳解

出發(fā)點(diǎn)

一個(gè)不小心引發(fā)的bad case

工作中遇到了一個(gè)case,有一部分代碼被重構(gòu)了,一個(gè)函數(shù)被徹底的廢棄并且.m文件中的具體函數(shù)實(shí)現(xiàn)已經(jīng)被整體注釋掉了,但是.h文件這個(gè)函數(shù)還存在.

由于被重構(gòu)的那部分在客戶端很多處代碼都有調(diào)用,沒有及時(shí)的替換成最新的函數(shù),導(dǎo)致造成了線上crash,unrecognized selector.

我最開始想用JsPatch發(fā)出一個(gè)hotfix,既然是unrecognized selector,具體的函數(shù)實(shí)現(xiàn)不存在,那么我用JSPatch動(dòng)態(tài)補(bǔ)上這個(gè)函數(shù)實(shí)現(xiàn),就可以封住crash了.

結(jié)果操作后發(fā)現(xiàn),無法實(shí)現(xiàn),原因是.h文件中這個(gè)selector里面有一個(gè)非id類型的參數(shù).

JSPatch只能新增參數(shù)類型為id的方法

在JsPatch的Wiki中defineClass 有一句說明

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

為什么會(huì)這樣,探究其源碼可以發(fā)現(xiàn)

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

當(dāng)使用defineClass對(duì)新方法命名的時(shí)候,defineClass能通過_自動(dòng)識(shí)別參數(shù)的位置和個(gè)數(shù),但是并沒有能識(shí)別參數(shù)的類型。

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

關(guān)于type encode后面會(huì)詳細(xì)解釋

當(dāng)我在嘗試通過JsPatch修復(fù)我的case的時(shí)候,由于我希望新增的方法是一個(gè)含有非id類型參數(shù)的方法,而JsPatch最終添加的新方法的參數(shù)都是id,所以程序運(yùn)行的時(shí)候依然會(huì)crash,因?yàn)樗€是找不到那個(gè)他想要的方法,依然是unrecognized selector

修改思路

知道原因,尋找思路

  • defineClass為覆蓋修改方法而設(shè)計(jì),對(duì)于新增方法,傳入的信息不足,不能生成正確的type encode,所以無法正確的添加任意參數(shù)類型的方法,于是統(tǒng)一設(shè)定為id類型
  • 如果由使用者傳入足夠的信息,借而生成正確的type encode,則我們的目的就可以達(dá)成

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

但是眼下還有2個(gè)問題

  • defineClass在設(shè)計(jì)上,新增方法和覆蓋修改方法走的是同一個(gè)輸入口,單獨(dú)為新增方法而重新調(diào)整輸入接口,會(huì)使代碼邏輯和設(shè)計(jì)模式變化比較大
  • 在用戶已經(jīng)養(yǎng)成的JsPatch編寫習(xí)慣上,新增和覆蓋二者本是統(tǒng)一的,為新增方法而大改defineClass的輸入模式,勢(shì)必會(huì)讓已經(jīng)習(xí)慣使用的用戶有很大不便
  • 尋找一個(gè)合適的方案,能不大范圍影響現(xiàn)在的設(shè)計(jì)模式,又能完成我的想法

defineClass的Protocol

JsPatch的defineClass 中提到的Protocol的作用

可以在定義時(shí)讓一個(gè)類實(shí)現(xiàn)某些 Protocol 接口,寫法跟 OC 一樣:

defineClass("JPViewController: UIViewController<UIScrollViewDelegate, UITextViewDelegate>", {})

這樣做的作用是,當(dāng)添加 Protocol 里定義的方法,而類里沒有實(shí)現(xiàn)的方法時(shí),參數(shù)類型不再全是 id,而是自動(dòng)轉(zhuǎn)為 Protocol 里定義的類型:

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

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

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]);} } 復(fù)制代碼

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

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

那就是設(shè)計(jì)一個(gè)全新的接口defineProtocol,在這個(gè)全新的接口里面輸入足夠多的參數(shù)信息,進(jìn)而通過運(yùn)行時(shí)創(chuàng)建全新的Protocol,創(chuàng)建完成的新Protocol就自然可以借助defineClass里面的功能,引入正確的新增方法

具體實(shí)現(xiàn)

JS接口設(shè)計(jì)

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

defineProtocol('lalalala',{testProtocol: {paramsType:"int, id",returnType:"BOOL"},... }, {... }); 復(fù)制代碼

使用者直接輸入int,float,id,void等,由代碼自動(dòng)識(shí)別生成最終的type encode,而且因?yàn)樽詣?dòng)識(shí)別需代碼進(jìn)行逐一的支持和轉(zhuǎn)換,有些特殊的參數(shù)類型,代碼轉(zhuǎn)換并不能完全覆蓋,于是還添加了一個(gè)可選的參數(shù)typeEncode,一旦自動(dòng)轉(zhuǎn)換無法支持的參數(shù)類型,就可以通過可選參數(shù),需要使用者自己想辦法手寫type encode了,主要無法支持的參數(shù)是用戶自定義的struct

代碼實(shí)現(xiàn)

JS接口這部分實(shí)現(xiàn)就不詳細(xì)描述了,和JSPatch其他接口完全一致,

看下對(duì)比是不是和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);}; 復(fù)制代碼

通過運(yùn)行時(shí)objc_allocateProtocol創(chuàng)建新Protocol,通過protocol_addMethodDescription來為新Protocol增加方法,通過objc_registerProtocol來注冊(cè)新Protocol,這是基本的runtime代碼,不多描述了,源碼里都可以看到

唯一需要注意的是新protocol一經(jīng)注冊(cè)生效objc_registerProtocol,就不可在更改了,所以defineProtocol不能修改已經(jīng)存在的Protocol

protocol_addMethodDescription需要輸入seletorName和type encode,接下來重點(diǎn)說下如何在js返回的字典里識(shí)別這兩個(gè)參數(shù)

識(shí)別selector

如接口設(shè)計(jì)里面的樣例testProtocol,是被當(dāng)做字典中的key,可以直接取出來的,因?yàn)槲覀冊(cè)O(shè)計(jì)defineProtocol中Js新方法的命名和defineClass一致,都是參數(shù)用_代替,原本的_下劃線用__代替,所以解析key這個(gè)字符串的步驟和defineClass也一致

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

識(shí)別type encode

如接口設(shè)計(jì)里面的樣例,參數(shù)會(huì)輸入"int, id"這樣的字符串,返回值會(huì)輸入"void"這樣的字符串,前者再通過,號(hào)拆分成字符串?dāng)?shù)組,就接下來就可以通過代碼獲取了,我打算構(gòu)建一個(gè)有限字符串映射表typeEncodeDic,以type字符串為key,映射int到i這樣。

typeEncodeDic這個(gè)表已經(jīng)構(gòu)建好了,這樣從js傳來的type字符串當(dāng)做key,直接從這個(gè)表里就能get到編碼。

人肉去寫這個(gè)表太low了,怎么也得用酷炫一點(diǎn)的方式支持一下,看到原作者bang,在JsPatch里面風(fēng)騷的宏的用法,我也照貓畫虎了一個(gè)

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); 復(fù)制代碼

JP_DEFINE_TYPE_ENCODE_CASE這個(gè)宏就自動(dòng)的將輸入?yún)?shù)_type通過語法糖@encode()寫入字典,這里面還有一處很nb的地方

宏里面用參數(shù)生成靜態(tài)字符串

這是一個(gè)很trick的地方,原本我的宏是這么設(shè)計(jì)的JP_DEFINE_TYPE_ENCODE_CASE(@"id",id)為什么這么設(shè)計(jì)?因?yàn)槲腋悴欢ㄔ趺丛诤昀飳d轉(zhuǎn)成@“id”,試了很多種方法都不行╮(╯_╰)╭

后來原作者bang交流,他給了解決辦法,@#_type他在JsPatch里已經(jīng)用到了,說他當(dāng)初也遇到一樣的困擾,然后查到的。

所以最終這個(gè)宏被設(shè)計(jì)成了這樣。

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); 復(fù)制代碼

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

處理id類型參數(shù)

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

所以我額外做了一個(gè)處理,當(dāng)從typeEncodeDic表里面找不到對(duì)應(yīng)的key的時(shí)候,就會(huì)NSClassFromString來判斷是否是一個(gè)Oc對(duì)象,如果是自動(dòng)轉(zhuǎn)換為id的類型編碼@

NSString* argencode = [typeEncodeDic objectForKey:argstr]; if (argencode.length <= 0) {Class cls = NSClassFromString(argstr);if ([(id)cls isKindOfClass:[NSObject class]]) {argencode = @"@";} } 復(fù)制代碼

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

paramsType:"id" paramsType:"CustomObject" 復(fù)制代碼

生成SEL的類型編碼

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

- (void) setSomething:(id) anObject 復(fù)制代碼

這個(gè)函數(shù)他的類型編碼是

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

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

代碼中可選參數(shù)typeEncode優(yōu)先級(jí)最高,如果用戶手寫了可選參數(shù),則不會(huì)執(zhí)行代碼自動(dòng)生成,直接使用用戶輸入的typeEncode,生成Protocol。

if (typeEncode) {addMethodToProtocol(protocol, selectorName, typeEncode, isInstance); }else {//type encode string automatic create } 復(fù)制代碼

詳探TypeEncode

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

我們先在代碼中實(shí)現(xiàn)- (void) setSomething:(id) anObject這個(gè)方法,然后使用下面的代碼,就能通過系統(tǒng)取出SEL的typeEncode

Class cls = self.class; SEL selstr = NSSelectorFromString(@"setSomething:"); Method method = class_getInstanceMethod(cls, selstr); const char* type = method_getTypeEncoding(method); 復(fù)制代碼

經(jīng)過系統(tǒng)的讀取,驚訝的發(fā)現(xiàn),系統(tǒng)算出來的type居然是v12@0:4@8,這他喵的一堆數(shù)字是什么鬼!,剛才不是說v@:@嘛????????!!!!!!

經(jīng)過我反復(fù)地測(cè)試,發(fā)現(xiàn)無論是輸入v12@0:4@8還是v@:@,Protocol都能正常的生成,一點(diǎn)區(qū)別也沒有,完全不影響使用,但是他喵的為什么系統(tǒng)就會(huì)多出來這么多數(shù)字?

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

和gitHub上的@DevSonw聊,覺得這可能是一個(gè)字節(jié)補(bǔ)齊的過程,并不影響使用

總結(jié)

以上是生活随笔為你收集整理的JSPatch defineProtocol 实现详解的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。