JSPatch defineProtocol 实现详解
迁移老文章到掘金
这是上一篇博客提到的代码的深入剖析
note:这个是JSPatch附属新增的小功能点,想要详细了解JsPatch整体部分的工作及原理戳这个wiki JSPatch实现原理详解
出发点
一个不小心引发的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,专门在新增方法处开新的接口传入参数,从而使得一切信息都能到手,正常生成正确的新方法。
但是眼下还有2个问题
- 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 < 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其他接口完全一致,
看下对比是不是和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返回的字典里识别这两个参数
识别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] > 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 <= 0) {Class cls = NSClassFromString(argstr);if ([(id)cls isKindOfClass:[NSObject class]]) {argencode = @"@";}
}
复制代码
这样无论用户输入类名
还是id
,我这边的处理都是完全一样,等效的
paramsType:"id"
paramsType:"CustomObject"
复制代码
生成SEL的类型编码
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
我们可以手写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聊,觉得这可能是一个字节补齐的过程,并不影响使用
JSPatch defineProtocol 实现详解相关推荐
- JSPatch实现原理详解:让JS调用/替换任意OC方法
JSPatch实现原理详解:让JS调用/替换任意OC方法 2015-07-10 09:05 编辑: suiling 分类:iOS开发 来源:bang JSPatch以小巧的体积做到了让JS调用/替换任 ...
- 【iOS沉思录】如何招聘一个靠谱的 iOS程序员+面试题详解
说明:面试题来源是微博@我就叫Sunny怎么了的这篇博文:<招聘一个靠谱的 iOS>,其中共55题,除第一题为纠错题外,其他54道均为简答题. 出题者简介: 孙源(sunnyxx),目前就 ...
- mysql udf提权原理_udf提权原理详解
0x00-前言 这个udf提权复现搞了三天,终于搞出来了.网上的教程对于初学者不太友好,以至于我一直迷迷糊糊的,走了不少弯路.下面就来总结一下我的理解. 想要知道udf提权是怎么回事,首先要先知道ud ...
- iOS中的HotFix方案总结详解
iOS中的HotFix方案总结详解 相信HotFix大家应该都很熟悉了,今天主要对于最近调研的一些方案做一些总结.iOS中的HotFix方案大致可以分为四种: WaxPatch(Alibaba) Dy ...
- 从命令行到IDE,版本管理工具Git详解(远程仓库创建+命令行讲解+IDEA集成使用)
首先,Git已经并不只是GitHub,而是所有基于Git的平台,只要在你的电脑上面下载了Git,你就可以通过Git去管理"基于Git的平台"上的代码,常用的平台有GitHub.Gi ...
- JVM年轻代,老年代,永久代详解
秉承不重复造轮子的原则,查看印象笔记分享连接↓↓↓↓ 传送门:JVM年轻代,老年代,永久代详解 速读摘要 最近被问到了这个问题,解释的不是很清晰,有一些概念略微模糊,在此进行整理和记录,分享给大家.在 ...
- docker常用命令详解
docker常用命令详解 本文只记录docker命令在大部分情境下的使用,如果想了解每一个选项的细节,请参考官方文档,这里只作为自己以后的备忘记录下来. 根据自己的理解,总的来说分为以下几种: Doc ...
- 通俗易懂word2vec详解词嵌入-深度学习
https://blog.csdn.net/just_so_so_fnc/article/details/103304995 skip-gram 原理没看完 https://blog.csdn.net ...
- 深度学习优化函数详解(5)-- Nesterov accelerated gradient (NAG) 优化算法
深度学习优化函数详解系列目录 深度学习优化函数详解(0)– 线性回归问题 深度学习优化函数详解(1)– Gradient Descent 梯度下降法 深度学习优化函数详解(2)– SGD 随机梯度下降 ...
最新文章
- Add margining capability to a dc/dc converter
- 设计模式之 Singleton 单例模式
- 机器学习Sklearn实战——手写线性回归
- ASP.NET 4.0升级至ASP.NET 4.5需要注意的地方
- 怎么取消苹果手机自动续费_知乎会员怎样取消自动续费
- win8计算机丢失xinput1+3.dll,xinput1 3.dll丢失怎么办 win8下xinput1 3.dll丢失解决方法
- python中linspace函数_numpy.linspace函数具体使用详解
- ipmitool介绍_ipmitool命令行使用详解
- 四步相移法怎么获得相位信息_不一样的费曼学习法!|高中篇|”
- web.xml中的主要元素说明(listener, filter, servlet)
- Android使用Aspectj(AOP)
- 201771010112罗松《面向对象程序设计(java)》第十周学习总结
- talk record
- 【word】如何在word宏里面写vb代码选中所有表格
- 一个月攻克托业--复旦大学考生
- VMware虚拟机转换为kvm虚拟机
- 使用OpenCV进行人脸检测和戴墨镜特效实战(附Python源码)
- java自动化测试语言高级之HashMap
- 17.战略管理.组织级项目管理.项目集.项目组合.量化项目管理
- 【RS-M1系列 - 1】Windows下使用RSView查看点云
热门文章
- 如何测试机房的速度和带宽?
- 未曾秋高气爽,亦然爬山去也
- Endnote X9安装教程
- 光流 | 稠密光流估计(基于LK光流)(源代码分享)
- Ubuntu | 使用 SecureCRT 远程登录 Ubuntu
- HTML+CSS+JavaScript复习笔记持更(五)——CSS选择器
- 易语言单窗口单ip软件源码_好人多窗口同步器:多台电脑同步视频演示
- Scikit-Learn 常用函数
- opencv精要(3)-win下codelite的opencv配置
- linux内核杂记(12)-进程调度(7)