这个库就是限流的那个库,他能限制一个方法的调用,在一段时间内无论你调几次都只执行一次~ 地址:https://github.com/yulingtianxia/MessageThrottle
使用也挺简单的:
Stub *s = [Stub new];
// You can also assign `Stub.class` or `mt_metaClass(Stub.class)` to `target` argument.
MTRule *rule = [[MTRule alloc] initWithTarget:s selector:@selector(foo:) durationThreshold:0.01];
rule.mode = MTModePerformLast; // Or `MTModePerformFirstly`, ect
[rule apply];
所以其实限流就是在创建一个rule然后apply的时候做的~
所以先来看看apply:
- (BOOL)apply
{
return [MTEngine.defaultEngine applyRule:self];
}
这里可以看到一个MTEngine.defaultEngine
单例~ 先看看它是啥~
@interface MTEngine ()
@property (nonatomic) NSMapTable<id, NSMutableSet<NSString *> *> *targetSELs;
@property (nonatomic) NSMutableSet<Class> *classHooked;
- (void)discardRule:(MTRule *)rule whenTargetDealloc:(MTDealloc *)mtDealloc;
@end
@implementation MTEngine
static pthread_mutex_t mutex;
NSString * const kMTPersistentRulesKey = @"kMTPersistentRulesKey";
这里MTEngine
是单例,它有两个用于存储限制什么对象的什么方法调用频次的属性,一个是NSMapTable
类型的targetSELs
,一个是NSMutableSet
类型的classHooked
。这俩我们可以先不看,只看持久化的部分。
+ (void)load
{
NSArray<NSData *> *array = [NSUserDefaults.standardUserDefaults objectForKey:kMTPersistentRulesKey];
for (NSData *data in array) {
if (@available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, *)) {
NSError *error = nil;
MTRule *rule = [NSKeyedUnarchiver unarchivedObjectOfClass:MTRule.class fromData:data error:&error];
if (error) {
NSLog(@"%@", error.localizedDescription);
}
else {
[rule apply];
}
} else {
@try {
MTRule *rule = [NSKeyedUnarchiver unarchiveObjectWithData:data];
[rule apply];
} @catch (NSException *exception) {
NSLog(@"%@", exception.description);
}
}
}
}
在load的时候,也就是类被加载的时候,他就会到NSUserDefaults.standardUserDefaults
里面找kMTPersistentRulesKey
的值,然后解包为MTRule
以后依次apply。
并且其实这个类init的时候就注册了监听,当监听到app kill的时候去保存当前的rules:
- (instancetype)init
{
self = [super init];
if (self) {
_targetSELs = [NSMapTable weakToStrongObjectsMapTable];
_classHooked = [NSMutableSet set];
pthread_mutex_init(&mutex, NULL);
NSNotificationName name = nil;
#if TARGET_OS_IOS || TARGET_OS_TV
name = UIApplicationWillTerminateNotification;
#elif TARGET_OS_OSX
name = NSApplicationWillTerminateNotification;
#endif
if (name.length > 0) {
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(handleAppWillTerminateNotification:) name:name object:nil];
}
}
return self;
}
- (void)handleAppWillTerminateNotification:(NSNotification *)notification
{
if (@available(macOS 10.11, *)) {
[self savePersistentRules];
}
}
- (void)savePersistentRules
{
NSMutableArray<NSData *> *array = [NSMutableArray array];
for (MTRule *rule in self.allRules) {
if (rule.isPersistent) {
NSData *data;
if (@available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, *)) {
NSError *error = nil;
data = [NSKeyedArchiver archivedDataWithRootObject:rule requiringSecureCoding:YES error:&error];
if (error) {
NSLog(@"%@", error.localizedDescription);
}
} else {
data = [NSKeyedArchiver archivedDataWithRootObject:rule];
}
if (data) {
[array addObject:data];
}
}
}
[NSUserDefaults.standardUserDefaults setObject:array forKey:kMTPersistentRulesKey];
}
那么这里为什么MTRule
是可以被解包的呢?
@interface MTRule () <NSSecureCoding>
#pragma mark NSSecureCoding
+ (BOOL)supportsSecureCoding
{
return YES;
}
- (void)encodeWithCoder:(NSCoder *)aCoder
{
if (mt_object_isClass(self.target)) {
Class cls = self.target;
NSString *classKey = @"target";
if (class_isMetaClass(cls)) {
classKey = @"meta_target";
}
[aCoder encodeObject:NSStringFromClass(cls) forKey:classKey];
[aCoder encodeObject:NSStringFromSelector(self.selector) forKey:@"selector"];
[aCoder encodeDouble:self.durationThreshold forKey:@"durationThreshold"];
[aCoder encodeObject:@(self.mode) forKey:@"mode"];
[aCoder encodeDouble:self.lastTimeRequest forKey:@"lastTimeRequest"];
[aCoder encodeBool:self.isPersistent forKey:@"persistent"];
[aCoder encodeObject:NSStringFromSelector(self.aliasSelector) forKey:@"aliasSelector"];
}
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
id target = NSClassFromString([aDecoder decodeObjectOfClass:NSString.class forKey:@"target"]);
if (!target) {
target = NSClassFromString([aDecoder decodeObjectOfClass:NSString.class forKey:@"meta_target"]);
target = mt_metaClass(target);
}
if (target) {
SEL selector = NSSelectorFromString([aDecoder decodeObjectOfClass:NSString.class forKey:@"selector"]);
NSTimeInterval durationThreshold = [aDecoder decodeDoubleForKey:@"durationThreshold"];
MTPerformMode mode = [[aDecoder decodeObjectForKey:@"mode"] unsignedIntegerValue];
NSTimeInterval lastTimeRequest = [aDecoder decodeDoubleForKey:@"lastTimeRequest"];
BOOL persistent = [aDecoder decodeBoolForKey:@"persistent"];
NSString *aliasSelector = [aDecoder decodeObjectOfClass:NSString.class forKey:@"aliasSelector"];
self = [self initWithTarget:target selector:selector durationThreshold:durationThreshold];
self.mode = mode;
self.lastTimeRequest = lastTimeRequest;
self.persistent = persistent;
self.aliasSelector = NSSelectorFromString(aliasSelector);
return self;
}
return nil;
}
使用NSCoding来读写用户数据文件的问题在于,把全部的类编码到一个文件里,也就间接地给了这个文件访问你APP里面实例类的权限。
虽然你不能在一个NSCoded文件里(至少在iOS中的)存储可执行代码,但是一名黑客可以使用特制地文件骗过你的APP进入到实例化类中,这是你从没打算做的,或者是你想要在另一个不同的上下文时才做的。尽管以这种方式造成实际性的破坏很难,但是无疑会导致用户的APP崩溃掉或者数据丢失。
在iOS6中,苹果引入了一个新的协议,是基于NSCoding的,叫做NSSecureCoding。NSSecureCoding和NSCoding是一样的,除了在解码时要同时指定key和要解码的对象的类,如果要求的类和从文件中解码出的对象的类不匹配,NSCoder会抛出异常,告诉你数据已经被篡改了。
因为MTRule
实现了NSSecureCoding
的协议,并且注意哦,只有if (mt_object_isClass(self.target))
的时候,才会encode,也就是只对类对象的rule会持久化。
那么MTEngine
的targetSELs
的结构是什么样子呢?我们可以看看它是怎么把所有rule都拿出来的,也就是知道它存入的结构了~
- (NSArray<MTRule *> *)allRules
{
pthread_mutex_lock(&mutex);
NSMutableArray *rules = [NSMutableArray array];
for (id target in [[self.targetSELs keyEnumerator] allObjects]) {
NSMutableSet *selectors = [self.targetSELs objectForKey:target];
for (NSString *selectorName in selectors) {
MTDealloc *mtDealloc = objc_getAssociatedObject(target, NSSelectorFromString(selectorName));
if (mtDealloc.rule) {
[rules addObject:mtDealloc.rule];
}
}
}
pthread_mutex_unlock(&mutex);
return [rules copy];
}
所以其实
targetSELs
这个map是以target对象为key,value是一个set,这个set存了所有这个对象限流的方法。而实际的rule,会被包装为
MTDealloc
通过关联对象保存给对象,关联对象的key就是限流方法名。
这点可以在看applyRule的时候再验证这个推测~
MTEngine
的applyRule
是酱紫的:
- (BOOL)applyRule:(MTRule *)rule
{
pthread_mutex_lock(&mutex);
MTDealloc *mtDealloc = [rule mt_deallocObject];
[mtDealloc lock];
BOOL shouldApply = YES;
if (mt_checkRuleValid(rule)) {
……
shouldApply = shouldApply && mt_overrideMethod(rule);
if (shouldApply) {
[self addSelector:rule.selector onTarget:rule.target];
rule.active = YES;
}
}
else {
shouldApply = NO;
NSLog(@"Sorry: invalid rule.");
}
[mtDealloc unlock];
if (!shouldApply) {
objc_setAssociatedObject(rule.target, rule.selector, nil, OBJC_ASSOCIATION_RETAIN);
}
pthread_mutex_unlock(&mutex);
return shouldApply;
}
会先生成一个记录rule的MTDealloc对象,然后check是不是应该apply rule,首先这个rule应该是合法的(mt_checkRuleValid
做的事情就是check target & selector不为空,durationThreshold>0,方法不是forwardInvocation
,并且对象不是MTRule
以及MTEngine
类)。
如果是不合法,那么就给target对象设置关联对象,key是selector,value是nil。
那么如果是valid rule会做什么呢?
for (id target in [[self.targetSELs keyEnumerator] allObjects]) {
NSMutableSet *selectors = [self.targetSELs objectForKey:target];
NSString *selectorName = NSStringFromSelector(rule.selector);
// 遍历targetSELs的key,拿对应已经加过rule的selectors,如果包含要加的selector,则比对是不是target一致,一致则跳过
if ([selectors containsObject:selectorName]) {
if (target == rule.target) {
shouldApply = NO;
continue;
}
// 如果有加过同名selector,但是target不一致
// 并且两者都是Class
if (mt_object_isClass(rule.target) && mt_object_isClass(target)) {
Class clsA = rule.target;
Class clsB = target;
// 如果两者是subclass的关系,就不应该apply
shouldApply = !([clsA isSubclassOfClass:clsB] || [clsB isSubclassOfClass:clsA]);
// inheritance relationship
if (!shouldApply) {
NSLog(@"Sorry: %@ already apply rule in %@. A message can only have one rule per class hierarchy.", selectorName, NSStringFromClass(clsB));
break;
}
}
// 如果target是class,并且是想要应用的rule.target的class,并且selector同名,那么也不应该apply
else if (mt_object_isClass(target) && target == object_getClass(rule.target)) {
shouldApply = NO;
NSLog(@"Sorry: %@ already apply rule in target's Class(%@).", selectorName, target);
break;
}
}
}
// 如果到这里shouldApply为YES,会执行mt_overrideMethod
shouldApply = shouldApply && mt_overrideMethod(rule);
// 如果仍旧应该apply,把selector和target记录到自己的targetSELs里面,并把rule active
if (shouldApply) {
[self addSelector:rule.selector onTarget:rule.target];
rule.active = YES;
}
上面的判断是循环target,然后看对应的selectors是不是包含想要加的selector名字,这里为什么不直接通过rule.target为key从self. targetSELs取对应的set,然后遍历看有木有同名selector即可??
- 原因其实代码已经告诉我们了,因为有的时候是你给子类加了rule,然后你又给父类加rule,如果直接拿父类对象作为key,你就发现没有加过,然后又去加,这就有问题了,所以必须check每个对象的selectors。
那么mt_overrideMethod
里面做了什么呢?
static BOOL mt_overrideMethod(MTRule *rule)
{
id target = rule.target;
SEL selector = rule.selector;
// 加了_mt_前缀的方法
SEL aliasSelector = rule.aliasSelector;
Class cls;
Class statedClass = [target class];
Class baseClass = object_getClass(target);
NSString *className = NSStringFromClass(baseClass);
// 已经被MT Hook过了
if ([className hasPrefix:MTSubclassPrefix]) {
cls = baseClass;
}
// target是class,不是实例对象
else if (mt_object_isClass(target)) {
cls = target;
}
// class方法返回和object_getClass不一致,以object_getClass为准,因为可能被其他库hook过
else if (statedClass != baseClass) {
cls = baseClass;
}
// 正常走下面,创建一个MTSubclassPrefix前缀的子类,让它的父类指向原来的target的class
else {
const char *subclassName = [MTSubclassPrefix stringByAppendingString:className].UTF8String;
Class subclass = objc_getClass(subclassName);
if (subclass == nil) {
subclass = objc_allocateClassPair(baseClass, subclassName, 0);
if (subclass == nil) {
NSLog(@"objc_allocateClassPair failed to allocate class %s.", subclassName);
return NO;
}
// 让subclass的class方法返回原类,以及subclass的class的class方法也返回原类
mt_hookedGetClass(subclass, statedClass);
mt_hookedGetClass(object_getClass(subclass), statedClass);
objc_registerClassPair(subclass);
}
// 让target的class是subclass,也就是MTSubclassPrefix前缀的子类
object_setClass(target, subclass);
cls = subclass;
}
// check if subclass has hooked!
for (Class clsHooked in MTEngine.defaultEngine.classHooked) {
if (clsHooked != cls && [clsHooked isSubclassOfClass:cls]) {
NSLog(@"Sorry: %@ used to be applied, can't apply it's super class %@!", NSStringFromClass(cls), NSStringFromClass(cls));
return NO;
}
}
// 设置MTDealloc的class为新建的子类
[rule mt_deallocObject].cls = cls;
// 如果原类覆写过forwardInvocation方法,且这个方法不是我们hook的mt_forwardInvocation方法,就把他放到MTForwardInvocationSelectorName这个method里面保存起来,并用mt_forwardInvocation替代原版的方法。
if (class_getMethodImplementation(cls, @selector(forwardInvocation:)) != (IMP)mt_forwardInvocation) {
IMP originalImplementation = class_replaceMethod(cls, @selector(forwardInvocation:), (IMP)mt_forwardInvocation, "v@:@");
if (originalImplementation) {
class_addMethod(cls, NSSelectorFromString(MTForwardInvocationSelectorName), originalImplementation, "v@:@");
}
}
// superCls就是原有target的类
Class superCls = class_getSuperclass(cls);
// 子类的target method
Method targetMethod = class_getInstanceMethod(cls, selector);
IMP targetMethodIMP = method_getImplementation(targetMethod);
// 如果这个targetMethodIMP不是消息转发方法
if (!mt_isMsgForwardIMP(targetMethodIMP)) {
const char *typeEncoding = method_getTypeEncoding(targetMethod);
Method targetAliasMethod = class_getInstanceMethod(cls, aliasSelector);
Method targetAliasMethodSuper = class_getInstanceMethod(superCls, aliasSelector);
// 把原有的method转给aliasSelector
if (![cls instancesRespondToSelector:aliasSelector] || targetAliasMethod == targetAliasMethodSuper) {
__unused BOOL addedAlias = class_addMethod(cls, aliasSelector, method_getImplementation(targetMethod), typeEncoding);
NSCAssert(addedAlias, @"Original implementation for %@ is already copied to %@ on %@", NSStringFromSelector(selector), NSStringFromSelector(aliasSelector), cls);
}
// 替换新建子类的selector为mt_getMsgForwardIMP方法,然后记录一下这个类已经被hook过了
class_replaceMethod(cls, selector, mt_getMsgForwardIMP(statedClass, selector), typeEncoding);
[MTEngine.defaultEngine.classHooked addObject:cls];
}
return YES;
}
所以其实上面这段做了:
- 创建
MTSubclassPrefix = @"_MessageThrottle_"
前缀的子类,让它的父类指向原来target的class - 替换子类的
forwardInvocation
方法为mt_forwardInvocation
,然后把原来的forwardInvocation
方法存到MTForwardInvocationSelectorName
里面 - 把原有的selector方法存入aliasSelector里
- 用
mt_getMsgForwardIMP
替代原有的selector
我用MTDemo跑了一下,打印了加throttle前后的class方法列表:
2020-04-22 08:00:23.214945+0800 MTDemo[4987:1268506] 方法名:foo:,参数个数:3,编码方式:v24@0:8@16
2020-04-22 08:00:23.215030+0800 MTDemo[4987:1268506] 方法名:.cxx_destruct,参数个数:2,编码方式:v16@0:8
2020-04-22 08:00:23.215072+0800 MTDemo[4987:1268506] 方法名:bar,参数个数:2,编码方式:@16@0:8
2020-04-22 08:00:23.215116+0800 MTDemo[4987:1268506] 方法名:setBar:,参数个数:3,编码方式:v24@0:8@16
2020-04-22 08:00:23.215177+0800 MTDemo[4987:1268506] ===========
2020-04-22 08:00:23.215704+0800 MTDemo[4987:1268506] 方法名:foo:,参数个数:3,编码方式:v24@0:8@16
2020-04-22 08:00:23.215763+0800 MTDemo[4987:1268506] 方法名:__mt_foo:,参数个数:3,编码方式:v24@0:8@16
2020-04-22 08:00:23.215800+0800 MTDemo[4987:1268506] 方法名:forwardInvocation:,参数个数:3,编码方式:v@:@
2020-04-22 08:00:23.215834+0800 MTDemo[4987:1268506] 方法名:class,参数个数:2,编码方式:#16@0:8
注意这里加了throttle以后不能用[obj class]
去那方法列表,否则和之前是一样的,因为它的代码改了新建子类的class方法返回值,所以要用object_getClass
拿到真正的class。
mt_getMsgForwardIMP
返回的就是一个_objc_msgForward
,所以当我们调用target被限流的selector的时候,其实是先转发给了mt_forwardInvocation
,然后如果应该相应就执行mt_handleInvocation
,如果不应该就转发给它原始的forward。
static void mt_forwardInvocation(__unsafe_unretained id assignSlf, SEL selector, NSInvocation *invocation)
{
MTDealloc *mtDealloc = nil;
// 如果target不是class
if (!mt_object_isClass(invocation.target)) {
mtDealloc = objc_getAssociatedObject(invocation.target, invocation.selector);
}
// 如果target是class
else {
mtDealloc = objc_getAssociatedObject(object_getClass(invocation.target), invocation.selector);
}
BOOL respondsToAlias = YES;
Class cls = object_getClass(invocation.target);
do {
// 如果类自己的关联对象上面没有拿到mtDealloc,就找它的class拿
if (!mtDealloc.rule) {
mtDealloc = objc_getAssociatedObject(cls, invocation.selector);
}
// target的class是新建的子类,这里看子类是不是响应aliasSelector
if ((respondsToAlias = [cls instancesRespondToSelector:mtDealloc.rule.aliasSelector])) {
break;
}
mtDealloc = nil;
}
// 如果当前类不响应aliasSelector就一直上溯找superclass,看父类是不是响应
while (!respondsToAlias && (cls = class_getSuperclass(cls)));
[mtDealloc lock];
// 如果始终不响应,那么这个selector就转给最原始的forward方法
if (!respondsToAlias) {
mt_executeOrigForwardInvocation(assignSlf, selector, invocation);
}
else {
// 如果响应,则自己去判断是不是满足了阈值要求,满足了才invoke
mt_handleInvocation(invocation, mtDealloc.rule);
}
[mtDealloc unlock];
}
然后看下handle是如何做的,其实就是看是不是满足时间条件,如果满足就调用rule.aliasSelector:
static void mt_handleInvocation(NSInvocation *invocation, MTRule *rule)
{
NSCParameterAssert(invocation);
NSCParameterAssert(rule);
// rule如果不isActive就直接invoke
if (!rule.isActive) {
[invocation invoke];
return;
}
// 如果rule.durationThreshold小于等于0,或者mt_invokeFilterBlock返回yes,就调用rule.aliasSelector
if (rule.durationThreshold <= 0 || mt_invokeFilterBlock(rule, invocation)) {
invocation.selector = rule.aliasSelector;
[invocation invoke];
return;
}
NSTimeInterval now = [[NSDate date] timeIntervalSince1970];
now += MTEngine.defaultEngine.correctionForSystemTime;
switch (rule.mode) {
case MTPerformModeFirstly: {
if (now - rule.lastTimeRequest > rule.durationThreshold) {
// 如果当前-上次调用>时间阈值,则立刻调用
invocation.selector = rule.aliasSelector;
[invocation invoke];
rule.lastTimeRequest = now;
dispatch_async(rule.messageQueue, ^{
// May switch from other modes, set nil just in case.
rule.lastInvocation = nil;
});
}
break;
}
case MTPerformModeLast: {
invocation.selector = rule.aliasSelector;
[invocation retainArguments];
dispatch_async(rule.messageQueue, ^{
rule.lastInvocation = invocation;
// 如果当前-上次调用>时间阈值,dispatch_after在rule.durationThreshold时间之后调用
if (now - rule.lastTimeRequest > rule.durationThreshold) {
rule.lastTimeRequest = now;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(rule.durationThreshold * NSEC_PER_SEC)), rule.messageQueue, ^{
if (!rule.isActive) {
rule.lastInvocation.selector = rule.selector;
}
[rule.lastInvocation invoke];
rule.lastInvocation = nil;
});
}
});
break;
}
case MTPerformModeDebounce: {
invocation.selector = rule.aliasSelector;
[invocation retainArguments];
dispatch_async(rule.messageQueue, ^{
rule.lastInvocation = invocation;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(rule.durationThreshold * NSEC_PER_SEC)), rule.messageQueue, ^{
if (rule.lastInvocation == invocation) {
if (!rule.isActive) {
rule.lastInvocation.selector = rule.selector;
}
[rule.lastInvocation invoke];
rule.lastInvocation = nil;
}
});
});
break;
}
}
}
上面有两个地方觉得比较奇怪:
-
一个是如果rule不active,那么就会调用原来的selector而不是rule.aliasSelector,那么由于原来的selector已经被hook了,就会又调用到消息转发,然后继续调到这里造成死循环
=> 这个原因其实是如果rule不active了,说明hook不存在了,所以调用原来的方法而非alisa -
另一个是,如果MTPerformModeLast模式,当现在调用是valid的状况,为什么要dispatch在rule.durationThreshold以后再调用呢?
=> 这个设想第一次调用满足了阈值,但是还需要等一个阈值时间才能调用就能理解了。并且每次都会把最新的invoke存起来,当时间阈值以后,延后执行,保证一定会执行一次再阈值时间内,并且执行最新的。
那么卸载rule的时候做了啥呢?其实就是让target的类还原为之前的类,而非加了MT前缀的子类,并且通过mt_revertHook方法把selector以及消息转发都还原了~
- (BOOL)discardRule:(MTRule *)rule
{
pthread_mutex_lock(&mutex);
MTDealloc *mtDealloc = [rule mt_deallocObject];
[mtDealloc lock];
BOOL shouldDiscard = NO;
if (mt_checkRuleValid(rule)) {
[self removeSelector:rule.selector onTarget:rule.target];
shouldDiscard = mt_recoverMethod(rule.target, rule.selector, rule.aliasSelector);
rule.active = NO;
}
[mtDealloc unlock];
pthread_mutex_unlock(&mutex);
return shouldDiscard;
}
主要是这个方法,并且设置了active,但这里其实有一个问题诶,如果shouldDiscard是NO,那么就不应该被丢弃,那么就不应该还原,这个时候如果就把rule.active设置为NO了,但又没还原selector方法,不就可能死循环了么?
但mt_recoverMethod
做了神马呢?
static BOOL mt_recoverMethod(id target, SEL selector, SEL aliasSelector)
{
Class cls;
if (mt_object_isClass(target)) {
cls = target;
if ([MTEngine.defaultEngine containsSelector:selector onTargetsOfClass:cls]) {
return NO;
}
}
else {
MTDealloc *mtDealloc = objc_getAssociatedObject(target, selector);
// get class when apply rule on target.
cls = mtDealloc.cls;
// target current real class name
NSString *className = NSStringFromClass(object_getClass(target));
if ([className hasPrefix:MTSubclassPrefix]) {
Class originalClass = NSClassFromString([className stringByReplacingOccurrencesOfString:MTSubclassPrefix withString:@""]);
NSCAssert(originalClass != nil, @"Original class must exist");
if (originalClass) {
object_setClass(target, originalClass);
}
}
if ([MTEngine.defaultEngine containsSelector:selector onTarget:cls] ||
[MTEngine.defaultEngine containsSelector:selector onTargetsOfClass:cls]) {
return NO;
}
}
mt_revertHook(cls, selector, aliasSelector);
return YES;
}
这个库我们用的时候有遇到的crash,感觉其实有点问题,就是改了太多runtime的了,因为我们的crash我尝试想自己实现一个不hook的,然后就更深刻的体会到了这个库超方便虽然我还没想好咋写,膜拜灰常厉害的作者~
网友评论