当前位置: 首页 > article >正文

【iOS】Runtime

目录

    • 动态性
    • 底层相关数据结构
      • isa结构
      • Class结构
        • class_rw_t
        • class_ro_t
        • method_t
      • 方法缓存cache_t
        • 清空缓存cache_t::reallocate
        • 哈希算法cache_hash
        • 更新索引值cache_next
    • 方法调用底层结构
    • 方法调用执行流程
      • 消息发送
      • 动态方法解析
      • 消息转发
    • super关键字
      • super本质
      • 经典问题
      • LVVM的中间代码
    • Runtime常用API
      • 成员变量
      • 属性
      • 方法
    • Runtime具体应用


动态性

Objective-C是一门动态性比较强的编程语言,跟CC++等语言有着很大的不同,其动态性是由Runtime API来支撑的
Runtime API提供的接口基本都是C语言的,源码由C\C++\汇编语言编写

什么是Runtime?

OC是一门动态性很强的编程语言,不像C/C++等编译型语言,程序运行结果就是编译后的结果,OC允许程序在运行时动态地去修改一些东西,比如动态添加方法、动态替换方法的实现、动态地去修改实例对象的类型,也就是允许这些操作推迟到运行时,这种动态性就是由Runtime来支撑和提供的
Runtime就是一套C语言API,封装了很多动态性相关的函数,平时编写的OC中动态相关的代码底层都会转换成Runtime API进行调用

底层相关数据结构

isa结构

ARM64架构之前,isa只是一个普通的指针,存储着classmeta-class对象的地址;之后,对isa进行了优化,其结构采用了union联合体,将一个64位的内存数据bits分开存储了许多数据,其中的33位才是存储class或meta-class的地址值

在这里插入图片描述

优化过的isa其实就是位域,进一步了解isa结构见这篇文章:【iOS】OC类与对象的本质分析

在这里插入图片描述

Class结构

在这里插入图片描述

class_rw_t

Class对象的底层结构objc_class中,我们知道通过bits & FAST_DATA_MASK就可以得到class_rw_t类型的表结构

在这里插入图片描述

class_rw_t里面的methodspropertiesprotocols都是二维数组,是可读可写的,包含了类的初始内容分类的内容

method_array_t举例,里面的元素都是method_list_t类型的二维数组,每一个二维数组又是method_t类型的元素,表示每一个方法类型

class_ro_t

在这里插入图片描述

class_ro_t里面的baseMethodList、baseProtocols、ivars、baseProperties是一维数组,是只读的,包含了类的初始内容

objc源码里的头文件objc-runtime-new.mm,通过查看函数realizeClassWithoutSwift的实现,程序运行时会将class_ro_t里面的数据和分类里面的数据信息全部合并到一起放到class_rw_t里面来

method_t

method_t是对函数的封装,里面包含了函数名编码信息以及函数地址

在这里插入图片描述
IMP代表函数的具体实现,指向着该函数的地址

typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 

SEL代表函数名,一般叫做选择器,底层结构跟char *类似,可以通过@selector()sel_registerName()获得

typedef struct objc_selector *SEL;
// 不同类中相同名字的方法,所对应的方法选择器是相同的,可以通过sel_getName()和NSStringFromSelector()转成字符串

types包含了函数返回值、参数编码的字符串,排列顺序如下

在这里插入图片描述

iOS中提供了一个叫做@encode的指令,可以将具体的类型表示成字符串编码

在这里插入图片描述

一个函数默认会带有两个参数id _NonnullSEL _Nonnull,之后才是写入的参数
下面举例说明,函数的types是多少

- (int)test:(int)age height:(float)height
{
    NSLog(@"%s", __func__);
    return 0;
}

// 该函数types为i24@0:8i16f20
// i 返回值int类型
// 24 几个返回值类型占据的大小总和(8 + 8 + 4 + 4)
// @ id类型
// 0 表示从第0位开始
// : SEL类型
// 8 从第8位开始
// i 参数int类型
// 16 从第16位开始
// f 参数float类型
// 20 从第20位开始

// 可以通过输出日志验证
NSLog(@"%s %s %s", @encode(unsigned short), @encode(Class), @encode(SEL));

方法缓存cache_t

Class内部结构中有个方法缓存cache_t,用散列表(哈希表) 来缓存曾经调用过的方法,可以提高方法的查找速度

在这里插入图片描述

objc-cache.mm文件里可以查看cache_t::insert函数,是通过一套哈希算法计算出索引,然后根据索引在散列表数组里直接插入数据进行缓存

void cache_t::insert(SEL sel, IMP imp, id receiver) {

    ....
    
    mask_t newOccupied = occupied() + 1;
    unsigned oldCapacity = capacity(), capacity = oldCapacity;
    
//  ....

    else {
        // 如果空间已满,那么就进行扩容,乘以2倍
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        
        // 将旧的缓存释放,清空缓存,然后设置最新的mask值
        reallocate(oldCapacity, capacity, true);
    }

    bucket_t *b = buckets();
    mask_t m = capacity - 1;
    
    // 通过 sel&mask 计算出索引(哈希算法)
    mask_t begin = cache_hash(sel, m);
    mask_t i = begin;

    do {
        // 通过索引找到的该SEL为空,那么就插入bucket_t
        if (fastpath(b[i].sel() == 0)) {
            incrementOccupied();
            b[i].set<Atomic, Encoded>(b, sel, imp, cls());
            return;
        }
        
        // 用索引从bucket里面取sel和传进来的sel做比较,如果一样证明已经存有,直接返回
        if (b[i].sel() == sel) {
            return;
        }
        
        // 从散列表里查找,如果上述条件不成立(索引冲突),那么通过cache_next计算出新的索引再查找插入
    } while (fastpath((i = cache_next(i, m)) != begin));

    bad_cache(receiver, (SEL)sel);
#endif // !DEBUG_TASK_THREADS
}
清空缓存cache_t::reallocate

当存储空间已满时,会进行扩容,并且将旧的缓存全部释放清空,然后设置最新的mask值,mask值是散列表的存储容量-1,也正好对应散列表的索引值

void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld) {
    bucket_t *oldBuckets = buckets();
    bucket_t *newBuckets = allocateBuckets(newCapacity);

    ASSERT(newCapacity > 0);
    ASSERT((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);

    setBucketsAndMask(newBuckets, newCapacity - 1);
   
    // 将旧的缓存和mask释放
    if (freeOld) {
        collect_free(oldBuckets, oldCapacity);
    }
}
哈希算法cache_hash
static inline mask_t cache_hash(SEL sel, mask_t mask) {
    uintptr_t value = (uintptr_t)sel;
#if CONFIG_USE_PREOPT_CACHES
    value ^= value >> 7;
#endif
    return (mask_t)(value & mask);
    // 与mask按位与,有时也可对mask取余%
}

如果计算出的索引在散列表中已经有了缓存数据,那么就通过cache_next更新下索引值,再去对应的位置插入缓存数据

更新索引值cache_next

通过源码可以看到计算方式如下:

#if CACHE_END_MARKER
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return (i+1) & mask;
}
#elif __arm64__
// arm64架构下如果索引非0,就是i-1,索引为0返回mask
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return i ? i-1 : mask;
}

有冲突的索引如果不为0就直接索引值减1,然后再根据新的索引值去散列表中对应插入
如果冲突的索引为0,那么直接就将mask赋值给新的索引值,再去对应查找插入

方法调用底层结构

比如一个实例或类去调用方法

Person* person = [[Person alloc] init];
[person test];
[Person test];

// 将他们转换成Cpp代码
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("test"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("test"));

发现函数调用最后都是转化为objc_msgSend,尤其我们可以推断出方法的调用本质就是objc_msgSend消息发送

简化后得到以下方法

//由于sel_registerName和@selector返回值都是SEL,我们通过打印两个方法的地址是一样的,可以确定两个方法是可以等同的
NSLog(@"%p, %p", @selector(test), sel_registerName("test")); // 打印结果是一样的

objc_msgSend(person, @selector(test));
// 消息接收者(receiver):person
// 消息名称:test

objc_msgSend([Person class], @selector(test));
// 消息接收者(receiver):[Person class]
// 消息名称:test

方法调用执行流程

objc_msgSend的执行流程可以分为3大阶段:消息发送、动态方法解析、消息转发

消息发送

我们在objc源码里全局搜索关键字objc_msgSend可以发现,消息发送的入口一开始是在objc-msg-arm64.s中通过汇编来实现第一步的

【第一步】 这里主要就是判断receiver是否有值,然后对应的跳转,isa指针ISA_MASK进行位运算获取到Class数据(GetClassFromIsa_p16),并且要先去查找是否有缓存方法,在CacheLookup里进行缓存查找的过程,主要就是SEL & MASK得出一个索引,根据索引去buckets散列表中取对应的方法数据,如果没有则要去更深的Class数据中查找了

【第二步】 上述一系列操作如果没有取到方法缓存,那么就会进到__objc_msgSend_uncached中,再进一步跳转到MethodTableLookup,发现最终会调用到C语言函数lookUpImpOrForward
知识点: C的函数名称对应到汇编中都会在其函数名之前再加上一个_,作为函数名称

【第三步】 跳转到objc-rumtime-new.mmlookUpImpOrForward函数来看,会到当前类的方法列表里查找,如果没有再去父类的方法缓存以及方法列表中查找,直到找到调用为止;如果都没有找到,那么就会进入到动态方法解析的阶段

第三步的方法查找详细步骤:
(1)根据传进来的类遍历查找,而且每次都要先去_cache_getImp中查找是否有方法缓存,进而调用回CacheLookup进一步查找
(2)在getMethodNoSuper_nolock里会找到class_rw_tmethods方法列表里进行遍历查找,在search_method_list_inline里会根据是否排序来选择是采用二分查找还是线性查找
(3)在log_and_fill_cache里将查找到的方法插入到缓存中,调用到了objc-cache.mm中的cache_t::insert函数

在这里插入图片描述

动态方法解析

如果消息发送阶段没有找到方法实现,那么就会进入到动态方法解析阶段

【第一步】 从objc源码中找到函数resolveMethod_locked,分别对类对象和元类对象做了不同的调用处理

static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior) {

    runtimeLock.assertLocked();
    ASSERT(cls->isRealized());

    runtimeLock.unlock();

    // 不是元类对象
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        resolveInstanceMethod(inst, sel, cls);
    } 
    else { // 是元类对象
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        resolveClassMethod(inst, sel, cls);
        
        // 这句印证了在元类对象里找类方法找不到,会去类对象里找同名的对象方法
        if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }

    // chances are that calling the resolver have populated the cache
    // so attempt using it
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}

注意: 能调用到这里,说明已经找到了基类的元类对象,如果还是没有,那么就会去基类的类对象里找同名的对象方法,正好印证了之前分析的元类对象的superclass指针指向类对象的原理

【第二步】 如果是类对象则会进入resolveInstanceMethod函数中,会走消息发送的流程去查找是否有实现resolveInstanceMethod方法,如果没有实现则返回;如果有实现就发送消息调用resolveInstanceMethod方法,并且再次走消息发送流程查找是否有实现对应的实例方法

如果是元类对象则会进入resolveClassMethod函数中,同类对象的动态方法解析大体相似;先会走消息发送的流程去查找是否有实现resolveClassMethod方法,如果没有实现则返回;如果有实现就发送消息调用
知识点:Method的底层实现struct method_t类型,所以可以理解为等价于struct method_t *

在这里插入图片描述

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(test)) {
        // 获取其他方法
        Method method = class_getInstanceMethod(self, @selector(other));
        class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncoding(method));

        return YES;
    }
    return [super resolveInstanceMethod: sel];
}

消息转发

如果没有进行动态分析的代码实现,就会进入到消息转发阶段
消息转发的源码是不开源的,所以我们只能通过一些其他的方法来分析其内部的实现

【第一步】 首先会查看是否实现forwardingTargetForSelector方法,如果实现了该方法并且返回值不为nil,那么会调用objc_msgSend进行消息发送流程;如果该方法未实现或者返回值为nil,就会调用方法签名methodSignatureForSelector

【第二步】 如果methodSignatureForSelector的返回值不为nil,那么就会调用forwardInvocation,如果该方法未实现或者返回值为nil,那么就会调用doesNotRecognizeSelector进行崩溃报错

【第三步】 如果forwardInvocation未实现,也会进行崩溃报错

在这里插入图片描述

#pragma mark 消息转发

// 当前类无法处理这个消息,就可以讲这个消息转发给别的类,进行消息转发
- (id)forwardingTargetForSelector:(SEL)aSelector {
    
    NSLog(@"%s", __func__);
    
    // aSelector == @selector(test)
    // 针对test进行验证
    
    // objc_msgSend([[Cat alloc] init], aSelector)
    if (aSelector == @selector(test)) {
//        return [[NSObject alloc] init];
        return nil;
        // 返回nil,就不会转发给备援接收者,就调用以下方法
    }
    
    return [super forwardingTargetForSelector: aSelector];
}

// 方法签名:包括返回值类型、参数类型
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSLog(@"%s", __func__);
    
    if (aSelector == @selector(test)) {
        return [NSMethodSignature signatureWithObjCTypes: "v16@0:8"];
        // 返回nil,就报错识别不出该方法
    }
    
    return [super methodSignatureForSelector: aSelector];
}

// NSInvocation封装了一个方法调用,包括:方法调用者、方法名、方法参数
//    anInvocation.target 方法调用者
//    anInvocation.selector 方法名
//    [anInvocation getArgument: NULL atIndex: 0];
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"%s", __func__);
//    anInvocation.target = [[Cat alloc] init];
//    anInvocation.selector = @selector(other);
//    [anInvocation invoke];
    
    // 开发者可以在此方法中自定义任何逻辑
    
    // 随便打印个日志
    NSLog(@"La la land....Do whatever I want....");
    
    // 更改方法调用者
    [anInvocation invokeWithTarget: [[Cat alloc] init]];
    
    // 更改方法调用者forwardingTargetForSelector方法就可以解决
    // 所以该方法里还可以有以下操作:
    
    // 拿到参数信息,传递的是地址值
    int age; // 3个参数:reciever、SEl、age,加了个age参数,字符串编码也要改变
    [anInvocation getArgument: &age atIndex: 2]; // 返回值为空,通过传递地址获取值
    
    // 拿到返回值信息
    int returnVal;
    [anInvocation getReturnValue: &returnVal];
}

super关键字

super本质

运行以下代码:

NSLog(@"[self class] = %@", [self class]); // Student
NSLog(@"[self superclass] = %@", [self superclass]); // Person

NSLog(@"[super class] = %@", [super class]); // Student
NSLog(@"[super superclass] = %@", [super superclass]); // Person

super调用class方法也会返回当前类,这是为什么呢?

通过clang命令将OC代码[super run];转换成C++代码,会发现super调用方法时底层调用的是objc_msgSendSuper函数

struct objc_super {
    __unsafe_unretained _Nonnull id receiver; // 消息接收者
    __unsafe_unretained _Nonnull Class super_class; // 消息接收者的父类
};

//    struct objc_super arg = {self, [Person class]};
//    objc_msgSendSuper(arg, @selector(run));
struct objc_super arg = {self, class_getSuperclass(objc_getClass("Student")};
objc_msgSendSuper(arg, sel_registerName("run"));

平时生成的C++代码仅为一个参考,实际实现还是得窥探中间代码的实现

objc_msgSendSuper函数的实现,发现最终会调用到_objc_msgSendSuper2函数,传进来的第一个结构体变量的真实类型是objc_super2,虽然第二个参数变成了当前类,但函数实现逻辑也有变化,其实现原理会根据当前类型的superclass指针去查找父类方法来调用
所以效果跟objc_superclass函数是一样的

struct objc_super2 {
    id receiver; // 消息接收者
    Class current_class; // 当前类
};

也就是说,self调用方法会从当前类开始向上查找方法实现,super调用方法会从父类开始向上查找方法实现

classsuperclass方法都声明在NSObject类,所以不论是self还是super调用都会找到NSObject类,而这两个方法的返回类型取决于消息接收者

  • class中传进去的self就是当前调用者,所以不论是[self class]还是[super class]得到的都是当前调用者的类型,也就是Student类型
  • superclass中获取的是当前调用者的父类,所以不论是[self superclass]还是[super superclass]得到的都是当前调用者的父类类型,也就是Person类型
+ (Class)class {
    return self;
}

- (Class)class {
    return object_getClass(self);
}

+ (Class)superclass {
    return self->getSuperclass();
}

- (Class)superclass {
    return [self class]->getSuperclass();
}

superclass方法的实现就是获取当前调用者类型的父类类型,所以[self superclass]还是[super superclass]得到的都是Person类型

经典问题

@interface Person : NSObject

@property (nonatomic, copy)NSString *name;
@property (nonatomic, strong)NSNumber* age;

- (void)print;

@end

@implementation Person

- (void)print {
    NSLog(@"my name is %@", self->_name, self->_age);
}

@end

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
	[super viewDidLoad];
	
    // 栈区的最高地址的局部变量其实是[UIViewControlle class]的地址
    // 下一个是self
//    struct objc_super arg = {self, [UIViewController class]};
//    objc_msgSendSuper(arg, sel_registerName("viewDidLoad"));
    

    
    __unused NSString* test = @"21yr";
//    __unused NSString* test2 = @"19yr";
//    __unused NSObject* obj2 = [[NSObject alloc] init];
    
    // 打印结果只认比cls地址高的地址值
   	id cls = [Person class];
   	void *obj = &cls;
   	[(__bridge id)obj print];
}
// my name is 21yr, my age is <ViewController: 0x10300f660><0x10300f660>
@end

OC方法print为什么能通过一个obj指针调用成功?

将上述代码用下图来表示关系可以看出,创建一个Person对象Person *person = [[Person alloc] init]

  • person指针指向isa指针的地址,isa又指向[Person class]类对象,
  • 等同于obj指向cls指针的地址,cls指向[Person class]类对象,

所以obj可以直接调用print函数

在这里插入图片描述

为什么会打印test字符串和ViewController对象?

super调用viewDidLoad方法本质是调用objc_msgSendSuper2函数

struct objc_super2 arg = {self, [ViewController class]};
objc_msgSendSuper2(arg, sel_registerName("viewDidLoad"));

函数调用栈(栈区)中存储局部变量的内存地址都是从小到大排列的

在这里插入图片描述

类比于person对象访问成员变量,self->_name就是在结构体里跳过前8个字节isa指针找到_name成员变量,等同于obj跳过cls找到test字符串,再跳就是结构体变量里的self,那么取得值就是ViewController的内存地址

struct Person_IMPL {
   Class isa;
   NSString *_name;
};

以后解决什么问题的话,都是跟我们的知识储备有关的,如果知识储备不够,你会发现很多问题是解决不了的,而且虽然我们现在学的是iOS的东西,但是底层学好了,你会发现许多东西是相通的,比如不管是C、C++还是OC或是其他语言,栈空间的内存分配都是从高地址向低地址分配的,像这种底层内存分配相关许多都是相通的,所以学好底层我觉得对我整个技术生涯是很有帮助的

LVVM的中间代码

LVVM是iOS编译器,OC -> 中间代码(.ll) -> 汇编、机器代码

在这里插入图片描述

中间代码有着LLVM特有的语法和跨平台的特性

Runtime常用API

创建一个Person类

@interface Person : NSObject {
    int _age;
    int _name;
}

- (void)run;

@end

@implementation Person

- (void)run {
    NSLog(@"%s", __func__);
}

@end

Person* person = [[Person alloc] init];
[person run]; // -[Person run]

获取类对象(isa所指)

NSLog(@"类对象:%p %p 元类对象:%p", [Person class], object_getClass(person), object_getClass([Person class]));

设置类对象(isa所指)

object_setClass(person, [Car class]);
[person run]; // -[Car run]

运行时动态创建一个类

void run(id self, SEL _cmd) {
    NSLog(@"%@ --- %@", self, NSStringFromSelector(_cmd));
}

Class dogClass = objc_allocateClassPair([NSObject class], "Dog", 0);

// 添加成员变量
class_addIvar(dogClass, "_age", 4, 1, @encode(int));
class_addIvar(dogClass, "_weight", 4, 1, @encode(int));
// 添加方法
class_addMethod(dogClass, @selector(run), (IMP)run, "v@:");
//        class_addProperty(<#Class  _Nullable __unsafe_unretained cls#>, <#const char * _Nonnull name#>, <#const objc_property_attribute_t * _Nullable attributes#>, <#unsigned int attributeCount#>);
//        class_addProtocol(<#Class  _Nullable __unsafe_unretained cls#>, <#Protocol * _Nonnull protocol#>);
        
// 注册一个类(要在类注册前添加成员变量)
objc_registerClassPair(dogClass);

id dog = [[dogClass alloc] init];
// 使用KVC访问成员变量
[dog setValue: @10 forKey: @"_age"];
[dog setValue: @20 forKey: @"_weight"];

NSLog(@"%@ %@ %@", dog, [dog valueForKey: @"_age"], [dog valueForKey: @"_weight"]); // <Dog: 0x6000039a0090> 10 20

// 调用方法
[dog run]; // <Dog: 0x6000039a0090> --- run
NSLog(@"%zd", class_getInstanceSize(dogClass)); // 8 + 4 + 4

// 在不需要类销毁释放
objc_disposeClassPair(dogClass);

类一旦注册完毕,类的结构就确定了,因为成员变量是只读ro的,所以成员变量在类的结构确定后就不能添加或修改,而方法是可读写rw的,所以方法可以在类的结构确定后动态地添加
为了规范,属性、成员变量、方法都最好在注册类之前添加好

判断是否为类对象、元类对象

// 判断对象是否为类对象
NSLog(@"%d %d %d",
      object_isClass(person), // 实例对象
      object_isClass([Person class]),
      object_isClass(object_getClass([Person class]))
      ); // 0 1 1
// 判断类是否为一个元类
NSLog(@"%d %d", class_isMetaClass([Person class]), class_isMetaClass(object_getClass([Person
class]))); // 0 1

获取类的父类(superclass所指)

// 获取类的父类(superclass所指)
NSLog(@"%@", class_getSuperclass([Person class]));
// 设置类的父类(superclass所指)
// class_setSuperclass([Car class], [Person class]); // deprecated
// NSLog(@"%@", class_getSuperclass([Car class])); // Person

成员变量

获取成员变量信息

Ivar ageIvar = class_getInstanceVariable([Person class], "_age");
NSLog(@"111 %s %s", ivar_getName(ageIvar), ivar_getTypeEncoding(ageIvar));
// 111 _age i

设置和获取成员变量的值

Ivar nameIvar = class_getInstanceVariable([Person class], "_name");
Person* p = [Person new];
object_setIvar(p, nameIvar, @"21");
// 不能直接传基本数据类型,所以先转成指针类型,再变为id类型
// object_setIvar(p, nameIvar, (__bridge id)(void *)21);
id ivar = object_getIvar(p, nameIvar);
NSLog(@"%@", ivar); // 21

拷贝实例变量列表(最后要用free释放)

unsigned int count;
Ivar* ivars = class_copyIvarList([Person class], &count);
NSLog(@"%u", count); // 获取成员变量的数量 2
for (int i = 0 ; i < count; ++i) {
    Ivar ivar = ivars[i];
    NSLog(@"222 %s %s", ivar_getName(ivar), ivar_getTypeEncoding(ivar));
    // 222 _age i
    // 222 _name i
}
free(ivars);

属性

在这里插入图片描述

方法

在这里插入图片描述

在这里插入图片描述

Runtime具体应用

动态地去修改内存中的一些东西(比如isa、类)

  • 利用关联对象给分类添加属性

  • 利用消息转发机制解决方法找不到的异常问题,进行一个兜底操作

  • 动态地去创建类

    // 写一个接口去创建类,@"aaa"为参数
    NSString* className = [NSString stringWithFormat: @"allocClass%@", @"aaa"];
    Class newClass = objc_allocateClassPair([NSObject class], className.UTF8String, 0);
    objc_registerClassPair(newClass);
    id newClassInstance = [[newClass alloc] init];
    NSLog(@"%@", newClassInstance);
    
  • 遍历类的所有成员变量(访问到私有成员变量,字典转模型、自动归档解档)

    • 用于访问系统类内部的一些私有成员变量

      unsigned int count;
      Ivar* ivars = class_copyIvarList([UITextField class], &count);
      for (int i = 0; i < count; ++i) {
          // 取出i位置的成员变量
          Ivar ivar = ivars[i];
          NSLog(@"%s %s", ivar_getName(ivar), ivar_getTypeEncoding(ivar));
      }
      
    • 字典(JSON)转模型

      // 写个分类,封装到NSObject里面去,注意命名规范
      + (instancetype)xy_objcetWithJson:(NSDictionary *)json {
          id obj = [[self alloc] init];
          unsigned int count;
          Ivar* ivars = class_copyIvarList(self, &count);
          for (int i = 0; i < count; ++i) {
              Ivar ivar = ivars[i];
              NSMutableString* name = [NSMutableString stringWithUTF8String: ivar_getName(ivar)];
          
              // 去掉成员变量的下划线
              [name deleteCharactersInRange: NSMakeRange(0, 1)];
          
              // 设值
              id value = json[name];
              if ([name isEqualToString: @"ID"]) {
                  value = json[@"id"];
              }
              [obj setValue: value forKey: name];
          }
          free(ivars);
          return obj;
      }
      

      写一个完整的字典转模型SDK,需要考虑的方面很多,比如,继承体系、JSON数据与Model不一致、键值对格式问题、模型嵌套模型等等,比较著名的第三方字典转模型框架有JSONModelYYModelMJExtension

  • 方法交换
    一般用于交换系统类内部方法的实现IMP、第三方框架中的方法,方法交换的原理就是交换类对象rw结构体methods中的method_list_t中的方法method _t结构体中的方法实现IMP指针(使用中间变量的交换方法),还会提前清空cache_t缓存flushCaches(nil)

    在这里插入图片描述

    • 利用方法交换进行容错,对NSArray、NSDictionary和NSString进行兜底操作,在分类中重写load方法(分类文件一旦参与编译,都会执行load方法)

      @implementation NSMutableDictionary (InsertNilProtect)
      + (void)load {
          // 类簇:NSString、NSArray、NSDictionary,真实类型是其他类型
          // 真实类型和实际调用方法可以在报错信息中查看
          Class cls = NSClassFromString(@"__NSDictionaryM");
          Method method1 = class_getInstanceMethod(cls, @selector(setObject:forKeyedSubscript:));
          Method method2 = class_getInstanceMethod(cls, @selector(xy_setObject:forKeyedSubscript:));
          method_exchangeImplementations(method1, method2);
      }
      // NSMutableDictionary中nil对象作为键值兜底
      - (void)xy_setObject:(id)obj forKeyedSubscript:(id<NSCopying>)key {
          if (!key) {
              NSLog(@"可变字典传入键值为空-xy_%@ %@", obj, key);
          } else {
              NSLog(@"xy_%@ %@", obj, key);
              [self xy_setObject: obj forKeyedSubscript: key];
          }
      }
      // NSMutableArray插入nil对象兜底
      - (void)xy_insertObject:(id)anObject atIndex:(NSUInteger)index {
          if (!anObject) {
              NSLog(@"可变数组传入对象为空-xy_%@ %zd", anObject, index);
          } else {
              NSLog(@"xy_%@ %zd", anObject, index);
              [self xy_insertObject: anObject atIndex: index];
          }
      }
      @end
      
    • 拦截UIButton点击事件
      给UIButton调用addTarget方法后,点击button响应方法实际是UIControl中的sendAction:to:forEvent:,这个方法会调用给button添加的事件函数,通过交换此方法可以拦截所有button的点击事件

      @implementation UIControl (HookUIButtonClick)
      
      + (void)load {
          Method method1 = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
          Method method2 = class_getInstanceMethod(self, @selector(xy_sendAction:to:forEvent:));
      method_exchangeImplementations(method1, method2);
      }
      
      
      // hook函数
      - (void)xy_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
      //    NSLog(@"%@-%@-%@", self, target, NSStringFromSelector(action));
          // 回到默认实现
      //    [target performSelector: action];
      
          if ([self isKindOfClass: [UIButton class]]) {
              // 拦截所有按钮的事件
              NSLog(@"%@-%@-%@", self, target, NSStringFromSelector(action));
          } else {
              // 系统方法原来的实现
              [self xy_sendAction: action to: target forEvent: event];
          }
      }
      
      @end
      

http://www.kler.cn/a/293806.html

相关文章:

  • 光流法与直接法在SLAM中的应用
  • 【AI写作宝-注册安全分析报告-无验证方式导致安全隐患】
  • Linux系统的网络设置
  • SwiftUI开发教程系列 - 第1章:简介与环境配置
  • leetcode day10 动态规划篇 64+139
  • VMWare虚拟机NAT模式下与外部主机(非宿主机)通信
  • 以低代码技术加速推动企业数字创新
  • 一款可以替代Notepad++的免费高级文本编辑器
  • 数字人模型像素流送实时渲染网页手机平板用语音交互
  • 363_C++_配合360_负责读取和处理录像数据RecordReader类
  • OrangePi AIpro 香橙派 昇腾 Ascend 开发系列
  • AIOT边缘计算机助力智慧储能,开启能源管理新时代
  • 【秋招笔试】9.06去哪儿秋招改编题(第一套)-三语言题解
  • 修改打包后element-ui的字体文件名;JS文件名;CSS文件名
  • 启动.cmd文件一闪而过,看不到报错信息
  • (二)十分简易快速 自己训练样本 opencv级联lbp分类器 车牌识别
  • 策略模式的小记
  • GS-SLAM论文阅读笔记--LoopSplat
  • 智能新纪元:GPT-Next引领的AI革命及其跨领域应用
  • Identifying User Goals from UI Trajectories论文学习
  • SpringBoot整合Minio及阿里云OSS(配置文件无缝切换)
  • MySQL · 性能优化 · 提高查询效率的实用指南(下)
  • 用Python导入CSV和Excel表格数据到Word表格
  • 启动 Spring Boot 项目时指定特定的 application.yml 文件位置
  • 无人机种类详解!!!
  • DPDK基础入门(四):从源码角度解析同步互斥机制