1. 前言
- 本篇主要是解決iOS 13 中KVC 訪問限制,即訪問私有屬性遇到access prohibited異常的問題。
- 本篇主要是為了適配已經(jīng)使用了大量存在KVC限制的項目,對于新幼項目筆者建議還是按照官方要求來處理,畢竟后期官方再做變動,這些方法就不一定有效果了
- 聲明:本篇核心代碼來自
QMUIKit,欲知更多請前往QMUIKit
2. 解決方案
本篇提供了三種處理方式,且均是采用給分類關聯(lián)屬性或方法替換的方式實現(xiàn)(具體請查看下方代碼)
- 通過
全局的宏定義閥值來全局忽略KVC 訪問限制- 通過
封裝好的kvc方法來替換系統(tǒng)自帶的方法,局部忽略KVC 訪問限制- 通過
線程來忽略某代碼片段中對KVC 的訪問限制
3. 代碼
- 代碼依賴關系:
NSObject -依賴于-> NSThread -依賴于-> NSException- 將完整的代碼拷貝到項目中即可進行測試和使用
// 分類聲明
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
// MARK: - 宏定義
/// 通過該宏來 全局忽略系統(tǒng)的 KVC 訪問限制
#define IgnoreKVCAccessProhibited YES
/// 快捷書寫
/// 將 KVC 代碼包裹在這個宏中,可局部忽略系統(tǒng)的 KVC 訪問限制, IgnoreKVCAccessProhibited == NO時有效
#define BeginIgnoreUIKVCAccessProhibited if (@available(iOS 13.0, *)) NSThread.currentThread.shouldIgnoreUIKVCAccessProhibited = YES;
#define EndIgnoreUIKVCAccessProhibited if (@available(iOS 13.0, *)) NSThread.currentThread.shouldIgnoreUIKVCAccessProhibited = NO;
// MARK: - NSThread
@interface NSThread (KVC)
/// 是否將當前線程標記為忽略系統(tǒng)的 KVC access prohibited 警告,默認為 NO,當開啟后,NSException 將不會再拋出 access prohibited 異常
/// @see BeginIgnoreUIKVCAccessProhibited、EndIgnoreUIKVCAccessProhibited
@property(nonatomic, assign) BOOL shouldIgnoreUIKVCAccessProhibited;
@end
// MARK: - NSObject
@interface NSObject (KVC)
/**
iOS 13 下系統(tǒng)禁止通過 KVC 訪問私有 API,因此提供這種方式在遇到 access prohibited 的異常時可以取代 valueForKey: 使用。
對 iOS 12 及以下的版本,等價于 valueForKey:。
@note 本篇提供2種方式兼容系統(tǒng)的 access prohibited 異常:
1. 通過將 IgnoreKVCAccessProhibited 置為 YES 來全局屏蔽系統(tǒng)的異常警告,代碼中依然正常使用系統(tǒng)的 valueForKey:、setValue:forKey:,當開啟后再遇到 access prohibited 異常時,將會用 NSAssert 來提醒,Release模式下不再中斷 App,這是首選推薦方案。
2. 使用 ousi_valueForKey:、ousi_setValue:forKey: 代替系統(tǒng)的 valueForKey:、setValue:forKey:,
適用于不希望全局屏蔽,只針對某個局部代碼自己處理的場景。
@link https://github.com/Tencent/QMUI_iOS/issues/617
@param key ivar 屬性名,支持下劃線或不帶下劃線
@return key 對應的 value,如果該 key 原本是非對象的值,會被用 NSNumber、NSValue 包裹后返回
*/
- (nullable id)ousi_valueForKey:(NSString *)key;
/**
iOS 13 下系統(tǒng)禁止通過 KVC 訪問私有 API,因此提供這種方式在遇到 access prohibited 的異常時可以取代 setValue:forKey: 使用。
對 iOS 12 及以下的版本,等價于 setValue:forKey:。
@note QMUI 提供2種方式兼容系統(tǒng)的 access prohibited 異常:
1. 通過將 IgnoreKVCAccessProhibited 置為 YES 來全局屏蔽系統(tǒng)的異常警告,代碼中依然正常使用系統(tǒng)的 valueForKey:、setValue:forKey:,當開啟后再遇到 access prohibited 異常時,將會用 NSAssert 來提醒,Release模式下不再中斷 App 的運行,這是首選推薦方案。
2. 使用 ousi_valueForKey:、ousi_setValue:forKey: 代替系統(tǒng)的 valueForKey:、setValue:forKey:,
適用于不希望全局屏蔽,只針對某個局部代碼自己處理的場景。
@link https://github.com/Tencent/QMUI_iOS/issues/617
@param key ivar 屬性名,支持下劃線或不帶下劃線
*/
- (void)ousi_setValue:(nullable id)value forKey:(NSString *)key;
@end
// MARK: - NSException
@interface NSException (KVC)
@end
NS_ASSUME_NONNULL_END
// 分類實現(xiàn)
#import <objc/message.h>
// MARK: - NSThread
@implementation NSThread (KVC)
- (void)setShouldIgnoreUIKVCAccessProhibited:(BOOL)shouldIgnoreUIKVCAccessProhibited {
objc_setAssociatedObject(
self,
@selector(shouldIgnoreUIKVCAccessProhibited),
@(shouldIgnoreUIKVCAccessProhibited),
OBJC_ASSOCIATION_RETAIN_NONATOMIC
);
}
- (BOOL)shouldIgnoreUIKVCAccessProhibited {
return [objc_getAssociatedObject(self, _cmd) boolValue];
}
@end
// MARK: - NSObject
@implementation NSObject (KVC)
- (id)ousi_valueForKey:(NSString *)key {
if (@available(iOS 13.0, *)) {
if ([self isKindOfClass:[UIView class]] && !IgnoreKVCAccessProhibited) {
BeginIgnoreUIKVCAccessProhibited
id value = [self valueForKey:key];
EndIgnoreUIKVCAccessProhibited
return value;
}
}
return [self valueForKey:key];
}
- (void)ousi_setValue:(id)value forKey:(NSString *)key {
if (@available(iOS 13.0, *)) {
if ([self isKindOfClass:[UIView class]] && !IgnoreKVCAccessProhibited) {
BeginIgnoreUIKVCAccessProhibited
[self setValue:value forKey:key];
EndIgnoreUIKVCAccessProhibited
return;
}
}
[self setValue:value forKey:key];
}
@end
// MARK: - NSException
@implementation NSException (KVC)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class cls = [self class];
Method method1 = class_getClassMethod(cls, @selector(raise: format:));
Method method2 = class_getClassMethod(cls, @selector(ousi_raise: format:));
method_exchangeImplementations(method1, method2);
});
}
+ (void)ousi_raise:(NSExceptionName)name format:(NSString *)format,...{
if (name == NSGenericException && [format isEqualToString:@"Access to %@'s %@ ivar is prohibited. This is an application bug"]) {
BOOL shouldIgnoreUIKVCAccessProhibited = IgnoreKVCAccessProhibited || NSThread.currentThread.shouldIgnoreUIKVCAccessProhibited;
if (shouldIgnoreUIKVCAccessProhibited) return;
NSAssert(NO, @"使用 KVC 訪問了 UIKit 的私有屬性,會觸發(fā)系統(tǒng)的 NSException,建議盡量避免此類操作,仍需訪問可使用 BeginIgnoreUIKVCAccessProhibited 和 EndIgnoreUIKVCAccessProhibited 把相關代碼包裹起來,或者直接使用 ousi_valueForKey: 、ousi_setValue:forKey:");
}
va_list args;
va_start(args, format);
NSString *reason = [[NSString alloc] initWithFormat:format arguments:args];
// NSDictionary *userInfo = nil;
// @throw [NSException exceptionWithName:name reason:reason userInfo:userInfo];
[self ousi_raise:name format:reason];
va_end(args);
}
@end