IOS开发Objective-C Runtime使用示例详解

目录
  • 前言
  • 一些关键字
  • 消息传递 (Messaging)
  • KVO
  • 关联对象 (Associated Objects)
  • AOP(Method Swizzling)
  • 其它

前言

Runtime 是使用 C 和汇编实现的运行时代码库,Objective-C 中有很多语言特性都是通过它来实现。了解 Runtime 开发可以帮助我们更灵活的使用 Objective-C 这门语言,我们可以将程序功能推迟到运行时再去决定怎么做,还可以利用 Runtime 来解决项目开发中的一些设计和技术问题,使开发过程更加具有灵活性。

一些关键字

  • self:类的隐藏参数变量,指向当前调用方法的对象
  • super:是编译器的标示符,通过 super 调用方法会被翻译成 objc_msgSendSuper(self, _cmd,…)
  • SEL:以方法名为内容的 C 字符串
  • IMP:指向方法实现的函数指针
  • id:指向类对象或实例对象的指针
  • isa:为 id 对象所属类型 (objc_class),Objc 中的继承就是通过 isa 指针找到 objc_class,然后再通过 super_class 去找对应的父类
  • metaclass:在 Objc 中,类本身也是对象,实例对象的 isa 指向它所属的类,而类对象的 isa 指向元类 (metaclass),元类的 isa 直接指向根元类,根元类的isa指向它自己,它们之间的关系如下图所示。

消息传递 (Messaging)

Objective-C 对于调用对象的某个方法这种行为叫做给对象发送消息,实际上就是沿着它的 isa 指针去查找真正的函数地址。下面我们来了解一下这个过程:

我们写一个给对象发送消息的代码

[array insertObject:obj atIndex:5];

编译器首先会将上面代码翻译成这种样子

objc_msgSend(array, @selector(insertObject:atIndex:), obj, 5);

系统在运行时会通过 array 对象的 isa 指针找到对应的 class(如果是给类发消息,则找到的是metaclass),然后在 class 的 cache 方法列表中用 SEL 去找对应 method,如果找不到便去 class 的方法列表中去找,如果在方法列表中也找不对对应 method 时,便沿着继承体系继续向上查找,找到后将 method 放入 cache,以便下次能快速定位,然后再去执行 method 的 IMP,找不到时系统便报错:unrecognized selector sent to insertObject:atIndex:

Runtime 提供了三种方法避免因为找不到方法而崩溃

当找不到方法实现时,Runtime 会先发送 +resolveInstanceMethod: 或 +resolveClassMethod: 消息,我们可以重写它然后为对象指定一个处理方法。

void dynamicXXXMethod(id obj, SEL _cmd) {
    NSLog(@"ok...");
}
+ (BOOL)resolveInstanceMethod:(SEL)aSEL {
    if(aSEL == @selector(xxx:)) {
        class_addMethod([self class], aSEL, (IMP)dynamicXXXMethod, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod];
}

class_addMethod 方法的最后一个参数用来指定所添加方法的参数及返回值,叫 Type Encodings

如果 resolve 方法返回 NO,Runtime 会发送 -forwardingTargetForSelector: 消息,允许我们将消息转发给能处理它的其它对象。

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if(aSelector == @selector(xxx:)){
        return otherObject;
    }
    return [super forwardingTargetForSelector:aSelector];
}

当 -forwardingTargetForSelector: 返回 nil 时,Runtime 会发送 -methodSignatureForSelector: 和 -forwardInvocation: 消息。我们可以选择忽略消息、抛出异常、将消息转由当前对象或其它对象的任意消息来处理。

//根据 SEL 生成 NSInvocation 对象,然后再由 -forwardInvocation: 方法进行转发。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
    if (!signature) {
        signature = [otherObject instanceMethodSignatureForSelector:aSelector];
    }
    return signature;
}
- (void)forwardInvocation:(NSInvocation *)invocation {
    SEL sel = invocation.selector;
    if([otherObject respondsToSelector:sel]) {
        [invocation invokeWithTarget:otherObject]; // 转发消息
    }
    else {
        [self doesNotRecognizeSelector:sel]; // 抛出异常
    }
}

KVO

当我们为对象添加观察者后,Runtime 会在运行时创建这个对象所在类的子类,并且将该对象的 isa 指针指向这个子类,然后重写监听属性的 set 方法并在方法中调用 -willChangeValueForKey: 和 -didChangeValueForKey: 来通知观察者,所以如果直接修改实例变量便不会触发监听方法。当移除观察者后,Runtime 便会将这个子类删除。

所以 isa 指针并不总是指向实例对象所属的类,也有可能指向一个中间类,所以不能依靠它来确定类型,而是应该用 class 方法来确定实例对象的类。

关联对象 (Associated Objects)

在 Category 中可以为类添加实例方法或类方法,但是不支持添加实例变量,所以即使我们在 Category 中为类添加了 property,也不能直接使用它,Runtime 可以解决这个问题,我们只需要定义一个指针,然后通过 objc_setAssociatedObject 方法将指针与对象进行关联并指定内存管理方式,数据以 KeyValue 的形式存储在一个 HashMap 里。

Objc 中的类和对象都是结构体,Category 也是这样,定义的方法和属性在结构体中的存储,并在运行时按倒序添加到主类中(添加的方法会放在方法列表的上面),所以如果添加的方法与原类中的一样,那么在调用此方法时,优先找到的便是我们添加的这个方法。如果有多个 Category 添加同样名称的方法,那么这些方法在方法列表中的顺序取决于他们的编译顺序,也就是这些 Category 文件在 Compile Sources 中的顺序。

@interface NSObject (JC)
@property (nonatomic, copy) NSString *ID;
@end
@implementation NSObject (JC)
static const void *IDKey;
- (NSString *)ID {
    return objc_getAssociatedObject(self, &IDKey);
}
- (void)setID:(NSString *)ID {
    objc_setAssociatedObject(self, &IDKey, ID, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
@end

AOP(Method Swizzling)

我们可以通过继承、Category、AOP 方式来扩展类的功能。

  • 继承比较适合在设计底层代码架构时使用,不适当的使用会让代码看起来很啰嗦,并且增加维护难度。
  • Category 适合为现有类添加方法。
  • 当需要修改现有类的方法并且拿不到源码时,继承和 AOP 都能解决问题,但是用 AOP 来解决代码耦合度更低。其实就算能拿到源码,往往直接去改源码也不是个好办法。

在 Objective-C 中,可以通过 Method Swizzling 技术来实现 AOP,下面我们通过交换两个方法的实现代码来向已存在的方法中添加其它功能。

#import <objc/runtime.h>
@implementation UIViewController (Tracking)
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class aClass = [self class];
        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(swizzled_viewWillAppear:);
        Method originalMethod = class_getInstanceMethod(aClass, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(aClass, swizzledSelector);
        // 如果要对类方法进行交换,使用下面注释的代码
        // Class aClass = object_getClass((id)self);
        //
        // Method originalMethod = class_getClassMethod(aClass, originalSelector);
        // Method swizzledMethod = class_getClassMethod(aClass, swizzledSelector);
 		// 交换两个方法的实现
 		// 防止 aClass 不存在 originalSelector,所以添加一下试试,但指向地址为新方法地址
        BOOL didAddMethod = class_addMethod(aClass, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        if (didAddMethod) {
        	// 添加成功,说明 aClass 不存在 originalSelector,所以替换 swizzledSelector 的 IMP 为 originalMethod,实质上它们都指向 swizzledMethod
            class_replaceMethod(aClass, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        }
        else {
         	// 添加失败,说明 aClass 存在 originalSelector,直接交换
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}
#pragma mark - Method Swizzling
// 由于方法实现已经被交换,所以系统在调用 viewWillAppear: 时,实际上会调用 swizzled_viewWillAppear:
- (void)swizzled_viewWillAppear:(BOOL)animated {
	// 下面代码表面上看起来会引起递归调用,由于函数实现已经被交换,实际上会调用 viewWillAppear:
   [self swizzled_viewWillAppear:animated];
	// 在原有基础上添加其它功能(写日志等)
}
@end

使用 Method Swizzling 需要注意下面几个问题

  • 需要在 +load 方法中执行 Method Swizzling,+initialize 方法有可能不会被调用
  • 避免父类与子类同时 hook 父类的某方法,避免不了时至少要保证不在 +load 方法中执行 super.load(),否则父类中的 +load 方法会被执行两次
  • 需要在 dispatch_once 中执行,避免因多线程等问题倒致的偶数次交换后失效的问题
  • 如果你用了 swizzled_viewWillAppear 作为方法名,那么如果你引用的第三方 SDK 中也用了这个方法名来做方法交换,那会造成方法的递归调用,所以你最好换一个不太会被重复使用的方法名,例如 mx_swizzled_viewWillAppear
  • 即便使用 mx_swizzled_viewWillAppear 尽量避免了与第三方库或自己项目中别的地方对 viewWillAppear 交换倒致的递归调用问题,仍然会存在调用顺序问题,解决办法就是在 Build Phases 中调整类文件的顺序

其它

我们可以通过 Runtime 特性来获得类的所有属性名称和类型,然后再通过 KVC 将 JSON 中的值填充给该类的对象。还可以在程序运行时为类添加方法或替换方法从而使对象能够更灵活的根据需要来选择实现方法。总之 Runtime 库就象一堆积木,只要发挥想象力便能实现各种各样的功能,但前提是你需要了解它。

以上就是Objective-C Runtime 开发示例详解的详细内容,更多关于Objective-C Runtime 开发的资料请关注我们其它相关文章!

(0)

相关推荐

  • Objective-C优雅使用KVO观察属性值变化

    目录 引言 KVOController YYCategories 引言 KVO 是苹果为我们提供的一套强大的机制,用于观察属性值的变化,但是大家在日常开发中想必多少也感受到了使用上的一些不便利,比如: 添加观察者和移除观察者的次数需要一一对应,否则会 Crash. 添加观察者和接受到属性变更通知的位置是分开的,不利于判断上下文. 多次对同一个属性值进行观察,会触发多次回调,影响业务逻辑. 为了解决上述三个问题,业界提出了一些方便开发者的开源方案,我们一起来看一下. KVOController K

  • iOS Objective-c实现左右滑动切换页面

    本文实例为大家分享了iOS Objective-c实现左右滑动切换页面的具体代码,供大家参考,具体内容如下 ScrollView + n个view 1.storyboard布局一个ScrollView 2.拖出两个输出口,定义三个属性 @property (weak, nonatomic) IBOutlet UIScrollView *XMScrollView; @property (weak, nonatomic) IBOutlet UIView *scrollContentView; ///

  • Objective-C关键字@property使用原理探究

    目录 @property 主要包含内容 存取器方法 读写权限 内存管理 数据结构 清除weak 添加weak 原子性 总结 @property @property是OC开发中常用到的关键字,今天这篇文章就为它做一个较为系统全面的总结 主要包含内容 接下来我会分别解析 存取器方法 一般访问存取器方法只需要使用.propertyName即可,需要特别指定存取器方法时可通过getter=getterName与setter=setterName,具体示例如下: // 指定getter访问名为isOpen

  • Objective-C const常量的优雅使用方法

    目录 正文 Objective-C 的常量声明方式 在 Objective-C 中使用 let 来声明常量 正文 在编写代码时经常要使用常量,来替代 magic number.比较简单的做法是通过预处理指令 #define 来实现. #define ANIMATION_DURATION 0.3 上述预处理指令会在编译时的预处理阶段会将代码中 ANIMATION_DURATION 字符串替换为 0.3.这种定义常量的方式比较简便,但是存在两个问题: 丢失了类型信息. 若该预处理指令声明在头文件中,

  • Objective-C之Category实现分类示例详解

    目录 引言 编译时 运行时 引言 在写 Objective-C 代码的时候,如果想给没法获得源码的类增加一些方法,Category 即分类是一种很好的方法,本文将带你了解分类是如何实现为类添加方法的. 先说结论,分类中的方法会在编译时变成 category_t 结构体的变量,在运行时合并进主类,分类中的方法会放在主类中方法的前面,主类中原有的方法不会被覆盖.同时,同名的分类方法,后编译的分类方法会“覆盖”先编译的分类方法. 编译时 在编译时,所有我们写的分类,都会转化为 category_t 结

  • iOS开发之Objective-c的Runtime理解指南

    目录 一.Runtime 1.概念: 2.特性:编写的代码具备有运行时.动态特性,从而衍生出 以下4.5 3.原理:Runtimer在Object-c的使用 程序在三个不同的层次上与运行时系统交互: 4.作用: 5.典型事例: 6.Objc-msgSend所做的事情 7.消息传递的关键要素 8.Msg_sender机制:先查询本类是否又该方法的实现--->如果没有逐级找父类,还有一个快速映射表(提高性能)---> 匹配方法 ---> 设置一个执行者---> 消息转发 --->

  • iOS开发探索多线程GCD任务示例详解

    目录 引言 同步任务 死锁 异步任务 总结 引言 在上一篇文章中,我们探寻了队列是怎么创建的,串行队列和并发队列之间的区别,接下来我们在探寻一下GCD的另一个核心 - 任务 同步任务 void dispatch_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block); 我们先通过lldb查看其堆栈信息,分别查看其正常运行和死锁状态的信息 我们再通过源码查询其实现 #define _dispatch_Block_

  • iOS开发探索多线程GCD队列示例详解

    目录 引言 进程与线程 1.进程的定义 2.线程的定义 3. 进程和线程的关系 4. 多线程 5. 时间片 6. 线程池 GCD 1.任务 2.队列 3.死锁 总结 引言 在iOS开发过程中,绕不开网络请求.下载图片之类的耗时操作,这些操作放在主线程中处理会造成卡顿现象,所以我们都是放在子线程进行处理,处理完成后再返回到主线程进行展示. 多线程贯穿了我们整个的开发过程,iOS的多线程操作有NSThread.GCD.NSOperation,其中我们最常用的就是GCD. 进程与线程 在了解GCD之前

  • ios开发中的容错处理示例详解

    前言 后台服务器返回给客户端的值有时会是null,有时会是"<null>",直接赋值并进行后续操作有时会导致崩溃. 之前的处理方式都是尽量让后台服务器返回数据时不返回null或者是"<null>",但是他们还是时不时返回这些数据,所以app时不时就会出现闪退现象.一出现这种问题,调试后发现是他们返回null或者是"null"的数据类型,因为是线上的问题,所以让他们直接在后台将出现问题的字段进行处理就好了.久而久之,发现这种

  • IOS开发基础之二维数组详解

    IOS开发基础之二维数组详解 首先我们知道OC中是没有二维数组的,二维数组是通过一位数组的嵌套实现的,但是别忘了我们有字面量,实际上可以和C/C++类似的简洁地创建和使用二维数组.这里总结了创建二维数组的两种方法以及数组的访问方式. 通过字面量创建和使用二维数组(推荐) // 1.字面量创建二维数组并访问(推荐) NSArray *array2d = @[ @[@11,@12,@13], @[@21,@22,@23], @[@31,@32,@33] ]; // 字面量访问方式(推荐) NSLog

  • IOS 开发中画扇形图实例详解

    IOS 开发中画扇形图实例详解 昨天在做项目中,遇到一个需要显示扇形图的功能,网上搜了一下,发现code4app里面也没有找到我想要的那种类似的效果,没办法了,只能自己学习一下如何画了. 首先我们需要了解一个uiview的方法 -(void)drawRect:(CGRect)rect 我们知道了这个方法,就可以在自定义UIView的子类的- (void)drawRect:(CGRect)rect里面绘图了,关于drawrect的调用周期,网上也是一找一大堆,等下我会整理一下,转载一篇供你们参考.

  • Android开发Kotlin实现圆弧计步器示例详解

    目录 效果图 定义控件的样式 自定义StepView 绘制文本坐标 Android获取中线到基线距离 效果图 定义控件的样式 看完效果后,我们先定义控件的样式 <!-- 自定义View的名字 StepView --> <!-- name 属性名称 format 格式 string 文字 color 颜色 dimension 字体大小 integer 数字 reference 资源或者颜色 --> <declare-styleable name="StepView&q

  • ios开发UITableViewCell图片加载优化详解

    目录 前言 图片自适应比例 XHWebImageAutoSize 仅加载当前屏幕的内容 预加载 前言 我们平时用UITableView用的很多,所以对列表的优化也是很关注的.很多时候,我们设置UIImageView,都是比例固定好宽高的,然后通过 scaleAspectFill 和 clipsToBounds 保持图片不变形,这样子做开发的效率是很高的,毕竟图片宽高我们都是固定好的了. 那如果产品要求图片按真正的比例展示出来呢?如果服务器有返回宽和高,那就好办了,那如果没有呢,我们应该怎么去做呢

  • Python开发自定义Web框架的示例详解

    目录 开发自定义Web框架 1.开发Web服务器主体程序 2.开发Web框架主体程序 3.使用模板来展示响应内容 4.开发框架的路由列表功能 5.采用装饰器的方式添加路由 6.电影列表页面的开发案例 开发自定义Web框架 接收web服务器的动态资源请求,给web服务器提供处理动态资源请求的服务.根据请求资源路径的后缀名进行判断: 如果请求资源路径的后缀名是.html则是动态资源请求, 让web框架程序进行处理. 否则是静态资源请求,让web服务器程序进行处理. 1.开发Web服务器主体程序 1.

  • java开发RocketMQ生产者高可用示例详解

    目录 引言 1 消息 1.1 topic 1.2 Body 1.3 tag 1.4 key 1.5 延迟级别 2 生产者高可用 2.1 客户端保证生产者高可用 2.1.1 重试机制 2.1.2 客户端容错 2.2 Broker端保证生产者高可用 引言 前边两章说了点基础的,从这章开始,我们挖挖源码.看看RocketMQ是怎么工作的. 首先呢,这个生产者就是送孩子去码头的家长,孩子们呢,就是消息了. 我们看看消息孩子们都长啥样. 1 消息 public class Message implemen

  • IOS开发Swift 与 OC相互调用详解

    目录 1.创建桥接文件 2.Swift调用OC NS_SWIFT_NAME.NS_SWIFT_UNAVAILABLE NS_REFINED_FOR_SWIFT 规则 3.OC调用Swift 4.坑点 1.创建桥接文件 在创建另一种语言的文件时XCode会提示创建项目名-Bridging-Header.h的桥接文件 2.Swift调用OC 1.创建OC文件 #import "MyViewController.h" @interface MyViewController () @end @

随机推荐