關于iOS 13 中KVC 訪問限制的一些處理

1. 前言

  1. 本篇主要是解決iOS 13 中KVC 訪問限制,即訪問私有屬性遇到access prohibited異常的問題。
  2. 本篇主要是為了適配已經(jīng)使用了大量存在KVC限制的項目,對于新幼項目筆者建議還是按照官方要求來處理,畢竟后期官方再做變動,這些方法就不一定有效果了
  3. 聲明:本篇核心代碼來自QMUIKit,欲知更多請前往QMUIKit

2. 解決方案

本篇提供了三種處理方式,且均是采用給分類關聯(lián)屬性或方法替換的方式實現(xiàn)(具體請查看下方代碼)

  1. 通過全局的宏定義閥值全局忽略KVC 訪問限制
  2. 通過封裝好的kvc方法來替換系統(tǒng)自帶的方法,局部忽略KVC 訪問限制
  3. 通過線程忽略某代碼片段中對KVC 的訪問限制

3. 代碼

  1. 代碼依賴關系:NSObject -依賴于-> NSThread -依賴于-> NSException
  2. 將完整的代碼拷貝到項目中即可進行測試和使用
// 分類聲明

#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

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容