Runtime奇技淫巧之class_copyIvarList class_copyPropertyList

字数 1723阅读 1712

今天我们来介绍这两个神奇的方法,它们可以在一定程度上改变你对于系统控件的认识,也提供了你深入了解系统控件的一个小窗口,在开发中可能会带来你意想不到的惊喜,但是风险性同样也不是一般的大。

我们之前解析了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实用技巧(不扯淡,不套路)

推荐阅读更多精彩内容