Skip to content

Commit

Permalink
NSObject (QMUI) 里将 qmui_enumrateIvarsUsingBlock 的 block 参数从 ivarName …
Browse files Browse the repository at this point in the history
…改为完整的 ivarDescription
  • Loading branch information
MoLice committed Jun 25, 2019
1 parent 1dedd76 commit 4da328d
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 35 deletions.
41 changes: 37 additions & 4 deletions QMUIKit/UIKitExtensions/NSObject+QMUI.h
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ NS_ASSUME_NONNULL_BEGIN
@param block 用于遍历的 block
*/
- (void)qmui_enumrateIvarsUsingBlock:(void (^)(Ivar ivar, NSString *ivarName))block;
- (void)qmui_enumrateIvarsUsingBlock:(void (^)(Ivar ivar, NSString *ivarDescription))block;

/**
使用 block 遍历指定 class 的所有成员变量(也即 _xxx 那种),不包含 property 对应的 _property 成员变量
Expand All @@ -115,7 +115,7 @@ NS_ASSUME_NONNULL_BEGIN
@param includingInherited 是否要包含由继承链带过来的 ivars
@param block 用于遍历的 block
*/
+ (void)qmui_enumrateIvarsOfClass:(Class)aClass includingInherited:(BOOL)includingInherited usingBlock:(void (^)(Ivar ivar, NSString *ivarName))block;
+ (void)qmui_enumrateIvarsOfClass:(Class)aClass includingInherited:(BOOL)includingInherited usingBlock:(void (^)(Ivar ivar, NSString *ivarDescription))block;

/**
使用 block 遍历指定 class 的所有属性,不包含 superclasses 里定义的 property
Expand Down Expand Up @@ -155,10 +155,36 @@ NS_ASSUME_NONNULL_BEGIN
*/
+ (void)qmui_enumerateProtocolMethods:(Protocol *)protocol usingBlock:(void (^)(SEL selector))block;

/// iOS 13 下系统禁止通过 KVC 访问私有 API,因此提供这种方式兼容
/// https://github.com/Tencent/QMUI_iOS/issues/617
/**
iOS 13 下系统禁止通过 KVC 访问私有 API,因此提供这种方式在遇到 access prohibited 的异常时可以取代 valueForKey: 使用。
对 iOS 12 及以下的版本,等价于 valueForKey:。
@note QMUI 提供2种方式兼容系统的 access prohibited 异常:
1. 通过将配置表的 IgnoreKVCAccessProhibited 置为 YES 来全局屏蔽系统的异常警告,代码中依然正常使用系统的 valueForKey:、setValue:forKey:,当开启后再遇到 access prohibited 异常时,将会用 QMUIWarnLog 来提醒,不再中断 App 的运行,这是首选推荐方案。
2. 使用 qmui_valueForKey:、qmui_setValue:forKey: 代替系统的 valueForKey:、setValue:forKey:,适用于不希望全局屏蔽,只针对某个局部代码自己处理的场景。
@link https://github.com/Tencent/QMUI_iOS/issues/617
@param key ivar 属性名,支持下划线或不带下划线
@return key 对应的 value,如果该 key 原本是非对象的值,会被用 NSNumber、NSValue 包裹后返回
*/
- (nullable id)qmui_valueForKey:(NSString *)key;

/**
iOS 13 下系统禁止通过 KVC 访问私有 API,因此提供这种方式在遇到 access prohibited 的异常时可以取代 setValue:forKey: 使用。
对 iOS 12 及以下的版本,等价于 setValue:forKey:。
@note QMUI 提供2种方式兼容系统的 access prohibited 异常:
1. 通过将配置表的 IgnoreKVCAccessProhibited 置为 YES 来全局屏蔽系统的异常警告,代码中依然正常使用系统的 valueForKey:、setValue:forKey:,当开启后再遇到 access prohibited 异常时,将会用 QMUIWarnLog 来提醒,不再中断 App 的运行,这是首选推荐方案。
2. 使用 qmui_valueForKey:、qmui_setValue:forKey: 代替系统的 valueForKey:、setValue:forKey:,适用于不希望全局屏蔽,只针对某个局部代码自己处理的场景。
@link https://github.com/Tencent/QMUI_iOS/issues/617
@param key ivar 属性名,支持下划线或不带下划线
@return key 对应的 value,如果该 key 原本是非对象的值,会被用 NSNumber、NSValue 包裹后返回
*/
- (void)qmui_setValue:(nullable id)value forKey:(NSString *)key;

@end
Expand Down Expand Up @@ -261,4 +287,11 @@ NS_ASSUME_NONNULL_BEGIN
- (NSString *)qmui_ivarList;
@end

@interface NSThread (QMUI_KVC)

/// 是否将当前线程标记为忽略系统的 KVC access prohibited 警告,默认为 NO,当开启后,NSException 将不会再抛出 access prohibited 异常
/// @see BeginIgnoreUIKVCAccessProhibited、EndIgnoreUIKVCAccessProhibited
@property(nonatomic, assign) BOOL qmui_shouldIgnoreUIKVCAccessProhibited;
@end

NS_ASSUME_NONNULL_END
114 changes: 83 additions & 31 deletions QMUIKit/UIKitExtensions/NSObject+QMUI.m
Original file line number Diff line number Diff line change
Expand Up @@ -122,32 +122,69 @@ - (void)qmui_performSelector:(SEL)selector withPrimitiveReturnValue:(void *)retu
}
}

- (void)qmui_enumrateIvarsUsingBlock:(void (^)(Ivar ivar, NSString *ivarName))block {
[NSObject qmui_enumrateIvarsOfClass:self.class includingInherited:NO usingBlock:block];
}

+ (void)qmui_enumrateIvarsOfClass:(Class)aClass includingInherited:(BOOL)includingInherited usingBlock:(void (^)(Ivar, NSString *))block {
- (void)qmui_enumrateIvarsUsingBlock:(void (^)(Ivar ivar, NSString *ivarDescription))block {
[self qmui_enumrateIvarsIncludingInherited:NO usingBlock:block];
}

- (void)qmui_enumrateIvarsIncludingInherited:(BOOL)includingInherited usingBlock:(void (^)(Ivar ivar, NSString *ivarDescription))block {
NSMutableArray<NSString *> *ivarDescriptions = [NSMutableArray new];
NSString *ivarList = [self qmui_ivarList];
NSError *error;
NSRegularExpression *reg = [NSRegularExpression regularExpressionWithPattern:[NSString stringWithFormat:@"in %@:(.*?)((?=in \\w+:)|$)", NSStringFromClass(self.class)] options:NSRegularExpressionDotMatchesLineSeparators error:&error];
if (!error) {
NSArray<NSTextCheckingResult *> *result = [reg matchesInString:ivarList options:NSMatchingReportCompletion range:NSMakeRange(0, ivarList.length)];
[result enumerateObjectsUsingBlock:^(NSTextCheckingResult * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSString *ivars = [ivarList substringWithRange:[obj rangeAtIndex:1]];
[ivars enumerateLinesUsingBlock:^(NSString * _Nonnull line, BOOL * _Nonnull stop) {
if (![line hasPrefix:@"\t\t"]) {// 有些 struct 类型的变量,会把 struct 的成员也缩进打出来,所以用这种方式过滤掉
line = line.qmui_trim;
if (line.length > 2) {// 过滤掉空行或者 struct 结尾的"}"
NSRange range = [line rangeOfString:@":"];
if (range.location != NSNotFound)// 有些"unknow type"的变量不会显示指针地址(例如 UIView->_viewFlags)
line = [line substringToIndex:range.location];// 去掉指针地址
NSUInteger typeStart = [line rangeOfString:@" ("].location;
line = [NSString stringWithFormat:@"%@ %@", [line substringWithRange:NSMakeRange(typeStart + 2, line.length - 1 - (typeStart + 2))], [line substringToIndex:typeStart]];// 交换变量类型和变量名的位置,变量类型在前,变量名在后,空格隔开
[ivarDescriptions addObject:line];
}
}
}];
}];
}

unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList(aClass, &outCount);
Ivar *ivars = class_copyIvarList(self.class, &outCount);
for (unsigned int i = 0; i < outCount; i ++) {
Ivar ivar = ivars[i];
if (block) block(ivar, [NSString stringWithFormat:@"%s", ivar_getName(ivar)]);
NSString *ivarName = [NSString stringWithFormat:@"%s", ivar_getName(ivar)];
for (NSString *desc in ivarDescriptions) {
if ([desc hasSuffix:ivarName]) {
block(ivar, desc);
break;
}
}
}
free(ivars);

if (includingInherited) {
Class superclass = class_getSuperclass(aClass);
Class superclass = self.superclass;
if (superclass) {
[NSObject qmui_enumrateIvarsOfClass:superclass includingInherited:includingInherited usingBlock:block];
}
}
}

+ (void)qmui_enumrateIvarsOfClass:(Class)aClass includingInherited:(BOOL)includingInherited usingBlock:(void (^)(Ivar, NSString *))block {
if (!block) return;
[[aClass new] qmui_enumrateIvarsIncludingInherited:includingInherited usingBlock:block];
}

- (void)qmui_enumratePropertiesUsingBlock:(void (^)(objc_property_t property, NSString *propertyName))block {
[NSObject qmui_enumratePropertiesOfClass:self.class includingInherited:NO usingBlock:block];
}

+ (void)qmui_enumratePropertiesOfClass:(Class)aClass includingInherited:(BOOL)includingInherited usingBlock:(void (^)(objc_property_t, NSString *))block {
if (!block) return;

unsigned int propertiesCount = 0;
objc_property_t *properties = class_copyPropertyList(aClass, &propertiesCount);

Expand All @@ -171,6 +208,8 @@ - (void)qmui_enumrateInstanceMethodsUsingBlock:(void (^)(Method, SEL))block {
}

+ (void)qmui_enumrateInstanceMethodsOfClass:(Class)aClass includingInherited:(BOOL)includingInherited usingBlock:(void (^)(Method, SEL))block {
if (!block) return;

unsigned int methodCount = 0;
Method *methods = class_copyMethodList(aClass, &methodCount);

Expand All @@ -191,6 +230,8 @@ + (void)qmui_enumrateInstanceMethodsOfClass:(Class)aClass includingInherited:(BO
}

+ (void)qmui_enumerateProtocolMethods:(Protocol *)protocol usingBlock:(void (^)(SEL))block {
if (!block) return;

unsigned int methodCount = 0;
struct objc_method_description *methods = protocol_copyMethodDescriptionList(protocol, NO, YES, &methodCount);
for (int i = 0; i < methodCount; i++) {
Expand All @@ -203,7 +244,7 @@ + (void)qmui_enumerateProtocolMethods:(Protocol *)protocol usingBlock:(void (^)(
}

- (id)qmui_valueForKey:(NSString *)key {
if (![self isKindOfClass:[UIView class]] || (QMUICMIActivated && IgnoreKVCAccessProhibited)) return [self valueForKey:key];
if (IOS_VERSION < 13.0 || ![self isKindOfClass:[UIView class]] || (QMUICMIActivated && IgnoreKVCAccessProhibited)) return [self valueForKey:key];

BeginIgnoreUIKVCAccessProhibited
id value = [self valueForKey:key];
Expand All @@ -212,7 +253,7 @@ - (id)qmui_valueForKey:(NSString *)key {
}

- (void)qmui_setValue:(id)value forKey:(NSString *)key {
if (![self isKindOfClass:[UIView class]] || (QMUICMIActivated && IgnoreKVCAccessProhibited)) return [self setValue:value forKey:key];
if (IOS_VERSION < 13.0 || ![self isKindOfClass:[UIView class]] || (QMUICMIActivated && IgnoreKVCAccessProhibited)) return [self setValue:value forKey:key];

BeginIgnoreUIKVCAccessProhibited
[self setValue:value forKey:key];
Expand Down Expand Up @@ -353,32 +394,43 @@ - (NSString *)qmui_ivarList {

@end

@implementation NSThread (QMUI_KVC)

QMUISynthesizeBOOLProperty(qmui_shouldIgnoreUIKVCAccessProhibited, setQmui_shouldIgnoreUIKVCAccessProhibited)

@implementation NSException (QMUI)
@end

@interface NSException (QMUI_KVC)

@end

@implementation NSException (QMUI_KVC)

+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
OverrideImplementation(object_getClass([NSException class]), @selector(raise:format:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
return ^(NSObject *selfObject, NSExceptionName raise, NSString *format, ...) {

if (raise == NSGenericException && [format isEqualToString:@"Access to %@'s %@ ivar is prohibited. This is an application bug"]) {
BOOL shouldIgnoreUIKVCAccessProhibited = ((QMUICMIActivated && IgnoreKVCAccessProhibited) || [[NSThread currentThread] qmui_getBoundBOOLForKey:QMUIIgnoreUIKVCAccessProhibitedKey]);
if (shouldIgnoreUIKVCAccessProhibited) return;
if (@available(iOS 13.0, *)) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
OverrideImplementation(object_getClass([NSException class]), @selector(raise:format:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
return ^(NSObject *selfObject, NSExceptionName raise, NSString *format, ...) {

QMUILogWarn(@"NSObject (QMUI)", @"使用 KVC 访问了 UIKit 的私有属性,会触发系统的 NSException,建议尽量避免此类操作,仍需访问可使用 BeginIgnoreUIKVCAccessProhibited 和 EndIgnoreUIKVCAccessProhibited 把相关代码包裹起来,或者直接使用 qmui_valueForKey: 、qmui_setValue:forKey:");
}

id (*originSelectorIMP)(id, SEL, NSExceptionName name, NSString *, ...);
originSelectorIMP = (id (*)(id, SEL, NSExceptionName name, NSString *, ...))originalIMPProvider();
va_list args;
va_start(args, format);
NSString *reason = [[NSString alloc] initWithFormat:format arguments:args];
originSelectorIMP(selfObject, originCMD, raise, reason);
va_end(args);
};
if (raise == NSGenericException && [format isEqualToString:@"Access to %@'s %@ ivar is prohibited. This is an application bug"]) {
BOOL shouldIgnoreUIKVCAccessProhibited = ((QMUICMIActivated && IgnoreKVCAccessProhibited) || NSThread.currentThread.qmui_shouldIgnoreUIKVCAccessProhibited);
if (shouldIgnoreUIKVCAccessProhibited) return;

QMUILogWarn(@"NSObject (QMUI)", @"使用 KVC 访问了 UIKit 的私有属性,会触发系统的 NSException,建议尽量避免此类操作,仍需访问可使用 BeginIgnoreUIKVCAccessProhibited 和 EndIgnoreUIKVCAccessProhibited 把相关代码包裹起来,或者直接使用 qmui_valueForKey: 、qmui_setValue:forKey:");
}

id (*originSelectorIMP)(id, SEL, NSExceptionName name, NSString *, ...);
originSelectorIMP = (id (*)(id, SEL, NSExceptionName name, NSString *, ...))originalIMPProvider();
va_list args;
va_start(args, format);
NSString *reason = [[NSString alloc] initWithFormat:format arguments:args];
originSelectorIMP(selfObject, originCMD, raise, reason);
va_end(args);
};
});
});
});
}
}

@end

0 comments on commit 4da328d

Please sign in to comment.