迁移老文章到掘金

这是上一篇博客提到的代码的深入剖析

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,映射inti这样。

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 实现详解相关推荐

  1. JSPatch实现原理详解:让JS调用/替换任意OC方法

    JSPatch实现原理详解:让JS调用/替换任意OC方法 2015-07-10 09:05 编辑: suiling 分类:iOS开发 来源:bang JSPatch以小巧的体积做到了让JS调用/替换任 ...

  2. 【iOS沉思录】如何招聘一个靠谱的 iOS程序员+面试题详解

    说明:面试题来源是微博@我就叫Sunny怎么了的这篇博文:<招聘一个靠谱的 iOS>,其中共55题,除第一题为纠错题外,其他54道均为简答题. 出题者简介: 孙源(sunnyxx),目前就 ...

  3. mysql udf提权原理_udf提权原理详解

    0x00-前言 这个udf提权复现搞了三天,终于搞出来了.网上的教程对于初学者不太友好,以至于我一直迷迷糊糊的,走了不少弯路.下面就来总结一下我的理解. 想要知道udf提权是怎么回事,首先要先知道ud ...

  4. iOS中的HotFix方案总结详解

    iOS中的HotFix方案总结详解 相信HotFix大家应该都很熟悉了,今天主要对于最近调研的一些方案做一些总结.iOS中的HotFix方案大致可以分为四种: WaxPatch(Alibaba) Dy ...

  5. 从命令行到IDE,版本管理工具Git详解(远程仓库创建+命令行讲解+IDEA集成使用)

    首先,Git已经并不只是GitHub,而是所有基于Git的平台,只要在你的电脑上面下载了Git,你就可以通过Git去管理"基于Git的平台"上的代码,常用的平台有GitHub.Gi ...

  6. JVM年轻代,老年代,永久代详解​​​​​​​

    秉承不重复造轮子的原则,查看印象笔记分享连接↓↓↓↓ 传送门:JVM年轻代,老年代,永久代详解 速读摘要 最近被问到了这个问题,解释的不是很清晰,有一些概念略微模糊,在此进行整理和记录,分享给大家.在 ...

  7. docker常用命令详解

    docker常用命令详解 本文只记录docker命令在大部分情境下的使用,如果想了解每一个选项的细节,请参考官方文档,这里只作为自己以后的备忘记录下来. 根据自己的理解,总的来说分为以下几种: Doc ...

  8. 通俗易懂word2vec详解词嵌入-深度学习

    https://blog.csdn.net/just_so_so_fnc/article/details/103304995 skip-gram 原理没看完 https://blog.csdn.net ...

  9. 深度学习优化函数详解(5)-- Nesterov accelerated gradient (NAG) 优化算法

    深度学习优化函数详解系列目录 深度学习优化函数详解(0)– 线性回归问题 深度学习优化函数详解(1)– Gradient Descent 梯度下降法 深度学习优化函数详解(2)– SGD 随机梯度下降 ...

最新文章

  1. Add margining capability to a dc/dc converter
  2. 设计模式之 Singleton 单例模式
  3. 机器学习Sklearn实战——手写线性回归
  4. ASP.NET 4.0升级至ASP.NET 4.5需要注意的地方
  5. 怎么取消苹果手机自动续费_知乎会员怎样取消自动续费
  6. win8计算机丢失xinput1+3.dll,xinput1 3.dll丢失怎么办 win8下xinput1 3.dll丢失解决方法
  7. python中linspace函数_numpy.linspace函数具体使用详解
  8. ipmitool介绍_ipmitool命令行使用详解
  9. 四步相移法怎么获得相位信息_不一样的费曼学习法!|高中篇|”
  10. web.xml中的主要元素说明(listener, filter, servlet)
  11. Android使用Aspectj(AOP)
  12. 201771010112罗松《面向对象程序设计(java)》第十周学习总结
  13. talk record
  14. 【word】如何在word宏里面写vb代码选中所有表格
  15. 一个月攻克托业--复旦大学考生
  16. VMware虚拟机转换为kvm虚拟机
  17. 使用OpenCV进行人脸检测和戴墨镜特效实战(附Python源码)
  18. java自动化测试语言高级之HashMap
  19. 17.战略管理.组织级项目管理.项目集.项目组合.量化项目管理
  20. 【RS-M1系列 - 1】Windows下使用RSView查看点云

热门文章

  1. 如何测试机房的速度和带宽?
  2. 未曾秋高气爽,亦然爬山去也
  3. Endnote X9安装教程
  4. 光流 | 稠密光流估计(基于LK光流)(源代码分享)
  5. Ubuntu | 使用 SecureCRT 远程登录 Ubuntu
  6. HTML+CSS+JavaScript复习笔记持更(五)——CSS选择器
  7. 易语言单窗口单ip软件源码_好人多窗口同步器:多台电脑同步视频演示
  8. Scikit-Learn 常用函数
  9. opencv精要(3)-win下codelite的opencv配置
  10. linux内核杂记(12)-进程调度(7)