Runtime原理探究参考另一篇文章 Runtime原理探究, 本文主要整理Runtime在实际开发中的应用.
一. 交换方法(method swizzing)
涉及到的方法load
class_getInstanceMethod
class_getClassMethod
method_exchangeImplementations
method_getImplementation
class_addMethod
class_replaceMethod
开发使用场景
:系统自带的方法功能不够,给系统自带的方法扩展一些功能,并且保持原有的功能。方式一
:继承系统的类,重写方法.方式二
:使用Runtime,交换方法.1234567891011121314151617181920212223242526272829303132333435363738394041424344@implementation ViewController- (void)viewDidLoad {[super viewDidLoad];// Do any additional setup after loading the view, typically from a nib.// 需求:给imageNamed方法提供功能,每次加载图片就判断下图片是否加载成功。// 步骤一:先搞个分类,定义一个能加载图片并且能打印的方法+ (instancetype)imageWithName:(NSString *)name;// 步骤二:交换imageNamed和imageWithName的实现,就能调用imageWithName,间接调用imageWithName的实现。UIImage *image = [UIImage imageNamed:@"123"];}@end@implementation UIImage (Image)// 加载分类到内存的时候调用+ (void)load{// 交换方法// 获取imageWithName方法地址Method imageWithName = class_getClassMethod(self, @selector(imageWithName:));// 获取imageWithName方法地址Method imageName = class_getClassMethod(self, @selector(imageNamed:));// 交换方法地址,相当于交换实现方式method_exchangeImplementations(imageWithName, imageName);}// 不能在分类中重写系统方法imageNamed,因为会把系统的功能给覆盖掉,而且分类中不能调用super.// 既能加载图片又能打印+ (instancetype)imageWithName:(NSString *)name{// 这里调用imageWithName,相当于调用imageNameUIImage *image = [self imageWithName:name];if (image == nil) {NSLog(@"加载空的图片");}return image;}@end
Objective-C中的Runtime LeeJay
1.如果我现在想检查一下项目中有没有内存循环,怎么办?是不是要重写
dealloc
函数,看下dealloc
有没有执行,项目小的时候,一个一个controlle
r的写,还不麻烦,如果项目大,要是一个一个的写,估计你会疯掉的。这时候方法交换就派上用场了,你就可以尝试用自己的方法交换系统的dealloc
方法,几句代码就搞定了。12345678910111213141516171819202122#import "UIViewController+Dealloc.h"#import <objc/runtime.h>@implementation UIViewController (Dealloc)+ (void)load{static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{Method method1 = class_getInstanceMethod(self, NSSelectorFromString(@"dealloc"));Method method2 = class_getInstanceMethod(self, @selector(my_dealloc));method_exchangeImplementations(method1, method2);});}- (void)my_dealloc{NSLog(@"%@销毁了", self);[self my_dealloc];}@end2.数组越界,向数组中添加一个nil对象等等,都会造成闪退,我们可以用自己的方法交换数组相对应的方法。下面是一个交换数组addObject:方法的栗子:
123456789101112131415161718192021222324252627282930313233343536373839404142434445#import "NSMutableArray+Category.h"#import <objc/runtime.h>@implementation NSMutableArray (Category)+ (void)load{static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{SEL originalSelector = @selector(addObject:);SEL swizzledSelector = @selector(lj_AddObject:);// NSMutableArray是类簇,真正的类名是__NSArrayMMethod originalMethod = class_getInstanceMethod(objc_getClass("__NSArrayM"), originalSelector);Method swizzledMethod = class_getInstanceMethod(objc_getClass("__NSArrayM"), swizzledSelector);BOOL didAddMethod = class_addMethod(self,originalSelector,method_getImplementation(swizzledMethod),method_getTypeEncoding(swizzledMethod));if (didAddMethod){class_replaceMethod(self,swizzledSelector,method_getImplementation(originalMethod),method_getTypeEncoding(originalMethod));}else{method_exchangeImplementations(originalMethod, swizzledMethod);}});}- (void)lj_AddObject:(id)object{if (object != nil){[self lj_AddObject:object];}}@end
PS:我不太建议大家平时开发的时候使用这类数组安全操作的做法,不利于代码的调试,如果真的加入了nil对象,你可能就不会那么容易找出问题在哪,还是在项目发布的时候使用比较合适。
Runtime Method Swizzling开发实例汇总(持续更新中) 卖报的小画家Sure
Method Swizzling通用方法封装
在列举之前,我们可以将Method Swizzling功能封装为类方法,作为NSObject的类别,这样我们后续调用也会方便些。
|
|
- 为什么要添加didAddMethod判断?
先尝试添加原SEL其实是为了做一层保护,因为如果这个类没有实现originalSelector,但其父类实现了,那class_getInstanceMethod会返回父类的方法。这样method_exchangeImplementations替换的是父类的那个方法,这当然不是我们想要的。所以我们先尝试添加 orginalSelector,如果已经存在,再用 method_exchangeImplementations 把原方法的实现跟新的方法实现给交换掉。
如果理解还不够透彻,我们可以进入runtime.h中查看class_addMethod源码解释:123456789101112131415/*** Adds a new method to a class with a given name and implementation.** @param cls The class to which to add a method.* @param name A selector that specifies the name of the method being added.* @param imp A function which is the implementation of the new method. The function must take at least two arguments—self and _cmd.* @param types An array of characters that describe the types of the arguments to the method.** @return YES if the method was added successfully, otherwise NO* (for example, the class already contains a method implementation with that name).** @note class_addMethod will add an override of a superclass's implementation,* but will not replace an existing implementation in this class.* To change an existing implementation, use method_setImplementation.*/
大概的意思就是我们可以通过class_addMethod为一个类添加方法(包括方法名称(SEL)和方法的实现(IMP)),返回值为BOOL类型,表示方法是否成功添加。 需要注意的地方是class_addMethod会添加一个覆盖父类的实现,但不会取代原有类的实现。 也就是说如果class_addMethod返回YES,说明子类中没有方法originalSelector,通过class_addMethod为其添加了方法originalSelector,并使其实现(IMP)为我们想要替换的实现。
同时再将原有的实现(IMP)替换到swizzledMethod方法上,
从而实现了方法的交换,并且未影响父类方法的实现。反之如果class_addMethod返回NO,说明子类中本身就具有方法originalSelector的实现,直接调用交换即可。
实例一:替换ViewController生命周期方法
示当前请求情况或进度。这种界面都会存在这样一个问题,在请求较慢时,用户手动退出界面,这时候需要去除加载栏。
当然可以依次在每个界面的viewWillDisappear方法中添加去除方法,但如果类似的界面过多,一味的复制粘贴也不是方法。这时候就能体现Method Swizzling的作用了,我们可以替换系统的viewWillDisappear方法,使得每当执行该方法时即自动去除加载栏。
代码如上,这样就不用考虑界面是否移除加载栏的问题了。补充一点,通常我们也会在生命周期方法中设置默认界面背景颜色,因若背景颜色默认为透明对App的性能也有一定影响,这大家可以在UIKit性能优化那篇文章中查阅。但类似该类操作也可以书写在通用类中,所以具体使用还要靠自己定夺。
实例二:解决获取索引、添加、删除元素越界崩溃问题
对于NSArray、NSDictionary、NSMutableArray、NSMutableDictionary不免会进行索引访问、添加、删除元素的操作,越界问题也是很常见,这时我们可以通过Method Swizzling解决这些问题,越界给予提示防止崩溃。
这里以NSMutableArray为例说明
对应大家可以举一反三,相应的实现添加、删除等,以及NSArray、NSDictionary等操作,因代码篇幅较大,这里就不一一书写了。
这里没有使用self来调用,而是使用objc_getClass(“__NSArrayM”)来调用的。因为NSMutableArray的真实类只能通过后者来获取,而不能通过[self class]来获取,而method swizzling只对真实的类起作用。这里就涉及到一个小知识点:类簇。补充以上对象对应类簇表。
实例三:防止按钮重复暴力点击
程序中大量按钮没有做连续响应的校验,连续点击出现了很多不必要的问题,例如发表帖子操作,用户手快点击多次,就会导致同一帖子发布多次。
|
|
实例四:全局更换控件初始效果
以UILabel为例,在项目比较成熟的基础上,应用中需要引入新的字体,需要更换所有Label的默认字体,但是同时,对于一些特殊设置了字体的label又不需要更换。乍看起来,这个问题确实十分棘手,首先项目比较大,一个一个设置所有使用到的label的font工作量是巨大的,并且在许多动态展示的界面中,可能会漏掉一些label,产生bug。其次,项目中的label来源并不唯一,有用代码创建的,有xib和storyBoard中的,这也将浪费很大的精力。这时Method Swizzling可以解决此问题,避免繁琐的操作。
这一实例个人认为使用率可能不高,对于产品的设计这些点都是已经确定好的,更改的几率很低。况且我们也可以使用appearance来进行统一设置。
实例六:App异常加载占位图通用类封装(更新于:2016/12/01)
详情可见文章:《零行代码为App添加异常加载占位图》
在该功能模块中,使用Runtime Method Swizzling进行替换tableView、collectionView的reloadData方法,使得每当执行刷新操作时,自动检测当前组数与行数,从而实现零代码判断占位图是否显示的功能,同样也适用于网络异常等情况,详细设置可前往阅读。
实例七:全局修改导航栏后退(返回)按钮(更新于:2016/12/05)
在真实项目开发中,会全局统一某控件样式,以导航栏后退(返回)按钮为例,通常项目中会固定为返回字样,或者以图片进行显示等。
iOS默认的返回按钮样式如下,默认为蓝色左箭头,文字为上一界面标题文字。
这里我们仍可以通过Runtime Method Swizzling来实现该需求,在使用Method Swizzling进行更改之前,必须考虑注意事项,即尽可能的不影响原有操作,比如对于系统默认的返回按钮,与其对应的是有界面边缘右滑返回功能的,因此我们进行统一更改后不可使其功能废弃。
闲话少说,我们创建基于UINavigationItem的类别,在其load方法中替换方法backBarButtonItem
代码如下
这里进行将返回按钮的文字清空操作,其他需求样式大家也可随意替换,现在再次运行程序,就会发现所有的返回按钮均只剩左箭头,并右滑手势依然有效。如图所示
Runtime基本知识点以及应用场景 deft_mkjing
交换方法实现的需求场景:自己创建了一个功能性的方法,在项目中多次被引用,当项目的需求发生改变时,要使用另一种功能代替这个功能,要求是不改变旧的项目(也就是不改变原来方法的实现)。
可以在类的分类中,再写一个新的方法(是符合新的需求的),然后交换两个方法的实现。这样,在不改变项目的代码,而只是增加了新的代码 的情况下,就完成了项目的改进。
交换两个方法的实现一般写在类的load方法里面,因为load方法会在程序运行前加载一次,而initialize方法会在类或者子类在 第一次使用的时候调用,当有分类的时候会调用多次
二. 给分类添加属性
涉及到的方法:objc_getAssociatedObject
objc_setAssociatedObject
让你快速上手Runtime 袁峥
原理:给一个类声明属性,其实本质就是给这个类添加关联,并不是直接把这个值的内存空间添加到类存空间。
在分类只能对原类扩充方法, 并不能扩充属性, 你可以创建一个分类, 然后在分类中敲几个@property, 然后用第二节的方法打印下原类的property看看存不存在? 答案显然是不存在这个属性.
那么我们可以使用runtime中的一个叫关联对象的办法, 给分类添加一个property, 并且打印原类的property列表是真真切切存在的. 上代码
|
|
好的, 我们看看加了这个分类之后再利用第二节的办法打印下瞧瞧~
|
|
看到了嘛? speed这个属性乖乖的在那儿呢.
其实关联对象这个技术就是用哈希表实现的, 将一个类映射到一张哈希表上, 然后根据key找到关联对象, 所以严格说, 关联对象跟本类没有任何联系, 它不是储存在类的内部的. 它的底层原理就不多介绍了, 不属于本文的范畴, 大家感兴趣的可以到以下两篇文章里面看看
Objective-C Associated Objects 的实现原理
Objective-C中的Runtime LeeJay
类别不可以添加属性,我们可以在类别中设置关联,举个栗子:
Person+Category.h 文件
Person+Category.m 文件
|
|
当然你也可以这么写
Person+Category.m 文件
|
|
objc_setAssociatedObject
和objc_getAssociatedObject
传入的参数key:要求是唯一并且是常量,可以使用static char,然而一个更简单方便的方法就是:使用选择子。由于选择子是唯一并且是常量,你可以使用选择子作为关联的key。(PS:_cmd表示当前调用的方法,它就是一个方法选择器SEL,类似self表示当前对象)
Objecive-C runtime实践-给Category添加属性
在开发中经常遇到需要添加hud的情形,每次添加的代码都在10行左右,遂新建一个ViewController的Category来添加hud。由于分类不能直接添加属性,就考虑到了runtime。
下面讲具体的实施步骤
新建工程--新建文件,选择Objective-C File 如下图:
导入MBProgressHUD, 现在项目结构如图:
在UIViewController+HUD.h中导入MBProgressHUD.h,
|
|
在UIViewController+HUD.m中导入runtime.h,并添加hud属性:
|
|
接下来是关键的一步-设置hud的setter与getter方法:
在setter中设置连接, 在getter中初始化。
好了,接下来就可以正常使用属性了,现在,我们对hud进行扩展。
写两个基本的show、hide方法,其余的实现在其基础上变化即可:
再实现下面的方法大概就够用了:
具体的实现直接看代码吧!
https://github.com/Xigtun/RuntimeDemo
防止按钮重复暴力点击
同上
按钮防止被重复点击的方法 (iOS)
避免一个button被多次点击(共总结了3种)
第一种:每次在点击时先取消之前的操作
将这段代码放在你按钮点击的方法中,例如:
第二种:点击后设为不可被点击的状态,几秒后恢复:
第三种:使用runtime,一劳永逸我这设的是0.5秒内不会被重复点击
1.导入objc / runtime.h(可以放在PCH文件里)
2.创建uicontrol或UIButton的的分类!
创建分类文件:
2.1 打开Xcode中,新建文件,选择OC文件
2.2 在第二个界面,File名为UIControl+UIControl_buttonCon,将文件类型File Type选为Category类,在类里选继承的类别,这里咱们选的Class是UIButton
注:若用Unbutton分类,则会对对Unbutton创建的按钮反应。
2.3 分类创建完毕对分类进行操作
.h文件
.m文件
三. 字典转模型
涉及到的方法:class_copyIvarList
ivar_getName
ivar_getTypeEncoding
模型属性,通常需要跟字典中的key一一对应,提供一个分类,专门根据字典生成对应的属性字符串。
字典转模型的方式一:KVC
1234567891011121314- @implementation Status+ (instancetype)statusWithDict:(NSDictionary *)dict{Status *status = [[self alloc] init];[status setValuesForKeysWithDictionary:dict];return status;}@endKVC字典转模型弊端
:必须保证,模型中的属性和字典中的key一一对应。- 如果不一致,就会调用
[<Status 0x7fa74b545d60> setValue:forUndefinedKey:]
报key找不到的错。 - 分析:模型中的属性和字典的key不一一对应,系统就会调用
setValue:forUndefinedKey:
报错。 解决:重写对象的
setValue:forUndefinedKey:
,把系统的方法覆盖,
就能继续使用KVC,字典转模型了。1234- (void)setValue:(id)value forUndefinedKey:(NSString *)key{}字典转模型的方式二:Runtime
思路:利用运行时,遍历模型中所有属性,根据模型的属性名,去字典中查找key,取出对应的值,给模型的属性赋值。
- 步骤:提供一个NSObject分类,专门字典转模型,以后所有模型都可以通过这个分类转。
|
|
字典转模型
利用Runtime,遍历模型中所有成员变量,根据模型的属性名,去字典中查找key,取出对应的value,给模型的属性赋值,实现的思路主要借鉴MJExtension。
NSObject+Property.h文件:
NSObject+Property.m文件:
|
|
参考
Objective-C的hook方案(一): Method Swizzling
iOS开发进阶
Effective Objective-C 2.0
附上本文的所有demo下载链接,【GitHub】
作者:LeeJay
链接:http://www.jianshu.com/p/3e050ec3b759
根据字典自动生成属性代码
http://www.jianshu.com/p/2ad0ebfd5a63
日常开发中,我们拿到接口文档,会根据接口返回的数据来写模型。
在之前我都是根据返回的字典一个个key这样对照着来创建模型属性。后面遇到项目中有时候返回的数据里面要写成模型属性的key实在是太多,写起来花时间容易写错又没什么技术含量。
这时候就应该动用开发人员该有的程序思想了,干脆让它自动生成不就好了。废话不多说上代码。
这个方法利用NSDictionary类里面的这个方法帮我们遍历字典里面所有的key和value,期间要做的事情写到block中,也就是帮我们自动生成属性代码。
至于怎么用,可以写成NSDictionary的一个分类。然后用字典对象直接调用就好。像这样[dict createPropertyCode]。就可以帮我们打印出属性代码,然后复制粘贴就好。在面对属性超多的模型时,是不是方便许多了。
当然你也可以根据需要做一些调整,这里也只是提供一个开发中的小技巧,让机器帮我们做事往往
8.使用Runtime和KVC字典转模型
上面就是转换的核心代码,分析下主要功能参数
1.通过class_copyIvarList拿到属性列表的数组,ivargetName这方法拿到属性C类型字符去掉,转换成OC
2.这里会有个问题,如果自己建的model字段和Json返回的字段完全一致,那么就问题不大,但是由于可读性的关系,我们一般都会做一次映射,这就是replaceDict存在的意义,用例如下:
当你的属性名字是SubName,但是Json返回的字典key是sub_name,显然是不同的,需要映射,我们根据runtime拿到的key也是SubName,那么你根据字典取值,就会出现空值的问题,因此
replaceDict就用到了@{@”SubName”:@”sub_name”},只要映射好传进去,我们里面就能进一步做判断了
3.直接看代码注释
这里的F,D什么类型可以参考官方文档类型type类型文档
理解了字段和原理,调用代码如下
四. 动态添加一个类
涉及方法 objc_allocateClassPair
class_addIvar
objc_registerClassPair
class_getInstanceVariable
objc_disposeClassPair
class_addMethod
performSelector
所有runtime代码都是基于C的函数, 所以要用到runtime的函数必须导入
或者
就像KVO一样, 系统是在程序运行的时候根据你要监听的类, 动态添加一个新类继承自该类, 然后重写原类的setter方法并在里面通知observer的.
运行结果为
|
|
这样, 我们就在程序运行时动态添加了一个继承自NSObject的GoodPerson类, 并为该类添加了name和age成员变量. 这里我们需要注意的是, 添加成员变量的class_addIvar
方法必须要在objc_allocateClassPair
和objc_registerClassPair
之间调用才行, 这里涉及到OC中类的成员变量的偏移量, 如果在类注册之后再addIvar的话会破坏原来类成员变量正确的偏移量, 这样的话会导致你访问的那个成员变量并不是你想访问的成员变量, 如图 :
大家可以试试把class_addIvar
方法放在objc_registerClassPair
方法之后执行, 看看会发生什么? (用KVC赋值和取值直接报错, 用getIvar的话取值为null)
一、动态的创建一个类
五. 打印一个类的所有ivar, property 和 method
这个还是比较简单的, 应该直接看代码都能看懂
打印结果为 :
前面2节主要是熟悉runtime的函数调用, 毕竟有许多函数前缀objc
, class
, object
等等. 其实这里面也有规律 :
objc_
: 高于类的操作, 例如添加类, 注册类, 销毁类还有许多高于一个类本身的操作一般都是objc开头class
: 对类的内部进行修改的, 例如添加ivar, 添加property, 添加method等等object
: 对某个对象进行修改, 例如设置ivar值, 获取ivar值, 设置property值, 获取property值, 调用某个method等等ivar
,property
,method
: 这三个方法大家可以手动去敲敲看一看
六. 动态添加方法
涉及方法:performSelector
resolveInstanceMethod
class_addMethod
开发使用场景
:如果一个类方法非常多,加载类到内存的时候也比较耗费资源,需要给每个方法生成映射表,可以使用动态给某个类,添加方法解决。经典面试题
:有没有使用performSelector,其实主要想问你有没有动态添加过方法。简单使用
1234567891011121314151617181920212223242526272829303132333435363738394041424344@implementation ViewController- (void)viewDidLoad {[super viewDidLoad];// Do any additional setup after loading the view, typically from a nib.Person *p = [[Person alloc] init];// 默认person,没有实现eat方法,可以通过performSelector调用,但是会报错。// 动态添加方法就不会报错[p performSelector:@selector(eat)];}@end@implementation Person// void(*)()// 默认方法都有两个隐式参数,void eat(id self,SEL sel){NSLog(@"%@ %@",self,NSStringFromSelector(sel));}// 当一个对象调用未实现的方法,会调用这个方法处理,并且会把对应的方法列表传过来.// 刚好可以用来判断,未实现的方法是不是我们想要动态添加的方法+ (BOOL)resolveInstanceMethod:(SEL)sel{if (sel == @selector(eat)) {// 动态添加eat方法// 第一个参数:给哪个类添加方法// 第二个参数:添加方法的方法编号// 第三个参数:添加方法的函数实现(函数地址)// 第四个参数:函数的类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmdclass_addMethod(self, @selector(eat), eat, "v@:");}return [super resolveInstanceMethod:sel];}@end
动态添加方法实现
好了, 绕来绕去又回到了runtime强大的消息转发身上了, 当一个方法没有实现的时候, OC会怎么做的呢? 还记得那四个步骤吗, 不记得也没关系, 我们看代码!
程序运行结果 :
ps : 消息转发的另外3个方法会在下文放上, 因为本例子用不上所以就不放上来了
二、动态的给某个类添加方法
动态的给某个类添加方法,
class_addMethod
的参数:
self:给哪个类添加方法
sel:添加方法的方法编号(选择子)
IMP:添加方法的函数实现(函数地址)
types 函数的类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd
动态添加方法
通常做法都是在resolve方法内部指定sel的IMP,前提是该方法未实现会被拦截下来,就能实现动态创建的过程
|
|
七. 消息转发
动态消息转发
和上面一样我们创建的对象调用未实现的方法时,类和实例变量的内部是可以这样进行转发的
- 先在这里拦截resolveInstanceMethod
- 第一步未动态添加的话就调用forwardingTargetForSelector
- 第二步返回nil来到这里调用methodSignatureForSelector签名
- 重定向消息指针,实现消息转发
- 如果没有实现第四步就doesNotRecognizeSelector异常
八. 更换方法调用者
试想一下, 一个腿部残疾的人, 他想跑, runtime知道他自己跑不了, 于是就让他的狗替代他去跑了(person没有run方法的声明和实现, dog有run方法的声明和实现)
那么我们通过((void(*)(id, SEL))objc_msgSend)((id)p, @selector(run)); // 这里强转是为了不让编译器报参数过多的错误方法
调用person的run方法, 得到的输出为 :
同样, 其实可以在第二部就把这件事做了, 只需返回dog实例即可, 大家可以亲手操作试试
九. 更改特定方法的实现
一条狗在吃着骨头, 然后他的主人一把把一个球扔得远远的, 碍于主人的淫威之下, 狗就不得不停下来跑去捡球了(更改[dog eat]方法的实现为[dog run]
)
|
|
((void(*)(id, SEL))objc_msgSend)((id)dog, @selector(eat));
的输出结果为 :
demo在这里
Github
- 动态添加一个类
- 打印一个类的所有ivar, property 和 method
- 给分类增加属性
- 动态添加方法实现
- 更换方法调用者
- 更改特定方法的实现
十. 归档
涉及方法:class_copyIvarList
ignoredIvarNames
objc_setAssociatedObject
objc_getAssociatedObject
五、归档
大家都知道在归档的时候,需要先将属性一个一个的归档,然后再将属性一个一个的解档,3-5个属性还好,假如100个怎么办,那不得写累死。有了Runtime,就不用担心这个了,下面就是如何利用Runtime实现自动归档和解档。
NSObject+Archive.h文件:
NSObject+Archive.m文件:
|
|
然后再去需要归档的类实现文件里面写上这几行代码:
|
|
这几行代码都是固定写法,你也可以把它们定义成宏,这样就可以实现一行代码就归档了,思路源自MJExtension!
Runtime基本知识点以及应用场景 deft_mkjing
runtime实现归档和解档
当外部调用上面归档解档的代码时会走如下方法
十一
轻松学习之二——iOS利用Runtime自定义控制器POP手势动画 J_雨
利用runtime获取系统手势的target和actionclass_copyIvarList
十二
runtime运用 -从一个模型中找出所有属性赋值给另外一个模型 醋溜草莓便当
objc_property_t
class_copyPropertyList
property_getName
十三
热修复 JSPatch