iOS底层原理总结 - Runtime(5)RunTime APi

434 阅读5分钟

129-Runtime41-LLVM的中间代码

LLVM 编译器 转换OC 的本质过程

OC -> 中间代码(.ll) -> 汇编、机器代码

可以使用以下命令行指令生成中间代码

clang -emit-llvm -S main.m

Runtime 相关API

创建类 和注册类,释放类及改变对象的指针

        // 创建类
        Class newClass  =  objc_allocateClassPair([NSObject class], "YZDog", 0);
        
        class_addIvar(newClass, "_age", 4, 1, @encode(int));
        class_addIvar(newClass, "_weight", 4, 1, @encode(int));
        class_addMethod(newClass, @selector(run), (IMP)run, "v@:");

        //注册类
        objc_registerClassPair(newClass);
        
        id dog = [[newClass alloc] init];

        [dog setValue:@10 forKey:@"_age"];
        [dog setValue:@20 forKey:@"_weight"];
        [dog run];
        
        NSLog(@"%zd---%@------%@",class_getInstanceSize(newClass ),[dog valueForKey:@"_age"],[dog valueForKey:@"_weight"]);

成员变量必须要在注册类前添加进去,

因为根据对象的内存结构,成员变量是ro 类型的(readOnly)

对象方法变量不必在注册类前添加进去

方法列表是 class_rw_t 可读可写的。 比如动态解析方法的时候,就是随时添加的。

释放类

    // 在不需要这个类的时候释放
    objc_disposeClassPair(newClass);

object_setClass()

    YZPerson *person = [YZPerson new];
   [person run];
   object_setClass(person, [YZCat class]);
   [person run];

将此实例对象的指针指向 [YZCat class]

获取和设置成员变量的信息

// 获取成员变量的信息
        Ivar ageIvar = class_getInstanceVariable([YZPerson class], "_age");
        NSLog(@"%s--%s",ivar_getName(ageIvar),ivar_getTypeEncoding(ageIvar));
        
        // 设置和获取成员变量的值
        Ivar nameIvar = class_getInstanceVariable([YZPerson class], "_name");
        YZPerson *person = [[YZPerson alloc] init];
        
        object_setIvar(person, nameIvar, @"123");
        
        NSLog(@"----%@",person.name);

应用 设置textField 的提示文字的颜色(已启用,用私有成员变量总是不好的)

    self.textField.placeholder = @"123";
    
    [self.textField setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor"];
    
    
    
    // 成员变量的数量
    unsigned int count ;
    Ivar *ivars  = class_copyIvarList([UITextField class], &count);
    
    for (int i=0 ; i < count; i++) {
        
        Ivar ivar = ivars[i];
        
        NSLog(@"%s---%s",ivar_getName(ivar),ivar_getTypeEncoding(ivar));
    }
    
    free(ivars);

  1. 打印出所有的成员变量。
  2. 找到可能是其提示文字的label
  3. 通过KVC 设值
  4. 注意kvc 时 forkeyPath 和key 的区别

Ivars * 数组可以当做指针来用

方法

替换方法 class_replaceMethod()

YZPerson *person = [[YZPerson alloc] init];
        
        
        class_replaceMethod([YZPerson class], @selector(run), (IMP)myrun , "v");
        [person run];
        
        打印
        ---myrun

将block 报装成imp

        YZPerson *person = [[YZPerson alloc] init];

        class_replaceMethod([YZPerson class], @selector(run), imp_implementationWithBlock(^{
            
            NSLog(@"123");
        }) , "v");

        [person run];
        
        打印
        123

交换方法 method_exchangeImplementations

        Method runMethod = class_getInstanceMethod([YZPerson class], @selector(run));

        Method testMethod = class_getInstanceMethod([YZPerson class], @selector(test));
        method_exchangeImplementations(runMethod, testMethod);
        
        YZPerson *person = [[YZPerson alloc] init];
        
        [person run];
        打印 person test 

也可交换不同类的方法

        Method runMethod = class_getInstanceMethod([YZPerson class], @selector(run));
        
        Method runCat = class_getInstanceMethod([YZCat class], @selector(run));

        
        method_exchangeImplementations(runMethod, runCat);
        
        YZPerson *person = [[YZPerson alloc] init];
        
        [person run];

打印
yzcat run 

交换方法的具体应用 (交换系统的底层方法)

比如交换按钮的点击事件

  1. 在按钮的根方法中交换事件
  2. 为什么不用给button引入头文件就能实现交换方法(因为在load中交换了方法)
    • load 中交互方法最好加 GCD Once方法保证只调用一次。
+ (void)load {
    Method method1 = class_getInstanceMethod([self class], @selector(sendAction:to:forEvent:));

    Method method2 = class_getInstanceMethod([self class], @selector(yz_sendAction:to:forEvent:));

    method_exchangeImplementations(method1, method2);
}

- (void)yz_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
    
    // 会造成死循环,因为内部的实现方法已经交换
//    [self sendAction:action to:target forEvent:event];

    // 不会造成死循环
    [self yz_sendAction:action to:target forEvent:event];

    NSLog(@"123");
}

注意可能会造成的死循环

交换方法的具体应用 (交换系统的底层方法 之数组类簇)

数组中不能插入nil ,此时报的错为

[__NSArrayM insertObject:atIndex:]: object cannot be nil'

__NSArrayM 是NSMutableArray的真实类型

类簇知识扩展

  1. 类簇在iOS中的应用 NSNumber有两个比较常用的类方法
NSNumber *boolNumber = [NSNumber numberWithBool:YES];
NSLog(@"%@", [[boolNumber class] description]);
输出__NSCFBoolean
NSNumber *intNum = [NSNumber numberWithInt:1];
NSLog(@"%@", [[intNum class] description]);
会输出__NSCFNumber

[instance class]方法返回的当前的对象的类的名称,[NSNumber numberWithInt:1]和[NSNumber numberWithBool:YES]通过上面的验证可以看出明显不是同一个类,而且__NSCFNumber和__NSCFBoolean很明显是一个私有类。

  1. 为什么要这样做 ?

以NSNumber为例,我们知道NSNumber可以存储很多类型的数据,如int、Float、Double等等,具体支持哪些数据类型可以到NSNumber的头文件中查看。

  1. 一般情况下实现类似的效果一种方式是把NSNumber作为基类,然后分别去实现各自的子类,如下图所示

但是一旦需要实现的子类多起来之后就会发现这样需要继承的子类太多,比如如果要仿NSNumber需要写这样十多个类。 最好的就是把这些子类写成私有的类,所有对外都在NSNumber中调用即可,对于使用者来说就轻松很多。

4,类簇的应用 现在很多应用需要同时兼容iOS6、iOS7目前还需要针对iOS8进行适配。为了同时支持iOS6和iOS7,也在各个系统上显示的效果符合系统原生的风格,就可以采用类簇的设计进行设计。 外界调用者并不区分具体的iOS系统而直接调用,系统的适配在.m文件中。

runtime 小结

讲一下 OC 的消息机制

1,OC中的方法调用其实都是转成了objc_msgSend函数的调用,给receiver(方法调用者)发送了一条消息(selector方法名)

2,objc_msgSend底层有3大阶段 消息发送(当前类、父类中查找)、动态方法解析、消息转发

什么是Runtime?

  1. OC是一门动态性比较强的编程语言,允许很多操作推迟到程序运行时再进行 OC的动态性就是由Runtime来支撑和实现的.
  2. Runtime是一套C语言的API,封装了很多动态性相关的函数 平时编写的OC代码,底层都是转换成了Runtime API进行调用.

具体应用

  1. 利用关联对象(AssociatedObject)给分类添加属性
  2. 遍历类的所有成员变量(修改textfield的占位文字颜色、字典转模型、自动归档解档)
  3. 交换方法实现(交换系统的方法)
  4. 利用消息转发机制解决方法找不到的异常问题