Runtime奇技淫巧之class_copyIvarList class_copyPropertyList

今天我們來介紹這兩個神奇的方法,它們可以在一定程度上改變你對于系統控件的認識,也提供了你深入了解系統控件的一個小窗口,在開發中可能會帶來你意想不到的驚喜,但是風險性同樣也不是一般的大。

我們之前解析了Runtime中常見的數據結構(想看點這里)。我們知道對象的實例變量存在于Class結構體的一個ivars的鏈表中,同時runtime提供了豐富的函數對其進行操作。當然對于我們來說,私有變量才是感興趣的點,就像窺探別人隱私一樣。

問:你知道隔壁班有一個特別漂亮的小姑娘,但你只知道她們班只有她的名字是三個字,如果你想要找到她,攏共分幾步?
答:攏共分三步。步驟如下:
  • 首先你需要class_copyIvarList這個方法,獲取到他們班的花名冊,最終發現只有一個人的名字是三個字,她有一個美麗的名字叫做伍麗娟,具體操作如下:
/**
 *獲取當前類的所有實例變量
 */
+(void)getAllIvarNameWithClass:(Class)YSClass Completed:(void (^)(NSArray *ivarNameArray))completed{
    NSMutableArray *ivarNameArray = [NSMutableArray array];
    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList(YSClass, &count);
    for (int i = 0; i < count; i++){
        Ivar ivar = ivars[i];
        const char *ivarName = ivar_getName(ivar);
        NSString *ivarNameCode = [NSString stringWithUTF8String:ivarName];
#ifdef _YSDebugLog
        NSLog(@"%d : %@",i,ivarNameCode);
#endif
        [ivarNameArray addObject:ivarNameCode];
    }
    //由于ARC只適用于Foundation等框架,對runtime 等并不適用,所以ivars需要free()手動釋放。
    free(ivars);
    if (completed) completed(ivarNameArray);
}
  • 你找到了名字,下一步你就要通過這個名字找到這個人更多的信息,也就是Ivar結構體(其實找這個人的名字時候花名冊上就有她的信息,可以直接第三步,但是你就喜歡一步步來),你通過class_getInstanceVariable這個方法可以獲取到名字對應的Ivar信息,差不多你就已經知道了伍麗娟所有的外部信息了,比如身高啊,體重啊。具體操作如下:
Ivar WLJIvar = class_getInstanceVariable([BJ class], "伍麗娟");
  • 既然得到了他的全部信息,那你就要開始找這個人了,直接沖到他們班,根據你掌握的信息,一把把她拉出來。具體操作如下:
id WLJ = object_getIvar(BJ, WLJIvar);
  • 然后你就順利找到了 伍麗娟,就在你以為就要和她過上幸福生活的時候,她們班長站起來一腳把你踹了出去。甚至你連她們班長的臉都沒有看清,只知道他的名字好像是**三道杠
作為一個勇敢的男人,你怎么可能就這么認輸了呢,于是你要想辦法干掉她們班長,但是干掉一個人太明顯了,容易被懷疑,于是你想到了無敵暴力的KVCKVC雖然牛逼,但是性情不太穩點,要小心自取滅亡,但是你已經被愛情沖昏了頭腦,上去就是干!那么問題來了:干掉她們班長需要幾步?
答:干掉她們班長需要三步。首先按照之前的前兩步找到她們班長的名字無敵三道杠,然后讓KVC直接把你準備好的無敵四道杠本人替換掉無敵三道杠,具體操作如下:
[BJ setValue:無敵四道杠本人 forKey:@"無敵三道杠"];

她們班長換成了自己人,從此你和伍麗娟過上了幸福快樂的生活。

我們說下實際應用的場景:

問:如何給UITextView添加PlaceHolder?
答:創建一個UILabel,然后添加到UITextView上嘍。
剛才這么精彩的故事白講了!!!
正確回答應該是這樣的:
  • 首先我們先找到伍麗娟,然后... ...,我就不知道了。
  • 首先我們查找UITextView所有的實例變量,利用上面提到的方法:
[NSObject getAllIvarNameWithClass:[UITextView class] Completed:^(NSArray *ivarNameArray) {
    NSLog(@"ivars_%@",ivarNameArray);
}];

打印結果:

"_private",
"_textStorage",
... ...
"_preferredMaxLayoutWidth",
"_placeholderLabel",
"_inputAccessoryView",
... ...
"_inputView"

你會驚喜的發現,里面有一個叫做_placeholderLabel的實例變量,于是你按照找到伍麗娟的方式想要找到這個對象,不好意思,你得到的是nil,也就是說蘋果可能根本就沒有初始化這個東西,所以KVC閃亮登場:

//給TextView添加PlaceHolder
UITextView *textView = [[UITextView alloc]initWithFrame:CGRectMake(10, 50, CGRectGetWidth(self.view.frame) - 20, 200)];
[textView setBackgroundColor:[UIColor whiteColor]];
textView.font = [UIFont systemFontOfSize:16];
textView.delegate = self;
[self.view addSubview:textView];
UILabel *placeHolderLabel = [[UILabel alloc] init];
placeHolderLabel.text = @"我是PlaceHolder,不是伍麗娟。";
placeHolderLabel.numberOfLines = 0;
placeHolderLabel.textColor = [UIColor lightGrayColor];
[placeHolderLabel sizeToFit];
placeHolderLabel.font = textView.font;
[textView addSubview:placeHolderLabel];
[textView setValue:placeHolderLabel forKey:@"_placeholderLabel"];

運行結果:


然后再去獲取PlaceHolder,已經是你賦值的那個對象:

Ivar placeHolderIvar = class_getInstanceVariable([UITextView class], "_placeholderLabel");
id getPH = object_getIvar(textView, placeHolderIvar);
NSLog(@"placeHolder_%p_%p", placeHolderLabel,getPH);

打印結果:

TextViewDemo[2325:290654] 0x7ffa31e01d20_0x7ffa31e01d20

class_copyIvarList配合KVC這么用雖然有時候很方便,但是不免有風險,關于未公開的私有變量蘋果的改動沒必要寫到明面上,也許某天你的應用就會crash到你奔潰。當然你可以放心用到你自己定義的類上。


關于class_copyIvarList先說這么多,用法肯定不只是局限于我說的這些,下面我們說class_copyPropertyList這個方法。
類似于剛才找到伍麗娟的方式,我們同樣封裝一個獲取所有屬性的方法如下:

/**
 *獲取當前類的所有屬性
 */
+(void)getAllPropertyNameWithClass:(Class)YSClass Completed:(void (^)(NSArray *propertyNameArray))completed{
    NSMutableArray *propertyNameArray = [NSMutableArray array];
    unsigned int propertyCount = 0;
    objc_property_t *propertys = class_copyPropertyList(YSClass, &propertyCount);
    for (int i = 0; i < propertyCount; i++){
        objc_property_t property = propertys[i];
        const char *propertysName = property_getName(property);
        NSString *propertysNameCode = [NSString stringWithUTF8String:propertysName];
#ifdef _YSDebugLog
        NSLog(@"------%d : %@",i,propertysNameCode);
#endif
        [propertyNameArray addObject:propertysNameCode];
    }
    //由于ARC只適用于Foundation等框架,對runtime 等并不適用,所以propertys需要free()手動釋放。
    free(propertys);
    if (completed) completed(propertyNameArray);
}

我們同時調用獲取實例變量以及屬性的方法做一下對比:

[NSObject getAllIvarNameWithClass:[UITextView class] Completed:^(NSArray *ivarNameArray) {
    NSLog(@"ivars_%@",ivarNameArray);
}];
[NSObject getAllPropertyNameWithClass:[UITextView class] Completed:^(NSArray *propertyNameArray) {
    NSLog(@"propertys_%@",propertyNameArray);
}];

打印結果:

//實例變量
ivars_(
    "_private",
    "_textStorage",
    "_textContainer",
    "_layoutManager",
    "_containerView",
    "_inputDelegate",
    "_tokenizer",
    "_inputController",
    "_interactionAssistant",
    "_textInputTraits",
    "_autoscroll",
    "_tvFlags",
    "_contentSizeUpdateSeqNo",
    "_scrollTarget",
    "_scrollPositionDontRecordCount",
    "_scrollPosition",
    "_offsetFromScrollPosition",
    "_linkInteractionItem",
    "_dataDetectorTypes",
    "_preferredMaxLayoutWidth",
    "_placeholderLabel",
    "_inputAccessoryView",
    "_linkTextAttributes",
    "_streamingManager",
    "_characterStreamingManager",
    "_siriAnimationStyle",
    "_siriParameters",
    "_firstBaselineOffsetFromTop",
    "_lastBaselineOffsetFromBottom",
    "_cuiCatalog",
    "_beforeFreezingTextContainerInset",
    "_duringFreezingTextContainerInset",
    "_beforeFreezingFrameSize",
    "_unfreezingTextContainerSize",
    "_adjustsFontForContentSizeCategory",
    "_clearsOnInsertion",
    "_multilineContextWidth",
    "_inputView"
)
//屬性
propertys_(
    "_drawsDebugBaselines",
    hash,
    superclass,
    description,
    debugDescription,
    delegate,
    text,
    font,
    textColor,
    textAlignment,
    selectedRange,
    editable,
    selectable,
    dataDetectorTypes,
    allowsEditingTextAttributes,
    attributedText,
    typingAttributes,
    inputView,
    inputAccessoryView,
    clearsOnInsertion,
    textContainer,
    textContainerInset,
    layoutManager,
    textStorage,
    linkTextAttributes,
    hash,
    superclass,
    description,
    debugDescription,
    autocapitalizationType,
    autocorrectionType,
    spellCheckingType,
    keyboardType,
    keyboardAppearance,
    returnKeyType,
    enablesReturnKeyAutomatically,
    secureTextEntry,
    textContentType,
    recentInputIdentifier,
    validTextRange,
    PINEntrySeparatorIndexes,
    textTrimmingSet,
    insertionPointColor,
    selectionBarColor,
    selectionHighlightColor,
    selectionDragDotImage,
    insertionPointWidth,
    textLoupeVisibility,
    textSelectionBehavior,
    textSuggestionDelegate,
    isSingleLineDocument,
    contentsIsSingleValue,
    hasDefaultContents,
    acceptsEmoji,
    acceptsDictationSearchResults,
    forceEnableDictation,
    forceDisableDictation,
    forceDefaultDictationInfo,
    forceDictationKeyboardType,
    emptyContentReturnKeyType,
    returnKeyGoesToNextResponder,
    acceptsFloatingKeyboard,
    acceptsSplitKeyboard,
    displaySecureTextUsingPlainText,
    displaySecureEditsUsingPlainText,
    learnsCorrections,
    shortcutConversionType,
    suppressReturnKeyStyling,
    useInterfaceLanguageForLocalization,
    deferBecomingResponder,
    enablesReturnKeyOnNonWhiteSpaceContent,
    autocorrectionContext,
    responseContext,
    inputContextHistory,
    disablePrediction,
    disableInputBars,
    isCarPlayIdiom,
    textScriptType,
    devicePasscodeEntry,
    hasText,
    selectedTextRange,
    markedTextRange,
    markedTextStyle,
    beginningOfDocument,
    endOfDocument,
    inputDelegate,
    tokenizer,
    textInputView,
    selectionAffinity,
    insertDictationResultPlaceholder,
    adjustsFontForContentSizeCategory
)

對比一下,你會發現,并不是每一個屬性都對應了一個自己的實例變量,哎呀,平時自己寫的時候不是這樣的啊?并且就連公開的textdelegate都沒有對應的實例變量,這對于一些人可能會有些困惑,我們來看這么一個例子:
同樣,生成一個Person類:

.h
@interface Person : NSObject{
    NSInteger age;
}
@property(nonatomic,strong)NSString *name;
@end
-------------------------------------------------------------
.m
@implementation Person
-(void)setName:(NSString *)name{
    _name = name;
}

-(NSString *)name{
    return _name;
}
@end

你會發現報錯了,沒有發現這個實例變量:


我們創建一個Person類的分類如下:

.h
@interface Person (Character)
@property(nonatomic,strong)NSString* name;
@end
-------------------------------------------------------------
.m
@implementation Person (Character)
@end

下面我們打印這個類的實例變量和屬性:

[NSObject getAllIvarNameWithClass:[Person class] Completed:^(NSArray *ivarNameArray) {
    NSLog(@"ivars_%@",ivarNameArray);
}];
[NSObject getAllPropertyNameWithClass:[Person class] Completed:^(NSArray *propertyNameArray) {
    NSLog(@"propertys_%@",propertyNameArray);
}];

打印結果:

ivars_(
    age
)
propertys_(
    name
)

是不是發現了什么?一般情況下,聲明一個屬性相當于Ivar + setter方法 + getter方法(但是Ivar + setter方法 + getter方法并不代表它就是屬性),也就是相當于在Class結構體中Ivars鏈表中添加一個Ivar,同時在methodLists添加兩個Method。但是,在特定情況下,屬性不會生成對應的實例變量,包括settergetter方法也有特定生成原則。(可自行百度,應該有很多)
那這個東西有什么卵用嗎?之前我們說過objc_msgSend這個方法可以無限制的調用公開哪怕私有的方法,并且我對此進行了封裝,那我們就可以兩者結合通過settergetter方法給屬性賦值以及獲取屬性(就算它不生成實例變量又如何)。

UITextView *textView = [[UITextView alloc]initWithFrame:CGRectMake(10, 50, CGRectGetWidth(self.view.frame) - 20, 200)];
[textView setBackgroundColor:[UIColor whiteColor]];
textView.font = [UIFont systemFontOfSize:16];
textView.delegate = self;
[self.view addSubview:textView];
((void (*) (id , SEL, id)) (void *)objc_msgSend) (textView, sel_registerName("setText:"), @"我是伍麗娟");
bool acceptsEmoji = ((bool (*) (id, SEL)) (void *)objc_msgSend)(textView, sel_registerName("acceptsEmoji"));
NSLog(@"acceptsEmoji_%@",@(acceptsEmoji));

運行結果:


打印結果:acceptsEmoji_1

?? 關于class_copyIvarList class_copyPropertyList兩個方法對于系統的類來說能少用就少用,自己寫的類放心大膽的用,出了問題你砍我。就這些,看完點個關注,點個贊就走吧,如果你要打賞,那還不如在評論區表達一下你對伍麗娟的熱愛。

傳送門 : Runtime實用技巧(不扯淡,不套路)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容