Objective-C的Runtime机制的应用示例总结

Objective-C是一门动态语言,不同于许多静态语言,例如C语言,只能在编译和链接阶段把程序运行的上下文做好,在运行期间,无法修改,缺少动态性。Objective-C的动态性,给开发者提供了一种在运行期,修改程序执行流程的机会,这要归功于其强大的Runtime机制。
这篇文章主要介绍,目前,Runtime机制在我们项目中的应用场景。

  • 前言

ObjC语言中,runtime运行机制主要依赖于两个头文件

#include <objc/runtime.h>
#include <objc/message.h>

其中runtime.h声明了一些类,实例变量操作相关的东西,message.h声明了一些消息发送相关的东西。
下面列举一下,runtime.h中常用的方法,以及它们的用法。

object_getClass:    获取一个对象所属的类
object_getIvar:     读取一个对象中某个变量的值
objc_getClass:      通过字符串获取到类Class
class_getSuperclass:获取一个类的父类
class_getInstanceMethod:获取实例方法,返回Method
class_getMethodImplementation:获取实例方法的实现
class_getProperty:      获取某个类的属性
class_addMethod:        增加方法。
...等等

上面大部分列举了get相关的方法。runtime.h提供了获取(get),设置(set)(类的属性,实例变量,实例方法,类方法)的操作。

下面列举一下message.h的相关方法。

objc_msgSend:       给对象发送消息。
objc_msgSendSuper:  向父类的发送消息

message.h类,主要提供了这两个关键的方法。

  • 下面总结一下Runtime有哪些使用场景,并通过具体的代码,来说明各种使用场景如何运用Runtime。

总结了如下四种应用场景:

 1.动态拼接URL
 2.交换两个方法的实现(用自己的方法,替换系统的方法)
 3.给系统已有的方法添加新的功能(不影响系统方法原来的功能)
 4.消息转发中Runtime的应用

下面结合项目中使用到Runtime的地方进行说明,并列举了一些代码示例。

  • 第一个使用场景:动态拼接URL

    在做app开发的时候,肯定会遇到http请求,URL拼接的问题。初期大部分采取的方案是,采用NSString的格式化方法,自己手动去拼接URL. 例如:

    NSMutableString *stringUrl = [NSMutableString stringWithFormat:@"http:mytest/host/code=%@",code];
    [stringUrl appendString:@"&uin=45"];
    [stringUrl appendString:@"&my=5"];
    [stringUrl appendString:@"&your=6"];

这样写有几个问题:
1. 手动书写,很容易写错。
2. 如果很多类似的请求,都需要uin或者其他通用的参数,那么就会每个请求都需要写一遍。不满足OOP的特性。
3. 参数所代表的含义不够清晰,需要开发去猜测。
解决方法:
把url拼接,抽象成一个类。
上面的参数code, uin,my,your可以当成类的属性。通用的属性有uin,可以抽象出一个基类。实现方法如下:
CURLParamBase类

@interface CURLParamBase : NSObject

@property (nonatomic, strong)       NSString        *uin;

@end

需要构造的请求类XXX,如下定义:

@interface CURLParamXXX : CURLParamBase

@property (nonatomic, strong)       NSString*   my;
@property (nonatomic, strong)       NSString*   your;

@end

使用:定义CURLParamXXX的类的实例,使用Runtime来获取属性名和属性的value来拼接参数,如下:

     unsigned int propertyCount;
     //获取所有属性
     objc_property_t *properties =
        class_copyPropertyList(class, &propertyCount);
     for (unsigned int i = 0; i < propertyCount; i++)
     {
          objc_property_t property = properties[i];
          const char *propertyName = property_getName(property);
//获取实例变量,某个属性的值       
object_getInstanceVariable(self,propertyName,&value);
     }

-第二个使用场景:交换两个方法的实现

使用newSEL方法名,新的newIMP实现,替换原有的origSEL方法及其实现。
实现代码如下,代码中添加了相关的注释。

/**
 **新方法的实现替换旧方法
 **成功:返回YES,
 **失败:返回NO.
 */
BOOL replaceMethodNewImpl(Class c, SEL origSEL, SEL newSEL, IMP newIMP)
{
    //新方法已经存在
    if ([c instancesRespondToSelector:newSEL])
    {
        return YES;
    }

    //旧方法
    Method origMethod = class_getInstanceMethod(c, origSEL);

    //先把新方法加入到class的方法列表中
    if (!class_addMethod(c, newSEL, newIMP, method_getTypeEncoding(origMethod)))
    {
        //如果加入失败,直接返回
        NSLog(@"Failed to add method:%@ on %@",NSStringFromSelector(newSEL),c);
        return NO;
    }
    else
    {
        //如果加入成功
        Method newMethod = class_getInstanceMethod(c, newSEL);
        //给original添加新的实现
        //有可能失败失败原因:(for example, the class already contains a method implementation with that name).
        if (class_addMethod(c, origSEL, method_getImplementation(newMethod), method_getTypeEncoding(origMethod)))
        {
            //新方法的实现替换成旧方法的实现。
            class_replaceMethod(c, newSEL, method_getImplementation(origMethod), method_getTypeEncoding(newMethod));
        }
        else
        {
            //交换实现
            method_exchangeImplementations(origMethod, newMethod);
        }
    }

    return YES;
}
  • 第三个使用场景:给系统已有的方法添加功能
    例如给系统的UIView下的方法drawRect,添加NSLog的功能,这个可以参考上面说明的第三种使用场景。变化的只是,新方法的实现要去调用老的方法的实现。在override_drawRect方法中,调用drawRect方法。代码如下:
 - (void)override_drawRect:(CGRect)r
{
    // 调用旧的实现。因为它们已经被替换了
    [self override_drawRect: r];

    NSLog(@"rect = %@",NSStringFromCGRect(r));
}
  • 第四使用场景:消息转发
    开发过程中,经常遇到unrecognized selector sent to instance 0x87 Terminating app due to uncaught exception
    NSInvalidArgumentException’, 这个问题。
    这是什么原因?直观上看,是系统没有处理某个消息。
    情况分两种,第一,接受消息的对象错了。第二,对象没错,发送的消息不对。
    消息转发的整个流程如下图所示:
    消息转发过程
    总体来说,就是给某个实例,发送某个消息。
    首先如果没找到响应方法,系统会给你转到其他方法的机会,只要实现了resolveInstanceMethod方法即可。
    其次,如果没有实现,你还可以修改接受消息的对象,让其他对象去响应消息,覆盖方法forwardingTargetSelector即可。
    如果这两者都没做,你还可以在forwardInvocation,做自己的逻辑处理,是否继续处理消息。
    这篇文章不具体讨论这个转发流程的细节,只是为了说明runtime在整个过程中的运用。
    Runtime在其中的运用。
    在resolveInstanceMethod方法中,可以通过覆盖resolveInstanceMethod方法,如下:
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSString *selectorString = NSStringFromSelector(sel);
    if ([selectorString hasPrefix:@"set"]) {
        class_addMethod(self, sel, (IMP)mySetMethod, "v@:@");
    } else {
        class_addMethod(self, sel, (IMP)myGetMethod, "@@:");
    }
    return YES;
}

代码中,本类实现了动态的添加选择子,把选择子关联到自己定义的方法上,这样在访问某个属性的时候,会动态的访问我们自己定义的方法。在消息转发的流程中,实现了动态添加方法实现的能力。

综上所述,Runtime提供了Objective-C强大的动态性,可谓是方便灵活,运用起来能做很多事情,也欢迎大家补充说明在你们的项目中,Runtime都为你们做了什么。

(完)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值